3 # Copyright 2017 The Chromium Authors. All rights reserved.
4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file.
7 """Runs a gtest-based test on Swarming, optionally many times, collecting the
8 output of the runs into a directory. Useful for flake checking, and faster than
9 using trybots by avoiding repeated bot_update, compile, archive, etc. and
10 allowing greater parallelism.
12 To use, run in a new shell (it blocks until all Swarming jobs complete):
14 tools/run-swarmed.py out/rel base_unittests
16 The logs of the runs will be stored in results/ (or specify a results directory
17 with --results=some_dir). You can then do something like `grep -L SUCCESS
18 results/*` to find the tests that failed or otherwise process the log files.
20 See //docs/workflow/debugging-with-swarming.md for more details.
23 from __future__ import print_function
28 import multiprocessing.dummy
35 INTERNAL_ERROR_EXIT_CODE = -1000
37 DEFAULT_ANDROID_DEVICE_TYPE = "walleye"
41 """Triggers a swarming job. The arguments passed are:
42 - The index of the job;
43 - The command line arguments object;
44 - The hash of the isolate job used to trigger.
46 The return value is passed to a collect-style map() and consists of:
47 - The index of the job;
48 - The json file created by triggering and used to collect results;
49 - The command line arguments object.
51 index, args, isolated_hash = args
52 json_file = os.path.join(args.results, '%d.json' % index)
54 'tools/luci-go/swarming',
57 'https://chromium-swarm.appspot.com',
59 'https://isolateserver.appspot.com',
67 'os=' + args.swarming_os,
68 '-tag=purpose:user-debug-run-swarmed',
70 if args.target_os == 'fuchsia':
77 if args.arch != 'detect':
83 # The aliases for device type are stored here:
84 # luci/appengine/swarming/ui2/modules/alias.js
85 # for example 'blueline' = 'Pixel 3'
86 if args.target_os == 'android':
87 if args.device_type is None and args.device_os is None:
88 trigger_args += ['-d', 'device_type=' + DEFAULT_ANDROID_DEVICE_TYPE]
90 trigger_args += ['-d', 'device_type=' + args.device_type]
93 trigger_args += ['-d', 'device_os=' + args.device_os]
96 if not args.no_test_flags:
97 # These flags are recognized by our test runners, but do not work
98 # when running custom scripts.
100 '--test-launcher-summary-output=${ISOLATED_OUTDIR}/output.json',
101 '--system-log-file=${ISOLATED_OUTDIR}/system_log'
103 if args.gtest_filter:
104 runner_args.append('--gtest_filter=' + args.gtest_filter)
106 runner_args.append('--repeat=' + args.repeat)
107 elif args.target_os == 'fuchsia':
109 'testing/buildbot/filters/fuchsia.' + args.target_name + '.filter'
110 if os.path.isfile(filter_file):
111 runner_args.append('--test-launcher-filter-file=../../' + filter_file)
114 trigger_args.append('--')
115 trigger_args.extend(runner_args)
117 with open(os.devnull, 'w') as nul:
118 subprocess.check_call(trigger_args, stdout=nul)
119 return (index, json_file, args)
122 def _Collect(spawn_result):
123 index, json_file, args = spawn_result
124 with open(json_file) as f:
125 task_json = json.load(f)
126 task_ids = [task['task_id'] for task in task_json['tasks']]
129 print('Task {}: https://chromium-swarm.appspot.com/task?id={}'.format(
131 p = subprocess.Popen(
133 'tools/luci-go/swarming',
136 'https://chromium-swarm.appspot.com',
137 '--task-output-stdout=console',
139 stdout=subprocess.PIPE,
140 stderr=subprocess.STDOUT)
141 stdout = p.communicate()[0]
142 if p.returncode != 0 and len(stdout) < 2**10 and 'Internal error!' in stdout:
143 exit_code = INTERNAL_ERROR_EXIT_CODE
144 file_suffix = '.INTERNAL_ERROR'
146 exit_code = p.returncode
147 file_suffix = '' if exit_code == 0 else '.FAILED'
148 filename = '%d%s.stdout.txt' % (index, file_suffix)
149 with open(os.path.join(args.results, filename), 'w') as f:
155 parser = argparse.ArgumentParser()
156 parser.add_argument('--swarming-os', help='OS specifier for Swarming.')
157 parser.add_argument('--target-os', default='detect', help='gn target_os')
158 parser.add_argument('--arch', '-a', default='detect',
159 help='CPU architecture of the test binary.')
160 parser.add_argument('--build', dest='build', action='store_true',
161 help='Build before isolating (default).')
162 parser.add_argument( '--no-build', dest='build', action='store_false',
163 help='Do not build, just isolate.')
164 parser.add_argument('--isolate-map-file', '-i',
165 help='path to isolate map file if not using default')
166 parser.add_argument('--copies', '-n', type=int, default=1,
167 help='Number of copies to spawn.')
169 '--device-os', help='Run tests on the given version of Android.')
172 help='device_type specifier for Swarming'
173 ' from https://chromium-swarm.appspot.com/botlist .')
174 parser.add_argument('--pool',
175 default='chromium.tests',
176 help='Use the given swarming pool.')
177 parser.add_argument('--results', '-r', default='results',
178 help='Directory in which to store results.')
179 parser.add_argument('--gtest_filter',
180 help='Use the given gtest_filter, rather than the '
181 'default filter file, if any.')
183 '--repeat', help='Number of times to repeat the specified set of tests.')
184 parser.add_argument('--no-test-flags', action='store_true',
185 help='Do not add --test-launcher-summary-output and '
186 '--system-log-file flags to the comment.')
187 parser.add_argument('out_dir', type=str, help='Build directory.')
188 parser.add_argument('target_name', type=str, help='Name of target to run.')
190 args = parser.parse_args()
192 if args.target_os == 'detect':
193 with open(os.path.join(args.out_dir, 'args.gn')) as f:
196 l = l.split('#')[0].strip()
198 k, v = map(str.strip, l.split('=', 1))
200 if 'target_os' in gn_args:
201 args.target_os = gn_args['target_os'].strip('"')
203 args.target_os = { 'darwin': 'mac', 'linux2': 'linux', 'win32': 'win' }[
206 if args.swarming_os is None:
211 'android': 'Android',
215 if args.target_os == 'win' and args.target_name.endswith('.exe'):
216 # The machinery expects not to have a '.exe' suffix.
217 args.target_name = os.path.splitext(args.target_name)[0]
219 # Determine the CPU architecture of the test binary, if not specified.
220 if args.arch == 'detect' and args.target_os not in ('android', 'mac', 'win'):
221 executable_info = subprocess.check_output(
222 ['file', os.path.join(args.out_dir, args.target_name)])
223 if 'ARM aarch64' in executable_info:
228 mb_cmd = [sys.executable, 'tools/mb/mb.py', 'isolate']
230 mb_cmd.append('--no-build')
231 if args.isolate_map_file:
232 mb_cmd += ['--isolate-map-file', args.isolate_map_file]
233 mb_cmd += ['//' + args.out_dir, args.target_name]
234 subprocess.check_call(mb_cmd)
236 print('If you get authentication errors, follow:')
238 ' https://www.chromium.org/developers/testing/isolated-testing/for-swes#TOC-Login-on-the-services'
241 print('Uploading to isolate server, this can take a while...')
242 isolated = os.path.join(args.out_dir, args.target_name + '.isolated')
243 subprocess.check_output([
244 'tools/luci-go/isolate', 'archive', '-I',
245 'https://isolateserver.appspot.com', '-i',
246 os.path.join(args.out_dir, args.target_name + '.isolate'), '-s', isolated
248 with open(isolated) as f:
249 isolated_hash = hashlib.sha1(f.read()).hexdigest()
251 if os.path.isdir(args.results):
252 shutil.rmtree(args.results)
253 os.makedirs(args.results)
256 print('Triggering %d tasks...' % args.copies)
257 # Use dummy since threadpools give better exception messages
258 # than process pools do, and threads work fine for what we're doing.
259 pool = multiprocessing.dummy.Pool()
260 spawn_args = map(lambda i: (i, args, isolated_hash), range(args.copies))
261 spawn_results = pool.imap_unordered(_Spawn, spawn_args)
264 collect_results = pool.imap_unordered(_Collect, spawn_results)
265 for result in collect_results:
266 exit_codes.append(result)
267 successes = sum(1 for x in exit_codes if x == 0)
268 errors = sum(1 for x in exit_codes if x == INTERNAL_ERROR_EXIT_CODE)
269 failures = len(exit_codes) - successes - errors
270 clear_to_eol = '\033[K'
272 '\r[%d/%d] collected: '
273 '%d successes, %d failures, %d bot errors...%s' %
274 (len(exit_codes), args.copies, successes, failures, errors,
280 print('Results logs collected into', os.path.abspath(args.results) + '.')
287 if __name__ == '__main__':