blob: b6c25cab20a0aa4bca815d1782d1acd5bf62bc5e [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()):
Jeff Yoon67c3e832020-02-08 07:39:3897 # Variants allow more than one definition for a given test, and is defined
98 # in array format from resolve_variants().
99 if not isinstance(test_config, list):
100 test_config = [test_config]
101
102 for config in test_config:
103 test = self.bb_gen.generate_gtest(
104 waterfall, tester_name, tester_config, test_name, config)
105 if test:
106 # generate_gtest may veto the test generation on this tester.
107 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28108 return gtests
109
110 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28111 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28112
113
114class IsolatedScriptTestGenerator(BaseGenerator):
115 def __init__(self, bb_gen):
116 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
117
Kenneth Russell8ceeabf2017-12-11 17:53:28118 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28119 isolated_scripts = []
120 for test_name, test_config in sorted(input_tests.iteritems()):
121 test = self.bb_gen.generate_isolated_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28122 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28123 if test:
124 isolated_scripts.append(test)
125 return isolated_scripts
126
127 def sort(self, tests):
128 return sorted(tests, key=lambda x: x['name'])
129
130
131class ScriptGenerator(BaseGenerator):
132 def __init__(self, bb_gen):
133 super(ScriptGenerator, self).__init__(bb_gen)
134
Kenneth Russell8ceeabf2017-12-11 17:53:28135 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28136 scripts = []
137 for test_name, test_config in sorted(input_tests.iteritems()):
138 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28139 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28140 if test:
141 scripts.append(test)
142 return scripts
143
144 def sort(self, tests):
145 return sorted(tests, key=lambda x: x['name'])
146
147
148class JUnitGenerator(BaseGenerator):
149 def __init__(self, bb_gen):
150 super(JUnitGenerator, self).__init__(bb_gen)
151
Kenneth Russell8ceeabf2017-12-11 17:53:28152 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28153 scripts = []
154 for test_name, test_config in sorted(input_tests.iteritems()):
155 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28156 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28157 if test:
158 scripts.append(test)
159 return scripts
160
161 def sort(self, tests):
162 return sorted(tests, key=lambda x: x['test'])
163
164
165class CTSGenerator(BaseGenerator):
166 def __init__(self, bb_gen):
167 super(CTSGenerator, self).__init__(bb_gen)
168
Kenneth Russell8ceeabf2017-12-11 17:53:28169 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28170 # These only contain one entry and it's the contents of the input tests'
171 # dictionary, verbatim.
172 cts_tests = []
173 cts_tests.append(input_tests)
174 return cts_tests
175
176 def sort(self, tests):
177 return tests
178
179
180class InstrumentationTestGenerator(BaseGenerator):
181 def __init__(self, bb_gen):
182 super(InstrumentationTestGenerator, self).__init__(bb_gen)
183
Kenneth Russell8ceeabf2017-12-11 17:53:28184 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28185 scripts = []
186 for test_name, test_config in sorted(input_tests.iteritems()):
187 test = self.bb_gen.generate_instrumentation_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28188 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28189 if test:
190 scripts.append(test)
191 return scripts
192
193 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28194 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28195
196
Jeff Yoon67c3e832020-02-08 07:39:38197def check_compound_references(other_test_suites=None,
198 sub_suite=None,
199 suite=None,
200 target_test_suites=None,
201 test_type=None,
202 **kwargs):
203 """Ensure comound reference's don't target other compounds"""
204 del kwargs
205 if sub_suite in other_test_suites or sub_suite in target_test_suites:
206 raise BBGenErr('%s may not refer to other composition type test '
207 'suites (error found while processing %s)'
208 % (test_type, suite))
209
210def check_basic_references(basic_suites=None,
211 sub_suite=None,
212 suite=None,
213 **kwargs):
214 """Ensure test has a basic suite reference"""
215 del kwargs
216 if sub_suite not in basic_suites:
217 raise BBGenErr('Unable to find reference to %s while processing %s'
218 % (sub_suite, suite))
219
220def check_conflicting_definitions(basic_suites=None,
221 seen_tests=None,
222 sub_suite=None,
223 suite=None,
224 test_type=None,
225 **kwargs):
226 """Ensure that if a test is reachable via multiple basic suites,
227 all of them have an identical definition of the tests.
228 """
229 del kwargs
230 for test_name in basic_suites[sub_suite]:
231 if (test_name in seen_tests and
232 basic_suites[sub_suite][test_name] !=
233 basic_suites[seen_tests[test_name]][test_name]):
234 raise BBGenErr('Conflicting test definitions for %s from %s '
235 'and %s in %s (error found while processing %s)'
236 % (test_name, seen_tests[test_name], sub_suite,
237 test_type, suite))
238 seen_tests[test_name] = sub_suite
239
240def check_matrix_identifier(sub_suite=None,
241 suite=None,
242 suite_def=None,
243 **kwargs):
244 """Ensure 'idenfitier' is defined for each variant"""
245 del kwargs
246 sub_suite_config = suite_def[sub_suite]
247 for variant in sub_suite_config.get('variants', []):
248 if not 'identifier' in variant:
249 raise BBGenErr('Missing required identifier field in matrix '
250 'compound suite %s, %s' % (suite, sub_suite))
251
252
Kenneth Russelleb60cbd22017-12-05 07:54:28253class BBJSONGenerator(object):
254 def __init__(self):
255 self.this_dir = THIS_DIR
256 self.args = None
257 self.waterfalls = None
258 self.test_suites = None
259 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01260 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41261 self.gn_isolate_map = None
Kenneth Russelleb60cbd22017-12-05 07:54:28262
263 def generate_abs_file_path(self, relative_path):
264 return os.path.join(self.this_dir, relative_path) # pragma: no cover
265
Stephen Martinis7eb8b612018-09-21 00:17:50266 def print_line(self, line):
267 # Exists so that tests can mock
268 print line # pragma: no cover
269
Kenneth Russelleb60cbd22017-12-05 07:54:28270 def read_file(self, relative_path):
271 with open(self.generate_abs_file_path(
272 relative_path)) as fp: # pragma: no cover
273 return fp.read() # pragma: no cover
274
275 def write_file(self, relative_path, contents):
276 with open(self.generate_abs_file_path(
277 relative_path), 'wb') as fp: # pragma: no cover
278 fp.write(contents) # pragma: no cover
279
Zhiling Huangbe008172018-03-08 19:13:11280 def pyl_file_path(self, filename):
281 if self.args and self.args.pyl_files_dir:
282 return os.path.join(self.args.pyl_files_dir, filename)
283 return filename
284
Kenneth Russelleb60cbd22017-12-05 07:54:28285 def load_pyl_file(self, filename):
286 try:
Zhiling Huangbe008172018-03-08 19:13:11287 return ast.literal_eval(self.read_file(
288 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28289 except (SyntaxError, ValueError) as e: # pragma: no cover
290 raise BBGenErr('Failed to parse pyl file "%s": %s' %
291 (filename, e)) # pragma: no cover
292
Kenneth Russell8a386d42018-06-02 09:48:01293 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
294 # Currently it is only mandatory for bots which run GPU tests. Change these to
295 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28296 def is_android(self, tester_config):
297 return tester_config.get('os_type') == 'android'
298
Ben Pastenea9e583b2019-01-16 02:57:26299 def is_chromeos(self, tester_config):
300 return tester_config.get('os_type') == 'chromeos'
301
Kenneth Russell8a386d42018-06-02 09:48:01302 def is_linux(self, tester_config):
303 return tester_config.get('os_type') == 'linux'
304
Kai Ninomiya40de9f52019-10-18 21:38:49305 def is_mac(self, tester_config):
306 return tester_config.get('os_type') == 'mac'
307
308 def is_win(self, tester_config):
309 return tester_config.get('os_type') == 'win'
310
311 def is_win64(self, tester_config):
312 return (tester_config.get('os_type') == 'win' and
313 tester_config.get('browser_config') == 'release_x64')
314
Kenneth Russelleb60cbd22017-12-05 07:54:28315 def get_exception_for_test(self, test_name, test_config):
316 # gtests may have both "test" and "name" fields, and usually, if the "name"
317 # field is specified, it means that the same test is being repurposed
318 # multiple times with different command line arguments. To handle this case,
319 # prefer to lookup per the "name" field of the test itself, as opposed to
320 # the "test_name", which is actually the "test" field.
321 if 'name' in test_config:
322 return self.exceptions.get(test_config['name'])
323 else:
324 return self.exceptions.get(test_name)
325
Nico Weberb0b3f5862018-07-13 18:45:15326 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28327 # Currently, the only reason a test should not run on a given tester is that
328 # it's in the exceptions. (Once the GPU waterfall generation script is
329 # incorporated here, the rules will become more complex.)
330 exception = self.get_exception_for_test(test_name, test_config)
331 if not exception:
332 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28333 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28334 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28335 if remove_from:
336 if tester_name in remove_from:
337 return False
338 # TODO(kbr): this code path was added for some tests (including
339 # android_webview_unittests) on one machine (Nougat Phone
340 # Tester) which exists with the same name on two waterfalls,
341 # chromium.android and chromium.fyi; the tests are run on one
342 # but not the other. Once the bots are all uniquely named (a
343 # different ongoing project) this code should be removed.
344 # TODO(kbr): add coverage.
345 return (tester_name + ' ' + waterfall['name']
346 not in remove_from) # pragma: no cover
347 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28348
Nico Weber79dc5f6852018-07-13 19:38:49349 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28350 exception = self.get_exception_for_test(test_name, test)
351 if not exception:
352 return None
Nico Weber79dc5f6852018-07-13 19:38:49353 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28354
Brian Sheedye6ea0ee2019-07-11 02:54:37355 def get_test_replacements(self, test, test_name, tester_name):
356 exception = self.get_exception_for_test(test_name, test)
357 if not exception:
358 return None
359 return exception.get('replacements', {}).get(tester_name)
360
Kenneth Russell8a386d42018-06-02 09:48:01361 def merge_command_line_args(self, arr, prefix, splitter):
362 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01363 idx = 0
364 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01365 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01366 while idx < len(arr):
367 flag = arr[idx]
368 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01369 if flag.startswith(prefix):
370 arg = flag[prefix_len:]
371 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01372 if first_idx < 0:
373 first_idx = idx
374 else:
375 delete_current_entry = True
376 if delete_current_entry:
377 del arr[idx]
378 else:
379 idx += 1
380 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01381 arr[first_idx] = prefix + splitter.join(accumulated_args)
382 return arr
383
384 def maybe_fixup_args_array(self, arr):
385 # The incoming array of strings may be an array of command line
386 # arguments. To make it easier to turn on certain features per-bot or
387 # per-test-suite, look specifically for certain flags and merge them
388 # appropriately.
389 # --enable-features=Feature1 --enable-features=Feature2
390 # are merged to:
391 # --enable-features=Feature1,Feature2
392 # and:
393 # --extra-browser-args=arg1 --extra-browser-args=arg2
394 # are merged to:
395 # --extra-browser-args=arg1 arg2
396 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
397 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01398 return arr
399
Kenneth Russelleb60cbd22017-12-05 07:54:28400 def dictionary_merge(self, a, b, path=None, update=True):
401 """http://stackoverflow.com/questions/7204805/
402 python-dictionaries-of-dictionaries-merge
403 merges b into a
404 """
405 if path is None:
406 path = []
407 for key in b:
408 if key in a:
409 if isinstance(a[key], dict) and isinstance(b[key], dict):
410 self.dictionary_merge(a[key], b[key], path + [str(key)])
411 elif a[key] == b[key]:
412 pass # same leaf value
413 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06414 # Args arrays are lists of strings. Just concatenate them,
415 # and don't sort them, in order to keep some needed
416 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28417 if all(isinstance(x, str)
418 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01419 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28420 else:
421 # TODO(kbr): this only works properly if the two arrays are
422 # the same length, which is currently always the case in the
423 # swarming dimension_sets that we have to merge. It will fail
424 # to merge / override 'args' arrays which are different
425 # length.
426 for idx in xrange(len(b[key])):
427 try:
428 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
429 path + [str(key), str(idx)],
430 update=update)
Jeff Yoon8154e582019-12-03 23:30:01431 except (IndexError, TypeError):
432 raise BBGenErr('Error merging lists by key "%s" from source %s '
433 'into target %s at index %s. Verify target list '
434 'length is equal or greater than source'
435 % (str(key), str(b), str(a), str(idx)))
John Budorick5bc387fe2019-05-09 20:02:53436 elif update:
437 if b[key] is None:
438 del a[key]
439 else:
440 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28441 else:
442 raise BBGenErr('Conflict at %s' % '.'.join(
443 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53444 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28445 a[key] = b[key]
446 return a
447
John Budorickab108712018-09-01 00:12:21448 def initialize_args_for_test(
449 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21450 args = []
451 args.extend(generated_test.get('args', []))
452 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22453
Kenneth Russell8a386d42018-06-02 09:48:01454 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21455 val = generated_test.pop(key, [])
456 if fn(tester_config):
457 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01458
459 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
460 add_conditional_args('linux_args', self.is_linux)
461 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36462 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49463 add_conditional_args('mac_args', self.is_mac)
464 add_conditional_args('win_args', self.is_win)
465 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01466
John Budorickab108712018-09-01 00:12:21467 for key in additional_arg_keys or []:
468 args.extend(generated_test.pop(key, []))
469 args.extend(tester_config.get(key, []))
470
471 if args:
472 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01473
Kenneth Russelleb60cbd22017-12-05 07:54:28474 def initialize_swarming_dictionary_for_test(self, generated_test,
475 tester_config):
476 if 'swarming' not in generated_test:
477 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28478 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
479 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38480 'can_use_on_swarming_builders': tester_config.get('use_swarming',
481 True)
Dirk Pranke81ff51c2017-12-09 19:24:28482 })
Kenneth Russelleb60cbd22017-12-05 07:54:28483 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03484 if ('dimension_sets' not in generated_test['swarming'] and
485 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28486 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
487 tester_config['swarming']['dimension_sets'])
488 self.dictionary_merge(generated_test['swarming'],
489 tester_config['swarming'])
490 # Apply any Android-specific Swarming dimensions after the generic ones.
491 if 'android_swarming' in generated_test:
492 if self.is_android(tester_config): # pragma: no cover
493 self.dictionary_merge(
494 generated_test['swarming'],
495 generated_test['android_swarming']) # pragma: no cover
496 del generated_test['android_swarming'] # pragma: no cover
497
498 def clean_swarming_dictionary(self, swarming_dict):
499 # Clean out redundant entries from a test's "swarming" dictionary.
500 # This is really only needed to retain 100% parity with the
501 # handwritten JSON files, and can be removed once all the files are
502 # autogenerated.
503 if 'shards' in swarming_dict:
504 if swarming_dict['shards'] == 1: # pragma: no cover
505 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24506 if 'hard_timeout' in swarming_dict:
507 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
508 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43509 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28510 # Remove all other keys.
511 for k in swarming_dict.keys(): # pragma: no cover
512 if k != 'can_use_on_swarming_builders': # pragma: no cover
513 del swarming_dict[k] # pragma: no cover
514
Stephen Martinis0382bc12018-09-17 22:29:07515 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
516 waterfall):
517 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01518 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07519 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28520 # See if there are any exceptions that need to be merged into this
521 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49522 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28523 if modifications:
524 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23525 if 'swarming' in test:
526 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28527 # Ensure all Android Swarming tests run only on userdebug builds if another
528 # build type was not specified.
529 if 'swarming' in test and self.is_android(tester_config):
530 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22531 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28532 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37533 self.replace_test_args(test, test_name, tester_name)
Ben Pastenee012aea42019-05-14 22:32:28534
Kenneth Russelleb60cbd22017-12-05 07:54:28535 return test
536
Brian Sheedye6ea0ee2019-07-11 02:54:37537 def replace_test_args(self, test, test_name, tester_name):
538 replacements = self.get_test_replacements(
539 test, test_name, tester_name) or {}
540 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
541 for key, replacement_dict in replacements.iteritems():
542 if key not in valid_replacement_keys:
543 raise BBGenErr(
544 'Given replacement key %s for %s on %s is not in the list of valid '
545 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
546 for replacement_key, replacement_val in replacement_dict.iteritems():
547 found_key = False
548 for i, test_key in enumerate(test.get(key, [])):
549 # Handle both the key/value being replaced being defined as two
550 # separate items or as key=value.
551 if test_key == replacement_key:
552 found_key = True
553 # Handle flags without values.
554 if replacement_val == None:
555 del test[key][i]
556 else:
557 test[key][i+1] = replacement_val
558 break
559 elif test_key.startswith(replacement_key + '='):
560 found_key = True
561 if replacement_val == None:
562 del test[key][i]
563 else:
564 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
565 break
566 if not found_key:
567 raise BBGenErr('Could not find %s in existing list of values for key '
568 '%s in %s on %s' % (replacement_key, key, test_name,
569 tester_name))
570
Shenghua Zhangaba8bad2018-02-07 02:12:09571 def add_common_test_properties(self, test, tester_config):
572 if tester_config.get('use_multi_dimension_trigger_script'):
Kenneth Russell73c3bd8b2018-10-19 22:30:19573 # Assumes update_and_cleanup_test has already been called, so the
574 # builder's mixins have been flattened into the test.
Shenghua Zhangaba8bad2018-02-07 02:12:09575 test['trigger_script'] = {
576 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
577 'args': [
578 '--multiple-trigger-configs',
Kenneth Russell73c3bd8b2018-10-19 22:30:19579 json.dumps(test['swarming']['dimension_sets'] +
Shenghua Zhangaba8bad2018-02-07 02:12:09580 tester_config.get('alternate_swarming_dimensions', [])),
581 '--multiple-dimension-script-verbose',
582 'True'
583 ],
584 }
Ben Pastenea9e583b2019-01-16 02:57:26585 elif self.is_chromeos(tester_config) and tester_config.get('use_swarming',
586 True):
587 # The presence of the "device_type" dimension indicates that the tests
588 # are targetting CrOS hardware and so need the special trigger script.
589 dimension_sets = tester_config['swarming']['dimension_sets']
590 if all('device_type' in ds for ds in dimension_sets):
591 test['trigger_script'] = {
592 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
593 }
Shenghua Zhangaba8bad2018-02-07 02:12:09594
Ben Pastene858f4be2019-01-09 23:52:09595 def add_android_presentation_args(self, tester_config, test_name, result):
596 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38597 bucket = tester_config.get('results_bucket', 'chromium-result-details')
598 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09599 if (result['swarming']['can_use_on_swarming_builders'] and not
600 tester_config.get('skip_merge_script', False)):
601 result['merge'] = {
602 'args': [
603 '--bucket',
John Budorick262ae112019-07-12 19:24:38604 bucket,
Ben Pastene858f4be2019-01-09 23:52:09605 '--test-name',
606 test_name
607 ],
608 'script': '//build/android/pylib/results/presentation/'
609 'test_results_presentation.py',
610 }
611 if not tester_config.get('skip_cipd_packages', False):
Ben Pastenee5949ea82019-01-10 21:45:26612 cipd_packages = result['swarming'].get('cipd_packages', [])
613 cipd_packages.append(
Ben Pastene858f4be2019-01-09 23:52:09614 {
615 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
616 'location': 'bin',
617 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
618 }
Ben Pastenee5949ea82019-01-10 21:45:26619 )
620 result['swarming']['cipd_packages'] = cipd_packages
Ben Pastene858f4be2019-01-09 23:52:09621 if not tester_config.get('skip_output_links', False):
622 result['swarming']['output_links'] = [
623 {
624 'link': [
625 'https://luci-logdog.appspot.com/v/?s',
626 '=android%2Fswarming%2Flogcats%2F',
627 '${TASK_ID}%2F%2B%2Funified_logcats',
628 ],
629 'name': 'shard #${SHARD_INDEX} logcats',
630 },
631 ]
632 if args:
633 result['args'] = args
634
Kenneth Russelleb60cbd22017-12-05 07:54:28635 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
636 test_config):
637 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15638 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28639 return None
640 result = copy.deepcopy(test_config)
641 if 'test' in result:
642 result['name'] = test_name
643 else:
644 result['test'] = test_name
645 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21646
647 self.initialize_args_for_test(
648 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28649 if self.is_android(tester_config) and tester_config.get('use_swarming',
650 True):
Ben Pastene858f4be2019-01-09 23:52:09651 self.add_android_presentation_args(tester_config, test_name, result)
652 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42653
Stephen Martinis0382bc12018-09-17 22:29:07654 result = self.update_and_cleanup_test(
655 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09656 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43657
658 if not result.get('merge'):
659 # TODO(https://crbug.com/958376): Consider adding the ability to not have
660 # this default.
661 result['merge'] = {
662 'script': '//testing/merge_scripts/standard_gtest_merge.py',
663 'args': [],
664 }
Kenneth Russelleb60cbd22017-12-05 07:54:28665 return result
666
667 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
668 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01669 if not self.should_run_on_tester(waterfall, tester_name, test_name,
670 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28671 return None
672 result = copy.deepcopy(test_config)
673 result['isolate_name'] = result.get('isolate_name', test_name)
674 result['name'] = test_name
675 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01676 self.initialize_args_for_test(result, tester_config)
Ben Pastene858f4be2019-01-09 23:52:09677 if tester_config.get('use_android_presentation', False):
678 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07679 result = self.update_and_cleanup_test(
680 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09681 self.add_common_test_properties(result, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17682
683 if not result.get('merge'):
684 # TODO(https://crbug.com/958376): Consider adding the ability to not have
685 # this default.
686 result['merge'] = {
687 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
688 'args': [],
689 }
Kenneth Russelleb60cbd22017-12-05 07:54:28690 return result
691
692 def generate_script_test(self, waterfall, tester_name, tester_config,
693 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44694 # TODO(https://crbug.com/953072): Remove this check whenever a better
695 # long-term solution is implemented.
696 if (waterfall.get('forbid_script_tests', False) or
697 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
698 raise BBGenErr('Attempted to generate a script test on tester ' +
699 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01700 if not self.should_run_on_tester(waterfall, tester_name, test_name,
701 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28702 return None
703 result = {
704 'name': test_name,
705 'script': test_config['script']
706 }
Stephen Martinis0382bc12018-09-17 22:29:07707 result = self.update_and_cleanup_test(
708 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28709 return result
710
711 def generate_junit_test(self, waterfall, tester_name, tester_config,
712 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01713 if not self.should_run_on_tester(waterfall, tester_name, test_name,
714 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28715 return None
John Budorickdef6acb2019-09-17 22:51:09716 result = copy.deepcopy(test_config)
717 result.update({
John Budorickcadc4952019-09-16 23:51:37718 'name': test_name,
719 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09720 })
721 self.initialize_args_for_test(result, tester_config)
722 result = self.update_and_cleanup_test(
723 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28724 return result
725
726 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
727 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01728 if not self.should_run_on_tester(waterfall, tester_name, test_name,
729 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28730 return None
731 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28732 if 'test' in result and result['test'] != test_name:
733 result['name'] = test_name
734 else:
735 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07736 result = self.update_and_cleanup_test(
737 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28738 return result
739
Stephen Martinis2a0667022018-09-25 22:31:14740 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01741 substitutions = {
742 # Any machine in waterfalls.pyl which desires to run GPU tests
743 # must provide the os_type key.
744 'os_type': tester_config['os_type'],
745 'gpu_vendor_id': '0',
746 'gpu_device_id': '0',
747 }
Stephen Martinis2a0667022018-09-25 22:31:14748 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01749 if 'gpu' in dimension_set:
750 # First remove the driver version, then split into vendor and device.
751 gpu = dimension_set['gpu']
Kenneth Russell384a1732019-03-16 02:36:02752 # Handle certain specialized named GPUs.
753 if gpu.startswith('nvidia-quadro-p400'):
754 gpu = ['10de', '1cb3']
755 elif gpu.startswith('intel-hd-630'):
756 gpu = ['8086', '5912']
Brian Sheedyf9387db7b2019-08-05 19:26:10757 elif gpu.startswith('intel-uhd-630'):
758 gpu = ['8086', '3e92']
Kenneth Russell384a1732019-03-16 02:36:02759 else:
760 gpu = gpu.split('-')[0].split(':')
Kenneth Russell8a386d42018-06-02 09:48:01761 substitutions['gpu_vendor_id'] = gpu[0]
762 substitutions['gpu_device_id'] = gpu[1]
763 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
764
765 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56766 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01767 # These are all just specializations of isolated script tests with
768 # a bunch of boilerplate command line arguments added.
769
770 # The step name must end in 'test' or 'tests' in order for the
771 # results to automatically show up on the flakiness dashboard.
772 # (At least, this was true some time ago.) Continue to use this
773 # naming convention for the time being to minimize changes.
774 step_name = test_config.get('name', test_name)
775 if not (step_name.endswith('test') or step_name.endswith('tests')):
776 step_name = '%s_tests' % step_name
777 result = self.generate_isolated_script_test(
778 waterfall, tester_name, tester_config, step_name, test_config)
779 if not result:
780 return None
781 result['isolate_name'] = 'telemetry_gpu_integration_test'
782 args = result.get('args', [])
783 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14784
785 # These tests upload and download results from cloud storage and therefore
786 # aren't idempotent yet. https://crbug.com/549140.
787 result['swarming']['idempotent'] = False
788
Kenneth Russell44910c32018-12-03 23:35:11789 # The GPU tests act much like integration tests for the entire browser, and
790 # tend to uncover flakiness bugs more readily than other test suites. In
791 # order to surface any flakiness more readily to the developer of the CL
792 # which is introducing it, we disable retries with patch on the commit
793 # queue.
794 result['should_retry_with_patch'] = False
795
Bo Liu555a0f92019-03-29 12:11:56796 browser = ('android-webview-instrumentation'
797 if is_android_webview else tester_config['browser_config'])
Kenneth Russell8a386d42018-06-02 09:48:01798 args = [
Bo Liu555a0f92019-03-29 12:11:56799 test_to_run,
800 '--show-stdout',
801 '--browser=%s' % browser,
802 # --passthrough displays more of the logging in Telemetry when
803 # run via typ, in particular some of the warnings about tests
804 # being expected to fail, but passing.
805 '--passthrough',
806 '-v',
807 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
Kenneth Russell8a386d42018-06-02 09:48:01808 ] + args
809 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14810 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01811 return result
812
Kenneth Russelleb60cbd22017-12-05 07:54:28813 def get_test_generator_map(self):
814 return {
Bo Liu555a0f92019-03-29 12:11:56815 'android_webview_gpu_telemetry_tests':
816 GPUTelemetryTestGenerator(self, is_android_webview=True),
817 'cts_tests':
818 CTSGenerator(self),
819 'gpu_telemetry_tests':
820 GPUTelemetryTestGenerator(self),
821 'gtest_tests':
822 GTestGenerator(self),
823 'instrumentation_tests':
824 InstrumentationTestGenerator(self),
825 'isolated_scripts':
826 IsolatedScriptTestGenerator(self),
827 'junit_tests':
828 JUnitGenerator(self),
829 'scripts':
830 ScriptGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28831 }
832
Kenneth Russell8a386d42018-06-02 09:48:01833 def get_test_type_remapper(self):
834 return {
835 # These are a specialization of isolated_scripts with a bunch of
836 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:56837 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:01838 'gpu_telemetry_tests': 'isolated_scripts',
839 }
840
Jeff Yoon67c3e832020-02-08 07:39:38841 def check_composition_type_test_suites(self, test_type,
842 additional_validators=None):
843 """Pre-pass to catch errors reliabily for compound/matrix suites"""
844 validators = [check_compound_references,
845 check_basic_references,
846 check_conflicting_definitions]
847 if additional_validators:
848 validators += additional_validators
849
850 target_suites = self.test_suites.get(test_type, {})
851 other_test_type = ('compound_suites'
852 if test_type == 'matrix_compound_suites'
853 else 'matrix_compound_suites')
854 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:01855 basic_suites = self.test_suites.get('basic_suites', {})
856
Jeff Yoon67c3e832020-02-08 07:39:38857 for suite, suite_def in target_suites.iteritems():
Jeff Yoon8154e582019-12-03 23:30:01858 if suite in basic_suites:
859 raise BBGenErr('%s names may not duplicate basic test suite names '
860 '(error found while processsing %s)'
861 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:01862
Jeff Yoon67c3e832020-02-08 07:39:38863 seen_tests = {}
864 for sub_suite in suite_def:
865 for validator in validators:
866 validator(
867 basic_suites=basic_suites,
868 other_test_suites=other_suites,
869 seen_tests=seen_tests,
870 sub_suite=sub_suite,
871 suite=suite,
872 suite_def=suite_def,
873 target_test_suites=target_suites,
874 test_type=test_type,
875 )
Kenneth Russelleb60cbd22017-12-05 07:54:28876
Stephen Martinis54d64ad2018-09-21 22:16:20877 def flatten_test_suites(self):
878 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:01879 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
880 for category in test_types:
881 for name, value in self.test_suites.get(category, {}).iteritems():
882 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:20883 self.test_suites = new_test_suites
884
Nodir Turakulovfce34292019-12-18 17:05:41885 def resolve_full_test_targets(self):
886 for suite in self.test_suites['basic_suites'].itervalues():
887 for key, test in suite.iteritems():
888 if not isinstance(test, dict):
889 # Some test definitions are just strings, such as CTS.
890 # Skip them.
891 continue
892
893 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
894 # https://source.chromium.org/chromium/chromium/tools/build/+/master:scripts/slave/recipe_modules/chromium_tests/generators.py;l=89;drc=14c062ba0eb418d3c4623dde41a753241b9df06b
895 # TODO(crbug.com/1035124): clean this up.
896 isolate_name = test.get('test') or test.get('isolate_name') or key
897 gn_entry = self.gn_isolate_map.get(isolate_name)
898 if gn_entry:
899 test['test_target'] = gn_entry['label']
900 else: # pragma: no cover
901 # Some tests do not have an entry gn_isolate_map.pyl, such as
902 # telemetry tests.
903 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
904 pass
905
Kenneth Russelleb60cbd22017-12-05 07:54:28906 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:01907 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:20908
Jeff Yoon8154e582019-12-03 23:30:01909 compound_suites = self.test_suites.get('compound_suites', {})
910 # check_composition_type_test_suites() checks that all basic suites
911 # referenced by compound suites exist.
912 basic_suites = self.test_suites.get('basic_suites')
913
914 for name, value in compound_suites.iteritems():
915 # Resolve this to a dictionary.
916 full_suite = {}
917 for entry in value:
918 suite = basic_suites[entry]
919 full_suite.update(suite)
920 compound_suites[name] = full_suite
921
Jeff Yoon67c3e832020-02-08 07:39:38922 def resolve_variants(self, basic_test_definition, variants):
923 """ Merge variant-defined configurations to each test case definition in a
924 test suite.
925
926 The output maps a unique test name to an array of configurations because
927 there may exist more than one definition for a test name using variants. The
928 test name is referenced while mapping machines to test suites, so unpacking
929 the array is done by the generators.
930
931 Args:
932 basic_test_definition: a {} defined test suite in the format
933 test_name:test_config
934 variants: an [] of {} defining configurations to be applied to each test
935 case in the basic test_definition
936
937 Return:
938 a {} of test_name:[{}], where each {} is a merged configuration
939 """
940
941 # Each test in a basic test suite will have a definition per variant.
942 test_suite = {}
943 for test_name, test_config in basic_test_definition.iteritems():
944 definitions = []
945 for variant in variants:
946 # Clone a copy of test_config so that we can have a uniquely updated
947 # version of it per variant
948 cloned_config = copy.deepcopy(test_config)
949 # The variant definition needs to be re-used for each test, so we'll
950 # create a clone and work with it as well.
951 cloned_variant = copy.deepcopy(variant)
952
953 cloned_config['args'] = (cloned_config.get('args', []) +
954 cloned_variant.get('args', []))
955 cloned_config['mixins'] = (cloned_config.get('mixins', []) +
956 cloned_variant.get('mixins', []))
957
958 basic_swarming_def = cloned_config.get('swarming', {})
959 variant_swarming_def = cloned_variant.get('swarming', {})
960 if basic_swarming_def and variant_swarming_def:
961 if ('dimension_sets' in basic_swarming_def and
962 'dimension_sets' in variant_swarming_def):
963 # Retain swarming dimension set merge behavior when both variant and
964 # the basic test configuration both define it
965 self.dictionary_merge(basic_swarming_def, variant_swarming_def)
966 # Remove dimension_sets from the variant definition, so that it does
967 # not replace what's been done by dictionary_merge in the update
968 # call below.
969 del variant_swarming_def['dimension_sets']
970
971 # Update the swarming definition with whatever is defined for swarming
972 # by the variant.
973 basic_swarming_def.update(variant_swarming_def)
974 cloned_config['swarming'] = basic_swarming_def
975
976 # The identifier is used to make the name of the test unique.
977 # Generators in the recipe uniquely identify a test by it's name, so we
978 # don't want to have the same name for each variant.
979 cloned_config['name'] = '{}_{}'.format(test_name,
980 cloned_variant['identifier'])
981
982 definitions.append(cloned_config)
983 test_suite[test_name] = definitions
984 return test_suite
985
Jeff Yoon8154e582019-12-03 23:30:01986 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:38987 self.check_composition_type_test_suites('matrix_compound_suites',
988 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:01989
990 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:38991 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:01992 # referenced by matrix suites exist.
993 basic_suites = self.test_suites.get('basic_suites')
994
Jeff Yoon67c3e832020-02-08 07:39:38995 for test_name, matrix_config in matrix_compound_suites.iteritems():
Jeff Yoon8154e582019-12-03 23:30:01996 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:38997
998 for test_suite, mtx_test_suite_config in matrix_config.iteritems():
999 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1000
1001 if 'variants' in mtx_test_suite_config:
1002 result = self.resolve_variants(basic_test_def,
1003 mtx_test_suite_config['variants'])
1004 full_suite.update(result)
1005 matrix_compound_suites[test_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281006
1007 def link_waterfalls_to_test_suites(self):
1008 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431009 for tester_name, tester in waterfall['machines'].iteritems():
1010 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281011 if not value in self.test_suites:
1012 # Hard / impossible to cover this in the unit test.
1013 raise self.unknown_test_suite(
1014 value, tester_name, waterfall['name']) # pragma: no cover
1015 tester['test_suites'][suite] = self.test_suites[value]
1016
1017 def load_configuration_files(self):
1018 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
1019 self.test_suites = self.load_pyl_file('test_suites.pyl')
1020 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:011021 self.mixins = self.load_pyl_file('mixins.pyl')
Nodir Turakulovfce34292019-12-18 17:05:411022 self.gn_isolate_map = self.load_pyl_file('gn_isolate_map.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:281023
1024 def resolve_configuration_files(self):
Nodir Turakulovfce34292019-12-18 17:05:411025 self.resolve_full_test_targets()
Kenneth Russelleb60cbd22017-12-05 07:54:281026 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011027 self.resolve_matrix_compound_test_suites()
1028 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281029 self.link_waterfalls_to_test_suites()
1030
Nico Weberd18b8962018-05-16 19:39:381031 def unknown_bot(self, bot_name, waterfall_name):
1032 return BBGenErr(
1033 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1034
Kenneth Russelleb60cbd22017-12-05 07:54:281035 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1036 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381037 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281038 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1039
1040 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1041 return BBGenErr(
1042 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1043 ' on waterfall ' + waterfall_name)
1044
Stephen Martinisb72f6d22018-10-04 23:29:011045 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071046 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321047
1048 Checks in the waterfall, builder, and test objects for mixins.
1049 """
1050 def valid_mixin(mixin_name):
1051 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011052 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321053 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381054
Stephen Martinisb6a50492018-09-12 23:59:321055 def must_be_list(mixins, typ, name):
1056 """Asserts that given mixins are a list."""
1057 if not isinstance(mixins, list):
1058 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1059
Brian Sheedy7658c982020-01-08 02:27:581060 test_name = test.get('name')
1061 remove_mixins = set()
1062 if 'remove_mixins' in builder:
1063 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1064 for rm in builder['remove_mixins']:
1065 valid_mixin(rm)
1066 remove_mixins.add(rm)
1067 if 'remove_mixins' in test:
1068 must_be_list(test['remove_mixins'], 'test', test_name)
1069 for rm in test['remove_mixins']:
1070 valid_mixin(rm)
1071 remove_mixins.add(rm)
1072 del test['remove_mixins']
1073
Stephen Martinisb72f6d22018-10-04 23:29:011074 if 'mixins' in waterfall:
1075 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1076 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581077 if mixin in remove_mixins:
1078 continue
Stephen Martinisb6a50492018-09-12 23:59:321079 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011080 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321081
Stephen Martinisb72f6d22018-10-04 23:29:011082 if 'mixins' in builder:
1083 must_be_list(builder['mixins'], 'builder', builder_name)
1084 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581085 if mixin in remove_mixins:
1086 continue
Stephen Martinisb6a50492018-09-12 23:59:321087 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011088 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321089
Stephen Martinisb72f6d22018-10-04 23:29:011090 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071091 return test
1092
Stephen Martinis2a0667022018-09-25 22:31:141093 if not test_name:
1094 test_name = test.get('test')
1095 if not test_name: # pragma: no cover
1096 # Not the best name, but we should say something.
1097 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:011098 must_be_list(test['mixins'], 'test', test_name)
1099 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581100 # We don't bother checking if the given mixin is in remove_mixins here
1101 # since this is already the lowest level, so if a mixin is added here that
1102 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071103 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011104 test = self.apply_mixin(self.mixins[mixin], test)
Jeff Yoon67c3e832020-02-08 07:39:381105 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071106 return test
Stephen Martinisb6a50492018-09-12 23:59:321107
Stephen Martinisb72f6d22018-10-04 23:29:011108 def apply_mixin(self, mixin, test):
1109 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321110
Stephen Martinis0382bc12018-09-17 22:29:071111 Mixins will not override an existing key. This is to ensure exceptions can
1112 override a setting a mixin applies.
1113
Stephen Martinisb72f6d22018-10-04 23:29:011114 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:321115 'dimension_sets', which is how normal test suites specify their dimensions,
1116 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
1117 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011118
Stephen Martinisb6a50492018-09-12 23:59:321119 """
1120 new_test = copy.deepcopy(test)
1121 mixin = copy.deepcopy(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011122 if 'swarming' in mixin:
1123 swarming_mixin = mixin['swarming']
1124 new_test.setdefault('swarming', {})
1125 if 'dimensions' in swarming_mixin:
1126 new_test['swarming'].setdefault('dimension_sets', [{}])
1127 for dimension_set in new_test['swarming']['dimension_sets']:
1128 dimension_set.update(swarming_mixin['dimensions'])
1129 del swarming_mixin['dimensions']
Stephen Martinisb72f6d22018-10-04 23:29:011130 # python dict update doesn't do recursion at all. Just hard code the
1131 # nested update we need (mixin['swarming'] shouldn't clobber
1132 # test['swarming'], but should update it).
1133 new_test['swarming'].update(swarming_mixin)
1134 del mixin['swarming']
1135
Wezc0e835b702018-10-30 00:38:411136 if '$mixin_append' in mixin:
1137 # Values specified under $mixin_append should be appended to existing
1138 # lists, rather than replacing them.
1139 mixin_append = mixin['$mixin_append']
1140 for key in mixin_append:
1141 new_test.setdefault(key, [])
1142 if not isinstance(mixin_append[key], list):
1143 raise BBGenErr(
1144 'Key "' + key + '" in $mixin_append must be a list.')
1145 if not isinstance(new_test[key], list):
1146 raise BBGenErr(
1147 'Cannot apply $mixin_append to non-list "' + key + '".')
1148 new_test[key].extend(mixin_append[key])
1149 if 'args' in mixin_append:
1150 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1151 del mixin['$mixin_append']
1152
Stephen Martinisb72f6d22018-10-04 23:29:011153 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321154 return new_test
1155
Gregory Gutermand1dd3b82020-03-02 21:53:481156 def generate_waterfall_json(self, waterfall):
1157 all_tests = {}
Greg Guterman5c6144152020-02-28 20:08:531158 generator_map = self.get_test_generator_map()
1159 test_type_remapper = self.get_test_type_remapper()
Gregory Gutermand1dd3b82020-03-02 21:53:481160 for name, config in waterfall['machines'].iteritems():
1161 tests = {}
1162 # Copy only well-understood entries in the machine's configuration
1163 # verbatim into the generated JSON.
1164 if 'additional_compile_targets' in config:
1165 tests['additional_compile_targets'] = config[
1166 'additional_compile_targets']
1167 for test_type, input_tests in config.get('test_suites', {}).iteritems():
1168 if test_type not in generator_map:
1169 raise self.unknown_test_suite_type(
1170 test_type, name, waterfall['name']) # pragma: no cover
1171 test_generator = generator_map[test_type]
1172 # Let multiple kinds of generators generate the same kinds
1173 # of tests. For example, gpu_telemetry_tests are a
1174 # specialization of isolated_scripts.
1175 new_tests = test_generator.generate(
1176 waterfall, name, config, input_tests)
1177 remapped_test_type = test_type_remapper.get(test_type, test_type)
1178 tests[remapped_test_type] = test_generator.sort(
1179 tests.get(remapped_test_type, []) + new_tests)
1180 all_tests[name] = tests
1181 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1182 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1183 return json.dumps(all_tests, indent=2, separators=(',', ': '),
1184 sort_keys=True) + '\n'
Kenneth Russelleb60cbd22017-12-05 07:54:281185
1186 def generate_waterfalls(self): # pragma: no cover
1187 self.load_configuration_files()
1188 self.resolve_configuration_files()
1189 filters = self.args.waterfall_filters
1190 suffix = '.json'
1191 if self.args.new_files:
1192 suffix = '.new' + suffix
1193 for waterfall in self.waterfalls:
Gregory Gutermand1dd3b82020-03-02 21:53:481194 should_gen = not filters or waterfall['name'] in filters
1195 if should_gen:
1196 file_path = waterfall['name'] + suffix
1197 self.write_file(self.pyl_file_path(file_path),
1198 self.generate_waterfall_json(waterfall))
Kenneth Russelleb60cbd22017-12-05 07:54:281199
Nico Weberd18b8962018-05-16 19:39:381200 def get_valid_bot_names(self):
John Budorick699282e2019-02-13 01:27:331201 # Extract bot names from infra/config/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421202 # NOTE: This reference can cause issues; if a file changes there, the
1203 # presubmit here won't be run by default. A manually maintained list there
1204 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1205 # references to configs outside of this directory are added, please change
1206 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1207 # never ends up in an invalid state.
Nico Weberd18b8962018-05-16 19:39:381208 bot_names = set()
John Budorickc12abd12018-08-14 19:37:431209 infra_config_dir = os.path.abspath(
1210 os.path.join(os.path.dirname(__file__),
John Budorick699282e2019-02-13 01:27:331211 '..', '..', 'infra', 'config'))
John Budorickc12abd12018-08-14 19:37:431212 milo_configs = [
Garrett Beatybb8322bf2019-10-17 20:53:051213 os.path.join(infra_config_dir, 'generated', 'luci-milo.cfg'),
Garrett Beatye95b81722019-10-24 17:12:181214 os.path.join(infra_config_dir, 'generated', 'luci-milo-dev.cfg'),
John Budorickc12abd12018-08-14 19:37:431215 ]
1216 for c in milo_configs:
1217 for l in self.read_file(c).splitlines():
1218 if (not 'name: "buildbucket/luci.chromium.' in l and
Hans Wennborg98ffd7d92019-02-06 14:14:341219 not 'name: "buildbucket/luci.chrome.' in l and
John Budorickb1833612018-12-07 04:36:411220 not 'name: "buildbot/chromium.' in l and
1221 not 'name: "buildbot/tryserver.chromium.' in l):
John Budorickc12abd12018-08-14 19:37:431222 continue
1223 # l looks like
1224 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1225 # Extract win_chromium_dbg_ng part.
1226 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381227 return bot_names
1228
Ben Pastene9a010082019-09-25 20:41:371229 def get_builders_that_do_not_actually_exist(self):
Kenneth Russell8a386d42018-06-02 09:48:011230 # Some of the bots on the chromium.gpu.fyi waterfall in particular
1231 # are defined only to be mirrored into trybots, and don't actually
1232 # exist on any of the waterfalls or consoles.
1233 return [
Michael Spangeb07eba62019-05-14 22:22:581234 'GPU FYI Fuchsia Builder',
Yuly Novikoveb26b812019-07-26 02:08:191235 'ANGLE GPU Android Release (Nexus 5X)',
Jamie Madillda894ce2019-04-08 17:19:171236 'ANGLE GPU Linux Release (Intel HD 630)',
1237 'ANGLE GPU Linux Release (NVIDIA)',
1238 'ANGLE GPU Mac Release (Intel)',
1239 'ANGLE GPU Mac Retina Release (AMD)',
1240 'ANGLE GPU Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491241 'ANGLE GPU Win10 x64 Release (Intel HD 630)',
1242 'ANGLE GPU Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011243 'Optional Android Release (Nexus 5X)',
1244 'Optional Linux Release (Intel HD 630)',
1245 'Optional Linux Release (NVIDIA)',
1246 'Optional Mac Release (Intel)',
1247 'Optional Mac Retina Release (AMD)',
1248 'Optional Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491249 'Optional Win10 x64 Release (Intel HD 630)',
1250 'Optional Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011251 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:081252 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:291253 'linux-blink-rel-dummy',
1254 'mac10.10-blink-rel-dummy',
1255 'mac10.11-blink-rel-dummy',
1256 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:201257 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291258 'mac10.13-blink-rel-dummy',
John Chenad978322019-12-16 18:07:211259 'mac10.14-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291260 'win7-blink-rel-dummy',
1261 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:081262 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:331263 'WebKit Linux composite_after_paint Dummy Builder',
Scott Violet744e04662019-08-19 23:51:531264 'WebKit Linux layout_ng_disabled Builder',
Stephen Martinis769b25112018-08-30 18:52:061265 # chromium, due to https://crbug.com/878915
1266 'win-dbg',
1267 'win32-dbg',
Stephen Martinis47d77132019-04-24 23:51:331268 'win-archive-dbg',
1269 'win32-archive-dbg',
Sajjad Mirza2924a012019-12-20 03:46:541270 # TODO(crbug.com/1033753) Delete these when coverage is enabled by default
1271 # on Windows tryjobs.
1272 'GPU Win x64 Builder Code Coverage',
1273 'Win x64 Builder Code Coverage',
1274 'Win10 Tests x64 Code Coverage',
1275 'Win10 x64 Release (NVIDIA) Code Coverage',
Sajjad Mirzafa15665e2020-02-10 23:41:041276 # TODO(crbug.com/1024915) Delete these when coverage is enabled by default
1277 # on Mac OS tryjobs.
1278 'Mac Builder Code Coverage',
1279 'Mac10.13 Tests Code Coverage',
1280 'GPU Mac Builder Code Coverage',
1281 'Mac Release (Intel) Code Coverage',
1282 'Mac Retina Release (AMD) Code Coverage',
Kenneth Russell8a386d42018-06-02 09:48:011283 ]
1284
Ben Pastene9a010082019-09-25 20:41:371285 def get_internal_waterfalls(self):
1286 # Similar to get_builders_that_do_not_actually_exist above, but for
1287 # waterfalls defined in internal configs.
1288 return ['chrome']
1289
Stephen Martinisf83893722018-09-19 00:02:181290 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201291 self.check_input_files_sorting(verbose)
1292
Kenneth Russelleb60cbd22017-12-05 07:54:281293 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011294 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381295 self.check_composition_type_test_suites('matrix_compound_suites',
1296 [check_matrix_identifier])
Nodir Turakulovfce34292019-12-18 17:05:411297 self.resolve_full_test_targets()
Stephen Martinis54d64ad2018-09-21 22:16:201298 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381299
1300 # All bots should exist.
1301 bot_names = self.get_valid_bot_names()
Ben Pastene9a010082019-09-25 20:41:371302 internal_waterfalls = self.get_internal_waterfalls()
1303 builders_that_dont_exist = self.get_builders_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:381304 for waterfall in self.waterfalls:
Ben Pastene9a010082019-09-25 20:41:371305 # TODO(crbug.com/991417): Remove the need for this exception.
1306 if waterfall['name'] in internal_waterfalls:
1307 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381308 for bot_name in waterfall['machines']:
Ben Pastene9a010082019-09-25 20:41:371309 if bot_name in builders_that_dont_exist:
Kenneth Russell8a386d42018-06-02 09:48:011310 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381311 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:081312 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:381313 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:521314 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:321315 if waterfall['name'] in ['tryserver.webrtc',
1316 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:381317 # These waterfalls have their bot configs in a different repo.
1318 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:521319 continue # pragma: no cover
Jeff Yoon8154e582019-12-03 23:30:011320 if waterfall['name'] in ['client.devtools-frontend.integration',
Liviu Raud287b1f2020-01-14 07:30:331321 'tryserver.devtools-frontend',
1322 'chromium.devtools-frontend']:
Tamer Tas2c506412019-08-20 07:44:411323 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381324 raise self.unknown_bot(bot_name, waterfall['name'])
1325
Kenneth Russelleb60cbd22017-12-05 07:54:281326 # All test suites must be referenced.
1327 suites_seen = set()
1328 generator_map = self.get_test_generator_map()
1329 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431330 for bot_name, tester in waterfall['machines'].iteritems():
1331 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281332 if suite_type not in generator_map:
1333 raise self.unknown_test_suite_type(suite_type, bot_name,
1334 waterfall['name'])
1335 if suite not in self.test_suites:
1336 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1337 suites_seen.add(suite)
1338 # Since we didn't resolve the configuration files, this set
1339 # includes both composition test suites and regular ones.
1340 resolved_suites = set()
1341 for suite_name in suites_seen:
1342 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011343 for sub_suite in suite:
1344 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281345 resolved_suites.add(suite_name)
1346 # At this point, every key in test_suites.pyl should be referenced.
1347 missing_suites = set(self.test_suites.keys()) - resolved_suites
1348 if missing_suites:
1349 raise BBGenErr('The following test suites were unreferenced by bots on '
1350 'the waterfalls: ' + str(missing_suites))
1351
1352 # All test suite exceptions must refer to bots on the waterfall.
1353 all_bots = set()
1354 missing_bots = set()
1355 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431356 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281357 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281358 # In order to disambiguate between bots with the same name on
1359 # different waterfalls, support has been added to various
1360 # exceptions for concatenating the waterfall name after the bot
1361 # name.
1362 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281363 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381364 removals = (exception.get('remove_from', []) +
1365 exception.get('remove_gtest_from', []) +
1366 exception.get('modifications', {}).keys())
1367 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281368 if removal not in all_bots:
1369 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411370
Ben Pastene9a010082019-09-25 20:41:371371 missing_bots = missing_bots - set(builders_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281372 if missing_bots:
1373 raise BBGenErr('The following nonexistent machines were referenced in '
1374 'the test suite exceptions: ' + str(missing_bots))
1375
Stephen Martinis0382bc12018-09-17 22:29:071376 # All mixins must be referenced
1377 seen_mixins = set()
1378 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011379 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071380 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011381 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071382 for suite in self.test_suites.values():
1383 if isinstance(suite, list):
1384 # Don't care about this, it's a composition, which shouldn't include a
1385 # swarming mixin.
1386 continue
1387
1388 for test in suite.values():
1389 if not isinstance(test, dict):
1390 # Some test suites have top level keys, which currently can't be
1391 # swarming mixin entries. Ignore them
1392 continue
1393
Stephen Martinisb72f6d22018-10-04 23:29:011394 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071395
Stephen Martinisb72f6d22018-10-04 23:29:011396 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071397 if missing_mixins:
1398 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1399 ' referenced in a waterfall, machine, or test suite.' % (
1400 str(missing_mixins)))
1401
Stephen Martinis54d64ad2018-09-21 22:16:201402
1403 def type_assert(self, node, typ, filename, verbose=False):
1404 """Asserts that the Python AST node |node| is of type |typ|.
1405
1406 If verbose is set, it prints out some helpful context lines, showing where
1407 exactly the error occurred in the file.
1408 """
1409 if not isinstance(node, typ):
1410 if verbose:
1411 lines = [""] + self.read_file(filename).splitlines()
1412
1413 context = 2
1414 lines_start = max(node.lineno - context, 0)
1415 # Add one to include the last line
1416 lines_end = min(node.lineno + context, len(lines)) + 1
1417 lines = (
1418 ['== %s ==\n' % filename] +
1419 ["<snip>\n"] +
1420 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1421 lines[lines_start:lines_start + context])] +
1422 ['-' * 80 + '\n'] +
1423 ['%d %s' % (node.lineno, lines[node.lineno])] +
1424 ['-' * (node.col_offset + 3) + '^' + '-' * (
1425 80 - node.col_offset - 4) + '\n'] +
1426 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1427 lines[node.lineno + 1:lines_end])] +
1428 ["<snip>\n"]
1429 )
1430 # Print out a useful message when a type assertion fails.
1431 for l in lines:
1432 self.print_line(l.strip())
1433
1434 node_dumped = ast.dump(node, annotate_fields=False)
1435 # If the node is huge, truncate it so everything fits in a terminal
1436 # window.
1437 if len(node_dumped) > 60: # pragma: no cover
1438 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1439 raise BBGenErr(
1440 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1441 ' be %s, is %s' % (
1442 filename, node_dumped,
1443 node.lineno, typ, type(node)))
1444
Stephen Martinis5bef0fc2020-01-06 22:47:531445 def check_ast_list_formatted(self, keys, filename, verbose,
Stephen Martinis1384ff92020-01-07 19:52:151446 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531447 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201448
Stephen Martinis5bef0fc2020-01-06 22:47:531449 Currently only checks to ensure they're correctly sorted, and that there
1450 are no duplicates.
1451
1452 Args:
1453 keys: An python list of AST nodes.
1454
1455 It's a list of AST nodes instead of a list of strings because
1456 when verbose is set, it tries to print out context of where the
1457 diffs are in the file.
1458 filename: The name of the file this node is from.
1459 verbose: If set, print out diff information about how the keys are
1460 incorrectly formatted.
1461 check_sorting: If true, checks if the list is sorted.
1462 Returns:
1463 If the keys are correctly formatted.
1464 """
1465 if not keys:
1466 return True
1467
1468 assert isinstance(keys[0], ast.Str)
1469
1470 keys_strs = [k.s for k in keys]
1471 # Keys to diff against. Used below.
1472 keys_to_diff_against = None
1473 # If the list is properly formatted.
1474 list_formatted = True
1475
1476 # Duplicates are always bad.
1477 if len(set(keys_strs)) != len(keys_strs):
1478 list_formatted = False
1479 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1480
1481 if check_sorting and sorted(keys_strs) != keys_strs:
1482 list_formatted = False
1483 if list_formatted:
1484 return True
1485
1486 if verbose:
1487 line_num = keys[0].lineno
1488 keys = [k.s for k in keys]
1489 if check_sorting:
1490 # If we have duplicates, sorting this will take care of it anyways.
1491 keys_to_diff_against = sorted(set(keys))
1492 # else, keys_to_diff_against is set above already
1493
1494 self.print_line('=' * 80)
1495 self.print_line('(First line of keys is %s)' % line_num)
1496 for line in difflib.context_diff(
1497 keys, keys_to_diff_against,
1498 fromfile='current (%r)' % filename, tofile='sorted', lineterm=''):
1499 self.print_line(line)
1500 self.print_line('=' * 80)
1501
1502 return False
1503
Stephen Martinis1384ff92020-01-07 19:52:151504 def check_ast_dict_formatted(self, node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531505 """Checks if an ast dictionary's keys are correctly formatted.
1506
1507 Just a simple wrapper around check_ast_list_formatted.
1508 Args:
1509 node: An AST node. Assumed to be a dictionary.
1510 filename: The name of the file this node is from.
1511 verbose: If set, print out diff information about how the keys are
1512 incorrectly formatted.
1513 check_sorting: If true, checks if the list is sorted.
1514 Returns:
1515 If the dictionary is correctly formatted.
1516 """
Stephen Martinis54d64ad2018-09-21 22:16:201517 keys = []
1518 # The keys of this dict are ordered as ordered in the file; normal python
1519 # dictionary keys are given an arbitrary order, but since we parsed the
1520 # file itself, the order as given in the file is preserved.
1521 for key in node.keys:
1522 self.type_assert(key, ast.Str, filename, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531523 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201524
Stephen Martinis1384ff92020-01-07 19:52:151525 return self.check_ast_list_formatted(keys, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181526
1527 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201528 # TODO(https://crbug.com/886993): Add the ability for this script to
1529 # actually format the files, rather than just complain if they're
1530 # incorrectly formatted.
1531 bad_files = set()
Stephen Martinis5bef0fc2020-01-06 22:47:531532 def parse_file(filename):
1533 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201534
Stephen Martinis5bef0fc2020-01-06 22:47:531535 Returns an AST node representing the value in the pyl file."""
Stephen Martinisf83893722018-09-19 00:02:181536 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1537
Stephen Martinisf83893722018-09-19 00:02:181538 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201539 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181540 module = parsed.body
1541
1542 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201543 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181544 if len(module) != 1: # pragma: no cover
1545 raise BBGenErr('Invalid .pyl file %s' % filename)
1546 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201547 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181548
Stephen Martinis5bef0fc2020-01-06 22:47:531549 return expr.value
1550
1551 # Handle this separately
1552 filename = 'waterfalls.pyl'
1553 value = parse_file(filename)
1554 # Value should be a list.
1555 self.type_assert(value, ast.List, filename, verbose)
1556
1557 keys = []
1558 for val in value.elts:
1559 self.type_assert(val, ast.Dict, filename, verbose)
1560 waterfall_name = None
1561 for key, val in zip(val.keys, val.values):
1562 self.type_assert(key, ast.Str, filename, verbose)
1563 if key.s == 'machines':
1564 if not self.check_ast_dict_formatted(val, filename, verbose):
1565 bad_files.add(filename)
1566
1567 if key.s == "name":
1568 self.type_assert(val, ast.Str, filename, verbose)
1569 waterfall_name = val
1570 assert waterfall_name
1571 keys.append(waterfall_name)
1572
Stephen Martinis1384ff92020-01-07 19:52:151573 if not self.check_ast_list_formatted(keys, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531574 bad_files.add(filename)
1575
1576 for filename in (
1577 'mixins.pyl',
1578 'test_suites.pyl',
1579 'test_suite_exceptions.pyl',
1580 ):
1581 value = parse_file(filename)
Stephen Martinisf83893722018-09-19 00:02:181582 # Value should be a dictionary.
Stephen Martinis54d64ad2018-09-21 22:16:201583 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181584
Stephen Martinis5bef0fc2020-01-06 22:47:531585 if not self.check_ast_dict_formatted(
1586 value, filename, verbose):
1587 bad_files.add(filename)
1588
Stephen Martinis54d64ad2018-09-21 22:16:201589 if filename == 'test_suites.pyl':
Jeff Yoon8154e582019-12-03 23:30:011590 expected_keys = ['basic_suites',
1591 'compound_suites',
1592 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201593 actual_keys = [node.s for node in value.keys]
1594 assert all(key in expected_keys for key in actual_keys), (
1595 'Invalid %r file; expected keys %r, got %r' % (
1596 filename, expected_keys, actual_keys))
1597 suite_dicts = [node for node in value.values]
1598 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011599 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201600 for suite_group in suite_dicts:
Stephen Martinis5bef0fc2020-01-06 22:47:531601 if not self.check_ast_dict_formatted(
Stephen Martinis54d64ad2018-09-21 22:16:201602 suite_group, filename, verbose):
1603 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181604
Stephen Martinis5bef0fc2020-01-06 22:47:531605 for key, suite in zip(value.keys, value.values):
1606 # The compound suites are checked in
1607 # 'check_composition_type_test_suites()'
1608 if key.s == 'basic_suites':
1609 for group in suite.values:
Stephen Martinis1384ff92020-01-07 19:52:151610 if not self.check_ast_dict_formatted(group, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531611 bad_files.add(filename)
1612 break
Stephen Martinis54d64ad2018-09-21 22:16:201613
Stephen Martinis5bef0fc2020-01-06 22:47:531614 elif filename == 'test_suite_exceptions.pyl':
1615 # Check the values for each test.
1616 for test in value.values:
1617 for kind, node in zip(test.keys, test.values):
1618 if isinstance(node, ast.Dict):
Stephen Martinis1384ff92020-01-07 19:52:151619 if not self.check_ast_dict_formatted(node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531620 bad_files.add(filename)
1621 elif kind.s == 'remove_from':
1622 # Don't care about sorting; these are usually grouped, since the
1623 # same bug can affect multiple builders. Do want to make sure
1624 # there aren't duplicates.
1625 if not self.check_ast_list_formatted(node.elts, filename, verbose,
1626 check_sorting=False):
1627 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181628
1629 if bad_files:
1630 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201631 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531632 'unsorted, or have duplicates. Re-run this with --verbose to see '
1633 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181634
Kenneth Russelleb60cbd22017-12-05 07:54:281635 def check_output_file_consistency(self, verbose=False):
1636 self.load_configuration_files()
1637 # All waterfalls must have been written by this script already.
1638 self.resolve_configuration_files()
1639 ungenerated_waterfalls = set()
1640 for waterfall in self.waterfalls:
Gregory Gutermand1dd3b82020-03-02 21:53:481641 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111642 file_path = waterfall['name'] + '.json'
1643 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281644 if expected != current:
1645 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321646 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501647 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281648 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321649 'contents:')
1650 for line in difflib.unified_diff(
1651 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501652 current.splitlines(),
1653 fromfile='expected', tofile='current'):
1654 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281655 if ungenerated_waterfalls:
1656 raise BBGenErr('The following waterfalls have not been properly '
1657 'autogenerated by generate_buildbot_json.py: ' +
1658 str(ungenerated_waterfalls))
1659
1660 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501661 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281662 self.check_output_file_consistency(verbose) # pragma: no cover
1663
1664 def parse_args(self, argv): # pragma: no cover
Karen Qiane24b7ee2019-02-12 23:37:061665
1666 # RawTextHelpFormatter allows for styling of help statement
1667 parser = argparse.ArgumentParser(formatter_class=
1668 argparse.RawTextHelpFormatter)
1669
1670 group = parser.add_mutually_exclusive_group()
1671 group.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281672 '-c', '--check', action='store_true', help=
1673 'Do consistency checks of configuration and generated files and then '
1674 'exit. Used during presubmit. Causes the tool to not generate any files.')
Karen Qiane24b7ee2019-02-12 23:37:061675 group.add_argument(
1676 '--query', type=str, help=
1677 ("Returns raw JSON information of buildbots and tests.\n" +
1678 "Examples:\n" +
1679 " List all bots (all info):\n" +
1680 " --query bots\n\n" +
1681 " List all bots and only their associated tests:\n" +
1682 " --query bots/tests\n\n" +
1683 " List all information about 'bot1' " +
1684 "(make sure you have quotes):\n" +
1685 " --query bot/'bot1'\n\n" +
1686 " List tests running for 'bot1' (make sure you have quotes):\n" +
1687 " --query bot/'bot1'/tests\n\n" +
1688 " List all tests:\n" +
1689 " --query tests\n\n" +
1690 " List all tests and the bots running them:\n" +
1691 " --query tests/bots\n\n"+
1692 " List all tests that satisfy multiple parameters\n" +
1693 " (separation of parameters by '&' symbol):\n" +
1694 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
1695 " List all tests that run with a specific flag:\n" +
1696 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
1697 " List specific test (make sure you have quotes):\n"
1698 " --query test/'test1'\n\n"
1699 " List all bots running 'test1' " +
1700 "(make sure you have quotes):\n" +
1701 " --query test/'test1'/bots" ))
Kenneth Russelleb60cbd22017-12-05 07:54:281702 parser.add_argument(
1703 '-n', '--new-files', action='store_true', help=
1704 'Write output files as .new.json. Useful during development so old and '
1705 'new files can be looked at side-by-side.')
1706 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501707 '-v', '--verbose', action='store_true', help=
1708 'Increases verbosity. Affects consistency checks.')
1709 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281710 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1711 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111712 parser.add_argument(
1713 '--pyl-files-dir', type=os.path.realpath,
1714 help='Path to the directory containing the input .pyl files.')
Karen Qiane24b7ee2019-02-12 23:37:061715 parser.add_argument(
1716 '--json', help=
1717 ("Outputs results into a json file. Only works with query function.\n" +
1718 "Examples:\n" +
1719 " Outputs file into specified json file: \n" +
1720 " --json <file-name-here.json>"))
Kenneth Russelleb60cbd22017-12-05 07:54:281721 self.args = parser.parse_args(argv)
Karen Qiane24b7ee2019-02-12 23:37:061722 if self.args.json and not self.args.query:
1723 parser.error("The --json flag can only be used with --query.")
1724
1725 def does_test_match(self, test_info, params_dict):
1726 """Checks to see if the test matches the parameters given.
1727
1728 Compares the provided test_info with the params_dict to see
1729 if the bot matches the parameters given. If so, returns True.
1730 Else, returns false.
1731
1732 Args:
1733 test_info (dict): Information about a specific bot provided
1734 in the format shown in waterfalls.pyl
1735 params_dict (dict): Dictionary of parameters and their values
1736 to look for in the bot
1737 Ex: {
1738 'device_os':'android',
1739 '--flag':True,
1740 'mixins': ['mixin1', 'mixin2'],
1741 'ex_key':'ex_value'
1742 }
1743
1744 """
1745 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
1746 'kvm', 'pool', 'integrity'] # dimension parameters
1747 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
1748 'can_use_on_swarming_builders']
1749 for param in params_dict:
1750 # if dimension parameter
1751 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
1752 if not 'swarming' in test_info:
1753 return False
1754 swarming = test_info['swarming']
1755 if param in SWARMING_PARAMS:
1756 if not param in swarming:
1757 return False
1758 if not str(swarming[param]) == params_dict[param]:
1759 return False
1760 else:
1761 if not 'dimension_sets' in swarming:
1762 return False
1763 d_set = swarming['dimension_sets']
1764 # only looking at the first dimension set
1765 if not param in d_set[0]:
1766 return False
1767 if not d_set[0][param] == params_dict[param]:
1768 return False
1769
1770 # if flag
1771 elif param.startswith('--'):
1772 if not 'args' in test_info:
1773 return False
1774 if not param in test_info['args']:
1775 return False
1776
1777 # not dimension parameter/flag/mixin
1778 else:
1779 if not param in test_info:
1780 return False
1781 if not test_info[param] == params_dict[param]:
1782 return False
1783 return True
1784 def error_msg(self, msg):
1785 """Prints an error message.
1786
1787 In addition to a catered error message, also prints
1788 out where the user can find more help. Then, program exits.
1789 """
1790 self.print_line(msg + (' If you need more information, ' +
1791 'please run with -h or --help to see valid commands.'))
1792 sys.exit(1)
1793
1794 def find_bots_that_run_test(self, test, bots):
1795 matching_bots = []
1796 for bot in bots:
1797 bot_info = bots[bot]
1798 tests = self.flatten_tests_for_bot(bot_info)
1799 for test_info in tests:
1800 test_name = ""
1801 if 'name' in test_info:
1802 test_name = test_info['name']
1803 elif 'test' in test_info:
1804 test_name = test_info['test']
1805 if not test_name == test:
1806 continue
1807 matching_bots.append(bot)
1808 return matching_bots
1809
1810 def find_tests_with_params(self, tests, params_dict):
1811 matching_tests = []
1812 for test_name in tests:
1813 test_info = tests[test_name]
1814 if not self.does_test_match(test_info, params_dict):
1815 continue
1816 if not test_name in matching_tests:
1817 matching_tests.append(test_name)
1818 return matching_tests
1819
1820 def flatten_waterfalls_for_query(self, waterfalls):
1821 bots = {}
1822 for waterfall in waterfalls:
Gregory Gutermand1dd3b82020-03-02 21:53:481823 waterfall_json = json.loads(self.generate_waterfall_json(waterfall))
1824 for bot in waterfall_json:
1825 bot_info = waterfall_json[bot]
Karen Qiane24b7ee2019-02-12 23:37:061826 if 'AAAAA' not in bot:
1827 bots[bot] = bot_info
1828 return bots
1829
1830 def flatten_tests_for_bot(self, bot_info):
1831 """Returns a list of flattened tests.
1832
1833 Returns a list of tests not grouped by test category
1834 for a specific bot.
1835 """
1836 TEST_CATS = self.get_test_generator_map().keys()
1837 tests = []
1838 for test_cat in TEST_CATS:
1839 if not test_cat in bot_info:
1840 continue
1841 test_cat_tests = bot_info[test_cat]
1842 tests = tests + test_cat_tests
1843 return tests
1844
1845 def flatten_tests_for_query(self, test_suites):
1846 """Returns a flattened dictionary of tests.
1847
1848 Returns a dictionary of tests associate with their
1849 configuration, not grouped by their test suite.
1850 """
1851 tests = {}
1852 for test_suite in test_suites.itervalues():
1853 for test in test_suite:
1854 test_info = test_suite[test]
1855 test_name = test
1856 if 'name' in test_info:
1857 test_name = test_info['name']
1858 tests[test_name] = test_info
1859 return tests
1860
1861 def parse_query_filter_params(self, params):
1862 """Parses the filter parameters.
1863
1864 Creates a dictionary from the parameters provided
1865 to filter the bot array.
1866 """
1867 params_dict = {}
1868 for p in params:
1869 # flag
1870 if p.startswith("--"):
1871 params_dict[p] = True
1872 else:
1873 pair = p.split(":")
1874 if len(pair) != 2:
1875 self.error_msg('Invalid command.')
1876 # regular parameters
1877 if pair[1].lower() == "true":
1878 params_dict[pair[0]] = True
1879 elif pair[1].lower() == "false":
1880 params_dict[pair[0]] = False
1881 else:
1882 params_dict[pair[0]] = pair[1]
1883 return params_dict
1884
1885 def get_test_suites_dict(self, bots):
1886 """Returns a dictionary of bots and their tests.
1887
1888 Returns a dictionary of bots and a list of their associated tests.
1889 """
1890 test_suite_dict = dict()
1891 for bot in bots:
1892 bot_info = bots[bot]
1893 tests = self.flatten_tests_for_bot(bot_info)
1894 test_suite_dict[bot] = tests
1895 return test_suite_dict
1896
1897 def output_query_result(self, result, json_file=None):
1898 """Outputs the result of the query.
1899
1900 If a json file parameter name is provided, then
1901 the result is output into the json file. If not,
1902 then the result is printed to the console.
1903 """
1904 output = json.dumps(result, indent=2)
1905 if json_file:
1906 self.write_file(json_file, output)
1907 else:
1908 self.print_line(output)
1909 return
1910
1911 def query(self, args):
1912 """Queries tests or bots.
1913
1914 Depending on the arguments provided, outputs a json of
1915 tests or bots matching the appropriate optional parameters provided.
1916 """
1917 # split up query statement
1918 query = args.query.split('/')
1919 self.load_configuration_files()
1920 self.resolve_configuration_files()
1921
1922 # flatten bots json
1923 tests = self.test_suites
1924 bots = self.flatten_waterfalls_for_query(self.waterfalls)
1925
1926 cmd_class = query[0]
1927
1928 # For queries starting with 'bots'
1929 if cmd_class == "bots":
1930 if len(query) == 1:
1931 return self.output_query_result(bots, args.json)
1932 # query with specific parameters
1933 elif len(query) == 2:
1934 if query[1] == 'tests':
1935 test_suites_dict = self.get_test_suites_dict(bots)
1936 return self.output_query_result(test_suites_dict, args.json)
1937 else:
1938 self.error_msg("This query should be in the format: bots/tests.")
1939
1940 else:
1941 self.error_msg("This query should have 0 or 1 '/', found %s instead."
1942 % str(len(query)-1))
1943
1944 # For queries starting with 'bot'
1945 elif cmd_class == "bot":
1946 if not len(query) == 2 and not len(query) == 3:
1947 self.error_msg("Command should have 1 or 2 '/', found %s instead."
1948 % str(len(query)-1))
1949 bot_id = query[1]
1950 if not bot_id in bots:
1951 self.error_msg("No bot named '" + bot_id + "' found.")
1952 bot_info = bots[bot_id]
1953 if len(query) == 2:
1954 return self.output_query_result(bot_info, args.json)
1955 if not query[2] == 'tests':
1956 self.error_msg("The query should be in the format:" +
1957 "bot/<bot-name>/tests.")
1958
1959 bot_tests = self.flatten_tests_for_bot(bot_info)
1960 return self.output_query_result(bot_tests, args.json)
1961
1962 # For queries starting with 'tests'
1963 elif cmd_class == "tests":
1964 if not len(query) == 1 and not len(query) == 2:
1965 self.error_msg("The query should have 0 or 1 '/', found %s instead."
1966 % str(len(query)-1))
1967 flattened_tests = self.flatten_tests_for_query(tests)
1968 if len(query) == 1:
1969 return self.output_query_result(flattened_tests, args.json)
1970
1971 # create params dict
1972 params = query[1].split('&')
1973 params_dict = self.parse_query_filter_params(params)
1974 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
1975 return self.output_query_result(matching_bots)
1976
1977 # For queries starting with 'test'
1978 elif cmd_class == "test":
1979 if not len(query) == 2 and not len(query) == 3:
1980 self.error_msg("The query should have 1 or 2 '/', found %s instead."
1981 % str(len(query)-1))
1982 test_id = query[1]
1983 if len(query) == 2:
1984 flattened_tests = self.flatten_tests_for_query(tests)
1985 for test in flattened_tests:
1986 if test == test_id:
1987 return self.output_query_result(flattened_tests[test], args.json)
1988 self.error_msg("There is no test named %s." % test_id)
1989 if not query[2] == 'bots':
1990 self.error_msg("The query should be in the format: " +
1991 "test/<test-name>/bots")
1992 bots_for_test = self.find_bots_that_run_test(test_id, bots)
1993 return self.output_query_result(bots_for_test)
1994
1995 else:
1996 self.error_msg("Your command did not match any valid commands." +
1997 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:281998
1999 def main(self, argv): # pragma: no cover
2000 self.parse_args(argv)
2001 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502002 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062003 elif self.args.query:
2004 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282005 else:
2006 self.generate_waterfalls()
2007 return 0
2008
2009if __name__ == "__main__": # pragma: no cover
2010 generator = BBJSONGenerator()
John Budorick699282e2019-02-13 01:27:332011 sys.exit(generator.main(sys.argv[1:]))