2 # Copyright 2020 The Chromium Authors
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5 """Builds and runs a test by filename.
7 This script finds the appropriate test suites for the specified test files or
8 directories, builds it, then runs it with the (optionally) specified filter,
9 passing any extra args on to the test runner.
12 # Run the test target for bit_cast_unittest.cc. Use a custom test filter instead
13 # of the automatically generated one.
14 autotest.py -C out/Desktop bit_cast_unittest.cc --gtest_filter=BitCastTest*
16 # Find and run UrlUtilitiesUnitTest.java's tests, pass remaining parameters to
18 autotest.py -C out/Android UrlUtilitiesUnitTest --fast-local-dev -v
20 # Run all tests under base/strings.
21 autotest.py -C out/foo --run-all base/strings
23 # Run tests in multiple files or directories.
24 autotest.py -C out/foo base/strings base/pickle_unittest.cc
26 # Run only the test on line 11. Useful when running autotest.py from your text
28 autotest.py -C out/foo --line 11 base/strings/strcat_unittest.cc
41 from pathlib import Path
43 USE_PYTHON_3 = f'This script will only run under python3.'
45 SRC_DIR = Path(__file__).parent.parent.resolve()
46 sys.path.append(str(SRC_DIR / 'build'))
49 sys.path.append(str(SRC_DIR / 'build' / 'android'))
50 from pylib import constants
52 DEPOT_TOOLS_DIR = SRC_DIR / 'third_party' / 'depot_tools'
55 # Some test suites use suffixes that would also match non-test-suite targets.
56 # Those test suites should be manually added here.
57 _TEST_TARGET_ALLOWLIST = [
58 # Running ash_pixeltests requires the --no-try-android-wrappers flag.
59 '//ash:ash_pixeltests',
60 '//chrome/test:browser_tests',
61 '//chrome/test:interactive_ui_tests',
62 '//chrome/test:unit_tests',
65 _TEST_TARGET_REGEX = re.compile(r'(_browsertests|_perftests|_wpr_tests)$')
67 TEST_FILE_NAME_REGEX = re.compile(r'(.*Test\.java)|(.*_[a-z]*test\.cc)')
69 # Some tests don't directly include gtest.h and instead include it via gmock.h
70 # or a test_utils.h file, so make sure these cases are captured. Also include
71 # files that use <...> for #includes instead of quotes.
72 GTEST_INCLUDE_REGEX = re.compile(
73 r'#include.*(gtest|gmock|_test_utils|browser_test)\.h("|>)')
76 def ExitWithMessage(*args):
77 print(*args, file=sys.stderr)
81 class TestValidity(Enum):
82 NOT_A_TEST = 0 # Does not match test file regex.
83 MAYBE_A_TEST = 1 # Matches test file regex, but doesn't include gtest files.
84 VALID_TEST = 2 # Matches test file regex and includes gtest files.
87 def IsTestFile(file_path):
88 if not TEST_FILE_NAME_REGEX.match(file_path):
89 return TestValidity.NOT_A_TEST
90 if file_path.endswith('.cc'):
91 # Try a bit harder to remove non-test files for c++. Without this,
92 # 'autotest.py base/' finds non-test files.
94 with open(file_path, 'r', encoding='utf-8') as f:
95 if GTEST_INCLUDE_REGEX.search(f.read()) is not None:
96 return TestValidity.VALID_TEST
99 # It may still be a test file, even if it doesn't include a gtest file.
100 return TestValidity.MAYBE_A_TEST
101 return TestValidity.VALID_TEST
104 class CommandError(Exception):
105 """Exception thrown when a subcommand fails."""
107 def __init__(self, command, return_code, output=None):
108 Exception.__init__(self)
109 self.command = command
110 self.return_code = return_code
114 message = (f'\n***\nERROR: Error while running command {self.command}'
115 f'.\nExit status: {self.return_code}\n')
117 message += f'Output:\n{self.output}\n'
122 def StreamCommandOrExit(cmd, **kwargs):
124 subprocess.check_call(cmd, **kwargs)
125 except subprocess.CalledProcessError as e:
129 def RunCommand(cmd, **kwargs):
131 # Set an encoding to convert the binary output to a string.
132 return subprocess.check_output(
133 cmd, **kwargs, encoding=locale.getpreferredencoding())
134 except subprocess.CalledProcessError as e:
135 raise CommandError(e.cmd, e.returncode, e.output) from None
138 def BuildTestTargets(out_dir, targets, dry_run):
139 """Builds the specified targets with ninja"""
140 cmd = gn_helpers.CreateBuildCommand(out_dir) + targets
141 print('Building: ' + shlex.join(cmd))
145 subprocess.check_call(cmd)
146 except subprocess.CalledProcessError as e:
151 def RecursiveMatchFilename(folder, filename):
152 current_dir = os.path.split(folder)[-1]
153 if current_dir.startswith('out') or current_dir.startswith('.'):
157 with os.scandir(folder) as it:
159 if (entry.is_symlink()):
161 if (entry.is_file() and filename in entry.path and
162 not os.path.basename(entry.path).startswith('.')):
163 file_validity = IsTestFile(entry.path)
164 if file_validity is TestValidity.VALID_TEST:
165 exact.append(entry.path)
166 elif file_validity is TestValidity.MAYBE_A_TEST:
167 close.append(entry.path)
169 # On Windows, junctions are like a symlink that python interprets as a
170 # directory, leading to exceptions being thrown. We can just catch and
171 # ignore these exceptions like we would ignore symlinks.
173 matches = RecursiveMatchFilename(entry.path, filename)
176 except FileNotFoundError as e:
178 print(f'Failed to scan directory "{entry}" - junction?')
180 return [exact, close]
183 def FindTestFilesInDirectory(directory):
187 for root, _, files in os.walk(directory):
189 path = os.path.join(root, f)
190 file_validity = IsTestFile(path)
191 if file_validity is TestValidity.VALID_TEST:
194 test_files.append(path)
195 elif DEBUG and file_validity is TestValidity.MAYBE_A_TEST:
196 print(path + ' matched but doesn\'t include gtest files, skipping.')
200 def FindMatchingTestFiles(target):
201 # Return early if there's an exact file match.
202 if os.path.isfile(target):
203 # If the target is a C++ implementation file, try to guess the test file.
204 if target.endswith('.cc') or target.endswith('.h'):
205 target_validity = IsTestFile(target)
206 if target_validity is TestValidity.VALID_TEST:
208 alternate = f"{target.rsplit('.', 1)[0]}_unittest.cc"
209 alt_validity = TestValidity.NOT_A_TEST if not os.path.isfile(
210 alternate) else IsTestFile(alternate)
211 if alt_validity is TestValidity.VALID_TEST:
214 # If neither the target nor its alternative were valid, check if they just
215 # didn't include the gtest files before deciding to exit.
216 if target_validity is TestValidity.MAYBE_A_TEST:
218 if alt_validity is TestValidity.MAYBE_A_TEST:
220 ExitWithMessage(f"{target} doesn't look like a test file")
222 # If this is a directory, return all the test files it contains.
223 if os.path.isdir(target):
224 files = FindTestFilesInDirectory(target)
226 ExitWithMessage('No tests found in directory')
229 if sys.platform.startswith('win32') and os.path.altsep in target:
230 # Use backslash as the path separator on Windows to match os.scandir().
232 print('Replacing ' + os.path.altsep + ' with ' + os.path.sep + ' in: '
234 target = target.replace(os.path.altsep, os.path.sep)
236 print('Finding files with full path containing: ' + target)
238 [exact, close] = RecursiveMatchFilename(SRC_DIR, target)
241 print('Found exact matching file(s):')
242 print('\n'.join(exact))
244 print('Found possible matching file(s):')
245 print('\n'.join(close))
248 # Given "Foo", don't ask to disambiguate ModFoo.java vs Foo.java.
250 p for p in exact if os.path.basename(p) in (target, f'{target}.java')
252 if len(more_exact) == 1:
253 test_files = more_exact
259 if len(test_files) > 1:
260 if len(test_files) < 10:
261 test_files = [HaveUserPickFile(test_files)]
263 # Arbitrarily capping at 10 results so we don't print the name of every
264 # file in the repo if the target is poorly specified.
265 test_files = test_files[:10]
266 ExitWithMessage(f'Target "{target}" is ambiguous. Matching files: '
269 ExitWithMessage(f'Target "{target}" did not match any files.')
273 def HaveUserPickFile(paths):
274 paths = sorted(paths, key=lambda p: (len(p), p))
275 path_list = '\n'.join(f'{i}. {t}' for i, t in enumerate(paths))
278 user_input = input(f'Please choose the path you mean.\n{path_list}\n')
280 value = int(user_input)
282 except (ValueError, IndexError):
286 def HaveUserPickTarget(paths, targets):
287 # Cap to 10 targets for convenience [0-9].
288 targets = targets[:10]
289 target_list = '\n'.join(f'{i}. {t}' for i, t in enumerate(targets))
291 user_input = input(f'Target "{paths}" is used by multiple test targets.\n' +
292 target_list + '\nPlease pick a target: ')
294 value = int(user_input)
295 return targets[value]
296 except (ValueError, IndexError):
298 return HaveUserPickTarget(paths, targets)
301 # A persistent cache to avoid running gn on repeated runs of autotest.
303 def __init__(self, out_dir):
304 self.out_dir = out_dir
305 self.path = os.path.join(out_dir, 'autotest_cache')
306 self.gold_mtime = self.GetBuildNinjaMtime()
309 mtime, cache = json.load(open(self.path, 'r'))
310 if mtime == self.gold_mtime:
316 with open(self.path, 'w') as f:
317 json.dump([self.gold_mtime, self.cache], f)
319 def Find(self, test_paths):
320 key = ' '.join(test_paths)
321 return self.cache.get(key, None)
323 def Store(self, test_paths, test_targets):
324 key = ' '.join(test_paths)
325 self.cache[key] = test_targets
327 def GetBuildNinjaMtime(self):
328 return os.path.getmtime(os.path.join(self.out_dir, 'build.ninja'))
330 def IsStillValid(self):
331 return self.GetBuildNinjaMtime() == self.gold_mtime
334 def _TestTargetsFromGnRefs(targets):
335 # First apply allowlists:
336 ret = [t for t in targets if '__' not in t]
339 if _TEST_TARGET_REGEX.search(t) or t in _TEST_TARGET_ALLOWLIST
344 _SUBTARGET_SUFFIXES = (
345 '__java_binary', # robolectric_binary()
346 '__test_runner_script', # test() targets
347 '__test_apk', # instrumentation_test_apk() targets
350 for suffix in _SUBTARGET_SUFFIXES:
351 ret.extend(t[:-len(suffix)] for t in targets if t.endswith(suffix))
356 # TODO(b/305968611) remove when goma is deprecated.
357 def _IsGomaNotice(target):
358 return target.startswith('The gn arg use_goma=true will be deprecated')
361 def FindTestTargets(target_cache, out_dir, paths, run_all):
362 # Normalize paths, so they can be cached.
363 paths = [os.path.realpath(p) for p in paths]
364 test_targets = target_cache.Find(paths)
369 # Use gn refs to recursively find all targets that depend on |path|, filter
370 # internal gn targets, and match against well-known test suffixes, falling
371 # back to a list of known test targets if that fails.
372 gn_path = os.path.join(DEPOT_TOOLS_DIR, 'gn')
373 if sys.platform.startswith('win32'):
376 cmd = [gn_path, 'refs', out_dir, '--all'] + paths
377 targets = RunCommand(cmd).splitlines()
378 test_targets = _TestTargetsFromGnRefs(targets)
380 # If not targets were identified as tests by looking at their names, ask GN
381 # if any are executables.
382 if not test_targets and targets:
383 test_targets = RunCommand(cmd + ['--type=executable']).splitlines()
387 f'"{paths}" did not match any test targets. Consider adding'
388 f' one of the following targets to _TEST_TARGET_ALLOWLIST within '
389 f'{__file__}: \n' + '\n'.join(targets))
391 test_targets = [t for t in test_targets if not _IsGomaNotice(t)]
393 target_cache.Store(paths, test_targets)
396 if len(test_targets) > 1:
398 print(f'Warning, found {len(test_targets)} test targets.',
400 if len(test_targets) > 10:
401 ExitWithMessage('Your query likely involves non-test sources.')
402 print('Trying to run all of them!', file=sys.stderr)
404 test_targets = [HaveUserPickTarget(paths, test_targets)]
406 # Remove the // prefix to turn GN label into ninja target.
407 test_targets = [t[2:] for t in test_targets]
409 return (test_targets, used_cache)
412 def RunTestTargets(out_dir, targets, gtest_filter, extra_args, dry_run,
413 no_try_android_wrappers, no_fast_local_dev):
415 for target in targets:
416 target_binary = target.split(':')[1]
418 # Look for the Android wrapper script first.
419 path = os.path.join(out_dir, 'bin', f'run_{target_binary}')
420 if no_try_android_wrappers or not os.path.isfile(path):
421 # If the wrapper is not found or disabled use the Desktop target
422 # which is an executable.
423 path = os.path.join(out_dir, target_binary)
424 elif not no_fast_local_dev:
425 # Usually want this flag when developing locally.
426 extra_args = extra_args + ['--fast-local-dev']
428 cmd = [path, f'--gtest_filter={gtest_filter}'] + extra_args
429 print('Running test: ' + shlex.join(cmd))
431 StreamCommandOrExit(cmd)
434 def BuildCppTestFilter(filenames, line):
435 make_filter_command = [
436 sys.executable, SRC_DIR / 'tools' / 'make_gtest_filter.py'
439 make_filter_command += ['--line', str(line)]
441 make_filter_command += ['--class-only']
442 make_filter_command += filenames
443 return RunCommand(make_filter_command).strip()
446 def BuildJavaTestFilter(filenames):
447 return ':'.join('*.{}*'.format(os.path.splitext(os.path.basename(f))[0])
451 def BuildTestFilter(filenames, line):
452 java_files = [f for f in filenames if f.endswith('.java')]
453 cc_files = [f for f in filenames if f.endswith('.cc')]
456 filters.append(BuildJavaTestFilter(java_files))
458 filters.append(BuildCppTestFilter(cc_files, line))
460 return ':'.join(filters)
464 parser = argparse.ArgumentParser(
465 description=__doc__, formatter_class=argparse.RawTextHelpFormatter)
466 parser.add_argument('--out-dir',
467 '--output-directory',
470 help='output directory of the build')
474 help='Run all tests for the file or directory, instead of just one')
475 parser.add_argument('--line',
477 help='run only the test on this line number. c++ only.')
478 parser.add_argument('--gtest_filter',
487 help='Print ninja and test run commands without executing them.')
489 '--no-try-android-wrappers',
491 help='Do not try to use Android test wrappers to run tests.')
492 parser.add_argument('--no-fast-local-dev',
494 help='Do not add --fast-local-dev for Android tests.')
495 parser.add_argument('files',
498 help='test suite file (eg. FooTest.java)')
500 args, _extras = parser.parse_known_args()
503 constants.SetOutputDirectory(args.out_dir)
504 constants.CheckOutputDirectory()
505 out_dir: str = constants.GetOutDirectory()
507 if not os.path.isdir(out_dir):
508 parser.error(f'OUT_DIR "{out_dir}" does not exist.')
509 target_cache = TargetCache(out_dir)
511 for file in args.files:
512 filenames.extend(FindMatchingTestFiles(file))
514 targets, used_cache = FindTestTargets(target_cache, out_dir, filenames,
517 gtest_filter = args.gtest_filter
519 gtest_filter = BuildTestFilter(filenames, args.line)
522 ExitWithMessage('Failed to derive a gtest filter')
525 build_ok = BuildTestTargets(out_dir, targets, args.dry_run)
527 # If we used the target cache, it's possible we chose the wrong target because
528 # a gn file was changed. The build step above will check for gn modifications
529 # and update build.ninja. Use this opportunity the verify the cache is still
531 if used_cache and not target_cache.IsStillValid():
532 target_cache = TargetCache(out_dir)
533 new_targets, _ = FindTestTargets(target_cache, out_dir, filenames,
535 if targets != new_targets:
536 # Note that this can happen, for example, if you rename a test target.
537 print('gn config was changed, trying to build again', file=sys.stderr)
538 targets = new_targets
539 build_ok = BuildTestTargets(out_dir, targets, args.dry_run)
541 if not build_ok: sys.exit(1)
543 RunTestTargets(out_dir, targets, gtest_filter, _extras, args.dry_run,
544 args.no_try_android_wrappers, args.no_fast_local_dev)
547 if __name__ == '__main__':