blob: 5add32119cb8cae7eb61ff93be586f4d2f02783b [file] [log] [blame]
Jamie Madillcf4f8c72021-05-20 19:24:231#!/usr/bin/env python3
Kenneth Russelleb60cbd22017-12-05 07:54:282# 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
Jamie Madillcf4f8c72021-05-20 19:24:2315import functools
Garrett Beatyd5ca75962020-05-07 16:58:3116import glob
Kenneth Russell8ceeabf2017-12-11 17:53:2817import itertools
Kenneth Russelleb60cbd22017-12-05 07:54:2818import json
19import os
Kenneth Russelleb60cbd22017-12-05 07:54:2820import string
21import sys
Kenneth Russelleb60cbd22017-12-05 07:54:2822
Brian Sheedya31578e2020-05-18 20:24:3623import buildbot_json_magic_substitutions as magic_substitutions
24
Kenneth Russelleb60cbd22017-12-05 07:54:2825THIS_DIR = os.path.dirname(os.path.abspath(__file__))
26
Brian Sheedyf74819b2021-06-04 01:38:3827BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP = {
28 'android-chromium': '_android_chrome',
29 'android-chromium-monochrome': '_android_monochrome',
30 'android-weblayer': '_android_weblayer',
31 'android-webview': '_android_webview',
32}
33
Kenneth Russelleb60cbd22017-12-05 07:54:2834
35class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4936 def __init__(self, message):
37 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2838
39
Kenneth Russell8ceeabf2017-12-11 17:53:2840# This class is only present to accommodate certain machines on
41# chromium.android.fyi which run certain tests as instrumentation
42# tests, but not as gtests. If this discrepancy were fixed then the
43# notion could be removed.
44class TestSuiteTypes(object):
45 GTEST = 'gtest'
46
47
Kenneth Russelleb60cbd22017-12-05 07:54:2848class BaseGenerator(object):
49 def __init__(self, bb_gen):
50 self.bb_gen = bb_gen
51
Kenneth Russell8ceeabf2017-12-11 17:53:2852 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2853 raise NotImplementedError()
54
55 def sort(self, tests):
56 raise NotImplementedError()
57
58
Jamie Madillcf4f8c72021-05-20 19:24:2359def custom_cmp(a, b):
60 return int(a > b) - int(a < b)
61
62
Kenneth Russell8ceeabf2017-12-11 17:53:2863def cmp_tests(a, b):
64 # Prefer to compare based on the "test" key.
Jamie Madillcf4f8c72021-05-20 19:24:2365 val = custom_cmp(a['test'], b['test'])
Kenneth Russell8ceeabf2017-12-11 17:53:2866 if val != 0:
67 return val
68 if 'name' in a and 'name' in b:
Jamie Madillcf4f8c72021-05-20 19:24:2369 return custom_cmp(a['name'], b['name']) # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:2870 if 'name' not in a and 'name' not in b:
71 return 0 # pragma: no cover
72 # Prefer to put variants of the same test after the first one.
73 if 'name' in a:
74 return 1
75 # 'name' is in b.
76 return -1 # pragma: no cover
77
78
Kenneth Russell8a386d42018-06-02 09:48:0179class GPUTelemetryTestGenerator(BaseGenerator):
Bo Liu555a0f92019-03-29 12:11:5680
81 def __init__(self, bb_gen, is_android_webview=False):
Kenneth Russell8a386d42018-06-02 09:48:0182 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5683 self._is_android_webview = is_android_webview
Kenneth Russell8a386d42018-06-02 09:48:0184
85 def generate(self, waterfall, tester_name, tester_config, input_tests):
86 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:2387 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russell8a386d42018-06-02 09:48:0188 test = self.bb_gen.generate_gpu_telemetry_test(
Bo Liu555a0f92019-03-29 12:11:5689 waterfall, tester_name, tester_config, test_name, test_config,
90 self._is_android_webview)
Kenneth Russell8a386d42018-06-02 09:48:0191 if test:
92 isolated_scripts.append(test)
93 return isolated_scripts
94
95 def sort(self, tests):
96 return sorted(tests, key=lambda x: x['name'])
97
98
Kenneth Russelleb60cbd22017-12-05 07:54:2899class GTestGenerator(BaseGenerator):
100 def __init__(self, bb_gen):
101 super(GTestGenerator, self).__init__(bb_gen)
102
Kenneth Russell8ceeabf2017-12-11 17:53:28103 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28104 # The relative ordering of some of the tests is important to
105 # minimize differences compared to the handwritten JSON files, since
106 # Python's sorts are stable and there are some tests with the same
107 # key (see gles2_conform_d3d9_test and similar variants). Avoid
108 # losing the order by avoiding coalescing the dictionaries into one.
109 gtests = []
Jamie Madillcf4f8c72021-05-20 19:24:23110 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoon67c3e832020-02-08 07:39:38111 # Variants allow more than one definition for a given test, and is defined
112 # in array format from resolve_variants().
113 if not isinstance(test_config, list):
114 test_config = [test_config]
115
116 for config in test_config:
117 test = self.bb_gen.generate_gtest(
118 waterfall, tester_name, tester_config, test_name, config)
119 if test:
120 # generate_gtest may veto the test generation on this tester.
121 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28122 return gtests
123
124 def sort(self, tests):
Jamie Madillcf4f8c72021-05-20 19:24:23125 return sorted(tests, key=functools.cmp_to_key(cmp_tests))
Kenneth Russelleb60cbd22017-12-05 07:54:28126
127
128class IsolatedScriptTestGenerator(BaseGenerator):
129 def __init__(self, bb_gen):
130 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
131
Kenneth Russell8ceeabf2017-12-11 17:53:28132 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28133 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23134 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43135 # Variants allow more than one definition for a given test, and is defined
136 # in array format from resolve_variants().
137 if not isinstance(test_config, list):
138 test_config = [test_config]
139
140 for config in test_config:
141 test = self.bb_gen.generate_isolated_script_test(
142 waterfall, tester_name, tester_config, test_name, config)
143 if test:
144 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28145 return isolated_scripts
146
147 def sort(self, tests):
148 return sorted(tests, key=lambda x: x['name'])
149
150
151class ScriptGenerator(BaseGenerator):
152 def __init__(self, bb_gen):
153 super(ScriptGenerator, self).__init__(bb_gen)
154
Kenneth Russell8ceeabf2017-12-11 17:53:28155 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28156 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23157 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28158 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28159 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28160 if test:
161 scripts.append(test)
162 return scripts
163
164 def sort(self, tests):
165 return sorted(tests, key=lambda x: x['name'])
166
167
168class JUnitGenerator(BaseGenerator):
169 def __init__(self, bb_gen):
170 super(JUnitGenerator, self).__init__(bb_gen)
171
Kenneth Russell8ceeabf2017-12-11 17:53:28172 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28173 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23174 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28175 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28176 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28177 if test:
178 scripts.append(test)
179 return scripts
180
181 def sort(self, tests):
182 return sorted(tests, key=lambda x: x['test'])
183
184
Xinan Lin05fb9c1752020-12-17 00:15:52185class SkylabGenerator(BaseGenerator):
186 def __init__(self, bb_gen):
187 super(SkylabGenerator, self).__init__(bb_gen)
188
189 def generate(self, waterfall, tester_name, tester_config, input_tests):
190 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23191 for test_name, test_config in sorted(input_tests.items()):
Xinan Lin05fb9c1752020-12-17 00:15:52192 for config in test_config:
193 test = self.bb_gen.generate_skylab_test(waterfall, tester_name,
194 tester_config, test_name,
195 config)
196 if test:
197 scripts.append(test)
198 return scripts
199
200 def sort(self, tests):
201 return sorted(tests, key=lambda x: x['test'])
202
203
Jeff Yoon67c3e832020-02-08 07:39:38204def check_compound_references(other_test_suites=None,
205 sub_suite=None,
206 suite=None,
207 target_test_suites=None,
208 test_type=None,
209 **kwargs):
210 """Ensure comound reference's don't target other compounds"""
211 del kwargs
212 if sub_suite in other_test_suites or sub_suite in target_test_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15213 raise BBGenErr('%s may not refer to other composition type test '
214 'suites (error found while processing %s)' %
215 (test_type, suite))
216
Jeff Yoon67c3e832020-02-08 07:39:38217
218def check_basic_references(basic_suites=None,
219 sub_suite=None,
220 suite=None,
221 **kwargs):
222 """Ensure test has a basic suite reference"""
223 del kwargs
224 if sub_suite not in basic_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15225 raise BBGenErr('Unable to find reference to %s while processing %s' %
226 (sub_suite, suite))
227
Jeff Yoon67c3e832020-02-08 07:39:38228
229def check_conflicting_definitions(basic_suites=None,
230 seen_tests=None,
231 sub_suite=None,
232 suite=None,
233 test_type=None,
234 **kwargs):
235 """Ensure that if a test is reachable via multiple basic suites,
236 all of them have an identical definition of the tests.
237 """
238 del kwargs
239 for test_name in basic_suites[sub_suite]:
240 if (test_name in seen_tests and
241 basic_suites[sub_suite][test_name] !=
242 basic_suites[seen_tests[test_name]][test_name]):
243 raise BBGenErr('Conflicting test definitions for %s from %s '
244 'and %s in %s (error found while processing %s)'
245 % (test_name, seen_tests[test_name], sub_suite,
246 test_type, suite))
247 seen_tests[test_name] = sub_suite
248
249def check_matrix_identifier(sub_suite=None,
250 suite=None,
251 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05252 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38253 **kwargs):
254 """Ensure 'idenfitier' is defined for each variant"""
255 del kwargs
256 sub_suite_config = suite_def[sub_suite]
257 for variant in sub_suite_config.get('variants', []):
Jeff Yoonda581c32020-03-06 03:56:05258 if isinstance(variant, str):
259 if variant not in all_variants:
260 raise BBGenErr('Missing variant definition for %s in variants.pyl'
261 % variant)
262 variant = all_variants[variant]
263
Jeff Yoon67c3e832020-02-08 07:39:38264 if not 'identifier' in variant:
265 raise BBGenErr('Missing required identifier field in matrix '
266 'compound suite %s, %s' % (suite, sub_suite))
267
268
Kenneth Russelleb60cbd22017-12-05 07:54:28269class BBJSONGenerator(object):
Garrett Beaty1afaccc2020-06-25 19:58:15270 def __init__(self, args):
Kenneth Russelleb60cbd22017-12-05 07:54:28271 self.this_dir = THIS_DIR
Garrett Beaty1afaccc2020-06-25 19:58:15272 self.args = args
Kenneth Russelleb60cbd22017-12-05 07:54:28273 self.waterfalls = None
274 self.test_suites = None
275 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01276 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41277 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05278 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28279
Garrett Beaty1afaccc2020-06-25 19:58:15280 @staticmethod
281 def parse_args(argv):
282
283 # RawTextHelpFormatter allows for styling of help statement
284 parser = argparse.ArgumentParser(
285 formatter_class=argparse.RawTextHelpFormatter)
286
287 group = parser.add_mutually_exclusive_group()
288 group.add_argument(
289 '-c',
290 '--check',
291 action='store_true',
292 help=
293 'Do consistency checks of configuration and generated files and then '
294 'exit. Used during presubmit. '
295 'Causes the tool to not generate any files.')
296 group.add_argument(
297 '--query',
298 type=str,
299 help=(
300 "Returns raw JSON information of buildbots and tests.\n" +
301 "Examples:\n" + " List all bots (all info):\n" +
302 " --query bots\n\n" +
303 " List all bots and only their associated tests:\n" +
304 " --query bots/tests\n\n" +
305 " List all information about 'bot1' " +
306 "(make sure you have quotes):\n" + " --query bot/'bot1'\n\n" +
307 " List tests running for 'bot1' (make sure you have quotes):\n" +
308 " --query bot/'bot1'/tests\n\n" + " List all tests:\n" +
309 " --query tests\n\n" +
310 " List all tests and the bots running them:\n" +
311 " --query tests/bots\n\n" +
312 " List all tests that satisfy multiple parameters\n" +
313 " (separation of parameters by '&' symbol):\n" +
314 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
315 " List all tests that run with a specific flag:\n" +
316 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
317 " List specific test (make sure you have quotes):\n"
318 " --query test/'test1'\n\n"
319 " List all bots running 'test1' " +
320 "(make sure you have quotes):\n" + " --query test/'test1'/bots"))
321 parser.add_argument(
322 '-n',
323 '--new-files',
324 action='store_true',
325 help=
326 'Write output files as .new.json. Useful during development so old and '
327 'new files can be looked at side-by-side.')
328 parser.add_argument('-v',
329 '--verbose',
330 action='store_true',
331 help='Increases verbosity. Affects consistency checks.')
332 parser.add_argument('waterfall_filters',
333 metavar='waterfalls',
334 type=str,
335 nargs='*',
336 help='Optional list of waterfalls to generate.')
337 parser.add_argument(
338 '--pyl-files-dir',
339 type=os.path.realpath,
340 help='Path to the directory containing the input .pyl files.')
341 parser.add_argument(
342 '--json',
343 metavar='JSON_FILE_PATH',
344 help='Outputs results into a json file. Only works with query function.'
345 )
Chong Guee622242020-10-28 18:17:35346 parser.add_argument('--isolate-map-file',
347 metavar='PATH',
348 help='path to additional isolate map files.',
349 default=[],
350 action='append',
351 dest='isolate_map_files')
Garrett Beaty1afaccc2020-06-25 19:58:15352 parser.add_argument(
353 '--infra-config-dir',
354 help='Path to the LUCI services configuration directory',
355 default=os.path.abspath(
356 os.path.join(os.path.dirname(__file__), '..', '..', 'infra',
357 'config')))
358 args = parser.parse_args(argv)
359 if args.json and not args.query:
360 parser.error(
361 "The --json flag can only be used with --query.") # pragma: no cover
362 args.infra_config_dir = os.path.abspath(args.infra_config_dir)
363 return args
364
Kenneth Russelleb60cbd22017-12-05 07:54:28365 def generate_abs_file_path(self, relative_path):
Garrett Beaty1afaccc2020-06-25 19:58:15366 return os.path.join(self.this_dir, relative_path)
Kenneth Russelleb60cbd22017-12-05 07:54:28367
Stephen Martinis7eb8b612018-09-21 00:17:50368 def print_line(self, line):
369 # Exists so that tests can mock
Jamie Madillcf4f8c72021-05-20 19:24:23370 print(line) # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:50371
Kenneth Russelleb60cbd22017-12-05 07:54:28372 def read_file(self, relative_path):
Garrett Beaty1afaccc2020-06-25 19:58:15373 with open(self.generate_abs_file_path(relative_path)) as fp:
374 return fp.read()
Kenneth Russelleb60cbd22017-12-05 07:54:28375
376 def write_file(self, relative_path, contents):
Garrett Beaty1afaccc2020-06-25 19:58:15377 with open(self.generate_abs_file_path(relative_path), 'wb') as fp:
Jamie Madillcf4f8c72021-05-20 19:24:23378 fp.write(contents.encode('utf-8'))
Kenneth Russelleb60cbd22017-12-05 07:54:28379
Zhiling Huangbe008172018-03-08 19:13:11380 def pyl_file_path(self, filename):
381 if self.args and self.args.pyl_files_dir:
382 return os.path.join(self.args.pyl_files_dir, filename)
383 return filename
384
Kenneth Russelleb60cbd22017-12-05 07:54:28385 def load_pyl_file(self, filename):
386 try:
Zhiling Huangbe008172018-03-08 19:13:11387 return ast.literal_eval(self.read_file(
388 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28389 except (SyntaxError, ValueError) as e: # pragma: no cover
390 raise BBGenErr('Failed to parse pyl file "%s": %s' %
391 (filename, e)) # pragma: no cover
392
Kenneth Russell8a386d42018-06-02 09:48:01393 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
394 # Currently it is only mandatory for bots which run GPU tests. Change these to
395 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28396 def is_android(self, tester_config):
397 return tester_config.get('os_type') == 'android'
398
Ben Pastenea9e583b2019-01-16 02:57:26399 def is_chromeos(self, tester_config):
400 return tester_config.get('os_type') == 'chromeos'
401
Chong Guc2ca5d02022-01-11 19:52:17402 def is_fuchsia(self, tester_config):
403 return tester_config.get('os_type') == 'fuchsia'
404
Brian Sheedy781c8ca42021-03-08 22:03:21405 def is_lacros(self, tester_config):
406 return tester_config.get('os_type') == 'lacros'
407
Kenneth Russell8a386d42018-06-02 09:48:01408 def is_linux(self, tester_config):
409 return tester_config.get('os_type') == 'linux'
410
Kai Ninomiya40de9f52019-10-18 21:38:49411 def is_mac(self, tester_config):
412 return tester_config.get('os_type') == 'mac'
413
414 def is_win(self, tester_config):
415 return tester_config.get('os_type') == 'win'
416
417 def is_win64(self, tester_config):
418 return (tester_config.get('os_type') == 'win' and
419 tester_config.get('browser_config') == 'release_x64')
420
Kenneth Russelleb60cbd22017-12-05 07:54:28421 def get_exception_for_test(self, test_name, test_config):
422 # gtests may have both "test" and "name" fields, and usually, if the "name"
423 # field is specified, it means that the same test is being repurposed
424 # multiple times with different command line arguments. To handle this case,
425 # prefer to lookup per the "name" field of the test itself, as opposed to
426 # the "test_name", which is actually the "test" field.
427 if 'name' in test_config:
428 return self.exceptions.get(test_config['name'])
429 else:
430 return self.exceptions.get(test_name)
431
Nico Weberb0b3f5862018-07-13 18:45:15432 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28433 # Currently, the only reason a test should not run on a given tester is that
434 # it's in the exceptions. (Once the GPU waterfall generation script is
435 # incorporated here, the rules will become more complex.)
436 exception = self.get_exception_for_test(test_name, test_config)
437 if not exception:
438 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28439 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28440 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28441 if remove_from:
442 if tester_name in remove_from:
443 return False
444 # TODO(kbr): this code path was added for some tests (including
445 # android_webview_unittests) on one machine (Nougat Phone
446 # Tester) which exists with the same name on two waterfalls,
447 # chromium.android and chromium.fyi; the tests are run on one
448 # but not the other. Once the bots are all uniquely named (a
449 # different ongoing project) this code should be removed.
450 # TODO(kbr): add coverage.
451 return (tester_name + ' ' + waterfall['name']
452 not in remove_from) # pragma: no cover
453 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28454
Nico Weber79dc5f6852018-07-13 19:38:49455 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28456 exception = self.get_exception_for_test(test_name, test)
457 if not exception:
458 return None
Nico Weber79dc5f6852018-07-13 19:38:49459 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28460
Brian Sheedye6ea0ee2019-07-11 02:54:37461 def get_test_replacements(self, test, test_name, tester_name):
462 exception = self.get_exception_for_test(test_name, test)
463 if not exception:
464 return None
465 return exception.get('replacements', {}).get(tester_name)
466
Kenneth Russell8a386d42018-06-02 09:48:01467 def merge_command_line_args(self, arr, prefix, splitter):
468 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01469 idx = 0
470 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01471 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01472 while idx < len(arr):
473 flag = arr[idx]
474 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01475 if flag.startswith(prefix):
476 arg = flag[prefix_len:]
477 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01478 if first_idx < 0:
479 first_idx = idx
480 else:
481 delete_current_entry = True
482 if delete_current_entry:
483 del arr[idx]
484 else:
485 idx += 1
486 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01487 arr[first_idx] = prefix + splitter.join(accumulated_args)
488 return arr
489
490 def maybe_fixup_args_array(self, arr):
491 # The incoming array of strings may be an array of command line
492 # arguments. To make it easier to turn on certain features per-bot or
493 # per-test-suite, look specifically for certain flags and merge them
494 # appropriately.
495 # --enable-features=Feature1 --enable-features=Feature2
496 # are merged to:
497 # --enable-features=Feature1,Feature2
498 # and:
499 # --extra-browser-args=arg1 --extra-browser-args=arg2
500 # are merged to:
501 # --extra-browser-args=arg1 arg2
502 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
503 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Yuly Novikov8c487e72020-10-16 20:00:29504 arr = self.merge_command_line_args(arr, '--test-launcher-filter-file=', ';')
Kenneth Russell650995a2018-05-03 21:17:01505 return arr
506
Brian Sheedy5f173bb2021-11-24 00:45:54507 def substitute_magic_args(self, test_config, tester_name):
Brian Sheedya31578e2020-05-18 20:24:36508 """Substitutes any magic substitution args present in |test_config|.
509
510 Substitutions are done in-place.
511
512 See buildbot_json_magic_substitutions.py for more information on this
513 feature.
514
515 Args:
516 test_config: A dict containing a configuration for a specific test on
517 a specific builder, e.g. the output of update_and_cleanup_test.
Brian Sheedy5f173bb2021-11-24 00:45:54518 tester_name: A string containing the name of the tester that |test_config|
519 came from.
Brian Sheedya31578e2020-05-18 20:24:36520 """
521 substituted_array = []
522 for arg in test_config.get('args', []):
523 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
524 function = arg.replace(
525 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
526 if hasattr(magic_substitutions, function):
527 substituted_array.extend(
Brian Sheedy5f173bb2021-11-24 00:45:54528 getattr(magic_substitutions, function)(test_config, tester_name))
Brian Sheedya31578e2020-05-18 20:24:36529 else:
530 raise BBGenErr(
531 'Magic substitution function %s does not exist' % function)
532 else:
533 substituted_array.append(arg)
534 if substituted_array:
535 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
536
Kenneth Russelleb60cbd22017-12-05 07:54:28537 def dictionary_merge(self, a, b, path=None, update=True):
538 """http://stackoverflow.com/questions/7204805/
539 python-dictionaries-of-dictionaries-merge
540 merges b into a
541 """
542 if path is None:
543 path = []
544 for key in b:
545 if key in a:
546 if isinstance(a[key], dict) and isinstance(b[key], dict):
547 self.dictionary_merge(a[key], b[key], path + [str(key)])
548 elif a[key] == b[key]:
549 pass # same leaf value
550 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06551 # Args arrays are lists of strings. Just concatenate them,
552 # and don't sort them, in order to keep some needed
Weizhong Xia91b53362022-01-05 17:13:35553 # arguments adjacent (like --timeout-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28554 if all(isinstance(x, str)
555 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01556 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28557 else:
558 # TODO(kbr): this only works properly if the two arrays are
559 # the same length, which is currently always the case in the
560 # swarming dimension_sets that we have to merge. It will fail
561 # to merge / override 'args' arrays which are different
562 # length.
Jamie Madillcf4f8c72021-05-20 19:24:23563 for idx in range(len(b[key])):
Kenneth Russell8ceeabf2017-12-11 17:53:28564 try:
565 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
566 path + [str(key), str(idx)],
567 update=update)
Jeff Yoon8154e582019-12-03 23:30:01568 except (IndexError, TypeError):
569 raise BBGenErr('Error merging lists by key "%s" from source %s '
570 'into target %s at index %s. Verify target list '
571 'length is equal or greater than source'
572 % (str(key), str(b), str(a), str(idx)))
John Budorick5bc387fe2019-05-09 20:02:53573 elif update:
574 if b[key] is None:
575 del a[key]
576 else:
577 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28578 else:
579 raise BBGenErr('Conflict at %s' % '.'.join(
580 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53581 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28582 a[key] = b[key]
583 return a
584
John Budorickab108712018-09-01 00:12:21585 def initialize_args_for_test(
586 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21587 args = []
588 args.extend(generated_test.get('args', []))
589 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22590
Kenneth Russell8a386d42018-06-02 09:48:01591 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21592 val = generated_test.pop(key, [])
593 if fn(tester_config):
594 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01595
596 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
Brian Sheedy781c8ca42021-03-08 22:03:21597 add_conditional_args('lacros_args', self.is_lacros)
Kenneth Russell8a386d42018-06-02 09:48:01598 add_conditional_args('linux_args', self.is_linux)
599 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36600 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49601 add_conditional_args('mac_args', self.is_mac)
602 add_conditional_args('win_args', self.is_win)
603 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01604
John Budorickab108712018-09-01 00:12:21605 for key in additional_arg_keys or []:
606 args.extend(generated_test.pop(key, []))
607 args.extend(tester_config.get(key, []))
608
609 if args:
610 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01611
Kenneth Russelleb60cbd22017-12-05 07:54:28612 def initialize_swarming_dictionary_for_test(self, generated_test,
613 tester_config):
614 if 'swarming' not in generated_test:
615 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28616 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
617 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38618 'can_use_on_swarming_builders': tester_config.get('use_swarming',
619 True)
Dirk Pranke81ff51c2017-12-09 19:24:28620 })
Kenneth Russelleb60cbd22017-12-05 07:54:28621 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03622 if ('dimension_sets' not in generated_test['swarming'] and
623 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28624 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
625 tester_config['swarming']['dimension_sets'])
626 self.dictionary_merge(generated_test['swarming'],
627 tester_config['swarming'])
Brian Sheedybc984e242021-04-21 23:44:51628 # Apply any platform-specific Swarming dimensions after the generic ones.
Kenneth Russelleb60cbd22017-12-05 07:54:28629 if 'android_swarming' in generated_test:
630 if self.is_android(tester_config): # pragma: no cover
631 self.dictionary_merge(
632 generated_test['swarming'],
633 generated_test['android_swarming']) # pragma: no cover
634 del generated_test['android_swarming'] # pragma: no cover
Brian Sheedybc984e242021-04-21 23:44:51635 if 'chromeos_swarming' in generated_test:
636 if self.is_chromeos(tester_config): # pragma: no cover
637 self.dictionary_merge(
638 generated_test['swarming'],
639 generated_test['chromeos_swarming']) # pragma: no cover
640 del generated_test['chromeos_swarming'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28641
642 def clean_swarming_dictionary(self, swarming_dict):
643 # Clean out redundant entries from a test's "swarming" dictionary.
644 # This is really only needed to retain 100% parity with the
645 # handwritten JSON files, and can be removed once all the files are
646 # autogenerated.
647 if 'shards' in swarming_dict:
648 if swarming_dict['shards'] == 1: # pragma: no cover
649 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24650 if 'hard_timeout' in swarming_dict:
651 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
652 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43653 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28654 # Remove all other keys.
Jamie Madillcf4f8c72021-05-20 19:24:23655 for k in list(swarming_dict): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28656 if k != 'can_use_on_swarming_builders': # pragma: no cover
657 del swarming_dict[k] # pragma: no cover
658
Stephen Martinis0382bc12018-09-17 22:29:07659 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
660 waterfall):
661 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01662 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07663 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28664 # See if there are any exceptions that need to be merged into this
665 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49666 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28667 if modifications:
668 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23669 if 'swarming' in test:
670 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28671 # Ensure all Android Swarming tests run only on userdebug builds if another
672 # build type was not specified.
673 if 'swarming' in test and self.is_android(tester_config):
674 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22675 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28676 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37677 self.replace_test_args(test, test_name, tester_name)
Ben Pastenee012aea42019-05-14 22:32:28678
Kenneth Russelleb60cbd22017-12-05 07:54:28679 return test
680
Brian Sheedye6ea0ee2019-07-11 02:54:37681 def replace_test_args(self, test, test_name, tester_name):
682 replacements = self.get_test_replacements(
683 test, test_name, tester_name) or {}
684 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23685 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37686 if key not in valid_replacement_keys:
687 raise BBGenErr(
688 'Given replacement key %s for %s on %s is not in the list of valid '
689 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23690 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37691 found_key = False
692 for i, test_key in enumerate(test.get(key, [])):
693 # Handle both the key/value being replaced being defined as two
694 # separate items or as key=value.
695 if test_key == replacement_key:
696 found_key = True
697 # Handle flags without values.
698 if replacement_val == None:
699 del test[key][i]
700 else:
701 test[key][i+1] = replacement_val
702 break
703 elif test_key.startswith(replacement_key + '='):
704 found_key = True
705 if replacement_val == None:
706 del test[key][i]
707 else:
708 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
709 break
710 if not found_key:
711 raise BBGenErr('Could not find %s in existing list of values for key '
712 '%s in %s on %s' % (replacement_key, key, test_name,
713 tester_name))
714
Shenghua Zhangaba8bad2018-02-07 02:12:09715 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05716 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26717 True):
718 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06719 # are targeting CrOS hardware and so need the special trigger script.
720 dimension_sets = test['swarming']['dimension_sets']
Ben Pastenea9e583b2019-01-16 02:57:26721 if all('device_type' in ds for ds in dimension_sets):
722 test['trigger_script'] = {
723 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
724 }
Shenghua Zhangaba8bad2018-02-07 02:12:09725
Yuly Novikov26dd47052021-02-11 00:57:14726 def add_logdog_butler_cipd_package(self, tester_config, result):
727 if not tester_config.get('skip_cipd_packages', False):
728 cipd_packages = result['swarming'].get('cipd_packages', [])
729 already_added = len([
730 package for package in cipd_packages
731 if package.get('cipd_package', "").find('logdog/butler') > 0
732 ]) > 0
733 if not already_added:
734 cipd_packages.append({
735 'cipd_package':
736 'infra/tools/luci/logdog/butler/${platform}',
737 'location':
738 'bin',
739 'revision':
740 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
741 })
742 result['swarming']['cipd_packages'] = cipd_packages
743
Ben Pastene858f4be2019-01-09 23:52:09744 def add_android_presentation_args(self, tester_config, test_name, result):
745 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38746 bucket = tester_config.get('results_bucket', 'chromium-result-details')
747 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09748 if (result['swarming']['can_use_on_swarming_builders'] and not
749 tester_config.get('skip_merge_script', False)):
750 result['merge'] = {
751 'args': [
752 '--bucket',
John Budorick262ae112019-07-12 19:24:38753 bucket,
Ben Pastene858f4be2019-01-09 23:52:09754 '--test-name',
Rakib M. Hasanc9e01c62020-07-27 22:48:12755 result.get('name', test_name)
Ben Pastene858f4be2019-01-09 23:52:09756 ],
757 'script': '//build/android/pylib/results/presentation/'
758 'test_results_presentation.py',
759 }
Ben Pastene858f4be2019-01-09 23:52:09760 if not tester_config.get('skip_output_links', False):
761 result['swarming']['output_links'] = [
762 {
763 'link': [
764 'https://luci-logdog.appspot.com/v/?s',
765 '=android%2Fswarming%2Flogcats%2F',
766 '${TASK_ID}%2F%2B%2Funified_logcats',
767 ],
768 'name': 'shard #${SHARD_INDEX} logcats',
769 },
770 ]
771 if args:
772 result['args'] = args
773
Kenneth Russelleb60cbd22017-12-05 07:54:28774 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
775 test_config):
776 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15777 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28778 return None
779 result = copy.deepcopy(test_config)
780 if 'test' in result:
Rakib M. Hasanc9e01c62020-07-27 22:48:12781 if 'name' not in result:
782 result['name'] = test_name
Kenneth Russelleb60cbd22017-12-05 07:54:28783 else:
784 result['test'] = test_name
785 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21786
787 self.initialize_args_for_test(
788 result, tester_config, additional_arg_keys=['gtest_args'])
Jamie Madilla8be0d72020-10-02 05:24:04789 if self.is_android(tester_config) and tester_config.get(
Yuly Novikov26dd47052021-02-11 00:57:14790 'use_swarming', True):
791 if not test_config.get('use_isolated_scripts_api', False):
792 # TODO(https://crbug.com/1137998) make Android presentation work with
793 # isolated scripts in test_results_presentation.py merge script
794 self.add_android_presentation_args(tester_config, test_name, result)
795 result['args'] = result.get('args', []) + ['--recover-devices']
796 self.add_logdog_butler_cipd_package(tester_config, result)
Benjamin Pastene766d48f52017-12-18 21:47:42797
Stephen Martinis0382bc12018-09-17 22:29:07798 result = self.update_and_cleanup_test(
799 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09800 self.add_common_test_properties(result, tester_config)
Brian Sheedy5f173bb2021-11-24 00:45:54801 self.substitute_magic_args(result, tester_name)
Stephen Martinisbc7b7772019-05-01 22:01:43802
803 if not result.get('merge'):
804 # TODO(https://crbug.com/958376): Consider adding the ability to not have
805 # this default.
Jamie Madilla8be0d72020-10-02 05:24:04806 if test_config.get('use_isolated_scripts_api', False):
807 merge_script = 'standard_isolated_script_merge'
808 else:
809 merge_script = 'standard_gtest_merge'
810
Stephen Martinisbc7b7772019-05-01 22:01:43811 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04812 'script': '//testing/merge_scripts/%s.py' % merge_script,
813 'args': [],
Stephen Martinisbc7b7772019-05-01 22:01:43814 }
Kenneth Russelleb60cbd22017-12-05 07:54:28815 return result
816
817 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
818 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01819 if not self.should_run_on_tester(waterfall, tester_name, test_name,
820 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28821 return None
822 result = copy.deepcopy(test_config)
823 result['isolate_name'] = result.get('isolate_name', test_name)
Jeff Yoonb8bfdbf32020-03-13 19:14:43824 result['name'] = result.get('name', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28825 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01826 self.initialize_args_for_test(result, tester_config)
Yuly Novikov26dd47052021-02-11 00:57:14827 if self.is_android(tester_config) and tester_config.get(
828 'use_swarming', True):
829 if tester_config.get('use_android_presentation', False):
830 # TODO(https://crbug.com/1137998) make Android presentation work with
831 # isolated scripts in test_results_presentation.py merge script
832 self.add_android_presentation_args(tester_config, test_name, result)
833 self.add_logdog_butler_cipd_package(tester_config, result)
Stephen Martinis0382bc12018-09-17 22:29:07834 result = self.update_and_cleanup_test(
835 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09836 self.add_common_test_properties(result, tester_config)
Brian Sheedy5f173bb2021-11-24 00:45:54837 self.substitute_magic_args(result, tester_name)
Stephen Martinisf50047062019-05-06 22:26:17838
839 if not result.get('merge'):
840 # TODO(https://crbug.com/958376): Consider adding the ability to not have
841 # this default.
842 result['merge'] = {
843 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
844 'args': [],
845 }
Kenneth Russelleb60cbd22017-12-05 07:54:28846 return result
847
848 def generate_script_test(self, waterfall, tester_name, tester_config,
849 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44850 # TODO(https://crbug.com/953072): Remove this check whenever a better
851 # long-term solution is implemented.
852 if (waterfall.get('forbid_script_tests', False) or
853 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
854 raise BBGenErr('Attempted to generate a script test on tester ' +
855 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01856 if not self.should_run_on_tester(waterfall, tester_name, test_name,
857 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28858 return None
859 result = {
860 'name': test_name,
861 'script': test_config['script']
862 }
Stephen Martinis0382bc12018-09-17 22:29:07863 result = self.update_and_cleanup_test(
864 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy5f173bb2021-11-24 00:45:54865 self.substitute_magic_args(result, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28866 return result
867
868 def generate_junit_test(self, waterfall, tester_name, tester_config,
869 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01870 if not self.should_run_on_tester(waterfall, tester_name, test_name,
871 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28872 return None
John Budorickdef6acb2019-09-17 22:51:09873 result = copy.deepcopy(test_config)
874 result.update({
John Budorickcadc4952019-09-16 23:51:37875 'name': test_name,
876 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09877 })
878 self.initialize_args_for_test(result, tester_config)
879 result = self.update_and_cleanup_test(
880 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy5f173bb2021-11-24 00:45:54881 self.substitute_magic_args(result, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28882 return result
883
Xinan Lin05fb9c1752020-12-17 00:15:52884 def generate_skylab_test(self, waterfall, tester_name, tester_config,
885 test_name, test_config):
886 if not self.should_run_on_tester(waterfall, tester_name, test_name,
887 test_config):
888 return None
889 result = copy.deepcopy(test_config)
890 result.update({
891 'test': test_name,
892 })
893 self.initialize_args_for_test(result, tester_config)
894 result = self.update_and_cleanup_test(result, test_name, tester_name,
895 tester_config, waterfall)
Brian Sheedy5f173bb2021-11-24 00:45:54896 self.substitute_magic_args(result, tester_name)
Xinan Lin05fb9c1752020-12-17 00:15:52897 return result
898
Stephen Martinis2a0667022018-09-25 22:31:14899 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01900 substitutions = {
901 # Any machine in waterfalls.pyl which desires to run GPU tests
902 # must provide the os_type key.
903 'os_type': tester_config['os_type'],
904 'gpu_vendor_id': '0',
905 'gpu_device_id': '0',
906 }
Stephen Martinis2a0667022018-09-25 22:31:14907 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01908 if 'gpu' in dimension_set:
909 # First remove the driver version, then split into vendor and device.
910 gpu = dimension_set['gpu']
Yuly Novikove4b2fef2020-09-04 05:53:11911 if gpu != 'none':
912 gpu = gpu.split('-')[0].split(':')
913 substitutions['gpu_vendor_id'] = gpu[0]
914 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01915 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
916
917 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56918 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01919 # These are all just specializations of isolated script tests with
920 # a bunch of boilerplate command line arguments added.
921
922 # The step name must end in 'test' or 'tests' in order for the
923 # results to automatically show up on the flakiness dashboard.
924 # (At least, this was true some time ago.) Continue to use this
925 # naming convention for the time being to minimize changes.
926 step_name = test_config.get('name', test_name)
927 if not (step_name.endswith('test') or step_name.endswith('tests')):
928 step_name = '%s_tests' % step_name
929 result = self.generate_isolated_script_test(
930 waterfall, tester_name, tester_config, step_name, test_config)
931 if not result:
932 return None
Chong Gub75754b32020-03-13 16:39:20933 result['isolate_name'] = test_config.get(
Brian Sheedyf74819b2021-06-04 01:38:38934 'isolate_name',
935 self.get_default_isolate_name(tester_config, is_android_webview))
Chan Liab7d8dd82020-04-24 23:42:19936
Chan Lia3ad1502020-04-28 05:32:11937 # Populate test_id_prefix.
Brian Sheedyf74819b2021-06-04 01:38:38938 gn_entry = self.gn_isolate_map[result['isolate_name']]
Chan Li17d969f92020-07-10 00:50:03939 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19940
Kenneth Russell8a386d42018-06-02 09:48:01941 args = result.get('args', [])
942 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14943
Brian Sheedyd8c0c73d2021-07-05 02:11:30944 # TODO(skbug.com/12149): Remove this once Gold-based tests no longer clobber
945 # earlier results on retry attempts.
946 is_gold_based_test = False
947 for a in args:
948 if '--git-revision' in a:
949 is_gold_based_test = True
950 break
951 if is_gold_based_test:
952 for a in args:
953 if '--test-filter' in a or '--isolated-script-test-filter' in a:
954 raise RuntimeError(
955 '--test-filter/--isolated-script-test-filter are currently not '
956 'supported for Gold-based GPU tests. See skbug.com/12100 and '
957 'skbug.com/12149 for more details.')
958
erikchen6da2d9b2018-08-03 23:01:14959 # These tests upload and download results from cloud storage and therefore
960 # aren't idempotent yet. https://crbug.com/549140.
961 result['swarming']['idempotent'] = False
962
Kenneth Russell44910c32018-12-03 23:35:11963 # The GPU tests act much like integration tests for the entire browser, and
964 # tend to uncover flakiness bugs more readily than other test suites. In
965 # order to surface any flakiness more readily to the developer of the CL
966 # which is introducing it, we disable retries with patch on the commit
967 # queue.
968 result['should_retry_with_patch'] = False
969
Bo Liu555a0f92019-03-29 12:11:56970 browser = ('android-webview-instrumentation'
971 if is_android_webview else tester_config['browser_config'])
Brian Sheedy4053a702020-07-28 02:09:52972
973 # Most platforms require --enable-logging=stderr to get useful browser logs.
974 # However, this actively messes with logging on CrOS (because Chrome's
975 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
976 # in order to see JavaScript console messages. See
977 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
978 logging_arg = '--log-level=0' if self.is_chromeos(
979 tester_config) else '--enable-logging=stderr'
980
Kenneth Russell8a386d42018-06-02 09:48:01981 args = [
Bo Liu555a0f92019-03-29 12:11:56982 test_to_run,
983 '--show-stdout',
984 '--browser=%s' % browser,
985 # --passthrough displays more of the logging in Telemetry when
986 # run via typ, in particular some of the warnings about tests
987 # being expected to fail, but passing.
988 '--passthrough',
989 '-v',
Brian Sheedy4053a702020-07-28 02:09:52990 '--extra-browser-args=%s --js-flags=--expose-gc' % logging_arg,
Kenneth Russell8a386d42018-06-02 09:48:01991 ] + args
992 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14993 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01994 return result
995
Brian Sheedyf74819b2021-06-04 01:38:38996 def get_default_isolate_name(self, tester_config, is_android_webview):
997 if self.is_android(tester_config):
998 if is_android_webview:
999 return 'telemetry_gpu_integration_test_android_webview'
1000 return (
1001 'telemetry_gpu_integration_test' +
1002 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Chong Guc2ca5d02022-01-11 19:52:171003 elif self.is_fuchsia(tester_config):
1004 return 'telemetry_gpu_integration_test_fuchsia'
Brian Sheedyf74819b2021-06-04 01:38:381005 else:
1006 return 'telemetry_gpu_integration_test'
1007
Kenneth Russelleb60cbd22017-12-05 07:54:281008 def get_test_generator_map(self):
1009 return {
Bo Liu555a0f92019-03-29 12:11:561010 'android_webview_gpu_telemetry_tests':
1011 GPUTelemetryTestGenerator(self, is_android_webview=True),
Bo Liu555a0f92019-03-29 12:11:561012 'gpu_telemetry_tests':
1013 GPUTelemetryTestGenerator(self),
1014 'gtest_tests':
1015 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561016 'isolated_scripts':
1017 IsolatedScriptTestGenerator(self),
1018 'junit_tests':
1019 JUnitGenerator(self),
1020 'scripts':
1021 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:521022 'skylab_tests':
1023 SkylabGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281024 }
1025
Kenneth Russell8a386d42018-06-02 09:48:011026 def get_test_type_remapper(self):
1027 return {
1028 # These are a specialization of isolated_scripts with a bunch of
1029 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:561030 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:011031 'gpu_telemetry_tests': 'isolated_scripts',
1032 }
1033
Jeff Yoon67c3e832020-02-08 07:39:381034 def check_composition_type_test_suites(self, test_type,
1035 additional_validators=None):
1036 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1037 validators = [check_compound_references,
1038 check_basic_references,
1039 check_conflicting_definitions]
1040 if additional_validators:
1041 validators += additional_validators
1042
1043 target_suites = self.test_suites.get(test_type, {})
1044 other_test_type = ('compound_suites'
1045 if test_type == 'matrix_compound_suites'
1046 else 'matrix_compound_suites')
1047 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011048 basic_suites = self.test_suites.get('basic_suites', {})
1049
Jamie Madillcf4f8c72021-05-20 19:24:231050 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011051 if suite in basic_suites:
1052 raise BBGenErr('%s names may not duplicate basic test suite names '
1053 '(error found while processsing %s)'
1054 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011055
Jeff Yoon67c3e832020-02-08 07:39:381056 seen_tests = {}
1057 for sub_suite in suite_def:
1058 for validator in validators:
1059 validator(
1060 basic_suites=basic_suites,
1061 other_test_suites=other_suites,
1062 seen_tests=seen_tests,
1063 sub_suite=sub_suite,
1064 suite=suite,
1065 suite_def=suite_def,
1066 target_test_suites=target_suites,
1067 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051068 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381069 )
Kenneth Russelleb60cbd22017-12-05 07:54:281070
Stephen Martinis54d64ad2018-09-21 22:16:201071 def flatten_test_suites(self):
1072 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011073 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1074 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231075 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011076 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201077 self.test_suites = new_test_suites
1078
Chan Lia3ad1502020-04-28 05:32:111079 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231080 for suite in self.test_suites['basic_suites'].values():
1081 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561082 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411083
1084 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
John Palmera8515fca2021-05-20 03:35:321085 # https://source.chromium.org/chromium/chromium/tools/build/+/main:scripts/slave/recipe_modules/chromium_tests/generators.py;l=89;drc=14c062ba0eb418d3c4623dde41a753241b9df06b
Nodir Turakulovfce34292019-12-18 17:05:411086 # TODO(crbug.com/1035124): clean this up.
1087 isolate_name = test.get('test') or test.get('isolate_name') or key
1088 gn_entry = self.gn_isolate_map.get(isolate_name)
1089 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281090 label = gn_entry['label']
1091
1092 if label.count(':') != 1:
1093 raise BBGenErr(
1094 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1095 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1096 (label, isolate_name))
1097 if label.split(':')[1] != isolate_name:
1098 raise BBGenErr(
1099 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1100 ' label "%s" see http://crbug.com/1071091 for details.' %
1101 (isolate_name, label))
1102
Chan Lia3ad1502020-04-28 05:32:111103 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411104 else: # pragma: no cover
1105 # Some tests do not have an entry gn_isolate_map.pyl, such as
1106 # telemetry tests.
1107 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
1108 pass
1109
Kenneth Russelleb60cbd22017-12-05 07:54:281110 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011111 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201112
Jeff Yoon8154e582019-12-03 23:30:011113 compound_suites = self.test_suites.get('compound_suites', {})
1114 # check_composition_type_test_suites() checks that all basic suites
1115 # referenced by compound suites exist.
1116 basic_suites = self.test_suites.get('basic_suites')
1117
Jamie Madillcf4f8c72021-05-20 19:24:231118 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011119 # Resolve this to a dictionary.
1120 full_suite = {}
1121 for entry in value:
1122 suite = basic_suites[entry]
1123 full_suite.update(suite)
1124 compound_suites[name] = full_suite
1125
Jeff Yoon85fb8df2020-08-20 16:47:431126 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381127 """ Merge variant-defined configurations to each test case definition in a
1128 test suite.
1129
1130 The output maps a unique test name to an array of configurations because
1131 there may exist more than one definition for a test name using variants. The
1132 test name is referenced while mapping machines to test suites, so unpacking
1133 the array is done by the generators.
1134
1135 Args:
1136 basic_test_definition: a {} defined test suite in the format
1137 test_name:test_config
1138 variants: an [] of {} defining configurations to be applied to each test
1139 case in the basic test_definition
1140
1141 Return:
1142 a {} of test_name:[{}], where each {} is a merged configuration
1143 """
1144
1145 # Each test in a basic test suite will have a definition per variant.
1146 test_suite = {}
Jamie Madillcf4f8c72021-05-20 19:24:231147 for test_name, test_config in basic_test_definition.items():
Jeff Yoon67c3e832020-02-08 07:39:381148 definitions = []
1149 for variant in variants:
Jeff Yoonda581c32020-03-06 03:56:051150 # Unpack the variant from variants.pyl if it's string based.
1151 if isinstance(variant, str):
1152 variant = self.variants[variant]
1153
Jieting Yangef6b1042021-11-30 21:33:481154 # If 'enabled' is set to False, we will not use this variant;
1155 # otherwise if the variant doesn't include 'enabled' variable or
1156 # 'enabled' is set to True, we will use this variant
1157 if not variant.get('enabled', True):
1158 continue
Jeff Yoon67c3e832020-02-08 07:39:381159 # Clone a copy of test_config so that we can have a uniquely updated
1160 # version of it per variant
1161 cloned_config = copy.deepcopy(test_config)
1162 # The variant definition needs to be re-used for each test, so we'll
1163 # create a clone and work with it as well.
1164 cloned_variant = copy.deepcopy(variant)
1165
1166 cloned_config['args'] = (cloned_config.get('args', []) +
1167 cloned_variant.get('args', []))
1168 cloned_config['mixins'] = (cloned_config.get('mixins', []) +
Jeff Yoon85fb8df2020-08-20 16:47:431169 cloned_variant.get('mixins', []) + mixins)
Jeff Yoon67c3e832020-02-08 07:39:381170
1171 basic_swarming_def = cloned_config.get('swarming', {})
1172 variant_swarming_def = cloned_variant.get('swarming', {})
1173 if basic_swarming_def and variant_swarming_def:
1174 if ('dimension_sets' in basic_swarming_def and
1175 'dimension_sets' in variant_swarming_def):
1176 # Retain swarming dimension set merge behavior when both variant and
1177 # the basic test configuration both define it
1178 self.dictionary_merge(basic_swarming_def, variant_swarming_def)
1179 # Remove dimension_sets from the variant definition, so that it does
1180 # not replace what's been done by dictionary_merge in the update
1181 # call below.
1182 del variant_swarming_def['dimension_sets']
1183
1184 # Update the swarming definition with whatever is defined for swarming
1185 # by the variant.
1186 basic_swarming_def.update(variant_swarming_def)
1187 cloned_config['swarming'] = basic_swarming_def
1188
Xinan Lin05fb9c1752020-12-17 00:15:521189 # Copy all skylab fields defined by the variant.
1190 skylab_config = cloned_variant.get('skylab')
1191 if skylab_config:
1192 for k, v in skylab_config.items():
Jieting Yangef6b1042021-11-30 21:33:481193 # cros_chrome_version is the ash chrome version in the cros img
1194 # in the variant of cros_board. We don't want to include it in
1195 # the final json files; so remove it.
1196 if k == 'cros_chrome_version':
1197 continue
Xinan Lin05fb9c1752020-12-17 00:15:521198 cloned_config[k] = v
1199
Jeff Yoon67c3e832020-02-08 07:39:381200 # The identifier is used to make the name of the test unique.
1201 # Generators in the recipe uniquely identify a test by it's name, so we
1202 # don't want to have the same name for each variant.
1203 cloned_config['name'] = '{}_{}'.format(test_name,
1204 cloned_variant['identifier'])
Jeff Yoon67c3e832020-02-08 07:39:381205 definitions.append(cloned_config)
1206 test_suite[test_name] = definitions
1207 return test_suite
1208
Jeff Yoon8154e582019-12-03 23:30:011209 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381210 self.check_composition_type_test_suites('matrix_compound_suites',
1211 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011212
1213 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381214 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011215 # referenced by matrix suites exist.
1216 basic_suites = self.test_suites.get('basic_suites')
1217
Jamie Madillcf4f8c72021-05-20 19:24:231218 for test_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011219 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381220
Jamie Madillcf4f8c72021-05-20 19:24:231221 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381222 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1223
1224 if 'variants' in mtx_test_suite_config:
Jeff Yoon85fb8df2020-08-20 16:47:431225 mixins = mtx_test_suite_config.get('mixins', [])
Jeff Yoon67c3e832020-02-08 07:39:381226 result = self.resolve_variants(basic_test_def,
Jeff Yoon85fb8df2020-08-20 16:47:431227 mtx_test_suite_config['variants'],
1228 mixins)
Jeff Yoon67c3e832020-02-08 07:39:381229 full_suite.update(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271230 else:
1231 suite = basic_suites[test_suite]
1232 full_suite.update(suite)
Jeff Yoon67c3e832020-02-08 07:39:381233 matrix_compound_suites[test_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281234
1235 def link_waterfalls_to_test_suites(self):
1236 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231237 for tester_name, tester in waterfall['machines'].items():
1238 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281239 if not value in self.test_suites:
1240 # Hard / impossible to cover this in the unit test.
1241 raise self.unknown_test_suite(
1242 value, tester_name, waterfall['name']) # pragma: no cover
1243 tester['test_suites'][suite] = self.test_suites[value]
1244
1245 def load_configuration_files(self):
1246 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
1247 self.test_suites = self.load_pyl_file('test_suites.pyl')
1248 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:011249 self.mixins = self.load_pyl_file('mixins.pyl')
Nodir Turakulovfce34292019-12-18 17:05:411250 self.gn_isolate_map = self.load_pyl_file('gn_isolate_map.pyl')
Chong Guee622242020-10-28 18:17:351251 for isolate_map in self.args.isolate_map_files:
1252 isolate_map = self.load_pyl_file(isolate_map)
1253 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1254 if duplicates:
1255 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1256 ', '.join(duplicates))
1257 self.gn_isolate_map.update(isolate_map)
1258
Jeff Yoonda581c32020-03-06 03:56:051259 self.variants = self.load_pyl_file('variants.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:281260
1261 def resolve_configuration_files(self):
Chan Lia3ad1502020-04-28 05:32:111262 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281263 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011264 self.resolve_matrix_compound_test_suites()
1265 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281266 self.link_waterfalls_to_test_suites()
1267
Nico Weberd18b8962018-05-16 19:39:381268 def unknown_bot(self, bot_name, waterfall_name):
1269 return BBGenErr(
1270 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1271
Kenneth Russelleb60cbd22017-12-05 07:54:281272 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1273 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381274 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281275 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1276
1277 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1278 return BBGenErr(
1279 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1280 ' on waterfall ' + waterfall_name)
1281
Stephen Martinisb72f6d22018-10-04 23:29:011282 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071283 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321284
1285 Checks in the waterfall, builder, and test objects for mixins.
1286 """
1287 def valid_mixin(mixin_name):
1288 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011289 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321290 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381291
Stephen Martinisb6a50492018-09-12 23:59:321292 def must_be_list(mixins, typ, name):
1293 """Asserts that given mixins are a list."""
1294 if not isinstance(mixins, list):
1295 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1296
Brian Sheedy7658c982020-01-08 02:27:581297 test_name = test.get('name')
1298 remove_mixins = set()
1299 if 'remove_mixins' in builder:
1300 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1301 for rm in builder['remove_mixins']:
1302 valid_mixin(rm)
1303 remove_mixins.add(rm)
1304 if 'remove_mixins' in test:
1305 must_be_list(test['remove_mixins'], 'test', test_name)
1306 for rm in test['remove_mixins']:
1307 valid_mixin(rm)
1308 remove_mixins.add(rm)
1309 del test['remove_mixins']
1310
Stephen Martinisb72f6d22018-10-04 23:29:011311 if 'mixins' in waterfall:
1312 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1313 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581314 if mixin in remove_mixins:
1315 continue
Stephen Martinisb6a50492018-09-12 23:59:321316 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011317 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321318
Stephen Martinisb72f6d22018-10-04 23:29:011319 if 'mixins' in builder:
1320 must_be_list(builder['mixins'], 'builder', builder_name)
1321 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581322 if mixin in remove_mixins:
1323 continue
Stephen Martinisb6a50492018-09-12 23:59:321324 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011325 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321326
Stephen Martinisb72f6d22018-10-04 23:29:011327 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071328 return test
1329
Stephen Martinis2a0667022018-09-25 22:31:141330 if not test_name:
1331 test_name = test.get('test')
1332 if not test_name: # pragma: no cover
1333 # Not the best name, but we should say something.
1334 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:011335 must_be_list(test['mixins'], 'test', test_name)
1336 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581337 # We don't bother checking if the given mixin is in remove_mixins here
1338 # since this is already the lowest level, so if a mixin is added here that
1339 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071340 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011341 test = self.apply_mixin(self.mixins[mixin], test)
Jeff Yoon67c3e832020-02-08 07:39:381342 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071343 return test
Stephen Martinisb6a50492018-09-12 23:59:321344
Stephen Martinisb72f6d22018-10-04 23:29:011345 def apply_mixin(self, mixin, test):
1346 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321347
Stephen Martinis0382bc12018-09-17 22:29:071348 Mixins will not override an existing key. This is to ensure exceptions can
1349 override a setting a mixin applies.
1350
Stephen Martinisb72f6d22018-10-04 23:29:011351 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:321352 'dimension_sets', which is how normal test suites specify their dimensions,
1353 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
1354 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011355
Stephen Martinisb6a50492018-09-12 23:59:321356 """
1357 new_test = copy.deepcopy(test)
1358 mixin = copy.deepcopy(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011359 if 'swarming' in mixin:
1360 swarming_mixin = mixin['swarming']
1361 new_test.setdefault('swarming', {})
Brian Sheedycae63b22020-06-10 22:52:111362 # Copy over any explicit dimension sets first so that they will be updated
1363 # by any subsequent 'dimensions' entries.
1364 if 'dimension_sets' in swarming_mixin:
1365 existing_dimension_sets = new_test['swarming'].setdefault(
1366 'dimension_sets', [])
1367 # Appending to the existing list could potentially result in different
1368 # behavior depending on the order the mixins were applied, but that's
1369 # already the case for other parts of mixins, so trust that the user
1370 # will verify that the generated output is correct before submitting.
1371 for dimension_set in swarming_mixin['dimension_sets']:
1372 if dimension_set not in existing_dimension_sets:
1373 existing_dimension_sets.append(dimension_set)
1374 del swarming_mixin['dimension_sets']
Stephen Martinisb72f6d22018-10-04 23:29:011375 if 'dimensions' in swarming_mixin:
1376 new_test['swarming'].setdefault('dimension_sets', [{}])
1377 for dimension_set in new_test['swarming']['dimension_sets']:
1378 dimension_set.update(swarming_mixin['dimensions'])
1379 del swarming_mixin['dimensions']
Stephen Martinisb72f6d22018-10-04 23:29:011380 # python dict update doesn't do recursion at all. Just hard code the
1381 # nested update we need (mixin['swarming'] shouldn't clobber
1382 # test['swarming'], but should update it).
1383 new_test['swarming'].update(swarming_mixin)
1384 del mixin['swarming']
1385
Wezc0e835b702018-10-30 00:38:411386 if '$mixin_append' in mixin:
1387 # Values specified under $mixin_append should be appended to existing
1388 # lists, rather than replacing them.
1389 mixin_append = mixin['$mixin_append']
Zhaoyang Li473dd0ae2021-05-10 18:28:281390
1391 # Append swarming named cache and delete swarming key, since it's under
1392 # another layer of dict.
1393 if 'named_caches' in mixin_append.get('swarming', {}):
1394 new_test['swarming'].setdefault('named_caches', [])
1395 new_test['swarming']['named_caches'].extend(
1396 mixin_append['swarming']['named_caches'])
1397 if len(mixin_append['swarming']) > 1:
1398 raise BBGenErr('Only named_caches is supported under swarming key in '
1399 '$mixin_append, but there are: %s' %
1400 sorted(mixin_append['swarming'].keys()))
1401 del mixin_append['swarming']
Wezc0e835b702018-10-30 00:38:411402 for key in mixin_append:
1403 new_test.setdefault(key, [])
1404 if not isinstance(mixin_append[key], list):
1405 raise BBGenErr(
1406 'Key "' + key + '" in $mixin_append must be a list.')
1407 if not isinstance(new_test[key], list):
1408 raise BBGenErr(
1409 'Cannot apply $mixin_append to non-list "' + key + '".')
1410 new_test[key].extend(mixin_append[key])
1411 if 'args' in mixin_append:
1412 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1413 del mixin['$mixin_append']
1414
Stephen Martinisb72f6d22018-10-04 23:29:011415 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321416 return new_test
1417
Greg Gutermanf60eb052020-03-12 17:40:011418 def generate_output_tests(self, waterfall):
1419 """Generates the tests for a waterfall.
1420
1421 Args:
1422 waterfall: a dictionary parsed from a master pyl file
1423 Returns:
1424 A dictionary mapping builders to test specs
1425 """
1426 return {
Jamie Madillcf4f8c72021-05-20 19:24:231427 name: self.get_tests_for_config(waterfall, name, config)
1428 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011429 }
1430
1431 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531432 generator_map = self.get_test_generator_map()
1433 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281434
Greg Gutermanf60eb052020-03-12 17:40:011435 tests = {}
1436 # Copy only well-understood entries in the machine's configuration
1437 # verbatim into the generated JSON.
1438 if 'additional_compile_targets' in config:
1439 tests['additional_compile_targets'] = config[
1440 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231441 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011442 if test_type not in generator_map:
1443 raise self.unknown_test_suite_type(
1444 test_type, name, waterfall['name']) # pragma: no cover
1445 test_generator = generator_map[test_type]
1446 # Let multiple kinds of generators generate the same kinds
1447 # of tests. For example, gpu_telemetry_tests are a
1448 # specialization of isolated_scripts.
1449 new_tests = test_generator.generate(
1450 waterfall, name, config, input_tests)
1451 remapped_test_type = test_type_remapper.get(test_type, test_type)
1452 tests[remapped_test_type] = test_generator.sort(
1453 tests.get(remapped_test_type, []) + new_tests)
1454
1455 return tests
1456
1457 def jsonify(self, all_tests):
1458 return json.dumps(
1459 all_tests, indent=2, separators=(',', ': '),
1460 sort_keys=True) + '\n'
1461
1462 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281463 self.load_configuration_files()
1464 self.resolve_configuration_files()
1465 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011466 result = collections.defaultdict(dict)
1467
Dirk Pranke6269d302020-10-01 00:14:391468 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011469 for waterfall in self.waterfalls:
1470 for field in required_fields:
1471 # Verify required fields
1472 if field not in waterfall:
1473 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1474
1475 # Handle filter flag, if specified
1476 if filters and waterfall['name'] not in filters:
1477 continue
1478
1479 # Join config files and hardcoded values together
1480 all_tests = self.generate_output_tests(waterfall)
1481 result[waterfall['name']] = all_tests
1482
Greg Gutermanf60eb052020-03-12 17:40:011483 # Add do not edit warning
1484 for tests in result.values():
1485 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1486 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1487
1488 return result
1489
1490 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281491 suffix = '.json'
1492 if self.args.new_files:
1493 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011494
1495 for filename, contents in result.items():
1496 jsonstr = self.jsonify(contents)
1497 self.write_file(self.pyl_file_path(filename + suffix), jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281498
Nico Weberd18b8962018-05-16 19:39:381499 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161500 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421501 # NOTE: This reference can cause issues; if a file changes there, the
1502 # presubmit here won't be run by default. A manually maintained list there
1503 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1504 # references to configs outside of this directory are added, please change
1505 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1506 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491507
Garrett Beaty7e866fc2021-06-16 14:12:101508 # Get the generated project.pyl so we can check if we should be enforcing
1509 # that the specs are for builders that actually exist
1510 # If not, return None to indicate that we won't enforce that builders in
1511 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491512 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1513 'project.pyl')
1514 if os.path.exists(project_pyl_path):
1515 settings = ast.literal_eval(self.read_file(project_pyl_path))
1516 if not settings.get('validate_source_side_specs_have_builder', True):
1517 return None
1518
Nico Weberd18b8962018-05-16 19:39:381519 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311520 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161521 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1522 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431523 for c in milo_configs:
1524 for l in self.read_file(c).splitlines():
1525 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311526 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431527 continue
1528 # l looks like
1529 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1530 # Extract win_chromium_dbg_ng part.
1531 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381532 return bot_names
1533
Ben Pastene9a010082019-09-25 20:41:371534 def get_builders_that_do_not_actually_exist(self):
Kenneth Russell8a386d42018-06-02 09:48:011535 # Some of the bots on the chromium.gpu.fyi waterfall in particular
1536 # are defined only to be mirrored into trybots, and don't actually
1537 # exist on any of the waterfalls or consoles.
1538 return [
Yuke Liao8373de52020-08-14 18:30:541539 'GPU FYI Fuchsia Builder',
1540 'ANGLE GPU Android Release (Nexus 5X)',
1541 'ANGLE GPU Linux Release (Intel HD 630)',
1542 'ANGLE GPU Linux Release (NVIDIA)',
Yuke Liao8373de52020-08-14 18:30:541543 'Optional Android Release (Nexus 5X)',
Brian Sheedy9584d812021-05-26 02:07:251544 'Optional Android Release (Pixel 4)',
Yuke Liao8373de52020-08-14 18:30:541545 'Optional Linux Release (Intel HD 630)',
1546 'Optional Linux Release (NVIDIA)',
1547 'Optional Mac Release (Intel)',
1548 'Optional Mac Retina Release (AMD)',
1549 'Optional Mac Retina Release (NVIDIA)',
1550 'Optional Win10 x64 Release (Intel HD 630)',
1551 'Optional Win10 x64 Release (NVIDIA)',
Yuke Liao8373de52020-08-14 18:30:541552 # chromium.fyi
1553 'linux-blink-rel-dummy',
1554 'linux-blink-optional-highdpi-rel-dummy',
1555 'mac10.12-blink-rel-dummy',
1556 'mac10.13-blink-rel-dummy',
1557 'mac10.14-blink-rel-dummy',
1558 'mac10.15-blink-rel-dummy',
Stephanie Kim7fbfd912020-08-21 21:11:001559 'mac11.0-blink-rel-dummy',
Preethi Mohan9c0fa2992021-08-17 17:25:451560 'mac11.0.arm64-blink-rel-dummy',
Yuke Liao8373de52020-08-14 18:30:541561 'win7-blink-rel-dummy',
Preethi Mohan47d03dc2021-06-28 23:08:021562 'win10.20h2-blink-rel-dummy',
Yuke Liao8373de52020-08-14 18:30:541563 'WebKit Linux layout_ng_disabled Builder',
1564 # chromium, due to https://crbug.com/878915
1565 'win-dbg',
1566 'win32-dbg',
1567 'win-archive-dbg',
1568 'win32-archive-dbg',
Stephanie Kim107c1b0e2020-11-18 21:49:411569 # TODO crbug.com/1143924: Remove once experimentation is complete
1570 'Linux Builder Robocrop',
1571 'Linux Tests Robocrop',
Kenneth Russell8a386d42018-06-02 09:48:011572 ]
1573
Ben Pastene9a010082019-09-25 20:41:371574 def get_internal_waterfalls(self):
1575 # Similar to get_builders_that_do_not_actually_exist above, but for
1576 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201577 return [
1578 'chrome', 'chrome.pgo', 'internal.chrome.fyi', 'internal.chromeos.fyi',
1579 'internal.soda'
1580 ]
Ben Pastene9a010082019-09-25 20:41:371581
Stephen Martinisf83893722018-09-19 00:02:181582 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201583 self.check_input_files_sorting(verbose)
1584
Kenneth Russelleb60cbd22017-12-05 07:54:281585 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011586 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381587 self.check_composition_type_test_suites('matrix_compound_suites',
1588 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111589 self.resolve_test_id_prefixes()
Stephen Martinis54d64ad2018-09-21 22:16:201590 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381591
1592 # All bots should exist.
1593 bot_names = self.get_valid_bot_names()
Ben Pastene9a010082019-09-25 20:41:371594 builders_that_dont_exist = self.get_builders_that_do_not_actually_exist()
Garrett Beaty2a02de3c2020-05-15 13:57:351595 if bot_names is not None:
1596 internal_waterfalls = self.get_internal_waterfalls()
1597 for waterfall in self.waterfalls:
1598 # TODO(crbug.com/991417): Remove the need for this exception.
1599 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011600 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351601 for bot_name in waterfall['machines']:
1602 if bot_name in builders_that_dont_exist:
Kenneth Russell78fd8702018-05-17 01:15:521603 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351604 if bot_name not in bot_names:
1605 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
1606 # TODO(thakis): Remove this once these bots move to luci.
1607 continue # pragma: no cover
1608 if waterfall['name'] in ['tryserver.webrtc',
1609 'webrtc.chromium.fyi.experimental']:
1610 # These waterfalls have their bot configs in a different repo.
1611 # so we don't know about their bot names.
1612 continue # pragma: no cover
1613 if waterfall['name'] in ['client.devtools-frontend.integration',
1614 'tryserver.devtools-frontend',
1615 'chromium.devtools-frontend']:
1616 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201617 if waterfall['name'] in ['client.openscreen.chromium']:
1618 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351619 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381620
Kenneth Russelleb60cbd22017-12-05 07:54:281621 # All test suites must be referenced.
1622 suites_seen = set()
1623 generator_map = self.get_test_generator_map()
1624 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231625 for bot_name, tester in waterfall['machines'].items():
1626 for suite_type, suite in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281627 if suite_type not in generator_map:
1628 raise self.unknown_test_suite_type(suite_type, bot_name,
1629 waterfall['name'])
1630 if suite not in self.test_suites:
1631 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1632 suites_seen.add(suite)
1633 # Since we didn't resolve the configuration files, this set
1634 # includes both composition test suites and regular ones.
1635 resolved_suites = set()
1636 for suite_name in suites_seen:
1637 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011638 for sub_suite in suite:
1639 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281640 resolved_suites.add(suite_name)
1641 # At this point, every key in test_suites.pyl should be referenced.
1642 missing_suites = set(self.test_suites.keys()) - resolved_suites
1643 if missing_suites:
1644 raise BBGenErr('The following test suites were unreferenced by bots on '
1645 'the waterfalls: ' + str(missing_suites))
1646
1647 # All test suite exceptions must refer to bots on the waterfall.
1648 all_bots = set()
1649 missing_bots = set()
1650 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231651 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281652 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281653 # In order to disambiguate between bots with the same name on
1654 # different waterfalls, support has been added to various
1655 # exceptions for concatenating the waterfall name after the bot
1656 # name.
1657 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231658 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381659 removals = (exception.get('remove_from', []) +
1660 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231661 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381662 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281663 if removal not in all_bots:
1664 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411665
Ben Pastene9a010082019-09-25 20:41:371666 missing_bots = missing_bots - set(builders_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281667 if missing_bots:
1668 raise BBGenErr('The following nonexistent machines were referenced in '
1669 'the test suite exceptions: ' + str(missing_bots))
1670
Stephen Martinis0382bc12018-09-17 22:29:071671 # All mixins must be referenced
1672 seen_mixins = set()
1673 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011674 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231675 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011676 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071677 for suite in self.test_suites.values():
1678 if isinstance(suite, list):
1679 # Don't care about this, it's a composition, which shouldn't include a
1680 # swarming mixin.
1681 continue
1682
1683 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561684 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011685 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071686
Zhaoyang Li9da047d52021-05-10 21:31:441687 for variant in self.variants:
1688 # Unpack the variant from variants.pyl if it's string based.
1689 if isinstance(variant, str):
1690 variant = self.variants[variant]
1691 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1692
Stephen Martinisb72f6d22018-10-04 23:29:011693 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071694 if missing_mixins:
1695 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1696 ' referenced in a waterfall, machine, or test suite.' % (
1697 str(missing_mixins)))
1698
Jeff Yoonda581c32020-03-06 03:56:051699 # All variant references must be referenced
1700 seen_variants = set()
1701 for suite in self.test_suites.values():
1702 if isinstance(suite, list):
1703 continue
1704
1705 for test in suite.values():
1706 if isinstance(test, dict):
1707 for variant in test.get('variants', []):
1708 if isinstance(variant, str):
1709 seen_variants.add(variant)
1710
1711 missing_variants = set(self.variants.keys()) - seen_variants
1712 if missing_variants:
1713 raise BBGenErr('The following variants were unreferenced: %s. They must '
1714 'be referenced in a matrix test suite under the variants '
1715 'key.' % str(missing_variants))
1716
Stephen Martinis54d64ad2018-09-21 22:16:201717
1718 def type_assert(self, node, typ, filename, verbose=False):
1719 """Asserts that the Python AST node |node| is of type |typ|.
1720
1721 If verbose is set, it prints out some helpful context lines, showing where
1722 exactly the error occurred in the file.
1723 """
1724 if not isinstance(node, typ):
1725 if verbose:
1726 lines = [""] + self.read_file(filename).splitlines()
1727
1728 context = 2
1729 lines_start = max(node.lineno - context, 0)
1730 # Add one to include the last line
1731 lines_end = min(node.lineno + context, len(lines)) + 1
1732 lines = (
1733 ['== %s ==\n' % filename] +
1734 ["<snip>\n"] +
1735 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1736 lines[lines_start:lines_start + context])] +
1737 ['-' * 80 + '\n'] +
1738 ['%d %s' % (node.lineno, lines[node.lineno])] +
1739 ['-' * (node.col_offset + 3) + '^' + '-' * (
1740 80 - node.col_offset - 4) + '\n'] +
1741 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1742 lines[node.lineno + 1:lines_end])] +
1743 ["<snip>\n"]
1744 )
1745 # Print out a useful message when a type assertion fails.
1746 for l in lines:
1747 self.print_line(l.strip())
1748
1749 node_dumped = ast.dump(node, annotate_fields=False)
1750 # If the node is huge, truncate it so everything fits in a terminal
1751 # window.
1752 if len(node_dumped) > 60: # pragma: no cover
1753 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1754 raise BBGenErr(
1755 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1756 ' be %s, is %s' % (
1757 filename, node_dumped,
1758 node.lineno, typ, type(node)))
1759
Stephen Martinis5bef0fc2020-01-06 22:47:531760 def check_ast_list_formatted(self, keys, filename, verbose,
Stephen Martinis1384ff92020-01-07 19:52:151761 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531762 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201763
Stephen Martinis5bef0fc2020-01-06 22:47:531764 Currently only checks to ensure they're correctly sorted, and that there
1765 are no duplicates.
1766
1767 Args:
1768 keys: An python list of AST nodes.
1769
1770 It's a list of AST nodes instead of a list of strings because
1771 when verbose is set, it tries to print out context of where the
1772 diffs are in the file.
1773 filename: The name of the file this node is from.
1774 verbose: If set, print out diff information about how the keys are
1775 incorrectly formatted.
1776 check_sorting: If true, checks if the list is sorted.
1777 Returns:
1778 If the keys are correctly formatted.
1779 """
1780 if not keys:
1781 return True
1782
1783 assert isinstance(keys[0], ast.Str)
1784
1785 keys_strs = [k.s for k in keys]
1786 # Keys to diff against. Used below.
1787 keys_to_diff_against = None
1788 # If the list is properly formatted.
1789 list_formatted = True
1790
1791 # Duplicates are always bad.
1792 if len(set(keys_strs)) != len(keys_strs):
1793 list_formatted = False
1794 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1795
1796 if check_sorting and sorted(keys_strs) != keys_strs:
1797 list_formatted = False
1798 if list_formatted:
1799 return True
1800
1801 if verbose:
1802 line_num = keys[0].lineno
1803 keys = [k.s for k in keys]
1804 if check_sorting:
1805 # If we have duplicates, sorting this will take care of it anyways.
1806 keys_to_diff_against = sorted(set(keys))
1807 # else, keys_to_diff_against is set above already
1808
1809 self.print_line('=' * 80)
1810 self.print_line('(First line of keys is %s)' % line_num)
1811 for line in difflib.context_diff(
1812 keys, keys_to_diff_against,
1813 fromfile='current (%r)' % filename, tofile='sorted', lineterm=''):
1814 self.print_line(line)
1815 self.print_line('=' * 80)
1816
1817 return False
1818
Stephen Martinis1384ff92020-01-07 19:52:151819 def check_ast_dict_formatted(self, node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531820 """Checks if an ast dictionary's keys are correctly formatted.
1821
1822 Just a simple wrapper around check_ast_list_formatted.
1823 Args:
1824 node: An AST node. Assumed to be a dictionary.
1825 filename: The name of the file this node is from.
1826 verbose: If set, print out diff information about how the keys are
1827 incorrectly formatted.
1828 check_sorting: If true, checks if the list is sorted.
1829 Returns:
1830 If the dictionary is correctly formatted.
1831 """
Stephen Martinis54d64ad2018-09-21 22:16:201832 keys = []
1833 # The keys of this dict are ordered as ordered in the file; normal python
1834 # dictionary keys are given an arbitrary order, but since we parsed the
1835 # file itself, the order as given in the file is preserved.
1836 for key in node.keys:
1837 self.type_assert(key, ast.Str, filename, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531838 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201839
Stephen Martinis1384ff92020-01-07 19:52:151840 return self.check_ast_list_formatted(keys, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181841
1842 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201843 # TODO(https://crbug.com/886993): Add the ability for this script to
1844 # actually format the files, rather than just complain if they're
1845 # incorrectly formatted.
1846 bad_files = set()
Stephen Martinis5bef0fc2020-01-06 22:47:531847 def parse_file(filename):
1848 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201849
Stephen Martinis5bef0fc2020-01-06 22:47:531850 Returns an AST node representing the value in the pyl file."""
Stephen Martinisf83893722018-09-19 00:02:181851 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1852
Stephen Martinisf83893722018-09-19 00:02:181853 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201854 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181855 module = parsed.body
1856
1857 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201858 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181859 if len(module) != 1: # pragma: no cover
1860 raise BBGenErr('Invalid .pyl file %s' % filename)
1861 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201862 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181863
Stephen Martinis5bef0fc2020-01-06 22:47:531864 return expr.value
1865
1866 # Handle this separately
1867 filename = 'waterfalls.pyl'
1868 value = parse_file(filename)
1869 # Value should be a list.
1870 self.type_assert(value, ast.List, filename, verbose)
1871
1872 keys = []
1873 for val in value.elts:
1874 self.type_assert(val, ast.Dict, filename, verbose)
1875 waterfall_name = None
1876 for key, val in zip(val.keys, val.values):
1877 self.type_assert(key, ast.Str, filename, verbose)
1878 if key.s == 'machines':
1879 if not self.check_ast_dict_formatted(val, filename, verbose):
1880 bad_files.add(filename)
1881
1882 if key.s == "name":
1883 self.type_assert(val, ast.Str, filename, verbose)
1884 waterfall_name = val
1885 assert waterfall_name
1886 keys.append(waterfall_name)
1887
Stephen Martinis1384ff92020-01-07 19:52:151888 if not self.check_ast_list_formatted(keys, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531889 bad_files.add(filename)
1890
1891 for filename in (
1892 'mixins.pyl',
1893 'test_suites.pyl',
1894 'test_suite_exceptions.pyl',
1895 ):
1896 value = parse_file(filename)
Stephen Martinisf83893722018-09-19 00:02:181897 # Value should be a dictionary.
Stephen Martinis54d64ad2018-09-21 22:16:201898 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181899
Stephen Martinis5bef0fc2020-01-06 22:47:531900 if not self.check_ast_dict_formatted(
1901 value, filename, verbose):
1902 bad_files.add(filename)
1903
Stephen Martinis54d64ad2018-09-21 22:16:201904 if filename == 'test_suites.pyl':
Jeff Yoon8154e582019-12-03 23:30:011905 expected_keys = ['basic_suites',
1906 'compound_suites',
1907 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201908 actual_keys = [node.s for node in value.keys]
1909 assert all(key in expected_keys for key in actual_keys), (
1910 'Invalid %r file; expected keys %r, got %r' % (
1911 filename, expected_keys, actual_keys))
1912 suite_dicts = [node for node in value.values]
1913 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011914 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201915 for suite_group in suite_dicts:
Stephen Martinis5bef0fc2020-01-06 22:47:531916 if not self.check_ast_dict_formatted(
Stephen Martinis54d64ad2018-09-21 22:16:201917 suite_group, filename, verbose):
1918 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181919
Stephen Martinis5bef0fc2020-01-06 22:47:531920 for key, suite in zip(value.keys, value.values):
1921 # The compound suites are checked in
1922 # 'check_composition_type_test_suites()'
1923 if key.s == 'basic_suites':
1924 for group in suite.values:
Stephen Martinis1384ff92020-01-07 19:52:151925 if not self.check_ast_dict_formatted(group, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531926 bad_files.add(filename)
1927 break
Stephen Martinis54d64ad2018-09-21 22:16:201928
Stephen Martinis5bef0fc2020-01-06 22:47:531929 elif filename == 'test_suite_exceptions.pyl':
1930 # Check the values for each test.
1931 for test in value.values:
1932 for kind, node in zip(test.keys, test.values):
1933 if isinstance(node, ast.Dict):
Stephen Martinis1384ff92020-01-07 19:52:151934 if not self.check_ast_dict_formatted(node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531935 bad_files.add(filename)
1936 elif kind.s == 'remove_from':
1937 # Don't care about sorting; these are usually grouped, since the
1938 # same bug can affect multiple builders. Do want to make sure
1939 # there aren't duplicates.
1940 if not self.check_ast_list_formatted(node.elts, filename, verbose,
1941 check_sorting=False):
1942 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181943
1944 if bad_files:
1945 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201946 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531947 'unsorted, or have duplicates. Re-run this with --verbose to see '
1948 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181949
Kenneth Russelleb60cbd22017-12-05 07:54:281950 def check_output_file_consistency(self, verbose=False):
1951 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011952 # All waterfalls/bucket .json files must have been written
1953 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:281954 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011955 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:161956 outputs = self.generate_outputs()
1957 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:011958 expected = self.jsonify(expected_contents)
1959 file_path = filename + '.json'
Zhiling Huangbe008172018-03-08 19:13:111960 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281961 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:011962 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:321963 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:011964 self.print_line('File ' + filename +
1965 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321966 'contents:')
1967 for line in difflib.unified_diff(
1968 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501969 current.splitlines(),
1970 fromfile='expected', tofile='current'):
1971 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:011972
1973 if ungenerated_files:
1974 raise BBGenErr(
1975 'The following files have not been properly '
1976 'autogenerated by generate_buildbot_json.py: ' +
1977 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:281978
Dirk Pranke772f55f2021-04-28 04:51:161979 for builder_group, builders in outputs.items():
1980 for builder, step_types in builders.items():
1981 for step_data in step_types.get('gtest_tests', []):
1982 step_name = step_data.get('name', step_data['test'])
1983 self._check_swarming_config(builder_group, builder, step_name,
1984 step_data)
1985 for step_data in step_types.get('isolated_scripts', []):
1986 step_name = step_data.get('name', step_data['isolate_name'])
1987 self._check_swarming_config(builder_group, builder, step_name,
1988 step_data)
1989
1990 def _check_swarming_config(self, filename, builder, step_name, step_data):
1991 # TODO(crbug.com/1203436): Ensure all swarming tests specify os and cpu, not
1992 # just mac tests.
1993 if ('mac' in builder.lower()
1994 and step_data['swarming']['can_use_on_swarming_builders']):
1995 dimension_sets = step_data['swarming'].get('dimension_sets')
1996 if not dimension_sets:
1997 raise BBGenErr('%s: %s / %s : os and cpu must be specified for mac '
1998 'swarmed tests' % (filename, builder, step_name))
1999 for s in dimension_sets:
2000 if not s.get('os') or not s.get('cpu'):
2001 raise BBGenErr('%s: %s / %s : os and cpu must be specified for mac '
2002 'swarmed tests' % (filename, builder, step_name))
2003
Kenneth Russelleb60cbd22017-12-05 07:54:282004 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502005 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282006 self.check_output_file_consistency(verbose) # pragma: no cover
2007
Karen Qiane24b7ee2019-02-12 23:37:062008 def does_test_match(self, test_info, params_dict):
2009 """Checks to see if the test matches the parameters given.
2010
2011 Compares the provided test_info with the params_dict to see
2012 if the bot matches the parameters given. If so, returns True.
2013 Else, returns false.
2014
2015 Args:
2016 test_info (dict): Information about a specific bot provided
2017 in the format shown in waterfalls.pyl
2018 params_dict (dict): Dictionary of parameters and their values
2019 to look for in the bot
2020 Ex: {
2021 'device_os':'android',
2022 '--flag':True,
2023 'mixins': ['mixin1', 'mixin2'],
2024 'ex_key':'ex_value'
2025 }
2026
2027 """
2028 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2029 'kvm', 'pool', 'integrity'] # dimension parameters
2030 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2031 'can_use_on_swarming_builders']
2032 for param in params_dict:
2033 # if dimension parameter
2034 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2035 if not 'swarming' in test_info:
2036 return False
2037 swarming = test_info['swarming']
2038 if param in SWARMING_PARAMS:
2039 if not param in swarming:
2040 return False
2041 if not str(swarming[param]) == params_dict[param]:
2042 return False
2043 else:
2044 if not 'dimension_sets' in swarming:
2045 return False
2046 d_set = swarming['dimension_sets']
2047 # only looking at the first dimension set
2048 if not param in d_set[0]:
2049 return False
2050 if not d_set[0][param] == params_dict[param]:
2051 return False
2052
2053 # if flag
2054 elif param.startswith('--'):
2055 if not 'args' in test_info:
2056 return False
2057 if not param in test_info['args']:
2058 return False
2059
2060 # not dimension parameter/flag/mixin
2061 else:
2062 if not param in test_info:
2063 return False
2064 if not test_info[param] == params_dict[param]:
2065 return False
2066 return True
2067 def error_msg(self, msg):
2068 """Prints an error message.
2069
2070 In addition to a catered error message, also prints
2071 out where the user can find more help. Then, program exits.
2072 """
2073 self.print_line(msg + (' If you need more information, ' +
2074 'please run with -h or --help to see valid commands.'))
2075 sys.exit(1)
2076
2077 def find_bots_that_run_test(self, test, bots):
2078 matching_bots = []
2079 for bot in bots:
2080 bot_info = bots[bot]
2081 tests = self.flatten_tests_for_bot(bot_info)
2082 for test_info in tests:
2083 test_name = ""
2084 if 'name' in test_info:
2085 test_name = test_info['name']
2086 elif 'test' in test_info:
2087 test_name = test_info['test']
2088 if not test_name == test:
2089 continue
2090 matching_bots.append(bot)
2091 return matching_bots
2092
2093 def find_tests_with_params(self, tests, params_dict):
2094 matching_tests = []
2095 for test_name in tests:
2096 test_info = tests[test_name]
2097 if not self.does_test_match(test_info, params_dict):
2098 continue
2099 if not test_name in matching_tests:
2100 matching_tests.append(test_name)
2101 return matching_tests
2102
2103 def flatten_waterfalls_for_query(self, waterfalls):
2104 bots = {}
2105 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012106 waterfall_tests = self.generate_output_tests(waterfall)
2107 for bot in waterfall_tests:
2108 bot_info = waterfall_tests[bot]
2109 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062110 return bots
2111
2112 def flatten_tests_for_bot(self, bot_info):
2113 """Returns a list of flattened tests.
2114
2115 Returns a list of tests not grouped by test category
2116 for a specific bot.
2117 """
2118 TEST_CATS = self.get_test_generator_map().keys()
2119 tests = []
2120 for test_cat in TEST_CATS:
2121 if not test_cat in bot_info:
2122 continue
2123 test_cat_tests = bot_info[test_cat]
2124 tests = tests + test_cat_tests
2125 return tests
2126
2127 def flatten_tests_for_query(self, test_suites):
2128 """Returns a flattened dictionary of tests.
2129
2130 Returns a dictionary of tests associate with their
2131 configuration, not grouped by their test suite.
2132 """
2133 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232134 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062135 for test in test_suite:
2136 test_info = test_suite[test]
2137 test_name = test
2138 if 'name' in test_info:
2139 test_name = test_info['name']
2140 tests[test_name] = test_info
2141 return tests
2142
2143 def parse_query_filter_params(self, params):
2144 """Parses the filter parameters.
2145
2146 Creates a dictionary from the parameters provided
2147 to filter the bot array.
2148 """
2149 params_dict = {}
2150 for p in params:
2151 # flag
2152 if p.startswith("--"):
2153 params_dict[p] = True
2154 else:
2155 pair = p.split(":")
2156 if len(pair) != 2:
2157 self.error_msg('Invalid command.')
2158 # regular parameters
2159 if pair[1].lower() == "true":
2160 params_dict[pair[0]] = True
2161 elif pair[1].lower() == "false":
2162 params_dict[pair[0]] = False
2163 else:
2164 params_dict[pair[0]] = pair[1]
2165 return params_dict
2166
2167 def get_test_suites_dict(self, bots):
2168 """Returns a dictionary of bots and their tests.
2169
2170 Returns a dictionary of bots and a list of their associated tests.
2171 """
2172 test_suite_dict = dict()
2173 for bot in bots:
2174 bot_info = bots[bot]
2175 tests = self.flatten_tests_for_bot(bot_info)
2176 test_suite_dict[bot] = tests
2177 return test_suite_dict
2178
2179 def output_query_result(self, result, json_file=None):
2180 """Outputs the result of the query.
2181
2182 If a json file parameter name is provided, then
2183 the result is output into the json file. If not,
2184 then the result is printed to the console.
2185 """
2186 output = json.dumps(result, indent=2)
2187 if json_file:
2188 self.write_file(json_file, output)
2189 else:
2190 self.print_line(output)
2191 return
2192
2193 def query(self, args):
2194 """Queries tests or bots.
2195
2196 Depending on the arguments provided, outputs a json of
2197 tests or bots matching the appropriate optional parameters provided.
2198 """
2199 # split up query statement
2200 query = args.query.split('/')
2201 self.load_configuration_files()
2202 self.resolve_configuration_files()
2203
2204 # flatten bots json
2205 tests = self.test_suites
2206 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2207
2208 cmd_class = query[0]
2209
2210 # For queries starting with 'bots'
2211 if cmd_class == "bots":
2212 if len(query) == 1:
2213 return self.output_query_result(bots, args.json)
2214 # query with specific parameters
2215 elif len(query) == 2:
2216 if query[1] == 'tests':
2217 test_suites_dict = self.get_test_suites_dict(bots)
2218 return self.output_query_result(test_suites_dict, args.json)
2219 else:
2220 self.error_msg("This query should be in the format: bots/tests.")
2221
2222 else:
2223 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2224 % str(len(query)-1))
2225
2226 # For queries starting with 'bot'
2227 elif cmd_class == "bot":
2228 if not len(query) == 2 and not len(query) == 3:
2229 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2230 % str(len(query)-1))
2231 bot_id = query[1]
2232 if not bot_id in bots:
2233 self.error_msg("No bot named '" + bot_id + "' found.")
2234 bot_info = bots[bot_id]
2235 if len(query) == 2:
2236 return self.output_query_result(bot_info, args.json)
2237 if not query[2] == 'tests':
2238 self.error_msg("The query should be in the format:" +
2239 "bot/<bot-name>/tests.")
2240
2241 bot_tests = self.flatten_tests_for_bot(bot_info)
2242 return self.output_query_result(bot_tests, args.json)
2243
2244 # For queries starting with 'tests'
2245 elif cmd_class == "tests":
2246 if not len(query) == 1 and not len(query) == 2:
2247 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2248 % str(len(query)-1))
2249 flattened_tests = self.flatten_tests_for_query(tests)
2250 if len(query) == 1:
2251 return self.output_query_result(flattened_tests, args.json)
2252
2253 # create params dict
2254 params = query[1].split('&')
2255 params_dict = self.parse_query_filter_params(params)
2256 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2257 return self.output_query_result(matching_bots)
2258
2259 # For queries starting with 'test'
2260 elif cmd_class == "test":
2261 if not len(query) == 2 and not len(query) == 3:
2262 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2263 % str(len(query)-1))
2264 test_id = query[1]
2265 if len(query) == 2:
2266 flattened_tests = self.flatten_tests_for_query(tests)
2267 for test in flattened_tests:
2268 if test == test_id:
2269 return self.output_query_result(flattened_tests[test], args.json)
2270 self.error_msg("There is no test named %s." % test_id)
2271 if not query[2] == 'bots':
2272 self.error_msg("The query should be in the format: " +
2273 "test/<test-name>/bots")
2274 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2275 return self.output_query_result(bots_for_test)
2276
2277 else:
2278 self.error_msg("Your command did not match any valid commands." +
2279 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:282280
Garrett Beaty1afaccc2020-06-25 19:58:152281 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282282 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502283 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062284 elif self.args.query:
2285 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282286 else:
Greg Gutermanf60eb052020-03-12 17:40:012287 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282288 return 0
2289
2290if __name__ == "__main__": # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152291 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2292 sys.exit(generator.main())