blob: 8b9c9fc6ca4d46dc955062b0babab0f8a0f6bddd [file] [log] [blame]
Joshua Hood3455e1352022-03-03 23:23:591#!/usr/bin/env vpython3
Avi Drissmandfd880852022-09-15 20:11:092# Copyright 2016 The Chromium Authors
Kenneth Russelleb60cbd22017-12-05 07:54:283# 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
Joshua Hood56c673c2022-03-02 20:29:3320import six
Kenneth Russelleb60cbd22017-12-05 07:54:2821import string
22import sys
23
Brian Sheedya31578e2020-05-18 20:24:3624import buildbot_json_magic_substitutions as magic_substitutions
25
Joshua Hood56c673c2022-03-02 20:29:3326# pylint: disable=super-with-arguments,useless-super-delegation
27
Kenneth Russelleb60cbd22017-12-05 07:54:2828THIS_DIR = os.path.dirname(os.path.abspath(__file__))
29
Brian Sheedyf74819b2021-06-04 01:38:3830BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP = {
31 'android-chromium': '_android_chrome',
32 'android-chromium-monochrome': '_android_monochrome',
33 'android-weblayer': '_android_weblayer',
34 'android-webview': '_android_webview',
35}
36
Kenneth Russelleb60cbd22017-12-05 07:54:2837
38class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4939 def __init__(self, message):
40 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2841
42
Kenneth Russell8ceeabf2017-12-11 17:53:2843# This class is only present to accommodate certain machines on
44# chromium.android.fyi which run certain tests as instrumentation
45# tests, but not as gtests. If this discrepancy were fixed then the
46# notion could be removed.
Joshua Hood56c673c2022-03-02 20:29:3347class TestSuiteTypes(object): # pylint: disable=useless-object-inheritance
Kenneth Russell8ceeabf2017-12-11 17:53:2848 GTEST = 'gtest'
49
50
Joshua Hood56c673c2022-03-02 20:29:3351class BaseGenerator(object): # pylint: disable=useless-object-inheritance
Kenneth Russelleb60cbd22017-12-05 07:54:2852 def __init__(self, bb_gen):
53 self.bb_gen = bb_gen
54
Kenneth Russell8ceeabf2017-12-11 17:53:2855 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2856 raise NotImplementedError()
57
58 def sort(self, tests):
59 raise NotImplementedError()
60
61
Jamie Madillcf4f8c72021-05-20 19:24:2362def custom_cmp(a, b):
63 return int(a > b) - int(a < b)
64
65
Kenneth Russell8ceeabf2017-12-11 17:53:2866def cmp_tests(a, b):
67 # Prefer to compare based on the "test" key.
Jamie Madillcf4f8c72021-05-20 19:24:2368 val = custom_cmp(a['test'], b['test'])
Kenneth Russell8ceeabf2017-12-11 17:53:2869 if val != 0:
70 return val
71 if 'name' in a and 'name' in b:
Jamie Madillcf4f8c72021-05-20 19:24:2372 return custom_cmp(a['name'], b['name']) # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:2873 if 'name' not in a and 'name' not in b:
74 return 0 # pragma: no cover
75 # Prefer to put variants of the same test after the first one.
76 if 'name' in a:
77 return 1
78 # 'name' is in b.
79 return -1 # pragma: no cover
80
81
Kenneth Russell8a386d42018-06-02 09:48:0182class GPUTelemetryTestGenerator(BaseGenerator):
Fabrice de Ganscbd655f2022-08-04 20:15:3083 def __init__(self, bb_gen, is_android_webview=False, is_cast_streaming=False):
Kenneth Russell8a386d42018-06-02 09:48:0184 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5685 self._is_android_webview = is_android_webview
Fabrice de Ganscbd655f2022-08-04 20:15:3086 self._is_cast_streaming = is_cast_streaming
Kenneth Russell8a386d42018-06-02 09:48:0187
88 def generate(self, waterfall, tester_name, tester_config, input_tests):
89 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:2390 for test_name, test_config in sorted(input_tests.items()):
Ben Pastene8e7eb2652022-04-29 19:44:3191 # Variants allow more than one definition for a given test, and is defined
92 # in array format from resolve_variants().
93 if not isinstance(test_config, list):
94 test_config = [test_config]
95
96 for config in test_config:
97 test = self.bb_gen.generate_gpu_telemetry_test(waterfall, tester_name,
98 tester_config, test_name,
99 config,
Fabrice de Ganscbd655f2022-08-04 20:15:30100 self._is_android_webview,
101 self._is_cast_streaming)
Ben Pastene8e7eb2652022-04-29 19:44:31102 if test:
103 isolated_scripts.append(test)
104
Kenneth Russell8a386d42018-06-02 09:48:01105 return isolated_scripts
106
107 def sort(self, tests):
108 return sorted(tests, key=lambda x: x['name'])
109
110
Brian Sheedyb6491ba2022-09-26 20:49:49111class SkylabGPUTelemetryTestGenerator(GPUTelemetryTestGenerator):
112 def generate(self, *args, **kwargs):
113 # This should be identical to a regular GPU Telemetry test, but with any
114 # swarming arguments removed.
115 isolated_scripts = super(SkylabGPUTelemetryTestGenerator,
116 self).generate(*args, **kwargs)
117 for test in isolated_scripts:
118 if 'swarming' in test:
119 test['swarming'] = {'can_use_on_swarming_builders': False}
Brian Sheedyc860f022022-09-30 23:32:17120 if 'isolate_name' in test:
121 test['test'] = test['isolate_name']
122 del test['isolate_name']
Xinan Lind9b1d2e72022-11-14 20:57:02123 # chromium_GPU is the Autotest wrapper created for browser GPU tests
124 # run in Skylab.
125 test['autotest_name'] = 'chromium_GPU'
126 # As of 22Q4, Skylab tests are running on a CrOS flavored Autotest
127 # framework and it does not support the sub-args like
128 # extra-browser-args. So we have to pop it out and create a new
129 # key for it. See crrev.com/c/3965359 for details.
130 for idx, arg in enumerate(test.get('args', [])):
131 if '--extra-browser-args' in arg:
132 test['args'].pop(idx)
133 test['extra_browser_args'] = arg.replace('--extra-browser-args=', '')
134 break
Brian Sheedyb6491ba2022-09-26 20:49:49135 return isolated_scripts
136
137
Kenneth Russelleb60cbd22017-12-05 07:54:28138class GTestGenerator(BaseGenerator):
139 def __init__(self, bb_gen):
140 super(GTestGenerator, self).__init__(bb_gen)
141
Kenneth Russell8ceeabf2017-12-11 17:53:28142 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28143 # The relative ordering of some of the tests is important to
144 # minimize differences compared to the handwritten JSON files, since
145 # Python's sorts are stable and there are some tests with the same
146 # key (see gles2_conform_d3d9_test and similar variants). Avoid
147 # losing the order by avoiding coalescing the dictionaries into one.
148 gtests = []
Jamie Madillcf4f8c72021-05-20 19:24:23149 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoon67c3e832020-02-08 07:39:38150 # Variants allow more than one definition for a given test, and is defined
151 # in array format from resolve_variants().
152 if not isinstance(test_config, list):
153 test_config = [test_config]
154
155 for config in test_config:
156 test = self.bb_gen.generate_gtest(
157 waterfall, tester_name, tester_config, test_name, config)
158 if test:
159 # generate_gtest may veto the test generation on this tester.
160 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28161 return gtests
162
163 def sort(self, tests):
Jamie Madillcf4f8c72021-05-20 19:24:23164 return sorted(tests, key=functools.cmp_to_key(cmp_tests))
Kenneth Russelleb60cbd22017-12-05 07:54:28165
166
167class IsolatedScriptTestGenerator(BaseGenerator):
168 def __init__(self, bb_gen):
169 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
170
Kenneth Russell8ceeabf2017-12-11 17:53:28171 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28172 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23173 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43174 # Variants allow more than one definition for a given test, and is defined
175 # in array format from resolve_variants().
176 if not isinstance(test_config, list):
177 test_config = [test_config]
178
179 for config in test_config:
180 test = self.bb_gen.generate_isolated_script_test(
181 waterfall, tester_name, tester_config, test_name, config)
182 if test:
183 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28184 return isolated_scripts
185
186 def sort(self, tests):
187 return sorted(tests, key=lambda x: x['name'])
188
189
190class ScriptGenerator(BaseGenerator):
191 def __init__(self, bb_gen):
192 super(ScriptGenerator, self).__init__(bb_gen)
193
Kenneth Russell8ceeabf2017-12-11 17:53:28194 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28195 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23196 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28197 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28198 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28199 if test:
200 scripts.append(test)
201 return scripts
202
203 def sort(self, tests):
204 return sorted(tests, key=lambda x: x['name'])
205
206
207class JUnitGenerator(BaseGenerator):
208 def __init__(self, bb_gen):
209 super(JUnitGenerator, self).__init__(bb_gen)
210
Kenneth Russell8ceeabf2017-12-11 17:53:28211 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28212 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23213 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28214 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28215 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28216 if test:
217 scripts.append(test)
218 return scripts
219
220 def sort(self, tests):
221 return sorted(tests, key=lambda x: x['test'])
222
223
Xinan Lin05fb9c1752020-12-17 00:15:52224class SkylabGenerator(BaseGenerator):
225 def __init__(self, bb_gen):
226 super(SkylabGenerator, self).__init__(bb_gen)
227
228 def generate(self, waterfall, tester_name, tester_config, input_tests):
229 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23230 for test_name, test_config in sorted(input_tests.items()):
Xinan Lin05fb9c1752020-12-17 00:15:52231 for config in test_config:
232 test = self.bb_gen.generate_skylab_test(waterfall, tester_name,
233 tester_config, test_name,
234 config)
235 if test:
236 scripts.append(test)
237 return scripts
238
239 def sort(self, tests):
240 return sorted(tests, key=lambda x: x['test'])
241
242
Jeff Yoon67c3e832020-02-08 07:39:38243def check_compound_references(other_test_suites=None,
244 sub_suite=None,
245 suite=None,
246 target_test_suites=None,
247 test_type=None,
248 **kwargs):
249 """Ensure comound reference's don't target other compounds"""
250 del kwargs
251 if sub_suite in other_test_suites or sub_suite in target_test_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15252 raise BBGenErr('%s may not refer to other composition type test '
253 'suites (error found while processing %s)' %
254 (test_type, suite))
255
Jeff Yoon67c3e832020-02-08 07:39:38256
257def check_basic_references(basic_suites=None,
258 sub_suite=None,
259 suite=None,
260 **kwargs):
261 """Ensure test has a basic suite reference"""
262 del kwargs
263 if sub_suite not in basic_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15264 raise BBGenErr('Unable to find reference to %s while processing %s' %
265 (sub_suite, suite))
266
Jeff Yoon67c3e832020-02-08 07:39:38267
268def check_conflicting_definitions(basic_suites=None,
269 seen_tests=None,
270 sub_suite=None,
271 suite=None,
272 test_type=None,
273 **kwargs):
274 """Ensure that if a test is reachable via multiple basic suites,
275 all of them have an identical definition of the tests.
276 """
277 del kwargs
278 for test_name in basic_suites[sub_suite]:
279 if (test_name in seen_tests and
280 basic_suites[sub_suite][test_name] !=
281 basic_suites[seen_tests[test_name]][test_name]):
282 raise BBGenErr('Conflicting test definitions for %s from %s '
283 'and %s in %s (error found while processing %s)'
284 % (test_name, seen_tests[test_name], sub_suite,
285 test_type, suite))
286 seen_tests[test_name] = sub_suite
287
288def check_matrix_identifier(sub_suite=None,
289 suite=None,
290 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05291 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38292 **kwargs):
293 """Ensure 'idenfitier' is defined for each variant"""
294 del kwargs
295 sub_suite_config = suite_def[sub_suite]
296 for variant in sub_suite_config.get('variants', []):
Jeff Yoonda581c32020-03-06 03:56:05297 if isinstance(variant, str):
298 if variant not in all_variants:
299 raise BBGenErr('Missing variant definition for %s in variants.pyl'
300 % variant)
301 variant = all_variants[variant]
302
Jeff Yoon67c3e832020-02-08 07:39:38303 if not 'identifier' in variant:
304 raise BBGenErr('Missing required identifier field in matrix '
305 'compound suite %s, %s' % (suite, sub_suite))
Sven Zhengef0d0872022-04-04 22:13:29306 if variant['identifier'] == '':
307 raise BBGenErr('Identifier field can not be "" in matrix '
308 'compound suite %s, %s' % (suite, sub_suite))
309 if variant['identifier'].strip() != variant['identifier']:
310 raise BBGenErr('Identifier field can not have leading and trailing '
311 'whitespace in matrix compound suite %s, %s' %
312 (suite, sub_suite))
Jeff Yoon67c3e832020-02-08 07:39:38313
314
Joshua Hood56c673c2022-03-02 20:29:33315class BBJSONGenerator(object): # pylint: disable=useless-object-inheritance
Garrett Beaty1afaccc2020-06-25 19:58:15316 def __init__(self, args):
Kenneth Russelleb60cbd22017-12-05 07:54:28317 self.this_dir = THIS_DIR
Garrett Beaty1afaccc2020-06-25 19:58:15318 self.args = args
Kenneth Russelleb60cbd22017-12-05 07:54:28319 self.waterfalls = None
320 self.test_suites = None
321 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01322 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41323 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05324 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28325
Garrett Beaty1afaccc2020-06-25 19:58:15326 @staticmethod
327 def parse_args(argv):
328
329 # RawTextHelpFormatter allows for styling of help statement
330 parser = argparse.ArgumentParser(
331 formatter_class=argparse.RawTextHelpFormatter)
332
333 group = parser.add_mutually_exclusive_group()
334 group.add_argument(
335 '-c',
336 '--check',
337 action='store_true',
338 help=
339 'Do consistency checks of configuration and generated files and then '
340 'exit. Used during presubmit. '
341 'Causes the tool to not generate any files.')
342 group.add_argument(
343 '--query',
344 type=str,
345 help=(
346 "Returns raw JSON information of buildbots and tests.\n" +
347 "Examples:\n" + " List all bots (all info):\n" +
348 " --query bots\n\n" +
349 " List all bots and only their associated tests:\n" +
350 " --query bots/tests\n\n" +
351 " List all information about 'bot1' " +
352 "(make sure you have quotes):\n" + " --query bot/'bot1'\n\n" +
353 " List tests running for 'bot1' (make sure you have quotes):\n" +
354 " --query bot/'bot1'/tests\n\n" + " List all tests:\n" +
355 " --query tests\n\n" +
356 " List all tests and the bots running them:\n" +
357 " --query tests/bots\n\n" +
358 " List all tests that satisfy multiple parameters\n" +
359 " (separation of parameters by '&' symbol):\n" +
360 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
361 " List all tests that run with a specific flag:\n" +
362 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
363 " List specific test (make sure you have quotes):\n"
364 " --query test/'test1'\n\n"
365 " List all bots running 'test1' " +
366 "(make sure you have quotes):\n" + " --query test/'test1'/bots"))
367 parser.add_argument(
368 '-n',
369 '--new-files',
370 action='store_true',
371 help=
372 'Write output files as .new.json. Useful during development so old and '
373 'new files can be looked at side-by-side.')
374 parser.add_argument('-v',
375 '--verbose',
376 action='store_true',
377 help='Increases verbosity. Affects consistency checks.')
378 parser.add_argument('waterfall_filters',
379 metavar='waterfalls',
380 type=str,
381 nargs='*',
382 help='Optional list of waterfalls to generate.')
383 parser.add_argument(
384 '--pyl-files-dir',
385 type=os.path.realpath,
386 help='Path to the directory containing the input .pyl files.')
387 parser.add_argument(
388 '--json',
389 metavar='JSON_FILE_PATH',
390 help='Outputs results into a json file. Only works with query function.'
391 )
Chong Guee622242020-10-28 18:17:35392 parser.add_argument('--isolate-map-file',
393 metavar='PATH',
394 help='path to additional isolate map files.',
395 default=[],
396 action='append',
397 dest='isolate_map_files')
Garrett Beaty1afaccc2020-06-25 19:58:15398 parser.add_argument(
399 '--infra-config-dir',
400 help='Path to the LUCI services configuration directory',
401 default=os.path.abspath(
402 os.path.join(os.path.dirname(__file__), '..', '..', 'infra',
403 'config')))
404 args = parser.parse_args(argv)
405 if args.json and not args.query:
406 parser.error(
407 "The --json flag can only be used with --query.") # pragma: no cover
408 args.infra_config_dir = os.path.abspath(args.infra_config_dir)
409 return args
410
Kenneth Russelleb60cbd22017-12-05 07:54:28411 def generate_abs_file_path(self, relative_path):
Garrett Beaty1afaccc2020-06-25 19:58:15412 return os.path.join(self.this_dir, relative_path)
Kenneth Russelleb60cbd22017-12-05 07:54:28413
Stephen Martinis7eb8b612018-09-21 00:17:50414 def print_line(self, line):
415 # Exists so that tests can mock
Jamie Madillcf4f8c72021-05-20 19:24:23416 print(line) # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:50417
Kenneth Russelleb60cbd22017-12-05 07:54:28418 def read_file(self, relative_path):
Garrett Beaty1afaccc2020-06-25 19:58:15419 with open(self.generate_abs_file_path(relative_path)) as fp:
420 return fp.read()
Kenneth Russelleb60cbd22017-12-05 07:54:28421
422 def write_file(self, relative_path, contents):
Garrett Beaty1afaccc2020-06-25 19:58:15423 with open(self.generate_abs_file_path(relative_path), 'wb') as fp:
Jamie Madillcf4f8c72021-05-20 19:24:23424 fp.write(contents.encode('utf-8'))
Kenneth Russelleb60cbd22017-12-05 07:54:28425
Zhiling Huangbe008172018-03-08 19:13:11426 def pyl_file_path(self, filename):
427 if self.args and self.args.pyl_files_dir:
428 return os.path.join(self.args.pyl_files_dir, filename)
429 return filename
430
Joshua Hood56c673c2022-03-02 20:29:33431 # pylint: disable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:28432 def load_pyl_file(self, filename):
433 try:
Zhiling Huangbe008172018-03-08 19:13:11434 return ast.literal_eval(self.read_file(
435 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28436 except (SyntaxError, ValueError) as e: # pragma: no cover
Joshua Hood56c673c2022-03-02 20:29:33437 six.raise_from(
438 BBGenErr('Failed to parse pyl file "%s": %s' % (filename, e)),
439 e) # pragma: no cover
440 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:28441
Kenneth Russell8a386d42018-06-02 09:48:01442 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
443 # Currently it is only mandatory for bots which run GPU tests. Change these to
444 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28445 def is_android(self, tester_config):
446 return tester_config.get('os_type') == 'android'
447
Ben Pastenea9e583b2019-01-16 02:57:26448 def is_chromeos(self, tester_config):
449 return tester_config.get('os_type') == 'chromeos'
450
Chong Guc2ca5d02022-01-11 19:52:17451 def is_fuchsia(self, tester_config):
452 return tester_config.get('os_type') == 'fuchsia'
453
Brian Sheedy781c8ca42021-03-08 22:03:21454 def is_lacros(self, tester_config):
455 return tester_config.get('os_type') == 'lacros'
456
Kenneth Russell8a386d42018-06-02 09:48:01457 def is_linux(self, tester_config):
458 return tester_config.get('os_type') == 'linux'
459
Kai Ninomiya40de9f52019-10-18 21:38:49460 def is_mac(self, tester_config):
461 return tester_config.get('os_type') == 'mac'
462
463 def is_win(self, tester_config):
464 return tester_config.get('os_type') == 'win'
465
466 def is_win64(self, tester_config):
467 return (tester_config.get('os_type') == 'win' and
468 tester_config.get('browser_config') == 'release_x64')
469
Ben Pastene5f231cf22022-05-05 18:03:07470 def add_variant_to_test_name(self, test_name, variant_id):
471 return '{} {}'.format(test_name, variant_id)
472
473 def remove_variant_from_test_name(self, test_name, variant_id):
474 return test_name.split(variant_id)[0].strip()
475
Kenneth Russelleb60cbd22017-12-05 07:54:28476 def get_exception_for_test(self, test_name, test_config):
477 # gtests may have both "test" and "name" fields, and usually, if the "name"
478 # field is specified, it means that the same test is being repurposed
479 # multiple times with different command line arguments. To handle this case,
480 # prefer to lookup per the "name" field of the test itself, as opposed to
481 # the "test_name", which is actually the "test" field.
482 if 'name' in test_config:
483 return self.exceptions.get(test_config['name'])
Joshua Hood56c673c2022-03-02 20:29:33484 return self.exceptions.get(test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28485
Nico Weberb0b3f5862018-07-13 18:45:15486 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28487 # Currently, the only reason a test should not run on a given tester is that
488 # it's in the exceptions. (Once the GPU waterfall generation script is
489 # incorporated here, the rules will become more complex.)
490 exception = self.get_exception_for_test(test_name, test_config)
491 if not exception:
492 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28493 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28494 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28495 if remove_from:
496 if tester_name in remove_from:
497 return False
498 # TODO(kbr): this code path was added for some tests (including
499 # android_webview_unittests) on one machine (Nougat Phone
500 # Tester) which exists with the same name on two waterfalls,
501 # chromium.android and chromium.fyi; the tests are run on one
502 # but not the other. Once the bots are all uniquely named (a
503 # different ongoing project) this code should be removed.
504 # TODO(kbr): add coverage.
505 return (tester_name + ' ' + waterfall['name']
506 not in remove_from) # pragma: no cover
507 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28508
Nico Weber79dc5f6852018-07-13 19:38:49509 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28510 exception = self.get_exception_for_test(test_name, test)
511 if not exception:
512 return None
Nico Weber79dc5f6852018-07-13 19:38:49513 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28514
Brian Sheedye6ea0ee2019-07-11 02:54:37515 def get_test_replacements(self, test, test_name, tester_name):
516 exception = self.get_exception_for_test(test_name, test)
517 if not exception:
518 return None
519 return exception.get('replacements', {}).get(tester_name)
520
Kenneth Russell8a386d42018-06-02 09:48:01521 def merge_command_line_args(self, arr, prefix, splitter):
522 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01523 idx = 0
524 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01525 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01526 while idx < len(arr):
527 flag = arr[idx]
528 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01529 if flag.startswith(prefix):
530 arg = flag[prefix_len:]
531 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01532 if first_idx < 0:
533 first_idx = idx
534 else:
535 delete_current_entry = True
536 if delete_current_entry:
537 del arr[idx]
538 else:
539 idx += 1
540 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01541 arr[first_idx] = prefix + splitter.join(accumulated_args)
542 return arr
543
544 def maybe_fixup_args_array(self, arr):
545 # The incoming array of strings may be an array of command line
546 # arguments. To make it easier to turn on certain features per-bot or
547 # per-test-suite, look specifically for certain flags and merge them
548 # appropriately.
549 # --enable-features=Feature1 --enable-features=Feature2
550 # are merged to:
551 # --enable-features=Feature1,Feature2
552 # and:
553 # --extra-browser-args=arg1 --extra-browser-args=arg2
554 # are merged to:
555 # --extra-browser-args=arg1 arg2
556 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
557 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Yuly Novikov8c487e72020-10-16 20:00:29558 arr = self.merge_command_line_args(arr, '--test-launcher-filter-file=', ';')
Cameron Higgins971f0b92023-01-03 18:05:09559 arr = self.merge_command_line_args(arr, '--extra-app-args=', ',')
Kenneth Russell650995a2018-05-03 21:17:01560 return arr
561
Brian Sheedy910cda82022-07-19 11:58:34562 def substitute_magic_args(self, test_config, tester_name, tester_config):
Brian Sheedya31578e2020-05-18 20:24:36563 """Substitutes any magic substitution args present in |test_config|.
564
565 Substitutions are done in-place.
566
567 See buildbot_json_magic_substitutions.py for more information on this
568 feature.
569
570 Args:
571 test_config: A dict containing a configuration for a specific test on
572 a specific builder, e.g. the output of update_and_cleanup_test.
Brian Sheedy5f173bb2021-11-24 00:45:54573 tester_name: A string containing the name of the tester that |test_config|
574 came from.
Brian Sheedy910cda82022-07-19 11:58:34575 tester_config: A dict containing the configuration for the builder that
576 |test_config| is for.
Brian Sheedya31578e2020-05-18 20:24:36577 """
578 substituted_array = []
Brian Sheedyba13cf522022-09-13 21:00:09579 original_args = test_config.get('args', [])
580 for arg in original_args:
Brian Sheedya31578e2020-05-18 20:24:36581 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
582 function = arg.replace(
583 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
584 if hasattr(magic_substitutions, function):
585 substituted_array.extend(
Brian Sheedy910cda82022-07-19 11:58:34586 getattr(magic_substitutions, function)(test_config, tester_name,
587 tester_config))
Brian Sheedya31578e2020-05-18 20:24:36588 else:
589 raise BBGenErr(
590 'Magic substitution function %s does not exist' % function)
591 else:
592 substituted_array.append(arg)
Brian Sheedyba13cf522022-09-13 21:00:09593 if substituted_array != original_args:
Brian Sheedya31578e2020-05-18 20:24:36594 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
595
Kenneth Russelleb60cbd22017-12-05 07:54:28596 def dictionary_merge(self, a, b, path=None, update=True):
597 """http://stackoverflow.com/questions/7204805/
598 python-dictionaries-of-dictionaries-merge
599 merges b into a
600 """
601 if path is None:
602 path = []
603 for key in b:
604 if key in a:
605 if isinstance(a[key], dict) and isinstance(b[key], dict):
606 self.dictionary_merge(a[key], b[key], path + [str(key)])
607 elif a[key] == b[key]:
608 pass # same leaf value
609 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06610 # Args arrays are lists of strings. Just concatenate them,
611 # and don't sort them, in order to keep some needed
Weizhong Xia91b53362022-01-05 17:13:35612 # arguments adjacent (like --timeout-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28613 if all(isinstance(x, str)
614 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01615 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28616 else:
617 # TODO(kbr): this only works properly if the two arrays are
618 # the same length, which is currently always the case in the
619 # swarming dimension_sets that we have to merge. It will fail
620 # to merge / override 'args' arrays which are different
621 # length.
Jamie Madillcf4f8c72021-05-20 19:24:23622 for idx in range(len(b[key])):
Kenneth Russell8ceeabf2017-12-11 17:53:28623 try:
624 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
625 path + [str(key), str(idx)],
626 update=update)
Joshua Hood56c673c2022-03-02 20:29:33627 except (IndexError, TypeError) as e:
628 six.raise_from(
629 BBGenErr('Error merging lists by key "%s" from source %s '
630 'into target %s at index %s. Verify target list '
631 'length is equal or greater than source' %
632 (str(key), str(b), str(a), str(idx))), e)
John Budorick5bc387fe2019-05-09 20:02:53633 elif update:
634 if b[key] is None:
635 del a[key]
636 else:
637 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28638 else:
639 raise BBGenErr('Conflict at %s' % '.'.join(
640 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53641 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28642 a[key] = b[key]
643 return a
644
John Budorickab108712018-09-01 00:12:21645 def initialize_args_for_test(
646 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21647 args = []
648 args.extend(generated_test.get('args', []))
649 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22650
Kenneth Russell8a386d42018-06-02 09:48:01651 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21652 val = generated_test.pop(key, [])
653 if fn(tester_config):
654 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01655
656 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
Brian Sheedy781c8ca42021-03-08 22:03:21657 add_conditional_args('lacros_args', self.is_lacros)
Kenneth Russell8a386d42018-06-02 09:48:01658 add_conditional_args('linux_args', self.is_linux)
659 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36660 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49661 add_conditional_args('mac_args', self.is_mac)
662 add_conditional_args('win_args', self.is_win)
663 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01664
John Budorickab108712018-09-01 00:12:21665 for key in additional_arg_keys or []:
666 args.extend(generated_test.pop(key, []))
667 args.extend(tester_config.get(key, []))
668
669 if args:
670 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01671
Kenneth Russelleb60cbd22017-12-05 07:54:28672 def initialize_swarming_dictionary_for_test(self, generated_test,
673 tester_config):
674 if 'swarming' not in generated_test:
675 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28676 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
677 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38678 'can_use_on_swarming_builders': tester_config.get('use_swarming',
679 True)
Dirk Pranke81ff51c2017-12-09 19:24:28680 })
Kenneth Russelleb60cbd22017-12-05 07:54:28681 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03682 if ('dimension_sets' not in generated_test['swarming'] and
683 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28684 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
685 tester_config['swarming']['dimension_sets'])
686 self.dictionary_merge(generated_test['swarming'],
687 tester_config['swarming'])
Brian Sheedybc984e242021-04-21 23:44:51688 # Apply any platform-specific Swarming dimensions after the generic ones.
Kenneth Russelleb60cbd22017-12-05 07:54:28689 if 'android_swarming' in generated_test:
690 if self.is_android(tester_config): # pragma: no cover
691 self.dictionary_merge(
692 generated_test['swarming'],
693 generated_test['android_swarming']) # pragma: no cover
694 del generated_test['android_swarming'] # pragma: no cover
Brian Sheedybc984e242021-04-21 23:44:51695 if 'chromeos_swarming' in generated_test:
696 if self.is_chromeos(tester_config): # pragma: no cover
697 self.dictionary_merge(
698 generated_test['swarming'],
699 generated_test['chromeos_swarming']) # pragma: no cover
700 del generated_test['chromeos_swarming'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28701
702 def clean_swarming_dictionary(self, swarming_dict):
703 # Clean out redundant entries from a test's "swarming" dictionary.
704 # This is really only needed to retain 100% parity with the
705 # handwritten JSON files, and can be removed once all the files are
706 # autogenerated.
707 if 'shards' in swarming_dict:
708 if swarming_dict['shards'] == 1: # pragma: no cover
709 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24710 if 'hard_timeout' in swarming_dict:
711 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
712 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43713 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28714 # Remove all other keys.
Jamie Madillcf4f8c72021-05-20 19:24:23715 for k in list(swarming_dict): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28716 if k != 'can_use_on_swarming_builders': # pragma: no cover
717 del swarming_dict[k] # pragma: no cover
718
Stephen Martinis0382bc12018-09-17 22:29:07719 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
720 waterfall):
721 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01722 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07723 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28724 # See if there are any exceptions that need to be merged into this
725 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49726 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28727 if modifications:
728 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23729 if 'swarming' in test:
730 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28731 # Ensure all Android Swarming tests run only on userdebug builds if another
732 # build type was not specified.
733 if 'swarming' in test and self.is_android(tester_config):
734 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22735 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28736 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37737 self.replace_test_args(test, test_name, tester_name)
Ben Pastenee012aea42019-05-14 22:32:28738
Kenneth Russelleb60cbd22017-12-05 07:54:28739 return test
740
Brian Sheedye6ea0ee2019-07-11 02:54:37741 def replace_test_args(self, test, test_name, tester_name):
742 replacements = self.get_test_replacements(
743 test, test_name, tester_name) or {}
744 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23745 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37746 if key not in valid_replacement_keys:
747 raise BBGenErr(
748 'Given replacement key %s for %s on %s is not in the list of valid '
749 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23750 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37751 found_key = False
752 for i, test_key in enumerate(test.get(key, [])):
753 # Handle both the key/value being replaced being defined as two
754 # separate items or as key=value.
755 if test_key == replacement_key:
756 found_key = True
757 # Handle flags without values.
758 if replacement_val == None:
759 del test[key][i]
760 else:
761 test[key][i+1] = replacement_val
762 break
Joshua Hood56c673c2022-03-02 20:29:33763 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37764 found_key = True
765 if replacement_val == None:
766 del test[key][i]
767 else:
768 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
769 break
770 if not found_key:
771 raise BBGenErr('Could not find %s in existing list of values for key '
772 '%s in %s on %s' % (replacement_key, key, test_name,
773 tester_name))
774
Shenghua Zhangaba8bad2018-02-07 02:12:09775 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05776 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26777 True):
778 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06779 # are targeting CrOS hardware and so need the special trigger script.
780 dimension_sets = test['swarming']['dimension_sets']
Ben Pastenea9e583b2019-01-16 02:57:26781 if all('device_type' in ds for ds in dimension_sets):
782 test['trigger_script'] = {
783 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
784 }
Shenghua Zhangaba8bad2018-02-07 02:12:09785
Ben Pastene858f4be2019-01-09 23:52:09786 def add_android_presentation_args(self, tester_config, test_name, result):
787 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38788 bucket = tester_config.get('results_bucket', 'chromium-result-details')
789 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09790 if (result['swarming']['can_use_on_swarming_builders'] and not
791 tester_config.get('skip_merge_script', False)):
792 result['merge'] = {
793 'args': [
794 '--bucket',
John Budorick262ae112019-07-12 19:24:38795 bucket,
Ben Pastene858f4be2019-01-09 23:52:09796 '--test-name',
Rakib M. Hasanc9e01c62020-07-27 22:48:12797 result.get('name', test_name)
Ben Pastene858f4be2019-01-09 23:52:09798 ],
799 'script': '//build/android/pylib/results/presentation/'
800 'test_results_presentation.py',
801 }
Ben Pastene858f4be2019-01-09 23:52:09802 if not tester_config.get('skip_output_links', False):
803 result['swarming']['output_links'] = [
804 {
805 'link': [
806 'https://luci-logdog.appspot.com/v/?s',
807 '=android%2Fswarming%2Flogcats%2F',
808 '${TASK_ID}%2F%2B%2Funified_logcats',
809 ],
810 'name': 'shard #${SHARD_INDEX} logcats',
811 },
812 ]
813 if args:
814 result['args'] = args
815
Kenneth Russelleb60cbd22017-12-05 07:54:28816 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
817 test_config):
818 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15819 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28820 return None
821 result = copy.deepcopy(test_config)
822 if 'test' in result:
Rakib M. Hasanc9e01c62020-07-27 22:48:12823 if 'name' not in result:
824 result['name'] = test_name
Kenneth Russelleb60cbd22017-12-05 07:54:28825 else:
826 result['test'] = test_name
827 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21828
829 self.initialize_args_for_test(
830 result, tester_config, additional_arg_keys=['gtest_args'])
Jamie Madilla8be0d72020-10-02 05:24:04831 if self.is_android(tester_config) and tester_config.get(
Yuly Novikov26dd47052021-02-11 00:57:14832 'use_swarming', True):
833 if not test_config.get('use_isolated_scripts_api', False):
834 # TODO(https://crbug.com/1137998) make Android presentation work with
835 # isolated scripts in test_results_presentation.py merge script
836 self.add_android_presentation_args(tester_config, test_name, result)
837 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42838
Stephen Martinis0382bc12018-09-17 22:29:07839 result = self.update_and_cleanup_test(
840 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09841 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34842 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43843
844 if not result.get('merge'):
845 # TODO(https://crbug.com/958376): Consider adding the ability to not have
846 # this default.
Jamie Madilla8be0d72020-10-02 05:24:04847 if test_config.get('use_isolated_scripts_api', False):
848 merge_script = 'standard_isolated_script_merge'
849 else:
850 merge_script = 'standard_gtest_merge'
851
Stephen Martinisbc7b7772019-05-01 22:01:43852 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04853 'script': '//testing/merge_scripts/%s.py' % merge_script,
854 'args': [],
Stephen Martinisbc7b7772019-05-01 22:01:43855 }
Kenneth Russelleb60cbd22017-12-05 07:54:28856 return result
857
858 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
859 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01860 if not self.should_run_on_tester(waterfall, tester_name, test_name,
861 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28862 return None
863 result = copy.deepcopy(test_config)
864 result['isolate_name'] = result.get('isolate_name', test_name)
Jeff Yoonb8bfdbf32020-03-13 19:14:43865 result['name'] = result.get('name', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28866 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01867 self.initialize_args_for_test(result, tester_config)
Yuly Novikov26dd47052021-02-11 00:57:14868 if self.is_android(tester_config) and tester_config.get(
869 'use_swarming', True):
870 if tester_config.get('use_android_presentation', False):
871 # TODO(https://crbug.com/1137998) make Android presentation work with
872 # isolated scripts in test_results_presentation.py merge script
873 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07874 result = self.update_and_cleanup_test(
875 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09876 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34877 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17878
879 if not result.get('merge'):
880 # TODO(https://crbug.com/958376): Consider adding the ability to not have
881 # this default.
882 result['merge'] = {
883 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
884 'args': [],
885 }
Kenneth Russelleb60cbd22017-12-05 07:54:28886 return result
887
888 def generate_script_test(self, waterfall, tester_name, tester_config,
889 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44890 # TODO(https://crbug.com/953072): Remove this check whenever a better
891 # long-term solution is implemented.
892 if (waterfall.get('forbid_script_tests', False) or
893 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
894 raise BBGenErr('Attempted to generate a script test on tester ' +
895 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01896 if not self.should_run_on_tester(waterfall, tester_name, test_name,
897 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28898 return None
899 result = {
900 'name': test_name,
901 'script': test_config['script']
902 }
Stephen Martinis0382bc12018-09-17 22:29:07903 result = self.update_and_cleanup_test(
904 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34905 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28906 return result
907
908 def generate_junit_test(self, waterfall, tester_name, tester_config,
909 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01910 if not self.should_run_on_tester(waterfall, tester_name, test_name,
911 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28912 return None
John Budorickdef6acb2019-09-17 22:51:09913 result = copy.deepcopy(test_config)
914 result.update({
John Budorickcadc4952019-09-16 23:51:37915 'name': test_name,
916 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09917 })
918 self.initialize_args_for_test(result, tester_config)
919 result = self.update_and_cleanup_test(
920 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34921 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28922 return result
923
Xinan Lin05fb9c1752020-12-17 00:15:52924 def generate_skylab_test(self, waterfall, tester_name, tester_config,
925 test_name, test_config):
926 if not self.should_run_on_tester(waterfall, tester_name, test_name,
927 test_config):
928 return None
929 result = copy.deepcopy(test_config)
930 result.update({
931 'test': test_name,
932 })
933 self.initialize_args_for_test(result, tester_config)
934 result = self.update_and_cleanup_test(result, test_name, tester_name,
935 tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34936 self.substitute_magic_args(result, tester_name, tester_config)
Xinan Lin05fb9c1752020-12-17 00:15:52937 return result
938
Stephen Martinis2a0667022018-09-25 22:31:14939 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01940 substitutions = {
941 # Any machine in waterfalls.pyl which desires to run GPU tests
942 # must provide the os_type key.
943 'os_type': tester_config['os_type'],
944 'gpu_vendor_id': '0',
945 'gpu_device_id': '0',
946 }
Brian Sheedyb6491ba2022-09-26 20:49:49947 if swarming_config.get('dimension_sets'):
948 dimension_set = swarming_config['dimension_sets'][0]
949 if 'gpu' in dimension_set:
950 # First remove the driver version, then split into vendor and device.
951 gpu = dimension_set['gpu']
952 if gpu != 'none':
953 gpu = gpu.split('-')[0].split(':')
954 substitutions['gpu_vendor_id'] = gpu[0]
955 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01956 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
957
958 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30959 test_name, test_config, is_android_webview,
960 is_cast_streaming):
Kenneth Russell8a386d42018-06-02 09:48:01961 # These are all just specializations of isolated script tests with
962 # a bunch of boilerplate command line arguments added.
963
964 # The step name must end in 'test' or 'tests' in order for the
965 # results to automatically show up on the flakiness dashboard.
966 # (At least, this was true some time ago.) Continue to use this
967 # naming convention for the time being to minimize changes.
968 step_name = test_config.get('name', test_name)
Ben Pastene5f231cf22022-05-05 18:03:07969 variant_id = test_config.get('variant_id')
970 if variant_id:
971 step_name = self.remove_variant_from_test_name(step_name, variant_id)
Kenneth Russell8a386d42018-06-02 09:48:01972 if not (step_name.endswith('test') or step_name.endswith('tests')):
973 step_name = '%s_tests' % step_name
Ben Pastene5f231cf22022-05-05 18:03:07974 if variant_id:
975 step_name = self.add_variant_to_test_name(step_name, variant_id)
Ben Pastene5ff45d82022-05-05 21:54:00976 if 'name' in test_config:
977 test_config['name'] = step_name
Kenneth Russell8a386d42018-06-02 09:48:01978 result = self.generate_isolated_script_test(
979 waterfall, tester_name, tester_config, step_name, test_config)
980 if not result:
981 return None
Chong Gub75754b32020-03-13 16:39:20982 result['isolate_name'] = test_config.get(
Brian Sheedyf74819b2021-06-04 01:38:38983 'isolate_name',
984 self.get_default_isolate_name(tester_config, is_android_webview))
Chan Liab7d8dd82020-04-24 23:42:19985
Chan Lia3ad1502020-04-28 05:32:11986 # Populate test_id_prefix.
Brian Sheedyf74819b2021-06-04 01:38:38987 gn_entry = self.gn_isolate_map[result['isolate_name']]
Chan Li17d969f92020-07-10 00:50:03988 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19989
Kenneth Russell8a386d42018-06-02 09:48:01990 args = result.get('args', [])
991 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14992
erikchen6da2d9b2018-08-03 23:01:14993 # These tests upload and download results from cloud storage and therefore
994 # aren't idempotent yet. https://crbug.com/549140.
995 result['swarming']['idempotent'] = False
996
Kenneth Russell44910c32018-12-03 23:35:11997 # The GPU tests act much like integration tests for the entire browser, and
998 # tend to uncover flakiness bugs more readily than other test suites. In
999 # order to surface any flakiness more readily to the developer of the CL
1000 # which is introducing it, we disable retries with patch on the commit
1001 # queue.
1002 result['should_retry_with_patch'] = False
1003
Fabrice de Ganscbd655f2022-08-04 20:15:301004 browser = ''
1005 if is_cast_streaming:
1006 browser = 'cast-streaming-shell'
1007 elif is_android_webview:
1008 browser = 'android-webview-instrumentation'
1009 else:
1010 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:521011
1012 # Most platforms require --enable-logging=stderr to get useful browser logs.
1013 # However, this actively messes with logging on CrOS (because Chrome's
1014 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
1015 # in order to see JavaScript console messages. See
1016 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
1017 logging_arg = '--log-level=0' if self.is_chromeos(
1018 tester_config) else '--enable-logging=stderr'
1019
Kenneth Russell8a386d42018-06-02 09:48:011020 args = [
Bo Liu555a0f92019-03-29 12:11:561021 test_to_run,
1022 '--show-stdout',
1023 '--browser=%s' % browser,
1024 # --passthrough displays more of the logging in Telemetry when
1025 # run via typ, in particular some of the warnings about tests
1026 # being expected to fail, but passing.
1027 '--passthrough',
1028 '-v',
Brian Sheedy814e0482022-10-03 23:24:121029 '--stable-jobs',
Brian Sheedy4053a702020-07-28 02:09:521030 '--extra-browser-args=%s --js-flags=--expose-gc' % logging_arg,
Kenneth Russell8a386d42018-06-02 09:48:011031 ] + args
1032 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:141033 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:011034 return result
1035
Brian Sheedyf74819b2021-06-04 01:38:381036 def get_default_isolate_name(self, tester_config, is_android_webview):
1037 if self.is_android(tester_config):
1038 if is_android_webview:
1039 return 'telemetry_gpu_integration_test_android_webview'
1040 return (
1041 'telemetry_gpu_integration_test' +
1042 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:331043 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:171044 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:331045 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:381046
Kenneth Russelleb60cbd22017-12-05 07:54:281047 def get_test_generator_map(self):
1048 return {
Bo Liu555a0f92019-03-29 12:11:561049 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301050 GPUTelemetryTestGenerator(self, is_android_webview=True),
1051 'cast_streaming_tests':
1052 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:561053 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301054 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561055 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301056 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561057 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301058 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561059 'junit_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301060 JUnitGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561061 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301062 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:521063 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301064 SkylabGenerator(self),
Brian Sheedyb6491ba2022-09-26 20:49:491065 'skylab_gpu_telemetry_tests':
1066 SkylabGPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281067 }
1068
Kenneth Russell8a386d42018-06-02 09:48:011069 def get_test_type_remapper(self):
1070 return {
Fabrice de Gans223272482022-08-08 16:56:571071 # These are a specialization of isolated_scripts with a bunch of
1072 # boilerplate command line arguments added to each one.
1073 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
1074 'cast_streaming_tests': 'isolated_scripts',
1075 'gpu_telemetry_tests': 'isolated_scripts',
Brian Sheedyb6491ba2022-09-26 20:49:491076 # These are the same as existing test types, just configured to run
1077 # in Skylab instead of via normal swarming.
1078 'skylab_gpu_telemetry_tests': 'skylab_tests',
Kenneth Russell8a386d42018-06-02 09:48:011079 }
1080
Jeff Yoon67c3e832020-02-08 07:39:381081 def check_composition_type_test_suites(self, test_type,
1082 additional_validators=None):
1083 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1084 validators = [check_compound_references,
1085 check_basic_references,
1086 check_conflicting_definitions]
1087 if additional_validators:
1088 validators += additional_validators
1089
1090 target_suites = self.test_suites.get(test_type, {})
1091 other_test_type = ('compound_suites'
1092 if test_type == 'matrix_compound_suites'
1093 else 'matrix_compound_suites')
1094 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011095 basic_suites = self.test_suites.get('basic_suites', {})
1096
Jamie Madillcf4f8c72021-05-20 19:24:231097 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011098 if suite in basic_suites:
1099 raise BBGenErr('%s names may not duplicate basic test suite names '
1100 '(error found while processsing %s)'
1101 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011102
Jeff Yoon67c3e832020-02-08 07:39:381103 seen_tests = {}
1104 for sub_suite in suite_def:
1105 for validator in validators:
1106 validator(
1107 basic_suites=basic_suites,
1108 other_test_suites=other_suites,
1109 seen_tests=seen_tests,
1110 sub_suite=sub_suite,
1111 suite=suite,
1112 suite_def=suite_def,
1113 target_test_suites=target_suites,
1114 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051115 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381116 )
Kenneth Russelleb60cbd22017-12-05 07:54:281117
Stephen Martinis54d64ad2018-09-21 22:16:201118 def flatten_test_suites(self):
1119 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011120 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1121 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231122 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011123 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201124 self.test_suites = new_test_suites
1125
Chan Lia3ad1502020-04-28 05:32:111126 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231127 for suite in self.test_suites['basic_suites'].values():
1128 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561129 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411130
1131 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
John Palmera8515fca2021-05-20 03:35:321132 # 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:411133 # TODO(crbug.com/1035124): clean this up.
1134 isolate_name = test.get('test') or test.get('isolate_name') or key
1135 gn_entry = self.gn_isolate_map.get(isolate_name)
1136 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281137 label = gn_entry['label']
1138
1139 if label.count(':') != 1:
1140 raise BBGenErr(
1141 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1142 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1143 (label, isolate_name))
1144 if label.split(':')[1] != isolate_name:
1145 raise BBGenErr(
1146 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1147 ' label "%s" see http://crbug.com/1071091 for details.' %
1148 (isolate_name, label))
1149
Chan Lia3ad1502020-04-28 05:32:111150 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411151 else: # pragma: no cover
1152 # Some tests do not have an entry gn_isolate_map.pyl, such as
1153 # telemetry tests.
1154 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
1155 pass
1156
Kenneth Russelleb60cbd22017-12-05 07:54:281157 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011158 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201159
Jeff Yoon8154e582019-12-03 23:30:011160 compound_suites = self.test_suites.get('compound_suites', {})
1161 # check_composition_type_test_suites() checks that all basic suites
1162 # referenced by compound suites exist.
1163 basic_suites = self.test_suites.get('basic_suites')
1164
Jamie Madillcf4f8c72021-05-20 19:24:231165 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011166 # Resolve this to a dictionary.
1167 full_suite = {}
1168 for entry in value:
1169 suite = basic_suites[entry]
1170 full_suite.update(suite)
1171 compound_suites[name] = full_suite
1172
Jeff Yoon85fb8df2020-08-20 16:47:431173 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381174 """ Merge variant-defined configurations to each test case definition in a
1175 test suite.
1176
1177 The output maps a unique test name to an array of configurations because
1178 there may exist more than one definition for a test name using variants. The
1179 test name is referenced while mapping machines to test suites, so unpacking
1180 the array is done by the generators.
1181
1182 Args:
1183 basic_test_definition: a {} defined test suite in the format
1184 test_name:test_config
1185 variants: an [] of {} defining configurations to be applied to each test
1186 case in the basic test_definition
1187
1188 Return:
1189 a {} of test_name:[{}], where each {} is a merged configuration
1190 """
1191
1192 # Each test in a basic test suite will have a definition per variant.
1193 test_suite = {}
Jamie Madillcf4f8c72021-05-20 19:24:231194 for test_name, test_config in basic_test_definition.items():
Jeff Yoon67c3e832020-02-08 07:39:381195 definitions = []
1196 for variant in variants:
Jeff Yoonda581c32020-03-06 03:56:051197 # Unpack the variant from variants.pyl if it's string based.
1198 if isinstance(variant, str):
1199 variant = self.variants[variant]
1200
Jieting Yangef6b1042021-11-30 21:33:481201 # If 'enabled' is set to False, we will not use this variant;
1202 # otherwise if the variant doesn't include 'enabled' variable or
1203 # 'enabled' is set to True, we will use this variant
1204 if not variant.get('enabled', True):
1205 continue
Jeff Yoon67c3e832020-02-08 07:39:381206 # Clone a copy of test_config so that we can have a uniquely updated
1207 # version of it per variant
1208 cloned_config = copy.deepcopy(test_config)
1209 # The variant definition needs to be re-used for each test, so we'll
1210 # create a clone and work with it as well.
1211 cloned_variant = copy.deepcopy(variant)
1212
1213 cloned_config['args'] = (cloned_config.get('args', []) +
1214 cloned_variant.get('args', []))
1215 cloned_config['mixins'] = (cloned_config.get('mixins', []) +
Jeff Yoon85fb8df2020-08-20 16:47:431216 cloned_variant.get('mixins', []) + mixins)
Jeff Yoon67c3e832020-02-08 07:39:381217
Sven Zhengb51bd0482022-08-26 18:26:251218 description = []
Sven Zhengdcf2ddf2022-08-30 04:24:331219 if cloned_config.get('description'):
1220 description.append(cloned_config.get('description'))
1221 if cloned_variant.get('description'):
1222 description.append(cloned_variant.get('description'))
Sven Zhengb51bd0482022-08-26 18:26:251223 if description:
1224 cloned_config['description'] = '\n'.join(description)
Jeff Yoon67c3e832020-02-08 07:39:381225 basic_swarming_def = cloned_config.get('swarming', {})
1226 variant_swarming_def = cloned_variant.get('swarming', {})
1227 if basic_swarming_def and variant_swarming_def:
1228 if ('dimension_sets' in basic_swarming_def and
1229 'dimension_sets' in variant_swarming_def):
1230 # Retain swarming dimension set merge behavior when both variant and
1231 # the basic test configuration both define it
1232 self.dictionary_merge(basic_swarming_def, variant_swarming_def)
1233 # Remove dimension_sets from the variant definition, so that it does
1234 # not replace what's been done by dictionary_merge in the update
1235 # call below.
1236 del variant_swarming_def['dimension_sets']
1237
1238 # Update the swarming definition with whatever is defined for swarming
1239 # by the variant.
1240 basic_swarming_def.update(variant_swarming_def)
1241 cloned_config['swarming'] = basic_swarming_def
1242
Xinan Lin05fb9c1752020-12-17 00:15:521243 # Copy all skylab fields defined by the variant.
1244 skylab_config = cloned_variant.get('skylab')
1245 if skylab_config:
1246 for k, v in skylab_config.items():
Jieting Yangef6b1042021-11-30 21:33:481247 # cros_chrome_version is the ash chrome version in the cros img
1248 # in the variant of cros_board. We don't want to include it in
1249 # the final json files; so remove it.
1250 if k == 'cros_chrome_version':
1251 continue
Xinan Lin05fb9c1752020-12-17 00:15:521252 cloned_config[k] = v
1253
Jeff Yoon67c3e832020-02-08 07:39:381254 # The identifier is used to make the name of the test unique.
1255 # Generators in the recipe uniquely identify a test by it's name, so we
1256 # don't want to have the same name for each variant.
Ben Pastene5f231cf22022-05-05 18:03:071257 cloned_config['name'] = self.add_variant_to_test_name(
1258 cloned_config.get('name') or test_name,
1259 cloned_variant['identifier'])
1260
1261 # Attach the variant identifier to the test config so downstream
1262 # generators can make modifications based on the original name. This
1263 # is mainly used in generate_gpu_telemetry_test().
1264 cloned_config['variant_id'] = cloned_variant['identifier']
1265
Jeff Yoon67c3e832020-02-08 07:39:381266 definitions.append(cloned_config)
1267 test_suite[test_name] = definitions
1268 return test_suite
1269
Jeff Yoon8154e582019-12-03 23:30:011270 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381271 self.check_composition_type_test_suites('matrix_compound_suites',
1272 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011273
1274 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381275 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011276 # referenced by matrix suites exist.
1277 basic_suites = self.test_suites.get('basic_suites')
1278
Jamie Madillcf4f8c72021-05-20 19:24:231279 for test_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011280 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381281
Jamie Madillcf4f8c72021-05-20 19:24:231282 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381283 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1284
1285 if 'variants' in mtx_test_suite_config:
Jeff Yoon85fb8df2020-08-20 16:47:431286 mixins = mtx_test_suite_config.get('mixins', [])
Jeff Yoon67c3e832020-02-08 07:39:381287 result = self.resolve_variants(basic_test_def,
Jeff Yoon85fb8df2020-08-20 16:47:431288 mtx_test_suite_config['variants'],
1289 mixins)
Jeff Yoon67c3e832020-02-08 07:39:381290 full_suite.update(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271291 else:
1292 suite = basic_suites[test_suite]
1293 full_suite.update(suite)
Jeff Yoon67c3e832020-02-08 07:39:381294 matrix_compound_suites[test_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281295
1296 def link_waterfalls_to_test_suites(self):
1297 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231298 for tester_name, tester in waterfall['machines'].items():
1299 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281300 if not value in self.test_suites:
1301 # Hard / impossible to cover this in the unit test.
1302 raise self.unknown_test_suite(
1303 value, tester_name, waterfall['name']) # pragma: no cover
1304 tester['test_suites'][suite] = self.test_suites[value]
1305
1306 def load_configuration_files(self):
1307 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
1308 self.test_suites = self.load_pyl_file('test_suites.pyl')
1309 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:011310 self.mixins = self.load_pyl_file('mixins.pyl')
Nodir Turakulovfce34292019-12-18 17:05:411311 self.gn_isolate_map = self.load_pyl_file('gn_isolate_map.pyl')
Chong Guee622242020-10-28 18:17:351312 for isolate_map in self.args.isolate_map_files:
1313 isolate_map = self.load_pyl_file(isolate_map)
1314 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1315 if duplicates:
1316 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1317 ', '.join(duplicates))
1318 self.gn_isolate_map.update(isolate_map)
1319
Jeff Yoonda581c32020-03-06 03:56:051320 self.variants = self.load_pyl_file('variants.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:281321
1322 def resolve_configuration_files(self):
Chan Lia3ad1502020-04-28 05:32:111323 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281324 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011325 self.resolve_matrix_compound_test_suites()
1326 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281327 self.link_waterfalls_to_test_suites()
1328
Nico Weberd18b8962018-05-16 19:39:381329 def unknown_bot(self, bot_name, waterfall_name):
1330 return BBGenErr(
1331 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1332
Kenneth Russelleb60cbd22017-12-05 07:54:281333 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1334 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381335 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281336 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1337
1338 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1339 return BBGenErr(
1340 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1341 ' on waterfall ' + waterfall_name)
1342
Stephen Martinisb72f6d22018-10-04 23:29:011343 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071344 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321345
1346 Checks in the waterfall, builder, and test objects for mixins.
1347 """
1348 def valid_mixin(mixin_name):
1349 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011350 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321351 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381352
Stephen Martinisb6a50492018-09-12 23:59:321353 def must_be_list(mixins, typ, name):
1354 """Asserts that given mixins are a list."""
1355 if not isinstance(mixins, list):
1356 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1357
Brian Sheedy7658c982020-01-08 02:27:581358 test_name = test.get('name')
1359 remove_mixins = set()
1360 if 'remove_mixins' in builder:
1361 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1362 for rm in builder['remove_mixins']:
1363 valid_mixin(rm)
1364 remove_mixins.add(rm)
1365 if 'remove_mixins' in test:
1366 must_be_list(test['remove_mixins'], 'test', test_name)
1367 for rm in test['remove_mixins']:
1368 valid_mixin(rm)
1369 remove_mixins.add(rm)
1370 del test['remove_mixins']
1371
Stephen Martinisb72f6d22018-10-04 23:29:011372 if 'mixins' in waterfall:
1373 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1374 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581375 if mixin in remove_mixins:
1376 continue
Stephen Martinisb6a50492018-09-12 23:59:321377 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531378 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321379
Stephen Martinisb72f6d22018-10-04 23:29:011380 if 'mixins' in builder:
1381 must_be_list(builder['mixins'], 'builder', builder_name)
1382 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581383 if mixin in remove_mixins:
1384 continue
Stephen Martinisb6a50492018-09-12 23:59:321385 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531386 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321387
Stephen Martinisb72f6d22018-10-04 23:29:011388 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071389 return test
1390
Stephen Martinis2a0667022018-09-25 22:31:141391 if not test_name:
1392 test_name = test.get('test')
1393 if not test_name: # pragma: no cover
1394 # Not the best name, but we should say something.
1395 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:011396 must_be_list(test['mixins'], 'test', test_name)
1397 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581398 # We don't bother checking if the given mixin is in remove_mixins here
1399 # since this is already the lowest level, so if a mixin is added here that
1400 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071401 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531402 test = self.apply_mixin(self.mixins[mixin], test, builder)
Jeff Yoon67c3e832020-02-08 07:39:381403 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071404 return test
Stephen Martinisb6a50492018-09-12 23:59:321405
Austin Eng148d9f0f2022-02-08 19:18:531406 def apply_mixin(self, mixin, test, builder):
Stephen Martinisb72f6d22018-10-04 23:29:011407 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321408
Stephen Martinis0382bc12018-09-17 22:29:071409 Mixins will not override an existing key. This is to ensure exceptions can
1410 override a setting a mixin applies.
1411
Stephen Martinisb72f6d22018-10-04 23:29:011412 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:321413 'dimension_sets', which is how normal test suites specify their dimensions,
1414 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
1415 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011416
Stephen Martinisb6a50492018-09-12 23:59:321417 """
1418 new_test = copy.deepcopy(test)
1419 mixin = copy.deepcopy(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011420 if 'swarming' in mixin:
1421 swarming_mixin = mixin['swarming']
1422 new_test.setdefault('swarming', {})
Brian Sheedycae63b22020-06-10 22:52:111423 # Copy over any explicit dimension sets first so that they will be updated
1424 # by any subsequent 'dimensions' entries.
1425 if 'dimension_sets' in swarming_mixin:
1426 existing_dimension_sets = new_test['swarming'].setdefault(
1427 'dimension_sets', [])
1428 # Appending to the existing list could potentially result in different
1429 # behavior depending on the order the mixins were applied, but that's
1430 # already the case for other parts of mixins, so trust that the user
1431 # will verify that the generated output is correct before submitting.
1432 for dimension_set in swarming_mixin['dimension_sets']:
1433 if dimension_set not in existing_dimension_sets:
1434 existing_dimension_sets.append(dimension_set)
1435 del swarming_mixin['dimension_sets']
Stephen Martinisb72f6d22018-10-04 23:29:011436 if 'dimensions' in swarming_mixin:
1437 new_test['swarming'].setdefault('dimension_sets', [{}])
1438 for dimension_set in new_test['swarming']['dimension_sets']:
1439 dimension_set.update(swarming_mixin['dimensions'])
1440 del swarming_mixin['dimensions']
Stephen Martinisb72f6d22018-10-04 23:29:011441 # python dict update doesn't do recursion at all. Just hard code the
1442 # nested update we need (mixin['swarming'] shouldn't clobber
1443 # test['swarming'], but should update it).
1444 new_test['swarming'].update(swarming_mixin)
1445 del mixin['swarming']
1446
Wezc0e835b702018-10-30 00:38:411447 if '$mixin_append' in mixin:
1448 # Values specified under $mixin_append should be appended to existing
1449 # lists, rather than replacing them.
1450 mixin_append = mixin['$mixin_append']
Austin Eng148d9f0f2022-02-08 19:18:531451 del mixin['$mixin_append']
Zhaoyang Li473dd0ae2021-05-10 18:28:281452
1453 # Append swarming named cache and delete swarming key, since it's under
1454 # another layer of dict.
1455 if 'named_caches' in mixin_append.get('swarming', {}):
1456 new_test['swarming'].setdefault('named_caches', [])
1457 new_test['swarming']['named_caches'].extend(
1458 mixin_append['swarming']['named_caches'])
1459 if len(mixin_append['swarming']) > 1:
1460 raise BBGenErr('Only named_caches is supported under swarming key in '
1461 '$mixin_append, but there are: %s' %
1462 sorted(mixin_append['swarming'].keys()))
1463 del mixin_append['swarming']
Wezc0e835b702018-10-30 00:38:411464 for key in mixin_append:
1465 new_test.setdefault(key, [])
1466 if not isinstance(mixin_append[key], list):
1467 raise BBGenErr(
1468 'Key "' + key + '" in $mixin_append must be a list.')
1469 if not isinstance(new_test[key], list):
1470 raise BBGenErr(
1471 'Cannot apply $mixin_append to non-list "' + key + '".')
1472 new_test[key].extend(mixin_append[key])
Austin Eng148d9f0f2022-02-08 19:18:531473
1474 args = new_test.get('args', [])
1475 # Array so we can assign to it in a nested scope.
1476 args_need_fixup = [False]
Wezc0e835b702018-10-30 00:38:411477 if 'args' in mixin_append:
Austin Eng148d9f0f2022-02-08 19:18:531478 args_need_fixup[0] = True
1479
1480 def add_conditional_args(key, fn):
1481 val = new_test.pop(key, [])
1482 if val and fn(builder):
1483 args.extend(val)
1484 args_need_fixup[0] = True
1485
1486 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
1487 add_conditional_args('lacros_args', self.is_lacros)
1488 add_conditional_args('linux_args', self.is_linux)
1489 add_conditional_args('android_args', self.is_android)
1490 add_conditional_args('chromeos_args', self.is_chromeos)
1491 add_conditional_args('mac_args', self.is_mac)
1492 add_conditional_args('win_args', self.is_win)
1493 add_conditional_args('win64_args', self.is_win64)
1494
1495 if args_need_fixup[0]:
1496 new_test['args'] = self.maybe_fixup_args_array(args)
Wezc0e835b702018-10-30 00:38:411497
Stephen Martinisb72f6d22018-10-04 23:29:011498 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321499 return new_test
1500
Greg Gutermanf60eb052020-03-12 17:40:011501 def generate_output_tests(self, waterfall):
1502 """Generates the tests for a waterfall.
1503
1504 Args:
1505 waterfall: a dictionary parsed from a master pyl file
1506 Returns:
1507 A dictionary mapping builders to test specs
1508 """
1509 return {
Jamie Madillcf4f8c72021-05-20 19:24:231510 name: self.get_tests_for_config(waterfall, name, config)
1511 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011512 }
1513
1514 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531515 generator_map = self.get_test_generator_map()
1516 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281517
Greg Gutermanf60eb052020-03-12 17:40:011518 tests = {}
1519 # Copy only well-understood entries in the machine's configuration
1520 # verbatim into the generated JSON.
1521 if 'additional_compile_targets' in config:
1522 tests['additional_compile_targets'] = config[
1523 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231524 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011525 if test_type not in generator_map:
1526 raise self.unknown_test_suite_type(
1527 test_type, name, waterfall['name']) # pragma: no cover
1528 test_generator = generator_map[test_type]
1529 # Let multiple kinds of generators generate the same kinds
1530 # of tests. For example, gpu_telemetry_tests are a
1531 # specialization of isolated_scripts.
1532 new_tests = test_generator.generate(
1533 waterfall, name, config, input_tests)
1534 remapped_test_type = test_type_remapper.get(test_type, test_type)
1535 tests[remapped_test_type] = test_generator.sort(
1536 tests.get(remapped_test_type, []) + new_tests)
1537
1538 return tests
1539
1540 def jsonify(self, all_tests):
1541 return json.dumps(
1542 all_tests, indent=2, separators=(',', ': '),
1543 sort_keys=True) + '\n'
1544
1545 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281546 self.load_configuration_files()
1547 self.resolve_configuration_files()
1548 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011549 result = collections.defaultdict(dict)
1550
Dirk Pranke6269d302020-10-01 00:14:391551 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011552 for waterfall in self.waterfalls:
1553 for field in required_fields:
1554 # Verify required fields
1555 if field not in waterfall:
1556 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1557
1558 # Handle filter flag, if specified
1559 if filters and waterfall['name'] not in filters:
1560 continue
1561
1562 # Join config files and hardcoded values together
1563 all_tests = self.generate_output_tests(waterfall)
1564 result[waterfall['name']] = all_tests
1565
Greg Gutermanf60eb052020-03-12 17:40:011566 # Add do not edit warning
1567 for tests in result.values():
1568 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1569 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1570
1571 return result
1572
1573 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281574 suffix = '.json'
1575 if self.args.new_files:
1576 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011577
1578 for filename, contents in result.items():
1579 jsonstr = self.jsonify(contents)
1580 self.write_file(self.pyl_file_path(filename + suffix), jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281581
Nico Weberd18b8962018-05-16 19:39:381582 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161583 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421584 # NOTE: This reference can cause issues; if a file changes there, the
1585 # presubmit here won't be run by default. A manually maintained list there
1586 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1587 # references to configs outside of this directory are added, please change
1588 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1589 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491590
Garrett Beaty7e866fc2021-06-16 14:12:101591 # Get the generated project.pyl so we can check if we should be enforcing
1592 # that the specs are for builders that actually exist
1593 # If not, return None to indicate that we won't enforce that builders in
1594 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491595 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1596 'project.pyl')
1597 if os.path.exists(project_pyl_path):
1598 settings = ast.literal_eval(self.read_file(project_pyl_path))
1599 if not settings.get('validate_source_side_specs_have_builder', True):
1600 return None
1601
Nico Weberd18b8962018-05-16 19:39:381602 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311603 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161604 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1605 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431606 for c in milo_configs:
1607 for l in self.read_file(c).splitlines():
1608 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311609 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431610 continue
1611 # l looks like
1612 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1613 # Extract win_chromium_dbg_ng part.
1614 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381615 return bot_names
1616
Ben Pastene9a010082019-09-25 20:41:371617 def get_internal_waterfalls(self):
1618 # Similar to get_builders_that_do_not_actually_exist above, but for
1619 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201620 return [
1621 'chrome', 'chrome.pgo', 'internal.chrome.fyi', 'internal.chromeos.fyi',
1622 'internal.soda'
1623 ]
Ben Pastene9a010082019-09-25 20:41:371624
Stephen Martinisf83893722018-09-19 00:02:181625 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201626 self.check_input_files_sorting(verbose)
1627
Kenneth Russelleb60cbd22017-12-05 07:54:281628 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011629 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381630 self.check_composition_type_test_suites('matrix_compound_suites',
1631 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111632 self.resolve_test_id_prefixes()
Stephen Martinis54d64ad2018-09-21 22:16:201633 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381634
1635 # All bots should exist.
1636 bot_names = self.get_valid_bot_names()
Garrett Beaty2a02de3c2020-05-15 13:57:351637 if bot_names is not None:
1638 internal_waterfalls = self.get_internal_waterfalls()
1639 for waterfall in self.waterfalls:
1640 # TODO(crbug.com/991417): Remove the need for this exception.
1641 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011642 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351643 for bot_name in waterfall['machines']:
Garrett Beaty2a02de3c2020-05-15 13:57:351644 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581645 if waterfall['name'] in [
1646 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1647 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351648 # TODO(thakis): Remove this once these bots move to luci.
1649 continue # pragma: no cover
1650 if waterfall['name'] in ['tryserver.webrtc',
1651 'webrtc.chromium.fyi.experimental']:
1652 # These waterfalls have their bot configs in a different repo.
1653 # so we don't know about their bot names.
1654 continue # pragma: no cover
1655 if waterfall['name'] in ['client.devtools-frontend.integration',
1656 'tryserver.devtools-frontend',
1657 'chromium.devtools-frontend']:
1658 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201659 if waterfall['name'] in ['client.openscreen.chromium']:
1660 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351661 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381662
Kenneth Russelleb60cbd22017-12-05 07:54:281663 # All test suites must be referenced.
1664 suites_seen = set()
1665 generator_map = self.get_test_generator_map()
1666 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231667 for bot_name, tester in waterfall['machines'].items():
1668 for suite_type, suite in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281669 if suite_type not in generator_map:
1670 raise self.unknown_test_suite_type(suite_type, bot_name,
1671 waterfall['name'])
1672 if suite not in self.test_suites:
1673 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1674 suites_seen.add(suite)
1675 # Since we didn't resolve the configuration files, this set
1676 # includes both composition test suites and regular ones.
1677 resolved_suites = set()
1678 for suite_name in suites_seen:
1679 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011680 for sub_suite in suite:
1681 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281682 resolved_suites.add(suite_name)
1683 # At this point, every key in test_suites.pyl should be referenced.
1684 missing_suites = set(self.test_suites.keys()) - resolved_suites
1685 if missing_suites:
1686 raise BBGenErr('The following test suites were unreferenced by bots on '
1687 'the waterfalls: ' + str(missing_suites))
1688
1689 # All test suite exceptions must refer to bots on the waterfall.
1690 all_bots = set()
1691 missing_bots = set()
1692 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231693 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281694 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281695 # In order to disambiguate between bots with the same name on
1696 # different waterfalls, support has been added to various
1697 # exceptions for concatenating the waterfall name after the bot
1698 # name.
1699 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231700 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381701 removals = (exception.get('remove_from', []) +
1702 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231703 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381704 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281705 if removal not in all_bots:
1706 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411707
Kenneth Russelleb60cbd22017-12-05 07:54:281708 if missing_bots:
1709 raise BBGenErr('The following nonexistent machines were referenced in '
1710 'the test suite exceptions: ' + str(missing_bots))
1711
Stephen Martinis0382bc12018-09-17 22:29:071712 # All mixins must be referenced
1713 seen_mixins = set()
1714 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011715 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231716 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011717 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071718 for suite in self.test_suites.values():
1719 if isinstance(suite, list):
1720 # Don't care about this, it's a composition, which shouldn't include a
1721 # swarming mixin.
1722 continue
1723
1724 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561725 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011726 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071727
Zhaoyang Li9da047d52021-05-10 21:31:441728 for variant in self.variants:
1729 # Unpack the variant from variants.pyl if it's string based.
1730 if isinstance(variant, str):
1731 variant = self.variants[variant]
1732 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1733
Stephen Martinisb72f6d22018-10-04 23:29:011734 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071735 if missing_mixins:
1736 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1737 ' referenced in a waterfall, machine, or test suite.' % (
1738 str(missing_mixins)))
1739
Jeff Yoonda581c32020-03-06 03:56:051740 # All variant references must be referenced
1741 seen_variants = set()
1742 for suite in self.test_suites.values():
1743 if isinstance(suite, list):
1744 continue
1745
1746 for test in suite.values():
1747 if isinstance(test, dict):
1748 for variant in test.get('variants', []):
1749 if isinstance(variant, str):
1750 seen_variants.add(variant)
1751
1752 missing_variants = set(self.variants.keys()) - seen_variants
1753 if missing_variants:
1754 raise BBGenErr('The following variants were unreferenced: %s. They must '
1755 'be referenced in a matrix test suite under the variants '
1756 'key.' % str(missing_variants))
1757
Stephen Martinis54d64ad2018-09-21 22:16:201758
1759 def type_assert(self, node, typ, filename, verbose=False):
1760 """Asserts that the Python AST node |node| is of type |typ|.
1761
1762 If verbose is set, it prints out some helpful context lines, showing where
1763 exactly the error occurred in the file.
1764 """
1765 if not isinstance(node, typ):
1766 if verbose:
1767 lines = [""] + self.read_file(filename).splitlines()
1768
1769 context = 2
1770 lines_start = max(node.lineno - context, 0)
1771 # Add one to include the last line
1772 lines_end = min(node.lineno + context, len(lines)) + 1
1773 lines = (
1774 ['== %s ==\n' % filename] +
1775 ["<snip>\n"] +
1776 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1777 lines[lines_start:lines_start + context])] +
1778 ['-' * 80 + '\n'] +
1779 ['%d %s' % (node.lineno, lines[node.lineno])] +
1780 ['-' * (node.col_offset + 3) + '^' + '-' * (
1781 80 - node.col_offset - 4) + '\n'] +
1782 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1783 lines[node.lineno + 1:lines_end])] +
1784 ["<snip>\n"]
1785 )
1786 # Print out a useful message when a type assertion fails.
1787 for l in lines:
1788 self.print_line(l.strip())
1789
1790 node_dumped = ast.dump(node, annotate_fields=False)
1791 # If the node is huge, truncate it so everything fits in a terminal
1792 # window.
1793 if len(node_dumped) > 60: # pragma: no cover
1794 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1795 raise BBGenErr(
1796 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1797 ' be %s, is %s' % (
1798 filename, node_dumped,
1799 node.lineno, typ, type(node)))
1800
Stephen Martinis5bef0fc2020-01-06 22:47:531801 def check_ast_list_formatted(self, keys, filename, verbose,
Stephen Martinis1384ff92020-01-07 19:52:151802 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531803 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201804
Stephen Martinis5bef0fc2020-01-06 22:47:531805 Currently only checks to ensure they're correctly sorted, and that there
1806 are no duplicates.
1807
1808 Args:
1809 keys: An python list of AST nodes.
1810
1811 It's a list of AST nodes instead of a list of strings because
1812 when verbose is set, it tries to print out context of where the
1813 diffs are in the file.
1814 filename: The name of the file this node is from.
1815 verbose: If set, print out diff information about how the keys are
1816 incorrectly formatted.
1817 check_sorting: If true, checks if the list is sorted.
1818 Returns:
1819 If the keys are correctly formatted.
1820 """
1821 if not keys:
1822 return True
1823
1824 assert isinstance(keys[0], ast.Str)
1825
1826 keys_strs = [k.s for k in keys]
1827 # Keys to diff against. Used below.
1828 keys_to_diff_against = None
1829 # If the list is properly formatted.
1830 list_formatted = True
1831
1832 # Duplicates are always bad.
1833 if len(set(keys_strs)) != len(keys_strs):
1834 list_formatted = False
1835 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1836
1837 if check_sorting and sorted(keys_strs) != keys_strs:
1838 list_formatted = False
1839 if list_formatted:
1840 return True
1841
1842 if verbose:
1843 line_num = keys[0].lineno
1844 keys = [k.s for k in keys]
1845 if check_sorting:
1846 # If we have duplicates, sorting this will take care of it anyways.
1847 keys_to_diff_against = sorted(set(keys))
1848 # else, keys_to_diff_against is set above already
1849
1850 self.print_line('=' * 80)
1851 self.print_line('(First line of keys is %s)' % line_num)
1852 for line in difflib.context_diff(
1853 keys, keys_to_diff_against,
1854 fromfile='current (%r)' % filename, tofile='sorted', lineterm=''):
1855 self.print_line(line)
1856 self.print_line('=' * 80)
1857
1858 return False
1859
Stephen Martinis1384ff92020-01-07 19:52:151860 def check_ast_dict_formatted(self, node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531861 """Checks if an ast dictionary's keys are correctly formatted.
1862
1863 Just a simple wrapper around check_ast_list_formatted.
1864 Args:
1865 node: An AST node. Assumed to be a dictionary.
1866 filename: The name of the file this node is from.
1867 verbose: If set, print out diff information about how the keys are
1868 incorrectly formatted.
1869 check_sorting: If true, checks if the list is sorted.
1870 Returns:
1871 If the dictionary is correctly formatted.
1872 """
Stephen Martinis54d64ad2018-09-21 22:16:201873 keys = []
1874 # The keys of this dict are ordered as ordered in the file; normal python
1875 # dictionary keys are given an arbitrary order, but since we parsed the
1876 # file itself, the order as given in the file is preserved.
1877 for key in node.keys:
1878 self.type_assert(key, ast.Str, filename, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531879 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201880
Stephen Martinis1384ff92020-01-07 19:52:151881 return self.check_ast_list_formatted(keys, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181882
1883 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201884 # TODO(https://crbug.com/886993): Add the ability for this script to
1885 # actually format the files, rather than just complain if they're
1886 # incorrectly formatted.
1887 bad_files = set()
Stephen Martinis5bef0fc2020-01-06 22:47:531888 def parse_file(filename):
1889 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201890
Stephen Martinis5bef0fc2020-01-06 22:47:531891 Returns an AST node representing the value in the pyl file."""
Stephen Martinisf83893722018-09-19 00:02:181892 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1893
Stephen Martinisf83893722018-09-19 00:02:181894 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201895 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181896 module = parsed.body
1897
1898 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201899 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181900 if len(module) != 1: # pragma: no cover
1901 raise BBGenErr('Invalid .pyl file %s' % filename)
1902 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201903 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181904
Stephen Martinis5bef0fc2020-01-06 22:47:531905 return expr.value
1906
1907 # Handle this separately
1908 filename = 'waterfalls.pyl'
1909 value = parse_file(filename)
1910 # Value should be a list.
1911 self.type_assert(value, ast.List, filename, verbose)
1912
1913 keys = []
Joshua Hood56c673c2022-03-02 20:29:331914 for elm in value.elts:
1915 self.type_assert(elm, ast.Dict, filename, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531916 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331917 for key, val in zip(elm.keys, elm.values):
Stephen Martinis5bef0fc2020-01-06 22:47:531918 self.type_assert(key, ast.Str, filename, verbose)
1919 if key.s == 'machines':
1920 if not self.check_ast_dict_formatted(val, filename, verbose):
1921 bad_files.add(filename)
1922
1923 if key.s == "name":
1924 self.type_assert(val, ast.Str, filename, verbose)
1925 waterfall_name = val
1926 assert waterfall_name
1927 keys.append(waterfall_name)
1928
Stephen Martinis1384ff92020-01-07 19:52:151929 if not self.check_ast_list_formatted(keys, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531930 bad_files.add(filename)
1931
1932 for filename in (
1933 'mixins.pyl',
1934 'test_suites.pyl',
1935 'test_suite_exceptions.pyl',
1936 ):
1937 value = parse_file(filename)
Stephen Martinisf83893722018-09-19 00:02:181938 # Value should be a dictionary.
Stephen Martinis54d64ad2018-09-21 22:16:201939 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181940
Stephen Martinis5bef0fc2020-01-06 22:47:531941 if not self.check_ast_dict_formatted(
1942 value, filename, verbose):
1943 bad_files.add(filename)
1944
Stephen Martinis54d64ad2018-09-21 22:16:201945 if filename == 'test_suites.pyl':
Jeff Yoon8154e582019-12-03 23:30:011946 expected_keys = ['basic_suites',
1947 'compound_suites',
1948 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201949 actual_keys = [node.s for node in value.keys]
1950 assert all(key in expected_keys for key in actual_keys), (
1951 'Invalid %r file; expected keys %r, got %r' % (
1952 filename, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:331953 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:201954 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011955 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201956 for suite_group in suite_dicts:
Stephen Martinis5bef0fc2020-01-06 22:47:531957 if not self.check_ast_dict_formatted(
Stephen Martinis54d64ad2018-09-21 22:16:201958 suite_group, filename, verbose):
1959 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181960
Stephen Martinis5bef0fc2020-01-06 22:47:531961 for key, suite in zip(value.keys, value.values):
1962 # The compound suites are checked in
1963 # 'check_composition_type_test_suites()'
1964 if key.s == 'basic_suites':
1965 for group in suite.values:
Stephen Martinis1384ff92020-01-07 19:52:151966 if not self.check_ast_dict_formatted(group, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531967 bad_files.add(filename)
1968 break
Stephen Martinis54d64ad2018-09-21 22:16:201969
Stephen Martinis5bef0fc2020-01-06 22:47:531970 elif filename == 'test_suite_exceptions.pyl':
1971 # Check the values for each test.
1972 for test in value.values:
1973 for kind, node in zip(test.keys, test.values):
1974 if isinstance(node, ast.Dict):
Stephen Martinis1384ff92020-01-07 19:52:151975 if not self.check_ast_dict_formatted(node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531976 bad_files.add(filename)
1977 elif kind.s == 'remove_from':
1978 # Don't care about sorting; these are usually grouped, since the
1979 # same bug can affect multiple builders. Do want to make sure
1980 # there aren't duplicates.
1981 if not self.check_ast_list_formatted(node.elts, filename, verbose,
1982 check_sorting=False):
1983 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181984
1985 if bad_files:
1986 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201987 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531988 'unsorted, or have duplicates. Re-run this with --verbose to see '
1989 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181990
Kenneth Russelleb60cbd22017-12-05 07:54:281991 def check_output_file_consistency(self, verbose=False):
1992 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011993 # All waterfalls/bucket .json files must have been written
1994 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:281995 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011996 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:161997 outputs = self.generate_outputs()
1998 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:011999 expected = self.jsonify(expected_contents)
2000 file_path = filename + '.json'
Zhiling Huangbe008172018-03-08 19:13:112001 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:282002 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:012003 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:322004 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:012005 self.print_line('File ' + filename +
2006 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:322007 'contents:')
2008 for line in difflib.unified_diff(
2009 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:502010 current.splitlines(),
2011 fromfile='expected', tofile='current'):
2012 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:012013
2014 if ungenerated_files:
2015 raise BBGenErr(
2016 'The following files have not been properly '
2017 'autogenerated by generate_buildbot_json.py: ' +
2018 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:282019
Dirk Pranke772f55f2021-04-28 04:51:162020 for builder_group, builders in outputs.items():
2021 for builder, step_types in builders.items():
2022 for step_data in step_types.get('gtest_tests', []):
2023 step_name = step_data.get('name', step_data['test'])
2024 self._check_swarming_config(builder_group, builder, step_name,
2025 step_data)
2026 for step_data in step_types.get('isolated_scripts', []):
2027 step_name = step_data.get('name', step_data['isolate_name'])
2028 self._check_swarming_config(builder_group, builder, step_name,
2029 step_data)
2030
2031 def _check_swarming_config(self, filename, builder, step_name, step_data):
2032 # TODO(crbug.com/1203436): Ensure all swarming tests specify os and cpu, not
2033 # just mac tests.
2034 if ('mac' in builder.lower()
2035 and step_data['swarming']['can_use_on_swarming_builders']):
2036 dimension_sets = step_data['swarming'].get('dimension_sets')
2037 if not dimension_sets:
2038 raise BBGenErr('%s: %s / %s : os and cpu must be specified for mac '
2039 'swarmed tests' % (filename, builder, step_name))
2040 for s in dimension_sets:
2041 if not s.get('os') or not s.get('cpu'):
2042 raise BBGenErr('%s: %s / %s : os and cpu must be specified for mac '
2043 'swarmed tests' % (filename, builder, step_name))
2044
Kenneth Russelleb60cbd22017-12-05 07:54:282045 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502046 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282047 self.check_output_file_consistency(verbose) # pragma: no cover
2048
Karen Qiane24b7ee2019-02-12 23:37:062049 def does_test_match(self, test_info, params_dict):
2050 """Checks to see if the test matches the parameters given.
2051
2052 Compares the provided test_info with the params_dict to see
2053 if the bot matches the parameters given. If so, returns True.
2054 Else, returns false.
2055
2056 Args:
2057 test_info (dict): Information about a specific bot provided
2058 in the format shown in waterfalls.pyl
2059 params_dict (dict): Dictionary of parameters and their values
2060 to look for in the bot
2061 Ex: {
2062 'device_os':'android',
2063 '--flag':True,
2064 'mixins': ['mixin1', 'mixin2'],
2065 'ex_key':'ex_value'
2066 }
2067
2068 """
2069 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2070 'kvm', 'pool', 'integrity'] # dimension parameters
2071 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2072 'can_use_on_swarming_builders']
2073 for param in params_dict:
2074 # if dimension parameter
2075 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2076 if not 'swarming' in test_info:
2077 return False
2078 swarming = test_info['swarming']
2079 if param in SWARMING_PARAMS:
2080 if not param in swarming:
2081 return False
2082 if not str(swarming[param]) == params_dict[param]:
2083 return False
2084 else:
2085 if not 'dimension_sets' in swarming:
2086 return False
2087 d_set = swarming['dimension_sets']
2088 # only looking at the first dimension set
2089 if not param in d_set[0]:
2090 return False
2091 if not d_set[0][param] == params_dict[param]:
2092 return False
2093
2094 # if flag
2095 elif param.startswith('--'):
2096 if not 'args' in test_info:
2097 return False
2098 if not param in test_info['args']:
2099 return False
2100
2101 # not dimension parameter/flag/mixin
2102 else:
2103 if not param in test_info:
2104 return False
2105 if not test_info[param] == params_dict[param]:
2106 return False
2107 return True
2108 def error_msg(self, msg):
2109 """Prints an error message.
2110
2111 In addition to a catered error message, also prints
2112 out where the user can find more help. Then, program exits.
2113 """
2114 self.print_line(msg + (' If you need more information, ' +
2115 'please run with -h or --help to see valid commands.'))
2116 sys.exit(1)
2117
2118 def find_bots_that_run_test(self, test, bots):
2119 matching_bots = []
2120 for bot in bots:
2121 bot_info = bots[bot]
2122 tests = self.flatten_tests_for_bot(bot_info)
2123 for test_info in tests:
2124 test_name = ""
2125 if 'name' in test_info:
2126 test_name = test_info['name']
2127 elif 'test' in test_info:
2128 test_name = test_info['test']
2129 if not test_name == test:
2130 continue
2131 matching_bots.append(bot)
2132 return matching_bots
2133
2134 def find_tests_with_params(self, tests, params_dict):
2135 matching_tests = []
2136 for test_name in tests:
2137 test_info = tests[test_name]
2138 if not self.does_test_match(test_info, params_dict):
2139 continue
2140 if not test_name in matching_tests:
2141 matching_tests.append(test_name)
2142 return matching_tests
2143
2144 def flatten_waterfalls_for_query(self, waterfalls):
2145 bots = {}
2146 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012147 waterfall_tests = self.generate_output_tests(waterfall)
2148 for bot in waterfall_tests:
2149 bot_info = waterfall_tests[bot]
2150 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062151 return bots
2152
2153 def flatten_tests_for_bot(self, bot_info):
2154 """Returns a list of flattened tests.
2155
2156 Returns a list of tests not grouped by test category
2157 for a specific bot.
2158 """
2159 TEST_CATS = self.get_test_generator_map().keys()
2160 tests = []
2161 for test_cat in TEST_CATS:
2162 if not test_cat in bot_info:
2163 continue
2164 test_cat_tests = bot_info[test_cat]
2165 tests = tests + test_cat_tests
2166 return tests
2167
2168 def flatten_tests_for_query(self, test_suites):
2169 """Returns a flattened dictionary of tests.
2170
2171 Returns a dictionary of tests associate with their
2172 configuration, not grouped by their test suite.
2173 """
2174 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232175 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062176 for test in test_suite:
2177 test_info = test_suite[test]
2178 test_name = test
2179 if 'name' in test_info:
2180 test_name = test_info['name']
2181 tests[test_name] = test_info
2182 return tests
2183
2184 def parse_query_filter_params(self, params):
2185 """Parses the filter parameters.
2186
2187 Creates a dictionary from the parameters provided
2188 to filter the bot array.
2189 """
2190 params_dict = {}
2191 for p in params:
2192 # flag
2193 if p.startswith("--"):
2194 params_dict[p] = True
2195 else:
2196 pair = p.split(":")
2197 if len(pair) != 2:
2198 self.error_msg('Invalid command.')
2199 # regular parameters
2200 if pair[1].lower() == "true":
2201 params_dict[pair[0]] = True
2202 elif pair[1].lower() == "false":
2203 params_dict[pair[0]] = False
2204 else:
2205 params_dict[pair[0]] = pair[1]
2206 return params_dict
2207
2208 def get_test_suites_dict(self, bots):
2209 """Returns a dictionary of bots and their tests.
2210
2211 Returns a dictionary of bots and a list of their associated tests.
2212 """
2213 test_suite_dict = dict()
2214 for bot in bots:
2215 bot_info = bots[bot]
2216 tests = self.flatten_tests_for_bot(bot_info)
2217 test_suite_dict[bot] = tests
2218 return test_suite_dict
2219
2220 def output_query_result(self, result, json_file=None):
2221 """Outputs the result of the query.
2222
2223 If a json file parameter name is provided, then
2224 the result is output into the json file. If not,
2225 then the result is printed to the console.
2226 """
2227 output = json.dumps(result, indent=2)
2228 if json_file:
2229 self.write_file(json_file, output)
2230 else:
2231 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062232
Joshua Hood56c673c2022-03-02 20:29:332233 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062234 def query(self, args):
2235 """Queries tests or bots.
2236
2237 Depending on the arguments provided, outputs a json of
2238 tests or bots matching the appropriate optional parameters provided.
2239 """
2240 # split up query statement
2241 query = args.query.split('/')
2242 self.load_configuration_files()
2243 self.resolve_configuration_files()
2244
2245 # flatten bots json
2246 tests = self.test_suites
2247 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2248
2249 cmd_class = query[0]
2250
2251 # For queries starting with 'bots'
2252 if cmd_class == "bots":
2253 if len(query) == 1:
2254 return self.output_query_result(bots, args.json)
2255 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332256 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062257 if query[1] == 'tests':
2258 test_suites_dict = self.get_test_suites_dict(bots)
2259 return self.output_query_result(test_suites_dict, args.json)
Joshua Hood56c673c2022-03-02 20:29:332260 self.error_msg("This query should be in the format: bots/tests.")
Karen Qiane24b7ee2019-02-12 23:37:062261
2262 else:
2263 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2264 % str(len(query)-1))
2265
2266 # For queries starting with 'bot'
2267 elif cmd_class == "bot":
2268 if not len(query) == 2 and not len(query) == 3:
2269 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2270 % str(len(query)-1))
2271 bot_id = query[1]
2272 if not bot_id in bots:
2273 self.error_msg("No bot named '" + bot_id + "' found.")
2274 bot_info = bots[bot_id]
2275 if len(query) == 2:
2276 return self.output_query_result(bot_info, args.json)
2277 if not query[2] == 'tests':
2278 self.error_msg("The query should be in the format:" +
2279 "bot/<bot-name>/tests.")
2280
2281 bot_tests = self.flatten_tests_for_bot(bot_info)
2282 return self.output_query_result(bot_tests, args.json)
2283
2284 # For queries starting with 'tests'
2285 elif cmd_class == "tests":
2286 if not len(query) == 1 and not len(query) == 2:
2287 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2288 % str(len(query)-1))
2289 flattened_tests = self.flatten_tests_for_query(tests)
2290 if len(query) == 1:
2291 return self.output_query_result(flattened_tests, args.json)
2292
2293 # create params dict
2294 params = query[1].split('&')
2295 params_dict = self.parse_query_filter_params(params)
2296 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2297 return self.output_query_result(matching_bots)
2298
2299 # For queries starting with 'test'
2300 elif cmd_class == "test":
2301 if not len(query) == 2 and not len(query) == 3:
2302 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2303 % str(len(query)-1))
2304 test_id = query[1]
2305 if len(query) == 2:
2306 flattened_tests = self.flatten_tests_for_query(tests)
2307 for test in flattened_tests:
2308 if test == test_id:
2309 return self.output_query_result(flattened_tests[test], args.json)
2310 self.error_msg("There is no test named %s." % test_id)
2311 if not query[2] == 'bots':
2312 self.error_msg("The query should be in the format: " +
2313 "test/<test-name>/bots")
2314 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2315 return self.output_query_result(bots_for_test)
2316
2317 else:
2318 self.error_msg("Your command did not match any valid commands." +
2319 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Joshua Hood56c673c2022-03-02 20:29:332320 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282321
Garrett Beaty1afaccc2020-06-25 19:58:152322 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282323 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502324 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062325 elif self.args.query:
2326 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282327 else:
Greg Gutermanf60eb052020-03-12 17:40:012328 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282329 return 0
2330
2331if __name__ == "__main__": # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152332 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2333 sys.exit(generator.main())