blob: 0f84ed03a505979181023d9fc744268d0e004055 [file] [log] [blame]
Michael Thiessen09c0e1d2020-03-23 18:44:501#!/usr/bin/env python3
2# Copyright 2020 The Chromium Authors. All rights reserved.
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.
6
Dan Harrington27d104d2020-09-08 18:30:147This script finds the appropriate test suites for the specified test file or
8directory, builds it, then runs it with the (optionally) specified filter,
9passing any extra args on to the test runner.
Michael Thiessen09c0e1d2020-03-23 18:44:5010
11Examples:
Dan Harrington27d104d2020-09-08 18:30:1412# Run the test target for bit_cast_unittest.cc. Use a custom test filter instead
13# of the automatically generated one.
14autotest.py -C out/Desktop bit_cast_unittest.cc --gtest_filter=BitCastTest*
15
16# Find and run UrlUtilitiesUnitTest.java's tests, pass remaining parameters to
17# the test binary.
Michael Thiessen09c0e1d2020-03-23 18:44:5018autotest.py -C out/Android UrlUtilitiesUnitTest --fast-local-dev -v
Dan Harrington27d104d2020-09-08 18:30:1419
20# Run all tests under base/strings
21autotest.py -C out/foo --run-all base/strings
22
23# Run only the test on line 11. Useful when running autotest.py from your text
24# editor.
25autotest.py -C out/foo --line 11 base/strings/strcat_unittest.cc
Michael Thiessen09c0e1d2020-03-23 18:44:5026"""
27
28import argparse
29import locale
Michael Thiessen09c0e1d2020-03-23 18:44:5030import os
Dan Harrington27d104d2020-09-08 18:30:1431import json
Michael Thiessen09c0e1d2020-03-23 18:44:5032import re
Michael Thiessen09c0e1d2020-03-23 18:44:5033import subprocess
34import sys
35
36from pathlib import Path
37
38USE_PYTHON_3 = f'This script will only run under python3.'
39
40SRC_DIR = Path(__file__).parent.parent.resolve()
41DEPOT_TOOLS_DIR = SRC_DIR.joinpath('third_party', 'depot_tools')
42DEBUG = False
43
44_TEST_TARGET_SUFFIXES = [
45 '_browsertests',
46 '_junit_tests',
47 '_perftests',
48 '_test_apk',
49 '_unittests',
50]
51
Michael Thiessenf46171e2020-03-31 17:29:3852# Some test suites use suffixes that would also match non-test-suite targets.
53# Those test suites should be manually added here.
Michael Thiessen09c0e1d2020-03-23 18:44:5054_OTHER_TEST_TARGETS = [
55 '//chrome/test:browser_tests',
Michael Thiessenf46171e2020-03-31 17:29:3856 '//chrome/test:unit_tests',
Michael Thiessen09c0e1d2020-03-23 18:44:5057]
58
Dan Harrington27d104d2020-09-08 18:30:1459TEST_FILE_NAME_REGEX = re.compile(r'(.*Test\.java)|(.*_[a-z]*test\.cc)')
60GTEST_INCLUDE_REGEX = re.compile(r'#include.*gtest\.h"')
61
62
63def ExitWithMessage(*args):
64 print(*args, file=sys.stderr)
65 sys.exit(1)
66
67
68def IsTestFile(file_path):
69 if not TEST_FILE_NAME_REGEX.match(file_path):
70 return False
71 if file_path.endswith('.cc'):
72 # Try a bit harder to remove non-test files for c++. Without this,
73 # 'autotest.py base/' finds non-test files.
74 try:
75 with open(file_path, 'r') as f:
76 if GTEST_INCLUDE_REGEX.search(f.read()) is not None:
77 return True
78 except IOError:
79 pass
80 return False
81 return True
82
Michael Thiessen09c0e1d2020-03-23 18:44:5083
84class CommandError(Exception):
Dan Harrington27d104d2020-09-08 18:30:1485 """Exception thrown when a subcommand fails."""
Michael Thiessen09c0e1d2020-03-23 18:44:5086
87 def __init__(self, command, return_code, output=None):
88 Exception.__init__(self)
89 self.command = command
90 self.return_code = return_code
91 self.output = output
92
93 def __str__(self):
94 message = (f'\n***\nERROR: Error while running command {self.command}'
95 f'.\nExit status: {self.return_code}\n')
96 if self.output:
97 message += f'Output:\n{self.output}\n'
98 message += '***'
99 return message
100
101
Dan Harrington27d104d2020-09-08 18:30:14102def StreamCommandOrExit(cmd, **kwargs):
Michael Thiessen09c0e1d2020-03-23 18:44:50103 try:
104 subprocess.check_call(cmd, **kwargs)
105 except subprocess.CalledProcessError as e:
Dan Harrington27d104d2020-09-08 18:30:14106 sys.exit(1)
Michael Thiessen09c0e1d2020-03-23 18:44:50107
108
109def RunCommand(cmd, **kwargs):
Michael Thiessen09c0e1d2020-03-23 18:44:50110 try:
111 # Set an encoding to convert the binary output to a string.
112 return subprocess.check_output(
113 cmd, **kwargs, encoding=locale.getpreferredencoding())
114 except subprocess.CalledProcessError as e:
115 raise CommandError(e.cmd, e.returncode, e.output) from None
116
117
Dan Harrington27d104d2020-09-08 18:30:14118def BuildTestTargetsWithNinja(out_dir, targets, dry_run):
119 """Builds the specified targets with ninja"""
Andrew Grievea5193d3a2020-09-21 14:58:34120 # Use autoninja from PATH to match version used for manual builds.
121 ninja_path = 'autoninja'
K. Moon01548662020-04-03 00:14:25122 if sys.platform.startswith('win32'):
Michael Thiessen09c0e1d2020-03-23 18:44:50123 ninja_path += '.bat'
Dan Harrington27d104d2020-09-08 18:30:14124 cmd = [ninja_path, '-C', out_dir] + targets
Michael Thiessen09c0e1d2020-03-23 18:44:50125 print('Building: ' + ' '.join(cmd))
126 if (dry_run):
127 return
Dan Harringtonaa2c7ba2020-09-16 15:34:24128 try:
129 subprocess.check_call(cmd)
130 except subprocess.CalledProcessError as e:
131 return False
132 return True
Michael Thiessen09c0e1d2020-03-23 18:44:50133
134
135def RecursiveMatchFilename(folder, filename):
136 current_dir = os.path.split(folder)[-1]
137 if current_dir.startswith('out') or current_dir.startswith('.'):
138 return []
139 matches = []
140 with os.scandir(folder) as it:
141 for entry in it:
142 if (entry.is_symlink()):
143 continue
Michael Thiessen0264afc62020-04-03 20:31:34144 if (entry.is_file() and filename in entry.path and
145 not os.path.basename(entry.path).startswith('.')):
Dan Harringtonaa2c7ba2020-09-16 15:34:24146 if IsTestFile(entry.path):
147 matches.append(entry.path)
Michael Thiessen09c0e1d2020-03-23 18:44:50148 if entry.is_dir():
149 # On Windows, junctions are like a symlink that python interprets as a
150 # directory, leading to exceptions being thrown. We can just catch and
151 # ignore these exceptions like we would ignore symlinks.
152 try:
153 matches += RecursiveMatchFilename(entry.path, filename)
154 except FileNotFoundError as e:
155 if DEBUG:
156 print(f'Failed to scan directory "{entry}" - junction?')
157 pass
158 return matches
159
160
Dan Harrington27d104d2020-09-08 18:30:14161def FindTestFilesInDirectory(directory):
162 test_files = []
163 for root, dirs, files in os.walk(directory):
164 for f in files:
165 path = os.path.join(root, f)
166 if IsTestFile(path):
167 test_files.append(path)
168 return test_files
169
170
171def FindMatchingTestFiles(target):
172 # Return early if there's an exact file match.
173 if os.path.isfile(target):
Dan Harringtonaa2c7ba2020-09-16 15:34:24174 # If the target is a C++ implementation file, try to guess the test file.
175 if target.endswith('.cc') or target.endswith('.h'):
176 if IsTestFile(target):
177 return [target]
178 alternate = f"{target.rsplit('.', 1)[0]}_unittest.cc"
179 if os.path.isfile(alternate) and IsTestFile(alternate):
180 return [alternate]
181 ExitWithMessage(f"{target} doesn't look like a test file")
Dan Harrington27d104d2020-09-08 18:30:14182 return [target]
183 # If this is a directory, return all the test files it contains.
184 if os.path.isdir(target):
185 files = FindTestFilesInDirectory(target)
186 if not files:
187 ExitWithMessage('No tests found in directory')
188 return files
189
Jesse McKenna83b6ac1b2020-05-07 18:25:38190 if sys.platform.startswith('win32') and os.path.altsep in target:
191 # Use backslash as the path separator on Windows to match os.scandir().
192 if DEBUG:
193 print('Replacing ' + os.path.altsep + ' with ' + os.path.sep + ' in: '
194 + target)
195 target = target.replace(os.path.altsep, os.path.sep)
Michael Thiessen09c0e1d2020-03-23 18:44:50196 if DEBUG:
197 print('Finding files with full path containing: ' + target)
198 results = RecursiveMatchFilename(SRC_DIR, target)
199 if DEBUG:
200 print('Found matching file(s): ' + ' '.join(results))
201 if len(results) > 1:
202 # Arbitrarily capping at 10 results so we don't print the name of every file
203 # in the repo if the target is poorly specified.
204 results = results[:10]
Dan Harrington27d104d2020-09-08 18:30:14205 ExitWithMessage(f'Target "{target}" is ambiguous. Matching files: '
Michael Thiessen09c0e1d2020-03-23 18:44:50206 f'{results}')
207 if not results:
Dan Harrington27d104d2020-09-08 18:30:14208 ExitWithMessage(f'Target "{target}" did not match any files.')
209 return results
Michael Thiessen09c0e1d2020-03-23 18:44:50210
211
212def IsTestTarget(target):
213 for suffix in _TEST_TARGET_SUFFIXES:
214 if target.endswith(suffix):
215 return True
216 return target in _OTHER_TEST_TARGETS
217
218
Dan Harrington27d104d2020-09-08 18:30:14219def HaveUserPickTarget(paths, targets):
Michael Thiessen09c0e1d2020-03-23 18:44:50220 # Cap to 10 targets for convenience [0-9].
221 targets = targets[:10]
Dan Harrington27d104d2020-09-08 18:30:14222 target_list = '\n'.join(f'{i}. {t}' for i, t in enumerate(targets))
223
224 user_input = input(f'Target "{paths}" is used by multiple test targets.\n' +
225 target_list + '\nPlease pick a target: ')
Michael Thiessen09c0e1d2020-03-23 18:44:50226 try:
Dan Harrington27d104d2020-09-08 18:30:14227 value = int(user_input)
Michael Thiessen09c0e1d2020-03-23 18:44:50228 return targets[value]
Dan Harrington27d104d2020-09-08 18:30:14229 except (ValueError, IndexError):
Michael Thiessen09c0e1d2020-03-23 18:44:50230 print('Try again')
Dan Harrington27d104d2020-09-08 18:30:14231 return HaveUserPickTarget(paths, targets)
Michael Thiessen09c0e1d2020-03-23 18:44:50232
233
Dan Harrington27d104d2020-09-08 18:30:14234# A persistent cache to avoid running gn on repeated runs of autotest.
235class TargetCache:
236 def __init__(self, out_dir):
Dan Harringtonaa2c7ba2020-09-16 15:34:24237 self.out_dir = out_dir
Dan Harrington27d104d2020-09-08 18:30:14238 self.path = os.path.join(out_dir, 'autotest_cache')
Dan Harringtonaa2c7ba2020-09-16 15:34:24239 self.gold_mtime = self.GetBuildNinjaMtime()
Dan Harrington27d104d2020-09-08 18:30:14240 self.cache = {}
241 try:
242 mtime, cache = json.load(open(self.path, 'r'))
243 if mtime == self.gold_mtime:
244 self.cache = cache
245 except Exception:
246 pass
247
248 def Save(self):
249 with open(self.path, 'w') as f:
250 json.dump([self.gold_mtime, self.cache], f)
251
252 def Find(self, test_paths):
253 key = ' '.join(test_paths)
254 return self.cache.get(key, None)
255
256 def Store(self, test_paths, test_targets):
257 key = ' '.join(test_paths)
258 self.cache[key] = test_targets
259
Dan Harringtonaa2c7ba2020-09-16 15:34:24260 def GetBuildNinjaMtime(self):
261 return os.path.getmtime(os.path.join(self.out_dir, 'build.ninja'))
262
263 def IsStillValid(self):
264 return self.GetBuildNinjaMtime() == self.gold_mtime
265
Dan Harrington27d104d2020-09-08 18:30:14266
267def FindTestTargets(target_cache, out_dir, paths, run_all):
268 # Normalize paths, so they can be cached.
269 paths = [os.path.realpath(p) for p in paths]
270 test_targets = target_cache.Find(paths)
Dan Harringtonaa2c7ba2020-09-16 15:34:24271 used_cache = True
Dan Harrington27d104d2020-09-08 18:30:14272 if not test_targets:
Dan Harringtonaa2c7ba2020-09-16 15:34:24273 used_cache = False
Dan Harrington27d104d2020-09-08 18:30:14274
275 # Use gn refs to recursively find all targets that depend on |path|, filter
276 # internal gn targets, and match against well-known test suffixes, falling
277 # back to a list of known test targets if that fails.
278 gn_path = os.path.join(DEPOT_TOOLS_DIR, 'gn')
279 if sys.platform.startswith('win32'):
280 gn_path += '.bat'
281
282 cmd = [gn_path, 'refs', out_dir, '--all'] + paths
283 targets = RunCommand(cmd).splitlines()
284 targets = [t for t in targets if '__' not in t]
285 test_targets = [t for t in targets if IsTestTarget(t)]
Michael Thiessen09c0e1d2020-03-23 18:44:50286
287 if not test_targets:
Dan Harrington27d104d2020-09-08 18:30:14288 ExitWithMessage(
289 f'Target(s) "{paths}" did not match any test targets. Consider adding'
290 f' one of the following targets to the top of this file: {targets}')
291
292 target_cache.Store(paths, test_targets)
293 target_cache.Save()
294
Michael Thiessen09c0e1d2020-03-23 18:44:50295 if len(test_targets) > 1:
Dan Harrington27d104d2020-09-08 18:30:14296 if run_all:
297 print(f'Warning, found {len(test_targets)} test targets.',
298 file=sys.stderr)
299 if len(test_targets) > 10:
300 ExitWithMessage('Your query likely involves non-test sources.')
301 print('Trying to run all of them!', file=sys.stderr)
302 else:
303 test_targets = [HaveUserPickTarget(paths, test_targets)]
Michael Thiessen09c0e1d2020-03-23 18:44:50304
Dan Harrington27d104d2020-09-08 18:30:14305 test_targets = list(set([t.split(':')[-1] for t in test_targets]))
306
Dan Harringtonaa2c7ba2020-09-16 15:34:24307 return (test_targets, used_cache)
Michael Thiessen09c0e1d2020-03-23 18:44:50308
309
Dan Harrington27d104d2020-09-08 18:30:14310def RunTestTargets(out_dir, targets, gtest_filter, extra_args, dry_run):
311 for target in targets:
312 # Look for the Android wrapper script first.
313 path = os.path.join(out_dir, 'bin', f'run_{target}')
314 if not os.path.isfile(path):
315 # Otherwise, use the Desktop target which is an executable.
316 path = os.path.join(out_dir, target)
317 cmd = [path, f'--gtest_filter={gtest_filter}'] + extra_args
318 print('Running test: ' + ' '.join(cmd))
319 if not dry_run:
320 StreamCommandOrExit(cmd)
321
322
323def BuildCppTestFilter(filenames, line):
324 make_filter_command = [os.path.join(SRC_DIR, 'tools', 'make-gtest-filter.py')]
325 if line:
326 make_filter_command += ['--line', str(line)]
327 else:
328 make_filter_command += ['--class-only']
329 make_filter_command += filenames
330 return RunCommand(make_filter_command).strip()
331
332
333def BuildJavaTestFilter(filenames):
Michael Thiessen7bbda482020-09-19 02:07:34334 return ':'.join('*.{}*'.format(os.path.splitext(os.path.basename(f))[0])
Dan Harrington27d104d2020-09-08 18:30:14335 for f in filenames)
336
337
338def BuildTestFilter(filenames, line):
339 java_files = [f for f in filenames if f.endswith('.java')]
340 cc_files = [f for f in filenames if f.endswith('.cc')]
341 filters = []
342 if java_files:
343 filters.append(BuildJavaTestFilter(java_files))
344 if cc_files:
345 filters.append(BuildCppTestFilter(cc_files, line))
346
347 return ':'.join(filters)
Michael Thiessen09c0e1d2020-03-23 18:44:50348
349
350def main():
351 parser = argparse.ArgumentParser(
352 description=__doc__, formatter_class=argparse.RawTextHelpFormatter)
Andrew Grievea5193d3a2020-09-21 14:58:34353 parser.add_argument('--out-dir',
354 '-C',
355 metavar='OUT_DIR',
356 help='output directory of the build')
Michael Thiessen09c0e1d2020-03-23 18:44:50357 parser.add_argument(
Dan Harrington27d104d2020-09-08 18:30:14358 '--run-all',
359 action='store_true',
360 help='Run all tests for the file or directory, instead of just one')
361 parser.add_argument('--line',
362 type=int,
363 help='run only the test on this line number. c++ only.')
364 parser.add_argument(
Michael Thiessen09c0e1d2020-03-23 18:44:50365 '--gtest_filter', '-f', metavar='FILTER', help='test filter')
366 parser.add_argument(
Dan Harrington27d104d2020-09-08 18:30:14367 '--dry-run',
Michael Thiessen09c0e1d2020-03-23 18:44:50368 '-n',
369 action='store_true',
370 help='Print ninja and test run commands without executing them.')
Dan Harringtonaa2c7ba2020-09-16 15:34:24371 parser.add_argument('file',
372 metavar='FILE_NAME',
373 help='test suite file (eg. FooTest.java)')
Michael Thiessen09c0e1d2020-03-23 18:44:50374
375 args, _extras = parser.parse_known_args()
376
Andrew Grievea5193d3a2020-09-21 14:58:34377 # Use CWD as out_dir when build.ninja exists.
378 if not args.out_dir and os.path.exists('build.ninja'):
379 args.out_dir = '.'
380
Michael Thiessen09c0e1d2020-03-23 18:44:50381 if not os.path.isdir(args.out_dir):
382 parser.error(f'OUT_DIR "{args.out_dir}" does not exist.')
Dan Harrington27d104d2020-09-08 18:30:14383 target_cache = TargetCache(args.out_dir)
384 filenames = FindMatchingTestFiles(args.file)
385
Dan Harringtonaa2c7ba2020-09-16 15:34:24386 targets, used_cache = FindTestTargets(target_cache, args.out_dir, filenames,
387 args.run_all)
Michael Thiessen09c0e1d2020-03-23 18:44:50388
389 gtest_filter = args.gtest_filter
390 if not gtest_filter:
Dan Harrington27d104d2020-09-08 18:30:14391 gtest_filter = BuildTestFilter(filenames, args.line)
Michael Thiessen09c0e1d2020-03-23 18:44:50392
Dan Harrington27d104d2020-09-08 18:30:14393 if not gtest_filter:
394 ExitWithMessage('Failed to derive a gtest filter')
395
396 assert targets
Dan Harringtonaa2c7ba2020-09-16 15:34:24397 build_ok = BuildTestTargetsWithNinja(args.out_dir, targets, args.dry_run)
398
399 # If we used the target cache, it's possible we chose the wrong target because
400 # a gn file was changed. The build step above will check for gn modifications
401 # and update build.ninja. Use this opportunity the verify the cache is still
402 # valid.
403 if used_cache and not target_cache.IsStillValid():
404 target_cache = TargetCache(args.out_dir)
405 new_targets, _ = FindTestTargets(target_cache, args.out_dir, filenames,
406 args.run_all)
407 if targets != new_targets:
408 # Note that this can happen, for example, if you rename a test target.
409 print('gn config was changed, trying to build again', file=sys.stderr)
410 targets = new_targets
411 if not BuildTestTargetsWithNinja(args.out_dir, targets, args.dry_run):
412 sys.exit(1)
413 else: # cache still valid, quit if the build failed
414 if not build_ok: sys.exit(1)
415
Dan Harrington27d104d2020-09-08 18:30:14416 RunTestTargets(args.out_dir, targets, gtest_filter, _extras, args.dry_run)
Michael Thiessen09c0e1d2020-03-23 18:44:50417
418
419if __name__ == '__main__':
420 sys.exit(main())