blob: c20b411b568286f6d6baa261689706f085fc8437 [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')
93 if 'win' in sys.platform:
94 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
111 if entry.is_file() and filename in entry.path:
112 matches.append(entry.path)
113 if entry.is_dir():
114 # On Windows, junctions are like a symlink that python interprets as a
115 # directory, leading to exceptions being thrown. We can just catch and
116 # ignore these exceptions like we would ignore symlinks.
117 try:
118 matches += RecursiveMatchFilename(entry.path, filename)
119 except FileNotFoundError as e:
120 if DEBUG:
121 print(f'Failed to scan directory "{entry}" - junction?')
122 pass
123 return matches
124
125
126def FindMatchingTestFile(target):
127 if DEBUG:
128 print('Finding files with full path containing: ' + target)
129 results = RecursiveMatchFilename(SRC_DIR, target)
130 if DEBUG:
131 print('Found matching file(s): ' + ' '.join(results))
132 if len(results) > 1:
133 # Arbitrarily capping at 10 results so we don't print the name of every file
134 # in the repo if the target is poorly specified.
135 results = results[:10]
136 raise Exception(f'Target "{target}" is ambiguous. Matching files: '
137 f'{results}')
138 if not results:
139 raise Exception(f'Target "{target}" did not match any files.')
140 return results[0]
141
142
143def IsTestTarget(target):
144 for suffix in _TEST_TARGET_SUFFIXES:
145 if target.endswith(suffix):
146 return True
147 return target in _OTHER_TEST_TARGETS
148
149
150def HaveUserPickTarget(path, targets):
151 # Cap to 10 targets for convenience [0-9].
152 targets = targets[:10]
153 target_list = ''
154 i = 0
155 for target in targets:
156 target_list += f'{i}. {target}\n'
157 i += 1
158 try:
159 value = int(
160 input(f'Target "{path}" is used by multiple test targets.\n' +
161 target_list + 'Please pick a target: '))
162 return targets[value]
163 except Exception as e:
164 print('Try again')
165 return HaveUserPickTarget(path, targets)
166
167
168def FindTestTarget(out_dir, path):
169 # Use gn refs to recursively find all targets that depend on |path|, filter
170 # internal gn targets, and match against well-known test suffixes, falling
171 # back to a list of known test targets if that fails.
172 gn_path = os.path.join(DEPOT_TOOLS_DIR, 'gn')
173 if 'win' in sys.platform:
174 gn_path += '.bat'
175 cmd = [gn_path, 'refs', out_dir, '--all', path]
176 targets = RunCommand(cmd, cwd=SRC_DIR).splitlines()
177 targets = [t for t in targets if '__' not in t]
178 test_targets = [t for t in targets if IsTestTarget(t)]
179
180 if not test_targets:
181 raise Exception(
182 f'Target "{path}" did not match any test targets. Consider adding '
183 f'one of the following targets to the top of this file: {targets}')
184 target = test_targets[0]
185 if len(test_targets) > 1:
186 target = HaveUserPickTarget(path, test_targets)
187
188 return target.split(':')[-1]
189
190
191def RunTestTarget(out_dir, target, gtest_filter, extra_args, dry_run):
192 # Look for the Android wrapper script first.
193 path = os.path.join(out_dir, 'bin', f'run_{target}')
194 if not os.path.isfile(path):
195 # Otherwise, use the Desktop target which is an executable.
196 path = os.path.join(out_dir, target)
197 extra_args = ' '.join(extra_args)
198 cmd = [path, f'--gtest_filter={gtest_filter}'] + shlex.split(extra_args)
199 print('Running test: ' + ' '.join(cmd))
200 if (dry_run):
201 return
202 LogCommand(cmd)
203
204
205def main():
206 parser = argparse.ArgumentParser(
207 description=__doc__, formatter_class=argparse.RawTextHelpFormatter)
208 parser.add_argument(
209 '--out-dir',
210 '-C',
211 metavar='OUT_DIR',
212 help='output directory of the build',
213 required=True)
214 parser.add_argument(
215 '--gtest_filter', '-f', metavar='FILTER', help='test filter')
216 parser.add_argument(
217 '--dry_run',
218 '-n',
219 action='store_true',
220 help='Print ninja and test run commands without executing them.')
221 parser.add_argument(
222 'file', metavar='FILE_NAME', help='test suite file (eg. FooTest.java)')
223
224 args, _extras = parser.parse_known_args()
225
226 if not os.path.isdir(args.out_dir):
227 parser.error(f'OUT_DIR "{args.out_dir}" does not exist.')
228 filename = FindMatchingTestFile(args.file)
229
230 gtest_filter = args.gtest_filter
231 if not gtest_filter:
232 if not filename.endswith('java'):
233 # In c++ tests, the test class often doesn't match the filename, or a
234 # single file will contain multiple test classes. It's likely possible to
235 # handle most cases with a regex and provide a default here.
236 # Patches welcome :)
237 parser.error('--gtest_filter must be specified for non-java tests.')
238 gtest_filter = '*' + os.path.splitext(os.path.basename(filename))[0] + '*'
239
240 target = FindTestTarget(args.out_dir, filename)
241 BuildTestTargetWithNinja(args.out_dir, target, args.dry_run)
242 RunTestTarget(args.out_dir, target, gtest_filter, _extras, args.dry_run)
243
244
245if __name__ == '__main__':
246 sys.exit(main())