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