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