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