blob: 4bb7a073555088a437382d486f34897e4181fbb4 [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
7This script finds the appropriate test suite for the specified test file, builds
8it, then runs it with the (optionally) specified filter, passing any extra args
9on to the test runner.
10
11Examples:
12autotest.py -C out/Desktop bit_cast_unittest.cc --gtest_filter=BitCastTest* -v
13autotest.py -C out/Android UrlUtilitiesUnitTest --fast-local-dev -v
14"""
15
16import argparse
17import locale
18import logging
19import multiprocessing
20import os
21import re
22import shlex
23import subprocess
24import sys
25
26from pathlib import Path
27
28USE_PYTHON_3 = f'This script will only run under python3.'
29
30SRC_DIR = Path(__file__).parent.parent.resolve()
31DEPOT_TOOLS_DIR = SRC_DIR.joinpath('third_party', 'depot_tools')
32DEBUG = False
33
34_TEST_TARGET_SUFFIXES = [
35 '_browsertests',
36 '_junit_tests',
37 '_perftests',
38 '_test_apk',
39 '_unittests',
40]
41
Michael Thiessenf46171e2020-03-31 17:29:3842# Some test suites use suffixes that would also match non-test-suite targets.
43# Those test suites should be manually added here.
Michael Thiessen09c0e1d2020-03-23 18:44:5044_OTHER_TEST_TARGETS = [
45 '//chrome/test:browser_tests',
Michael Thiessenf46171e2020-03-31 17:29:3846 '//chrome/test:unit_tests',
Michael Thiessen09c0e1d2020-03-23 18:44:5047]
48
49
50class CommandError(Exception):
51 """Exception thrown when we can't parse the input file."""
52
53 def __init__(self, command, return_code, output=None):
54 Exception.__init__(self)
55 self.command = command
56 self.return_code = return_code
57 self.output = output
58
59 def __str__(self):
60 message = (f'\n***\nERROR: Error while running command {self.command}'
61 f'.\nExit status: {self.return_code}\n')
62 if self.output:
63 message += f'Output:\n{self.output}\n'
64 message += '***'
65 return message
66
67
68def LogCommand(cmd, **kwargs):
69 if DEBUG:
70 print('Running command: ' + ' '.join(cmd))
71
72 try:
73 subprocess.check_call(cmd, **kwargs)
74 except subprocess.CalledProcessError as e:
75 raise CommandError(e.cmd, e.returncode) from None
76
77
78def RunCommand(cmd, **kwargs):
79 if DEBUG:
80 print('Running command: ' + ' '.join(cmd))
81
82 try:
83 # Set an encoding to convert the binary output to a string.
84 return subprocess.check_output(
85 cmd, **kwargs, encoding=locale.getpreferredencoding())
86 except subprocess.CalledProcessError as e:
87 raise CommandError(e.cmd, e.returncode, e.output) from None
88
89
90def BuildTestTargetWithNinja(out_dir, target, dry_run):
91 """Builds the specified target with ninja"""
92 ninja_path = os.path.join(DEPOT_TOOLS_DIR, 'autoninja')
K. Moon01548662020-04-03 00:14:2593 if sys.platform.startswith('win32'):
Michael Thiessen09c0e1d2020-03-23 18:44:5094 ninja_path += '.bat'
95 cmd = [ninja_path, '-C', out_dir, target]
96 print('Building: ' + ' '.join(cmd))
97 if (dry_run):
98 return
99 RunCommand(cmd)
100
101
102def RecursiveMatchFilename(folder, filename):
103 current_dir = os.path.split(folder)[-1]
104 if current_dir.startswith('out') or current_dir.startswith('.'):
105 return []
106 matches = []
107 with os.scandir(folder) as it:
108 for entry in it:
109 if (entry.is_symlink()):
110 continue
Michael Thiessen0264afc62020-04-03 20:31:34111 if (entry.is_file() and filename in entry.path and
112 not os.path.basename(entry.path).startswith('.')):
Michael Thiessen09c0e1d2020-03-23 18:44:50113 matches.append(entry.path)
114 if entry.is_dir():
115 # On Windows, junctions are like a symlink that python interprets as a
116 # directory, leading to exceptions being thrown. We can just catch and
117 # ignore these exceptions like we would ignore symlinks.
118 try:
119 matches += RecursiveMatchFilename(entry.path, filename)
120 except FileNotFoundError as e:
121 if DEBUG:
122 print(f'Failed to scan directory "{entry}" - junction?')
123 pass
124 return matches
125
126
127def FindMatchingTestFile(target):
Jesse McKenna83b6ac1b2020-05-07 18:25:38128 if sys.platform.startswith('win32') and os.path.altsep in target:
129 # Use backslash as the path separator on Windows to match os.scandir().
130 if DEBUG:
131 print('Replacing ' + os.path.altsep + ' with ' + os.path.sep + ' in: '
132 + target)
133 target = target.replace(os.path.altsep, os.path.sep)
Michael Thiessen09c0e1d2020-03-23 18:44:50134 if DEBUG:
135 print('Finding files with full path containing: ' + target)
136 results = RecursiveMatchFilename(SRC_DIR, target)
137 if DEBUG:
138 print('Found matching file(s): ' + ' '.join(results))
139 if len(results) > 1:
140 # Arbitrarily capping at 10 results so we don't print the name of every file
141 # in the repo if the target is poorly specified.
142 results = results[:10]
143 raise Exception(f'Target "{target}" is ambiguous. Matching files: '
144 f'{results}')
145 if not results:
146 raise Exception(f'Target "{target}" did not match any files.')
147 return results[0]
148
149
150def IsTestTarget(target):
151 for suffix in _TEST_TARGET_SUFFIXES:
152 if target.endswith(suffix):
153 return True
154 return target in _OTHER_TEST_TARGETS
155
156
157def HaveUserPickTarget(path, targets):
158 # Cap to 10 targets for convenience [0-9].
159 targets = targets[:10]
160 target_list = ''
161 i = 0
162 for target in targets:
163 target_list += f'{i}. {target}\n'
164 i += 1
165 try:
166 value = int(
167 input(f'Target "{path}" is used by multiple test targets.\n' +
168 target_list + 'Please pick a target: '))
169 return targets[value]
170 except Exception as e:
171 print('Try again')
172 return HaveUserPickTarget(path, targets)
173
174
175def FindTestTarget(out_dir, path):
176 # Use gn refs to recursively find all targets that depend on |path|, filter
177 # internal gn targets, and match against well-known test suffixes, falling
178 # back to a list of known test targets if that fails.
179 gn_path = os.path.join(DEPOT_TOOLS_DIR, 'gn')
K. Moon01548662020-04-03 00:14:25180 if sys.platform.startswith('win32'):
Michael Thiessen09c0e1d2020-03-23 18:44:50181 gn_path += '.bat'
182 cmd = [gn_path, 'refs', out_dir, '--all', path]
183 targets = RunCommand(cmd, cwd=SRC_DIR).splitlines()
184 targets = [t for t in targets if '__' not in t]
185 test_targets = [t for t in targets if IsTestTarget(t)]
186
187 if not test_targets:
188 raise Exception(
189 f'Target "{path}" did not match any test targets. Consider adding '
190 f'one of the following targets to the top of this file: {targets}')
191 target = test_targets[0]
192 if len(test_targets) > 1:
193 target = HaveUserPickTarget(path, test_targets)
194
195 return target.split(':')[-1]
196
197
198def RunTestTarget(out_dir, target, gtest_filter, extra_args, dry_run):
199 # Look for the Android wrapper script first.
200 path = os.path.join(out_dir, 'bin', f'run_{target}')
201 if not os.path.isfile(path):
202 # Otherwise, use the Desktop target which is an executable.
203 path = os.path.join(out_dir, target)
204 extra_args = ' '.join(extra_args)
205 cmd = [path, f'--gtest_filter={gtest_filter}'] + shlex.split(extra_args)
206 print('Running test: ' + ' '.join(cmd))
207 if (dry_run):
208 return
209 LogCommand(cmd)
210
211
212def main():
213 parser = argparse.ArgumentParser(
214 description=__doc__, formatter_class=argparse.RawTextHelpFormatter)
215 parser.add_argument(
216 '--out-dir',
217 '-C',
218 metavar='OUT_DIR',
219 help='output directory of the build',
220 required=True)
221 parser.add_argument(
222 '--gtest_filter', '-f', metavar='FILTER', help='test filter')
223 parser.add_argument(
224 '--dry_run',
225 '-n',
226 action='store_true',
227 help='Print ninja and test run commands without executing them.')
228 parser.add_argument(
229 'file', metavar='FILE_NAME', help='test suite file (eg. FooTest.java)')
230
231 args, _extras = parser.parse_known_args()
232
233 if not os.path.isdir(args.out_dir):
234 parser.error(f'OUT_DIR "{args.out_dir}" does not exist.')
235 filename = FindMatchingTestFile(args.file)
236
237 gtest_filter = args.gtest_filter
238 if not gtest_filter:
239 if not filename.endswith('java'):
240 # In c++ tests, the test class often doesn't match the filename, or a
241 # single file will contain multiple test classes. It's likely possible to
242 # handle most cases with a regex and provide a default here.
243 # Patches welcome :)
244 parser.error('--gtest_filter must be specified for non-java tests.')
245 gtest_filter = '*' + os.path.splitext(os.path.basename(filename))[0] + '*'
246
247 target = FindTestTarget(args.out_dir, filename)
248 BuildTestTargetWithNinja(args.out_dir, target, args.dry_run)
249 RunTestTarget(args.out_dir, target, gtest_filter, _extras, args.dry_run)
250
251
252if __name__ == '__main__':
253 sys.exit(main())