blob: 23190b7e90ca3ef1660f63eca39f8ce023232da3 [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
Garrett Beatyd5ca75962020-05-07 16:58:3115import glob
Kenneth Russell8ceeabf2017-12-11 17:53:2816import itertools
Kenneth Russelleb60cbd22017-12-05 07:54:2817import json
18import os
Kenneth Russelleb60cbd22017-12-05 07:54:2819import string
20import sys
21
Brian Sheedya31578e2020-05-18 20:24:3622import buildbot_json_magic_substitutions as magic_substitutions
23
Joshua Hood56c673c2022-03-02 20:29:3324# pylint: disable=super-with-arguments,useless-super-delegation
25
Kenneth Russelleb60cbd22017-12-05 07:54:2826THIS_DIR = os.path.dirname(os.path.abspath(__file__))
27
Brian Sheedyf74819b2021-06-04 01:38:3828BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP = {
29 'android-chromium': '_android_chrome',
30 'android-chromium-monochrome': '_android_monochrome',
Brian Sheedyf74819b2021-06-04 01:38:3831 'android-webview': '_android_webview',
32}
33
Kenneth Russelleb60cbd22017-12-05 07:54:2834
35class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4936 def __init__(self, message):
37 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2838
39
Joshua Hood56c673c2022-03-02 20:29:3340class BaseGenerator(object): # pylint: disable=useless-object-inheritance
Kenneth Russelleb60cbd22017-12-05 07:54:2841 def __init__(self, bb_gen):
42 self.bb_gen = bb_gen
43
Kenneth Russell8ceeabf2017-12-11 17:53:2844 def generate(self, waterfall, tester_name, tester_config, input_tests):
Garrett Beatyffe83c4f2023-09-08 19:07:3745 raise NotImplementedError() # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:2846
47
Kenneth Russell8a386d42018-06-02 09:48:0148class GPUTelemetryTestGenerator(BaseGenerator):
Xinan Linedcf05b32023-10-19 23:13:5049 def __init__(self,
50 bb_gen,
51 is_android_webview=False,
52 is_cast_streaming=False,
53 is_skylab=False):
Kenneth Russell8a386d42018-06-02 09:48:0154 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5655 self._is_android_webview = is_android_webview
Fabrice de Ganscbd655f2022-08-04 20:15:3056 self._is_cast_streaming = is_cast_streaming
Xinan Linedcf05b32023-10-19 23:13:5057 self._is_skylab = is_skylab
Kenneth Russell8a386d42018-06-02 09:48:0158
59 def generate(self, waterfall, tester_name, tester_config, input_tests):
60 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:2361 for test_name, test_config in sorted(input_tests.items()):
Ben Pastene8e7eb2652022-04-29 19:44:3162 # Variants allow more than one definition for a given test, and is defined
63 # in array format from resolve_variants().
64 if not isinstance(test_config, list):
65 test_config = [test_config]
66
67 for config in test_config:
Xinan Linedcf05b32023-10-19 23:13:5068 test = self.bb_gen.generate_gpu_telemetry_test(
69 waterfall, tester_name, tester_config, test_name, config,
70 self._is_android_webview, self._is_cast_streaming, self._is_skylab)
Ben Pastene8e7eb2652022-04-29 19:44:3171 if test:
72 isolated_scripts.append(test)
73
Kenneth Russell8a386d42018-06-02 09:48:0174 return isolated_scripts
75
Kenneth Russell8a386d42018-06-02 09:48:0176
Brian Sheedyb6491ba2022-09-26 20:49:4977class SkylabGPUTelemetryTestGenerator(GPUTelemetryTestGenerator):
Xinan Linedcf05b32023-10-19 23:13:5078 def __init__(self, bb_gen):
79 super(SkylabGPUTelemetryTestGenerator, self).__init__(bb_gen,
80 is_skylab=True)
81
Brian Sheedyb6491ba2022-09-26 20:49:4982 def generate(self, *args, **kwargs):
83 # This should be identical to a regular GPU Telemetry test, but with any
84 # swarming arguments removed.
85 isolated_scripts = super(SkylabGPUTelemetryTestGenerator,
86 self).generate(*args, **kwargs)
87 for test in isolated_scripts:
Xinan Lind9b1d2e72022-11-14 20:57:0288 # chromium_GPU is the Autotest wrapper created for browser GPU tests
89 # run in Skylab.
Xinan Lin1f28a0d2023-03-13 17:39:4190 test['autotest_name'] = 'chromium_Graphics'
Xinan Lind9b1d2e72022-11-14 20:57:0291 # As of 22Q4, Skylab tests are running on a CrOS flavored Autotest
92 # framework and it does not support the sub-args like
93 # extra-browser-args. So we have to pop it out and create a new
94 # key for it. See crrev.com/c/3965359 for details.
95 for idx, arg in enumerate(test.get('args', [])):
96 if '--extra-browser-args' in arg:
97 test['args'].pop(idx)
98 test['extra_browser_args'] = arg.replace('--extra-browser-args=', '')
99 break
Brian Sheedyb6491ba2022-09-26 20:49:49100 return isolated_scripts
101
102
Kenneth Russelleb60cbd22017-12-05 07:54:28103class GTestGenerator(BaseGenerator):
104 def __init__(self, bb_gen):
105 super(GTestGenerator, self).__init__(bb_gen)
106
Kenneth Russell8ceeabf2017-12-11 17:53:28107 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28108 # The relative ordering of some of the tests is important to
109 # minimize differences compared to the handwritten JSON files, since
110 # Python's sorts are stable and there are some tests with the same
111 # key (see gles2_conform_d3d9_test and similar variants). Avoid
112 # losing the order by avoiding coalescing the dictionaries into one.
113 gtests = []
Jamie Madillcf4f8c72021-05-20 19:24:23114 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoon67c3e832020-02-08 07:39:38115 # Variants allow more than one definition for a given test, and is defined
116 # in array format from resolve_variants().
117 if not isinstance(test_config, list):
118 test_config = [test_config]
119
120 for config in test_config:
121 test = self.bb_gen.generate_gtest(
122 waterfall, tester_name, tester_config, test_name, config)
123 if test:
124 # generate_gtest may veto the test generation on this tester.
125 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28126 return gtests
127
Kenneth Russelleb60cbd22017-12-05 07:54:28128
129class IsolatedScriptTestGenerator(BaseGenerator):
130 def __init__(self, bb_gen):
131 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
132
Kenneth Russell8ceeabf2017-12-11 17:53:28133 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28134 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23135 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43136 # Variants allow more than one definition for a given test, and is defined
137 # in array format from resolve_variants().
138 if not isinstance(test_config, list):
139 test_config = [test_config]
140
141 for config in test_config:
142 test = self.bb_gen.generate_isolated_script_test(
143 waterfall, tester_name, tester_config, test_name, config)
144 if test:
145 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28146 return isolated_scripts
147
Kenneth Russelleb60cbd22017-12-05 07:54:28148
149class ScriptGenerator(BaseGenerator):
150 def __init__(self, bb_gen):
151 super(ScriptGenerator, self).__init__(bb_gen)
152
Kenneth Russell8ceeabf2017-12-11 17:53:28153 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28154 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23155 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28156 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28157 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28158 if test:
159 scripts.append(test)
160 return scripts
161
Kenneth Russelleb60cbd22017-12-05 07:54:28162
163class JUnitGenerator(BaseGenerator):
164 def __init__(self, bb_gen):
165 super(JUnitGenerator, self).__init__(bb_gen)
166
Kenneth Russell8ceeabf2017-12-11 17:53:28167 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28168 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23169 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28170 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28171 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28172 if test:
173 scripts.append(test)
174 return scripts
175
Kenneth Russelleb60cbd22017-12-05 07:54:28176
Xinan Lin05fb9c1752020-12-17 00:15:52177class SkylabGenerator(BaseGenerator):
178 def __init__(self, bb_gen):
179 super(SkylabGenerator, self).__init__(bb_gen)
180
181 def generate(self, waterfall, tester_name, tester_config, input_tests):
182 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23183 for test_name, test_config in sorted(input_tests.items()):
Xinan Lin05fb9c1752020-12-17 00:15:52184 for config in test_config:
185 test = self.bb_gen.generate_skylab_test(waterfall, tester_name,
186 tester_config, test_name,
187 config)
188 if test:
189 scripts.append(test)
190 return scripts
191
Xinan Lin05fb9c1752020-12-17 00:15:52192
Jeff Yoon67c3e832020-02-08 07:39:38193def check_compound_references(other_test_suites=None,
194 sub_suite=None,
195 suite=None,
196 target_test_suites=None,
197 test_type=None,
198 **kwargs):
199 """Ensure comound reference's don't target other compounds"""
200 del kwargs
201 if sub_suite in other_test_suites or sub_suite in target_test_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15202 raise BBGenErr('%s may not refer to other composition type test '
203 'suites (error found while processing %s)' %
204 (test_type, suite))
205
Jeff Yoon67c3e832020-02-08 07:39:38206
207def check_basic_references(basic_suites=None,
208 sub_suite=None,
209 suite=None,
210 **kwargs):
211 """Ensure test has a basic suite reference"""
212 del kwargs
213 if sub_suite not in basic_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15214 raise BBGenErr('Unable to find reference to %s while processing %s' %
215 (sub_suite, suite))
216
Jeff Yoon67c3e832020-02-08 07:39:38217
218def check_conflicting_definitions(basic_suites=None,
219 seen_tests=None,
220 sub_suite=None,
221 suite=None,
222 test_type=None,
Garrett Beaty235c1412023-08-29 20:26:29223 target_test_suites=None,
Jeff Yoon67c3e832020-02-08 07:39:38224 **kwargs):
225 """Ensure that if a test is reachable via multiple basic suites,
226 all of them have an identical definition of the tests.
227 """
228 del kwargs
Garrett Beaty235c1412023-08-29 20:26:29229 variants = None
230 if test_type == 'matrix_compound_suites':
231 variants = target_test_suites[suite][sub_suite].get('variants')
232 variants = variants or [None]
Jeff Yoon67c3e832020-02-08 07:39:38233 for test_name in basic_suites[sub_suite]:
Garrett Beaty235c1412023-08-29 20:26:29234 for variant in variants:
235 key = (test_name, variant)
236 if ((seen_sub_suite := seen_tests.get(key)) is not None
237 and basic_suites[sub_suite][test_name] !=
238 basic_suites[seen_sub_suite][test_name]):
239 test_description = (test_name if variant is None else
240 f'{test_name} with variant {variant} applied')
241 raise BBGenErr(
242 'Conflicting test definitions for %s from %s '
243 'and %s in %s (error found while processing %s)' %
244 (test_description, seen_tests[key], sub_suite, test_type, suite))
245 seen_tests[key] = sub_suite
246
Jeff Yoon67c3e832020-02-08 07:39:38247
248def check_matrix_identifier(sub_suite=None,
249 suite=None,
250 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05251 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38252 **kwargs):
253 """Ensure 'idenfitier' is defined for each variant"""
254 del kwargs
255 sub_suite_config = suite_def[sub_suite]
Garrett Beaty2022db42023-08-29 17:22:40256 for variant_name in sub_suite_config.get('variants', []):
257 if variant_name not in all_variants:
258 raise BBGenErr('Missing variant definition for %s in variants.pyl' %
259 variant_name)
260 variant = all_variants[variant_name]
Jeff Yoonda581c32020-03-06 03:56:05261
Jeff Yoon67c3e832020-02-08 07:39:38262 if not 'identifier' in variant:
263 raise BBGenErr('Missing required identifier field in matrix '
264 'compound suite %s, %s' % (suite, sub_suite))
Sven Zhengef0d0872022-04-04 22:13:29265 if variant['identifier'] == '':
266 raise BBGenErr('Identifier field can not be "" in matrix '
267 'compound suite %s, %s' % (suite, sub_suite))
268 if variant['identifier'].strip() != variant['identifier']:
269 raise BBGenErr('Identifier field can not have leading and trailing '
270 'whitespace in matrix compound suite %s, %s' %
271 (suite, sub_suite))
Jeff Yoon67c3e832020-02-08 07:39:38272
273
Joshua Hood56c673c2022-03-02 20:29:33274class BBJSONGenerator(object): # pylint: disable=useless-object-inheritance
Garrett Beaty1afaccc2020-06-25 19:58:15275 def __init__(self, args):
Garrett Beaty1afaccc2020-06-25 19:58:15276 self.args = args
Kenneth Russelleb60cbd22017-12-05 07:54:28277 self.waterfalls = None
278 self.test_suites = None
279 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01280 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41281 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05282 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28283
Garrett Beaty1afaccc2020-06-25 19:58:15284 @staticmethod
285 def parse_args(argv):
286
287 # RawTextHelpFormatter allows for styling of help statement
288 parser = argparse.ArgumentParser(
289 formatter_class=argparse.RawTextHelpFormatter)
290
291 group = parser.add_mutually_exclusive_group()
292 group.add_argument(
293 '-c',
294 '--check',
295 action='store_true',
296 help=
297 'Do consistency checks of configuration and generated files and then '
298 'exit. Used during presubmit. '
299 'Causes the tool to not generate any files.')
300 group.add_argument(
301 '--query',
302 type=str,
303 help=(
304 "Returns raw JSON information of buildbots and tests.\n" +
305 "Examples:\n" + " List all bots (all info):\n" +
306 " --query bots\n\n" +
307 " List all bots and only their associated tests:\n" +
308 " --query bots/tests\n\n" +
309 " List all information about 'bot1' " +
310 "(make sure you have quotes):\n" + " --query bot/'bot1'\n\n" +
311 " List tests running for 'bot1' (make sure you have quotes):\n" +
312 " --query bot/'bot1'/tests\n\n" + " List all tests:\n" +
313 " --query tests\n\n" +
314 " List all tests and the bots running them:\n" +
315 " --query tests/bots\n\n" +
316 " List all tests that satisfy multiple parameters\n" +
317 " (separation of parameters by '&' symbol):\n" +
318 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
319 " List all tests that run with a specific flag:\n" +
320 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
321 " List specific test (make sure you have quotes):\n"
322 " --query test/'test1'\n\n"
323 " List all bots running 'test1' " +
324 "(make sure you have quotes):\n" + " --query test/'test1'/bots"))
325 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47326 '--json',
327 metavar='JSON_FILE_PATH',
328 type=os.path.abspath,
329 help='Outputs results into a json file. Only works with query function.'
330 )
331 parser.add_argument(
Garrett Beaty1afaccc2020-06-25 19:58:15332 '-n',
333 '--new-files',
334 action='store_true',
335 help=
336 'Write output files as .new.json. Useful during development so old and '
337 'new files can be looked at side-by-side.')
Garrett Beatyade673d2023-08-04 22:00:25338 parser.add_argument('--dimension-sets-handling',
339 choices=['disable'],
340 default='disable',
341 help=('This flag no longer has any effect:'
342 ' dimension_sets fields are not allowed'))
Garrett Beaty1afaccc2020-06-25 19:58:15343 parser.add_argument('-v',
344 '--verbose',
345 action='store_true',
346 help='Increases verbosity. Affects consistency checks.')
347 parser.add_argument('waterfall_filters',
348 metavar='waterfalls',
349 type=str,
350 nargs='*',
351 help='Optional list of waterfalls to generate.')
352 parser.add_argument(
353 '--pyl-files-dir',
Garrett Beaty79339e182023-04-10 20:45:47354 type=os.path.abspath,
355 help=('Path to the directory containing the input .pyl files.'
356 ' By default the directory containing this script will be used.'))
Garrett Beaty1afaccc2020-06-25 19:58:15357 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47358 '--output-dir',
359 type=os.path.abspath,
360 help=('Path to the directory to output generated .json files.'
361 'By default, the pyl files directory will be used.'))
Chong Guee622242020-10-28 18:17:35362 parser.add_argument('--isolate-map-file',
363 metavar='PATH',
364 help='path to additional isolate map files.',
Garrett Beaty79339e182023-04-10 20:45:47365 type=os.path.abspath,
Chong Guee622242020-10-28 18:17:35366 default=[],
367 action='append',
368 dest='isolate_map_files')
Garrett Beaty1afaccc2020-06-25 19:58:15369 parser.add_argument(
370 '--infra-config-dir',
371 help='Path to the LUCI services configuration directory',
Garrett Beaty79339e182023-04-10 20:45:47372 type=os.path.abspath,
373 default=os.path.join(os.path.dirname(__file__), '..', '..', 'infra',
374 'config'))
375
Garrett Beaty1afaccc2020-06-25 19:58:15376 args = parser.parse_args(argv)
377 if args.json and not args.query:
378 parser.error(
379 "The --json flag can only be used with --query.") # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:15380
Garrett Beaty79339e182023-04-10 20:45:47381 args.pyl_files_dir = args.pyl_files_dir or THIS_DIR
382 args.output_dir = args.output_dir or args.pyl_files_dir
383
Stephanie Kim572b43c02023-04-13 14:24:13384 def absolute_file_path(filename):
Garrett Beaty79339e182023-04-10 20:45:47385 return os.path.join(args.pyl_files_dir, filename)
386
Stephanie Kim572b43c02023-04-13 14:24:13387 args.waterfalls_pyl_path = absolute_file_path('waterfalls.pyl')
Garrett Beaty96802d02023-07-07 14:18:05388 args.mixins_pyl_path = absolute_file_path('mixins.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13389 args.test_suites_pyl_path = absolute_file_path('test_suites.pyl')
390 args.test_suite_exceptions_pyl_path = absolute_file_path(
Garrett Beaty79339e182023-04-10 20:45:47391 'test_suite_exceptions.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13392 args.gn_isolate_map_pyl_path = absolute_file_path('gn_isolate_map.pyl')
393 args.variants_pyl_path = absolute_file_path('variants.pyl')
Garrett Beaty4999e9792024-04-03 23:29:11394 args.autoshard_exceptions_json_path = os.path.join(
395 args.infra_config_dir, 'targets', 'autoshard_exceptions.json')
Garrett Beaty79339e182023-04-10 20:45:47396
397 return args
Kenneth Russelleb60cbd22017-12-05 07:54:28398
Stephen Martinis7eb8b612018-09-21 00:17:50399 def print_line(self, line):
400 # Exists so that tests can mock
Jamie Madillcf4f8c72021-05-20 19:24:23401 print(line) # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:50402
Kenneth Russelleb60cbd22017-12-05 07:54:28403 def read_file(self, relative_path):
Garrett Beaty79339e182023-04-10 20:45:47404 with open(relative_path) as fp:
Garrett Beaty1afaccc2020-06-25 19:58:15405 return fp.read()
Kenneth Russelleb60cbd22017-12-05 07:54:28406
Garrett Beaty79339e182023-04-10 20:45:47407 def write_file(self, file_path, contents):
Peter Kastingacd55c12023-08-23 20:19:04408 with open(file_path, 'w', newline='') as fp:
Garrett Beaty79339e182023-04-10 20:45:47409 fp.write(contents)
Zhiling Huangbe008172018-03-08 19:13:11410
Joshua Hood56c673c2022-03-02 20:29:33411 # pylint: disable=inconsistent-return-statements
Garrett Beaty79339e182023-04-10 20:45:47412 def load_pyl_file(self, pyl_file_path):
Kenneth Russelleb60cbd22017-12-05 07:54:28413 try:
Garrett Beaty79339e182023-04-10 20:45:47414 return ast.literal_eval(self.read_file(pyl_file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:28415 except (SyntaxError, ValueError) as e: # pragma: no cover
Josip Sokcevic7110fb382023-06-06 01:05:29416 raise BBGenErr('Failed to parse pyl file "%s": %s' %
417 (pyl_file_path, e)) from e
Joshua Hood56c673c2022-03-02 20:29:33418 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:28419
Kenneth Russell8a386d42018-06-02 09:48:01420 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
421 # Currently it is only mandatory for bots which run GPU tests. Change these to
422 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28423 def is_android(self, tester_config):
424 return tester_config.get('os_type') == 'android'
425
Ben Pastenea9e583b2019-01-16 02:57:26426 def is_chromeos(self, tester_config):
427 return tester_config.get('os_type') == 'chromeos'
428
Chong Guc2ca5d02022-01-11 19:52:17429 def is_fuchsia(self, tester_config):
430 return tester_config.get('os_type') == 'fuchsia'
431
Brian Sheedy781c8ca42021-03-08 22:03:21432 def is_lacros(self, tester_config):
433 return tester_config.get('os_type') == 'lacros'
434
Kenneth Russell8a386d42018-06-02 09:48:01435 def is_linux(self, tester_config):
436 return tester_config.get('os_type') == 'linux'
437
Kai Ninomiya40de9f52019-10-18 21:38:49438 def is_mac(self, tester_config):
439 return tester_config.get('os_type') == 'mac'
440
441 def is_win(self, tester_config):
442 return tester_config.get('os_type') == 'win'
443
444 def is_win64(self, tester_config):
445 return (tester_config.get('os_type') == 'win' and
446 tester_config.get('browser_config') == 'release_x64')
447
Garrett Beatyffe83c4f2023-09-08 19:07:37448 def get_exception_for_test(self, test_config):
449 return self.exceptions.get(test_config['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28450
Garrett Beatyffe83c4f2023-09-08 19:07:37451 def should_run_on_tester(self, waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28452 # Currently, the only reason a test should not run on a given tester is that
453 # it's in the exceptions. (Once the GPU waterfall generation script is
454 # incorporated here, the rules will become more complex.)
Garrett Beatyffe83c4f2023-09-08 19:07:37455 exception = self.get_exception_for_test(test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28456 if not exception:
457 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28458 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28459 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28460 if remove_from:
461 if tester_name in remove_from:
462 return False
463 # TODO(kbr): this code path was added for some tests (including
464 # android_webview_unittests) on one machine (Nougat Phone
465 # Tester) which exists with the same name on two waterfalls,
466 # chromium.android and chromium.fyi; the tests are run on one
467 # but not the other. Once the bots are all uniquely named (a
468 # different ongoing project) this code should be removed.
469 # TODO(kbr): add coverage.
470 return (tester_name + ' ' + waterfall['name']
471 not in remove_from) # pragma: no cover
472 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28473
Garrett Beatyffe83c4f2023-09-08 19:07:37474 def get_test_modifications(self, test, tester_name):
475 exception = self.get_exception_for_test(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28476 if not exception:
477 return None
Nico Weber79dc5f6852018-07-13 19:38:49478 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28479
Garrett Beatyffe83c4f2023-09-08 19:07:37480 def get_test_replacements(self, test, tester_name):
481 exception = self.get_exception_for_test(test)
Brian Sheedye6ea0ee2019-07-11 02:54:37482 if not exception:
483 return None
484 return exception.get('replacements', {}).get(tester_name)
485
Kenneth Russell8a386d42018-06-02 09:48:01486 def merge_command_line_args(self, arr, prefix, splitter):
487 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01488 idx = 0
489 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01490 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01491 while idx < len(arr):
492 flag = arr[idx]
493 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01494 if flag.startswith(prefix):
495 arg = flag[prefix_len:]
496 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01497 if first_idx < 0:
498 first_idx = idx
499 else:
500 delete_current_entry = True
501 if delete_current_entry:
502 del arr[idx]
503 else:
504 idx += 1
505 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01506 arr[first_idx] = prefix + splitter.join(accumulated_args)
507 return arr
508
509 def maybe_fixup_args_array(self, arr):
510 # The incoming array of strings may be an array of command line
511 # arguments. To make it easier to turn on certain features per-bot or
512 # per-test-suite, look specifically for certain flags and merge them
513 # appropriately.
514 # --enable-features=Feature1 --enable-features=Feature2
515 # are merged to:
516 # --enable-features=Feature1,Feature2
517 # and:
518 # --extra-browser-args=arg1 --extra-browser-args=arg2
519 # are merged to:
520 # --extra-browser-args=arg1 arg2
521 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
522 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Yuly Novikov8c487e72020-10-16 20:00:29523 arr = self.merge_command_line_args(arr, '--test-launcher-filter-file=', ';')
Cameron Higgins971f0b92023-01-03 18:05:09524 arr = self.merge_command_line_args(arr, '--extra-app-args=', ',')
Kenneth Russell650995a2018-05-03 21:17:01525 return arr
526
Brian Sheedy910cda82022-07-19 11:58:34527 def substitute_magic_args(self, test_config, tester_name, tester_config):
Brian Sheedya31578e2020-05-18 20:24:36528 """Substitutes any magic substitution args present in |test_config|.
529
530 Substitutions are done in-place.
531
532 See buildbot_json_magic_substitutions.py for more information on this
533 feature.
534
535 Args:
536 test_config: A dict containing a configuration for a specific test on
Garrett Beatye3a606ceb2024-04-30 22:13:13537 a specific builder.
Brian Sheedy5f173bb2021-11-24 00:45:54538 tester_name: A string containing the name of the tester that |test_config|
539 came from.
Brian Sheedy910cda82022-07-19 11:58:34540 tester_config: A dict containing the configuration for the builder that
541 |test_config| is for.
Brian Sheedya31578e2020-05-18 20:24:36542 """
543 substituted_array = []
Brian Sheedyba13cf522022-09-13 21:00:09544 original_args = test_config.get('args', [])
545 for arg in original_args:
Brian Sheedya31578e2020-05-18 20:24:36546 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
547 function = arg.replace(
548 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
549 if hasattr(magic_substitutions, function):
550 substituted_array.extend(
Brian Sheedy910cda82022-07-19 11:58:34551 getattr(magic_substitutions, function)(test_config, tester_name,
552 tester_config))
Brian Sheedya31578e2020-05-18 20:24:36553 else:
554 raise BBGenErr(
555 'Magic substitution function %s does not exist' % function)
556 else:
557 substituted_array.append(arg)
Brian Sheedyba13cf522022-09-13 21:00:09558 if substituted_array != original_args:
Brian Sheedya31578e2020-05-18 20:24:36559 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
560
Garrett Beaty8d6708c2023-07-20 17:20:41561 def dictionary_merge(self, a, b, path=None):
Kenneth Russelleb60cbd22017-12-05 07:54:28562 """http://stackoverflow.com/questions/7204805/
563 python-dictionaries-of-dictionaries-merge
564 merges b into a
565 """
566 if path is None:
567 path = []
568 for key in b:
Garrett Beaty8d6708c2023-07-20 17:20:41569 if key not in a:
570 if b[key] is not None:
571 a[key] = b[key]
572 continue
573
574 if isinstance(a[key], dict) and isinstance(b[key], dict):
575 self.dictionary_merge(a[key], b[key], path + [str(key)])
576 elif a[key] == b[key]:
577 pass # same leaf value
578 elif isinstance(a[key], list) and isinstance(b[key], list):
Garrett Beatyade673d2023-08-04 22:00:25579 a[key] = a[key] + b[key]
580 if key.endswith('args'):
581 a[key] = self.maybe_fixup_args_array(a[key])
Garrett Beaty8d6708c2023-07-20 17:20:41582 elif b[key] is None:
583 del a[key]
584 else:
Kenneth Russelleb60cbd22017-12-05 07:54:28585 a[key] = b[key]
Garrett Beaty8d6708c2023-07-20 17:20:41586
Kenneth Russelleb60cbd22017-12-05 07:54:28587 return a
588
Kenneth Russelleb60cbd22017-12-05 07:54:28589 def clean_swarming_dictionary(self, swarming_dict):
590 # Clean out redundant entries from a test's "swarming" dictionary.
591 # This is really only needed to retain 100% parity with the
592 # handwritten JSON files, and can be removed once all the files are
593 # autogenerated.
594 if 'shards' in swarming_dict:
595 if swarming_dict['shards'] == 1: # pragma: no cover
596 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24597 if 'hard_timeout' in swarming_dict:
598 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
599 del swarming_dict['hard_timeout'] # pragma: no cover
Garrett Beatybb18d532023-06-26 22:16:33600 del swarming_dict['can_use_on_swarming_builders']
Kenneth Russelleb60cbd22017-12-05 07:54:28601
Garrett Beatye3a606ceb2024-04-30 22:13:13602 def resolve_os_conditional_values(self, test, builder):
603 for key, fn in (
604 ('android_swarming', self.is_android),
605 ('chromeos_swarming', self.is_chromeos),
606 ):
607 swarming = test.pop(key, None)
608 if swarming and fn(builder):
609 self.dictionary_merge(test['swarming'], swarming)
610
611 for key, fn in (
612 ('desktop_args', lambda cfg: not self.is_android(cfg)),
613 ('lacros_args', self.is_lacros),
614 ('linux_args', self.is_linux),
615 ('android_args', self.is_android),
616 ('chromeos_args', self.is_chromeos),
617 ('mac_args', self.is_mac),
618 ('win_args', self.is_win),
619 ('win64_args', self.is_win64),
620 ):
621 args = test.pop(key, [])
622 if fn(builder):
623 test.setdefault('args', []).extend(args)
624
625 def apply_common_transformations(self,
626 waterfall,
627 builder_name,
628 builder,
629 test,
630 test_name,
631 *,
632 swarmable=True,
633 supports_args=True):
634 # Initialize the swarming dictionary
635 swarmable = swarmable and builder.get('use_swarming', True)
636 test.setdefault('swarming', {}).setdefault('can_use_on_swarming_builders',
637 swarmable)
638
639 mixins_to_ignore = test.pop('remove_mixins', [])
640 self.ensure_valid_mixin_list(mixins_to_ignore,
641 f'test {test_name} remove_mixins')
642
643 # Add any swarming or args from the builder
644 self.dictionary_merge(test['swarming'], builder.get('swarming', {}))
645 if supports_args:
646 test.setdefault('args', []).extend(builder.get('args', []))
647
648 # Expand any conditional values
649 self.resolve_os_conditional_values(test, builder)
650
651 # Apply mixins from the waterfall
652 waterfall_mixins = waterfall.get('mixins', [])
653 self.ensure_valid_mixin_list(waterfall_mixins,
654 f"waterfall {waterfall['name']} mixins")
655 test = self.apply_mixins(test, waterfall_mixins, mixins_to_ignore, builder)
656
657 # Apply mixins from the builder
658 builder_mixins = builder.get('mixins', [])
659 self.ensure_valid_mixin_list(builder_mixins,
660 f"builder {builder_name} mixins")
661 test = self.apply_mixins(test, builder_mixins, mixins_to_ignore, builder)
662
663 # Apply mixins from the test
664 test_mixins = test.pop('mixins', [])
665 self.ensure_valid_mixin_list(test_mixins, f'test {test_name} mixins')
666 test = self.apply_mixins(test, test_mixins, mixins_to_ignore, builder)
667
Kenneth Russelleb60cbd22017-12-05 07:54:28668 # See if there are any exceptions that need to be merged into this
669 # test's specification.
Garrett Beatye3a606ceb2024-04-30 22:13:13670 modifications = self.get_test_modifications(test, builder_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28671 if modifications:
672 test = self.dictionary_merge(test, modifications)
Garrett Beatye3a606ceb2024-04-30 22:13:13673
674 # Clean up the swarming entry or remove it if it's unnecessary
Garrett Beatybfeff8f2023-06-16 18:57:25675 if (swarming_dict := test.get('swarming')) is not None:
Garrett Beatybb18d532023-06-26 22:16:33676 if swarming_dict.get('can_use_on_swarming_builders'):
Garrett Beatybfeff8f2023-06-16 18:57:25677 self.clean_swarming_dictionary(swarming_dict)
678 else:
679 del test['swarming']
Garrett Beatye3a606ceb2024-04-30 22:13:13680
Ben Pastenee012aea42019-05-14 22:32:28681 # Ensure all Android Swarming tests run only on userdebug builds if another
682 # build type was not specified.
Garrett Beatye3a606ceb2024-04-30 22:13:13683 if 'swarming' in test and self.is_android(builder):
Garrett Beatyade673d2023-08-04 22:00:25684 dimensions = test.get('swarming', {}).get('dimensions', {})
685 if (dimensions.get('os') == 'Android'
686 and not dimensions.get('device_os_type')):
687 dimensions['device_os_type'] = 'userdebug'
Garrett Beatye3a606ceb2024-04-30 22:13:13688
689 # Apply any replacements specified for the test for the builder
690 self.replace_test_args(test, test_name, builder_name)
691
692 # Remove args if it is empty
693 if 'args' in test:
694 if not test['args']:
695 del test['args']
696 else:
697 # Replace any magic arguments with their actual value
698 self.substitute_magic_args(test, builder_name, builder)
699
700 test['args'] = self.maybe_fixup_args_array(test['args'])
Ben Pastenee012aea42019-05-14 22:32:28701
Kenneth Russelleb60cbd22017-12-05 07:54:28702 return test
703
Brian Sheedye6ea0ee2019-07-11 02:54:37704 def replace_test_args(self, test, test_name, tester_name):
Garrett Beatyffe83c4f2023-09-08 19:07:37705 replacements = self.get_test_replacements(test, tester_name) or {}
Brian Sheedye6ea0ee2019-07-11 02:54:37706 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23707 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37708 if key not in valid_replacement_keys:
709 raise BBGenErr(
710 'Given replacement key %s for %s on %s is not in the list of valid '
711 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23712 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37713 found_key = False
714 for i, test_key in enumerate(test.get(key, [])):
715 # Handle both the key/value being replaced being defined as two
716 # separate items or as key=value.
717 if test_key == replacement_key:
718 found_key = True
719 # Handle flags without values.
720 if replacement_val == None:
721 del test[key][i]
722 else:
723 test[key][i+1] = replacement_val
724 break
Joshua Hood56c673c2022-03-02 20:29:33725 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37726 found_key = True
727 if replacement_val == None:
728 del test[key][i]
729 else:
730 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
731 break
732 if not found_key:
733 raise BBGenErr('Could not find %s in existing list of values for key '
734 '%s in %s on %s' % (replacement_key, key, test_name,
735 tester_name))
736
Shenghua Zhangaba8bad2018-02-07 02:12:09737 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05738 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26739 True):
740 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06741 # are targeting CrOS hardware and so need the special trigger script.
Garrett Beatyade673d2023-08-04 22:00:25742 if 'device_type' in test.get('swarming', {}).get('dimensions', {}):
Ben Pastenea9e583b2019-01-16 02:57:26743 test['trigger_script'] = {
744 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
745 }
Shenghua Zhangaba8bad2018-02-07 02:12:09746
Garrett Beatyffe83c4f2023-09-08 19:07:37747 def add_android_presentation_args(self, tester_config, result):
John Budorick262ae112019-07-12 19:24:38748 bucket = tester_config.get('results_bucket', 'chromium-result-details')
Garrett Beaty94af4272024-04-17 18:06:14749 result.setdefault('args', []).append('--gs-results-bucket=%s' % bucket)
750
751 if ('swarming' in result and 'merge' not in 'result'
752 and not tester_config.get('skip_merge_script', False)):
Ben Pastene858f4be2019-01-09 23:52:09753 result['merge'] = {
Garrett Beatyffe83c4f2023-09-08 19:07:37754 'args': [
755 '--bucket',
756 bucket,
757 '--test-name',
758 result['name'],
759 ],
760 'script': ('//build/android/pylib/results/presentation/'
761 'test_results_presentation.py'),
Ben Pastene858f4be2019-01-09 23:52:09762 }
Ben Pastene858f4be2019-01-09 23:52:09763
Kenneth Russelleb60cbd22017-12-05 07:54:28764 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
765 test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37766 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28767 return None
768 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37769 # Use test_name here instead of test['name'] because test['name'] will be
770 # modified with the variant identifier in a matrix compound suite
771 result.setdefault('test', test_name)
John Budorickab108712018-09-01 00:12:21772
Garrett Beatye3a606ceb2024-04-30 22:13:13773 result = self.apply_common_transformations(waterfall, tester_name,
774 tester_config, result, test_name)
Garrett Beaty94af4272024-04-17 18:06:14775 if self.is_android(tester_config) and 'swarming' in result:
776 if not result.get('use_isolated_scripts_api', False):
Alison Gale71bd8f152024-04-26 22:38:20777 # TODO(crbug.com/40725094) make Android presentation work with
Yuly Novikov26dd47052021-02-11 00:57:14778 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37779 self.add_android_presentation_args(tester_config, result)
Yuly Novikov26dd47052021-02-11 00:57:14780 result['args'] = result.get('args', []) + ['--recover-devices']
Shenghua Zhangaba8bad2018-02-07 02:12:09781 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43782
Garrett Beatybb18d532023-06-26 22:16:33783 if 'swarming' in result and not result.get('merge'):
Jamie Madilla8be0d72020-10-02 05:24:04784 if test_config.get('use_isolated_scripts_api', False):
785 merge_script = 'standard_isolated_script_merge'
786 else:
787 merge_script = 'standard_gtest_merge'
788
Stephen Martinisbc7b7772019-05-01 22:01:43789 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04790 'script': '//testing/merge_scripts/%s.py' % merge_script,
Stephen Martinisbc7b7772019-05-01 22:01:43791 }
Kenneth Russelleb60cbd22017-12-05 07:54:28792 return result
793
794 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
795 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37796 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28797 return None
798 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37799 # Use test_name here instead of test['name'] because test['name'] will be
800 # modified with the variant identifier in a matrix compound suite
Garrett Beatydca3d882023-09-14 23:50:32801 result.setdefault('test', test_name)
Garrett Beatye3a606ceb2024-04-30 22:13:13802 result = self.apply_common_transformations(waterfall, tester_name,
803 tester_config, result, test_name)
Garrett Beaty94af4272024-04-17 18:06:14804 if self.is_android(tester_config) and 'swarming' in result:
Yuly Novikov26dd47052021-02-11 00:57:14805 if tester_config.get('use_android_presentation', False):
Alison Gale71bd8f152024-04-26 22:38:20806 # TODO(crbug.com/40725094) make Android presentation work with
Yuly Novikov26dd47052021-02-11 00:57:14807 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37808 self.add_android_presentation_args(tester_config, result)
Shenghua Zhangaba8bad2018-02-07 02:12:09809 self.add_common_test_properties(result, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17810
Garrett Beatybb18d532023-06-26 22:16:33811 if 'swarming' in result and not result.get('merge'):
Alison Gale923a33e2024-04-22 23:34:28812 # TODO(crbug.com/41456107): Consider adding the ability to not have
Stephen Martinisf50047062019-05-06 22:26:17813 # this default.
814 result['merge'] = {
815 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
Stephen Martinisf50047062019-05-06 22:26:17816 }
Kenneth Russelleb60cbd22017-12-05 07:54:28817 return result
818
819 def generate_script_test(self, waterfall, tester_name, tester_config,
820 test_name, test_config):
Alison Gale47d1537d2024-04-19 21:31:46821 # TODO(crbug.com/40623237): Remove this check whenever a better
Brian Sheedy158cd0f2019-04-26 01:12:44822 # long-term solution is implemented.
823 if (waterfall.get('forbid_script_tests', False) or
824 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
825 raise BBGenErr('Attempted to generate a script test on tester ' +
826 tester_name + ', which explicitly forbids script tests')
Garrett Beatyffe83c4f2023-09-08 19:07:37827 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28828 return None
829 result = {
Garrett Beatyffe83c4f2023-09-08 19:07:37830 'name': test_config['name'],
831 'script': test_config['script'],
Kenneth Russelleb60cbd22017-12-05 07:54:28832 }
Garrett Beatye3a606ceb2024-04-30 22:13:13833 result = self.apply_common_transformations(waterfall,
834 tester_name,
835 tester_config,
836 result,
837 test_name,
838 swarmable=False,
839 supports_args=False)
Kenneth Russelleb60cbd22017-12-05 07:54:28840 return result
841
842 def generate_junit_test(self, waterfall, tester_name, tester_config,
843 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37844 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28845 return None
John Budorickdef6acb2019-09-17 22:51:09846 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37847 # Use test_name here instead of test['name'] because test['name'] will be
848 # modified with the variant identifier in a matrix compound suite
849 result.setdefault('test', test_name)
Garrett Beatye3a606ceb2024-04-30 22:13:13850 result = self.apply_common_transformations(waterfall,
851 tester_name,
852 tester_config,
853 result,
854 test_name,
855 swarmable=False)
Kenneth Russelleb60cbd22017-12-05 07:54:28856 return result
857
Xinan Lin05fb9c1752020-12-17 00:15:52858 def generate_skylab_test(self, waterfall, tester_name, tester_config,
859 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37860 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Xinan Lin05fb9c1752020-12-17 00:15:52861 return None
862 result = copy.deepcopy(test_config)
Brian Sheedy67937ad12024-03-06 22:53:55863 result.setdefault('test', test_name)
yoshiki iguchid1664ef2024-03-28 19:16:52864
865 if 'cros_board' in result or 'cros_board' in tester_config:
866 result['cros_board'] = tester_config.get('cros_board') or result.get(
867 'cros_board')
868 else:
869 raise BBGenErr("skylab tests must specify cros_board.")
870 if 'cros_model' in result or 'cros_model' in tester_config:
871 result['cros_model'] = tester_config.get('cros_model') or result.get(
872 'cros_model')
873 if 'dut_pool' in result or 'cros_dut_pool' in tester_config:
874 result['dut_pool'] = tester_config.get('cros_dut_pool') or result.get(
875 'dut_pool')
876
Garrett Beatye3a606ceb2024-04-30 22:13:13877 result = self.apply_common_transformations(waterfall,
878 tester_name,
879 tester_config,
880 result,
881 test_name,
882 swarmable=False)
Xinan Lin05fb9c1752020-12-17 00:15:52883 return result
884
Garrett Beaty65d44222023-08-01 17:22:11885 def substitute_gpu_args(self, tester_config, test, args):
Kenneth Russell8a386d42018-06-02 09:48:01886 substitutions = {
887 # Any machine in waterfalls.pyl which desires to run GPU tests
888 # must provide the os_type key.
889 'os_type': tester_config['os_type'],
890 'gpu_vendor_id': '0',
891 'gpu_device_id': '0',
892 }
Garrett Beatyade673d2023-08-04 22:00:25893 dimensions = test.get('swarming', {}).get('dimensions', {})
894 if 'gpu' in dimensions:
895 # First remove the driver version, then split into vendor and device.
896 gpu = dimensions['gpu']
897 if gpu != 'none':
898 gpu = gpu.split('-')[0].split(':')
899 substitutions['gpu_vendor_id'] = gpu[0]
900 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01901 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
902
903 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30904 test_name, test_config, is_android_webview,
Xinan Linedcf05b32023-10-19 23:13:50905 is_cast_streaming, is_skylab):
Kenneth Russell8a386d42018-06-02 09:48:01906 # These are all just specializations of isolated script tests with
907 # a bunch of boilerplate command line arguments added.
908
909 # The step name must end in 'test' or 'tests' in order for the
910 # results to automatically show up on the flakiness dashboard.
911 # (At least, this was true some time ago.) Continue to use this
912 # naming convention for the time being to minimize changes.
Garrett Beaty235c1412023-08-29 20:26:29913 #
914 # test name is the name of the test without the variant ID added
915 if not (test_name.endswith('test') or test_name.endswith('tests')):
916 raise BBGenErr(
917 f'telemetry test names must end with test or tests, got {test_name}')
Garrett Beatyffe83c4f2023-09-08 19:07:37918 result = self.generate_isolated_script_test(waterfall, tester_name,
919 tester_config, test_name,
920 test_config)
Kenneth Russell8a386d42018-06-02 09:48:01921 if not result:
922 return None
Garrett Beatydca3d882023-09-14 23:50:32923 result['test'] = test_config.get('test') or self.get_default_isolate_name(
924 tester_config, is_android_webview)
Chan Liab7d8dd82020-04-24 23:42:19925
Chan Lia3ad1502020-04-28 05:32:11926 # Populate test_id_prefix.
Garrett Beatydca3d882023-09-14 23:50:32927 gn_entry = self.gn_isolate_map[result['test']]
Chan Li17d969f92020-07-10 00:50:03928 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19929
Kenneth Russell8a386d42018-06-02 09:48:01930 args = result.get('args', [])
Garrett Beatyffe83c4f2023-09-08 19:07:37931 # Use test_name here instead of test['name'] because test['name'] will be
932 # modified with the variant identifier in a matrix compound suite
Kenneth Russell8a386d42018-06-02 09:48:01933 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14934
erikchen6da2d9b2018-08-03 23:01:14935 # These tests upload and download results from cloud storage and therefore
936 # aren't idempotent yet. https://crbug.com/549140.
Garrett Beatybfeff8f2023-06-16 18:57:25937 if 'swarming' in result:
938 result['swarming']['idempotent'] = False
erikchen6da2d9b2018-08-03 23:01:14939
Fabrice de Ganscbd655f2022-08-04 20:15:30940 browser = ''
941 if is_cast_streaming:
942 browser = 'cast-streaming-shell'
943 elif is_android_webview:
944 browser = 'android-webview-instrumentation'
945 else:
946 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:52947
Greg Thompsoncec7d8d2023-01-10 19:11:53948 extra_browser_args = []
949
Brian Sheedy4053a702020-07-28 02:09:52950 # Most platforms require --enable-logging=stderr to get useful browser logs.
951 # However, this actively messes with logging on CrOS (because Chrome's
952 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
953 # in order to see JavaScript console messages. See
954 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
Greg Thompsoncec7d8d2023-01-10 19:11:53955 if self.is_chromeos(tester_config):
956 extra_browser_args.append('--log-level=0')
957 elif not self.is_fuchsia(tester_config) or browser != 'fuchsia-chrome':
958 # Stderr logging is not needed for Chrome browser on Fuchsia, as ordinary
959 # logging via syslog is captured.
960 extra_browser_args.append('--enable-logging=stderr')
961
962 # --expose-gc allows the WebGL conformance tests to more reliably
963 # reproduce GC-related bugs in the V8 bindings.
964 extra_browser_args.append('--js-flags=--expose-gc')
Brian Sheedy4053a702020-07-28 02:09:52965
Xinan Linedcf05b32023-10-19 23:13:50966 # Skylab supports sharding, so reuse swarming's shard config.
967 if is_skylab and 'shards' not in result and test_config.get(
968 'swarming', {}).get('shards'):
969 result['shards'] = test_config['swarming']['shards']
970
Kenneth Russell8a386d42018-06-02 09:48:01971 args = [
Bo Liu555a0f92019-03-29 12:11:56972 test_to_run,
973 '--show-stdout',
974 '--browser=%s' % browser,
975 # --passthrough displays more of the logging in Telemetry when
976 # run via typ, in particular some of the warnings about tests
977 # being expected to fail, but passing.
978 '--passthrough',
979 '-v',
Brian Sheedy814e0482022-10-03 23:24:12980 '--stable-jobs',
Greg Thompsoncec7d8d2023-01-10 19:11:53981 '--extra-browser-args=%s' % ' '.join(extra_browser_args),
Brian Sheedy997e4802023-10-18 02:28:13982 '--enforce-browser-version',
Kenneth Russell8a386d42018-06-02 09:48:01983 ] + args
Garrett Beatybfeff8f2023-06-16 18:57:25984 result['args'] = self.maybe_fixup_args_array(
Garrett Beaty65d44222023-08-01 17:22:11985 self.substitute_gpu_args(tester_config, result, args))
Kenneth Russell8a386d42018-06-02 09:48:01986 return result
987
Brian Sheedyf74819b2021-06-04 01:38:38988 def get_default_isolate_name(self, tester_config, is_android_webview):
989 if self.is_android(tester_config):
990 if is_android_webview:
991 return 'telemetry_gpu_integration_test_android_webview'
992 return (
993 'telemetry_gpu_integration_test' +
994 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:33995 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:17996 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:33997 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:38998
Kenneth Russelleb60cbd22017-12-05 07:54:28999 def get_test_generator_map(self):
1000 return {
Bo Liu555a0f92019-03-29 12:11:561001 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301002 GPUTelemetryTestGenerator(self, is_android_webview=True),
1003 'cast_streaming_tests':
1004 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:561005 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301006 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561007 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301008 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561009 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301010 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561011 'junit_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301012 JUnitGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561013 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301014 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:521015 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301016 SkylabGenerator(self),
Brian Sheedyb6491ba2022-09-26 20:49:491017 'skylab_gpu_telemetry_tests':
1018 SkylabGPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281019 }
1020
Kenneth Russell8a386d42018-06-02 09:48:011021 def get_test_type_remapper(self):
1022 return {
Fabrice de Gans223272482022-08-08 16:56:571023 # These are a specialization of isolated_scripts with a bunch of
1024 # boilerplate command line arguments added to each one.
1025 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
1026 'cast_streaming_tests': 'isolated_scripts',
1027 'gpu_telemetry_tests': 'isolated_scripts',
Brian Sheedyb6491ba2022-09-26 20:49:491028 # These are the same as existing test types, just configured to run
1029 # in Skylab instead of via normal swarming.
1030 'skylab_gpu_telemetry_tests': 'skylab_tests',
Kenneth Russell8a386d42018-06-02 09:48:011031 }
1032
Jeff Yoon67c3e832020-02-08 07:39:381033 def check_composition_type_test_suites(self, test_type,
1034 additional_validators=None):
1035 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1036 validators = [check_compound_references,
1037 check_basic_references,
1038 check_conflicting_definitions]
1039 if additional_validators:
1040 validators += additional_validators
1041
1042 target_suites = self.test_suites.get(test_type, {})
1043 other_test_type = ('compound_suites'
1044 if test_type == 'matrix_compound_suites'
1045 else 'matrix_compound_suites')
1046 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011047 basic_suites = self.test_suites.get('basic_suites', {})
1048
Jamie Madillcf4f8c72021-05-20 19:24:231049 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011050 if suite in basic_suites:
1051 raise BBGenErr('%s names may not duplicate basic test suite names '
1052 '(error found while processsing %s)'
1053 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011054
Jeff Yoon67c3e832020-02-08 07:39:381055 seen_tests = {}
1056 for sub_suite in suite_def:
1057 for validator in validators:
1058 validator(
1059 basic_suites=basic_suites,
1060 other_test_suites=other_suites,
1061 seen_tests=seen_tests,
1062 sub_suite=sub_suite,
1063 suite=suite,
1064 suite_def=suite_def,
1065 target_test_suites=target_suites,
1066 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051067 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381068 )
Kenneth Russelleb60cbd22017-12-05 07:54:281069
Stephen Martinis54d64ad2018-09-21 22:16:201070 def flatten_test_suites(self):
1071 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011072 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1073 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231074 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011075 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201076 self.test_suites = new_test_suites
1077
Chan Lia3ad1502020-04-28 05:32:111078 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231079 for suite in self.test_suites['basic_suites'].values():
1080 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561081 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411082
Garrett Beatydca3d882023-09-14 23:50:321083 isolate_name = test.get('test') or key
Nodir Turakulovfce34292019-12-18 17:05:411084 gn_entry = self.gn_isolate_map.get(isolate_name)
1085 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281086 label = gn_entry['label']
1087
1088 if label.count(':') != 1:
1089 raise BBGenErr(
1090 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1091 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1092 (label, isolate_name))
1093 if label.split(':')[1] != isolate_name:
1094 raise BBGenErr(
1095 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1096 ' label "%s" see http://crbug.com/1071091 for details.' %
1097 (isolate_name, label))
1098
Chan Lia3ad1502020-04-28 05:32:111099 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411100 else: # pragma: no cover
1101 # Some tests do not have an entry gn_isolate_map.pyl, such as
1102 # telemetry tests.
Alison Gale47d1537d2024-04-19 21:31:461103 # TODO(crbug.com/40112160): require an entry in gn_isolate_map.
Nodir Turakulovfce34292019-12-18 17:05:411104 pass
1105
Kenneth Russelleb60cbd22017-12-05 07:54:281106 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011107 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201108
Jeff Yoon8154e582019-12-03 23:30:011109 compound_suites = self.test_suites.get('compound_suites', {})
1110 # check_composition_type_test_suites() checks that all basic suites
1111 # referenced by compound suites exist.
1112 basic_suites = self.test_suites.get('basic_suites')
1113
Jamie Madillcf4f8c72021-05-20 19:24:231114 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011115 # Resolve this to a dictionary.
1116 full_suite = {}
1117 for entry in value:
1118 suite = basic_suites[entry]
1119 full_suite.update(suite)
1120 compound_suites[name] = full_suite
1121
Jeff Yoon85fb8df2020-08-20 16:47:431122 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381123 """ Merge variant-defined configurations to each test case definition in a
1124 test suite.
1125
1126 The output maps a unique test name to an array of configurations because
1127 there may exist more than one definition for a test name using variants. The
1128 test name is referenced while mapping machines to test suites, so unpacking
1129 the array is done by the generators.
1130
1131 Args:
1132 basic_test_definition: a {} defined test suite in the format
1133 test_name:test_config
1134 variants: an [] of {} defining configurations to be applied to each test
1135 case in the basic test_definition
1136
1137 Return:
1138 a {} of test_name:[{}], where each {} is a merged configuration
1139 """
1140
1141 # Each test in a basic test suite will have a definition per variant.
1142 test_suite = {}
Garrett Beaty8d6708c2023-07-20 17:20:411143 for variant in variants:
1144 # Unpack the variant from variants.pyl if it's string based.
1145 if isinstance(variant, str):
1146 variant = self.variants[variant]
Jeff Yoonda581c32020-03-06 03:56:051147
Garrett Beaty8d6708c2023-07-20 17:20:411148 # If 'enabled' is set to False, we will not use this variant; otherwise if
1149 # the variant doesn't include 'enabled' variable or 'enabled' is set to
1150 # True, we will use this variant
1151 if not variant.get('enabled', True):
1152 continue
Jeff Yoon67c3e832020-02-08 07:39:381153
Garrett Beaty8d6708c2023-07-20 17:20:411154 # Make a shallow copy of the variant to remove variant-specific fields,
1155 # leaving just mixin fields
1156 variant = copy.copy(variant)
1157 variant.pop('enabled', None)
1158 identifier = variant.pop('identifier')
1159 variant_mixins = variant.pop('mixins', [])
1160 variant_skylab = variant.pop('skylab', {})
Jeff Yoon67c3e832020-02-08 07:39:381161
Garrett Beaty8d6708c2023-07-20 17:20:411162 for test_name, test_config in basic_test_definition.items():
1163 new_test = self.apply_mixin(variant, test_config)
Jeff Yoon67c3e832020-02-08 07:39:381164
Garrett Beaty8d6708c2023-07-20 17:20:411165 new_test['mixins'] = (test_config.get('mixins', []) + variant_mixins +
1166 mixins)
Xinan Lin05fb9c1752020-12-17 00:15:521167
Jeff Yoon67c3e832020-02-08 07:39:381168 # The identifier is used to make the name of the test unique.
1169 # Generators in the recipe uniquely identify a test by it's name, so we
1170 # don't want to have the same name for each variant.
Garrett Beaty235c1412023-08-29 20:26:291171 new_test['name'] = f'{test_name} {identifier}'
Ben Pastene5f231cf22022-05-05 18:03:071172
1173 # Attach the variant identifier to the test config so downstream
1174 # generators can make modifications based on the original name. This
1175 # is mainly used in generate_gpu_telemetry_test().
Garrett Beaty8d6708c2023-07-20 17:20:411176 new_test['variant_id'] = identifier
Ben Pastene5f231cf22022-05-05 18:03:071177
Garrett Beaty8d6708c2023-07-20 17:20:411178 for k, v in variant_skylab.items():
Sven Zheng22ba6312023-10-16 22:59:351179 # cros_chrome_version is the ash chrome version in the cros img in the
1180 # variant of cros_board. We don't want to include it in the final json
1181 # files; so remove it.
Garrett Beaty8d6708c2023-07-20 17:20:411182 if k != 'cros_chrome_version':
1183 new_test[k] = v
1184
Sven Zheng22ba6312023-10-16 22:59:351185 # For skylab, we need to pop the correct `autotest_name`. This field
1186 # defines what wrapper we use in OS infra. e.g. for gtest it's
1187 # https://source.chromium.org/chromiumos/chromiumos/codesearch/+/main:src/third_party/autotest/files/server/site_tests/chromium/chromium.py
1188 if variant_skylab and 'autotest_name' not in new_test:
1189 if 'tast_expr' in test_config:
1190 if 'lacros' in test_config['name']:
1191 new_test['autotest_name'] = 'tast.lacros-from-gcs'
1192 else:
1193 new_test['autotest_name'] = 'tast.chrome-from-gcs'
1194 elif 'benchmark' in test_config:
1195 new_test['autotest_name'] = 'chromium_Telemetry'
1196 else:
1197 new_test['autotest_name'] = 'chromium'
1198
Garrett Beaty8d6708c2023-07-20 17:20:411199 test_suite.setdefault(test_name, []).append(new_test)
1200
Jeff Yoon67c3e832020-02-08 07:39:381201 return test_suite
1202
Jeff Yoon8154e582019-12-03 23:30:011203 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381204 self.check_composition_type_test_suites('matrix_compound_suites',
1205 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011206
1207 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381208 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011209 # referenced by matrix suites exist.
1210 basic_suites = self.test_suites.get('basic_suites')
1211
Garrett Beaty235c1412023-08-29 20:26:291212 for matrix_suite_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011213 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381214
Jamie Madillcf4f8c72021-05-20 19:24:231215 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381216 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1217
Garrett Beaty235c1412023-08-29 20:26:291218 def update_tests(expanded):
1219 for test_name, new_tests in expanded.items():
1220 if not isinstance(new_tests, list):
1221 new_tests = [new_tests]
1222 tests_for_name = full_suite.setdefault(test_name, [])
1223 for t in new_tests:
1224 if t not in tests_for_name:
1225 tests_for_name.append(t)
1226
Garrett Beaty60a7b2a2023-09-13 23:00:401227 if (variants := mtx_test_suite_config.get('variants')):
Jeff Yoon85fb8df2020-08-20 16:47:431228 mixins = mtx_test_suite_config.get('mixins', [])
Garrett Beaty60a7b2a2023-09-13 23:00:401229 result = self.resolve_variants(basic_test_def, variants, mixins)
Garrett Beaty235c1412023-08-29 20:26:291230 update_tests(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271231 else:
1232 suite = basic_suites[test_suite]
Garrett Beaty235c1412023-08-29 20:26:291233 update_tests(suite)
1234 matrix_compound_suites[matrix_suite_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281235
1236 def link_waterfalls_to_test_suites(self):
1237 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231238 for tester_name, tester in waterfall['machines'].items():
1239 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281240 if not value in self.test_suites:
1241 # Hard / impossible to cover this in the unit test.
1242 raise self.unknown_test_suite(
1243 value, tester_name, waterfall['name']) # pragma: no cover
1244 tester['test_suites'][suite] = self.test_suites[value]
1245
1246 def load_configuration_files(self):
Garrett Beaty79339e182023-04-10 20:45:471247 self.waterfalls = self.load_pyl_file(self.args.waterfalls_pyl_path)
1248 self.test_suites = self.load_pyl_file(self.args.test_suites_pyl_path)
1249 self.exceptions = self.load_pyl_file(
1250 self.args.test_suite_exceptions_pyl_path)
1251 self.mixins = self.load_pyl_file(self.args.mixins_pyl_path)
1252 self.gn_isolate_map = self.load_pyl_file(self.args.gn_isolate_map_pyl_path)
Chong Guee622242020-10-28 18:17:351253 for isolate_map in self.args.isolate_map_files:
1254 isolate_map = self.load_pyl_file(isolate_map)
1255 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1256 if duplicates:
1257 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1258 ', '.join(duplicates))
1259 self.gn_isolate_map.update(isolate_map)
1260
Garrett Beaty79339e182023-04-10 20:45:471261 self.variants = self.load_pyl_file(self.args.variants_pyl_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281262
1263 def resolve_configuration_files(self):
Garrett Beaty235c1412023-08-29 20:26:291264 self.resolve_test_names()
Garrett Beatydca3d882023-09-14 23:50:321265 self.resolve_isolate_names()
Garrett Beaty65d44222023-08-01 17:22:111266 self.resolve_dimension_sets()
Chan Lia3ad1502020-04-28 05:32:111267 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281268 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011269 self.resolve_matrix_compound_test_suites()
1270 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281271 self.link_waterfalls_to_test_suites()
1272
Garrett Beaty235c1412023-08-29 20:26:291273 def resolve_test_names(self):
1274 for suite_name, suite in self.test_suites.get('basic_suites').items():
1275 for test_name, test in suite.items():
1276 if 'name' in test:
1277 raise BBGenErr(
1278 f'The name field is set in test {test_name} in basic suite '
1279 f'{suite_name}, this is not supported, the test name is the key '
1280 'within the basic suite')
Garrett Beatyffe83c4f2023-09-08 19:07:371281 # When a test is expanded with variants, this will be overwritten, but
1282 # this ensures every test definition has the name field set
1283 test['name'] = test_name
Garrett Beaty235c1412023-08-29 20:26:291284
Garrett Beatydca3d882023-09-14 23:50:321285 def resolve_isolate_names(self):
1286 for suite_name, suite in self.test_suites.get('basic_suites').items():
1287 for test_name, test in suite.items():
1288 if 'isolate_name' in test:
1289 raise BBGenErr(
1290 f'The isolate_name field is set in test {test_name} in basic '
1291 f'suite {suite_name}, the test field should be used instead')
1292
Garrett Beaty65d44222023-08-01 17:22:111293 def resolve_dimension_sets(self):
Garrett Beaty65d44222023-08-01 17:22:111294
1295 def definitions():
1296 for suite_name, suite in self.test_suites.get('basic_suites', {}).items():
1297 for test_name, test in suite.items():
1298 yield test, f'test {test_name} in basic suite {suite_name}'
1299
1300 for mixin_name, mixin in self.mixins.items():
1301 yield mixin, f'mixin {mixin_name}'
1302
1303 for waterfall in self.waterfalls:
1304 for builder_name, builder in waterfall.get('machines', {}).items():
1305 yield (
1306 builder,
1307 f'builder {builder_name} in waterfall {waterfall["name"]}',
1308 )
1309
1310 for test_name, exceptions in self.exceptions.items():
1311 modifications = exceptions.get('modifications', {})
1312 for builder_name, mods in modifications.items():
1313 yield (
1314 mods,
1315 f'exception for test {test_name} on builder {builder_name}',
1316 )
1317
1318 for definition, location in definitions():
1319 for swarming_attr in (
1320 'swarming',
1321 'android_swarming',
1322 'chromeos_swarming',
1323 ):
1324 if (swarming :=
1325 definition.get(swarming_attr)) and 'dimension_sets' in swarming:
Garrett Beatyade673d2023-08-04 22:00:251326 raise BBGenErr(
1327 f'dimension_sets is no longer supported (set in {location}),'
1328 ' instead, use set dimensions to a single dict')
Garrett Beaty65d44222023-08-01 17:22:111329
Nico Weberd18b8962018-05-16 19:39:381330 def unknown_bot(self, bot_name, waterfall_name):
1331 return BBGenErr(
1332 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1333
Kenneth Russelleb60cbd22017-12-05 07:54:281334 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1335 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381336 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281337 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1338
1339 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1340 return BBGenErr(
1341 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1342 ' on waterfall ' + waterfall_name)
1343
Garrett Beatye3a606ceb2024-04-30 22:13:131344 def ensure_valid_mixin_list(self, mixins, location):
1345 if not isinstance(mixins, list):
1346 raise BBGenErr(
1347 f"got '{mixins}', should be a list of mixin names: {location}")
1348 for mixin in mixins:
1349 if not mixin in self.mixins:
1350 raise BBGenErr(f'bad mixin {mixin}: {location}')
Stephen Martinisb6a50492018-09-12 23:59:321351
Garrett Beatye3a606ceb2024-04-30 22:13:131352 def apply_mixins(self, test, mixins, mixins_to_ignore, builder=None):
1353 for mixin in mixins:
1354 if mixin not in mixins_to_ignore:
Austin Eng148d9f0f2022-02-08 19:18:531355 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinis0382bc12018-09-17 22:29:071356 return test
Stephen Martinisb6a50492018-09-12 23:59:321357
Garrett Beaty8d6708c2023-07-20 17:20:411358 def apply_mixin(self, mixin, test, builder=None):
Stephen Martinisb72f6d22018-10-04 23:29:011359 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321360
Garrett Beaty4c35b142023-06-23 21:01:231361 A mixin is applied by copying all fields from the mixin into the
1362 test with the following exceptions:
1363 * For the various *args keys, the test's existing value (an empty
1364 list if not present) will be extended with the mixin's value.
1365 * The sub-keys of the swarming value will be copied to the test's
1366 swarming value with the following exceptions:
Garrett Beatyade673d2023-08-04 22:00:251367 * For the named_caches sub-keys, the test's existing value (an
1368 empty list if not present) will be extended with the mixin's
1369 value.
1370 * For the dimensions sub-key, the tests's existing value (an empty
1371 dict if not present) will be updated with the mixin's value.
Stephen Martinisb6a50492018-09-12 23:59:321372 """
Garrett Beaty4c35b142023-06-23 21:01:231373
Stephen Martinisb6a50492018-09-12 23:59:321374 new_test = copy.deepcopy(test)
1375 mixin = copy.deepcopy(mixin)
Garrett Beaty8d6708c2023-07-20 17:20:411376
1377 if 'description' in mixin:
1378 description = []
1379 if 'description' in new_test:
1380 description.append(new_test['description'])
1381 description.append(mixin.pop('description'))
1382 new_test['description'] = '\n'.join(description)
1383
Stephen Martinisb72f6d22018-10-04 23:29:011384 if 'swarming' in mixin:
1385 swarming_mixin = mixin['swarming']
1386 new_test.setdefault('swarming', {})
Stephen Martinisb72f6d22018-10-04 23:29:011387 if 'dimensions' in swarming_mixin:
Garrett Beatyade673d2023-08-04 22:00:251388 new_test['swarming'].setdefault('dimensions', {}).update(
1389 swarming_mixin.pop('dimensions'))
Garrett Beaty4c35b142023-06-23 21:01:231390 if 'named_caches' in swarming_mixin:
1391 new_test['swarming'].setdefault('named_caches', []).extend(
1392 swarming_mixin['named_caches'])
1393 del swarming_mixin['named_caches']
Stephen Martinisb72f6d22018-10-04 23:29:011394 # python dict update doesn't do recursion at all. Just hard code the
1395 # nested update we need (mixin['swarming'] shouldn't clobber
1396 # test['swarming'], but should update it).
1397 new_test['swarming'].update(swarming_mixin)
1398 del mixin['swarming']
1399
Garrett Beatye3a606ceb2024-04-30 22:13:131400 for a in ('args', 'precommit_args', 'non_precommit_args'):
Garrett Beaty4c35b142023-06-23 21:01:231401 if (value := mixin.pop(a, None)) is None:
1402 continue
1403 if not isinstance(value, list):
1404 raise BBGenErr(f'"{a}" must be a list')
1405 new_test.setdefault(a, []).extend(value)
1406
Garrett Beatye3a606ceb2024-04-30 22:13:131407 # At this point, all keys that require merging are taken care of, so the
1408 # remaining entries can be copied over. The os-conditional entries will be
1409 # resolved immediately after and they are resolved before any mixins are
1410 # applied, so there's are no concerns about overwriting the corresponding
1411 # entry in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011412 new_test.update(mixin)
Garrett Beatye3a606ceb2024-04-30 22:13:131413 if builder:
1414 self.resolve_os_conditional_values(new_test, builder)
1415
1416 if 'args' in new_test:
1417 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1418
Stephen Martinisb6a50492018-09-12 23:59:321419 return new_test
1420
Greg Gutermanf60eb052020-03-12 17:40:011421 def generate_output_tests(self, waterfall):
1422 """Generates the tests for a waterfall.
1423
1424 Args:
1425 waterfall: a dictionary parsed from a master pyl file
1426 Returns:
1427 A dictionary mapping builders to test specs
1428 """
1429 return {
Jamie Madillcf4f8c72021-05-20 19:24:231430 name: self.get_tests_for_config(waterfall, name, config)
1431 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011432 }
1433
1434 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531435 generator_map = self.get_test_generator_map()
1436 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281437
Greg Gutermanf60eb052020-03-12 17:40:011438 tests = {}
1439 # Copy only well-understood entries in the machine's configuration
1440 # verbatim into the generated JSON.
1441 if 'additional_compile_targets' in config:
1442 tests['additional_compile_targets'] = config[
1443 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231444 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011445 if test_type not in generator_map:
1446 raise self.unknown_test_suite_type(
1447 test_type, name, waterfall['name']) # pragma: no cover
1448 test_generator = generator_map[test_type]
1449 # Let multiple kinds of generators generate the same kinds
1450 # of tests. For example, gpu_telemetry_tests are a
1451 # specialization of isolated_scripts.
1452 new_tests = test_generator.generate(
1453 waterfall, name, config, input_tests)
1454 remapped_test_type = test_type_remapper.get(test_type, test_type)
Garrett Beatyffe83c4f2023-09-08 19:07:371455 tests.setdefault(remapped_test_type, []).extend(new_tests)
1456
1457 for test_type, tests_for_type in tests.items():
1458 if test_type == 'additional_compile_targets':
1459 continue
1460 tests[test_type] = sorted(tests_for_type, key=lambda t: t['name'])
Greg Gutermanf60eb052020-03-12 17:40:011461
1462 return tests
1463
1464 def jsonify(self, all_tests):
1465 return json.dumps(
1466 all_tests, indent=2, separators=(',', ': '),
1467 sort_keys=True) + '\n'
1468
1469 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281470 self.load_configuration_files()
1471 self.resolve_configuration_files()
1472 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011473 result = collections.defaultdict(dict)
1474
Stephanie Kim572b43c02023-04-13 14:24:131475 if os.path.exists(self.args.autoshard_exceptions_json_path):
1476 autoshards = json.loads(
1477 self.read_file(self.args.autoshard_exceptions_json_path))
1478 else:
1479 autoshards = {}
1480
Dirk Pranke6269d302020-10-01 00:14:391481 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011482 for waterfall in self.waterfalls:
1483 for field in required_fields:
1484 # Verify required fields
1485 if field not in waterfall:
1486 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1487
1488 # Handle filter flag, if specified
1489 if filters and waterfall['name'] not in filters:
1490 continue
1491
1492 # Join config files and hardcoded values together
1493 all_tests = self.generate_output_tests(waterfall)
1494 result[waterfall['name']] = all_tests
1495
Stephanie Kim572b43c02023-04-13 14:24:131496 if not autoshards:
1497 continue
1498 for builder, test_spec in all_tests.items():
1499 for target_type, test_list in test_spec.items():
1500 if target_type == 'additional_compile_targets':
1501 continue
1502 for test_dict in test_list:
1503 # Suites that apply variants or other customizations will create
1504 # test_dicts that have "name" value that is different from the
Garrett Beatyffe83c4f2023-09-08 19:07:371505 # "test" value.
Stephanie Kim572b43c02023-04-13 14:24:131506 # e.g. name = vulkan_swiftshader_content_browsertests, but
1507 # test = content_browsertests and
1508 # test_id_prefix = "ninja://content/test:content_browsertests/"
Garrett Beatyffe83c4f2023-09-08 19:07:371509 test_name = test_dict['name']
Stephanie Kim572b43c02023-04-13 14:24:131510 shard_info = autoshards.get(waterfall['name'],
1511 {}).get(builder, {}).get(test_name)
1512 if shard_info:
1513 test_dict['swarming'].update(
1514 {'shards': int(shard_info['shards'])})
1515
Greg Gutermanf60eb052020-03-12 17:40:011516 # Add do not edit warning
1517 for tests in result.values():
1518 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1519 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1520
1521 return result
1522
1523 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281524 suffix = '.json'
1525 if self.args.new_files:
1526 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011527
1528 for filename, contents in result.items():
1529 jsonstr = self.jsonify(contents)
Garrett Beaty79339e182023-04-10 20:45:471530 file_path = os.path.join(self.args.output_dir, filename + suffix)
1531 self.write_file(file_path, jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281532
Nico Weberd18b8962018-05-16 19:39:381533 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161534 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421535 # NOTE: This reference can cause issues; if a file changes there, the
1536 # presubmit here won't be run by default. A manually maintained list there
1537 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1538 # references to configs outside of this directory are added, please change
1539 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1540 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491541
Garrett Beaty7e866fc2021-06-16 14:12:101542 # Get the generated project.pyl so we can check if we should be enforcing
1543 # that the specs are for builders that actually exist
1544 # If not, return None to indicate that we won't enforce that builders in
1545 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491546 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1547 'project.pyl')
1548 if os.path.exists(project_pyl_path):
1549 settings = ast.literal_eval(self.read_file(project_pyl_path))
1550 if not settings.get('validate_source_side_specs_have_builder', True):
1551 return None
1552
Nico Weberd18b8962018-05-16 19:39:381553 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311554 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161555 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1556 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431557 for c in milo_configs:
1558 for l in self.read_file(c).splitlines():
1559 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311560 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431561 continue
1562 # l looks like
1563 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1564 # Extract win_chromium_dbg_ng part.
1565 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381566 return bot_names
1567
Ben Pastene9a010082019-09-25 20:41:371568 def get_internal_waterfalls(self):
1569 # Similar to get_builders_that_do_not_actually_exist above, but for
1570 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201571 return [
Kramer Ge3bf853a2023-04-13 19:39:471572 'chrome', 'chrome.pgo', 'chrome.gpu.fyi', 'internal.chrome.fyi',
yoshiki iguchi4de608082024-03-14 00:33:361573 'internal.chromeos.fyi', 'internal.optimization_guide', 'internal.soda',
1574 'chromeos.preuprev'
Yuke Liaoe6c23dd2021-07-28 16:12:201575 ]
Ben Pastene9a010082019-09-25 20:41:371576
Stephen Martinisf83893722018-09-19 00:02:181577 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201578 self.check_input_files_sorting(verbose)
1579
Kenneth Russelleb60cbd22017-12-05 07:54:281580 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011581 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381582 self.check_composition_type_test_suites('matrix_compound_suites',
1583 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111584 self.resolve_test_id_prefixes()
Garrett Beaty1ead4a52023-12-07 19:16:421585
1586 # All test suites must be referenced. Check this before flattening the test
1587 # suites so that we can transitively check the basic suites for compound
1588 # suites and matrix compound suites (otherwise we would determine a basic
1589 # suite is used if it shared a name with a test present in a basic suite
1590 # that is used).
1591 all_suites = set(
1592 itertools.chain(*(self.test_suites.get(a, {}) for a in (
1593 'basic_suites',
1594 'compound_suites',
1595 'matrix_compound_suites',
1596 ))))
1597 unused_suites = set(all_suites)
1598 generator_map = self.get_test_generator_map()
1599 for waterfall in self.waterfalls:
1600 for bot_name, tester in waterfall['machines'].items():
1601 for suite_type, suite in tester.get('test_suites', {}).items():
1602 if suite_type not in generator_map:
1603 raise self.unknown_test_suite_type(suite_type, bot_name,
1604 waterfall['name'])
1605 if suite not in all_suites:
1606 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1607 unused_suites.discard(suite)
1608 # For each compound suite or matrix compound suite, if the suite was used,
1609 # remove all of the basic suites that it composes from the set of unused
1610 # suites
1611 for a in ('compound_suites', 'matrix_compound_suites'):
1612 for suite, sub_suites in self.test_suites.get(a, {}).items():
1613 if suite not in unused_suites:
1614 unused_suites.difference_update(sub_suites)
1615 if unused_suites:
1616 raise BBGenErr('The following test suites were unreferenced by bots on '
1617 'the waterfalls: ' + str(unused_suites))
1618
Stephen Martinis54d64ad2018-09-21 22:16:201619 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381620
1621 # All bots should exist.
1622 bot_names = self.get_valid_bot_names()
Garrett Beaty2a02de3c2020-05-15 13:57:351623 if bot_names is not None:
1624 internal_waterfalls = self.get_internal_waterfalls()
1625 for waterfall in self.waterfalls:
Alison Gale923a33e2024-04-22 23:34:281626 # TODO(crbug.com/41474799): Remove the need for this exception.
Garrett Beaty2a02de3c2020-05-15 13:57:351627 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011628 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351629 for bot_name in waterfall['machines']:
Garrett Beaty2a02de3c2020-05-15 13:57:351630 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581631 if waterfall['name'] in [
1632 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1633 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351634 # TODO(thakis): Remove this once these bots move to luci.
1635 continue # pragma: no cover
1636 if waterfall['name'] in ['tryserver.webrtc',
1637 'webrtc.chromium.fyi.experimental']:
1638 # These waterfalls have their bot configs in a different repo.
1639 # so we don't know about their bot names.
1640 continue # pragma: no cover
1641 if waterfall['name'] in ['client.devtools-frontend.integration',
1642 'tryserver.devtools-frontend',
1643 'chromium.devtools-frontend']:
1644 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201645 if waterfall['name'] in ['client.openscreen.chromium']:
1646 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351647 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381648
Kenneth Russelleb60cbd22017-12-05 07:54:281649 # All test suite exceptions must refer to bots on the waterfall.
1650 all_bots = set()
1651 missing_bots = set()
1652 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231653 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281654 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281655 # In order to disambiguate between bots with the same name on
1656 # different waterfalls, support has been added to various
1657 # exceptions for concatenating the waterfall name after the bot
1658 # name.
1659 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231660 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381661 removals = (exception.get('remove_from', []) +
1662 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231663 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381664 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281665 if removal not in all_bots:
1666 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411667
Kenneth Russelleb60cbd22017-12-05 07:54:281668 if missing_bots:
1669 raise BBGenErr('The following nonexistent machines were referenced in '
1670 'the test suite exceptions: ' + str(missing_bots))
1671
Garrett Beatyb061e69d2023-06-27 16:15:351672 for name, mixin in self.mixins.items():
1673 if '$mixin_append' in mixin:
1674 raise BBGenErr(
1675 f'$mixin_append is no longer supported (set in mixin "{name}"),'
1676 ' args and named caches specified as normal will be appended')
1677
Stephen Martinis0382bc12018-09-17 22:29:071678 # All mixins must be referenced
1679 seen_mixins = set()
1680 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011681 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231682 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011683 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071684 for suite in self.test_suites.values():
1685 if isinstance(suite, list):
1686 # Don't care about this, it's a composition, which shouldn't include a
1687 # swarming mixin.
1688 continue
1689
1690 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561691 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011692 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071693
Zhaoyang Li9da047d52021-05-10 21:31:441694 for variant in self.variants:
1695 # Unpack the variant from variants.pyl if it's string based.
1696 if isinstance(variant, str):
1697 variant = self.variants[variant]
1698 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1699
Stephen Martinisb72f6d22018-10-04 23:29:011700 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071701 if missing_mixins:
1702 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1703 ' referenced in a waterfall, machine, or test suite.' % (
1704 str(missing_mixins)))
1705
Jeff Yoonda581c32020-03-06 03:56:051706 # All variant references must be referenced
1707 seen_variants = set()
1708 for suite in self.test_suites.values():
1709 if isinstance(suite, list):
1710 continue
1711
1712 for test in suite.values():
1713 if isinstance(test, dict):
1714 for variant in test.get('variants', []):
1715 if isinstance(variant, str):
1716 seen_variants.add(variant)
1717
1718 missing_variants = set(self.variants.keys()) - seen_variants
1719 if missing_variants:
1720 raise BBGenErr('The following variants were unreferenced: %s. They must '
1721 'be referenced in a matrix test suite under the variants '
1722 'key.' % str(missing_variants))
1723
Stephen Martinis54d64ad2018-09-21 22:16:201724
Garrett Beaty79339e182023-04-10 20:45:471725 def type_assert(self, node, typ, file_path, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201726 """Asserts that the Python AST node |node| is of type |typ|.
1727
1728 If verbose is set, it prints out some helpful context lines, showing where
1729 exactly the error occurred in the file.
1730 """
1731 if not isinstance(node, typ):
1732 if verbose:
Garrett Beaty79339e182023-04-10 20:45:471733 lines = [""] + self.read_file(file_path).splitlines()
Stephen Martinis54d64ad2018-09-21 22:16:201734
1735 context = 2
1736 lines_start = max(node.lineno - context, 0)
1737 # Add one to include the last line
1738 lines_end = min(node.lineno + context, len(lines)) + 1
Garrett Beaty79339e182023-04-10 20:45:471739 lines = itertools.chain(
1740 ['== %s ==\n' % file_path],
1741 ["<snip>\n"],
1742 [
1743 '%d %s' % (lines_start + i, line)
1744 for i, line in enumerate(lines[lines_start:lines_start +
1745 context])
1746 ],
1747 ['-' * 80 + '\n'],
1748 ['%d %s' % (node.lineno, lines[node.lineno])],
1749 [
1750 '-' * (node.col_offset + 3) + '^' + '-' *
1751 (80 - node.col_offset - 4) + '\n'
1752 ],
1753 [
1754 '%d %s' % (node.lineno + 1 + i, line)
1755 for i, line in enumerate(lines[node.lineno + 1:lines_end])
1756 ],
1757 ["<snip>\n"],
Stephen Martinis54d64ad2018-09-21 22:16:201758 )
1759 # Print out a useful message when a type assertion fails.
1760 for l in lines:
1761 self.print_line(l.strip())
1762
1763 node_dumped = ast.dump(node, annotate_fields=False)
1764 # If the node is huge, truncate it so everything fits in a terminal
1765 # window.
1766 if len(node_dumped) > 60: # pragma: no cover
1767 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1768 raise BBGenErr(
Garrett Beaty807011ab2023-04-12 00:52:391769 'Invalid .pyl file \'%s\'. Python AST node %r on line %s expected to'
Garrett Beaty79339e182023-04-10 20:45:471770 ' be %s, is %s' %
1771 (file_path, node_dumped, node.lineno, typ, type(node)))
Stephen Martinis54d64ad2018-09-21 22:16:201772
Garrett Beaty79339e182023-04-10 20:45:471773 def check_ast_list_formatted(self,
1774 keys,
1775 file_path,
1776 verbose,
Stephen Martinis1384ff92020-01-07 19:52:151777 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531778 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201779
Stephen Martinis5bef0fc2020-01-06 22:47:531780 Currently only checks to ensure they're correctly sorted, and that there
1781 are no duplicates.
1782
1783 Args:
1784 keys: An python list of AST nodes.
1785
1786 It's a list of AST nodes instead of a list of strings because
1787 when verbose is set, it tries to print out context of where the
1788 diffs are in the file.
Garrett Beaty79339e182023-04-10 20:45:471789 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531790 verbose: If set, print out diff information about how the keys are
1791 incorrectly formatted.
1792 check_sorting: If true, checks if the list is sorted.
1793 Returns:
1794 If the keys are correctly formatted.
1795 """
1796 if not keys:
1797 return True
1798
1799 assert isinstance(keys[0], ast.Str)
1800
1801 keys_strs = [k.s for k in keys]
1802 # Keys to diff against. Used below.
1803 keys_to_diff_against = None
1804 # If the list is properly formatted.
1805 list_formatted = True
1806
1807 # Duplicates are always bad.
1808 if len(set(keys_strs)) != len(keys_strs):
1809 list_formatted = False
1810 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1811
1812 if check_sorting and sorted(keys_strs) != keys_strs:
1813 list_formatted = False
1814 if list_formatted:
1815 return True
1816
1817 if verbose:
1818 line_num = keys[0].lineno
1819 keys = [k.s for k in keys]
1820 if check_sorting:
1821 # If we have duplicates, sorting this will take care of it anyways.
1822 keys_to_diff_against = sorted(set(keys))
1823 # else, keys_to_diff_against is set above already
1824
1825 self.print_line('=' * 80)
1826 self.print_line('(First line of keys is %s)' % line_num)
Garrett Beaty79339e182023-04-10 20:45:471827 for line in difflib.context_diff(keys,
1828 keys_to_diff_against,
1829 fromfile='current (%r)' % file_path,
1830 tofile='sorted',
1831 lineterm=''):
Stephen Martinis5bef0fc2020-01-06 22:47:531832 self.print_line(line)
1833 self.print_line('=' * 80)
1834
1835 return False
1836
Garrett Beaty79339e182023-04-10 20:45:471837 def check_ast_dict_formatted(self, node, file_path, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531838 """Checks if an ast dictionary's keys are correctly formatted.
1839
1840 Just a simple wrapper around check_ast_list_formatted.
1841 Args:
1842 node: An AST node. Assumed to be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471843 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531844 verbose: If set, print out diff information about how the keys are
1845 incorrectly formatted.
1846 check_sorting: If true, checks if the list is sorted.
1847 Returns:
1848 If the dictionary is correctly formatted.
1849 """
Stephen Martinis54d64ad2018-09-21 22:16:201850 keys = []
1851 # The keys of this dict are ordered as ordered in the file; normal python
1852 # dictionary keys are given an arbitrary order, but since we parsed the
1853 # file itself, the order as given in the file is preserved.
1854 for key in node.keys:
Garrett Beaty79339e182023-04-10 20:45:471855 self.type_assert(key, ast.Str, file_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531856 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201857
Garrett Beaty79339e182023-04-10 20:45:471858 return self.check_ast_list_formatted(keys, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181859
1860 def check_input_files_sorting(self, verbose=False):
Alison Gale923a33e2024-04-22 23:34:281861 # TODO(crbug.com/41415841): Add the ability for this script to
Stephen Martinis54d64ad2018-09-21 22:16:201862 # actually format the files, rather than just complain if they're
1863 # incorrectly formatted.
1864 bad_files = set()
Garrett Beaty79339e182023-04-10 20:45:471865
1866 def parse_file(file_path):
Stephen Martinis5bef0fc2020-01-06 22:47:531867 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201868
Stephen Martinis5bef0fc2020-01-06 22:47:531869 Returns an AST node representing the value in the pyl file."""
Garrett Beaty79339e182023-04-10 20:45:471870 parsed = ast.parse(self.read_file(file_path))
Stephen Martinisf83893722018-09-19 00:02:181871
Stephen Martinisf83893722018-09-19 00:02:181872 # Must be a module.
Garrett Beaty79339e182023-04-10 20:45:471873 self.type_assert(parsed, ast.Module, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181874 module = parsed.body
1875
1876 # Only one expression in the module.
Garrett Beaty79339e182023-04-10 20:45:471877 self.type_assert(module, list, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181878 if len(module) != 1: # pragma: no cover
Garrett Beaty79339e182023-04-10 20:45:471879 raise BBGenErr('Invalid .pyl file %s' % file_path)
Stephen Martinisf83893722018-09-19 00:02:181880 expr = module[0]
Garrett Beaty79339e182023-04-10 20:45:471881 self.type_assert(expr, ast.Expr, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181882
Stephen Martinis5bef0fc2020-01-06 22:47:531883 return expr.value
1884
1885 # Handle this separately
Garrett Beaty79339e182023-04-10 20:45:471886 value = parse_file(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531887 # Value should be a list.
Garrett Beaty79339e182023-04-10 20:45:471888 self.type_assert(value, ast.List, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531889
1890 keys = []
Joshua Hood56c673c2022-03-02 20:29:331891 for elm in value.elts:
Garrett Beaty79339e182023-04-10 20:45:471892 self.type_assert(elm, ast.Dict, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531893 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331894 for key, val in zip(elm.keys, elm.values):
Garrett Beaty79339e182023-04-10 20:45:471895 self.type_assert(key, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531896 if key.s == 'machines':
Garrett Beaty79339e182023-04-10 20:45:471897 if not self.check_ast_dict_formatted(
1898 val, self.args.waterfalls_pyl_path, verbose):
1899 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531900
1901 if key.s == "name":
Garrett Beaty79339e182023-04-10 20:45:471902 self.type_assert(val, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531903 waterfall_name = val
1904 assert waterfall_name
1905 keys.append(waterfall_name)
1906
Garrett Beaty79339e182023-04-10 20:45:471907 if not self.check_ast_list_formatted(keys, self.args.waterfalls_pyl_path,
1908 verbose):
1909 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531910
Garrett Beaty79339e182023-04-10 20:45:471911 for file_path in (
1912 self.args.mixins_pyl_path,
1913 self.args.test_suites_pyl_path,
1914 self.args.test_suite_exceptions_pyl_path,
Stephen Martinis5bef0fc2020-01-06 22:47:531915 ):
Garrett Beaty79339e182023-04-10 20:45:471916 value = parse_file(file_path)
Stephen Martinisf83893722018-09-19 00:02:181917 # Value should be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471918 self.type_assert(value, ast.Dict, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181919
Garrett Beaty79339e182023-04-10 20:45:471920 if not self.check_ast_dict_formatted(value, file_path, verbose):
1921 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531922
Garrett Beaty79339e182023-04-10 20:45:471923 if file_path == self.args.test_suites_pyl_path:
Jeff Yoon8154e582019-12-03 23:30:011924 expected_keys = ['basic_suites',
1925 'compound_suites',
1926 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201927 actual_keys = [node.s for node in value.keys]
1928 assert all(key in expected_keys for key in actual_keys), (
Garrett Beaty79339e182023-04-10 20:45:471929 'Invalid %r file; expected keys %r, got %r' %
1930 (file_path, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:331931 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:201932 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011933 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201934 for suite_group in suite_dicts:
Garrett Beaty79339e182023-04-10 20:45:471935 if not self.check_ast_dict_formatted(suite_group, file_path, verbose):
1936 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181937
Stephen Martinis5bef0fc2020-01-06 22:47:531938 for key, suite in zip(value.keys, value.values):
1939 # The compound suites are checked in
1940 # 'check_composition_type_test_suites()'
1941 if key.s == 'basic_suites':
1942 for group in suite.values:
Garrett Beaty79339e182023-04-10 20:45:471943 if not self.check_ast_dict_formatted(group, file_path, verbose):
1944 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531945 break
Stephen Martinis54d64ad2018-09-21 22:16:201946
Garrett Beaty79339e182023-04-10 20:45:471947 elif file_path == self.args.test_suite_exceptions_pyl_path:
Stephen Martinis5bef0fc2020-01-06 22:47:531948 # Check the values for each test.
1949 for test in value.values:
1950 for kind, node in zip(test.keys, test.values):
1951 if isinstance(node, ast.Dict):
Garrett Beaty79339e182023-04-10 20:45:471952 if not self.check_ast_dict_formatted(node, file_path, verbose):
1953 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531954 elif kind.s == 'remove_from':
1955 # Don't care about sorting; these are usually grouped, since the
1956 # same bug can affect multiple builders. Do want to make sure
1957 # there aren't duplicates.
Garrett Beaty79339e182023-04-10 20:45:471958 if not self.check_ast_list_formatted(
1959 node.elts, file_path, verbose, check_sorting=False):
1960 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181961
1962 if bad_files:
1963 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201964 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531965 'unsorted, or have duplicates. Re-run this with --verbose to see '
1966 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181967
Kenneth Russelleb60cbd22017-12-05 07:54:281968 def check_output_file_consistency(self, verbose=False):
1969 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011970 # All waterfalls/bucket .json files must have been written
1971 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:281972 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011973 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:161974 outputs = self.generate_outputs()
1975 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:011976 expected = self.jsonify(expected_contents)
Garrett Beaty79339e182023-04-10 20:45:471977 file_path = os.path.join(self.args.output_dir, filename + '.json')
Ben Pastenef21cda32023-03-30 22:00:571978 current = self.read_file(file_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281979 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:011980 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:321981 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:011982 self.print_line('File ' + filename +
1983 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321984 'contents:')
1985 for line in difflib.unified_diff(
1986 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501987 current.splitlines(),
1988 fromfile='expected', tofile='current'):
1989 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:011990
1991 if ungenerated_files:
1992 raise BBGenErr(
1993 'The following files have not been properly '
1994 'autogenerated by generate_buildbot_json.py: ' +
1995 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:281996
Dirk Pranke772f55f2021-04-28 04:51:161997 for builder_group, builders in outputs.items():
1998 for builder, step_types in builders.items():
Garrett Beatydca3d882023-09-14 23:50:321999 for test_type in ('gtest_tests', 'isolated_scripts'):
2000 for step_data in step_types.get(test_type, []):
2001 step_name = step_data['name']
2002 self._check_swarming_config(builder_group, builder, step_name,
2003 step_data)
Dirk Pranke772f55f2021-04-28 04:51:162004
2005 def _check_swarming_config(self, filename, builder, step_name, step_data):
Alison Gale47d1537d2024-04-19 21:31:462006 # TODO(crbug.com/40179524): Ensure all swarming tests specify cpu, not
Dirk Pranke772f55f2021-04-28 04:51:162007 # just mac tests.
Garrett Beatybb18d532023-06-26 22:16:332008 if 'swarming' in step_data:
Garrett Beatyade673d2023-08-04 22:00:252009 dimensions = step_data['swarming'].get('dimensions')
2010 if not dimensions:
Tatsuhisa Yamaguchif1878d52023-11-06 06:02:252011 raise BBGenErr('%s: %s / %s : dimensions must be specified for all '
Dirk Pranke772f55f2021-04-28 04:51:162012 'swarmed tests' % (filename, builder, step_name))
Garrett Beatyade673d2023-08-04 22:00:252013 if not dimensions.get('os'):
2014 raise BBGenErr('%s: %s / %s : os must be specified for all '
2015 'swarmed tests' % (filename, builder, step_name))
2016 if 'Mac' in dimensions.get('os') and not dimensions.get('cpu'):
2017 raise BBGenErr('%s: %s / %s : cpu must be specified for mac '
2018 'swarmed tests' % (filename, builder, step_name))
Dirk Pranke772f55f2021-04-28 04:51:162019
Kenneth Russelleb60cbd22017-12-05 07:54:282020 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502021 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282022 self.check_output_file_consistency(verbose) # pragma: no cover
2023
Karen Qiane24b7ee2019-02-12 23:37:062024 def does_test_match(self, test_info, params_dict):
2025 """Checks to see if the test matches the parameters given.
2026
2027 Compares the provided test_info with the params_dict to see
2028 if the bot matches the parameters given. If so, returns True.
2029 Else, returns false.
2030
2031 Args:
2032 test_info (dict): Information about a specific bot provided
2033 in the format shown in waterfalls.pyl
2034 params_dict (dict): Dictionary of parameters and their values
2035 to look for in the bot
2036 Ex: {
2037 'device_os':'android',
2038 '--flag':True,
2039 'mixins': ['mixin1', 'mixin2'],
2040 'ex_key':'ex_value'
2041 }
2042
2043 """
2044 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2045 'kvm', 'pool', 'integrity'] # dimension parameters
2046 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2047 'can_use_on_swarming_builders']
2048 for param in params_dict:
2049 # if dimension parameter
2050 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2051 if not 'swarming' in test_info:
2052 return False
2053 swarming = test_info['swarming']
2054 if param in SWARMING_PARAMS:
2055 if not param in swarming:
2056 return False
2057 if not str(swarming[param]) == params_dict[param]:
2058 return False
2059 else:
Garrett Beatyade673d2023-08-04 22:00:252060 if not 'dimensions' in swarming:
Karen Qiane24b7ee2019-02-12 23:37:062061 return False
Garrett Beatyade673d2023-08-04 22:00:252062 dimensions = swarming['dimensions']
Karen Qiane24b7ee2019-02-12 23:37:062063 # only looking at the first dimension set
Garrett Beatyade673d2023-08-04 22:00:252064 if not param in dimensions:
Karen Qiane24b7ee2019-02-12 23:37:062065 return False
Garrett Beatyade673d2023-08-04 22:00:252066 if not dimensions[param] == params_dict[param]:
Karen Qiane24b7ee2019-02-12 23:37:062067 return False
2068
2069 # if flag
2070 elif param.startswith('--'):
2071 if not 'args' in test_info:
2072 return False
2073 if not param in test_info['args']:
2074 return False
2075
2076 # not dimension parameter/flag/mixin
2077 else:
2078 if not param in test_info:
2079 return False
2080 if not test_info[param] == params_dict[param]:
2081 return False
2082 return True
2083 def error_msg(self, msg):
2084 """Prints an error message.
2085
2086 In addition to a catered error message, also prints
2087 out where the user can find more help. Then, program exits.
2088 """
2089 self.print_line(msg + (' If you need more information, ' +
2090 'please run with -h or --help to see valid commands.'))
2091 sys.exit(1)
2092
2093 def find_bots_that_run_test(self, test, bots):
2094 matching_bots = []
2095 for bot in bots:
2096 bot_info = bots[bot]
2097 tests = self.flatten_tests_for_bot(bot_info)
2098 for test_info in tests:
Garrett Beatyffe83c4f2023-09-08 19:07:372099 test_name = test_info['name']
Karen Qiane24b7ee2019-02-12 23:37:062100 if not test_name == test:
2101 continue
2102 matching_bots.append(bot)
2103 return matching_bots
2104
2105 def find_tests_with_params(self, tests, params_dict):
2106 matching_tests = []
2107 for test_name in tests:
2108 test_info = tests[test_name]
2109 if not self.does_test_match(test_info, params_dict):
2110 continue
2111 if not test_name in matching_tests:
2112 matching_tests.append(test_name)
2113 return matching_tests
2114
2115 def flatten_waterfalls_for_query(self, waterfalls):
2116 bots = {}
2117 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012118 waterfall_tests = self.generate_output_tests(waterfall)
2119 for bot in waterfall_tests:
2120 bot_info = waterfall_tests[bot]
2121 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062122 return bots
2123
2124 def flatten_tests_for_bot(self, bot_info):
2125 """Returns a list of flattened tests.
2126
2127 Returns a list of tests not grouped by test category
2128 for a specific bot.
2129 """
2130 TEST_CATS = self.get_test_generator_map().keys()
2131 tests = []
2132 for test_cat in TEST_CATS:
2133 if not test_cat in bot_info:
2134 continue
2135 test_cat_tests = bot_info[test_cat]
2136 tests = tests + test_cat_tests
2137 return tests
2138
2139 def flatten_tests_for_query(self, test_suites):
2140 """Returns a flattened dictionary of tests.
2141
2142 Returns a dictionary of tests associate with their
2143 configuration, not grouped by their test suite.
2144 """
2145 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232146 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062147 for test in test_suite:
2148 test_info = test_suite[test]
2149 test_name = test
Karen Qiane24b7ee2019-02-12 23:37:062150 tests[test_name] = test_info
2151 return tests
2152
2153 def parse_query_filter_params(self, params):
2154 """Parses the filter parameters.
2155
2156 Creates a dictionary from the parameters provided
2157 to filter the bot array.
2158 """
2159 params_dict = {}
2160 for p in params:
2161 # flag
2162 if p.startswith("--"):
2163 params_dict[p] = True
2164 else:
2165 pair = p.split(":")
2166 if len(pair) != 2:
2167 self.error_msg('Invalid command.')
2168 # regular parameters
2169 if pair[1].lower() == "true":
2170 params_dict[pair[0]] = True
2171 elif pair[1].lower() == "false":
2172 params_dict[pair[0]] = False
2173 else:
2174 params_dict[pair[0]] = pair[1]
2175 return params_dict
2176
2177 def get_test_suites_dict(self, bots):
2178 """Returns a dictionary of bots and their tests.
2179
2180 Returns a dictionary of bots and a list of their associated tests.
2181 """
2182 test_suite_dict = dict()
2183 for bot in bots:
2184 bot_info = bots[bot]
2185 tests = self.flatten_tests_for_bot(bot_info)
2186 test_suite_dict[bot] = tests
2187 return test_suite_dict
2188
2189 def output_query_result(self, result, json_file=None):
2190 """Outputs the result of the query.
2191
2192 If a json file parameter name is provided, then
2193 the result is output into the json file. If not,
2194 then the result is printed to the console.
2195 """
2196 output = json.dumps(result, indent=2)
2197 if json_file:
2198 self.write_file(json_file, output)
2199 else:
2200 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062201
Joshua Hood56c673c2022-03-02 20:29:332202 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062203 def query(self, args):
2204 """Queries tests or bots.
2205
2206 Depending on the arguments provided, outputs a json of
2207 tests or bots matching the appropriate optional parameters provided.
2208 """
2209 # split up query statement
2210 query = args.query.split('/')
2211 self.load_configuration_files()
2212 self.resolve_configuration_files()
2213
2214 # flatten bots json
2215 tests = self.test_suites
2216 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2217
2218 cmd_class = query[0]
2219
2220 # For queries starting with 'bots'
2221 if cmd_class == "bots":
2222 if len(query) == 1:
2223 return self.output_query_result(bots, args.json)
2224 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332225 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062226 if query[1] == 'tests':
2227 test_suites_dict = self.get_test_suites_dict(bots)
2228 return self.output_query_result(test_suites_dict, args.json)
Joshua Hood56c673c2022-03-02 20:29:332229 self.error_msg("This query should be in the format: bots/tests.")
Karen Qiane24b7ee2019-02-12 23:37:062230
2231 else:
2232 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2233 % str(len(query)-1))
2234
2235 # For queries starting with 'bot'
2236 elif cmd_class == "bot":
2237 if not len(query) == 2 and not len(query) == 3:
2238 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2239 % str(len(query)-1))
2240 bot_id = query[1]
2241 if not bot_id in bots:
2242 self.error_msg("No bot named '" + bot_id + "' found.")
2243 bot_info = bots[bot_id]
2244 if len(query) == 2:
2245 return self.output_query_result(bot_info, args.json)
2246 if not query[2] == 'tests':
2247 self.error_msg("The query should be in the format:" +
2248 "bot/<bot-name>/tests.")
2249
2250 bot_tests = self.flatten_tests_for_bot(bot_info)
2251 return self.output_query_result(bot_tests, args.json)
2252
2253 # For queries starting with 'tests'
2254 elif cmd_class == "tests":
2255 if not len(query) == 1 and not len(query) == 2:
2256 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2257 % str(len(query)-1))
2258 flattened_tests = self.flatten_tests_for_query(tests)
2259 if len(query) == 1:
2260 return self.output_query_result(flattened_tests, args.json)
2261
2262 # create params dict
2263 params = query[1].split('&')
2264 params_dict = self.parse_query_filter_params(params)
2265 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2266 return self.output_query_result(matching_bots)
2267
2268 # For queries starting with 'test'
2269 elif cmd_class == "test":
2270 if not len(query) == 2 and not len(query) == 3:
2271 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2272 % str(len(query)-1))
2273 test_id = query[1]
2274 if len(query) == 2:
2275 flattened_tests = self.flatten_tests_for_query(tests)
2276 for test in flattened_tests:
2277 if test == test_id:
2278 return self.output_query_result(flattened_tests[test], args.json)
2279 self.error_msg("There is no test named %s." % test_id)
2280 if not query[2] == 'bots':
2281 self.error_msg("The query should be in the format: " +
2282 "test/<test-name>/bots")
2283 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2284 return self.output_query_result(bots_for_test)
2285
2286 else:
2287 self.error_msg("Your command did not match any valid commands." +
2288 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Joshua Hood56c673c2022-03-02 20:29:332289 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282290
Garrett Beaty1afaccc2020-06-25 19:58:152291 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282292 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502293 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062294 elif self.args.query:
2295 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282296 else:
Greg Gutermanf60eb052020-03-12 17:40:012297 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282298 return 0
2299
2300if __name__ == "__main__": # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152301 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2302 sys.exit(generator.main())