blob: 5a0107438fa540382045ee16a8532aa16b64535d [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
Greg Guterman5c6144152020-02-28 20:08:5318import re
Kenneth Russelleb60cbd22017-12-05 07:54:2819import string
20import sys
John Budorick826d5ed2017-12-28 19:27:3221import traceback
Kenneth Russelleb60cbd22017-12-05 07:54:2822
23THIS_DIR = os.path.dirname(os.path.abspath(__file__))
24
25
26class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4927 def __init__(self, message):
28 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2829
30
Kenneth Russell8ceeabf2017-12-11 17:53:2831# This class is only present to accommodate certain machines on
32# chromium.android.fyi which run certain tests as instrumentation
33# tests, but not as gtests. If this discrepancy were fixed then the
34# notion could be removed.
35class TestSuiteTypes(object):
36 GTEST = 'gtest'
37
38
Kenneth Russelleb60cbd22017-12-05 07:54:2839class BaseGenerator(object):
40 def __init__(self, bb_gen):
41 self.bb_gen = bb_gen
42
Kenneth Russell8ceeabf2017-12-11 17:53:2843 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2844 raise NotImplementedError()
45
46 def sort(self, tests):
47 raise NotImplementedError()
48
49
Kenneth Russell8ceeabf2017-12-11 17:53:2850def cmp_tests(a, b):
51 # Prefer to compare based on the "test" key.
52 val = cmp(a['test'], b['test'])
53 if val != 0:
54 return val
55 if 'name' in a and 'name' in b:
56 return cmp(a['name'], b['name']) # pragma: no cover
57 if 'name' not in a and 'name' not in b:
58 return 0 # pragma: no cover
59 # Prefer to put variants of the same test after the first one.
60 if 'name' in a:
61 return 1
62 # 'name' is in b.
63 return -1 # pragma: no cover
64
65
Kenneth Russell8a386d42018-06-02 09:48:0166class GPUTelemetryTestGenerator(BaseGenerator):
Bo Liu555a0f92019-03-29 12:11:5667
68 def __init__(self, bb_gen, is_android_webview=False):
Kenneth Russell8a386d42018-06-02 09:48:0169 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5670 self._is_android_webview = is_android_webview
Kenneth Russell8a386d42018-06-02 09:48:0171
72 def generate(self, waterfall, tester_name, tester_config, input_tests):
73 isolated_scripts = []
74 for test_name, test_config in sorted(input_tests.iteritems()):
75 test = self.bb_gen.generate_gpu_telemetry_test(
Bo Liu555a0f92019-03-29 12:11:5676 waterfall, tester_name, tester_config, test_name, test_config,
77 self._is_android_webview)
Kenneth Russell8a386d42018-06-02 09:48:0178 if test:
79 isolated_scripts.append(test)
80 return isolated_scripts
81
82 def sort(self, tests):
83 return sorted(tests, key=lambda x: x['name'])
84
85
Kenneth Russelleb60cbd22017-12-05 07:54:2886class GTestGenerator(BaseGenerator):
87 def __init__(self, bb_gen):
88 super(GTestGenerator, self).__init__(bb_gen)
89
Kenneth Russell8ceeabf2017-12-11 17:53:2890 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2891 # The relative ordering of some of the tests is important to
92 # minimize differences compared to the handwritten JSON files, since
93 # Python's sorts are stable and there are some tests with the same
94 # key (see gles2_conform_d3d9_test and similar variants). Avoid
95 # losing the order by avoiding coalescing the dictionaries into one.
96 gtests = []
97 for test_name, test_config in sorted(input_tests.iteritems()):
Jeff Yoon67c3e832020-02-08 07:39:3898 # Variants allow more than one definition for a given test, and is defined
99 # in array format from resolve_variants().
100 if not isinstance(test_config, list):
101 test_config = [test_config]
102
103 for config in test_config:
104 test = self.bb_gen.generate_gtest(
105 waterfall, tester_name, tester_config, test_name, config)
106 if test:
107 # generate_gtest may veto the test generation on this tester.
108 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28109 return gtests
110
111 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28112 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28113
114
115class IsolatedScriptTestGenerator(BaseGenerator):
116 def __init__(self, bb_gen):
117 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
118
Kenneth Russell8ceeabf2017-12-11 17:53:28119 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28120 isolated_scripts = []
121 for test_name, test_config in sorted(input_tests.iteritems()):
122 test = self.bb_gen.generate_isolated_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28123 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28124 if test:
125 isolated_scripts.append(test)
126 return isolated_scripts
127
128 def sort(self, tests):
129 return sorted(tests, key=lambda x: x['name'])
130
131
132class ScriptGenerator(BaseGenerator):
133 def __init__(self, bb_gen):
134 super(ScriptGenerator, self).__init__(bb_gen)
135
Kenneth Russell8ceeabf2017-12-11 17:53:28136 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28137 scripts = []
138 for test_name, test_config in sorted(input_tests.iteritems()):
139 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28140 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28141 if test:
142 scripts.append(test)
143 return scripts
144
145 def sort(self, tests):
146 return sorted(tests, key=lambda x: x['name'])
147
148
149class JUnitGenerator(BaseGenerator):
150 def __init__(self, bb_gen):
151 super(JUnitGenerator, self).__init__(bb_gen)
152
Kenneth Russell8ceeabf2017-12-11 17:53:28153 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28154 scripts = []
155 for test_name, test_config in sorted(input_tests.iteritems()):
156 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28157 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28158 if test:
159 scripts.append(test)
160 return scripts
161
162 def sort(self, tests):
163 return sorted(tests, key=lambda x: x['test'])
164
165
166class CTSGenerator(BaseGenerator):
167 def __init__(self, bb_gen):
168 super(CTSGenerator, self).__init__(bb_gen)
169
Kenneth Russell8ceeabf2017-12-11 17:53:28170 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28171 # These only contain one entry and it's the contents of the input tests'
172 # dictionary, verbatim.
173 cts_tests = []
174 cts_tests.append(input_tests)
175 return cts_tests
176
177 def sort(self, tests):
178 return tests
179
180
181class InstrumentationTestGenerator(BaseGenerator):
182 def __init__(self, bb_gen):
183 super(InstrumentationTestGenerator, self).__init__(bb_gen)
184
Kenneth Russell8ceeabf2017-12-11 17:53:28185 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28186 scripts = []
187 for test_name, test_config in sorted(input_tests.iteritems()):
188 test = self.bb_gen.generate_instrumentation_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28189 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28190 if test:
191 scripts.append(test)
192 return scripts
193
194 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28195 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28196
197
Jeff Yoon67c3e832020-02-08 07:39:38198def check_compound_references(other_test_suites=None,
199 sub_suite=None,
200 suite=None,
201 target_test_suites=None,
202 test_type=None,
203 **kwargs):
204 """Ensure comound reference's don't target other compounds"""
205 del kwargs
206 if sub_suite in other_test_suites or sub_suite in target_test_suites:
207 raise BBGenErr('%s may not refer to other composition type test '
208 'suites (error found while processing %s)'
209 % (test_type, suite))
210
211def check_basic_references(basic_suites=None,
212 sub_suite=None,
213 suite=None,
214 **kwargs):
215 """Ensure test has a basic suite reference"""
216 del kwargs
217 if sub_suite not in basic_suites:
218 raise BBGenErr('Unable to find reference to %s while processing %s'
219 % (sub_suite, suite))
220
221def check_conflicting_definitions(basic_suites=None,
222 seen_tests=None,
223 sub_suite=None,
224 suite=None,
225 test_type=None,
226 **kwargs):
227 """Ensure that if a test is reachable via multiple basic suites,
228 all of them have an identical definition of the tests.
229 """
230 del kwargs
231 for test_name in basic_suites[sub_suite]:
232 if (test_name in seen_tests and
233 basic_suites[sub_suite][test_name] !=
234 basic_suites[seen_tests[test_name]][test_name]):
235 raise BBGenErr('Conflicting test definitions for %s from %s '
236 'and %s in %s (error found while processing %s)'
237 % (test_name, seen_tests[test_name], sub_suite,
238 test_type, suite))
239 seen_tests[test_name] = sub_suite
240
241def check_matrix_identifier(sub_suite=None,
242 suite=None,
243 suite_def=None,
244 **kwargs):
245 """Ensure 'idenfitier' is defined for each variant"""
246 del kwargs
247 sub_suite_config = suite_def[sub_suite]
248 for variant in sub_suite_config.get('variants', []):
249 if not 'identifier' in variant:
250 raise BBGenErr('Missing required identifier field in matrix '
251 'compound suite %s, %s' % (suite, sub_suite))
252
253
Kenneth Russelleb60cbd22017-12-05 07:54:28254class BBJSONGenerator(object):
255 def __init__(self):
256 self.this_dir = THIS_DIR
257 self.args = None
258 self.waterfalls = None
259 self.test_suites = None
260 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01261 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41262 self.gn_isolate_map = None
Kenneth Russelleb60cbd22017-12-05 07:54:28263
264 def generate_abs_file_path(self, relative_path):
265 return os.path.join(self.this_dir, relative_path) # pragma: no cover
266
Stephen Martinis7eb8b612018-09-21 00:17:50267 def print_line(self, line):
268 # Exists so that tests can mock
269 print line # pragma: no cover
270
Kenneth Russelleb60cbd22017-12-05 07:54:28271 def read_file(self, relative_path):
272 with open(self.generate_abs_file_path(
273 relative_path)) as fp: # pragma: no cover
274 return fp.read() # pragma: no cover
275
276 def write_file(self, relative_path, contents):
277 with open(self.generate_abs_file_path(
278 relative_path), 'wb') as fp: # pragma: no cover
279 fp.write(contents) # pragma: no cover
280
Zhiling Huangbe008172018-03-08 19:13:11281 def pyl_file_path(self, filename):
282 if self.args and self.args.pyl_files_dir:
283 return os.path.join(self.args.pyl_files_dir, filename)
284 return filename
285
Kenneth Russelleb60cbd22017-12-05 07:54:28286 def load_pyl_file(self, filename):
287 try:
Zhiling Huangbe008172018-03-08 19:13:11288 return ast.literal_eval(self.read_file(
289 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28290 except (SyntaxError, ValueError) as e: # pragma: no cover
291 raise BBGenErr('Failed to parse pyl file "%s": %s' %
292 (filename, e)) # pragma: no cover
293
Kenneth Russell8a386d42018-06-02 09:48:01294 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
295 # Currently it is only mandatory for bots which run GPU tests. Change these to
296 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28297 def is_android(self, tester_config):
298 return tester_config.get('os_type') == 'android'
299
Ben Pastenea9e583b2019-01-16 02:57:26300 def is_chromeos(self, tester_config):
301 return tester_config.get('os_type') == 'chromeos'
302
Kenneth Russell8a386d42018-06-02 09:48:01303 def is_linux(self, tester_config):
304 return tester_config.get('os_type') == 'linux'
305
Kai Ninomiya40de9f52019-10-18 21:38:49306 def is_mac(self, tester_config):
307 return tester_config.get('os_type') == 'mac'
308
309 def is_win(self, tester_config):
310 return tester_config.get('os_type') == 'win'
311
312 def is_win64(self, tester_config):
313 return (tester_config.get('os_type') == 'win' and
314 tester_config.get('browser_config') == 'release_x64')
315
Kenneth Russelleb60cbd22017-12-05 07:54:28316 def get_exception_for_test(self, test_name, test_config):
317 # gtests may have both "test" and "name" fields, and usually, if the "name"
318 # field is specified, it means that the same test is being repurposed
319 # multiple times with different command line arguments. To handle this case,
320 # prefer to lookup per the "name" field of the test itself, as opposed to
321 # the "test_name", which is actually the "test" field.
322 if 'name' in test_config:
323 return self.exceptions.get(test_config['name'])
324 else:
325 return self.exceptions.get(test_name)
326
Nico Weberb0b3f5862018-07-13 18:45:15327 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28328 # Currently, the only reason a test should not run on a given tester is that
329 # it's in the exceptions. (Once the GPU waterfall generation script is
330 # incorporated here, the rules will become more complex.)
331 exception = self.get_exception_for_test(test_name, test_config)
332 if not exception:
333 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28334 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28335 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28336 if remove_from:
337 if tester_name in remove_from:
338 return False
339 # TODO(kbr): this code path was added for some tests (including
340 # android_webview_unittests) on one machine (Nougat Phone
341 # Tester) which exists with the same name on two waterfalls,
342 # chromium.android and chromium.fyi; the tests are run on one
343 # but not the other. Once the bots are all uniquely named (a
344 # different ongoing project) this code should be removed.
345 # TODO(kbr): add coverage.
346 return (tester_name + ' ' + waterfall['name']
347 not in remove_from) # pragma: no cover
348 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28349
Nico Weber79dc5f6852018-07-13 19:38:49350 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28351 exception = self.get_exception_for_test(test_name, test)
352 if not exception:
353 return None
Nico Weber79dc5f6852018-07-13 19:38:49354 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28355
Brian Sheedye6ea0ee2019-07-11 02:54:37356 def get_test_replacements(self, test, test_name, tester_name):
357 exception = self.get_exception_for_test(test_name, test)
358 if not exception:
359 return None
360 return exception.get('replacements', {}).get(tester_name)
361
Kenneth Russell8a386d42018-06-02 09:48:01362 def merge_command_line_args(self, arr, prefix, splitter):
363 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01364 idx = 0
365 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01366 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01367 while idx < len(arr):
368 flag = arr[idx]
369 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01370 if flag.startswith(prefix):
371 arg = flag[prefix_len:]
372 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01373 if first_idx < 0:
374 first_idx = idx
375 else:
376 delete_current_entry = True
377 if delete_current_entry:
378 del arr[idx]
379 else:
380 idx += 1
381 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01382 arr[first_idx] = prefix + splitter.join(accumulated_args)
383 return arr
384
385 def maybe_fixup_args_array(self, arr):
386 # The incoming array of strings may be an array of command line
387 # arguments. To make it easier to turn on certain features per-bot or
388 # per-test-suite, look specifically for certain flags and merge them
389 # appropriately.
390 # --enable-features=Feature1 --enable-features=Feature2
391 # are merged to:
392 # --enable-features=Feature1,Feature2
393 # and:
394 # --extra-browser-args=arg1 --extra-browser-args=arg2
395 # are merged to:
396 # --extra-browser-args=arg1 arg2
397 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
398 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01399 return arr
400
Kenneth Russelleb60cbd22017-12-05 07:54:28401 def dictionary_merge(self, a, b, path=None, update=True):
402 """http://stackoverflow.com/questions/7204805/
403 python-dictionaries-of-dictionaries-merge
404 merges b into a
405 """
406 if path is None:
407 path = []
408 for key in b:
409 if key in a:
410 if isinstance(a[key], dict) and isinstance(b[key], dict):
411 self.dictionary_merge(a[key], b[key], path + [str(key)])
412 elif a[key] == b[key]:
413 pass # same leaf value
414 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06415 # Args arrays are lists of strings. Just concatenate them,
416 # and don't sort them, in order to keep some needed
417 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28418 if all(isinstance(x, str)
419 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01420 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28421 else:
422 # TODO(kbr): this only works properly if the two arrays are
423 # the same length, which is currently always the case in the
424 # swarming dimension_sets that we have to merge. It will fail
425 # to merge / override 'args' arrays which are different
426 # length.
427 for idx in xrange(len(b[key])):
428 try:
429 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
430 path + [str(key), str(idx)],
431 update=update)
Jeff Yoon8154e582019-12-03 23:30:01432 except (IndexError, TypeError):
433 raise BBGenErr('Error merging lists by key "%s" from source %s '
434 'into target %s at index %s. Verify target list '
435 'length is equal or greater than source'
436 % (str(key), str(b), str(a), str(idx)))
John Budorick5bc387fe2019-05-09 20:02:53437 elif update:
438 if b[key] is None:
439 del a[key]
440 else:
441 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28442 else:
443 raise BBGenErr('Conflict at %s' % '.'.join(
444 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53445 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28446 a[key] = b[key]
447 return a
448
John Budorickab108712018-09-01 00:12:21449 def initialize_args_for_test(
450 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21451 args = []
452 args.extend(generated_test.get('args', []))
453 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22454
Kenneth Russell8a386d42018-06-02 09:48:01455 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21456 val = generated_test.pop(key, [])
457 if fn(tester_config):
458 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01459
460 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
461 add_conditional_args('linux_args', self.is_linux)
462 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36463 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49464 add_conditional_args('mac_args', self.is_mac)
465 add_conditional_args('win_args', self.is_win)
466 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01467
John Budorickab108712018-09-01 00:12:21468 for key in additional_arg_keys or []:
469 args.extend(generated_test.pop(key, []))
470 args.extend(tester_config.get(key, []))
471
472 if args:
473 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01474
Kenneth Russelleb60cbd22017-12-05 07:54:28475 def initialize_swarming_dictionary_for_test(self, generated_test,
476 tester_config):
477 if 'swarming' not in generated_test:
478 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28479 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
480 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38481 'can_use_on_swarming_builders': tester_config.get('use_swarming',
482 True)
Dirk Pranke81ff51c2017-12-09 19:24:28483 })
Kenneth Russelleb60cbd22017-12-05 07:54:28484 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03485 if ('dimension_sets' not in generated_test['swarming'] and
486 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28487 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
488 tester_config['swarming']['dimension_sets'])
489 self.dictionary_merge(generated_test['swarming'],
490 tester_config['swarming'])
491 # Apply any Android-specific Swarming dimensions after the generic ones.
492 if 'android_swarming' in generated_test:
493 if self.is_android(tester_config): # pragma: no cover
494 self.dictionary_merge(
495 generated_test['swarming'],
496 generated_test['android_swarming']) # pragma: no cover
497 del generated_test['android_swarming'] # pragma: no cover
498
499 def clean_swarming_dictionary(self, swarming_dict):
500 # Clean out redundant entries from a test's "swarming" dictionary.
501 # This is really only needed to retain 100% parity with the
502 # handwritten JSON files, and can be removed once all the files are
503 # autogenerated.
504 if 'shards' in swarming_dict:
505 if swarming_dict['shards'] == 1: # pragma: no cover
506 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24507 if 'hard_timeout' in swarming_dict:
508 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
509 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43510 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28511 # Remove all other keys.
512 for k in swarming_dict.keys(): # pragma: no cover
513 if k != 'can_use_on_swarming_builders': # pragma: no cover
514 del swarming_dict[k] # pragma: no cover
515
Stephen Martinis0382bc12018-09-17 22:29:07516 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
517 waterfall):
518 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01519 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07520 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28521 # See if there are any exceptions that need to be merged into this
522 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49523 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28524 if modifications:
525 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23526 if 'swarming' in test:
527 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28528 # Ensure all Android Swarming tests run only on userdebug builds if another
529 # build type was not specified.
530 if 'swarming' in test and self.is_android(tester_config):
531 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22532 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28533 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37534 self.replace_test_args(test, test_name, tester_name)
Ben Pastenee012aea42019-05-14 22:32:28535
Kenneth Russelleb60cbd22017-12-05 07:54:28536 return test
537
Brian Sheedye6ea0ee2019-07-11 02:54:37538 def replace_test_args(self, test, test_name, tester_name):
539 replacements = self.get_test_replacements(
540 test, test_name, tester_name) or {}
541 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
542 for key, replacement_dict in replacements.iteritems():
543 if key not in valid_replacement_keys:
544 raise BBGenErr(
545 'Given replacement key %s for %s on %s is not in the list of valid '
546 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
547 for replacement_key, replacement_val in replacement_dict.iteritems():
548 found_key = False
549 for i, test_key in enumerate(test.get(key, [])):
550 # Handle both the key/value being replaced being defined as two
551 # separate items or as key=value.
552 if test_key == replacement_key:
553 found_key = True
554 # Handle flags without values.
555 if replacement_val == None:
556 del test[key][i]
557 else:
558 test[key][i+1] = replacement_val
559 break
560 elif test_key.startswith(replacement_key + '='):
561 found_key = True
562 if replacement_val == None:
563 del test[key][i]
564 else:
565 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
566 break
567 if not found_key:
568 raise BBGenErr('Could not find %s in existing list of values for key '
569 '%s in %s on %s' % (replacement_key, key, test_name,
570 tester_name))
571
Shenghua Zhangaba8bad2018-02-07 02:12:09572 def add_common_test_properties(self, test, tester_config):
573 if tester_config.get('use_multi_dimension_trigger_script'):
Kenneth Russell73c3bd8b2018-10-19 22:30:19574 # Assumes update_and_cleanup_test has already been called, so the
575 # builder's mixins have been flattened into the test.
Shenghua Zhangaba8bad2018-02-07 02:12:09576 test['trigger_script'] = {
577 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
578 'args': [
579 '--multiple-trigger-configs',
Kenneth Russell73c3bd8b2018-10-19 22:30:19580 json.dumps(test['swarming']['dimension_sets'] +
Shenghua Zhangaba8bad2018-02-07 02:12:09581 tester_config.get('alternate_swarming_dimensions', [])),
582 '--multiple-dimension-script-verbose',
583 'True'
584 ],
585 }
Ben Pastenea9e583b2019-01-16 02:57:26586 elif self.is_chromeos(tester_config) and tester_config.get('use_swarming',
587 True):
588 # The presence of the "device_type" dimension indicates that the tests
589 # are targetting CrOS hardware and so need the special trigger script.
590 dimension_sets = tester_config['swarming']['dimension_sets']
591 if all('device_type' in ds for ds in dimension_sets):
592 test['trigger_script'] = {
593 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
594 }
Shenghua Zhangaba8bad2018-02-07 02:12:09595
Ben Pastene858f4be2019-01-09 23:52:09596 def add_android_presentation_args(self, tester_config, test_name, result):
597 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38598 bucket = tester_config.get('results_bucket', 'chromium-result-details')
599 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09600 if (result['swarming']['can_use_on_swarming_builders'] and not
601 tester_config.get('skip_merge_script', False)):
602 result['merge'] = {
603 'args': [
604 '--bucket',
John Budorick262ae112019-07-12 19:24:38605 bucket,
Ben Pastene858f4be2019-01-09 23:52:09606 '--test-name',
607 test_name
608 ],
609 'script': '//build/android/pylib/results/presentation/'
610 'test_results_presentation.py',
611 }
612 if not tester_config.get('skip_cipd_packages', False):
Ben Pastenee5949ea82019-01-10 21:45:26613 cipd_packages = result['swarming'].get('cipd_packages', [])
614 cipd_packages.append(
Ben Pastene858f4be2019-01-09 23:52:09615 {
616 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
617 'location': 'bin',
618 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
619 }
Ben Pastenee5949ea82019-01-10 21:45:26620 )
621 result['swarming']['cipd_packages'] = cipd_packages
Ben Pastene858f4be2019-01-09 23:52:09622 if not tester_config.get('skip_output_links', False):
623 result['swarming']['output_links'] = [
624 {
625 'link': [
626 'https://luci-logdog.appspot.com/v/?s',
627 '=android%2Fswarming%2Flogcats%2F',
628 '${TASK_ID}%2F%2B%2Funified_logcats',
629 ],
630 'name': 'shard #${SHARD_INDEX} logcats',
631 },
632 ]
633 if args:
634 result['args'] = args
635
Kenneth Russelleb60cbd22017-12-05 07:54:28636 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
637 test_config):
638 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15639 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28640 return None
641 result = copy.deepcopy(test_config)
642 if 'test' in result:
643 result['name'] = test_name
644 else:
645 result['test'] = test_name
646 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21647
648 self.initialize_args_for_test(
649 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28650 if self.is_android(tester_config) and tester_config.get('use_swarming',
651 True):
Ben Pastene858f4be2019-01-09 23:52:09652 self.add_android_presentation_args(tester_config, test_name, result)
653 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42654
Stephen Martinis0382bc12018-09-17 22:29:07655 result = self.update_and_cleanup_test(
656 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09657 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43658
659 if not result.get('merge'):
660 # TODO(https://crbug.com/958376): Consider adding the ability to not have
661 # this default.
662 result['merge'] = {
663 'script': '//testing/merge_scripts/standard_gtest_merge.py',
664 'args': [],
665 }
Kenneth Russelleb60cbd22017-12-05 07:54:28666 return result
667
668 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
669 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01670 if not self.should_run_on_tester(waterfall, tester_name, test_name,
671 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28672 return None
673 result = copy.deepcopy(test_config)
674 result['isolate_name'] = result.get('isolate_name', test_name)
675 result['name'] = test_name
676 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01677 self.initialize_args_for_test(result, tester_config)
Ben Pastene858f4be2019-01-09 23:52:09678 if tester_config.get('use_android_presentation', False):
679 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07680 result = self.update_and_cleanup_test(
681 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09682 self.add_common_test_properties(result, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17683
684 if not result.get('merge'):
685 # TODO(https://crbug.com/958376): Consider adding the ability to not have
686 # this default.
687 result['merge'] = {
688 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
689 'args': [],
690 }
Kenneth Russelleb60cbd22017-12-05 07:54:28691 return result
692
693 def generate_script_test(self, waterfall, tester_name, tester_config,
694 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44695 # TODO(https://crbug.com/953072): Remove this check whenever a better
696 # long-term solution is implemented.
697 if (waterfall.get('forbid_script_tests', False) or
698 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
699 raise BBGenErr('Attempted to generate a script test on tester ' +
700 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01701 if not self.should_run_on_tester(waterfall, tester_name, test_name,
702 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28703 return None
704 result = {
705 'name': test_name,
706 'script': test_config['script']
707 }
Stephen Martinis0382bc12018-09-17 22:29:07708 result = self.update_and_cleanup_test(
709 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28710 return result
711
712 def generate_junit_test(self, waterfall, tester_name, tester_config,
713 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01714 if not self.should_run_on_tester(waterfall, tester_name, test_name,
715 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28716 return None
John Budorickdef6acb2019-09-17 22:51:09717 result = copy.deepcopy(test_config)
718 result.update({
John Budorickcadc4952019-09-16 23:51:37719 'name': test_name,
720 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09721 })
722 self.initialize_args_for_test(result, tester_config)
723 result = self.update_and_cleanup_test(
724 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28725 return result
726
727 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
728 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01729 if not self.should_run_on_tester(waterfall, tester_name, test_name,
730 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28731 return None
732 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28733 if 'test' in result and result['test'] != test_name:
734 result['name'] = test_name
735 else:
736 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07737 result = self.update_and_cleanup_test(
738 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28739 return result
740
Stephen Martinis2a0667022018-09-25 22:31:14741 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01742 substitutions = {
743 # Any machine in waterfalls.pyl which desires to run GPU tests
744 # must provide the os_type key.
745 'os_type': tester_config['os_type'],
746 'gpu_vendor_id': '0',
747 'gpu_device_id': '0',
748 }
Stephen Martinis2a0667022018-09-25 22:31:14749 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01750 if 'gpu' in dimension_set:
751 # First remove the driver version, then split into vendor and device.
752 gpu = dimension_set['gpu']
Kenneth Russell384a1732019-03-16 02:36:02753 # Handle certain specialized named GPUs.
754 if gpu.startswith('nvidia-quadro-p400'):
755 gpu = ['10de', '1cb3']
756 elif gpu.startswith('intel-hd-630'):
757 gpu = ['8086', '5912']
Brian Sheedyf9387db7b2019-08-05 19:26:10758 elif gpu.startswith('intel-uhd-630'):
759 gpu = ['8086', '3e92']
Kenneth Russell384a1732019-03-16 02:36:02760 else:
761 gpu = gpu.split('-')[0].split(':')
Kenneth Russell8a386d42018-06-02 09:48:01762 substitutions['gpu_vendor_id'] = gpu[0]
763 substitutions['gpu_device_id'] = gpu[1]
764 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
765
766 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56767 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01768 # These are all just specializations of isolated script tests with
769 # a bunch of boilerplate command line arguments added.
770
771 # The step name must end in 'test' or 'tests' in order for the
772 # results to automatically show up on the flakiness dashboard.
773 # (At least, this was true some time ago.) Continue to use this
774 # naming convention for the time being to minimize changes.
775 step_name = test_config.get('name', test_name)
776 if not (step_name.endswith('test') or step_name.endswith('tests')):
777 step_name = '%s_tests' % step_name
778 result = self.generate_isolated_script_test(
779 waterfall, tester_name, tester_config, step_name, test_config)
780 if not result:
781 return None
782 result['isolate_name'] = 'telemetry_gpu_integration_test'
783 args = result.get('args', [])
784 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14785
786 # These tests upload and download results from cloud storage and therefore
787 # aren't idempotent yet. https://crbug.com/549140.
788 result['swarming']['idempotent'] = False
789
Kenneth Russell44910c32018-12-03 23:35:11790 # The GPU tests act much like integration tests for the entire browser, and
791 # tend to uncover flakiness bugs more readily than other test suites. In
792 # order to surface any flakiness more readily to the developer of the CL
793 # which is introducing it, we disable retries with patch on the commit
794 # queue.
795 result['should_retry_with_patch'] = False
796
Bo Liu555a0f92019-03-29 12:11:56797 browser = ('android-webview-instrumentation'
798 if is_android_webview else tester_config['browser_config'])
Kenneth Russell8a386d42018-06-02 09:48:01799 args = [
Bo Liu555a0f92019-03-29 12:11:56800 test_to_run,
801 '--show-stdout',
802 '--browser=%s' % browser,
803 # --passthrough displays more of the logging in Telemetry when
804 # run via typ, in particular some of the warnings about tests
805 # being expected to fail, but passing.
806 '--passthrough',
807 '-v',
808 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
Kenneth Russell8a386d42018-06-02 09:48:01809 ] + args
810 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14811 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01812 return result
813
Kenneth Russelleb60cbd22017-12-05 07:54:28814 def get_test_generator_map(self):
815 return {
Bo Liu555a0f92019-03-29 12:11:56816 'android_webview_gpu_telemetry_tests':
817 GPUTelemetryTestGenerator(self, is_android_webview=True),
818 'cts_tests':
819 CTSGenerator(self),
820 'gpu_telemetry_tests':
821 GPUTelemetryTestGenerator(self),
822 'gtest_tests':
823 GTestGenerator(self),
824 'instrumentation_tests':
825 InstrumentationTestGenerator(self),
826 'isolated_scripts':
827 IsolatedScriptTestGenerator(self),
828 'junit_tests':
829 JUnitGenerator(self),
830 'scripts':
831 ScriptGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28832 }
833
Kenneth Russell8a386d42018-06-02 09:48:01834 def get_test_type_remapper(self):
835 return {
836 # These are a specialization of isolated_scripts with a bunch of
837 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:56838 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:01839 'gpu_telemetry_tests': 'isolated_scripts',
840 }
841
Jeff Yoon67c3e832020-02-08 07:39:38842 def check_composition_type_test_suites(self, test_type,
843 additional_validators=None):
844 """Pre-pass to catch errors reliabily for compound/matrix suites"""
845 validators = [check_compound_references,
846 check_basic_references,
847 check_conflicting_definitions]
848 if additional_validators:
849 validators += additional_validators
850
851 target_suites = self.test_suites.get(test_type, {})
852 other_test_type = ('compound_suites'
853 if test_type == 'matrix_compound_suites'
854 else 'matrix_compound_suites')
855 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:01856 basic_suites = self.test_suites.get('basic_suites', {})
857
Jeff Yoon67c3e832020-02-08 07:39:38858 for suite, suite_def in target_suites.iteritems():
Jeff Yoon8154e582019-12-03 23:30:01859 if suite in basic_suites:
860 raise BBGenErr('%s names may not duplicate basic test suite names '
861 '(error found while processsing %s)'
862 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:01863
Jeff Yoon67c3e832020-02-08 07:39:38864 seen_tests = {}
865 for sub_suite in suite_def:
866 for validator in validators:
867 validator(
868 basic_suites=basic_suites,
869 other_test_suites=other_suites,
870 seen_tests=seen_tests,
871 sub_suite=sub_suite,
872 suite=suite,
873 suite_def=suite_def,
874 target_test_suites=target_suites,
875 test_type=test_type,
876 )
Kenneth Russelleb60cbd22017-12-05 07:54:28877
Stephen Martinis54d64ad2018-09-21 22:16:20878 def flatten_test_suites(self):
879 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:01880 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
881 for category in test_types:
882 for name, value in self.test_suites.get(category, {}).iteritems():
883 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:20884 self.test_suites = new_test_suites
885
Nodir Turakulovfce34292019-12-18 17:05:41886 def resolve_full_test_targets(self):
887 for suite in self.test_suites['basic_suites'].itervalues():
888 for key, test in suite.iteritems():
889 if not isinstance(test, dict):
890 # Some test definitions are just strings, such as CTS.
891 # Skip them.
892 continue
893
894 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
895 # https://source.chromium.org/chromium/chromium/tools/build/+/master:scripts/slave/recipe_modules/chromium_tests/generators.py;l=89;drc=14c062ba0eb418d3c4623dde41a753241b9df06b
896 # TODO(crbug.com/1035124): clean this up.
897 isolate_name = test.get('test') or test.get('isolate_name') or key
898 gn_entry = self.gn_isolate_map.get(isolate_name)
899 if gn_entry:
900 test['test_target'] = gn_entry['label']
901 else: # pragma: no cover
902 # Some tests do not have an entry gn_isolate_map.pyl, such as
903 # telemetry tests.
904 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
905 pass
906
Kenneth Russelleb60cbd22017-12-05 07:54:28907 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:01908 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:20909
Jeff Yoon8154e582019-12-03 23:30:01910 compound_suites = self.test_suites.get('compound_suites', {})
911 # check_composition_type_test_suites() checks that all basic suites
912 # referenced by compound suites exist.
913 basic_suites = self.test_suites.get('basic_suites')
914
915 for name, value in compound_suites.iteritems():
916 # Resolve this to a dictionary.
917 full_suite = {}
918 for entry in value:
919 suite = basic_suites[entry]
920 full_suite.update(suite)
921 compound_suites[name] = full_suite
922
Jeff Yoon67c3e832020-02-08 07:39:38923 def resolve_variants(self, basic_test_definition, variants):
924 """ Merge variant-defined configurations to each test case definition in a
925 test suite.
926
927 The output maps a unique test name to an array of configurations because
928 there may exist more than one definition for a test name using variants. The
929 test name is referenced while mapping machines to test suites, so unpacking
930 the array is done by the generators.
931
932 Args:
933 basic_test_definition: a {} defined test suite in the format
934 test_name:test_config
935 variants: an [] of {} defining configurations to be applied to each test
936 case in the basic test_definition
937
938 Return:
939 a {} of test_name:[{}], where each {} is a merged configuration
940 """
941
942 # Each test in a basic test suite will have a definition per variant.
943 test_suite = {}
944 for test_name, test_config in basic_test_definition.iteritems():
945 definitions = []
946 for variant in variants:
947 # Clone a copy of test_config so that we can have a uniquely updated
948 # version of it per variant
949 cloned_config = copy.deepcopy(test_config)
950 # The variant definition needs to be re-used for each test, so we'll
951 # create a clone and work with it as well.
952 cloned_variant = copy.deepcopy(variant)
953
954 cloned_config['args'] = (cloned_config.get('args', []) +
955 cloned_variant.get('args', []))
956 cloned_config['mixins'] = (cloned_config.get('mixins', []) +
957 cloned_variant.get('mixins', []))
958
959 basic_swarming_def = cloned_config.get('swarming', {})
960 variant_swarming_def = cloned_variant.get('swarming', {})
961 if basic_swarming_def and variant_swarming_def:
962 if ('dimension_sets' in basic_swarming_def and
963 'dimension_sets' in variant_swarming_def):
964 # Retain swarming dimension set merge behavior when both variant and
965 # the basic test configuration both define it
966 self.dictionary_merge(basic_swarming_def, variant_swarming_def)
967 # Remove dimension_sets from the variant definition, so that it does
968 # not replace what's been done by dictionary_merge in the update
969 # call below.
970 del variant_swarming_def['dimension_sets']
971
972 # Update the swarming definition with whatever is defined for swarming
973 # by the variant.
974 basic_swarming_def.update(variant_swarming_def)
975 cloned_config['swarming'] = basic_swarming_def
976
977 # The identifier is used to make the name of the test unique.
978 # Generators in the recipe uniquely identify a test by it's name, so we
979 # don't want to have the same name for each variant.
980 cloned_config['name'] = '{}_{}'.format(test_name,
981 cloned_variant['identifier'])
982
983 definitions.append(cloned_config)
984 test_suite[test_name] = definitions
985 return test_suite
986
Jeff Yoon8154e582019-12-03 23:30:01987 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:38988 self.check_composition_type_test_suites('matrix_compound_suites',
989 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:01990
991 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:38992 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:01993 # referenced by matrix suites exist.
994 basic_suites = self.test_suites.get('basic_suites')
995
Jeff Yoon67c3e832020-02-08 07:39:38996 for test_name, matrix_config in matrix_compound_suites.iteritems():
Jeff Yoon8154e582019-12-03 23:30:01997 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:38998
999 for test_suite, mtx_test_suite_config in matrix_config.iteritems():
1000 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1001
1002 if 'variants' in mtx_test_suite_config:
1003 result = self.resolve_variants(basic_test_def,
1004 mtx_test_suite_config['variants'])
1005 full_suite.update(result)
1006 matrix_compound_suites[test_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281007
1008 def link_waterfalls_to_test_suites(self):
1009 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431010 for tester_name, tester in waterfall['machines'].iteritems():
1011 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281012 if not value in self.test_suites:
1013 # Hard / impossible to cover this in the unit test.
1014 raise self.unknown_test_suite(
1015 value, tester_name, waterfall['name']) # pragma: no cover
1016 tester['test_suites'][suite] = self.test_suites[value]
1017
1018 def load_configuration_files(self):
1019 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
1020 self.test_suites = self.load_pyl_file('test_suites.pyl')
1021 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:011022 self.mixins = self.load_pyl_file('mixins.pyl')
Nodir Turakulovfce34292019-12-18 17:05:411023 self.gn_isolate_map = self.load_pyl_file('gn_isolate_map.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:281024
1025 def resolve_configuration_files(self):
Nodir Turakulovfce34292019-12-18 17:05:411026 self.resolve_full_test_targets()
Kenneth Russelleb60cbd22017-12-05 07:54:281027 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011028 self.resolve_matrix_compound_test_suites()
1029 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281030 self.link_waterfalls_to_test_suites()
1031
Nico Weberd18b8962018-05-16 19:39:381032 def unknown_bot(self, bot_name, waterfall_name):
1033 return BBGenErr(
1034 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1035
Kenneth Russelleb60cbd22017-12-05 07:54:281036 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1037 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381038 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281039 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1040
1041 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1042 return BBGenErr(
1043 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1044 ' on waterfall ' + waterfall_name)
1045
Stephen Martinisb72f6d22018-10-04 23:29:011046 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071047 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321048
1049 Checks in the waterfall, builder, and test objects for mixins.
1050 """
1051 def valid_mixin(mixin_name):
1052 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011053 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321054 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381055
Stephen Martinisb6a50492018-09-12 23:59:321056 def must_be_list(mixins, typ, name):
1057 """Asserts that given mixins are a list."""
1058 if not isinstance(mixins, list):
1059 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1060
Brian Sheedy7658c982020-01-08 02:27:581061 test_name = test.get('name')
1062 remove_mixins = set()
1063 if 'remove_mixins' in builder:
1064 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1065 for rm in builder['remove_mixins']:
1066 valid_mixin(rm)
1067 remove_mixins.add(rm)
1068 if 'remove_mixins' in test:
1069 must_be_list(test['remove_mixins'], 'test', test_name)
1070 for rm in test['remove_mixins']:
1071 valid_mixin(rm)
1072 remove_mixins.add(rm)
1073 del test['remove_mixins']
1074
Stephen Martinisb72f6d22018-10-04 23:29:011075 if 'mixins' in waterfall:
1076 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1077 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581078 if mixin in remove_mixins:
1079 continue
Stephen Martinisb6a50492018-09-12 23:59:321080 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011081 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321082
Stephen Martinisb72f6d22018-10-04 23:29:011083 if 'mixins' in builder:
1084 must_be_list(builder['mixins'], 'builder', builder_name)
1085 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581086 if mixin in remove_mixins:
1087 continue
Stephen Martinisb6a50492018-09-12 23:59:321088 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011089 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321090
Stephen Martinisb72f6d22018-10-04 23:29:011091 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071092 return test
1093
Stephen Martinis2a0667022018-09-25 22:31:141094 if not test_name:
1095 test_name = test.get('test')
1096 if not test_name: # pragma: no cover
1097 # Not the best name, but we should say something.
1098 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:011099 must_be_list(test['mixins'], 'test', test_name)
1100 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581101 # We don't bother checking if the given mixin is in remove_mixins here
1102 # since this is already the lowest level, so if a mixin is added here that
1103 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071104 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011105 test = self.apply_mixin(self.mixins[mixin], test)
Jeff Yoon67c3e832020-02-08 07:39:381106 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071107 return test
Stephen Martinisb6a50492018-09-12 23:59:321108
Stephen Martinisb72f6d22018-10-04 23:29:011109 def apply_mixin(self, mixin, test):
1110 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321111
Stephen Martinis0382bc12018-09-17 22:29:071112 Mixins will not override an existing key. This is to ensure exceptions can
1113 override a setting a mixin applies.
1114
Stephen Martinisb72f6d22018-10-04 23:29:011115 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:321116 'dimension_sets', which is how normal test suites specify their dimensions,
1117 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
1118 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011119
Stephen Martinisb6a50492018-09-12 23:59:321120 """
1121 new_test = copy.deepcopy(test)
1122 mixin = copy.deepcopy(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011123 if 'swarming' in mixin:
1124 swarming_mixin = mixin['swarming']
1125 new_test.setdefault('swarming', {})
1126 if 'dimensions' in swarming_mixin:
1127 new_test['swarming'].setdefault('dimension_sets', [{}])
1128 for dimension_set in new_test['swarming']['dimension_sets']:
1129 dimension_set.update(swarming_mixin['dimensions'])
1130 del swarming_mixin['dimensions']
Stephen Martinisb72f6d22018-10-04 23:29:011131 # python dict update doesn't do recursion at all. Just hard code the
1132 # nested update we need (mixin['swarming'] shouldn't clobber
1133 # test['swarming'], but should update it).
1134 new_test['swarming'].update(swarming_mixin)
1135 del mixin['swarming']
1136
Wezc0e835b702018-10-30 00:38:411137 if '$mixin_append' in mixin:
1138 # Values specified under $mixin_append should be appended to existing
1139 # lists, rather than replacing them.
1140 mixin_append = mixin['$mixin_append']
1141 for key in mixin_append:
1142 new_test.setdefault(key, [])
1143 if not isinstance(mixin_append[key], list):
1144 raise BBGenErr(
1145 'Key "' + key + '" in $mixin_append must be a list.')
1146 if not isinstance(new_test[key], list):
1147 raise BBGenErr(
1148 'Cannot apply $mixin_append to non-list "' + key + '".')
1149 new_test[key].extend(mixin_append[key])
1150 if 'args' in mixin_append:
1151 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1152 del mixin['$mixin_append']
1153
Stephen Martinisb72f6d22018-10-04 23:29:011154 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321155 return new_test
1156
Greg Guterman5c6144152020-02-28 20:08:531157 def generate_waterfall_tests(self, waterfall):
1158 """Generates the tests for a waterfall.
1159
1160 Args:
1161 waterfall: a dictionary parsed from a master pyl file
1162 Returns:
1163 A dictionary mapping builders to test specs
1164 """
1165 all_tests = {
1166 name: self.get_tests_for_config(waterfall, name, config)
1167 for name, config
1168 in waterfall['machines'].iteritems()
1169 }
Kenneth Russelleb60cbd22017-12-05 07:54:281170 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1171 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
Greg Guterman5c6144152020-02-28 20:08:531172 return all_tests
1173
1174 def get_tests_for_config(self, waterfall, name, config):
1175 generator_map = self.get_test_generator_map()
1176 test_type_remapper = self.get_test_type_remapper()
1177
1178 tests = {}
1179 # Copy only well-understood entries in the machine's configuration
1180 # verbatim into the generated JSON.
1181 if 'additional_compile_targets' in config:
1182 tests['additional_compile_targets'] = config[
1183 'additional_compile_targets']
1184 for test_type, input_tests in config.get('test_suites', {}).iteritems():
1185 if test_type not in generator_map:
1186 raise self.unknown_test_suite_type(
1187 test_type, name, waterfall['name']) # pragma: no cover
1188 test_generator = generator_map[test_type]
1189 # Let multiple kinds of generators generate the same kinds
1190 # of tests. For example, gpu_telemetry_tests are a
1191 # specialization of isolated_scripts.
1192 new_tests = test_generator.generate(
1193 waterfall, name, config, input_tests)
1194 remapped_test_type = test_type_remapper.get(test_type, test_type)
1195 tests[remapped_test_type] = test_generator.sort(
1196 tests.get(remapped_test_type, []) + new_tests)
1197
1198 return tests
1199
1200 def jsonify(self, all_tests):
1201 return json.dumps(
1202 all_tests, indent=2, separators=(',', ': '),
1203 sort_keys=True) + '\n'
Kenneth Russelleb60cbd22017-12-05 07:54:281204
1205 def generate_waterfalls(self): # pragma: no cover
1206 self.load_configuration_files()
1207 self.resolve_configuration_files()
1208 filters = self.args.waterfall_filters
1209 suffix = '.json'
1210 if self.args.new_files:
1211 suffix = '.new' + suffix
Greg Guterman5c6144152020-02-28 20:08:531212
1213 bucket_map = collections.defaultdict(dict)
Kenneth Russelleb60cbd22017-12-05 07:54:281214 for waterfall in self.waterfalls:
Greg Guterman5c6144152020-02-28 20:08:531215 if filters and waterfall['name'] not in filters:
1216 continue
1217
1218 file_path = waterfall['name'] + suffix
1219 all_tests = self.generate_waterfall_tests(waterfall)
1220 waterfall_json = self.jsonify(all_tests)
1221 self.write_file(self.pyl_file_path(file_path), waterfall_json)
1222
1223 # Assign to buckets
1224 bucketname = waterfall['bucket']
1225 if bucketname != 'NA':
1226 # TODO(guterman): move the internal builders over and remove this
1227 # Currently, the waterfalls have builders in the internal buckets,
1228 # which we put 'NA' for. In the future, there shouldn't be any.
1229 for buildername in waterfall['machines'].keys():
1230 bucket_map[bucketname][buildername] = all_tests[buildername]
1231
1232 # Write bucket files
1233 for bucketname, bucket in bucket_map.items():
1234 bucket['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1235 bucket['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1236 bucket_json = self.jsonify(bucket)
1237 self.write_file(self.pyl_file_path(bucketname + '.json'), bucket_json)
Kenneth Russelleb60cbd22017-12-05 07:54:281238
Nico Weberd18b8962018-05-16 19:39:381239 def get_valid_bot_names(self):
John Budorick699282e2019-02-13 01:27:331240 # Extract bot names from infra/config/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421241 # NOTE: This reference can cause issues; if a file changes there, the
1242 # presubmit here won't be run by default. A manually maintained list there
1243 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1244 # references to configs outside of this directory are added, please change
1245 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1246 # never ends up in an invalid state.
Nico Weberd18b8962018-05-16 19:39:381247 bot_names = set()
John Budorickc12abd12018-08-14 19:37:431248 infra_config_dir = os.path.abspath(
1249 os.path.join(os.path.dirname(__file__),
John Budorick699282e2019-02-13 01:27:331250 '..', '..', 'infra', 'config'))
John Budorickc12abd12018-08-14 19:37:431251 milo_configs = [
Garrett Beatybb8322bf2019-10-17 20:53:051252 os.path.join(infra_config_dir, 'generated', 'luci-milo.cfg'),
Garrett Beatye95b81722019-10-24 17:12:181253 os.path.join(infra_config_dir, 'generated', 'luci-milo-dev.cfg'),
John Budorickc12abd12018-08-14 19:37:431254 ]
1255 for c in milo_configs:
1256 for l in self.read_file(c).splitlines():
1257 if (not 'name: "buildbucket/luci.chromium.' in l and
Hans Wennborg98ffd7d92019-02-06 14:14:341258 not 'name: "buildbucket/luci.chrome.' in l and
John Budorickb1833612018-12-07 04:36:411259 not 'name: "buildbot/chromium.' in l and
1260 not 'name: "buildbot/tryserver.chromium.' in l):
John Budorickc12abd12018-08-14 19:37:431261 continue
1262 # l looks like
1263 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1264 # Extract win_chromium_dbg_ng part.
1265 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381266 return bot_names
1267
Ben Pastene9a010082019-09-25 20:41:371268 def get_builders_that_do_not_actually_exist(self):
Kenneth Russell8a386d42018-06-02 09:48:011269 # Some of the bots on the chromium.gpu.fyi waterfall in particular
1270 # are defined only to be mirrored into trybots, and don't actually
1271 # exist on any of the waterfalls or consoles.
1272 return [
Michael Spangeb07eba62019-05-14 22:22:581273 'GPU FYI Fuchsia Builder',
Yuly Novikoveb26b812019-07-26 02:08:191274 'ANGLE GPU Android Release (Nexus 5X)',
Jamie Madillda894ce2019-04-08 17:19:171275 'ANGLE GPU Linux Release (Intel HD 630)',
1276 'ANGLE GPU Linux Release (NVIDIA)',
1277 'ANGLE GPU Mac Release (Intel)',
1278 'ANGLE GPU Mac Retina Release (AMD)',
1279 'ANGLE GPU Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491280 'ANGLE GPU Win10 x64 Release (Intel HD 630)',
1281 'ANGLE GPU Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011282 'Optional Android Release (Nexus 5X)',
1283 'Optional Linux Release (Intel HD 630)',
1284 'Optional Linux Release (NVIDIA)',
1285 'Optional Mac Release (Intel)',
1286 'Optional Mac Retina Release (AMD)',
1287 'Optional Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491288 'Optional Win10 x64 Release (Intel HD 630)',
1289 'Optional Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011290 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:081291 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:291292 'linux-blink-rel-dummy',
1293 'mac10.10-blink-rel-dummy',
1294 'mac10.11-blink-rel-dummy',
1295 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:201296 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291297 'mac10.13-blink-rel-dummy',
John Chenad978322019-12-16 18:07:211298 'mac10.14-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291299 'win7-blink-rel-dummy',
1300 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:081301 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:331302 'WebKit Linux composite_after_paint Dummy Builder',
Scott Violet744e04662019-08-19 23:51:531303 'WebKit Linux layout_ng_disabled Builder',
Stephen Martinis769b25112018-08-30 18:52:061304 # chromium, due to https://crbug.com/878915
1305 'win-dbg',
1306 'win32-dbg',
Stephen Martinis47d77132019-04-24 23:51:331307 'win-archive-dbg',
1308 'win32-archive-dbg',
Sajjad Mirza2924a012019-12-20 03:46:541309 # TODO(crbug.com/1033753) Delete these when coverage is enabled by default
1310 # on Windows tryjobs.
1311 'GPU Win x64 Builder Code Coverage',
1312 'Win x64 Builder Code Coverage',
1313 'Win10 Tests x64 Code Coverage',
1314 'Win10 x64 Release (NVIDIA) Code Coverage',
Sajjad Mirzafa15665e2020-02-10 23:41:041315 # TODO(crbug.com/1024915) Delete these when coverage is enabled by default
1316 # on Mac OS tryjobs.
1317 'Mac Builder Code Coverage',
1318 'Mac10.13 Tests Code Coverage',
1319 'GPU Mac Builder Code Coverage',
1320 'Mac Release (Intel) Code Coverage',
1321 'Mac Retina Release (AMD) Code Coverage',
Kenneth Russell8a386d42018-06-02 09:48:011322 ]
1323
Ben Pastene9a010082019-09-25 20:41:371324 def get_internal_waterfalls(self):
1325 # Similar to get_builders_that_do_not_actually_exist above, but for
1326 # waterfalls defined in internal configs.
1327 return ['chrome']
1328
Stephen Martinisf83893722018-09-19 00:02:181329 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201330 self.check_input_files_sorting(verbose)
1331
Kenneth Russelleb60cbd22017-12-05 07:54:281332 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011333 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381334 self.check_composition_type_test_suites('matrix_compound_suites',
1335 [check_matrix_identifier])
Nodir Turakulovfce34292019-12-18 17:05:411336 self.resolve_full_test_targets()
Stephen Martinis54d64ad2018-09-21 22:16:201337 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381338
1339 # All bots should exist.
1340 bot_names = self.get_valid_bot_names()
Ben Pastene9a010082019-09-25 20:41:371341 internal_waterfalls = self.get_internal_waterfalls()
1342 builders_that_dont_exist = self.get_builders_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:381343 for waterfall in self.waterfalls:
Ben Pastene9a010082019-09-25 20:41:371344 # TODO(crbug.com/991417): Remove the need for this exception.
1345 if waterfall['name'] in internal_waterfalls:
1346 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381347 for bot_name in waterfall['machines']:
Ben Pastene9a010082019-09-25 20:41:371348 if bot_name in builders_that_dont_exist:
Kenneth Russell8a386d42018-06-02 09:48:011349 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381350 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:081351 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:381352 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:521353 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:321354 if waterfall['name'] in ['tryserver.webrtc',
1355 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:381356 # These waterfalls have their bot configs in a different repo.
1357 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:521358 continue # pragma: no cover
Jeff Yoon8154e582019-12-03 23:30:011359 if waterfall['name'] in ['client.devtools-frontend.integration',
Liviu Raud287b1f2020-01-14 07:30:331360 'tryserver.devtools-frontend',
1361 'chromium.devtools-frontend']:
Tamer Tas2c506412019-08-20 07:44:411362 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381363 raise self.unknown_bot(bot_name, waterfall['name'])
1364
Kenneth Russelleb60cbd22017-12-05 07:54:281365 # All test suites must be referenced.
1366 suites_seen = set()
1367 generator_map = self.get_test_generator_map()
1368 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431369 for bot_name, tester in waterfall['machines'].iteritems():
1370 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281371 if suite_type not in generator_map:
1372 raise self.unknown_test_suite_type(suite_type, bot_name,
1373 waterfall['name'])
1374 if suite not in self.test_suites:
1375 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1376 suites_seen.add(suite)
1377 # Since we didn't resolve the configuration files, this set
1378 # includes both composition test suites and regular ones.
1379 resolved_suites = set()
1380 for suite_name in suites_seen:
1381 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011382 for sub_suite in suite:
1383 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281384 resolved_suites.add(suite_name)
1385 # At this point, every key in test_suites.pyl should be referenced.
1386 missing_suites = set(self.test_suites.keys()) - resolved_suites
1387 if missing_suites:
1388 raise BBGenErr('The following test suites were unreferenced by bots on '
1389 'the waterfalls: ' + str(missing_suites))
1390
1391 # All test suite exceptions must refer to bots on the waterfall.
1392 all_bots = set()
1393 missing_bots = set()
1394 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431395 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281396 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281397 # In order to disambiguate between bots with the same name on
1398 # different waterfalls, support has been added to various
1399 # exceptions for concatenating the waterfall name after the bot
1400 # name.
1401 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281402 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381403 removals = (exception.get('remove_from', []) +
1404 exception.get('remove_gtest_from', []) +
1405 exception.get('modifications', {}).keys())
1406 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281407 if removal not in all_bots:
1408 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411409
Ben Pastene9a010082019-09-25 20:41:371410 missing_bots = missing_bots - set(builders_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281411 if missing_bots:
1412 raise BBGenErr('The following nonexistent machines were referenced in '
1413 'the test suite exceptions: ' + str(missing_bots))
1414
Stephen Martinis0382bc12018-09-17 22:29:071415 # All mixins must be referenced
1416 seen_mixins = set()
1417 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011418 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071419 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011420 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071421 for suite in self.test_suites.values():
1422 if isinstance(suite, list):
1423 # Don't care about this, it's a composition, which shouldn't include a
1424 # swarming mixin.
1425 continue
1426
1427 for test in suite.values():
1428 if not isinstance(test, dict):
1429 # Some test suites have top level keys, which currently can't be
1430 # swarming mixin entries. Ignore them
1431 continue
1432
Stephen Martinisb72f6d22018-10-04 23:29:011433 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071434
Stephen Martinisb72f6d22018-10-04 23:29:011435 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071436 if missing_mixins:
1437 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1438 ' referenced in a waterfall, machine, or test suite.' % (
1439 str(missing_mixins)))
1440
Stephen Martinis54d64ad2018-09-21 22:16:201441
1442 def type_assert(self, node, typ, filename, verbose=False):
1443 """Asserts that the Python AST node |node| is of type |typ|.
1444
1445 If verbose is set, it prints out some helpful context lines, showing where
1446 exactly the error occurred in the file.
1447 """
1448 if not isinstance(node, typ):
1449 if verbose:
1450 lines = [""] + self.read_file(filename).splitlines()
1451
1452 context = 2
1453 lines_start = max(node.lineno - context, 0)
1454 # Add one to include the last line
1455 lines_end = min(node.lineno + context, len(lines)) + 1
1456 lines = (
1457 ['== %s ==\n' % filename] +
1458 ["<snip>\n"] +
1459 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1460 lines[lines_start:lines_start + context])] +
1461 ['-' * 80 + '\n'] +
1462 ['%d %s' % (node.lineno, lines[node.lineno])] +
1463 ['-' * (node.col_offset + 3) + '^' + '-' * (
1464 80 - node.col_offset - 4) + '\n'] +
1465 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1466 lines[node.lineno + 1:lines_end])] +
1467 ["<snip>\n"]
1468 )
1469 # Print out a useful message when a type assertion fails.
1470 for l in lines:
1471 self.print_line(l.strip())
1472
1473 node_dumped = ast.dump(node, annotate_fields=False)
1474 # If the node is huge, truncate it so everything fits in a terminal
1475 # window.
1476 if len(node_dumped) > 60: # pragma: no cover
1477 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1478 raise BBGenErr(
1479 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1480 ' be %s, is %s' % (
1481 filename, node_dumped,
1482 node.lineno, typ, type(node)))
1483
Stephen Martinis5bef0fc2020-01-06 22:47:531484 def check_ast_list_formatted(self, keys, filename, verbose,
Stephen Martinis1384ff92020-01-07 19:52:151485 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531486 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201487
Stephen Martinis5bef0fc2020-01-06 22:47:531488 Currently only checks to ensure they're correctly sorted, and that there
1489 are no duplicates.
1490
1491 Args:
1492 keys: An python list of AST nodes.
1493
1494 It's a list of AST nodes instead of a list of strings because
1495 when verbose is set, it tries to print out context of where the
1496 diffs are in the file.
1497 filename: The name of the file this node is from.
1498 verbose: If set, print out diff information about how the keys are
1499 incorrectly formatted.
1500 check_sorting: If true, checks if the list is sorted.
1501 Returns:
1502 If the keys are correctly formatted.
1503 """
1504 if not keys:
1505 return True
1506
1507 assert isinstance(keys[0], ast.Str)
1508
1509 keys_strs = [k.s for k in keys]
1510 # Keys to diff against. Used below.
1511 keys_to_diff_against = None
1512 # If the list is properly formatted.
1513 list_formatted = True
1514
1515 # Duplicates are always bad.
1516 if len(set(keys_strs)) != len(keys_strs):
1517 list_formatted = False
1518 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1519
1520 if check_sorting and sorted(keys_strs) != keys_strs:
1521 list_formatted = False
1522 if list_formatted:
1523 return True
1524
1525 if verbose:
1526 line_num = keys[0].lineno
1527 keys = [k.s for k in keys]
1528 if check_sorting:
1529 # If we have duplicates, sorting this will take care of it anyways.
1530 keys_to_diff_against = sorted(set(keys))
1531 # else, keys_to_diff_against is set above already
1532
1533 self.print_line('=' * 80)
1534 self.print_line('(First line of keys is %s)' % line_num)
1535 for line in difflib.context_diff(
1536 keys, keys_to_diff_against,
1537 fromfile='current (%r)' % filename, tofile='sorted', lineterm=''):
1538 self.print_line(line)
1539 self.print_line('=' * 80)
1540
1541 return False
1542
Stephen Martinis1384ff92020-01-07 19:52:151543 def check_ast_dict_formatted(self, node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531544 """Checks if an ast dictionary's keys are correctly formatted.
1545
1546 Just a simple wrapper around check_ast_list_formatted.
1547 Args:
1548 node: An AST node. Assumed to be a dictionary.
1549 filename: The name of the file this node is from.
1550 verbose: If set, print out diff information about how the keys are
1551 incorrectly formatted.
1552 check_sorting: If true, checks if the list is sorted.
1553 Returns:
1554 If the dictionary is correctly formatted.
1555 """
Stephen Martinis54d64ad2018-09-21 22:16:201556 keys = []
1557 # The keys of this dict are ordered as ordered in the file; normal python
1558 # dictionary keys are given an arbitrary order, but since we parsed the
1559 # file itself, the order as given in the file is preserved.
1560 for key in node.keys:
1561 self.type_assert(key, ast.Str, filename, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531562 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201563
Stephen Martinis1384ff92020-01-07 19:52:151564 return self.check_ast_list_formatted(keys, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181565
1566 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201567 # TODO(https://crbug.com/886993): Add the ability for this script to
1568 # actually format the files, rather than just complain if they're
1569 # incorrectly formatted.
1570 bad_files = set()
Stephen Martinis5bef0fc2020-01-06 22:47:531571 def parse_file(filename):
1572 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201573
Stephen Martinis5bef0fc2020-01-06 22:47:531574 Returns an AST node representing the value in the pyl file."""
Stephen Martinisf83893722018-09-19 00:02:181575 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1576
Stephen Martinisf83893722018-09-19 00:02:181577 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201578 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181579 module = parsed.body
1580
1581 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201582 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181583 if len(module) != 1: # pragma: no cover
1584 raise BBGenErr('Invalid .pyl file %s' % filename)
1585 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201586 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181587
Stephen Martinis5bef0fc2020-01-06 22:47:531588 return expr.value
1589
1590 # Handle this separately
1591 filename = 'waterfalls.pyl'
1592 value = parse_file(filename)
1593 # Value should be a list.
1594 self.type_assert(value, ast.List, filename, verbose)
1595
1596 keys = []
1597 for val in value.elts:
1598 self.type_assert(val, ast.Dict, filename, verbose)
1599 waterfall_name = None
1600 for key, val in zip(val.keys, val.values):
1601 self.type_assert(key, ast.Str, filename, verbose)
1602 if key.s == 'machines':
1603 if not self.check_ast_dict_formatted(val, filename, verbose):
1604 bad_files.add(filename)
1605
1606 if key.s == "name":
1607 self.type_assert(val, ast.Str, filename, verbose)
1608 waterfall_name = val
1609 assert waterfall_name
1610 keys.append(waterfall_name)
1611
Stephen Martinis1384ff92020-01-07 19:52:151612 if not self.check_ast_list_formatted(keys, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531613 bad_files.add(filename)
1614
1615 for filename in (
1616 'mixins.pyl',
1617 'test_suites.pyl',
1618 'test_suite_exceptions.pyl',
1619 ):
1620 value = parse_file(filename)
Stephen Martinisf83893722018-09-19 00:02:181621 # Value should be a dictionary.
Stephen Martinis54d64ad2018-09-21 22:16:201622 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181623
Stephen Martinis5bef0fc2020-01-06 22:47:531624 if not self.check_ast_dict_formatted(
1625 value, filename, verbose):
1626 bad_files.add(filename)
1627
Stephen Martinis54d64ad2018-09-21 22:16:201628 if filename == 'test_suites.pyl':
Jeff Yoon8154e582019-12-03 23:30:011629 expected_keys = ['basic_suites',
1630 'compound_suites',
1631 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201632 actual_keys = [node.s for node in value.keys]
1633 assert all(key in expected_keys for key in actual_keys), (
1634 'Invalid %r file; expected keys %r, got %r' % (
1635 filename, expected_keys, actual_keys))
1636 suite_dicts = [node for node in value.values]
1637 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011638 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201639 for suite_group in suite_dicts:
Stephen Martinis5bef0fc2020-01-06 22:47:531640 if not self.check_ast_dict_formatted(
Stephen Martinis54d64ad2018-09-21 22:16:201641 suite_group, filename, verbose):
1642 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181643
Stephen Martinis5bef0fc2020-01-06 22:47:531644 for key, suite in zip(value.keys, value.values):
1645 # The compound suites are checked in
1646 # 'check_composition_type_test_suites()'
1647 if key.s == 'basic_suites':
1648 for group in suite.values:
Stephen Martinis1384ff92020-01-07 19:52:151649 if not self.check_ast_dict_formatted(group, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531650 bad_files.add(filename)
1651 break
Stephen Martinis54d64ad2018-09-21 22:16:201652
Stephen Martinis5bef0fc2020-01-06 22:47:531653 elif filename == 'test_suite_exceptions.pyl':
1654 # Check the values for each test.
1655 for test in value.values:
1656 for kind, node in zip(test.keys, test.values):
1657 if isinstance(node, ast.Dict):
Stephen Martinis1384ff92020-01-07 19:52:151658 if not self.check_ast_dict_formatted(node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531659 bad_files.add(filename)
1660 elif kind.s == 'remove_from':
1661 # Don't care about sorting; these are usually grouped, since the
1662 # same bug can affect multiple builders. Do want to make sure
1663 # there aren't duplicates.
1664 if not self.check_ast_list_formatted(node.elts, filename, verbose,
1665 check_sorting=False):
1666 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181667
1668 if bad_files:
1669 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201670 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531671 'unsorted, or have duplicates. Re-run this with --verbose to see '
1672 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181673
Kenneth Russelleb60cbd22017-12-05 07:54:281674 def check_output_file_consistency(self, verbose=False):
1675 self.load_configuration_files()
1676 # All waterfalls must have been written by this script already.
1677 self.resolve_configuration_files()
1678 ungenerated_waterfalls = set()
1679 for waterfall in self.waterfalls:
Greg Guterman5c6144152020-02-28 20:08:531680 expected = self.jsonify(
1681 self.generate_waterfall_tests(waterfall))
Zhiling Huangbe008172018-03-08 19:13:111682 file_path = waterfall['name'] + '.json'
1683 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281684 if expected != current:
1685 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321686 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501687 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281688 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321689 'contents:')
1690 for line in difflib.unified_diff(
1691 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501692 current.splitlines(),
1693 fromfile='expected', tofile='current'):
1694 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281695 if ungenerated_waterfalls:
1696 raise BBGenErr('The following waterfalls have not been properly '
1697 'autogenerated by generate_buildbot_json.py: ' +
1698 str(ungenerated_waterfalls))
1699
1700 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501701 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281702 self.check_output_file_consistency(verbose) # pragma: no cover
1703
1704 def parse_args(self, argv): # pragma: no cover
Karen Qiane24b7ee2019-02-12 23:37:061705
1706 # RawTextHelpFormatter allows for styling of help statement
1707 parser = argparse.ArgumentParser(formatter_class=
1708 argparse.RawTextHelpFormatter)
1709
1710 group = parser.add_mutually_exclusive_group()
1711 group.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281712 '-c', '--check', action='store_true', help=
1713 'Do consistency checks of configuration and generated files and then '
1714 'exit. Used during presubmit. Causes the tool to not generate any files.')
Karen Qiane24b7ee2019-02-12 23:37:061715 group.add_argument(
1716 '--query', type=str, help=
1717 ("Returns raw JSON information of buildbots and tests.\n" +
1718 "Examples:\n" +
1719 " List all bots (all info):\n" +
1720 " --query bots\n\n" +
1721 " List all bots and only their associated tests:\n" +
1722 " --query bots/tests\n\n" +
1723 " List all information about 'bot1' " +
1724 "(make sure you have quotes):\n" +
1725 " --query bot/'bot1'\n\n" +
1726 " List tests running for 'bot1' (make sure you have quotes):\n" +
1727 " --query bot/'bot1'/tests\n\n" +
1728 " List all tests:\n" +
1729 " --query tests\n\n" +
1730 " List all tests and the bots running them:\n" +
1731 " --query tests/bots\n\n"+
1732 " List all tests that satisfy multiple parameters\n" +
1733 " (separation of parameters by '&' symbol):\n" +
1734 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
1735 " List all tests that run with a specific flag:\n" +
1736 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
1737 " List specific test (make sure you have quotes):\n"
1738 " --query test/'test1'\n\n"
1739 " List all bots running 'test1' " +
1740 "(make sure you have quotes):\n" +
1741 " --query test/'test1'/bots" ))
Kenneth Russelleb60cbd22017-12-05 07:54:281742 parser.add_argument(
1743 '-n', '--new-files', action='store_true', help=
1744 'Write output files as .new.json. Useful during development so old and '
1745 'new files can be looked at side-by-side.')
1746 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501747 '-v', '--verbose', action='store_true', help=
1748 'Increases verbosity. Affects consistency checks.')
1749 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281750 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1751 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111752 parser.add_argument(
1753 '--pyl-files-dir', type=os.path.realpath,
1754 help='Path to the directory containing the input .pyl files.')
Karen Qiane24b7ee2019-02-12 23:37:061755 parser.add_argument(
1756 '--json', help=
1757 ("Outputs results into a json file. Only works with query function.\n" +
1758 "Examples:\n" +
1759 " Outputs file into specified json file: \n" +
1760 " --json <file-name-here.json>"))
Kenneth Russelleb60cbd22017-12-05 07:54:281761 self.args = parser.parse_args(argv)
Karen Qiane24b7ee2019-02-12 23:37:061762 if self.args.json and not self.args.query:
1763 parser.error("The --json flag can only be used with --query.")
1764
1765 def does_test_match(self, test_info, params_dict):
1766 """Checks to see if the test matches the parameters given.
1767
1768 Compares the provided test_info with the params_dict to see
1769 if the bot matches the parameters given. If so, returns True.
1770 Else, returns false.
1771
1772 Args:
1773 test_info (dict): Information about a specific bot provided
1774 in the format shown in waterfalls.pyl
1775 params_dict (dict): Dictionary of parameters and their values
1776 to look for in the bot
1777 Ex: {
1778 'device_os':'android',
1779 '--flag':True,
1780 'mixins': ['mixin1', 'mixin2'],
1781 'ex_key':'ex_value'
1782 }
1783
1784 """
1785 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
1786 'kvm', 'pool', 'integrity'] # dimension parameters
1787 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
1788 'can_use_on_swarming_builders']
1789 for param in params_dict:
1790 # if dimension parameter
1791 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
1792 if not 'swarming' in test_info:
1793 return False
1794 swarming = test_info['swarming']
1795 if param in SWARMING_PARAMS:
1796 if not param in swarming:
1797 return False
1798 if not str(swarming[param]) == params_dict[param]:
1799 return False
1800 else:
1801 if not 'dimension_sets' in swarming:
1802 return False
1803 d_set = swarming['dimension_sets']
1804 # only looking at the first dimension set
1805 if not param in d_set[0]:
1806 return False
1807 if not d_set[0][param] == params_dict[param]:
1808 return False
1809
1810 # if flag
1811 elif param.startswith('--'):
1812 if not 'args' in test_info:
1813 return False
1814 if not param in test_info['args']:
1815 return False
1816
1817 # not dimension parameter/flag/mixin
1818 else:
1819 if not param in test_info:
1820 return False
1821 if not test_info[param] == params_dict[param]:
1822 return False
1823 return True
1824 def error_msg(self, msg):
1825 """Prints an error message.
1826
1827 In addition to a catered error message, also prints
1828 out where the user can find more help. Then, program exits.
1829 """
1830 self.print_line(msg + (' If you need more information, ' +
1831 'please run with -h or --help to see valid commands.'))
1832 sys.exit(1)
1833
1834 def find_bots_that_run_test(self, test, bots):
1835 matching_bots = []
1836 for bot in bots:
1837 bot_info = bots[bot]
1838 tests = self.flatten_tests_for_bot(bot_info)
1839 for test_info in tests:
1840 test_name = ""
1841 if 'name' in test_info:
1842 test_name = test_info['name']
1843 elif 'test' in test_info:
1844 test_name = test_info['test']
1845 if not test_name == test:
1846 continue
1847 matching_bots.append(bot)
1848 return matching_bots
1849
1850 def find_tests_with_params(self, tests, params_dict):
1851 matching_tests = []
1852 for test_name in tests:
1853 test_info = tests[test_name]
1854 if not self.does_test_match(test_info, params_dict):
1855 continue
1856 if not test_name in matching_tests:
1857 matching_tests.append(test_name)
1858 return matching_tests
1859
1860 def flatten_waterfalls_for_query(self, waterfalls):
1861 bots = {}
1862 for waterfall in waterfalls:
Greg Guterman5c6144152020-02-28 20:08:531863 waterfall_tests = self.generate_waterfall_tests(waterfall)
1864 for bot in waterfall_tests:
1865 bot_info = waterfall_tests[bot]
Karen Qiane24b7ee2019-02-12 23:37:061866 if 'AAAAA' not in bot:
1867 bots[bot] = bot_info
1868 return bots
1869
1870 def flatten_tests_for_bot(self, bot_info):
1871 """Returns a list of flattened tests.
1872
1873 Returns a list of tests not grouped by test category
1874 for a specific bot.
1875 """
1876 TEST_CATS = self.get_test_generator_map().keys()
1877 tests = []
1878 for test_cat in TEST_CATS:
1879 if not test_cat in bot_info:
1880 continue
1881 test_cat_tests = bot_info[test_cat]
1882 tests = tests + test_cat_tests
1883 return tests
1884
1885 def flatten_tests_for_query(self, test_suites):
1886 """Returns a flattened dictionary of tests.
1887
1888 Returns a dictionary of tests associate with their
1889 configuration, not grouped by their test suite.
1890 """
1891 tests = {}
1892 for test_suite in test_suites.itervalues():
1893 for test in test_suite:
1894 test_info = test_suite[test]
1895 test_name = test
1896 if 'name' in test_info:
1897 test_name = test_info['name']
1898 tests[test_name] = test_info
1899 return tests
1900
1901 def parse_query_filter_params(self, params):
1902 """Parses the filter parameters.
1903
1904 Creates a dictionary from the parameters provided
1905 to filter the bot array.
1906 """
1907 params_dict = {}
1908 for p in params:
1909 # flag
1910 if p.startswith("--"):
1911 params_dict[p] = True
1912 else:
1913 pair = p.split(":")
1914 if len(pair) != 2:
1915 self.error_msg('Invalid command.')
1916 # regular parameters
1917 if pair[1].lower() == "true":
1918 params_dict[pair[0]] = True
1919 elif pair[1].lower() == "false":
1920 params_dict[pair[0]] = False
1921 else:
1922 params_dict[pair[0]] = pair[1]
1923 return params_dict
1924
1925 def get_test_suites_dict(self, bots):
1926 """Returns a dictionary of bots and their tests.
1927
1928 Returns a dictionary of bots and a list of their associated tests.
1929 """
1930 test_suite_dict = dict()
1931 for bot in bots:
1932 bot_info = bots[bot]
1933 tests = self.flatten_tests_for_bot(bot_info)
1934 test_suite_dict[bot] = tests
1935 return test_suite_dict
1936
1937 def output_query_result(self, result, json_file=None):
1938 """Outputs the result of the query.
1939
1940 If a json file parameter name is provided, then
1941 the result is output into the json file. If not,
1942 then the result is printed to the console.
1943 """
1944 output = json.dumps(result, indent=2)
1945 if json_file:
1946 self.write_file(json_file, output)
1947 else:
1948 self.print_line(output)
1949 return
1950
1951 def query(self, args):
1952 """Queries tests or bots.
1953
1954 Depending on the arguments provided, outputs a json of
1955 tests or bots matching the appropriate optional parameters provided.
1956 """
1957 # split up query statement
1958 query = args.query.split('/')
1959 self.load_configuration_files()
1960 self.resolve_configuration_files()
1961
1962 # flatten bots json
1963 tests = self.test_suites
1964 bots = self.flatten_waterfalls_for_query(self.waterfalls)
1965
1966 cmd_class = query[0]
1967
1968 # For queries starting with 'bots'
1969 if cmd_class == "bots":
1970 if len(query) == 1:
1971 return self.output_query_result(bots, args.json)
1972 # query with specific parameters
1973 elif len(query) == 2:
1974 if query[1] == 'tests':
1975 test_suites_dict = self.get_test_suites_dict(bots)
1976 return self.output_query_result(test_suites_dict, args.json)
1977 else:
1978 self.error_msg("This query should be in the format: bots/tests.")
1979
1980 else:
1981 self.error_msg("This query should have 0 or 1 '/', found %s instead."
1982 % str(len(query)-1))
1983
1984 # For queries starting with 'bot'
1985 elif cmd_class == "bot":
1986 if not len(query) == 2 and not len(query) == 3:
1987 self.error_msg("Command should have 1 or 2 '/', found %s instead."
1988 % str(len(query)-1))
1989 bot_id = query[1]
1990 if not bot_id in bots:
1991 self.error_msg("No bot named '" + bot_id + "' found.")
1992 bot_info = bots[bot_id]
1993 if len(query) == 2:
1994 return self.output_query_result(bot_info, args.json)
1995 if not query[2] == 'tests':
1996 self.error_msg("The query should be in the format:" +
1997 "bot/<bot-name>/tests.")
1998
1999 bot_tests = self.flatten_tests_for_bot(bot_info)
2000 return self.output_query_result(bot_tests, args.json)
2001
2002 # For queries starting with 'tests'
2003 elif cmd_class == "tests":
2004 if not len(query) == 1 and not len(query) == 2:
2005 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2006 % str(len(query)-1))
2007 flattened_tests = self.flatten_tests_for_query(tests)
2008 if len(query) == 1:
2009 return self.output_query_result(flattened_tests, args.json)
2010
2011 # create params dict
2012 params = query[1].split('&')
2013 params_dict = self.parse_query_filter_params(params)
2014 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2015 return self.output_query_result(matching_bots)
2016
2017 # For queries starting with 'test'
2018 elif cmd_class == "test":
2019 if not len(query) == 2 and not len(query) == 3:
2020 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2021 % str(len(query)-1))
2022 test_id = query[1]
2023 if len(query) == 2:
2024 flattened_tests = self.flatten_tests_for_query(tests)
2025 for test in flattened_tests:
2026 if test == test_id:
2027 return self.output_query_result(flattened_tests[test], args.json)
2028 self.error_msg("There is no test named %s." % test_id)
2029 if not query[2] == 'bots':
2030 self.error_msg("The query should be in the format: " +
2031 "test/<test-name>/bots")
2032 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2033 return self.output_query_result(bots_for_test)
2034
2035 else:
2036 self.error_msg("Your command did not match any valid commands." +
2037 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:282038
2039 def main(self, argv): # pragma: no cover
2040 self.parse_args(argv)
2041 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502042 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062043 elif self.args.query:
2044 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282045 else:
2046 self.generate_waterfalls()
2047 return 0
2048
2049if __name__ == "__main__": # pragma: no cover
2050 generator = BBJSONGenerator()
John Budorick699282e2019-02-13 01:27:332051 sys.exit(generator.main(sys.argv[1:]))