Upload upstream chromium 94.0.4606.31
[platform/framework/web/chromium-efl.git] / tools / run-swarmed.py
1 #!/usr/bin/env python
2
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.
6
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.
11
12 To use, run in a new shell (it blocks until all Swarming jobs complete):
13
14   tools/run-swarmed.py out/rel base_unittests
15
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.
19
20 See //docs/workflow/debugging-with-swarming.md for more details.
21 """
22
23 from __future__ import print_function
24
25 import argparse
26 import hashlib
27 import json
28 import multiprocessing.dummy
29 import os
30 import shutil
31 import subprocess
32 import sys
33
34
35 INTERNAL_ERROR_EXIT_CODE = -1000
36
37 DEFAULT_ANDROID_DEVICE_TYPE = "walleye"
38
39
40 def _Spawn(args):
41   """Triggers a swarming job. The arguments passed are:
42   - The index of the job;
43   - The command line arguments object;
44   - The digest of test files.
45
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.
50   """
51   index, args, cas_digest, swarming_command = args
52   json_file = os.path.join(args.results, '%d.json' % index)
53   trigger_args = [
54       'tools/luci-go/swarming',
55       'trigger',
56       '-S',
57       'https://chromium-swarm.appspot.com',
58       '-d',
59       'pool=' + args.pool,
60       '-digest',
61       cas_digest,
62       '-dump-json',
63       json_file,
64       '-d',
65       'os=' + args.swarming_os,
66       '-tag=purpose:user-debug-run-swarmed',
67   ]
68   if args.target_os == 'fuchsia':
69     trigger_args += [
70         '-d',
71         'kvm=1',
72         '-d',
73         'gpu=none',
74     ]
75   if args.arch != 'detect':
76     trigger_args += [
77         '-d',
78         'cpu=' + args.arch,
79     ]
80
81   # The aliases for device type are stored here:
82   # luci/appengine/swarming/ui2/modules/alias.js
83   # for example 'blueline' = 'Pixel 3'
84   if args.target_os == 'android':
85     if args.device_type is None and args.device_os is None:
86       trigger_args += ['-d', 'device_type=' + DEFAULT_ANDROID_DEVICE_TYPE]
87   if args.device_type:
88     trigger_args += ['-d', 'device_type=' + args.device_type]
89
90   if args.device_os:
91     trigger_args += ['-d', 'device_os=' + args.device_os]
92
93   runner_args = []
94   if not args.no_test_flags:
95     # These flags are recognized by our test runners, but do not work
96     # when running custom scripts.
97     runner_args += [
98         '--test-launcher-summary-output=${ISOLATED_OUTDIR}/output.json',
99         '--system-log-file=${ISOLATED_OUTDIR}/system_log'
100     ]
101   if args.gtest_filter:
102     runner_args.append('--gtest_filter=' + args.gtest_filter)
103   if args.gtest_repeat:
104     runner_args.append('--gtest_repeat=' + args.gtest_repeat)
105   if args.test_launcher_shard_index and args.test_launcher_total_shards:
106     runner_args.append('--test-launcher-shard-index=' +
107                        args.test_launcher_shard_index)
108     runner_args.append('--test-launcher-total-shards=' +
109                        args.test_launcher_total_shards)
110   elif args.target_os == 'fuchsia':
111     filter_file = \
112         'testing/buildbot/filters/fuchsia.' + args.target_name + '.filter'
113     if os.path.isfile(filter_file):
114       runner_args.append('--test-launcher-filter-file=../../' + filter_file)
115
116   trigger_args.extend(['--relative-cwd', args.out_dir, '--'])
117   trigger_args.extend(swarming_command)
118   trigger_args.extend(runner_args)
119
120   with open(os.devnull, 'w') as nul:
121     subprocess.check_call(trigger_args, stdout=nul)
122   return (index, json_file, args)
123
124
125 def _Collect(spawn_result):
126   index, json_file, args = spawn_result
127   with open(json_file) as f:
128     task_json = json.load(f)
129   task_ids = [task['task_id'] for task in task_json['tasks']]
130
131   for t in task_ids:
132     print('Task {}: https://chromium-swarm.appspot.com/task?id={}'.format(
133         index, t))
134   p = subprocess.Popen(
135       [
136           'tools/luci-go/swarming',
137           'collect',
138           '-S',
139           'https://chromium-swarm.appspot.com',
140           '--task-output-stdout=console',
141       ] + task_ids,
142       stdout=subprocess.PIPE,
143       stderr=subprocess.STDOUT)
144   stdout = p.communicate()[0]
145   if p.returncode != 0 and len(stdout) < 2**10 and 'Internal error!' in stdout:
146     exit_code = INTERNAL_ERROR_EXIT_CODE
147     file_suffix = '.INTERNAL_ERROR'
148   else:
149     exit_code = p.returncode
150     file_suffix = '' if exit_code == 0 else '.FAILED'
151   filename = '%d%s.stdout.txt' % (index, file_suffix)
152   with open(os.path.join(args.results, filename), 'w') as f:
153     f.write(stdout)
154   return exit_code
155
156
157 def main():
158   parser = argparse.ArgumentParser()
159   parser.add_argument('--swarming-os', help='OS specifier for Swarming.')
160   parser.add_argument('--target-os', default='detect', help='gn target_os')
161   parser.add_argument('--arch', '-a', default='detect',
162                       help='CPU architecture of the test binary.')
163   parser.add_argument('--build',
164                       dest='build',
165                       action='store_true',
166                       help='Build before isolating.')
167   parser.add_argument('--no-build',
168                       dest='build',
169                       action='store_false',
170                       help='Do not build, just isolate (default).')
171   parser.add_argument('--isolate-map-file', '-i',
172                       help='path to isolate map file if not using default')
173   parser.add_argument('--copies', '-n', type=int, default=1,
174                       help='Number of copies to spawn.')
175   parser.add_argument(
176       '--device-os', help='Run tests on the given version of Android.')
177   parser.add_argument(
178       '--device-type',
179       help='device_type specifier for Swarming'
180       ' from https://chromium-swarm.appspot.com/botlist .')
181   parser.add_argument('--pool',
182                       default='chromium.tests',
183                       help='Use the given swarming pool.')
184   parser.add_argument('--results', '-r', default='results',
185                       help='Directory in which to store results.')
186   parser.add_argument('--gtest_filter',
187                       help='Use the given gtest_filter, rather than the '
188                            'default filter file, if any.')
189   parser.add_argument(
190       '--gtest_repeat',
191       help='Number of times to repeat the specified set of tests.')
192   parser.add_argument(
193       '--test-launcher-shard-index',
194       help='Shard index to run. Use with --test-launcher-total-shards.')
195   parser.add_argument('--test-launcher-total-shards',
196                       help='Number of shards to split the test into. Use with'
197                       ' --test-launcher-shard-index.')
198   parser.add_argument('--no-test-flags', action='store_true',
199                       help='Do not add --test-launcher-summary-output and '
200                            '--system-log-file flags to the comment.')
201   parser.add_argument('out_dir', type=str, help='Build directory.')
202   parser.add_argument('target_name', type=str, help='Name of target to run.')
203
204   args = parser.parse_args()
205
206   if args.target_os == 'detect':
207     with open(os.path.join(args.out_dir, 'args.gn')) as f:
208       gn_args = {}
209       for l in f:
210         l = l.split('#')[0].strip()
211         if not l: continue
212         k, v = map(str.strip, l.split('=', 1))
213         gn_args[k] = v
214     if 'target_os' in gn_args:
215       args.target_os = gn_args['target_os'].strip('"')
216     else:
217       args.target_os = { 'darwin': 'mac', 'linux2': 'linux', 'win32': 'win' }[
218                            sys.platform]
219
220   if args.swarming_os is None:
221     args.swarming_os = {
222       'mac': 'Mac',
223       'win': 'Windows',
224       'linux': 'Linux',
225       'android': 'Android',
226       'fuchsia': 'Linux'
227     }[args.target_os]
228
229   if args.target_os == 'win' and args.target_name.endswith('.exe'):
230     # The machinery expects not to have a '.exe' suffix.
231     args.target_name = os.path.splitext(args.target_name)[0]
232
233   # Determine the CPU architecture of the test binary, if not specified.
234   if args.arch == 'detect' and args.target_os not in ('android', 'mac', 'win'):
235     executable_info = subprocess.check_output(
236         ['file', os.path.join(args.out_dir, args.target_name)])
237     if 'ARM aarch64' in executable_info:
238       args.arch = 'arm64',
239     else:
240       args.arch = 'x86-64'
241
242   mb_cmd = [sys.executable, 'tools/mb/mb.py', 'isolate']
243   if not args.build:
244     mb_cmd.append('--no-build')
245   if args.isolate_map_file:
246     mb_cmd += ['--isolate-map-file', args.isolate_map_file]
247   mb_cmd += ['//' + args.out_dir, args.target_name]
248   subprocess.check_call(mb_cmd)
249
250   print('If you get authentication errors, follow:')
251   print(
252       '  https://chromium.googlesource.com/chromium/src/+/HEAD/docs/workflow/debugging-with-swarming.md#authenticating'
253   )
254
255   print('Uploading to isolate server, this can take a while...')
256   isolate = os.path.join(args.out_dir, args.target_name + '.isolate')
257   archive_json = os.path.join(args.out_dir, args.target_name + '.archive.json')
258   subprocess.check_output([
259       'tools/luci-go/isolate', 'archive', '-cas-instance', 'chromium-swarm',
260       '-isolate', isolate, '-dump-json', archive_json
261   ])
262   with open(archive_json) as f:
263     cas_digest = json.load(f).get(args.target_name)
264
265   mb_cmd = [
266       sys.executable, 'tools/mb/mb.py', 'get-swarming-command', '--as-list'
267   ]
268   if not args.build:
269     mb_cmd.append('--no-build')
270   if args.isolate_map_file:
271     mb_cmd += ['--isolate-map-file', args.isolate_map_file]
272   mb_cmd += ['//' + args.out_dir, args.target_name]
273   swarming_cmd = json.loads(subprocess.check_output(mb_cmd))
274
275   if os.path.isdir(args.results):
276     shutil.rmtree(args.results)
277   os.makedirs(args.results)
278
279   try:
280     print('Triggering %d tasks...' % args.copies)
281     # Use dummy since threadpools give better exception messages
282     # than process pools do, and threads work fine for what we're doing.
283     pool = multiprocessing.dummy.Pool()
284     spawn_args = map(lambda i: (i, args, cas_digest, swarming_cmd),
285                      range(args.copies))
286     spawn_results = pool.imap_unordered(_Spawn, spawn_args)
287
288     exit_codes = []
289     collect_results = pool.imap_unordered(_Collect, spawn_results)
290     for result in collect_results:
291       exit_codes.append(result)
292       successes = sum(1 for x in exit_codes if x == 0)
293       errors = sum(1 for x in exit_codes if x == INTERNAL_ERROR_EXIT_CODE)
294       failures = len(exit_codes) - successes - errors
295       clear_to_eol = '\033[K'
296       print(
297           '\r[%d/%d] collected: '
298           '%d successes, %d failures, %d bot errors...%s' %
299           (len(exit_codes), args.copies, successes, failures, errors,
300            clear_to_eol),
301           end=' ')
302       sys.stdout.flush()
303
304     print()
305     print('Results logs collected into', os.path.abspath(args.results) + '.')
306   finally:
307     pool.close()
308     pool.join()
309   return 0
310
311
312 if __name__ == '__main__':
313   sys.exit(main())