blob: 3b654fc552a585eecbfc294cb52a3e8df176ac29 [file] [log] [blame]
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +01001#!/usr/bin/env python3
2#
3# Copyright (C) 2020 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
Thiébaud Weksteen3604b752021-06-10 14:22:00 +020016"""Add or update tests to TEST_MAPPING.
17
18This script uses Bazel to find reverse dependencies on a crate and generates a
19TEST_MAPPING file. It accepts the absolute path to a crate as argument. If no
20argument is provided, it assumes the crate is the current directory.
21
22 Usage:
23 $ . build/envsetup.sh
24 $ lunch aosp_arm64-eng
25 $ update_crate_tests.py $ANDROID_BUILD_TOP/external/rust/crates/libc
26
27This script is automatically called by external_updater.
28"""
29
Joel Galenson08352442021-08-20 11:39:48 -070030import argparse
31import glob
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +010032import json
33import os
34import platform
35import subprocess
36import sys
Joel Galenson4a2a3a82021-08-20 12:26:37 -070037from datetime import datetime
Joel Galenson08352442021-08-20 11:39:48 -070038from pathlib import Path
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +010039
Thiébaud Weksteen3604b752021-06-10 14:22:00 +020040# Some tests requires specific options. Consider fixing the upstream crate
41# before updating this dictionary.
42TEST_OPTIONS = {
Joel Galensona0d4c5e2021-04-06 09:36:47 -070043 "ring_device_test_tests_digest_tests": [{"test-timeout": "600000"}],
44 "ring_device_test_src_lib": [{"test-timeout": "100000"}],
45}
Thiébaud Weksteen3604b752021-06-10 14:22:00 +020046
47# Excluded tests. These tests will be ignored by this script.
48TEST_EXCLUDE = [
Jeff Vander Stoep0b0e24f2021-01-24 20:50:26 +010049 "aidl_test_rust_client",
50 "aidl_test_rust_service"
Thiébaud Weksteen3604b752021-06-10 14:22:00 +020051]
52
53# Excluded modules.
54EXCLUDE_PATHS = [
Jeff Vander Stoep0b0e24f2021-01-24 20:50:26 +010055 "//external/adhd",
56 "//external/crosvm",
57 "//external/libchromeos-rs",
58 "//external/vm_tools"
Thiébaud Weksteen3604b752021-06-10 14:22:00 +020059]
Jeff Vander Stoep0b0e24f2021-01-24 20:50:26 +010060
Thiébaud Weksteen5212f8a2021-06-10 08:18:32 +020061
62class UpdaterException(Exception):
Thiébaud Weksteen3604b752021-06-10 14:22:00 +020063 """Exception generated by this script."""
Thiébaud Weksteen5212f8a2021-06-10 08:18:32 +020064
65
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +010066class Env(object):
Thiébaud Weksteen3604b752021-06-10 14:22:00 +020067 """Env captures the execution environment.
68
69 It ensures this script is executed within an AOSP repository.
70
71 Attributes:
72 ANDROID_BUILD_TOP: A string representing the absolute path to the top
73 of the repository.
74 """
Thiébaud Weksteenfc485b22021-06-10 13:30:20 +020075 def __init__(self):
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +010076 try:
77 self.ANDROID_BUILD_TOP = os.environ['ANDROID_BUILD_TOP']
Thiébaud Weksteen5212f8a2021-06-10 08:18:32 +020078 except KeyError:
79 raise UpdaterException('$ANDROID_BUILD_TOP is not defined; you '
80 'must first source build/envsetup.sh and '
81 'select a target.')
Thiébaud Weksteen5212f8a2021-06-10 08:18:32 +020082
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +010083
84class Bazel(object):
Thiébaud Weksteen3604b752021-06-10 14:22:00 +020085 """Bazel wrapper.
86
87 The wrapper is used to call bazel queryview and generate the list of
88 reverse dependencies.
89
90 Attributes:
91 path: The path to the bazel executable.
92 """
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +010093 def __init__(self, env):
Thiébaud Weksteen3604b752021-06-10 14:22:00 +020094 """Constructor.
95
96 Note that the current directory is changed to ANDROID_BUILD_TOP.
97
98 Args:
99 env: An instance of Env.
100
101 Raises:
102 UpdaterException: an error occurred while calling soong_ui.
103 """
Thiébaud Weksteen3e32afc2021-06-10 07:56:06 +0200104 if platform.system() != 'Linux':
Thiébaud Weksteen5212f8a2021-06-10 08:18:32 +0200105 raise UpdaterException('This script has only been tested on Linux.')
Thiébaud Weksteen3e32afc2021-06-10 07:56:06 +0200106 self.path = os.path.join(env.ANDROID_BUILD_TOP, "tools", "bazel")
Thiébaud Weksteendf132d62021-06-10 08:45:37 +0200107 soong_ui = os.path.join(env.ANDROID_BUILD_TOP, "build", "soong", "soong_ui.bash")
Thiébaud Weksteenfc485b22021-06-10 13:30:20 +0200108
109 # soong_ui requires to be at the root of the repository.
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100110 os.chdir(env.ANDROID_BUILD_TOP)
Thiébaud Weksteendf132d62021-06-10 08:45:37 +0200111 print("Generating Bazel files...")
112 cmd = [soong_ui, "--make-mode", "GENERATE_BAZEL_FILES=1", "nothing"]
Jeff Vander Stoep1b24dc32021-02-03 18:52:42 +0100113 try:
Thiébaud Weksteendf132d62021-06-10 08:45:37 +0200114 subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True)
115 except subprocess.CalledProcessError as e:
116 raise UpdaterException('Unable to generate bazel workspace: ' + e.output)
117
118 print("Building Bazel Queryview. This can take a couple of minutes...")
119 cmd = [soong_ui, "--build-mode", "--all-modules", "--dir=.", "queryview"]
120 try:
121 subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True)
Jeff Vander Stoep1b24dc32021-02-03 18:52:42 +0100122 except subprocess.CalledProcessError as e:
Thiébaud Weksteen5212f8a2021-06-10 08:18:32 +0200123 raise UpdaterException('Unable to update TEST_MAPPING: ' + e.output)
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100124
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100125 def query_modules(self, path):
Thiébaud Weksteen3604b752021-06-10 14:22:00 +0200126 """Returns all modules for a given path."""
Thiébaud Weksteen3e32afc2021-06-10 07:56:06 +0200127 cmd = self.path + " query --config=queryview /" + path + ":all"
Thiébaud Weksteen76c4e232021-06-10 07:35:19 +0200128 out = subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL, text=True).strip().split("\n")
129 modules = set()
130 for line in out:
131 # speed up by excluding unused modules.
132 if "windows_x86" in line:
133 continue
134 modules.add(line)
135 return modules
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100136
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100137 def query_rdeps(self, module):
Thiébaud Weksteen3604b752021-06-10 14:22:00 +0200138 """Returns all reverse dependencies for a single module."""
Thiébaud Weksteen3e32afc2021-06-10 07:56:06 +0200139 cmd = (self.path + " query --config=queryview \'rdeps(//..., " +
Thiébaud Weksteen76c4e232021-06-10 07:35:19 +0200140 module + ")\' --output=label_kind")
141 out = (subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL, text=True)
142 .strip().split("\n"))
143 if '' in out:
144 out.remove('')
145 return out
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100146
Jeff Vander Stoep0b0e24f2021-01-24 20:50:26 +0100147 def exclude_module(self, module):
Thiébaud Weksteen3604b752021-06-10 14:22:00 +0200148 for path in EXCLUDE_PATHS:
Jeff Vander Stoep0b0e24f2021-01-24 20:50:26 +0100149 if module.startswith(path):
150 return True
151 return False
152
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100153 def query_rdep_tests(self, modules):
Thiébaud Weksteen3604b752021-06-10 14:22:00 +0200154 """Returns all reverse dependency tests for modules in this package."""
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100155 rdep_tests = set()
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100156 for module in modules:
157 for rdep in self.query_rdeps(module):
Thiébaud Weksteen2e532bb2021-06-10 09:01:34 +0200158 rule_type, _, mod = rdep.split(" ")
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100159 if rule_type == "rust_test_" or rule_type == "rust_test":
Jeff Vander Stoep0b0e24f2021-01-24 20:50:26 +0100160 if self.exclude_module(mod) == False:
161 rdep_tests.add(mod.split(":")[1].split("--")[0])
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100162 return rdep_tests
163
164
Thiébaud Weksteen2e532bb2021-06-10 09:01:34 +0200165class Package(object):
Thiébaud Weksteen3604b752021-06-10 14:22:00 +0200166 """A Bazel package.
167
168 Attributes:
169 dir: The absolute path to this package.
170 dir_rel: The relative path to this package.
171 rdep_tests: The list of computed reverse dependencies.
172 """
Thiébaud Weksteenfc485b22021-06-10 13:30:20 +0200173 def __init__(self, path, env, bazel):
Thiébaud Weksteen3604b752021-06-10 14:22:00 +0200174 """Constructor.
175
176 Note that the current directory is changed to the package location when
177 called.
178
179 Args:
180 path: Path to the package.
181 env: An instance of Env.
182 bazel: An instance of Bazel.
183
184 Raises:
185 UpdaterException: the package does not appear to belong to the
186 current repository.
187 """
Joel Galenson17791042021-06-17 14:59:15 -0700188 self.dir = path
Thiébaud Weksteenfc485b22021-06-10 13:30:20 +0200189 try:
190 self.dir_rel = self.dir.split(env.ANDROID_BUILD_TOP)[1]
191 except IndexError:
192 raise UpdaterException('The path ' + self.dir + ' is not under ' +
193 env.ANDROID_BUILD_TOP + '; You must be in the '
194 'directory of a crate or pass its absolute path '
Joel Galenson17791042021-06-17 14:59:15 -0700195 'as the argument.')
Thiébaud Weksteenfc485b22021-06-10 13:30:20 +0200196
197 # Move to the package_directory.
198 os.chdir(self.dir)
199 modules = bazel.query_modules(self.dir_rel)
Thiébaud Weksteen2e532bb2021-06-10 09:01:34 +0200200 self.rdep_tests = bazel.query_rdep_tests(modules)
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100201
202 def get_rdep_tests(self):
203 return self.rdep_tests
204
205
206class TestMapping(object):
Thiébaud Weksteen3604b752021-06-10 14:22:00 +0200207 """A TEST_MAPPING file.
208
209 Attributes:
210 package: The package associated with this TEST_MAPPING file.
211 """
Joel Galenson17791042021-06-17 14:59:15 -0700212 def __init__(self, env, bazel, path):
Thiébaud Weksteen3604b752021-06-10 14:22:00 +0200213 """Constructor.
214
215 Args:
Joel Galenson17791042021-06-17 14:59:15 -0700216 env: An instance of Env.
217 bazel: An instance of Bazel.
Thiébaud Weksteen3604b752021-06-10 14:22:00 +0200218 path: The absolute path to the package.
219 """
Thiébaud Weksteenfc485b22021-06-10 13:30:20 +0200220 self.package = Package(path, env, bazel)
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100221
Thiébaud Weksteen2e532bb2021-06-10 09:01:34 +0200222 def create(self):
Thiébaud Weksteen3604b752021-06-10 14:22:00 +0200223 """Generates the TEST_MAPPING file."""
Thiébaud Weksteenfc485b22021-06-10 13:30:20 +0200224 tests = self.package.get_rdep_tests()
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100225 if not bool(tests):
226 return
227 test_mapping = self.tests_to_mapping(tests)
228 self.write_test_mapping(test_mapping)
229
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100230 def tests_to_mapping(self, tests):
Thiébaud Weksteen3604b752021-06-10 14:22:00 +0200231 """Translate the test list into a dictionary."""
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100232 test_mapping = {"presubmit": []}
233 for test in tests:
Thiébaud Weksteen3604b752021-06-10 14:22:00 +0200234 if test in TEST_EXCLUDE:
Jeff Vander Stoep0b0e24f2021-01-24 20:50:26 +0100235 continue
Thiébaud Weksteen3604b752021-06-10 14:22:00 +0200236 if test in TEST_OPTIONS:
237 test_mapping["presubmit"].append({"name": test, "options": TEST_OPTIONS[test]})
Jeff Vander Stoep0b0e24f2021-01-24 20:50:26 +0100238 else:
239 test_mapping["presubmit"].append({"name": test})
Joel Galenson4f9d11f2021-04-06 10:18:21 -0700240 test_mapping["presubmit"] = sorted(test_mapping["presubmit"], key=lambda t: t["name"])
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100241 return test_mapping
242
243 def write_test_mapping(self, test_mapping):
Thiébaud Weksteen3604b752021-06-10 14:22:00 +0200244 """Writes the TEST_MAPPING file."""
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100245 with open("TEST_MAPPING", "w") as json_file:
Jeff Vander Stoep1b24dc32021-02-03 18:52:42 +0100246 json_file.write("// Generated by update_crate_tests.py for tests that depend on this crate.\n")
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100247 json.dump(test_mapping, json_file, indent=2, separators=(',', ': '), sort_keys=True)
248 json_file.write("\n")
Joel Galenson17791042021-06-17 14:59:15 -0700249 print("TEST_MAPPING successfully updated for %s!" % self.package.dir_rel)
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100250
Thiébaud Weksteenfc485b22021-06-10 13:30:20 +0200251
Joel Galenson08352442021-08-20 11:39:48 -0700252def parse_args():
253 parser = argparse.ArgumentParser('update_crate_tests')
Joel Galenson4a2a3a82021-08-20 12:26:37 -0700254 parser.add_argument('paths',
255 nargs='*',
256 help='Absolute or relative paths of the projects as globs.')
257 parser.add_argument('--branch_and_commit',
258 action='store_true',
259 help='Starts a new branch and commit changes.')
260 parser.add_argument('--push_change',
261 action='store_true',
262 help='Pushes change to Gerrit.')
Joel Galenson08352442021-08-20 11:39:48 -0700263 return parser.parse_args()
264
265
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100266def main():
Joel Galenson08352442021-08-20 11:39:48 -0700267 args = parse_args()
268 paths = args.paths if len(args.paths) > 0 else [os.getcwd()]
269 # We want to use glob to get all the paths, so we first convert to absolute.
270 paths = [Path(path).resolve() for path in paths]
271 paths = sorted([path for abs_path in paths
272 for path in glob.glob(str(abs_path))])
273
Joel Galenson17791042021-06-17 14:59:15 -0700274 env = Env()
275 bazel = Bazel(env)
276 for path in paths:
277 try:
278 test_mapping = TestMapping(env, bazel, path)
Joel Galenson4a2a3a82021-08-20 12:26:37 -0700279 test_mapping.create()
280 changed = (subprocess.call(['git', 'diff', '--quiet']) == 1)
Joel Galensonbdf3ab42021-08-30 08:57:18 -0700281 untracked = (os.path.isfile('TEST_MAPPING') and
282 (subprocess.run(['git', 'ls-files', '--error-unmatch', 'TEST_MAPPING'],
283 stderr=subprocess.DEVNULL,
284 stdout=subprocess.DEVNULL).returncode == 1))
285 if args.branch_and_commit and (changed or untracked):
Joel Galenson4a2a3a82021-08-20 12:26:37 -0700286 subprocess.check_output(['repo', 'start',
287 'tmp_auto_test_mapping', '.'])
288 subprocess.check_output(['git', 'add', 'TEST_MAPPING'])
289 subprocess.check_output(['git', 'commit', '-m',
290 'Update TEST_MAPPING\n\nTest: None'])
Joel Galensonbdf3ab42021-08-30 08:57:18 -0700291 if args.push_change and (changed or untracked):
Joel Galenson4a2a3a82021-08-20 12:26:37 -0700292 date = datetime.today().strftime('%m-%d')
293 subprocess.check_output(['git', 'push', 'aosp', 'HEAD:refs/for/master',
294 '-o', 'topic=test-mapping-%s' % date])
295 except (UpdaterException, subprocess.CalledProcessError) as err:
Joel Galenson17791042021-06-17 14:59:15 -0700296 sys.exit("Error: " + str(err))
Jeff Vander Stoepcec8bac2021-01-15 14:15:37 +0100297
298if __name__ == '__main__':
299 main()