blob: eaae55205babfad1731a8c9f557854fd84e0b103 [file] [log] [blame]
Kenneth Russelleb60cbd22017-12-05 07:54:281#!/usr/bin/env python
2# Copyright 2016 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Script to generate the majority of the JSON files in the src/testing/buildbot
7directory. Maintaining these files by hand is too unwieldy.
8"""
9
10import argparse
11import ast
12import collections
13import copy
John Budorick826d5ed2017-12-28 19:27:3214import difflib
Kenneth Russell8ceeabf2017-12-11 17:53:2815import itertools
Kenneth Russelleb60cbd22017-12-05 07:54:2816import json
17import os
18import string
19import sys
John Budorick826d5ed2017-12-28 19:27:3220import traceback
Kenneth Russelleb60cbd22017-12-05 07:54:2821
22THIS_DIR = os.path.dirname(os.path.abspath(__file__))
23
24
25class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4926 def __init__(self, message):
27 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2828
29
Kenneth Russell8ceeabf2017-12-11 17:53:2830# This class is only present to accommodate certain machines on
31# chromium.android.fyi which run certain tests as instrumentation
32# tests, but not as gtests. If this discrepancy were fixed then the
33# notion could be removed.
34class TestSuiteTypes(object):
35 GTEST = 'gtest'
36
37
Kenneth Russelleb60cbd22017-12-05 07:54:2838class BaseGenerator(object):
39 def __init__(self, bb_gen):
40 self.bb_gen = bb_gen
41
Kenneth Russell8ceeabf2017-12-11 17:53:2842 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2843 raise NotImplementedError()
44
45 def sort(self, tests):
46 raise NotImplementedError()
47
48
Kenneth Russell8ceeabf2017-12-11 17:53:2849def cmp_tests(a, b):
50 # Prefer to compare based on the "test" key.
51 val = cmp(a['test'], b['test'])
52 if val != 0:
53 return val
54 if 'name' in a and 'name' in b:
55 return cmp(a['name'], b['name']) # pragma: no cover
56 if 'name' not in a and 'name' not in b:
57 return 0 # pragma: no cover
58 # Prefer to put variants of the same test after the first one.
59 if 'name' in a:
60 return 1
61 # 'name' is in b.
62 return -1 # pragma: no cover
63
64
Kenneth Russell8a386d42018-06-02 09:48:0165class GPUTelemetryTestGenerator(BaseGenerator):
Bo Liu555a0f92019-03-29 12:11:5666
67 def __init__(self, bb_gen, is_android_webview=False):
Kenneth Russell8a386d42018-06-02 09:48:0168 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5669 self._is_android_webview = is_android_webview
Kenneth Russell8a386d42018-06-02 09:48:0170
71 def generate(self, waterfall, tester_name, tester_config, input_tests):
72 isolated_scripts = []
73 for test_name, test_config in sorted(input_tests.iteritems()):
74 test = self.bb_gen.generate_gpu_telemetry_test(
Bo Liu555a0f92019-03-29 12:11:5675 waterfall, tester_name, tester_config, test_name, test_config,
76 self._is_android_webview)
Kenneth Russell8a386d42018-06-02 09:48:0177 if test:
78 isolated_scripts.append(test)
79 return isolated_scripts
80
81 def sort(self, tests):
82 return sorted(tests, key=lambda x: x['name'])
83
84
Kenneth Russelleb60cbd22017-12-05 07:54:2885class GTestGenerator(BaseGenerator):
86 def __init__(self, bb_gen):
87 super(GTestGenerator, self).__init__(bb_gen)
88
Kenneth Russell8ceeabf2017-12-11 17:53:2889 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2890 # The relative ordering of some of the tests is important to
91 # minimize differences compared to the handwritten JSON files, since
92 # Python's sorts are stable and there are some tests with the same
93 # key (see gles2_conform_d3d9_test and similar variants). Avoid
94 # losing the order by avoiding coalescing the dictionaries into one.
95 gtests = []
96 for test_name, test_config in sorted(input_tests.iteritems()):
Nico Weber79dc5f6852018-07-13 19:38:4997 test = self.bb_gen.generate_gtest(
98 waterfall, tester_name, tester_config, test_name, test_config)
99 if test:
100 # generate_gtest may veto the test generation on this tester.
101 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28102 return gtests
103
104 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28105 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28106
107
108class IsolatedScriptTestGenerator(BaseGenerator):
109 def __init__(self, bb_gen):
110 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
111
Kenneth Russell8ceeabf2017-12-11 17:53:28112 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28113 isolated_scripts = []
114 for test_name, test_config in sorted(input_tests.iteritems()):
115 test = self.bb_gen.generate_isolated_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28116 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28117 if test:
118 isolated_scripts.append(test)
119 return isolated_scripts
120
121 def sort(self, tests):
122 return sorted(tests, key=lambda x: x['name'])
123
124
125class ScriptGenerator(BaseGenerator):
126 def __init__(self, bb_gen):
127 super(ScriptGenerator, self).__init__(bb_gen)
128
Kenneth Russell8ceeabf2017-12-11 17:53:28129 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28130 scripts = []
131 for test_name, test_config in sorted(input_tests.iteritems()):
132 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28133 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28134 if test:
135 scripts.append(test)
136 return scripts
137
138 def sort(self, tests):
139 return sorted(tests, key=lambda x: x['name'])
140
141
142class JUnitGenerator(BaseGenerator):
143 def __init__(self, bb_gen):
144 super(JUnitGenerator, self).__init__(bb_gen)
145
Kenneth Russell8ceeabf2017-12-11 17:53:28146 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28147 scripts = []
148 for test_name, test_config in sorted(input_tests.iteritems()):
149 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28150 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28151 if test:
152 scripts.append(test)
153 return scripts
154
155 def sort(self, tests):
156 return sorted(tests, key=lambda x: x['test'])
157
158
159class CTSGenerator(BaseGenerator):
160 def __init__(self, bb_gen):
161 super(CTSGenerator, self).__init__(bb_gen)
162
Kenneth Russell8ceeabf2017-12-11 17:53:28163 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28164 # These only contain one entry and it's the contents of the input tests'
165 # dictionary, verbatim.
166 cts_tests = []
167 cts_tests.append(input_tests)
168 return cts_tests
169
170 def sort(self, tests):
171 return tests
172
173
174class InstrumentationTestGenerator(BaseGenerator):
175 def __init__(self, bb_gen):
176 super(InstrumentationTestGenerator, self).__init__(bb_gen)
177
Kenneth Russell8ceeabf2017-12-11 17:53:28178 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28179 scripts = []
180 for test_name, test_config in sorted(input_tests.iteritems()):
181 test = self.bb_gen.generate_instrumentation_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28182 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28183 if test:
184 scripts.append(test)
185 return scripts
186
187 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28188 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28189
190
191class BBJSONGenerator(object):
192 def __init__(self):
193 self.this_dir = THIS_DIR
194 self.args = None
195 self.waterfalls = None
196 self.test_suites = None
197 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01198 self.mixins = None
Kenneth Russelleb60cbd22017-12-05 07:54:28199
200 def generate_abs_file_path(self, relative_path):
201 return os.path.join(self.this_dir, relative_path) # pragma: no cover
202
Stephen Martinis7eb8b612018-09-21 00:17:50203 def print_line(self, line):
204 # Exists so that tests can mock
205 print line # pragma: no cover
206
Kenneth Russelleb60cbd22017-12-05 07:54:28207 def read_file(self, relative_path):
208 with open(self.generate_abs_file_path(
209 relative_path)) as fp: # pragma: no cover
210 return fp.read() # pragma: no cover
211
212 def write_file(self, relative_path, contents):
213 with open(self.generate_abs_file_path(
214 relative_path), 'wb') as fp: # pragma: no cover
215 fp.write(contents) # pragma: no cover
216
Zhiling Huangbe008172018-03-08 19:13:11217 def pyl_file_path(self, filename):
218 if self.args and self.args.pyl_files_dir:
219 return os.path.join(self.args.pyl_files_dir, filename)
220 return filename
221
Kenneth Russelleb60cbd22017-12-05 07:54:28222 def load_pyl_file(self, filename):
223 try:
Zhiling Huangbe008172018-03-08 19:13:11224 return ast.literal_eval(self.read_file(
225 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28226 except (SyntaxError, ValueError) as e: # pragma: no cover
227 raise BBGenErr('Failed to parse pyl file "%s": %s' %
228 (filename, e)) # pragma: no cover
229
Kenneth Russell8a386d42018-06-02 09:48:01230 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
231 # Currently it is only mandatory for bots which run GPU tests. Change these to
232 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28233 def is_android(self, tester_config):
234 return tester_config.get('os_type') == 'android'
235
Ben Pastenea9e583b2019-01-16 02:57:26236 def is_chromeos(self, tester_config):
237 return tester_config.get('os_type') == 'chromeos'
238
Kenneth Russell8a386d42018-06-02 09:48:01239 def is_linux(self, tester_config):
240 return tester_config.get('os_type') == 'linux'
241
Kenneth Russelleb60cbd22017-12-05 07:54:28242 def get_exception_for_test(self, test_name, test_config):
243 # gtests may have both "test" and "name" fields, and usually, if the "name"
244 # field is specified, it means that the same test is being repurposed
245 # multiple times with different command line arguments. To handle this case,
246 # prefer to lookup per the "name" field of the test itself, as opposed to
247 # the "test_name", which is actually the "test" field.
248 if 'name' in test_config:
249 return self.exceptions.get(test_config['name'])
250 else:
251 return self.exceptions.get(test_name)
252
Nico Weberb0b3f5862018-07-13 18:45:15253 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28254 # Currently, the only reason a test should not run on a given tester is that
255 # it's in the exceptions. (Once the GPU waterfall generation script is
256 # incorporated here, the rules will become more complex.)
257 exception = self.get_exception_for_test(test_name, test_config)
258 if not exception:
259 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28260 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28261 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28262 if remove_from:
263 if tester_name in remove_from:
264 return False
265 # TODO(kbr): this code path was added for some tests (including
266 # android_webview_unittests) on one machine (Nougat Phone
267 # Tester) which exists with the same name on two waterfalls,
268 # chromium.android and chromium.fyi; the tests are run on one
269 # but not the other. Once the bots are all uniquely named (a
270 # different ongoing project) this code should be removed.
271 # TODO(kbr): add coverage.
272 return (tester_name + ' ' + waterfall['name']
273 not in remove_from) # pragma: no cover
274 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28275
Nico Weber79dc5f6852018-07-13 19:38:49276 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28277 exception = self.get_exception_for_test(test_name, test)
278 if not exception:
279 return None
Nico Weber79dc5f6852018-07-13 19:38:49280 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28281
Brian Sheedye6ea0ee2019-07-11 02:54:37282 def get_test_replacements(self, test, test_name, tester_name):
283 exception = self.get_exception_for_test(test_name, test)
284 if not exception:
285 return None
286 return exception.get('replacements', {}).get(tester_name)
287
Kenneth Russell8a386d42018-06-02 09:48:01288 def merge_command_line_args(self, arr, prefix, splitter):
289 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01290 idx = 0
291 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01292 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01293 while idx < len(arr):
294 flag = arr[idx]
295 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01296 if flag.startswith(prefix):
297 arg = flag[prefix_len:]
298 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01299 if first_idx < 0:
300 first_idx = idx
301 else:
302 delete_current_entry = True
303 if delete_current_entry:
304 del arr[idx]
305 else:
306 idx += 1
307 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01308 arr[first_idx] = prefix + splitter.join(accumulated_args)
309 return arr
310
311 def maybe_fixup_args_array(self, arr):
312 # The incoming array of strings may be an array of command line
313 # arguments. To make it easier to turn on certain features per-bot or
314 # per-test-suite, look specifically for certain flags and merge them
315 # appropriately.
316 # --enable-features=Feature1 --enable-features=Feature2
317 # are merged to:
318 # --enable-features=Feature1,Feature2
319 # and:
320 # --extra-browser-args=arg1 --extra-browser-args=arg2
321 # are merged to:
322 # --extra-browser-args=arg1 arg2
323 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
324 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01325 return arr
326
Kenneth Russelleb60cbd22017-12-05 07:54:28327 def dictionary_merge(self, a, b, path=None, update=True):
328 """http://stackoverflow.com/questions/7204805/
329 python-dictionaries-of-dictionaries-merge
330 merges b into a
331 """
332 if path is None:
333 path = []
334 for key in b:
335 if key in a:
336 if isinstance(a[key], dict) and isinstance(b[key], dict):
337 self.dictionary_merge(a[key], b[key], path + [str(key)])
338 elif a[key] == b[key]:
339 pass # same leaf value
340 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06341 # Args arrays are lists of strings. Just concatenate them,
342 # and don't sort them, in order to keep some needed
343 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28344 if all(isinstance(x, str)
345 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01346 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28347 else:
348 # TODO(kbr): this only works properly if the two arrays are
349 # the same length, which is currently always the case in the
350 # swarming dimension_sets that we have to merge. It will fail
351 # to merge / override 'args' arrays which are different
352 # length.
353 for idx in xrange(len(b[key])):
354 try:
355 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
356 path + [str(key), str(idx)],
357 update=update)
358 except (IndexError, TypeError): # pragma: no cover
359 raise BBGenErr('Error merging list keys ' + str(key) +
360 ' and indices ' + str(idx) + ' between ' +
361 str(a) + ' and ' + str(b)) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53362 elif update:
363 if b[key] is None:
364 del a[key]
365 else:
366 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28367 else:
368 raise BBGenErr('Conflict at %s' % '.'.join(
369 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53370 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28371 a[key] = b[key]
372 return a
373
John Budorickab108712018-09-01 00:12:21374 def initialize_args_for_test(
375 self, generated_test, tester_config, additional_arg_keys=None):
376
377 args = []
378 args.extend(generated_test.get('args', []))
379 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22380
Kenneth Russell8a386d42018-06-02 09:48:01381 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21382 val = generated_test.pop(key, [])
383 if fn(tester_config):
384 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01385
386 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
387 add_conditional_args('linux_args', self.is_linux)
388 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36389 add_conditional_args('chromeos_args', self.is_chromeos)
Kenneth Russell8a386d42018-06-02 09:48:01390
John Budorickab108712018-09-01 00:12:21391 for key in additional_arg_keys or []:
392 args.extend(generated_test.pop(key, []))
393 args.extend(tester_config.get(key, []))
394
395 if args:
396 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01397
Kenneth Russelleb60cbd22017-12-05 07:54:28398 def initialize_swarming_dictionary_for_test(self, generated_test,
399 tester_config):
400 if 'swarming' not in generated_test:
401 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28402 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
403 generated_test['swarming'].update({
404 'can_use_on_swarming_builders': tester_config.get('use_swarming', True)
405 })
Kenneth Russelleb60cbd22017-12-05 07:54:28406 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03407 if ('dimension_sets' not in generated_test['swarming'] and
408 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28409 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
410 tester_config['swarming']['dimension_sets'])
411 self.dictionary_merge(generated_test['swarming'],
412 tester_config['swarming'])
413 # Apply any Android-specific Swarming dimensions after the generic ones.
414 if 'android_swarming' in generated_test:
415 if self.is_android(tester_config): # pragma: no cover
416 self.dictionary_merge(
417 generated_test['swarming'],
418 generated_test['android_swarming']) # pragma: no cover
419 del generated_test['android_swarming'] # pragma: no cover
420
421 def clean_swarming_dictionary(self, swarming_dict):
422 # Clean out redundant entries from a test's "swarming" dictionary.
423 # This is really only needed to retain 100% parity with the
424 # handwritten JSON files, and can be removed once all the files are
425 # autogenerated.
426 if 'shards' in swarming_dict:
427 if swarming_dict['shards'] == 1: # pragma: no cover
428 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24429 if 'hard_timeout' in swarming_dict:
430 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
431 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43432 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28433 # Remove all other keys.
434 for k in swarming_dict.keys(): # pragma: no cover
435 if k != 'can_use_on_swarming_builders': # pragma: no cover
436 del swarming_dict[k] # pragma: no cover
437
Stephen Martinis0382bc12018-09-17 22:29:07438 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
439 waterfall):
440 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01441 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07442 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28443 # See if there are any exceptions that need to be merged into this
444 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49445 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28446 if modifications:
447 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23448 if 'swarming' in test:
449 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28450 # Ensure all Android Swarming tests run only on userdebug builds if another
451 # build type was not specified.
452 if 'swarming' in test and self.is_android(tester_config):
453 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22454 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28455 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37456 self.replace_test_args(test, test_name, tester_name)
Ben Pastenee012aea42019-05-14 22:32:28457
Kenneth Russelleb60cbd22017-12-05 07:54:28458 return test
459
Brian Sheedye6ea0ee2019-07-11 02:54:37460 def replace_test_args(self, test, test_name, tester_name):
461 replacements = self.get_test_replacements(
462 test, test_name, tester_name) or {}
463 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
464 for key, replacement_dict in replacements.iteritems():
465 if key not in valid_replacement_keys:
466 raise BBGenErr(
467 'Given replacement key %s for %s on %s is not in the list of valid '
468 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
469 for replacement_key, replacement_val in replacement_dict.iteritems():
470 found_key = False
471 for i, test_key in enumerate(test.get(key, [])):
472 # Handle both the key/value being replaced being defined as two
473 # separate items or as key=value.
474 if test_key == replacement_key:
475 found_key = True
476 # Handle flags without values.
477 if replacement_val == None:
478 del test[key][i]
479 else:
480 test[key][i+1] = replacement_val
481 break
482 elif test_key.startswith(replacement_key + '='):
483 found_key = True
484 if replacement_val == None:
485 del test[key][i]
486 else:
487 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
488 break
489 if not found_key:
490 raise BBGenErr('Could not find %s in existing list of values for key '
491 '%s in %s on %s' % (replacement_key, key, test_name,
492 tester_name))
493
Shenghua Zhangaba8bad2018-02-07 02:12:09494 def add_common_test_properties(self, test, tester_config):
495 if tester_config.get('use_multi_dimension_trigger_script'):
Kenneth Russell73c3bd8b2018-10-19 22:30:19496 # Assumes update_and_cleanup_test has already been called, so the
497 # builder's mixins have been flattened into the test.
Shenghua Zhangaba8bad2018-02-07 02:12:09498 test['trigger_script'] = {
499 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
500 'args': [
501 '--multiple-trigger-configs',
Kenneth Russell73c3bd8b2018-10-19 22:30:19502 json.dumps(test['swarming']['dimension_sets'] +
Shenghua Zhangaba8bad2018-02-07 02:12:09503 tester_config.get('alternate_swarming_dimensions', [])),
504 '--multiple-dimension-script-verbose',
505 'True'
506 ],
507 }
Ben Pastenea9e583b2019-01-16 02:57:26508 elif self.is_chromeos(tester_config) and tester_config.get('use_swarming',
509 True):
510 # The presence of the "device_type" dimension indicates that the tests
511 # are targetting CrOS hardware and so need the special trigger script.
512 dimension_sets = tester_config['swarming']['dimension_sets']
513 if all('device_type' in ds for ds in dimension_sets):
514 test['trigger_script'] = {
515 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
516 }
Shenghua Zhangaba8bad2018-02-07 02:12:09517
Ben Pastene858f4be2019-01-09 23:52:09518 def add_android_presentation_args(self, tester_config, test_name, result):
519 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38520 bucket = tester_config.get('results_bucket', 'chromium-result-details')
521 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09522 if (result['swarming']['can_use_on_swarming_builders'] and not
523 tester_config.get('skip_merge_script', False)):
524 result['merge'] = {
525 'args': [
526 '--bucket',
John Budorick262ae112019-07-12 19:24:38527 bucket,
Ben Pastene858f4be2019-01-09 23:52:09528 '--test-name',
529 test_name
530 ],
531 'script': '//build/android/pylib/results/presentation/'
532 'test_results_presentation.py',
533 }
534 if not tester_config.get('skip_cipd_packages', False):
Ben Pastenee5949ea82019-01-10 21:45:26535 cipd_packages = result['swarming'].get('cipd_packages', [])
536 cipd_packages.append(
Ben Pastene858f4be2019-01-09 23:52:09537 {
538 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
539 'location': 'bin',
540 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
541 }
Ben Pastenee5949ea82019-01-10 21:45:26542 )
543 result['swarming']['cipd_packages'] = cipd_packages
Ben Pastene858f4be2019-01-09 23:52:09544 if not tester_config.get('skip_output_links', False):
545 result['swarming']['output_links'] = [
546 {
547 'link': [
548 'https://luci-logdog.appspot.com/v/?s',
549 '=android%2Fswarming%2Flogcats%2F',
550 '${TASK_ID}%2F%2B%2Funified_logcats',
551 ],
552 'name': 'shard #${SHARD_INDEX} logcats',
553 },
554 ]
555 if args:
556 result['args'] = args
557
Kenneth Russelleb60cbd22017-12-05 07:54:28558 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
559 test_config):
560 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15561 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28562 return None
563 result = copy.deepcopy(test_config)
564 if 'test' in result:
565 result['name'] = test_name
566 else:
567 result['test'] = test_name
568 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21569
570 self.initialize_args_for_test(
571 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28572 if self.is_android(tester_config) and tester_config.get('use_swarming',
573 True):
Ben Pastene858f4be2019-01-09 23:52:09574 self.add_android_presentation_args(tester_config, test_name, result)
575 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42576
Stephen Martinis0382bc12018-09-17 22:29:07577 result = self.update_and_cleanup_test(
578 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09579 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43580
581 if not result.get('merge'):
582 # TODO(https://crbug.com/958376): Consider adding the ability to not have
583 # this default.
584 result['merge'] = {
585 'script': '//testing/merge_scripts/standard_gtest_merge.py',
586 'args': [],
587 }
Kenneth Russelleb60cbd22017-12-05 07:54:28588 return result
589
590 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
591 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01592 if not self.should_run_on_tester(waterfall, tester_name, test_name,
593 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28594 return None
595 result = copy.deepcopy(test_config)
596 result['isolate_name'] = result.get('isolate_name', test_name)
597 result['name'] = test_name
598 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01599 self.initialize_args_for_test(result, tester_config)
Ben Pastene858f4be2019-01-09 23:52:09600 if tester_config.get('use_android_presentation', False):
601 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07602 result = self.update_and_cleanup_test(
603 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09604 self.add_common_test_properties(result, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17605
606 if not result.get('merge'):
607 # TODO(https://crbug.com/958376): Consider adding the ability to not have
608 # this default.
609 result['merge'] = {
610 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
611 'args': [],
612 }
Kenneth Russelleb60cbd22017-12-05 07:54:28613 return result
614
615 def generate_script_test(self, waterfall, tester_name, tester_config,
616 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44617 # TODO(https://crbug.com/953072): Remove this check whenever a better
618 # long-term solution is implemented.
619 if (waterfall.get('forbid_script_tests', False) or
620 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
621 raise BBGenErr('Attempted to generate a script test on tester ' +
622 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01623 if not self.should_run_on_tester(waterfall, tester_name, test_name,
624 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28625 return None
626 result = {
627 'name': test_name,
628 'script': test_config['script']
629 }
Stephen Martinis0382bc12018-09-17 22:29:07630 result = self.update_and_cleanup_test(
631 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28632 return result
633
634 def generate_junit_test(self, waterfall, tester_name, tester_config,
635 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01636 del tester_config
637 if not self.should_run_on_tester(waterfall, tester_name, test_name,
638 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28639 return None
640 result = {
641 'test': test_name,
642 }
643 return result
644
645 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
646 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01647 if not self.should_run_on_tester(waterfall, tester_name, test_name,
648 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28649 return None
650 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28651 if 'test' in result and result['test'] != test_name:
652 result['name'] = test_name
653 else:
654 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07655 result = self.update_and_cleanup_test(
656 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28657 return result
658
Stephen Martinis2a0667022018-09-25 22:31:14659 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01660 substitutions = {
661 # Any machine in waterfalls.pyl which desires to run GPU tests
662 # must provide the os_type key.
663 'os_type': tester_config['os_type'],
664 'gpu_vendor_id': '0',
665 'gpu_device_id': '0',
666 }
Stephen Martinis2a0667022018-09-25 22:31:14667 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01668 if 'gpu' in dimension_set:
669 # First remove the driver version, then split into vendor and device.
670 gpu = dimension_set['gpu']
Kenneth Russell384a1732019-03-16 02:36:02671 # Handle certain specialized named GPUs.
672 if gpu.startswith('nvidia-quadro-p400'):
673 gpu = ['10de', '1cb3']
674 elif gpu.startswith('intel-hd-630'):
675 gpu = ['8086', '5912']
Brian Sheedyf9387db7b2019-08-05 19:26:10676 elif gpu.startswith('intel-uhd-630'):
677 gpu = ['8086', '3e92']
Kenneth Russell384a1732019-03-16 02:36:02678 else:
679 gpu = gpu.split('-')[0].split(':')
Kenneth Russell8a386d42018-06-02 09:48:01680 substitutions['gpu_vendor_id'] = gpu[0]
681 substitutions['gpu_device_id'] = gpu[1]
682 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
683
684 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56685 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01686 # These are all just specializations of isolated script tests with
687 # a bunch of boilerplate command line arguments added.
688
689 # The step name must end in 'test' or 'tests' in order for the
690 # results to automatically show up on the flakiness dashboard.
691 # (At least, this was true some time ago.) Continue to use this
692 # naming convention for the time being to minimize changes.
693 step_name = test_config.get('name', test_name)
694 if not (step_name.endswith('test') or step_name.endswith('tests')):
695 step_name = '%s_tests' % step_name
696 result = self.generate_isolated_script_test(
697 waterfall, tester_name, tester_config, step_name, test_config)
698 if not result:
699 return None
700 result['isolate_name'] = 'telemetry_gpu_integration_test'
701 args = result.get('args', [])
702 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14703
704 # These tests upload and download results from cloud storage and therefore
705 # aren't idempotent yet. https://crbug.com/549140.
706 result['swarming']['idempotent'] = False
707
Kenneth Russell44910c32018-12-03 23:35:11708 # The GPU tests act much like integration tests for the entire browser, and
709 # tend to uncover flakiness bugs more readily than other test suites. In
710 # order to surface any flakiness more readily to the developer of the CL
711 # which is introducing it, we disable retries with patch on the commit
712 # queue.
713 result['should_retry_with_patch'] = False
714
Bo Liu555a0f92019-03-29 12:11:56715 browser = ('android-webview-instrumentation'
716 if is_android_webview else tester_config['browser_config'])
Kenneth Russell8a386d42018-06-02 09:48:01717 args = [
Bo Liu555a0f92019-03-29 12:11:56718 test_to_run,
719 '--show-stdout',
720 '--browser=%s' % browser,
721 # --passthrough displays more of the logging in Telemetry when
722 # run via typ, in particular some of the warnings about tests
723 # being expected to fail, but passing.
724 '--passthrough',
725 '-v',
726 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
Kenneth Russell8a386d42018-06-02 09:48:01727 ] + args
728 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14729 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01730 return result
731
Kenneth Russelleb60cbd22017-12-05 07:54:28732 def get_test_generator_map(self):
733 return {
Bo Liu555a0f92019-03-29 12:11:56734 'android_webview_gpu_telemetry_tests':
735 GPUTelemetryTestGenerator(self, is_android_webview=True),
736 'cts_tests':
737 CTSGenerator(self),
738 'gpu_telemetry_tests':
739 GPUTelemetryTestGenerator(self),
740 'gtest_tests':
741 GTestGenerator(self),
742 'instrumentation_tests':
743 InstrumentationTestGenerator(self),
744 'isolated_scripts':
745 IsolatedScriptTestGenerator(self),
746 'junit_tests':
747 JUnitGenerator(self),
748 'scripts':
749 ScriptGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28750 }
751
Kenneth Russell8a386d42018-06-02 09:48:01752 def get_test_type_remapper(self):
753 return {
754 # These are a specialization of isolated_scripts with a bunch of
755 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:56756 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:01757 'gpu_telemetry_tests': 'isolated_scripts',
758 }
759
Kenneth Russelleb60cbd22017-12-05 07:54:28760 def check_composition_test_suites(self):
761 # Pre-pass to catch errors reliably.
Andrew Luo0f1dee02019-09-06 16:50:47762 for suite, suite_def in self.test_suites.iteritems():
763 if isinstance(suite_def, list):
764 seen_tests = {}
765 for sub_suite in suite_def:
766 if isinstance(self.test_suites[sub_suite], list):
Nico Weberd18b8962018-05-16 19:39:38767 raise BBGenErr('Composition test suites may not refer to other '
768 'composition test suites (error found while '
Andrew Luo0f1dee02019-09-06 16:50:47769 'processing %s)' % suite)
770 else:
771 # test name -> basic_suite that it came from
772 basic_tests = {k: sub_suite for k in self.test_suites[sub_suite]}
773 for test_name, test_suite in basic_tests.iteritems():
774 if (test_name in seen_tests and
775 self.test_suites[test_suite][test_name] !=
776 self.test_suites[seen_tests[test_name]][test_name]):
777 raise BBGenErr('Conflicting test definitions for %s from %s '
778 'and %s in Composition test suite (error found '
779 'while processing %s)' % (test_name,
780 seen_tests[test_name], test_suite, suite))
781 seen_tests.update(basic_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28782
Stephen Martinis54d64ad2018-09-21 22:16:20783 def flatten_test_suites(self):
784 new_test_suites = {}
785 for name, value in self.test_suites.get('basic_suites', {}).iteritems():
786 new_test_suites[name] = value
787 for name, value in self.test_suites.get('compound_suites', {}).iteritems():
788 if name in new_test_suites:
789 raise BBGenErr('Composition test suite names may not duplicate basic '
790 'test suite names (error found while processsing %s' % (
791 name))
792 new_test_suites[name] = value
793 self.test_suites = new_test_suites
794
Kenneth Russelleb60cbd22017-12-05 07:54:28795 def resolve_composition_test_suites(self):
Stephen Martinis54d64ad2018-09-21 22:16:20796 self.flatten_test_suites()
797
Kenneth Russelleb60cbd22017-12-05 07:54:28798 self.check_composition_test_suites()
799 for name, value in self.test_suites.iteritems():
800 if isinstance(value, list):
801 # Resolve this to a dictionary.
802 full_suite = {}
803 for entry in value:
804 suite = self.test_suites[entry]
805 full_suite.update(suite)
806 self.test_suites[name] = full_suite
807
808 def link_waterfalls_to_test_suites(self):
809 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43810 for tester_name, tester in waterfall['machines'].iteritems():
811 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28812 if not value in self.test_suites:
813 # Hard / impossible to cover this in the unit test.
814 raise self.unknown_test_suite(
815 value, tester_name, waterfall['name']) # pragma: no cover
816 tester['test_suites'][suite] = self.test_suites[value]
817
818 def load_configuration_files(self):
819 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
820 self.test_suites = self.load_pyl_file('test_suites.pyl')
821 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:01822 self.mixins = self.load_pyl_file('mixins.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:28823
824 def resolve_configuration_files(self):
825 self.resolve_composition_test_suites()
826 self.link_waterfalls_to_test_suites()
827
Nico Weberd18b8962018-05-16 19:39:38828 def unknown_bot(self, bot_name, waterfall_name):
829 return BBGenErr(
830 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
831
Kenneth Russelleb60cbd22017-12-05 07:54:28832 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
833 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38834 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28835 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
836
837 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
838 return BBGenErr(
839 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
840 ' on waterfall ' + waterfall_name)
841
Stephen Martinisb72f6d22018-10-04 23:29:01842 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:07843 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:32844
845 Checks in the waterfall, builder, and test objects for mixins.
846 """
847 def valid_mixin(mixin_name):
848 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:01849 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:32850 raise BBGenErr("bad mixin %s" % mixin_name)
851 def must_be_list(mixins, typ, name):
852 """Asserts that given mixins are a list."""
853 if not isinstance(mixins, list):
854 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
855
Stephen Martinisb72f6d22018-10-04 23:29:01856 if 'mixins' in waterfall:
857 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
858 for mixin in waterfall['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32859 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01860 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32861
Stephen Martinisb72f6d22018-10-04 23:29:01862 if 'mixins' in builder:
863 must_be_list(builder['mixins'], 'builder', builder_name)
864 for mixin in builder['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32865 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01866 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32867
Stephen Martinisb72f6d22018-10-04 23:29:01868 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:07869 return test
870
Stephen Martinis2a0667022018-09-25 22:31:14871 test_name = test.get('name')
872 if not test_name:
873 test_name = test.get('test')
874 if not test_name: # pragma: no cover
875 # Not the best name, but we should say something.
876 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:01877 must_be_list(test['mixins'], 'test', test_name)
878 for mixin in test['mixins']:
Stephen Martinis0382bc12018-09-17 22:29:07879 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01880 test = self.apply_mixin(self.mixins[mixin], test)
881 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:07882 return test
Stephen Martinisb6a50492018-09-12 23:59:32883
Stephen Martinisb72f6d22018-10-04 23:29:01884 def apply_mixin(self, mixin, test):
885 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:32886
Stephen Martinis0382bc12018-09-17 22:29:07887 Mixins will not override an existing key. This is to ensure exceptions can
888 override a setting a mixin applies.
889
Stephen Martinisb72f6d22018-10-04 23:29:01890 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:32891 'dimension_sets', which is how normal test suites specify their dimensions,
892 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
893 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:01894
Stephen Martinisb6a50492018-09-12 23:59:32895 """
896 new_test = copy.deepcopy(test)
897 mixin = copy.deepcopy(mixin)
898
Stephen Martinisb72f6d22018-10-04 23:29:01899 if 'swarming' in mixin:
900 swarming_mixin = mixin['swarming']
901 new_test.setdefault('swarming', {})
902 if 'dimensions' in swarming_mixin:
903 new_test['swarming'].setdefault('dimension_sets', [{}])
904 for dimension_set in new_test['swarming']['dimension_sets']:
905 dimension_set.update(swarming_mixin['dimensions'])
906 del swarming_mixin['dimensions']
Stephen Martinisb6a50492018-09-12 23:59:32907
Stephen Martinisb72f6d22018-10-04 23:29:01908 # python dict update doesn't do recursion at all. Just hard code the
909 # nested update we need (mixin['swarming'] shouldn't clobber
910 # test['swarming'], but should update it).
911 new_test['swarming'].update(swarming_mixin)
912 del mixin['swarming']
913
Wezc0e835b702018-10-30 00:38:41914 if '$mixin_append' in mixin:
915 # Values specified under $mixin_append should be appended to existing
916 # lists, rather than replacing them.
917 mixin_append = mixin['$mixin_append']
918 for key in mixin_append:
919 new_test.setdefault(key, [])
920 if not isinstance(mixin_append[key], list):
921 raise BBGenErr(
922 'Key "' + key + '" in $mixin_append must be a list.')
923 if not isinstance(new_test[key], list):
924 raise BBGenErr(
925 'Cannot apply $mixin_append to non-list "' + key + '".')
926 new_test[key].extend(mixin_append[key])
927 if 'args' in mixin_append:
928 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
929 del mixin['$mixin_append']
930
Stephen Martinisb72f6d22018-10-04 23:29:01931 new_test.update(mixin)
Stephen Martinis0382bc12018-09-17 22:29:07932
Stephen Martinisb6a50492018-09-12 23:59:32933 return new_test
934
Kenneth Russelleb60cbd22017-12-05 07:54:28935 def generate_waterfall_json(self, waterfall):
936 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28937 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:01938 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:43939 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28940 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43941 # Copy only well-understood entries in the machine's configuration
942 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28943 if 'additional_compile_targets' in config:
944 tests['additional_compile_targets'] = config[
945 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43946 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28947 if test_type not in generator_map:
948 raise self.unknown_test_suite_type(
949 test_type, name, waterfall['name']) # pragma: no cover
950 test_generator = generator_map[test_type]
Nico Weber79dc5f6852018-07-13 19:38:49951 # Let multiple kinds of generators generate the same kinds
952 # of tests. For example, gpu_telemetry_tests are a
953 # specialization of isolated_scripts.
954 new_tests = test_generator.generate(
955 waterfall, name, config, input_tests)
956 remapped_test_type = test_type_remapper.get(test_type, test_type)
957 tests[remapped_test_type] = test_generator.sort(
958 tests.get(remapped_test_type, []) + new_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28959 all_tests[name] = tests
960 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
961 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
962 return json.dumps(all_tests, indent=2, separators=(',', ': '),
963 sort_keys=True) + '\n'
964
965 def generate_waterfalls(self): # pragma: no cover
966 self.load_configuration_files()
967 self.resolve_configuration_files()
968 filters = self.args.waterfall_filters
969 suffix = '.json'
970 if self.args.new_files:
971 suffix = '.new' + suffix
972 for waterfall in self.waterfalls:
973 should_gen = not filters or waterfall['name'] in filters
974 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:11975 file_path = waterfall['name'] + suffix
976 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:28977 self.generate_waterfall_json(waterfall))
978
Nico Weberd18b8962018-05-16 19:39:38979 def get_valid_bot_names(self):
John Budorick699282e2019-02-13 01:27:33980 # Extract bot names from infra/config/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:42981 # NOTE: This reference can cause issues; if a file changes there, the
982 # presubmit here won't be run by default. A manually maintained list there
983 # tries to run presubmit here when luci-milo.cfg is changed. If any other
984 # references to configs outside of this directory are added, please change
985 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
986 # never ends up in an invalid state.
Nico Weberd18b8962018-05-16 19:39:38987 bot_names = set()
John Budorickc12abd12018-08-14 19:37:43988 infra_config_dir = os.path.abspath(
989 os.path.join(os.path.dirname(__file__),
John Budorick699282e2019-02-13 01:27:33990 '..', '..', 'infra', 'config'))
John Budorickc12abd12018-08-14 19:37:43991 milo_configs = [
992 os.path.join(infra_config_dir, 'luci-milo.cfg'),
993 os.path.join(infra_config_dir, 'luci-milo-dev.cfg'),
994 ]
995 for c in milo_configs:
996 for l in self.read_file(c).splitlines():
997 if (not 'name: "buildbucket/luci.chromium.' in l and
Hans Wennborg98ffd7d92019-02-06 14:14:34998 not 'name: "buildbucket/luci.chrome.' in l and
John Budorickb1833612018-12-07 04:36:41999 not 'name: "buildbot/chromium.' in l and
1000 not 'name: "buildbot/tryserver.chromium.' in l):
John Budorickc12abd12018-08-14 19:37:431001 continue
1002 # l looks like
1003 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1004 # Extract win_chromium_dbg_ng part.
1005 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381006 return bot_names
1007
Kenneth Russell8a386d42018-06-02 09:48:011008 def get_bots_that_do_not_actually_exist(self):
1009 # Some of the bots on the chromium.gpu.fyi waterfall in particular
1010 # are defined only to be mirrored into trybots, and don't actually
1011 # exist on any of the waterfalls or consoles.
1012 return [
Michael Spangeb07eba62019-05-14 22:22:581013 'GPU FYI Fuchsia Builder',
Yuly Novikoveb26b812019-07-26 02:08:191014 'ANGLE GPU Android Release (Nexus 5X)',
Jamie Madillda894ce2019-04-08 17:19:171015 'ANGLE GPU Linux Release (Intel HD 630)',
1016 'ANGLE GPU Linux Release (NVIDIA)',
1017 'ANGLE GPU Mac Release (Intel)',
1018 'ANGLE GPU Mac Retina Release (AMD)',
1019 'ANGLE GPU Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491020 'ANGLE GPU Win10 x64 Release (Intel HD 630)',
1021 'ANGLE GPU Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011022 'Optional Android Release (Nexus 5X)',
1023 'Optional Linux Release (Intel HD 630)',
1024 'Optional Linux Release (NVIDIA)',
1025 'Optional Mac Release (Intel)',
1026 'Optional Mac Retina Release (AMD)',
1027 'Optional Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491028 'Optional Win10 x64 Release (Intel HD 630)',
1029 'Optional Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011030 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:081031 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:291032 'linux-blink-rel-dummy',
1033 'mac10.10-blink-rel-dummy',
1034 'mac10.11-blink-rel-dummy',
1035 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:201036 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291037 'mac10.13-blink-rel-dummy',
1038 'win7-blink-rel-dummy',
1039 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:081040 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:331041 'WebKit Linux composite_after_paint Dummy Builder',
Scott Violet744e04662019-08-19 23:51:531042 'WebKit Linux layout_ng_disabled Builder',
Stephen Martinis769b25112018-08-30 18:52:061043 # chromium, due to https://crbug.com/878915
1044 'win-dbg',
1045 'win32-dbg',
Stephen Martinis47d77132019-04-24 23:51:331046 'win-archive-dbg',
1047 'win32-archive-dbg',
Stephen Martinis07a9f742019-03-20 19:16:561048 # chromium.mac, see https://crbug.com/943804
1049 'mac-dummy-rel',
Ben Pastene7687c0112019-03-05 22:43:141050 # Defined in internal configs.
1051 'chromeos-amd64-generic-google-rel',
Anushruth9420fddf2019-04-04 00:24:591052 'chromeos-betty-google-rel',
Anushruth549965302019-08-06 15:43:001053 'chromeos-kevin-google-rel',
Yuke Liaoa5d2f5a2019-09-04 00:26:071054 # code coverage, see https://crbug.com/1000367.
1055 'linux-chromeos-coverage-rel-dummy',
Kenneth Russell8a386d42018-06-02 09:48:011056 ]
1057
Stephen Martinisf83893722018-09-19 00:02:181058 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201059 self.check_input_files_sorting(verbose)
1060
Kenneth Russelleb60cbd22017-12-05 07:54:281061 self.load_configuration_files()
Stephen Martinis54d64ad2018-09-21 22:16:201062 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281063 self.check_composition_test_suites()
Nico Weberd18b8962018-05-16 19:39:381064
1065 # All bots should exist.
1066 bot_names = self.get_valid_bot_names()
Kenneth Russell8a386d42018-06-02 09:48:011067 bots_that_dont_exist = self.get_bots_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:381068 for waterfall in self.waterfalls:
1069 for bot_name in waterfall['machines']:
Kenneth Russell8a386d42018-06-02 09:48:011070 if bot_name in bots_that_dont_exist:
1071 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381072 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:081073 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:381074 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:521075 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:321076 if waterfall['name'] in ['tryserver.webrtc',
1077 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:381078 # These waterfalls have their bot configs in a different repo.
1079 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:521080 continue # pragma: no cover
Tamer Tas2c506412019-08-20 07:44:411081 if waterfall['name'] in ['client.devtools-frontend.integration']:
1082 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381083 raise self.unknown_bot(bot_name, waterfall['name'])
1084
Kenneth Russelleb60cbd22017-12-05 07:54:281085 # All test suites must be referenced.
1086 suites_seen = set()
1087 generator_map = self.get_test_generator_map()
1088 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431089 for bot_name, tester in waterfall['machines'].iteritems():
1090 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281091 if suite_type not in generator_map:
1092 raise self.unknown_test_suite_type(suite_type, bot_name,
1093 waterfall['name'])
1094 if suite not in self.test_suites:
1095 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1096 suites_seen.add(suite)
1097 # Since we didn't resolve the configuration files, this set
1098 # includes both composition test suites and regular ones.
1099 resolved_suites = set()
1100 for suite_name in suites_seen:
1101 suite = self.test_suites[suite_name]
1102 if isinstance(suite, list):
1103 for sub_suite in suite:
1104 resolved_suites.add(sub_suite)
1105 resolved_suites.add(suite_name)
1106 # At this point, every key in test_suites.pyl should be referenced.
1107 missing_suites = set(self.test_suites.keys()) - resolved_suites
1108 if missing_suites:
1109 raise BBGenErr('The following test suites were unreferenced by bots on '
1110 'the waterfalls: ' + str(missing_suites))
1111
1112 # All test suite exceptions must refer to bots on the waterfall.
1113 all_bots = set()
1114 missing_bots = set()
1115 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431116 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281117 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281118 # In order to disambiguate between bots with the same name on
1119 # different waterfalls, support has been added to various
1120 # exceptions for concatenating the waterfall name after the bot
1121 # name.
1122 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281123 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381124 removals = (exception.get('remove_from', []) +
1125 exception.get('remove_gtest_from', []) +
1126 exception.get('modifications', {}).keys())
1127 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281128 if removal not in all_bots:
1129 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411130
1131 missing_bots = missing_bots - set(bots_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281132 if missing_bots:
1133 raise BBGenErr('The following nonexistent machines were referenced in '
1134 'the test suite exceptions: ' + str(missing_bots))
1135
Stephen Martinis0382bc12018-09-17 22:29:071136 # All mixins must be referenced
1137 seen_mixins = set()
1138 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011139 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071140 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011141 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071142 for suite in self.test_suites.values():
1143 if isinstance(suite, list):
1144 # Don't care about this, it's a composition, which shouldn't include a
1145 # swarming mixin.
1146 continue
1147
1148 for test in suite.values():
1149 if not isinstance(test, dict):
1150 # Some test suites have top level keys, which currently can't be
1151 # swarming mixin entries. Ignore them
1152 continue
1153
Stephen Martinisb72f6d22018-10-04 23:29:011154 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071155
Stephen Martinisb72f6d22018-10-04 23:29:011156 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071157 if missing_mixins:
1158 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1159 ' referenced in a waterfall, machine, or test suite.' % (
1160 str(missing_mixins)))
1161
Stephen Martinis54d64ad2018-09-21 22:16:201162
1163 def type_assert(self, node, typ, filename, verbose=False):
1164 """Asserts that the Python AST node |node| is of type |typ|.
1165
1166 If verbose is set, it prints out some helpful context lines, showing where
1167 exactly the error occurred in the file.
1168 """
1169 if not isinstance(node, typ):
1170 if verbose:
1171 lines = [""] + self.read_file(filename).splitlines()
1172
1173 context = 2
1174 lines_start = max(node.lineno - context, 0)
1175 # Add one to include the last line
1176 lines_end = min(node.lineno + context, len(lines)) + 1
1177 lines = (
1178 ['== %s ==\n' % filename] +
1179 ["<snip>\n"] +
1180 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1181 lines[lines_start:lines_start + context])] +
1182 ['-' * 80 + '\n'] +
1183 ['%d %s' % (node.lineno, lines[node.lineno])] +
1184 ['-' * (node.col_offset + 3) + '^' + '-' * (
1185 80 - node.col_offset - 4) + '\n'] +
1186 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1187 lines[node.lineno + 1:lines_end])] +
1188 ["<snip>\n"]
1189 )
1190 # Print out a useful message when a type assertion fails.
1191 for l in lines:
1192 self.print_line(l.strip())
1193
1194 node_dumped = ast.dump(node, annotate_fields=False)
1195 # If the node is huge, truncate it so everything fits in a terminal
1196 # window.
1197 if len(node_dumped) > 60: # pragma: no cover
1198 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1199 raise BBGenErr(
1200 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1201 ' be %s, is %s' % (
1202 filename, node_dumped,
1203 node.lineno, typ, type(node)))
1204
1205 def ensure_ast_dict_keys_sorted(self, node, filename, verbose):
1206 is_valid = True
1207
1208 keys = []
1209 # The keys of this dict are ordered as ordered in the file; normal python
1210 # dictionary keys are given an arbitrary order, but since we parsed the
1211 # file itself, the order as given in the file is preserved.
1212 for key in node.keys:
1213 self.type_assert(key, ast.Str, filename, verbose)
1214 keys.append(key.s)
1215
1216 keys_sorted = sorted(keys)
1217 if keys_sorted != keys:
1218 is_valid = False
1219 if verbose:
1220 for line in difflib.unified_diff(
1221 keys,
1222 keys_sorted, fromfile='current (%r)' % filename, tofile='sorted'):
1223 self.print_line(line)
1224
1225 if len(set(keys)) != len(keys):
1226 for i in range(len(keys_sorted)-1):
1227 if keys_sorted[i] == keys_sorted[i+1]:
1228 self.print_line('Key %s is duplicated' % keys_sorted[i])
1229 is_valid = False
1230 return is_valid
Stephen Martinisf83893722018-09-19 00:02:181231
1232 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201233 # TODO(https://crbug.com/886993): Add the ability for this script to
1234 # actually format the files, rather than just complain if they're
1235 # incorrectly formatted.
1236 bad_files = set()
1237
1238 for filename in (
Stephen Martinisb72f6d22018-10-04 23:29:011239 'mixins.pyl',
Stephen Martinis54d64ad2018-09-21 22:16:201240 'test_suites.pyl',
1241 'test_suite_exceptions.pyl',
1242 ):
Stephen Martinisf83893722018-09-19 00:02:181243 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1244
Stephen Martinisf83893722018-09-19 00:02:181245 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201246 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181247 module = parsed.body
1248
1249 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201250 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181251 if len(module) != 1: # pragma: no cover
1252 raise BBGenErr('Invalid .pyl file %s' % filename)
1253 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201254 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181255
1256 # Value should be a dictionary.
1257 value = expr.value
Stephen Martinis54d64ad2018-09-21 22:16:201258 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181259
Stephen Martinis54d64ad2018-09-21 22:16:201260 if filename == 'test_suites.pyl':
1261 expected_keys = ['basic_suites', 'compound_suites']
1262 actual_keys = [node.s for node in value.keys]
1263 assert all(key in expected_keys for key in actual_keys), (
1264 'Invalid %r file; expected keys %r, got %r' % (
1265 filename, expected_keys, actual_keys))
1266 suite_dicts = [node for node in value.values]
1267 # Only two keys should mean only 1 or 2 values
1268 assert len(suite_dicts) <= 2
1269 for suite_group in suite_dicts:
1270 if not self.ensure_ast_dict_keys_sorted(
1271 suite_group, filename, verbose):
1272 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181273
Stephen Martinis54d64ad2018-09-21 22:16:201274 else:
1275 if not self.ensure_ast_dict_keys_sorted(
1276 value, filename, verbose):
1277 bad_files.add(filename)
1278
1279 # waterfalls.pyl is slightly different, just do it manually here
1280 filename = 'waterfalls.pyl'
1281 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1282
1283 # Must be a module.
1284 self.type_assert(parsed, ast.Module, filename, verbose)
1285 module = parsed.body
1286
1287 # Only one expression in the module.
1288 self.type_assert(module, list, filename, verbose)
1289 if len(module) != 1: # pragma: no cover
1290 raise BBGenErr('Invalid .pyl file %s' % filename)
1291 expr = module[0]
1292 self.type_assert(expr, ast.Expr, filename, verbose)
1293
1294 # Value should be a list.
1295 value = expr.value
1296 self.type_assert(value, ast.List, filename, verbose)
1297
1298 keys = []
1299 for val in value.elts:
1300 self.type_assert(val, ast.Dict, filename, verbose)
1301 waterfall_name = None
1302 for key, val in zip(val.keys, val.values):
1303 self.type_assert(key, ast.Str, filename, verbose)
1304 if key.s == 'machines':
1305 if not self.ensure_ast_dict_keys_sorted(val, filename, verbose):
1306 bad_files.add(filename)
1307
1308 if key.s == "name":
1309 self.type_assert(val, ast.Str, filename, verbose)
1310 waterfall_name = val.s
1311 assert waterfall_name
1312 keys.append(waterfall_name)
1313
1314 if sorted(keys) != keys:
1315 bad_files.add(filename)
1316 if verbose: # pragma: no cover
1317 for line in difflib.unified_diff(
1318 keys,
1319 sorted(keys), fromfile='current', tofile='sorted'):
1320 self.print_line(line)
Stephen Martinisf83893722018-09-19 00:02:181321
1322 if bad_files:
1323 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201324 'The following files have invalid keys: %s\n. They are either '
1325 'unsorted, or have duplicates.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181326
Kenneth Russelleb60cbd22017-12-05 07:54:281327 def check_output_file_consistency(self, verbose=False):
1328 self.load_configuration_files()
1329 # All waterfalls must have been written by this script already.
1330 self.resolve_configuration_files()
1331 ungenerated_waterfalls = set()
1332 for waterfall in self.waterfalls:
1333 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111334 file_path = waterfall['name'] + '.json'
1335 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281336 if expected != current:
1337 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321338 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501339 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281340 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321341 'contents:')
1342 for line in difflib.unified_diff(
1343 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501344 current.splitlines(),
1345 fromfile='expected', tofile='current'):
1346 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281347 if ungenerated_waterfalls:
1348 raise BBGenErr('The following waterfalls have not been properly '
1349 'autogenerated by generate_buildbot_json.py: ' +
1350 str(ungenerated_waterfalls))
1351
1352 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501353 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281354 self.check_output_file_consistency(verbose) # pragma: no cover
1355
1356 def parse_args(self, argv): # pragma: no cover
Karen Qiane24b7ee2019-02-12 23:37:061357
1358 # RawTextHelpFormatter allows for styling of help statement
1359 parser = argparse.ArgumentParser(formatter_class=
1360 argparse.RawTextHelpFormatter)
1361
1362 group = parser.add_mutually_exclusive_group()
1363 group.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281364 '-c', '--check', action='store_true', help=
1365 'Do consistency checks of configuration and generated files and then '
1366 'exit. Used during presubmit. Causes the tool to not generate any files.')
Karen Qiane24b7ee2019-02-12 23:37:061367 group.add_argument(
1368 '--query', type=str, help=
1369 ("Returns raw JSON information of buildbots and tests.\n" +
1370 "Examples:\n" +
1371 " List all bots (all info):\n" +
1372 " --query bots\n\n" +
1373 " List all bots and only their associated tests:\n" +
1374 " --query bots/tests\n\n" +
1375 " List all information about 'bot1' " +
1376 "(make sure you have quotes):\n" +
1377 " --query bot/'bot1'\n\n" +
1378 " List tests running for 'bot1' (make sure you have quotes):\n" +
1379 " --query bot/'bot1'/tests\n\n" +
1380 " List all tests:\n" +
1381 " --query tests\n\n" +
1382 " List all tests and the bots running them:\n" +
1383 " --query tests/bots\n\n"+
1384 " List all tests that satisfy multiple parameters\n" +
1385 " (separation of parameters by '&' symbol):\n" +
1386 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
1387 " List all tests that run with a specific flag:\n" +
1388 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
1389 " List specific test (make sure you have quotes):\n"
1390 " --query test/'test1'\n\n"
1391 " List all bots running 'test1' " +
1392 "(make sure you have quotes):\n" +
1393 " --query test/'test1'/bots" ))
Kenneth Russelleb60cbd22017-12-05 07:54:281394 parser.add_argument(
1395 '-n', '--new-files', action='store_true', help=
1396 'Write output files as .new.json. Useful during development so old and '
1397 'new files can be looked at side-by-side.')
1398 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501399 '-v', '--verbose', action='store_true', help=
1400 'Increases verbosity. Affects consistency checks.')
1401 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281402 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1403 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111404 parser.add_argument(
1405 '--pyl-files-dir', type=os.path.realpath,
1406 help='Path to the directory containing the input .pyl files.')
Karen Qiane24b7ee2019-02-12 23:37:061407 parser.add_argument(
1408 '--json', help=
1409 ("Outputs results into a json file. Only works with query function.\n" +
1410 "Examples:\n" +
1411 " Outputs file into specified json file: \n" +
1412 " --json <file-name-here.json>"))
Kenneth Russelleb60cbd22017-12-05 07:54:281413 self.args = parser.parse_args(argv)
Karen Qiane24b7ee2019-02-12 23:37:061414 if self.args.json and not self.args.query:
1415 parser.error("The --json flag can only be used with --query.")
1416
1417 def does_test_match(self, test_info, params_dict):
1418 """Checks to see if the test matches the parameters given.
1419
1420 Compares the provided test_info with the params_dict to see
1421 if the bot matches the parameters given. If so, returns True.
1422 Else, returns false.
1423
1424 Args:
1425 test_info (dict): Information about a specific bot provided
1426 in the format shown in waterfalls.pyl
1427 params_dict (dict): Dictionary of parameters and their values
1428 to look for in the bot
1429 Ex: {
1430 'device_os':'android',
1431 '--flag':True,
1432 'mixins': ['mixin1', 'mixin2'],
1433 'ex_key':'ex_value'
1434 }
1435
1436 """
1437 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
1438 'kvm', 'pool', 'integrity'] # dimension parameters
1439 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
1440 'can_use_on_swarming_builders']
1441 for param in params_dict:
1442 # if dimension parameter
1443 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
1444 if not 'swarming' in test_info:
1445 return False
1446 swarming = test_info['swarming']
1447 if param in SWARMING_PARAMS:
1448 if not param in swarming:
1449 return False
1450 if not str(swarming[param]) == params_dict[param]:
1451 return False
1452 else:
1453 if not 'dimension_sets' in swarming:
1454 return False
1455 d_set = swarming['dimension_sets']
1456 # only looking at the first dimension set
1457 if not param in d_set[0]:
1458 return False
1459 if not d_set[0][param] == params_dict[param]:
1460 return False
1461
1462 # if flag
1463 elif param.startswith('--'):
1464 if not 'args' in test_info:
1465 return False
1466 if not param in test_info['args']:
1467 return False
1468
1469 # not dimension parameter/flag/mixin
1470 else:
1471 if not param in test_info:
1472 return False
1473 if not test_info[param] == params_dict[param]:
1474 return False
1475 return True
1476 def error_msg(self, msg):
1477 """Prints an error message.
1478
1479 In addition to a catered error message, also prints
1480 out where the user can find more help. Then, program exits.
1481 """
1482 self.print_line(msg + (' If you need more information, ' +
1483 'please run with -h or --help to see valid commands.'))
1484 sys.exit(1)
1485
1486 def find_bots_that_run_test(self, test, bots):
1487 matching_bots = []
1488 for bot in bots:
1489 bot_info = bots[bot]
1490 tests = self.flatten_tests_for_bot(bot_info)
1491 for test_info in tests:
1492 test_name = ""
1493 if 'name' in test_info:
1494 test_name = test_info['name']
1495 elif 'test' in test_info:
1496 test_name = test_info['test']
1497 if not test_name == test:
1498 continue
1499 matching_bots.append(bot)
1500 return matching_bots
1501
1502 def find_tests_with_params(self, tests, params_dict):
1503 matching_tests = []
1504 for test_name in tests:
1505 test_info = tests[test_name]
1506 if not self.does_test_match(test_info, params_dict):
1507 continue
1508 if not test_name in matching_tests:
1509 matching_tests.append(test_name)
1510 return matching_tests
1511
1512 def flatten_waterfalls_for_query(self, waterfalls):
1513 bots = {}
1514 for waterfall in waterfalls:
1515 waterfall_json = json.loads(self.generate_waterfall_json(waterfall))
1516 for bot in waterfall_json:
1517 bot_info = waterfall_json[bot]
1518 if 'AAAAA' not in bot:
1519 bots[bot] = bot_info
1520 return bots
1521
1522 def flatten_tests_for_bot(self, bot_info):
1523 """Returns a list of flattened tests.
1524
1525 Returns a list of tests not grouped by test category
1526 for a specific bot.
1527 """
1528 TEST_CATS = self.get_test_generator_map().keys()
1529 tests = []
1530 for test_cat in TEST_CATS:
1531 if not test_cat in bot_info:
1532 continue
1533 test_cat_tests = bot_info[test_cat]
1534 tests = tests + test_cat_tests
1535 return tests
1536
1537 def flatten_tests_for_query(self, test_suites):
1538 """Returns a flattened dictionary of tests.
1539
1540 Returns a dictionary of tests associate with their
1541 configuration, not grouped by their test suite.
1542 """
1543 tests = {}
1544 for test_suite in test_suites.itervalues():
1545 for test in test_suite:
1546 test_info = test_suite[test]
1547 test_name = test
1548 if 'name' in test_info:
1549 test_name = test_info['name']
1550 tests[test_name] = test_info
1551 return tests
1552
1553 def parse_query_filter_params(self, params):
1554 """Parses the filter parameters.
1555
1556 Creates a dictionary from the parameters provided
1557 to filter the bot array.
1558 """
1559 params_dict = {}
1560 for p in params:
1561 # flag
1562 if p.startswith("--"):
1563 params_dict[p] = True
1564 else:
1565 pair = p.split(":")
1566 if len(pair) != 2:
1567 self.error_msg('Invalid command.')
1568 # regular parameters
1569 if pair[1].lower() == "true":
1570 params_dict[pair[0]] = True
1571 elif pair[1].lower() == "false":
1572 params_dict[pair[0]] = False
1573 else:
1574 params_dict[pair[0]] = pair[1]
1575 return params_dict
1576
1577 def get_test_suites_dict(self, bots):
1578 """Returns a dictionary of bots and their tests.
1579
1580 Returns a dictionary of bots and a list of their associated tests.
1581 """
1582 test_suite_dict = dict()
1583 for bot in bots:
1584 bot_info = bots[bot]
1585 tests = self.flatten_tests_for_bot(bot_info)
1586 test_suite_dict[bot] = tests
1587 return test_suite_dict
1588
1589 def output_query_result(self, result, json_file=None):
1590 """Outputs the result of the query.
1591
1592 If a json file parameter name is provided, then
1593 the result is output into the json file. If not,
1594 then the result is printed to the console.
1595 """
1596 output = json.dumps(result, indent=2)
1597 if json_file:
1598 self.write_file(json_file, output)
1599 else:
1600 self.print_line(output)
1601 return
1602
1603 def query(self, args):
1604 """Queries tests or bots.
1605
1606 Depending on the arguments provided, outputs a json of
1607 tests or bots matching the appropriate optional parameters provided.
1608 """
1609 # split up query statement
1610 query = args.query.split('/')
1611 self.load_configuration_files()
1612 self.resolve_configuration_files()
1613
1614 # flatten bots json
1615 tests = self.test_suites
1616 bots = self.flatten_waterfalls_for_query(self.waterfalls)
1617
1618 cmd_class = query[0]
1619
1620 # For queries starting with 'bots'
1621 if cmd_class == "bots":
1622 if len(query) == 1:
1623 return self.output_query_result(bots, args.json)
1624 # query with specific parameters
1625 elif len(query) == 2:
1626 if query[1] == 'tests':
1627 test_suites_dict = self.get_test_suites_dict(bots)
1628 return self.output_query_result(test_suites_dict, args.json)
1629 else:
1630 self.error_msg("This query should be in the format: bots/tests.")
1631
1632 else:
1633 self.error_msg("This query should have 0 or 1 '/', found %s instead."
1634 % str(len(query)-1))
1635
1636 # For queries starting with 'bot'
1637 elif cmd_class == "bot":
1638 if not len(query) == 2 and not len(query) == 3:
1639 self.error_msg("Command should have 1 or 2 '/', found %s instead."
1640 % str(len(query)-1))
1641 bot_id = query[1]
1642 if not bot_id in bots:
1643 self.error_msg("No bot named '" + bot_id + "' found.")
1644 bot_info = bots[bot_id]
1645 if len(query) == 2:
1646 return self.output_query_result(bot_info, args.json)
1647 if not query[2] == 'tests':
1648 self.error_msg("The query should be in the format:" +
1649 "bot/<bot-name>/tests.")
1650
1651 bot_tests = self.flatten_tests_for_bot(bot_info)
1652 return self.output_query_result(bot_tests, args.json)
1653
1654 # For queries starting with 'tests'
1655 elif cmd_class == "tests":
1656 if not len(query) == 1 and not len(query) == 2:
1657 self.error_msg("The query should have 0 or 1 '/', found %s instead."
1658 % str(len(query)-1))
1659 flattened_tests = self.flatten_tests_for_query(tests)
1660 if len(query) == 1:
1661 return self.output_query_result(flattened_tests, args.json)
1662
1663 # create params dict
1664 params = query[1].split('&')
1665 params_dict = self.parse_query_filter_params(params)
1666 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
1667 return self.output_query_result(matching_bots)
1668
1669 # For queries starting with 'test'
1670 elif cmd_class == "test":
1671 if not len(query) == 2 and not len(query) == 3:
1672 self.error_msg("The query should have 1 or 2 '/', found %s instead."
1673 % str(len(query)-1))
1674 test_id = query[1]
1675 if len(query) == 2:
1676 flattened_tests = self.flatten_tests_for_query(tests)
1677 for test in flattened_tests:
1678 if test == test_id:
1679 return self.output_query_result(flattened_tests[test], args.json)
1680 self.error_msg("There is no test named %s." % test_id)
1681 if not query[2] == 'bots':
1682 self.error_msg("The query should be in the format: " +
1683 "test/<test-name>/bots")
1684 bots_for_test = self.find_bots_that_run_test(test_id, bots)
1685 return self.output_query_result(bots_for_test)
1686
1687 else:
1688 self.error_msg("Your command did not match any valid commands." +
1689 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:281690
1691 def main(self, argv): # pragma: no cover
1692 self.parse_args(argv)
1693 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:501694 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:061695 elif self.args.query:
1696 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:281697 else:
1698 self.generate_waterfalls()
1699 return 0
1700
1701if __name__ == "__main__": # pragma: no cover
1702 generator = BBJSONGenerator()
John Budorick699282e2019-02-13 01:27:331703 sys.exit(generator.main(sys.argv[1:]))