Upload upstream chromium 114.0.5735.31
[platform/framework/web/chromium-efl.git] / tools / run-swarmed.py
1 #!/usr/bin/env python3
2
3 # Copyright 2017 The Chromium Authors
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
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 import traceback
34
35 CHROMIUM_ROOT = os.path.join(os.path.dirname(__file__), os.pardir)
36 BUILD_DIR = os.path.join(CHROMIUM_ROOT, 'build')
37
38 if BUILD_DIR not in sys.path:
39   sys.path.insert(0, BUILD_DIR)
40 import gn_helpers
41
42 INTERNAL_ERROR_EXIT_CODE = -1000
43
44 DEFAULT_ANDROID_DEVICE_TYPE = "walleye"
45
46
47 def _Spawn(args):
48   """Triggers a swarming job. The arguments passed are:
49   - The index of the job;
50   - The command line arguments object;
51   - The digest of test files.
52
53   The return value is passed to a collect-style map() and consists of:
54   - The index of the job;
55   - The json file created by triggering and used to collect results;
56   - The command line arguments object.
57   """
58   try:
59     return _DoSpawn(args)
60   except Exception as e:
61     traceback.print_exc()
62     return None
63
64
65 def _DoSpawn(args):
66   index, args, cas_digest, swarming_command = args
67   runner_args = []
68   json_file = os.path.join(args.results, '%d.json' % index)
69   trigger_args = [
70       'tools/luci-go/swarming',
71       'trigger',
72       '-S',
73       'https://chromium-swarm.appspot.com',
74       '-digest',
75       cas_digest,
76       '-dump-json',
77       json_file,
78       '-tag=purpose:user-debug-run-swarmed',
79   ]
80   if args.target_os == 'fuchsia':
81     trigger_args += [
82         '-d',
83         'kvm=1',
84     ]
85     if args.gpu is None:
86       trigger_args += [
87           '-d',
88           'gpu=none',
89       ]
90   elif args.target_os == 'android':
91     if args.arch == 'x86':
92       # No x86 Android devices are available in swarming. So assume we want to
93       # run on emulators when building for x86 on Android.
94       args.swarming_os = 'Linux'
95       args.pool = 'chromium.tests.avd'
96       # generic_android28 == Android P emulator. See //tools/android/avd/proto/
97       # for other options.
98       runner_args.append(
99           '--avd-config=../../tools/android/avd/proto/generic_android28.textpb')
100     elif args.device_type is None and args.device_os is None:
101       # The aliases for device type are stored here:
102       # luci/appengine/swarming/ui2/modules/alias.js
103       # for example 'blueline' = 'Pixel 3'
104       trigger_args += ['-d', 'device_type=' + DEFAULT_ANDROID_DEVICE_TYPE]
105   elif args.arch != 'detect':
106     trigger_args += [
107         '-d',
108         'cpu=' + args.arch,
109     ]
110
111   if args.device_type:
112     trigger_args += ['-d', 'device_type=' + args.device_type]
113
114   if args.device_os:
115     trigger_args += ['-d', 'device_os=' + args.device_os]
116
117   if args.gpu:
118     trigger_args += ['-d', 'gpu=' + args.gpu]
119
120   if not args.no_test_flags:
121     # These flags are recognized by our test runners, but do not work
122     # when running custom scripts.
123     runner_args += [
124         '--test-launcher-summary-output=${ISOLATED_OUTDIR}/output.json'
125     ]
126     if 'junit' not in args.target_name:
127       runner_args += ['--system-log-file=${ISOLATED_OUTDIR}/system_log']
128   if args.gtest_filter:
129     runner_args.append('--gtest_filter=' + args.gtest_filter)
130   if args.gtest_repeat:
131     runner_args.append('--gtest_repeat=' + args.gtest_repeat)
132   if args.test_launcher_shard_index and args.test_launcher_total_shards:
133     runner_args.append('--test-launcher-shard-index=' +
134                        args.test_launcher_shard_index)
135     runner_args.append('--test-launcher-total-shards=' +
136                        args.test_launcher_total_shards)
137   elif args.target_os == 'fuchsia':
138     filter_file = \
139         'testing/buildbot/filters/fuchsia.' + args.target_name + '.filter'
140     if os.path.isfile(filter_file):
141       runner_args.append('--test-launcher-filter-file=../../' + filter_file)
142
143   runner_args.extend(args.runner_args)
144
145   trigger_args.extend(['-d', 'os=' + args.swarming_os])
146   trigger_args.extend(['-d', 'pool=' + args.pool])
147   trigger_args.extend(['--relative-cwd', args.out_dir, '--'])
148   trigger_args.extend(swarming_command)
149   trigger_args.extend(runner_args)
150
151   with open(os.devnull, 'w') as nul:
152     subprocess.check_call(trigger_args, stdout=nul)
153   return (index, json_file, args)
154
155
156 def _Collect(spawn_result):
157   if spawn_result is None:
158     return 1
159
160   index, json_file, args = spawn_result
161   with open(json_file) as f:
162     task_json = json.load(f)
163   task_ids = [task['task_id'] for task in task_json['tasks']]
164
165   for t in task_ids:
166     print('Task {}: https://chromium-swarm.appspot.com/task?id={}'.format(
167         index, t))
168   p = subprocess.Popen(
169       [
170           'tools/luci-go/swarming',
171           'collect',
172           '-S',
173           'https://chromium-swarm.appspot.com',
174           '--task-output-stdout=console',
175       ] + task_ids,
176       stdout=subprocess.PIPE,
177       stderr=subprocess.STDOUT)
178   stdout = p.communicate()[0]
179   if p.returncode != 0 and len(stdout) < 2**10 and 'Internal error!' in stdout:
180     exit_code = INTERNAL_ERROR_EXIT_CODE
181     file_suffix = '.INTERNAL_ERROR'
182   else:
183     exit_code = p.returncode
184     file_suffix = '' if exit_code == 0 else '.FAILED'
185   filename = '%d%s.stdout.txt' % (index, file_suffix)
186   with open(os.path.join(args.results, filename), 'wb') as f:
187     f.write(stdout)
188   return exit_code
189
190
191 def main():
192   parser = argparse.ArgumentParser()
193   parser.add_argument('--swarming-os', help='OS specifier for Swarming.')
194   parser.add_argument('--target-os', default='detect', help='gn target_os')
195   parser.add_argument('--arch', '-a', default='detect',
196                       help='CPU architecture of the test binary.')
197   parser.add_argument('--build',
198                       dest='build',
199                       action='store_true',
200                       help='Build before isolating.')
201   parser.add_argument('--no-build',
202                       dest='build',
203                       action='store_false',
204                       help='Do not build, just isolate (default).')
205   parser.add_argument('--isolate-map-file', '-i',
206                       help='path to isolate map file if not using default')
207   parser.add_argument('--copies', '-n', type=int, default=1,
208                       help='Number of copies to spawn.')
209   parser.add_argument(
210       '--device-os', help='Run tests on the given version of Android.')
211   parser.add_argument('--device-type',
212                       help='device_type specifier for Swarming'
213                       ' from https://chromium-swarm.appspot.com/botlist .')
214   parser.add_argument('--gpu',
215                       help='gpu specifier for Swarming'
216                       ' from https://chromium-swarm.appspot.com/botlist .')
217   parser.add_argument('--pool',
218                       default='chromium.tests',
219                       help='Use the given swarming pool.')
220   parser.add_argument('--results', '-r', default='results',
221                       help='Directory in which to store results.')
222   parser.add_argument(
223       '--gtest_filter',
224       help='Deprecated. Pass as test runner arg instead, like \'-- '
225       '--gtest_filter="*#testFoo"\'')
226   parser.add_argument(
227       '--gtest_repeat',
228       help='Deprecated. Pass as test runner arg instead, like \'-- '
229       '--gtest_repeat=99\'')
230   parser.add_argument(
231       '--test-launcher-shard-index',
232       help='Shard index to run. Use with --test-launcher-total-shards.')
233   parser.add_argument('--test-launcher-total-shards',
234                       help='Number of shards to split the test into. Use with'
235                       ' --test-launcher-shard-index.')
236   parser.add_argument('--no-test-flags', action='store_true',
237                       help='Do not add --test-launcher-summary-output and '
238                            '--system-log-file flags to the comment.')
239   parser.add_argument('out_dir', type=str, help='Build directory.')
240   parser.add_argument('target_name', type=str, help='Name of target to run.')
241   parser.add_argument(
242       'runner_args',
243       nargs='*',
244       type=str,
245       help='Arguments to pass to the test runner, e.g. gtest_filter and '
246       'gtest_repeat.')
247
248   args = parser.parse_intermixed_args()
249
250   with open(os.path.join(args.out_dir, 'args.gn')) as f:
251     gn_args = gn_helpers.FromGNArgs(f.read())
252
253   if args.target_os == 'detect':
254     if 'target_os' in gn_args:
255       args.target_os = gn_args['target_os'].strip('"')
256     else:
257       args.target_os = {
258           'darwin': 'mac',
259           'linux': 'linux',
260           'win32': 'win'
261       }[sys.platform]
262
263   if args.swarming_os is None:
264     args.swarming_os = {
265       'mac': 'Mac',
266       'win': 'Windows',
267       'linux': 'Linux',
268       'android': 'Android',
269       'fuchsia': 'Linux'
270     }[args.target_os]
271
272   if args.target_os == 'win' and args.target_name.endswith('.exe'):
273     # The machinery expects not to have a '.exe' suffix.
274     args.target_name = os.path.splitext(args.target_name)[0]
275
276   # Determine the CPU architecture of the test binary, if not specified.
277   if args.arch == 'detect':
278     if args.target_os not in ('android', 'mac', 'win'):
279       executable_info = subprocess.check_output(
280           ['file', os.path.join(args.out_dir, args.target_name)], text=True)
281       if 'ARM aarch64' in executable_info:
282         args.arch = 'arm64',
283       else:
284         args.arch = 'x86-64'
285     elif args.target_os == 'android':
286       args.arch = gn_args.get('target_cpu', 'detect')
287
288   mb_cmd = [sys.executable, 'tools/mb/mb.py', 'isolate']
289   if not args.build:
290     mb_cmd.append('--no-build')
291   if args.isolate_map_file:
292     mb_cmd += ['--isolate-map-file', args.isolate_map_file]
293   mb_cmd += ['//' + args.out_dir, args.target_name]
294   subprocess.check_call(mb_cmd, shell=os.name == 'nt')
295
296   print('If you get authentication errors, follow:')
297   print(
298       '  https://chromium.googlesource.com/chromium/src/+/HEAD/docs/workflow/debugging-with-swarming.md#authenticating'
299   )
300
301   print('Uploading to isolate server, this can take a while...')
302   isolate = os.path.join(args.out_dir, args.target_name + '.isolate')
303   archive_json = os.path.join(args.out_dir, args.target_name + '.archive.json')
304   subprocess.check_output([
305       'tools/luci-go/isolate', 'archive', '-cas-instance', 'chromium-swarm',
306       '-isolate', isolate, '-dump-json', archive_json
307   ])
308   with open(archive_json) as f:
309     cas_digest = json.load(f).get(args.target_name)
310
311   mb_cmd = [
312       sys.executable, 'tools/mb/mb.py', 'get-swarming-command', '--as-list'
313   ]
314   if not args.build:
315     mb_cmd.append('--no-build')
316   if args.isolate_map_file:
317     mb_cmd += ['--isolate-map-file', args.isolate_map_file]
318   mb_cmd += ['//' + args.out_dir, args.target_name]
319   mb_output = subprocess.check_output(mb_cmd, shell=os.name == 'nt')
320   swarming_cmd = json.loads(mb_output)
321
322   if os.path.isdir(args.results):
323     shutil.rmtree(args.results)
324   os.makedirs(args.results)
325
326   try:
327     print('Triggering %d tasks...' % args.copies)
328     # Use dummy since threadpools give better exception messages
329     # than process pools do, and threads work fine for what we're doing.
330     pool = multiprocessing.dummy.Pool()
331     spawn_args = [(i, args, cas_digest, swarming_cmd)
332                   for i in range(args.copies)]
333     spawn_results = pool.imap_unordered(_Spawn, spawn_args)
334
335     exit_codes = []
336     collect_results = pool.imap_unordered(_Collect, spawn_results)
337     for result in collect_results:
338       exit_codes.append(result)
339       successes = sum(1 for x in exit_codes if x == 0)
340       errors = sum(1 for x in exit_codes if x == INTERNAL_ERROR_EXIT_CODE)
341       failures = len(exit_codes) - successes - errors
342       clear_to_eol = '\033[K'
343       print(
344           '\r[%d/%d] collected: '
345           '%d successes, %d failures, %d bot errors...%s' %
346           (len(exit_codes), args.copies, successes, failures, errors,
347            clear_to_eol),
348           end=' ')
349       sys.stdout.flush()
350
351     print()
352     print('Results logs collected into', os.path.abspath(args.results) + '.')
353   finally:
354     pool.close()
355     pool.join()
356   return 0
357
358
359 if __name__ == '__main__':
360   sys.exit(main())