blob: 063d7682a385051689b775c8012c0fbfc5a73a5a [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
Brian Sheedy822e03742024-08-09 18:48:1415import functools
Garrett Beatyd5ca75962020-05-07 16:58:3116import glob
Kenneth Russell8ceeabf2017-12-11 17:53:2817import itertools
Kenneth Russelleb60cbd22017-12-05 07:54:2818import json
19import os
20import string
21import sys
22
Brian Sheedyfe2702e2024-12-13 21:48:2023# //testing/buildbot imports.
Brian Sheedya31578e2020-05-18 20:24:3624import buildbot_json_magic_substitutions as magic_substitutions
25
Joshua Hood56c673c2022-03-02 20:29:3326# pylint: disable=super-with-arguments,useless-super-delegation
27
Brian Sheedy84721d72024-12-10 06:17:4328# Disabled instead of fixing to avoid a large amount of churn.
29# pylint: disable=no-self-use
30
Kenneth Russelleb60cbd22017-12-05 07:54:2831THIS_DIR = os.path.dirname(os.path.abspath(__file__))
32
Brian Sheedyf74819b2021-06-04 01:38:3833BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP = {
34 'android-chromium': '_android_chrome',
35 'android-chromium-monochrome': '_android_monochrome',
Brian Sheedyf74819b2021-06-04 01:38:3836 'android-webview': '_android_webview',
37}
38
Kenneth Russelleb60cbd22017-12-05 07:54:2839
40class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4941 def __init__(self, message):
42 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2843
44
Joshua Hood56c673c2022-03-02 20:29:3345class BaseGenerator(object): # pylint: disable=useless-object-inheritance
Kenneth Russelleb60cbd22017-12-05 07:54:2846 def __init__(self, bb_gen):
47 self.bb_gen = bb_gen
48
Kenneth Russell8ceeabf2017-12-11 17:53:2849 def generate(self, waterfall, tester_name, tester_config, input_tests):
Garrett Beatyffe83c4f2023-09-08 19:07:3750 raise NotImplementedError() # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:2851
52
Kenneth Russell8a386d42018-06-02 09:48:0153class GPUTelemetryTestGenerator(BaseGenerator):
Xinan Linedcf05b32023-10-19 23:13:5054 def __init__(self,
55 bb_gen,
56 is_android_webview=False,
57 is_cast_streaming=False,
58 is_skylab=False):
Kenneth Russell8a386d42018-06-02 09:48:0159 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5660 self._is_android_webview = is_android_webview
Fabrice de Ganscbd655f2022-08-04 20:15:3061 self._is_cast_streaming = is_cast_streaming
Xinan Linedcf05b32023-10-19 23:13:5062 self._is_skylab = is_skylab
Kenneth Russell8a386d42018-06-02 09:48:0163
64 def generate(self, waterfall, tester_name, tester_config, input_tests):
65 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:2366 for test_name, test_config in sorted(input_tests.items()):
Ben Pastene8e7eb2652022-04-29 19:44:3167 # Variants allow more than one definition for a given test, and is defined
68 # in array format from resolve_variants().
69 if not isinstance(test_config, list):
70 test_config = [test_config]
71
72 for config in test_config:
Xinan Linedcf05b32023-10-19 23:13:5073 test = self.bb_gen.generate_gpu_telemetry_test(
74 waterfall, tester_name, tester_config, test_name, config,
75 self._is_android_webview, self._is_cast_streaming, self._is_skylab)
Ben Pastene8e7eb2652022-04-29 19:44:3176 if test:
77 isolated_scripts.append(test)
78
Kenneth Russell8a386d42018-06-02 09:48:0179 return isolated_scripts
80
Kenneth Russell8a386d42018-06-02 09:48:0181
Brian Sheedyb6491ba2022-09-26 20:49:4982class SkylabGPUTelemetryTestGenerator(GPUTelemetryTestGenerator):
Xinan Linedcf05b32023-10-19 23:13:5083 def __init__(self, bb_gen):
84 super(SkylabGPUTelemetryTestGenerator, self).__init__(bb_gen,
85 is_skylab=True)
86
Brian Sheedyb6491ba2022-09-26 20:49:4987 def generate(self, *args, **kwargs):
88 # This should be identical to a regular GPU Telemetry test, but with any
89 # swarming arguments removed.
90 isolated_scripts = super(SkylabGPUTelemetryTestGenerator,
91 self).generate(*args, **kwargs)
92 for test in isolated_scripts:
Xinan Lind9b1d2e72022-11-14 20:57:0293 # chromium_GPU is the Autotest wrapper created for browser GPU tests
94 # run in Skylab.
Xinan Lin1f28a0d2023-03-13 17:39:4195 test['autotest_name'] = 'chromium_Graphics'
Xinan Lind9b1d2e72022-11-14 20:57:0296 # As of 22Q4, Skylab tests are running on a CrOS flavored Autotest
97 # framework and it does not support the sub-args like
98 # extra-browser-args. So we have to pop it out and create a new
99 # key for it. See crrev.com/c/3965359 for details.
100 for idx, arg in enumerate(test.get('args', [])):
101 if '--extra-browser-args' in arg:
102 test['args'].pop(idx)
103 test['extra_browser_args'] = arg.replace('--extra-browser-args=', '')
104 break
Brian Sheedyb6491ba2022-09-26 20:49:49105 return isolated_scripts
106
107
Kenneth Russelleb60cbd22017-12-05 07:54:28108class GTestGenerator(BaseGenerator):
Kenneth Russell8ceeabf2017-12-11 17:53:28109 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28110 # The relative ordering of some of the tests is important to
111 # minimize differences compared to the handwritten JSON files, since
112 # Python's sorts are stable and there are some tests with the same
113 # key (see gles2_conform_d3d9_test and similar variants). Avoid
114 # losing the order by avoiding coalescing the dictionaries into one.
115 gtests = []
Jamie Madillcf4f8c72021-05-20 19:24:23116 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoon67c3e832020-02-08 07:39:38117 # Variants allow more than one definition for a given test, and is defined
118 # in array format from resolve_variants().
119 if not isinstance(test_config, list):
120 test_config = [test_config]
121
122 for config in test_config:
123 test = self.bb_gen.generate_gtest(
124 waterfall, tester_name, tester_config, test_name, config)
125 if test:
126 # generate_gtest may veto the test generation on this tester.
127 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28128 return gtests
129
Kenneth Russelleb60cbd22017-12-05 07:54:28130
131class IsolatedScriptTestGenerator(BaseGenerator):
Kenneth Russell8ceeabf2017-12-11 17:53:28132 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28133 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23134 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43135 # Variants allow more than one definition for a given test, and is defined
136 # in array format from resolve_variants().
137 if not isinstance(test_config, list):
138 test_config = [test_config]
139
140 for config in test_config:
141 test = self.bb_gen.generate_isolated_script_test(
142 waterfall, tester_name, tester_config, test_name, config)
143 if test:
144 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28145 return isolated_scripts
146
Kenneth Russelleb60cbd22017-12-05 07:54:28147
148class ScriptGenerator(BaseGenerator):
Kenneth Russell8ceeabf2017-12-11 17:53:28149 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28150 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23151 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28152 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28153 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28154 if test:
155 scripts.append(test)
156 return scripts
157
Kenneth Russelleb60cbd22017-12-05 07:54:28158
Xinan Lin05fb9c1752020-12-17 00:15:52159class SkylabGenerator(BaseGenerator):
Xinan Lin05fb9c1752020-12-17 00:15:52160 def generate(self, waterfall, tester_name, tester_config, input_tests):
161 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23162 for test_name, test_config in sorted(input_tests.items()):
Xinan Lin05fb9c1752020-12-17 00:15:52163 for config in test_config:
164 test = self.bb_gen.generate_skylab_test(waterfall, tester_name,
165 tester_config, test_name,
166 config)
167 if test:
168 scripts.append(test)
169 return scripts
170
Xinan Lin05fb9c1752020-12-17 00:15:52171
Jeff Yoon67c3e832020-02-08 07:39:38172def check_compound_references(other_test_suites=None,
173 sub_suite=None,
174 suite=None,
175 target_test_suites=None,
176 test_type=None,
177 **kwargs):
178 """Ensure comound reference's don't target other compounds"""
179 del kwargs
180 if sub_suite in other_test_suites or sub_suite in target_test_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15181 raise BBGenErr('%s may not refer to other composition type test '
182 'suites (error found while processing %s)' %
183 (test_type, suite))
184
Jeff Yoon67c3e832020-02-08 07:39:38185
186def check_basic_references(basic_suites=None,
187 sub_suite=None,
188 suite=None,
189 **kwargs):
190 """Ensure test has a basic suite reference"""
191 del kwargs
192 if sub_suite not in basic_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15193 raise BBGenErr('Unable to find reference to %s while processing %s' %
194 (sub_suite, suite))
195
Jeff Yoon67c3e832020-02-08 07:39:38196
197def check_conflicting_definitions(basic_suites=None,
198 seen_tests=None,
199 sub_suite=None,
200 suite=None,
201 test_type=None,
Garrett Beaty235c1412023-08-29 20:26:29202 target_test_suites=None,
Jeff Yoon67c3e832020-02-08 07:39:38203 **kwargs):
204 """Ensure that if a test is reachable via multiple basic suites,
205 all of them have an identical definition of the tests.
206 """
207 del kwargs
Garrett Beaty235c1412023-08-29 20:26:29208 variants = None
209 if test_type == 'matrix_compound_suites':
210 variants = target_test_suites[suite][sub_suite].get('variants')
211 variants = variants or [None]
Jeff Yoon67c3e832020-02-08 07:39:38212 for test_name in basic_suites[sub_suite]:
Garrett Beaty235c1412023-08-29 20:26:29213 for variant in variants:
214 key = (test_name, variant)
215 if ((seen_sub_suite := seen_tests.get(key)) is not None
216 and basic_suites[sub_suite][test_name] !=
217 basic_suites[seen_sub_suite][test_name]):
218 test_description = (test_name if variant is None else
219 f'{test_name} with variant {variant} applied')
220 raise BBGenErr(
221 'Conflicting test definitions for %s from %s '
222 'and %s in %s (error found while processing %s)' %
223 (test_description, seen_tests[key], sub_suite, test_type, suite))
224 seen_tests[key] = sub_suite
225
Jeff Yoon67c3e832020-02-08 07:39:38226
227def check_matrix_identifier(sub_suite=None,
228 suite=None,
229 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05230 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38231 **kwargs):
232 """Ensure 'idenfitier' is defined for each variant"""
233 del kwargs
234 sub_suite_config = suite_def[sub_suite]
Garrett Beaty2022db42023-08-29 17:22:40235 for variant_name in sub_suite_config.get('variants', []):
236 if variant_name not in all_variants:
237 raise BBGenErr('Missing variant definition for %s in variants.pyl' %
238 variant_name)
239 variant = all_variants[variant_name]
Jeff Yoonda581c32020-03-06 03:56:05240
Jeff Yoon67c3e832020-02-08 07:39:38241 if not 'identifier' in variant:
242 raise BBGenErr('Missing required identifier field in matrix '
243 'compound suite %s, %s' % (suite, sub_suite))
Sven Zhengef0d0872022-04-04 22:13:29244 if variant['identifier'] == '':
245 raise BBGenErr('Identifier field can not be "" in matrix '
246 'compound suite %s, %s' % (suite, sub_suite))
247 if variant['identifier'].strip() != variant['identifier']:
248 raise BBGenErr('Identifier field can not have leading and trailing '
249 'whitespace in matrix compound suite %s, %s' %
250 (suite, sub_suite))
Jeff Yoon67c3e832020-02-08 07:39:38251
252
Joshua Hood56c673c2022-03-02 20:29:33253class BBJSONGenerator(object): # pylint: disable=useless-object-inheritance
Garrett Beaty1afaccc2020-06-25 19:58:15254 def __init__(self, args):
Garrett Beaty1afaccc2020-06-25 19:58:15255 self.args = args
Kenneth Russelleb60cbd22017-12-05 07:54:28256 self.waterfalls = None
257 self.test_suites = None
258 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01259 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41260 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05261 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28262
Garrett Beaty1afaccc2020-06-25 19:58:15263 @staticmethod
264 def parse_args(argv):
265
266 # RawTextHelpFormatter allows for styling of help statement
267 parser = argparse.ArgumentParser(
268 formatter_class=argparse.RawTextHelpFormatter)
269
270 group = parser.add_mutually_exclusive_group()
271 group.add_argument(
272 '-c',
273 '--check',
274 action='store_true',
275 help=
276 'Do consistency checks of configuration and generated files and then '
277 'exit. Used during presubmit. '
278 'Causes the tool to not generate any files.')
279 group.add_argument(
280 '--query',
281 type=str,
Brian Sheedy0d2300f32024-08-13 23:14:41282 help=('Returns raw JSON information of buildbots and tests.\n'
283 'Examples:\n List all bots (all info):\n'
284 ' --query bots\n\n'
285 ' List all bots and only their associated tests:\n'
286 ' --query bots/tests\n\n'
287 ' List all information about "bot1" '
288 '(make sure you have quotes):\n --query bot/"bot1"\n\n'
289 ' List tests running for "bot1" (make sure you have quotes):\n'
290 ' --query bot/"bot1"/tests\n\n List all tests:\n'
291 ' --query tests\n\n'
292 ' List all tests and the bots running them:\n'
293 ' --query tests/bots\n\n'
294 ' List all tests that satisfy multiple parameters\n'
295 ' (separation of parameters by "&" symbol):\n'
296 ' --query tests/"device_os:Android&device_type:hammerhead"\n\n'
297 ' List all tests that run with a specific flag:\n'
298 ' --query bots/"--test-launcher-print-test-studio=always"\n\n'
299 ' List specific test (make sure you have quotes):\n'
300 ' --query test/"test1"\n\n'
301 ' List all bots running "test1" '
302 '(make sure you have quotes):\n --query test/"test1"/bots'))
Garrett Beaty1afaccc2020-06-25 19:58:15303 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47304 '--json',
305 metavar='JSON_FILE_PATH',
306 type=os.path.abspath,
307 help='Outputs results into a json file. Only works with query function.'
308 )
309 parser.add_argument(
Garrett Beaty1afaccc2020-06-25 19:58:15310 '-n',
311 '--new-files',
312 action='store_true',
313 help=
314 'Write output files as .new.json. Useful during development so old and '
315 'new files can be looked at side-by-side.')
Garrett Beatyade673d2023-08-04 22:00:25316 parser.add_argument('--dimension-sets-handling',
317 choices=['disable'],
318 default='disable',
319 help=('This flag no longer has any effect:'
320 ' dimension_sets fields are not allowed'))
Garrett Beaty1afaccc2020-06-25 19:58:15321 parser.add_argument('-v',
322 '--verbose',
323 action='store_true',
324 help='Increases verbosity. Affects consistency checks.')
325 parser.add_argument('waterfall_filters',
326 metavar='waterfalls',
327 type=str,
328 nargs='*',
329 help='Optional list of waterfalls to generate.')
330 parser.add_argument(
331 '--pyl-files-dir',
Garrett Beaty79339e182023-04-10 20:45:47332 type=os.path.abspath,
333 help=('Path to the directory containing the input .pyl files.'
334 ' By default the directory containing this script will be used.'))
Garrett Beaty1afaccc2020-06-25 19:58:15335 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47336 '--output-dir',
337 type=os.path.abspath,
338 help=('Path to the directory to output generated .json files.'
339 'By default, the pyl files directory will be used.'))
Chong Guee622242020-10-28 18:17:35340 parser.add_argument('--isolate-map-file',
341 metavar='PATH',
342 help='path to additional isolate map files.',
Garrett Beaty79339e182023-04-10 20:45:47343 type=os.path.abspath,
Chong Guee622242020-10-28 18:17:35344 default=[],
345 action='append',
346 dest='isolate_map_files')
Garrett Beaty1afaccc2020-06-25 19:58:15347 parser.add_argument(
348 '--infra-config-dir',
349 help='Path to the LUCI services configuration directory',
Garrett Beaty79339e182023-04-10 20:45:47350 type=os.path.abspath,
351 default=os.path.join(os.path.dirname(__file__), '..', '..', 'infra',
352 'config'))
353
Garrett Beaty1afaccc2020-06-25 19:58:15354 args = parser.parse_args(argv)
355 if args.json and not args.query:
356 parser.error(
Brian Sheedy0d2300f32024-08-13 23:14:41357 'The --json flag can only be used with --query.') # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:15358
Garrett Beaty79339e182023-04-10 20:45:47359 args.pyl_files_dir = args.pyl_files_dir or THIS_DIR
360 args.output_dir = args.output_dir or args.pyl_files_dir
361
Garrett Beatyee0e5552024-08-28 18:58:18362 def pyl_dir_path(filename):
Garrett Beaty79339e182023-04-10 20:45:47363 return os.path.join(args.pyl_files_dir, filename)
364
Garrett Beatyee0e5552024-08-28 18:58:18365 args.waterfalls_pyl_path = pyl_dir_path('waterfalls.pyl')
366 args.test_suite_exceptions_pyl_path = pyl_dir_path(
Garrett Beaty79339e182023-04-10 20:45:47367 'test_suite_exceptions.pyl')
Garrett Beaty4999e9792024-04-03 23:29:11368 args.autoshard_exceptions_json_path = os.path.join(
369 args.infra_config_dir, 'targets', 'autoshard_exceptions.json')
Garrett Beaty79339e182023-04-10 20:45:47370
Garrett Beatyee0e5552024-08-28 18:58:18371 if args.pyl_files_dir == THIS_DIR:
372
373 def infra_config_testing_path(filename):
374 return os.path.join(args.infra_config_dir, 'generated', 'testing',
375 filename)
376
377 args.gn_isolate_map_pyl_path = infra_config_testing_path(
378 'gn_isolate_map.pyl')
379 args.mixins_pyl_path = infra_config_testing_path('mixins.pyl')
380 args.test_suites_pyl_path = infra_config_testing_path('test_suites.pyl')
381 args.variants_pyl_path = infra_config_testing_path('variants.pyl')
382 else:
383 args.gn_isolate_map_pyl_path = pyl_dir_path('gn_isolate_map.pyl')
384 args.mixins_pyl_path = pyl_dir_path('mixins.pyl')
385 args.test_suites_pyl_path = pyl_dir_path('test_suites.pyl')
386 args.variants_pyl_path = pyl_dir_path('variants.pyl')
387
Garrett Beaty79339e182023-04-10 20:45:47388 return args
Kenneth Russelleb60cbd22017-12-05 07:54:28389
Stephen Martinis7eb8b612018-09-21 00:17:50390 def print_line(self, line):
391 # Exists so that tests can mock
Jamie Madillcf4f8c72021-05-20 19:24:23392 print(line) # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:50393
Kenneth Russelleb60cbd22017-12-05 07:54:28394 def read_file(self, relative_path):
Garrett Beaty79339e182023-04-10 20:45:47395 with open(relative_path) as fp:
Garrett Beaty1afaccc2020-06-25 19:58:15396 return fp.read()
Kenneth Russelleb60cbd22017-12-05 07:54:28397
Garrett Beaty79339e182023-04-10 20:45:47398 def write_file(self, file_path, contents):
Peter Kastingacd55c12023-08-23 20:19:04399 with open(file_path, 'w', newline='') as fp:
Garrett Beaty79339e182023-04-10 20:45:47400 fp.write(contents)
Zhiling Huangbe008172018-03-08 19:13:11401
Joshua Hood56c673c2022-03-02 20:29:33402 # pylint: disable=inconsistent-return-statements
Garrett Beaty79339e182023-04-10 20:45:47403 def load_pyl_file(self, pyl_file_path):
Kenneth Russelleb60cbd22017-12-05 07:54:28404 try:
Garrett Beaty79339e182023-04-10 20:45:47405 return ast.literal_eval(self.read_file(pyl_file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:28406 except (SyntaxError, ValueError) as e: # pragma: no cover
Josip Sokcevic7110fb382023-06-06 01:05:29407 raise BBGenErr('Failed to parse pyl file "%s": %s' %
408 (pyl_file_path, e)) from e
Joshua Hood56c673c2022-03-02 20:29:33409 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:28410
Kenneth Russell8a386d42018-06-02 09:48:01411 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
412 # Currently it is only mandatory for bots which run GPU tests. Change these to
413 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28414 def is_android(self, tester_config):
415 return tester_config.get('os_type') == 'android'
416
Ben Pastenea9e583b2019-01-16 02:57:26417 def is_chromeos(self, tester_config):
418 return tester_config.get('os_type') == 'chromeos'
419
Chong Guc2ca5d02022-01-11 19:52:17420 def is_fuchsia(self, tester_config):
421 return tester_config.get('os_type') == 'fuchsia'
422
Brian Sheedy781c8ca42021-03-08 22:03:21423 def is_lacros(self, tester_config):
424 return tester_config.get('os_type') == 'lacros'
425
Kenneth Russell8a386d42018-06-02 09:48:01426 def is_linux(self, tester_config):
427 return tester_config.get('os_type') == 'linux'
428
Kai Ninomiya40de9f52019-10-18 21:38:49429 def is_mac(self, tester_config):
430 return tester_config.get('os_type') == 'mac'
431
432 def is_win(self, tester_config):
433 return tester_config.get('os_type') == 'win'
434
435 def is_win64(self, tester_config):
436 return (tester_config.get('os_type') == 'win' and
437 tester_config.get('browser_config') == 'release_x64')
438
Garrett Beatyffe83c4f2023-09-08 19:07:37439 def get_exception_for_test(self, test_config):
440 return self.exceptions.get(test_config['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28441
Garrett Beatyffe83c4f2023-09-08 19:07:37442 def should_run_on_tester(self, waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28443 # Currently, the only reason a test should not run on a given tester is that
444 # it's in the exceptions. (Once the GPU waterfall generation script is
445 # incorporated here, the rules will become more complex.)
Garrett Beatyffe83c4f2023-09-08 19:07:37446 exception = self.get_exception_for_test(test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28447 if not exception:
448 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28449 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28450 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28451 if remove_from:
452 if tester_name in remove_from:
453 return False
454 # TODO(kbr): this code path was added for some tests (including
455 # android_webview_unittests) on one machine (Nougat Phone
456 # Tester) which exists with the same name on two waterfalls,
457 # chromium.android and chromium.fyi; the tests are run on one
458 # but not the other. Once the bots are all uniquely named (a
459 # different ongoing project) this code should be removed.
460 # TODO(kbr): add coverage.
461 return (tester_name + ' ' + waterfall['name']
462 not in remove_from) # pragma: no cover
463 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28464
Garrett Beatyffe83c4f2023-09-08 19:07:37465 def get_test_modifications(self, test, tester_name):
466 exception = self.get_exception_for_test(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28467 if not exception:
468 return None
Nico Weber79dc5f6852018-07-13 19:38:49469 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28470
Garrett Beatyffe83c4f2023-09-08 19:07:37471 def get_test_replacements(self, test, tester_name):
472 exception = self.get_exception_for_test(test)
Brian Sheedye6ea0ee2019-07-11 02:54:37473 if not exception:
474 return None
475 return exception.get('replacements', {}).get(tester_name)
476
Kenneth Russell8a386d42018-06-02 09:48:01477 def merge_command_line_args(self, arr, prefix, splitter):
478 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01479 idx = 0
480 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01481 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01482 while idx < len(arr):
483 flag = arr[idx]
484 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01485 if flag.startswith(prefix):
486 arg = flag[prefix_len:]
487 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01488 if first_idx < 0:
489 first_idx = idx
490 else:
491 delete_current_entry = True
492 if delete_current_entry:
493 del arr[idx]
494 else:
495 idx += 1
496 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01497 arr[first_idx] = prefix + splitter.join(accumulated_args)
498 return arr
499
500 def maybe_fixup_args_array(self, arr):
501 # The incoming array of strings may be an array of command line
502 # arguments. To make it easier to turn on certain features per-bot or
503 # per-test-suite, look specifically for certain flags and merge them
504 # appropriately.
505 # --enable-features=Feature1 --enable-features=Feature2
506 # are merged to:
507 # --enable-features=Feature1,Feature2
508 # and:
509 # --extra-browser-args=arg1 --extra-browser-args=arg2
510 # are merged to:
511 # --extra-browser-args=arg1 arg2
512 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
513 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Yuly Novikov8c487e72020-10-16 20:00:29514 arr = self.merge_command_line_args(arr, '--test-launcher-filter-file=', ';')
Cameron Higgins971f0b92023-01-03 18:05:09515 arr = self.merge_command_line_args(arr, '--extra-app-args=', ',')
Kenneth Russell650995a2018-05-03 21:17:01516 return arr
517
Brian Sheedy910cda82022-07-19 11:58:34518 def substitute_magic_args(self, test_config, tester_name, tester_config):
Brian Sheedya31578e2020-05-18 20:24:36519 """Substitutes any magic substitution args present in |test_config|.
520
521 Substitutions are done in-place.
522
523 See buildbot_json_magic_substitutions.py for more information on this
524 feature.
525
526 Args:
527 test_config: A dict containing a configuration for a specific test on
Garrett Beatye3a606ceb2024-04-30 22:13:13528 a specific builder.
Brian Sheedy5f173bb2021-11-24 00:45:54529 tester_name: A string containing the name of the tester that |test_config|
530 came from.
Brian Sheedy910cda82022-07-19 11:58:34531 tester_config: A dict containing the configuration for the builder that
532 |test_config| is for.
Brian Sheedya31578e2020-05-18 20:24:36533 """
534 substituted_array = []
Brian Sheedyba13cf522022-09-13 21:00:09535 original_args = test_config.get('args', [])
536 for arg in original_args:
Brian Sheedya31578e2020-05-18 20:24:36537 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
538 function = arg.replace(
539 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
540 if hasattr(magic_substitutions, function):
541 substituted_array.extend(
Brian Sheedy910cda82022-07-19 11:58:34542 getattr(magic_substitutions, function)(test_config, tester_name,
543 tester_config))
Brian Sheedya31578e2020-05-18 20:24:36544 else:
545 raise BBGenErr(
546 'Magic substitution function %s does not exist' % function)
547 else:
548 substituted_array.append(arg)
Brian Sheedyba13cf522022-09-13 21:00:09549 if substituted_array != original_args:
Brian Sheedya31578e2020-05-18 20:24:36550 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
551
Garrett Beaty1b271622024-10-01 22:30:25552 @staticmethod
553 def merge_swarming(swarming1, swarming2):
554 swarming2 = dict(swarming2)
555 if 'dimensions' in swarming2:
556 swarming1.setdefault('dimensions', {}).update(swarming2.pop('dimensions'))
557 if 'named_caches' in swarming2:
558 named_caches = swarming1.setdefault('named_caches', [])
559 named_caches.extend(swarming2.pop('named_caches'))
560 swarming1.update(swarming2)
Kenneth Russelleb60cbd22017-12-05 07:54:28561
Kenneth Russelleb60cbd22017-12-05 07:54:28562 def clean_swarming_dictionary(self, swarming_dict):
563 # Clean out redundant entries from a test's "swarming" dictionary.
564 # This is really only needed to retain 100% parity with the
565 # handwritten JSON files, and can be removed once all the files are
566 # autogenerated.
567 if 'shards' in swarming_dict:
568 if swarming_dict['shards'] == 1: # pragma: no cover
569 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24570 if 'hard_timeout' in swarming_dict:
571 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
572 del swarming_dict['hard_timeout'] # pragma: no cover
Garrett Beatybb18d532023-06-26 22:16:33573 del swarming_dict['can_use_on_swarming_builders']
Kenneth Russelleb60cbd22017-12-05 07:54:28574
Garrett Beatye3a606ceb2024-04-30 22:13:13575 def resolve_os_conditional_values(self, test, builder):
576 for key, fn in (
577 ('android_swarming', self.is_android),
578 ('chromeos_swarming', self.is_chromeos),
579 ):
580 swarming = test.pop(key, None)
581 if swarming and fn(builder):
Garrett Beaty1b271622024-10-01 22:30:25582 self.merge_swarming(test['swarming'], swarming)
Garrett Beatye3a606ceb2024-04-30 22:13:13583
584 for key, fn in (
585 ('desktop_args', lambda cfg: not self.is_android(cfg)),
586 ('lacros_args', self.is_lacros),
587 ('linux_args', self.is_linux),
588 ('android_args', self.is_android),
589 ('chromeos_args', self.is_chromeos),
590 ('mac_args', self.is_mac),
591 ('win_args', self.is_win),
592 ('win64_args', self.is_win64),
593 ):
594 args = test.pop(key, [])
595 if fn(builder):
596 test.setdefault('args', []).extend(args)
597
598 def apply_common_transformations(self,
599 waterfall,
600 builder_name,
601 builder,
602 test,
603 test_name,
604 *,
605 swarmable=True,
606 supports_args=True):
607 # Initialize the swarming dictionary
608 swarmable = swarmable and builder.get('use_swarming', True)
609 test.setdefault('swarming', {}).setdefault('can_use_on_swarming_builders',
610 swarmable)
611
Garrett Beaty4b9f1752024-09-26 20:02:50612 # Test common mixins are mixins specified in the test declaration itself. To
613 # match the order of expansion in starlark, they take effect before anything
614 # specified in the legacy_test_config.
615 test_common = test.pop('test_common', {})
616 if test_common:
617 test_common_mixins = test_common.pop('mixins', [])
618 self.ensure_valid_mixin_list(test_common_mixins,
619 f'test {test_name} test_common mixins')
620 test_common = self.apply_mixins(test_common, test_common_mixins, [],
621 builder)
622 test = self.apply_mixin(test, test_common, builder)
623
Garrett Beatye3a606ceb2024-04-30 22:13:13624 mixins_to_ignore = test.pop('remove_mixins', [])
625 self.ensure_valid_mixin_list(mixins_to_ignore,
626 f'test {test_name} remove_mixins')
627
Garrett Beatycc184692024-05-01 14:57:09628 # Expand any conditional values
629 self.resolve_os_conditional_values(test, builder)
630
631 # Apply mixins from the test
632 test_mixins = test.pop('mixins', [])
633 self.ensure_valid_mixin_list(test_mixins, f'test {test_name} mixins')
634 test = self.apply_mixins(test, test_mixins, mixins_to_ignore, builder)
635
Garrett Beaty65b7d362024-10-01 16:21:42636 # Apply any variant details
637 variant = test.pop('*variant*', None)
638 if variant is not None:
639 test = self.apply_mixin(variant, test)
640 variant_mixins = test.pop('*variant_mixins*', [])
641 self.ensure_valid_mixin_list(
642 variant_mixins,
643 (f'variant mixins for test {test_name}'
644 f' with variant with identifier{test["variant_id"]}'))
645 test = self.apply_mixins(test, variant_mixins, mixins_to_ignore, builder)
646
Garrett Beatye3a606ceb2024-04-30 22:13:13647 # Add any swarming or args from the builder
Garrett Beaty1b271622024-10-01 22:30:25648 self.merge_swarming(test['swarming'], builder.get('swarming', {}))
Garrett Beatye3a606ceb2024-04-30 22:13:13649 if supports_args:
650 test.setdefault('args', []).extend(builder.get('args', []))
651
Garrett Beatye3a606ceb2024-04-30 22:13:13652 # Apply mixins from the waterfall
653 waterfall_mixins = waterfall.get('mixins', [])
654 self.ensure_valid_mixin_list(waterfall_mixins,
655 f"waterfall {waterfall['name']} mixins")
656 test = self.apply_mixins(test, waterfall_mixins, mixins_to_ignore, builder)
657
658 # Apply mixins from the builder
659 builder_mixins = builder.get('mixins', [])
660 self.ensure_valid_mixin_list(builder_mixins,
Brian Sheedy0d2300f32024-08-13 23:14:41661 f'builder {builder_name} mixins')
Garrett Beatye3a606ceb2024-04-30 22:13:13662 test = self.apply_mixins(test, builder_mixins, mixins_to_ignore, builder)
663
Kenneth Russelleb60cbd22017-12-05 07:54:28664 # See if there are any exceptions that need to be merged into this
665 # test's specification.
Garrett Beatye3a606ceb2024-04-30 22:13:13666 modifications = self.get_test_modifications(test, builder_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28667 if modifications:
Garrett Beaty1b271622024-10-01 22:30:25668 test = self.apply_mixin(modifications, test, builder)
Garrett Beatye3a606ceb2024-04-30 22:13:13669
670 # Clean up the swarming entry or remove it if it's unnecessary
Garrett Beatybfeff8f2023-06-16 18:57:25671 if (swarming_dict := test.get('swarming')) is not None:
Garrett Beatybb18d532023-06-26 22:16:33672 if swarming_dict.get('can_use_on_swarming_builders'):
Garrett Beatybfeff8f2023-06-16 18:57:25673 self.clean_swarming_dictionary(swarming_dict)
674 else:
675 del test['swarming']
Garrett Beatye3a606ceb2024-04-30 22:13:13676
Ben Pastenee012aea42019-05-14 22:32:28677 # Ensure all Android Swarming tests run only on userdebug builds if another
678 # build type was not specified.
Garrett Beatye3a606ceb2024-04-30 22:13:13679 if 'swarming' in test and self.is_android(builder):
Garrett Beatyade673d2023-08-04 22:00:25680 dimensions = test.get('swarming', {}).get('dimensions', {})
681 if (dimensions.get('os') == 'Android'
682 and not dimensions.get('device_os_type')):
683 dimensions['device_os_type'] = 'userdebug'
Garrett Beatye3a606ceb2024-04-30 22:13:13684
Garrett Beatydb0ead62025-01-15 20:06:39685 skylab = test.pop('skylab', {})
686 if skylab.get('cros_board'):
Garrett Beaty050fc4c2025-01-09 19:44:50687 for k, v in skylab.items():
688 test[k] = v
Qijiang Fan65681ba2025-06-06 10:48:44689
690 # Should use chromiumos.test.api.TestSuite.test_case_tag_criteria.
691 # When this is set cros_test_platform(CTP) will find tests to run based
692 # on the criteria rather than one single test.
693 # The autotest wrapper that launches tast test is considered one single
694 # test at scheduling. If we specify test_case_tag_criteria, CTP can
695 # enumerate the tast tests and launches direct run of tast tests without
696 # autotest wrapper. In this case we don't need to populate autotest_name
697 # (maps to chromiumos.test.api.TestSuite.test_case_ids) to be the wrapper.
698 has_ctp_tag_criteria = bool(
699 test.keys() & {
700 'cros_test_tags', 'cros_test_tags_exclude', 'cros_test_names',
701 'cros_test_names_exclude', 'cros_test_names_from_file',
702 'cros_test_names_exclude_from_file'
703 })
704
Garrett Beaty050fc4c2025-01-09 19:44:50705 # For skylab, we need to pop the correct `autotest_name`. This field
706 # defines what wrapper we use in OS infra. e.g. for gtest it's
707 # https://source.chromium.org/chromiumos/chromiumos/codesearch/+/main:src/third_party/autotest/files/server/site_tests/chromium/chromium.py
Qijiang Fan65681ba2025-06-06 10:48:44708 if 'autotest_name' not in test and not has_ctp_tag_criteria:
Garrett Beaty050fc4c2025-01-09 19:44:50709 if 'tast_expr' in test:
710 if 'lacros' in test['name']:
711 test['autotest_name'] = 'tast.lacros-from-gcs'
712 else:
713 test['autotest_name'] = 'tast.chrome-from-gcs'
714 elif 'benchmark' in test:
715 test['autotest_name'] = 'chromium_Telemetry'
716 else:
717 test['autotest_name'] = 'chromium'
718
Garrett Beatye3a606ceb2024-04-30 22:13:13719 # Apply any replacements specified for the test for the builder
720 self.replace_test_args(test, test_name, builder_name)
721
722 # Remove args if it is empty
723 if 'args' in test:
724 if not test['args']:
725 del test['args']
726 else:
727 # Replace any magic arguments with their actual value
728 self.substitute_magic_args(test, builder_name, builder)
729
730 test['args'] = self.maybe_fixup_args_array(test['args'])
Ben Pastenee012aea42019-05-14 22:32:28731
Kenneth Russelleb60cbd22017-12-05 07:54:28732 return test
733
Brian Sheedye6ea0ee2019-07-11 02:54:37734 def replace_test_args(self, test, test_name, tester_name):
Garrett Beatyffe83c4f2023-09-08 19:07:37735 replacements = self.get_test_replacements(test, tester_name) or {}
Brian Sheedye6ea0ee2019-07-11 02:54:37736 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23737 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37738 if key not in valid_replacement_keys:
739 raise BBGenErr(
740 'Given replacement key %s for %s on %s is not in the list of valid '
741 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23742 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37743 found_key = False
744 for i, test_key in enumerate(test.get(key, [])):
745 # Handle both the key/value being replaced being defined as two
746 # separate items or as key=value.
747 if test_key == replacement_key:
748 found_key = True
749 # Handle flags without values.
Brian Sheedy822e03742024-08-09 18:48:14750 if replacement_val is None:
Brian Sheedye6ea0ee2019-07-11 02:54:37751 del test[key][i]
752 else:
753 test[key][i+1] = replacement_val
754 break
Joshua Hood56c673c2022-03-02 20:29:33755 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37756 found_key = True
Brian Sheedy822e03742024-08-09 18:48:14757 if replacement_val is None:
Brian Sheedye6ea0ee2019-07-11 02:54:37758 del test[key][i]
759 else:
760 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
761 break
762 if not found_key:
763 raise BBGenErr('Could not find %s in existing list of values for key '
764 '%s in %s on %s' % (replacement_key, key, test_name,
765 tester_name))
766
Shenghua Zhangaba8bad2018-02-07 02:12:09767 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05768 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26769 True):
770 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06771 # are targeting CrOS hardware and so need the special trigger script.
Garrett Beatyade673d2023-08-04 22:00:25772 if 'device_type' in test.get('swarming', {}).get('dimensions', {}):
Ben Pastenea9e583b2019-01-16 02:57:26773 test['trigger_script'] = {
774 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
775 }
Shenghua Zhangaba8bad2018-02-07 02:12:09776
Garrett Beatyffe83c4f2023-09-08 19:07:37777 def add_android_presentation_args(self, tester_config, result):
John Budorick262ae112019-07-12 19:24:38778 bucket = tester_config.get('results_bucket', 'chromium-result-details')
Garrett Beaty94af4272024-04-17 18:06:14779 result.setdefault('args', []).append('--gs-results-bucket=%s' % bucket)
780
781 if ('swarming' in result and 'merge' not in 'result'
782 and not tester_config.get('skip_merge_script', False)):
Ben Pastene858f4be2019-01-09 23:52:09783 result['merge'] = {
Garrett Beatyffe83c4f2023-09-08 19:07:37784 'args': [
785 '--bucket',
786 bucket,
787 '--test-name',
788 result['name'],
789 ],
790 'script': ('//build/android/pylib/results/presentation/'
791 'test_results_presentation.py'),
Ben Pastene858f4be2019-01-09 23:52:09792 }
Ben Pastene858f4be2019-01-09 23:52:09793
Kenneth Russelleb60cbd22017-12-05 07:54:28794 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
795 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
801 result.setdefault('test', test_name)
John Budorickab108712018-09-01 00:12:21802
Garrett Beatye3a606ceb2024-04-30 22:13:13803 result = self.apply_common_transformations(waterfall, tester_name,
804 tester_config, result, test_name)
Garrett Beaty94af4272024-04-17 18:06:14805 if self.is_android(tester_config) and 'swarming' in result:
806 if not result.get('use_isolated_scripts_api', False):
Alison Gale71bd8f152024-04-26 22:38:20807 # TODO(crbug.com/40725094) make Android presentation work with
Yuly Novikov26dd47052021-02-11 00:57:14808 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37809 self.add_android_presentation_args(tester_config, result)
Yuly Novikov26dd47052021-02-11 00:57:14810 result['args'] = result.get('args', []) + ['--recover-devices']
Shenghua Zhangaba8bad2018-02-07 02:12:09811 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43812
Garrett Beatybb18d532023-06-26 22:16:33813 if 'swarming' in result and not result.get('merge'):
Jamie Madilla8be0d72020-10-02 05:24:04814 if test_config.get('use_isolated_scripts_api', False):
815 merge_script = 'standard_isolated_script_merge'
816 else:
817 merge_script = 'standard_gtest_merge'
818
Stephen Martinisbc7b7772019-05-01 22:01:43819 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04820 'script': '//testing/merge_scripts/%s.py' % merge_script,
Stephen Martinisbc7b7772019-05-01 22:01:43821 }
Kenneth Russelleb60cbd22017-12-05 07:54:28822 return result
823
824 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
825 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37826 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28827 return None
828 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37829 # Use test_name here instead of test['name'] because test['name'] will be
830 # modified with the variant identifier in a matrix compound suite
Garrett Beatydca3d882023-09-14 23:50:32831 result.setdefault('test', test_name)
Garrett Beatye3a606ceb2024-04-30 22:13:13832 result = self.apply_common_transformations(waterfall, tester_name,
833 tester_config, result, test_name)
Garrett Beaty94af4272024-04-17 18:06:14834 if self.is_android(tester_config) and 'swarming' in result:
Yuly Novikov26dd47052021-02-11 00:57:14835 if tester_config.get('use_android_presentation', False):
Alison Gale71bd8f152024-04-26 22:38:20836 # TODO(crbug.com/40725094) make Android presentation work with
Yuly Novikov26dd47052021-02-11 00:57:14837 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37838 self.add_android_presentation_args(tester_config, result)
Shenghua Zhangaba8bad2018-02-07 02:12:09839 self.add_common_test_properties(result, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17840
Garrett Beatybb18d532023-06-26 22:16:33841 if 'swarming' in result and not result.get('merge'):
Alison Gale923a33e2024-04-22 23:34:28842 # TODO(crbug.com/41456107): Consider adding the ability to not have
Stephen Martinisf50047062019-05-06 22:26:17843 # this default.
844 result['merge'] = {
845 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
Stephen Martinisf50047062019-05-06 22:26:17846 }
Kenneth Russelleb60cbd22017-12-05 07:54:28847 return result
848
Garrett Beaty938560e32024-09-26 18:57:35849 _SCRIPT_FIELDS = ('name', 'script', 'args', 'precommit_args',
850 'non_precommit_args', 'resultdb')
851
Kenneth Russelleb60cbd22017-12-05 07:54:28852 def generate_script_test(self, waterfall, tester_name, tester_config,
853 test_name, test_config):
Alison Gale47d1537d2024-04-19 21:31:46854 # TODO(crbug.com/40623237): Remove this check whenever a better
Brian Sheedy158cd0f2019-04-26 01:12:44855 # long-term solution is implemented.
856 if (waterfall.get('forbid_script_tests', False) or
857 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
858 raise BBGenErr('Attempted to generate a script test on tester ' +
859 tester_name + ', which explicitly forbids script tests')
Garrett Beatyffe83c4f2023-09-08 19:07:37860 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28861 return None
Garrett Beaty938560e32024-09-26 18:57:35862 result = copy.deepcopy(test_config)
Garrett Beatye3a606ceb2024-04-30 22:13:13863 result = self.apply_common_transformations(waterfall,
864 tester_name,
865 tester_config,
866 result,
867 test_name,
868 swarmable=False,
869 supports_args=False)
Garrett Beaty938560e32024-09-26 18:57:35870 result = {k: result[k] for k in self._SCRIPT_FIELDS if k in result}
Kenneth Russelleb60cbd22017-12-05 07:54:28871 return result
872
Xinan Lin05fb9c1752020-12-17 00:15:52873 def generate_skylab_test(self, waterfall, tester_name, tester_config,
874 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37875 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Xinan Lin05fb9c1752020-12-17 00:15:52876 return None
877 result = copy.deepcopy(test_config)
Brian Sheedy67937ad12024-03-06 22:53:55878 result.setdefault('test', test_name)
yoshiki iguchid1664ef2024-03-28 19:16:52879
Garrett Beaty050fc4c2025-01-09 19:44:50880 skylab = result.setdefault('skylab', {})
881 if result.get('experiment_percentage') != 100:
882 skylab.setdefault('shard_level_retries_on_ctp', 1)
yoshiki iguchid1664ef2024-03-28 19:16:52883
Garrett Beaty050fc4c2025-01-09 19:44:50884 for src, dst in (
885 ('cros_board', 'cros_board'),
886 ('cros_model', 'cros_model'),
887 ('cros_dut_pool', 'dut_pool'),
888 ('cros_build_target', 'cros_build_target'),
889 ('shard_level_retries_on_ctp', 'shard_level_retries_on_ctp'),
890 ):
891 if src in tester_config:
892 skylab[dst] = tester_config[src]
yoshiki iguchia5f87c7d2024-06-19 02:48:34893
Garrett Beatye3a606ceb2024-04-30 22:13:13894 result = self.apply_common_transformations(waterfall,
895 tester_name,
896 tester_config,
897 result,
898 test_name,
899 swarmable=False)
Garrett Beaty050fc4c2025-01-09 19:44:50900
901 if 'cros_board' not in result:
902 raise BBGenErr('skylab tests must specify cros_board.')
903
Xinan Lin05fb9c1752020-12-17 00:15:52904 return result
905
Garrett Beaty65d44222023-08-01 17:22:11906 def substitute_gpu_args(self, tester_config, test, args):
Kenneth Russell8a386d42018-06-02 09:48:01907 substitutions = {
908 # Any machine in waterfalls.pyl which desires to run GPU tests
909 # must provide the os_type key.
910 'os_type': tester_config['os_type'],
911 'gpu_vendor_id': '0',
912 'gpu_device_id': '0',
913 }
Garrett Beatyade673d2023-08-04 22:00:25914 dimensions = test.get('swarming', {}).get('dimensions', {})
915 if 'gpu' in dimensions:
916 # First remove the driver version, then split into vendor and device.
917 gpu = dimensions['gpu']
918 if gpu != 'none':
919 gpu = gpu.split('-')[0].split(':')
920 substitutions['gpu_vendor_id'] = gpu[0]
921 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01922 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
923
Garrett Beaty7436fb72024-08-07 20:20:58924 # LINT.IfChange(gpu_telemetry_test)
925
Kenneth Russell8a386d42018-06-02 09:48:01926 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30927 test_name, test_config, is_android_webview,
Xinan Linedcf05b32023-10-19 23:13:50928 is_cast_streaming, is_skylab):
Kenneth Russell8a386d42018-06-02 09:48:01929 # These are all just specializations of isolated script tests with
930 # a bunch of boilerplate command line arguments added.
931
932 # The step name must end in 'test' or 'tests' in order for the
933 # results to automatically show up on the flakiness dashboard.
934 # (At least, this was true some time ago.) Continue to use this
935 # naming convention for the time being to minimize changes.
Garrett Beaty235c1412023-08-29 20:26:29936 #
937 # test name is the name of the test without the variant ID added
938 if not (test_name.endswith('test') or test_name.endswith('tests')):
939 raise BBGenErr(
940 f'telemetry test names must end with test or tests, got {test_name}')
Garrett Beatyffe83c4f2023-09-08 19:07:37941 result = self.generate_isolated_script_test(waterfall, tester_name,
942 tester_config, test_name,
943 test_config)
Kenneth Russell8a386d42018-06-02 09:48:01944 if not result:
945 return None
Garrett Beatydca3d882023-09-14 23:50:32946 result['test'] = test_config.get('test') or self.get_default_isolate_name(
947 tester_config, is_android_webview)
Chan Liab7d8dd82020-04-24 23:42:19948
Chan Lia3ad1502020-04-28 05:32:11949 # Populate test_id_prefix.
Garrett Beatydca3d882023-09-14 23:50:32950 gn_entry = self.gn_isolate_map[result['test']]
Chan Li17d969f92020-07-10 00:50:03951 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19952
Kenneth Russell8a386d42018-06-02 09:48:01953 args = result.get('args', [])
Garrett Beatyffe83c4f2023-09-08 19:07:37954 # Use test_name here instead of test['name'] because test['name'] will be
955 # modified with the variant identifier in a matrix compound suite
Kenneth Russell8a386d42018-06-02 09:48:01956 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14957
958 # These tests upload and download results from cloud storage and therefore
959 # aren't idempotent yet. https://crbug.com/549140.
Garrett Beatybfeff8f2023-06-16 18:57:25960 if 'swarming' in result:
961 result['swarming']['idempotent'] = False
erikchen6da2d9b2018-08-03 23:01:14962
Fabrice de Ganscbd655f2022-08-04 20:15:30963 browser = ''
964 if is_cast_streaming:
965 browser = 'cast-streaming-shell'
966 elif is_android_webview:
967 browser = 'android-webview-instrumentation'
968 else:
969 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:52970
Greg Thompsoncec7d8d2023-01-10 19:11:53971 extra_browser_args = []
972
Brian Sheedy4053a702020-07-28 02:09:52973 # Most platforms require --enable-logging=stderr to get useful browser logs.
974 # However, this actively messes with logging on CrOS (because Chrome's
975 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
976 # in order to see JavaScript console messages. See
977 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
Greg Thompsoncec7d8d2023-01-10 19:11:53978 if self.is_chromeos(tester_config):
979 extra_browser_args.append('--log-level=0')
980 elif not self.is_fuchsia(tester_config) or browser != 'fuchsia-chrome':
981 # Stderr logging is not needed for Chrome browser on Fuchsia, as ordinary
982 # logging via syslog is captured.
983 extra_browser_args.append('--enable-logging=stderr')
984
985 # --expose-gc allows the WebGL conformance tests to more reliably
986 # reproduce GC-related bugs in the V8 bindings.
987 extra_browser_args.append('--js-flags=--expose-gc')
Brian Sheedy4053a702020-07-28 02:09:52988
Xinan Linedcf05b32023-10-19 23:13:50989 # Skylab supports sharding, so reuse swarming's shard config.
990 if is_skylab and 'shards' not in result and test_config.get(
991 'swarming', {}).get('shards'):
992 result['shards'] = test_config['swarming']['shards']
993
Kenneth Russell8a386d42018-06-02 09:48:01994 args = [
Bo Liu555a0f92019-03-29 12:11:56995 test_to_run,
996 '--show-stdout',
997 '--browser=%s' % browser,
998 # --passthrough displays more of the logging in Telemetry when
999 # run via typ, in particular some of the warnings about tests
1000 # being expected to fail, but passing.
1001 '--passthrough',
1002 '-v',
Brian Sheedy814e0482022-10-03 23:24:121003 '--stable-jobs',
Greg Thompsoncec7d8d2023-01-10 19:11:531004 '--extra-browser-args=%s' % ' '.join(extra_browser_args),
Brian Sheedy997e48062023-10-18 02:28:131005 '--enforce-browser-version',
Kenneth Russell8a386d42018-06-02 09:48:011006 ] + args
Garrett Beatybfeff8f2023-06-16 18:57:251007 result['args'] = self.maybe_fixup_args_array(
Garrett Beaty65d44222023-08-01 17:22:111008 self.substitute_gpu_args(tester_config, result, args))
Kenneth Russell8a386d42018-06-02 09:48:011009 return result
1010
Garrett Beaty7436fb72024-08-07 20:20:581011 # pylint: disable=line-too-long
1012 # LINT.ThenChange(//infra/config/lib/targets-internal/test-types/gpu_telemetry_test.star)
1013 # pylint: enable=line-too-long
1014
Brian Sheedyf74819b2021-06-04 01:38:381015 def get_default_isolate_name(self, tester_config, is_android_webview):
1016 if self.is_android(tester_config):
1017 if is_android_webview:
1018 return 'telemetry_gpu_integration_test_android_webview'
1019 return (
1020 'telemetry_gpu_integration_test' +
1021 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:331022 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:171023 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:331024 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:381025
Kenneth Russelleb60cbd22017-12-05 07:54:281026 def get_test_generator_map(self):
1027 return {
Bo Liu555a0f92019-03-29 12:11:561028 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301029 GPUTelemetryTestGenerator(self, is_android_webview=True),
1030 'cast_streaming_tests':
1031 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:561032 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301033 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561034 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301035 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561036 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301037 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561038 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301039 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:521040 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301041 SkylabGenerator(self),
Brian Sheedyb6491ba2022-09-26 20:49:491042 'skylab_gpu_telemetry_tests':
1043 SkylabGPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281044 }
1045
Kenneth Russell8a386d42018-06-02 09:48:011046 def get_test_type_remapper(self):
1047 return {
Fabrice de Gans223272482022-08-08 16:56:571048 # These are a specialization of isolated_scripts with a bunch of
1049 # boilerplate command line arguments added to each one.
1050 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
1051 'cast_streaming_tests': 'isolated_scripts',
1052 'gpu_telemetry_tests': 'isolated_scripts',
Brian Sheedyb6491ba2022-09-26 20:49:491053 # These are the same as existing test types, just configured to run
1054 # in Skylab instead of via normal swarming.
1055 'skylab_gpu_telemetry_tests': 'skylab_tests',
Kenneth Russell8a386d42018-06-02 09:48:011056 }
1057
Jeff Yoon67c3e832020-02-08 07:39:381058 def check_composition_type_test_suites(self, test_type,
1059 additional_validators=None):
1060 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1061 validators = [check_compound_references,
1062 check_basic_references,
1063 check_conflicting_definitions]
1064 if additional_validators:
1065 validators += additional_validators
1066
1067 target_suites = self.test_suites.get(test_type, {})
1068 other_test_type = ('compound_suites'
1069 if test_type == 'matrix_compound_suites'
1070 else 'matrix_compound_suites')
1071 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011072 basic_suites = self.test_suites.get('basic_suites', {})
1073
Jamie Madillcf4f8c72021-05-20 19:24:231074 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011075 if suite in basic_suites:
1076 raise BBGenErr('%s names may not duplicate basic test suite names '
1077 '(error found while processsing %s)'
1078 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011079
Jeff Yoon67c3e832020-02-08 07:39:381080 seen_tests = {}
1081 for sub_suite in suite_def:
1082 for validator in validators:
1083 validator(
1084 basic_suites=basic_suites,
1085 other_test_suites=other_suites,
1086 seen_tests=seen_tests,
1087 sub_suite=sub_suite,
1088 suite=suite,
1089 suite_def=suite_def,
1090 target_test_suites=target_suites,
1091 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051092 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381093 )
Kenneth Russelleb60cbd22017-12-05 07:54:281094
Stephen Martinis54d64ad2018-09-21 22:16:201095 def flatten_test_suites(self):
1096 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011097 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1098 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231099 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011100 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201101 self.test_suites = new_test_suites
1102
Chan Lia3ad1502020-04-28 05:32:111103 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231104 for suite in self.test_suites['basic_suites'].values():
1105 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561106 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411107
Garrett Beatydca3d882023-09-14 23:50:321108 isolate_name = test.get('test') or key
Nodir Turakulovfce34292019-12-18 17:05:411109 gn_entry = self.gn_isolate_map.get(isolate_name)
1110 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281111 label = gn_entry['label']
1112
1113 if label.count(':') != 1:
1114 raise BBGenErr(
1115 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1116 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1117 (label, isolate_name))
1118 if label.split(':')[1] != isolate_name:
1119 raise BBGenErr(
1120 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1121 ' label "%s" see http://crbug.com/1071091 for details.' %
1122 (isolate_name, label))
1123
Chan Lia3ad1502020-04-28 05:32:111124 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411125 else: # pragma: no cover
1126 # Some tests do not have an entry gn_isolate_map.pyl, such as
1127 # telemetry tests.
Alison Gale47d1537d2024-04-19 21:31:461128 # TODO(crbug.com/40112160): require an entry in gn_isolate_map.
Nodir Turakulovfce34292019-12-18 17:05:411129 pass
1130
Kenneth Russelleb60cbd22017-12-05 07:54:281131 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011132 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201133
Jeff Yoon8154e582019-12-03 23:30:011134 compound_suites = self.test_suites.get('compound_suites', {})
1135 # check_composition_type_test_suites() checks that all basic suites
1136 # referenced by compound suites exist.
1137 basic_suites = self.test_suites.get('basic_suites')
1138
Jamie Madillcf4f8c72021-05-20 19:24:231139 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011140 # Resolve this to a dictionary.
1141 full_suite = {}
1142 for entry in value:
1143 suite = basic_suites[entry]
1144 full_suite.update(suite)
1145 compound_suites[name] = full_suite
1146
Jeff Yoon85fb8df2020-08-20 16:47:431147 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381148 """ Merge variant-defined configurations to each test case definition in a
1149 test suite.
1150
1151 The output maps a unique test name to an array of configurations because
1152 there may exist more than one definition for a test name using variants. The
1153 test name is referenced while mapping machines to test suites, so unpacking
1154 the array is done by the generators.
1155
1156 Args:
1157 basic_test_definition: a {} defined test suite in the format
1158 test_name:test_config
1159 variants: an [] of {} defining configurations to be applied to each test
1160 case in the basic test_definition
1161
1162 Return:
1163 a {} of test_name:[{}], where each {} is a merged configuration
1164 """
1165
1166 # Each test in a basic test suite will have a definition per variant.
1167 test_suite = {}
Garrett Beaty8d6708c2023-07-20 17:20:411168 for variant in variants:
1169 # Unpack the variant from variants.pyl if it's string based.
1170 if isinstance(variant, str):
1171 variant = self.variants[variant]
Jeff Yoonda581c32020-03-06 03:56:051172
Garrett Beaty8d6708c2023-07-20 17:20:411173 # If 'enabled' is set to False, we will not use this variant; otherwise if
1174 # the variant doesn't include 'enabled' variable or 'enabled' is set to
1175 # True, we will use this variant
1176 if not variant.get('enabled', True):
1177 continue
Jeff Yoon67c3e832020-02-08 07:39:381178
Garrett Beaty8d6708c2023-07-20 17:20:411179 # Make a shallow copy of the variant to remove variant-specific fields,
1180 # leaving just mixin fields
1181 variant = copy.copy(variant)
1182 variant.pop('enabled', None)
1183 identifier = variant.pop('identifier')
1184 variant_mixins = variant.pop('mixins', [])
Jeff Yoon67c3e832020-02-08 07:39:381185
Garrett Beaty8d6708c2023-07-20 17:20:411186 for test_name, test_config in basic_test_definition.items():
Garrett Beaty65b7d362024-10-01 16:21:421187 new_test = copy.copy(test_config)
Xinan Lin05fb9c1752020-12-17 00:15:521188
Jeff Yoon67c3e832020-02-08 07:39:381189 # The identifier is used to make the name of the test unique.
1190 # Generators in the recipe uniquely identify a test by it's name, so we
1191 # don't want to have the same name for each variant.
Garrett Beaty235c1412023-08-29 20:26:291192 new_test['name'] = f'{test_name} {identifier}'
Ben Pastene5f231cf22022-05-05 18:03:071193
1194 # Attach the variant identifier to the test config so downstream
1195 # generators can make modifications based on the original name. This
1196 # is mainly used in generate_gpu_telemetry_test().
Garrett Beaty8d6708c2023-07-20 17:20:411197 new_test['variant_id'] = identifier
Ben Pastene5f231cf22022-05-05 18:03:071198
Garrett Beaty65b7d362024-10-01 16:21:421199 # Save the variant details and mixins to be applied in
1200 # apply_common_transformations to match the order that starlark will
1201 # apply things
1202 new_test['*variant*'] = variant
1203 new_test['*variant_mixins*'] = variant_mixins + mixins
1204
Garrett Beaty8d6708c2023-07-20 17:20:411205 test_suite.setdefault(test_name, []).append(new_test)
1206
Jeff Yoon67c3e832020-02-08 07:39:381207 return test_suite
1208
Jeff Yoon8154e582019-12-03 23:30:011209 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381210 self.check_composition_type_test_suites('matrix_compound_suites',
1211 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011212
1213 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381214 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011215 # referenced by matrix suites exist.
1216 basic_suites = self.test_suites.get('basic_suites')
1217
Brian Sheedy822e03742024-08-09 18:48:141218 def update_tests_uncurried(full_suite, 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 Beaty235c1412023-08-29 20:26:291227 for matrix_suite_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011228 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381229
Jamie Madillcf4f8c72021-05-20 19:24:231230 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381231 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1232
Brian Sheedy822e03742024-08-09 18:48:141233 update_tests = functools.partial(update_tests_uncurried, full_suite)
Garrett Beaty235c1412023-08-29 20:26:291234
Garrett Beaty73c4cd42024-10-04 17:55:081235 mixins = mtx_test_suite_config.get('mixins', [])
Garrett Beaty60a7b2a2023-09-13 23:00:401236 if (variants := mtx_test_suite_config.get('variants')):
Garrett Beaty60a7b2a2023-09-13 23:00:401237 result = self.resolve_variants(basic_test_def, variants, mixins)
Garrett Beaty235c1412023-08-29 20:26:291238 update_tests(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271239 else:
Garrett Beaty73c4cd42024-10-04 17:55:081240 suite = copy.deepcopy(basic_suites[test_suite])
1241 for test_config in suite.values():
1242 test_config['mixins'] = test_config.get('mixins', []) + mixins
Garrett Beaty235c1412023-08-29 20:26:291243 update_tests(suite)
1244 matrix_compound_suites[matrix_suite_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281245
1246 def link_waterfalls_to_test_suites(self):
1247 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231248 for tester_name, tester in waterfall['machines'].items():
1249 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281250 if not value in self.test_suites:
1251 # Hard / impossible to cover this in the unit test.
1252 raise self.unknown_test_suite(
1253 value, tester_name, waterfall['name']) # pragma: no cover
1254 tester['test_suites'][suite] = self.test_suites[value]
1255
1256 def load_configuration_files(self):
Garrett Beaty79339e182023-04-10 20:45:471257 self.waterfalls = self.load_pyl_file(self.args.waterfalls_pyl_path)
1258 self.test_suites = self.load_pyl_file(self.args.test_suites_pyl_path)
1259 self.exceptions = self.load_pyl_file(
1260 self.args.test_suite_exceptions_pyl_path)
1261 self.mixins = self.load_pyl_file(self.args.mixins_pyl_path)
1262 self.gn_isolate_map = self.load_pyl_file(self.args.gn_isolate_map_pyl_path)
Chong Guee622242020-10-28 18:17:351263 for isolate_map in self.args.isolate_map_files:
1264 isolate_map = self.load_pyl_file(isolate_map)
1265 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1266 if duplicates:
1267 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1268 ', '.join(duplicates))
1269 self.gn_isolate_map.update(isolate_map)
1270
Garrett Beaty79339e182023-04-10 20:45:471271 self.variants = self.load_pyl_file(self.args.variants_pyl_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281272
1273 def resolve_configuration_files(self):
Garrett Beaty086b3402024-09-25 23:45:341274 self.resolve_mixins()
Garrett Beaty235c1412023-08-29 20:26:291275 self.resolve_test_names()
Garrett Beatydca3d882023-09-14 23:50:321276 self.resolve_isolate_names()
Garrett Beaty65d44222023-08-01 17:22:111277 self.resolve_dimension_sets()
Chan Lia3ad1502020-04-28 05:32:111278 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281279 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011280 self.resolve_matrix_compound_test_suites()
1281 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281282 self.link_waterfalls_to_test_suites()
1283
Garrett Beaty086b3402024-09-25 23:45:341284 def resolve_mixins(self):
1285 for mixin in self.mixins.values():
1286 mixin.pop('fail_if_unused', None)
1287
Garrett Beaty235c1412023-08-29 20:26:291288 def resolve_test_names(self):
1289 for suite_name, suite in self.test_suites.get('basic_suites').items():
1290 for test_name, test in suite.items():
1291 if 'name' in test:
1292 raise BBGenErr(
1293 f'The name field is set in test {test_name} in basic suite '
1294 f'{suite_name}, this is not supported, the test name is the key '
1295 'within the basic suite')
Garrett Beatyffe83c4f2023-09-08 19:07:371296 # When a test is expanded with variants, this will be overwritten, but
1297 # this ensures every test definition has the name field set
1298 test['name'] = test_name
Garrett Beaty235c1412023-08-29 20:26:291299
Garrett Beatydca3d882023-09-14 23:50:321300 def resolve_isolate_names(self):
1301 for suite_name, suite in self.test_suites.get('basic_suites').items():
1302 for test_name, test in suite.items():
1303 if 'isolate_name' in test:
1304 raise BBGenErr(
1305 f'The isolate_name field is set in test {test_name} in basic '
1306 f'suite {suite_name}, the test field should be used instead')
1307
Garrett Beaty65d44222023-08-01 17:22:111308 def resolve_dimension_sets(self):
Garrett Beaty65d44222023-08-01 17:22:111309
1310 def definitions():
1311 for suite_name, suite in self.test_suites.get('basic_suites', {}).items():
1312 for test_name, test in suite.items():
1313 yield test, f'test {test_name} in basic suite {suite_name}'
1314
1315 for mixin_name, mixin in self.mixins.items():
1316 yield mixin, f'mixin {mixin_name}'
1317
1318 for waterfall in self.waterfalls:
1319 for builder_name, builder in waterfall.get('machines', {}).items():
1320 yield (
1321 builder,
1322 f'builder {builder_name} in waterfall {waterfall["name"]}',
1323 )
1324
1325 for test_name, exceptions in self.exceptions.items():
1326 modifications = exceptions.get('modifications', {})
1327 for builder_name, mods in modifications.items():
1328 yield (
1329 mods,
1330 f'exception for test {test_name} on builder {builder_name}',
1331 )
1332
1333 for definition, location in definitions():
1334 for swarming_attr in (
1335 'swarming',
1336 'android_swarming',
1337 'chromeos_swarming',
1338 ):
1339 if (swarming :=
1340 definition.get(swarming_attr)) and 'dimension_sets' in swarming:
Garrett Beatyade673d2023-08-04 22:00:251341 raise BBGenErr(
1342 f'dimension_sets is no longer supported (set in {location}),'
1343 ' instead, use set dimensions to a single dict')
Garrett Beaty65d44222023-08-01 17:22:111344
Nico Weberd18b8962018-05-16 19:39:381345 def unknown_bot(self, bot_name, waterfall_name):
1346 return BBGenErr(
1347 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1348
Kenneth Russelleb60cbd22017-12-05 07:54:281349 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1350 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381351 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281352 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1353
1354 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1355 return BBGenErr(
1356 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1357 ' on waterfall ' + waterfall_name)
1358
Garrett Beatye3a606ceb2024-04-30 22:13:131359 def ensure_valid_mixin_list(self, mixins, location):
1360 if not isinstance(mixins, list):
1361 raise BBGenErr(
1362 f"got '{mixins}', should be a list of mixin names: {location}")
1363 for mixin in mixins:
1364 if not mixin in self.mixins:
1365 raise BBGenErr(f'bad mixin {mixin}: {location}')
Stephen Martinisb6a50492018-09-12 23:59:321366
Garrett Beatye3a606ceb2024-04-30 22:13:131367 def apply_mixins(self, test, mixins, mixins_to_ignore, builder=None):
1368 for mixin in mixins:
1369 if mixin not in mixins_to_ignore:
Austin Eng148d9f0f2022-02-08 19:18:531370 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinis0382bc12018-09-17 22:29:071371 return test
Stephen Martinisb6a50492018-09-12 23:59:321372
Garrett Beaty8d6708c2023-07-20 17:20:411373 def apply_mixin(self, mixin, test, builder=None):
Stephen Martinisb72f6d22018-10-04 23:29:011374 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321375
Garrett Beaty4c35b142023-06-23 21:01:231376 A mixin is applied by copying all fields from the mixin into the
1377 test with the following exceptions:
1378 * For the various *args keys, the test's existing value (an empty
1379 list if not present) will be extended with the mixin's value.
1380 * The sub-keys of the swarming value will be copied to the test's
1381 swarming value with the following exceptions:
Garrett Beatyade673d2023-08-04 22:00:251382 * For the named_caches sub-keys, the test's existing value (an
1383 empty list if not present) will be extended with the mixin's
1384 value.
1385 * For the dimensions sub-key, the tests's existing value (an empty
1386 dict if not present) will be updated with the mixin's value.
Stephen Martinisb6a50492018-09-12 23:59:321387 """
Garrett Beaty4c35b142023-06-23 21:01:231388
Stephen Martinisb6a50492018-09-12 23:59:321389 new_test = copy.deepcopy(test)
1390 mixin = copy.deepcopy(mixin)
Garrett Beaty8d6708c2023-07-20 17:20:411391
1392 if 'description' in mixin:
1393 description = []
1394 if 'description' in new_test:
1395 description.append(new_test['description'])
1396 description.append(mixin.pop('description'))
1397 new_test['description'] = '\n'.join(description)
1398
Stephen Martinisb72f6d22018-10-04 23:29:011399 if 'swarming' in mixin:
Garrett Beaty1b271622024-10-01 22:30:251400 self.merge_swarming(new_test.setdefault('swarming', {}),
1401 mixin.pop('swarming'))
Stephen Martinisb72f6d22018-10-04 23:29:011402
Garrett Beaty050fc4c2025-01-09 19:44:501403 if 'skylab' in mixin:
1404 new_test.setdefault('skylab', {}).update(mixin.pop('skylab'))
1405
Garrett Beatye3a606ceb2024-04-30 22:13:131406 for a in ('args', 'precommit_args', 'non_precommit_args'):
Garrett Beaty4c35b142023-06-23 21:01:231407 if (value := mixin.pop(a, None)) is None:
1408 continue
1409 if not isinstance(value, list):
1410 raise BBGenErr(f'"{a}" must be a list')
1411 new_test.setdefault(a, []).extend(value)
1412
Garrett Beatye3a606ceb2024-04-30 22:13:131413 # At this point, all keys that require merging are taken care of, so the
1414 # remaining entries can be copied over. The os-conditional entries will be
1415 # resolved immediately after and they are resolved before any mixins are
1416 # applied, so there's are no concerns about overwriting the corresponding
1417 # entry in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011418 new_test.update(mixin)
Garrett Beatye3a606ceb2024-04-30 22:13:131419 if builder:
1420 self.resolve_os_conditional_values(new_test, builder)
1421
1422 if 'args' in new_test:
1423 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1424
Stephen Martinisb6a50492018-09-12 23:59:321425 return new_test
1426
Greg Gutermanf60eb052020-03-12 17:40:011427 def generate_output_tests(self, waterfall):
1428 """Generates the tests for a waterfall.
1429
1430 Args:
1431 waterfall: a dictionary parsed from a master pyl file
1432 Returns:
1433 A dictionary mapping builders to test specs
1434 """
1435 return {
Jamie Madillcf4f8c72021-05-20 19:24:231436 name: self.get_tests_for_config(waterfall, name, config)
1437 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011438 }
1439
1440 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531441 generator_map = self.get_test_generator_map()
1442 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281443
Greg Gutermanf60eb052020-03-12 17:40:011444 tests = {}
1445 # Copy only well-understood entries in the machine's configuration
1446 # verbatim into the generated JSON.
1447 if 'additional_compile_targets' in config:
1448 tests['additional_compile_targets'] = config[
1449 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231450 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011451 if test_type not in generator_map:
1452 raise self.unknown_test_suite_type(
1453 test_type, name, waterfall['name']) # pragma: no cover
1454 test_generator = generator_map[test_type]
1455 # Let multiple kinds of generators generate the same kinds
1456 # of tests. For example, gpu_telemetry_tests are a
1457 # specialization of isolated_scripts.
1458 new_tests = test_generator.generate(
1459 waterfall, name, config, input_tests)
1460 remapped_test_type = test_type_remapper.get(test_type, test_type)
Garrett Beatyffe83c4f2023-09-08 19:07:371461 tests.setdefault(remapped_test_type, []).extend(new_tests)
1462
1463 for test_type, tests_for_type in tests.items():
1464 if test_type == 'additional_compile_targets':
1465 continue
1466 tests[test_type] = sorted(tests_for_type, key=lambda t: t['name'])
Greg Gutermanf60eb052020-03-12 17:40:011467
1468 return tests
1469
1470 def jsonify(self, all_tests):
1471 return json.dumps(
1472 all_tests, indent=2, separators=(',', ': '),
1473 sort_keys=True) + '\n'
1474
1475 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281476 self.load_configuration_files()
1477 self.resolve_configuration_files()
1478 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011479 result = collections.defaultdict(dict)
1480
Stephanie Kim572b43c02023-04-13 14:24:131481 if os.path.exists(self.args.autoshard_exceptions_json_path):
1482 autoshards = json.loads(
1483 self.read_file(self.args.autoshard_exceptions_json_path))
1484 else:
1485 autoshards = {}
1486
Dirk Pranke6269d302020-10-01 00:14:391487 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011488 for waterfall in self.waterfalls:
1489 for field in required_fields:
1490 # Verify required fields
1491 if field not in waterfall:
Brian Sheedy0d2300f32024-08-13 23:14:411492 raise BBGenErr('Waterfall %s has no %s' % (waterfall['name'], field))
Greg Gutermanf60eb052020-03-12 17:40:011493
1494 # Handle filter flag, if specified
1495 if filters and waterfall['name'] not in filters:
1496 continue
1497
1498 # Join config files and hardcoded values together
1499 all_tests = self.generate_output_tests(waterfall)
1500 result[waterfall['name']] = all_tests
1501
Stephanie Kim572b43c02023-04-13 14:24:131502 if not autoshards:
1503 continue
1504 for builder, test_spec in all_tests.items():
1505 for target_type, test_list in test_spec.items():
1506 if target_type == 'additional_compile_targets':
1507 continue
1508 for test_dict in test_list:
1509 # Suites that apply variants or other customizations will create
1510 # test_dicts that have "name" value that is different from the
Garrett Beatyffe83c4f2023-09-08 19:07:371511 # "test" value.
Stephanie Kim572b43c02023-04-13 14:24:131512 # e.g. name = vulkan_swiftshader_content_browsertests, but
1513 # test = content_browsertests and
1514 # test_id_prefix = "ninja://content/test:content_browsertests/"
Garrett Beatyffe83c4f2023-09-08 19:07:371515 test_name = test_dict['name']
Stephanie Kim572b43c02023-04-13 14:24:131516 shard_info = autoshards.get(waterfall['name'],
1517 {}).get(builder, {}).get(test_name)
1518 if shard_info:
1519 test_dict['swarming'].update(
1520 {'shards': int(shard_info['shards'])})
1521
Greg Gutermanf60eb052020-03-12 17:40:011522 # Add do not edit warning
1523 for tests in result.values():
1524 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1525 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1526
1527 return result
1528
1529 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281530 suffix = '.json'
1531 if self.args.new_files:
1532 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011533
1534 for filename, contents in result.items():
1535 jsonstr = self.jsonify(contents)
Garrett Beaty79339e182023-04-10 20:45:471536 file_path = os.path.join(self.args.output_dir, filename + suffix)
1537 self.write_file(file_path, jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281538
Nico Weberd18b8962018-05-16 19:39:381539 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161540 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421541 # NOTE: This reference can cause issues; if a file changes there, the
1542 # presubmit here won't be run by default. A manually maintained list there
1543 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1544 # references to configs outside of this directory are added, please change
1545 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1546 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491547
Garrett Beaty7e866fc2021-06-16 14:12:101548 # Get the generated project.pyl so we can check if we should be enforcing
1549 # that the specs are for builders that actually exist
1550 # If not, return None to indicate that we won't enforce that builders in
1551 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491552 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1553 'project.pyl')
1554 if os.path.exists(project_pyl_path):
1555 settings = ast.literal_eval(self.read_file(project_pyl_path))
1556 if not settings.get('validate_source_side_specs_have_builder', True):
1557 return None
1558
Nico Weberd18b8962018-05-16 19:39:381559 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311560 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161561 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1562 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431563 for c in milo_configs:
1564 for l in self.read_file(c).splitlines():
1565 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311566 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431567 continue
1568 # l looks like
1569 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1570 # Extract win_chromium_dbg_ng part.
1571 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381572 return bot_names
1573
Ben Pastene9a010082019-09-25 20:41:371574 def get_internal_waterfalls(self):
1575 # Similar to get_builders_that_do_not_actually_exist above, but for
1576 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201577 return [
Kramer Ge3bf853a2023-04-13 19:39:471578 'chrome', 'chrome.pgo', 'chrome.gpu.fyi', 'internal.chrome.fyi',
Ming-Ying Chungc71698b2025-03-07 19:11:511579 'internal.chromeos.fyi', 'internal.optimization_guide',
1580 'internal.translatekit', 'internal.soda', 'chromeos.preuprev'
Yuke Liaoe6c23dd2021-07-28 16:12:201581 ]
Ben Pastene9a010082019-09-25 20:41:371582
Stephen Martinisf83893722018-09-19 00:02:181583 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201584 self.check_input_files_sorting(verbose)
1585
Kenneth Russelleb60cbd22017-12-05 07:54:281586 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011587 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381588 self.check_composition_type_test_suites('matrix_compound_suites',
1589 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111590 self.resolve_test_id_prefixes()
Garrett Beaty1ead4a52023-12-07 19:16:421591
1592 # All test suites must be referenced. Check this before flattening the test
1593 # suites so that we can transitively check the basic suites for compound
1594 # suites and matrix compound suites (otherwise we would determine a basic
1595 # suite is used if it shared a name with a test present in a basic suite
1596 # that is used).
1597 all_suites = set(
1598 itertools.chain(*(self.test_suites.get(a, {}) for a in (
1599 'basic_suites',
1600 'compound_suites',
1601 'matrix_compound_suites',
1602 ))))
1603 unused_suites = set(all_suites)
1604 generator_map = self.get_test_generator_map()
1605 for waterfall in self.waterfalls:
1606 for bot_name, tester in waterfall['machines'].items():
1607 for suite_type, suite in tester.get('test_suites', {}).items():
1608 if suite_type not in generator_map:
1609 raise self.unknown_test_suite_type(suite_type, bot_name,
1610 waterfall['name'])
1611 if suite not in all_suites:
1612 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1613 unused_suites.discard(suite)
1614 # For each compound suite or matrix compound suite, if the suite was used,
1615 # remove all of the basic suites that it composes from the set of unused
1616 # suites
1617 for a in ('compound_suites', 'matrix_compound_suites'):
1618 for suite, sub_suites in self.test_suites.get(a, {}).items():
1619 if suite not in unused_suites:
1620 unused_suites.difference_update(sub_suites)
1621 if unused_suites:
1622 raise BBGenErr('The following test suites were unreferenced by bots on '
1623 'the waterfalls: ' + str(unused_suites))
1624
Stephen Martinis54d64ad2018-09-21 22:16:201625 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381626
1627 # All bots should exist.
1628 bot_names = self.get_valid_bot_names()
Garrett Beaty2a02de3c2020-05-15 13:57:351629 if bot_names is not None:
1630 internal_waterfalls = self.get_internal_waterfalls()
1631 for waterfall in self.waterfalls:
Alison Gale923a33e2024-04-22 23:34:281632 # TODO(crbug.com/41474799): Remove the need for this exception.
Garrett Beaty2a02de3c2020-05-15 13:57:351633 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011634 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351635 for bot_name in waterfall['machines']:
Garrett Beaty2a02de3c2020-05-15 13:57:351636 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581637 if waterfall['name'] in [
1638 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1639 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351640 # TODO(thakis): Remove this once these bots move to luci.
1641 continue # pragma: no cover
1642 if waterfall['name'] in ['tryserver.webrtc',
1643 'webrtc.chromium.fyi.experimental']:
1644 # These waterfalls have their bot configs in a different repo.
1645 # so we don't know about their bot names.
1646 continue # pragma: no cover
1647 if waterfall['name'] in ['client.devtools-frontend.integration',
1648 'tryserver.devtools-frontend',
1649 'chromium.devtools-frontend']:
1650 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201651 if waterfall['name'] in ['client.openscreen.chromium']:
1652 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351653 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381654
Kenneth Russelleb60cbd22017-12-05 07:54:281655 # All test suite exceptions must refer to bots on the waterfall.
1656 all_bots = set()
1657 missing_bots = set()
1658 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231659 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281660 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281661 # In order to disambiguate between bots with the same name on
1662 # different waterfalls, support has been added to various
1663 # exceptions for concatenating the waterfall name after the bot
1664 # name.
1665 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231666 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381667 removals = (exception.get('remove_from', []) +
1668 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231669 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381670 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281671 if removal not in all_bots:
1672 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411673
Kenneth Russelleb60cbd22017-12-05 07:54:281674 if missing_bots:
1675 raise BBGenErr('The following nonexistent machines were referenced in '
1676 'the test suite exceptions: ' + str(missing_bots))
1677
Garrett Beatyb061e69d2023-06-27 16:15:351678 for name, mixin in self.mixins.items():
1679 if '$mixin_append' in mixin:
1680 raise BBGenErr(
1681 f'$mixin_append is no longer supported (set in mixin "{name}"),'
1682 ' args and named caches specified as normal will be appended')
1683
Garrett Beatyac3dc962024-10-11 17:30:351684 # All variant references must be referenced
1685 seen_variants = set()
1686 for suite in self.test_suites.values():
1687 if isinstance(suite, list):
1688 continue
1689
1690 for test in suite.values():
1691 if isinstance(test, dict):
1692 for variant in test.get('variants', []):
1693 if isinstance(variant, str):
1694 seen_variants.add(variant)
1695
1696 missing_variants = set(self.variants.keys()) - seen_variants
1697 if missing_variants:
1698 raise BBGenErr('The following variants were unreferenced: %s. They must '
1699 'be referenced in a matrix test suite under the variants '
1700 'key.' % str(missing_variants))
1701
Stephen Martinis0382bc12018-09-17 22:29:071702 # All mixins must be referenced
1703 seen_mixins = set()
1704 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011705 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231706 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011707 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071708 for suite in self.test_suites.values():
1709 if isinstance(suite, list):
1710 # Don't care about this, it's a composition, which shouldn't include a
1711 # swarming mixin.
1712 continue
1713
1714 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561715 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011716 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Garrett Beaty4b9f1752024-09-26 20:02:501717 seen_mixins = seen_mixins.union(
1718 test.get('test_common', {}).get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071719
Zhaoyang Li9da047d52021-05-10 21:31:441720 for variant in self.variants:
1721 # Unpack the variant from variants.pyl if it's string based.
1722 if isinstance(variant, str):
1723 variant = self.variants[variant]
1724 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1725
Garrett Beaty086b3402024-09-25 23:45:341726 missing_mixins = set()
1727 for name, mixin_value in self.mixins.items():
1728 if name not in seen_mixins and mixin_value.get('fail_if_unused', True):
1729 missing_mixins.add(name)
Stephen Martinis0382bc12018-09-17 22:29:071730 if missing_mixins:
1731 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1732 ' referenced in a waterfall, machine, or test suite.' % (
1733 str(missing_mixins)))
1734
Stephen Martinis54d64ad2018-09-21 22:16:201735
Garrett Beaty79339e182023-04-10 20:45:471736 def type_assert(self, node, typ, file_path, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201737 """Asserts that the Python AST node |node| is of type |typ|.
1738
1739 If verbose is set, it prints out some helpful context lines, showing where
1740 exactly the error occurred in the file.
1741 """
1742 if not isinstance(node, typ):
1743 if verbose:
Brian Sheedy0d2300f32024-08-13 23:14:411744 lines = [''] + self.read_file(file_path).splitlines()
Stephen Martinis54d64ad2018-09-21 22:16:201745
1746 context = 2
1747 lines_start = max(node.lineno - context, 0)
1748 # Add one to include the last line
1749 lines_end = min(node.lineno + context, len(lines)) + 1
Garrett Beaty79339e182023-04-10 20:45:471750 lines = itertools.chain(
1751 ['== %s ==\n' % file_path],
Brian Sheedy0d2300f32024-08-13 23:14:411752 ['<snip>\n'],
Garrett Beaty79339e182023-04-10 20:45:471753 [
1754 '%d %s' % (lines_start + i, line)
1755 for i, line in enumerate(lines[lines_start:lines_start +
1756 context])
1757 ],
1758 ['-' * 80 + '\n'],
1759 ['%d %s' % (node.lineno, lines[node.lineno])],
1760 [
1761 '-' * (node.col_offset + 3) + '^' + '-' *
1762 (80 - node.col_offset - 4) + '\n'
1763 ],
1764 [
1765 '%d %s' % (node.lineno + 1 + i, line)
1766 for i, line in enumerate(lines[node.lineno + 1:lines_end])
1767 ],
Brian Sheedy0d2300f32024-08-13 23:14:411768 ['<snip>\n'],
Stephen Martinis54d64ad2018-09-21 22:16:201769 )
1770 # Print out a useful message when a type assertion fails.
1771 for l in lines:
1772 self.print_line(l.strip())
1773
1774 node_dumped = ast.dump(node, annotate_fields=False)
1775 # If the node is huge, truncate it so everything fits in a terminal
1776 # window.
1777 if len(node_dumped) > 60: # pragma: no cover
1778 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1779 raise BBGenErr(
Brian Sheedy0d2300f32024-08-13 23:14:411780 "Invalid .pyl file '%s'. Python AST node %r on line %s expected to"
Garrett Beaty79339e182023-04-10 20:45:471781 ' be %s, is %s' %
1782 (file_path, node_dumped, node.lineno, typ, type(node)))
Stephen Martinis54d64ad2018-09-21 22:16:201783
Garrett Beaty79339e182023-04-10 20:45:471784 def check_ast_list_formatted(self,
1785 keys,
1786 file_path,
1787 verbose,
Stephen Martinis1384ff92020-01-07 19:52:151788 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531789 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201790
Stephen Martinis5bef0fc2020-01-06 22:47:531791 Currently only checks to ensure they're correctly sorted, and that there
1792 are no duplicates.
1793
1794 Args:
1795 keys: An python list of AST nodes.
1796
1797 It's a list of AST nodes instead of a list of strings because
1798 when verbose is set, it tries to print out context of where the
1799 diffs are in the file.
Garrett Beaty79339e182023-04-10 20:45:471800 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531801 verbose: If set, print out diff information about how the keys are
1802 incorrectly formatted.
1803 check_sorting: If true, checks if the list is sorted.
1804 Returns:
1805 If the keys are correctly formatted.
1806 """
1807 if not keys:
1808 return True
1809
1810 assert isinstance(keys[0], ast.Str)
1811
1812 keys_strs = [k.s for k in keys]
1813 # Keys to diff against. Used below.
1814 keys_to_diff_against = None
1815 # If the list is properly formatted.
1816 list_formatted = True
1817
1818 # Duplicates are always bad.
1819 if len(set(keys_strs)) != len(keys_strs):
1820 list_formatted = False
1821 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1822
1823 if check_sorting and sorted(keys_strs) != keys_strs:
1824 list_formatted = False
1825 if list_formatted:
1826 return True
1827
1828 if verbose:
1829 line_num = keys[0].lineno
1830 keys = [k.s for k in keys]
1831 if check_sorting:
1832 # If we have duplicates, sorting this will take care of it anyways.
1833 keys_to_diff_against = sorted(set(keys))
1834 # else, keys_to_diff_against is set above already
1835
1836 self.print_line('=' * 80)
1837 self.print_line('(First line of keys is %s)' % line_num)
Garrett Beaty79339e182023-04-10 20:45:471838 for line in difflib.context_diff(keys,
1839 keys_to_diff_against,
1840 fromfile='current (%r)' % file_path,
1841 tofile='sorted',
1842 lineterm=''):
Stephen Martinis5bef0fc2020-01-06 22:47:531843 self.print_line(line)
1844 self.print_line('=' * 80)
1845
1846 return False
1847
Garrett Beaty79339e182023-04-10 20:45:471848 def check_ast_dict_formatted(self, node, file_path, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531849 """Checks if an ast dictionary's keys are correctly formatted.
1850
1851 Just a simple wrapper around check_ast_list_formatted.
1852 Args:
1853 node: An AST node. Assumed to be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471854 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531855 verbose: If set, print out diff information about how the keys are
1856 incorrectly formatted.
1857 check_sorting: If true, checks if the list is sorted.
1858 Returns:
1859 If the dictionary is correctly formatted.
1860 """
Stephen Martinis54d64ad2018-09-21 22:16:201861 keys = []
1862 # The keys of this dict are ordered as ordered in the file; normal python
1863 # dictionary keys are given an arbitrary order, but since we parsed the
1864 # file itself, the order as given in the file is preserved.
1865 for key in node.keys:
Garrett Beaty79339e182023-04-10 20:45:471866 self.type_assert(key, ast.Str, file_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531867 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201868
Garrett Beaty79339e182023-04-10 20:45:471869 return self.check_ast_list_formatted(keys, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181870
1871 def check_input_files_sorting(self, verbose=False):
Alison Gale923a33e2024-04-22 23:34:281872 # TODO(crbug.com/41415841): Add the ability for this script to
Stephen Martinis54d64ad2018-09-21 22:16:201873 # actually format the files, rather than just complain if they're
1874 # incorrectly formatted.
1875 bad_files = set()
Garrett Beaty79339e182023-04-10 20:45:471876
1877 def parse_file(file_path):
Stephen Martinis5bef0fc2020-01-06 22:47:531878 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201879
Stephen Martinis5bef0fc2020-01-06 22:47:531880 Returns an AST node representing the value in the pyl file."""
Garrett Beaty79339e182023-04-10 20:45:471881 parsed = ast.parse(self.read_file(file_path))
Stephen Martinisf83893722018-09-19 00:02:181882
Stephen Martinisf83893722018-09-19 00:02:181883 # Must be a module.
Garrett Beaty79339e182023-04-10 20:45:471884 self.type_assert(parsed, ast.Module, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181885 module = parsed.body
1886
1887 # Only one expression in the module.
Garrett Beaty79339e182023-04-10 20:45:471888 self.type_assert(module, list, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181889 if len(module) != 1: # pragma: no cover
Garrett Beaty79339e182023-04-10 20:45:471890 raise BBGenErr('Invalid .pyl file %s' % file_path)
Stephen Martinisf83893722018-09-19 00:02:181891 expr = module[0]
Garrett Beaty79339e182023-04-10 20:45:471892 self.type_assert(expr, ast.Expr, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181893
Stephen Martinis5bef0fc2020-01-06 22:47:531894 return expr.value
1895
1896 # Handle this separately
Garrett Beaty79339e182023-04-10 20:45:471897 value = parse_file(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531898 # Value should be a list.
Garrett Beaty79339e182023-04-10 20:45:471899 self.type_assert(value, ast.List, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531900
1901 keys = []
Joshua Hood56c673c2022-03-02 20:29:331902 for elm in value.elts:
Garrett Beaty79339e182023-04-10 20:45:471903 self.type_assert(elm, ast.Dict, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531904 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331905 for key, val in zip(elm.keys, elm.values):
Garrett Beaty79339e182023-04-10 20:45:471906 self.type_assert(key, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531907 if key.s == 'machines':
Garrett Beaty79339e182023-04-10 20:45:471908 if not self.check_ast_dict_formatted(
1909 val, self.args.waterfalls_pyl_path, verbose):
1910 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531911
Brian Sheedy0d2300f32024-08-13 23:14:411912 if key.s == 'name':
Garrett Beaty79339e182023-04-10 20:45:471913 self.type_assert(val, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531914 waterfall_name = val
1915 assert waterfall_name
1916 keys.append(waterfall_name)
1917
Garrett Beaty79339e182023-04-10 20:45:471918 if not self.check_ast_list_formatted(keys, self.args.waterfalls_pyl_path,
1919 verbose):
1920 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531921
Garrett Beaty79339e182023-04-10 20:45:471922 for file_path in (
1923 self.args.mixins_pyl_path,
1924 self.args.test_suites_pyl_path,
1925 self.args.test_suite_exceptions_pyl_path,
Stephen Martinis5bef0fc2020-01-06 22:47:531926 ):
Garrett Beaty79339e182023-04-10 20:45:471927 value = parse_file(file_path)
Stephen Martinisf83893722018-09-19 00:02:181928 # Value should be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471929 self.type_assert(value, ast.Dict, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181930
Garrett Beaty79339e182023-04-10 20:45:471931 if not self.check_ast_dict_formatted(value, file_path, verbose):
1932 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531933
Garrett Beaty79339e182023-04-10 20:45:471934 if file_path == self.args.test_suites_pyl_path:
Jeff Yoon8154e582019-12-03 23:30:011935 expected_keys = ['basic_suites',
1936 'compound_suites',
1937 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201938 actual_keys = [node.s for node in value.keys]
1939 assert all(key in expected_keys for key in actual_keys), (
Garrett Beaty79339e182023-04-10 20:45:471940 'Invalid %r file; expected keys %r, got %r' %
1941 (file_path, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:331942 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:201943 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011944 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201945 for suite_group in suite_dicts:
Garrett Beaty79339e182023-04-10 20:45:471946 if not self.check_ast_dict_formatted(suite_group, file_path, verbose):
1947 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181948
Stephen Martinis5bef0fc2020-01-06 22:47:531949 for key, suite in zip(value.keys, value.values):
1950 # The compound suites are checked in
1951 # 'check_composition_type_test_suites()'
1952 if key.s == 'basic_suites':
1953 for group in suite.values:
Garrett Beaty79339e182023-04-10 20:45:471954 if not self.check_ast_dict_formatted(group, file_path, verbose):
1955 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531956 break
Stephen Martinis54d64ad2018-09-21 22:16:201957
Garrett Beaty79339e182023-04-10 20:45:471958 elif file_path == self.args.test_suite_exceptions_pyl_path:
Stephen Martinis5bef0fc2020-01-06 22:47:531959 # Check the values for each test.
1960 for test in value.values:
1961 for kind, node in zip(test.keys, test.values):
1962 if isinstance(node, ast.Dict):
Garrett Beaty79339e182023-04-10 20:45:471963 if not self.check_ast_dict_formatted(node, file_path, verbose):
1964 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531965 elif kind.s == 'remove_from':
1966 # Don't care about sorting; these are usually grouped, since the
1967 # same bug can affect multiple builders. Do want to make sure
1968 # there aren't duplicates.
Garrett Beaty79339e182023-04-10 20:45:471969 if not self.check_ast_list_formatted(
1970 node.elts, file_path, verbose, check_sorting=False):
1971 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181972
1973 if bad_files:
1974 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201975 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531976 'unsorted, or have duplicates. Re-run this with --verbose to see '
1977 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181978
Kenneth Russelleb60cbd22017-12-05 07:54:281979 def check_output_file_consistency(self, verbose=False):
1980 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011981 # All waterfalls/bucket .json files must have been written
1982 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:281983 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011984 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:161985 outputs = self.generate_outputs()
1986 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:011987 expected = self.jsonify(expected_contents)
Garrett Beaty79339e182023-04-10 20:45:471988 file_path = os.path.join(self.args.output_dir, filename + '.json')
Ben Pastenef21cda32023-03-30 22:00:571989 current = self.read_file(file_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281990 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:011991 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:321992 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:011993 self.print_line('File ' + filename +
1994 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321995 'contents:')
1996 for line in difflib.unified_diff(
1997 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501998 current.splitlines(),
1999 fromfile='expected', tofile='current'):
2000 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:012001
2002 if ungenerated_files:
2003 raise BBGenErr(
2004 'The following files have not been properly '
2005 'autogenerated by generate_buildbot_json.py: ' +
2006 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:282007
Dirk Pranke772f55f2021-04-28 04:51:162008 for builder_group, builders in outputs.items():
2009 for builder, step_types in builders.items():
Garrett Beatydca3d882023-09-14 23:50:322010 for test_type in ('gtest_tests', 'isolated_scripts'):
2011 for step_data in step_types.get(test_type, []):
2012 step_name = step_data['name']
2013 self._check_swarming_config(builder_group, builder, step_name,
2014 step_data)
Dirk Pranke772f55f2021-04-28 04:51:162015
2016 def _check_swarming_config(self, filename, builder, step_name, step_data):
Alison Gale47d1537d2024-04-19 21:31:462017 # TODO(crbug.com/40179524): Ensure all swarming tests specify cpu, not
Dirk Pranke772f55f2021-04-28 04:51:162018 # just mac tests.
Garrett Beatybb18d532023-06-26 22:16:332019 if 'swarming' in step_data:
Garrett Beatyade673d2023-08-04 22:00:252020 dimensions = step_data['swarming'].get('dimensions')
2021 if not dimensions:
Tatsuhisa Yamaguchif1878d52023-11-06 06:02:252022 raise BBGenErr('%s: %s / %s : dimensions must be specified for all '
Dirk Pranke772f55f2021-04-28 04:51:162023 'swarmed tests' % (filename, builder, step_name))
Garrett Beatyade673d2023-08-04 22:00:252024 if not dimensions.get('os'):
2025 raise BBGenErr('%s: %s / %s : os must be specified for all '
2026 'swarmed tests' % (filename, builder, step_name))
2027 if 'Mac' in dimensions.get('os') and not dimensions.get('cpu'):
2028 raise BBGenErr('%s: %s / %s : cpu must be specified for mac '
2029 'swarmed tests' % (filename, builder, step_name))
Dirk Pranke772f55f2021-04-28 04:51:162030
Kenneth Russelleb60cbd22017-12-05 07:54:282031 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502032 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282033 self.check_output_file_consistency(verbose) # pragma: no cover
2034
Karen Qiane24b7ee2019-02-12 23:37:062035 def does_test_match(self, test_info, params_dict):
2036 """Checks to see if the test matches the parameters given.
2037
2038 Compares the provided test_info with the params_dict to see
2039 if the bot matches the parameters given. If so, returns True.
2040 Else, returns false.
2041
2042 Args:
2043 test_info (dict): Information about a specific bot provided
2044 in the format shown in waterfalls.pyl
2045 params_dict (dict): Dictionary of parameters and their values
2046 to look for in the bot
2047 Ex: {
2048 'device_os':'android',
2049 '--flag':True,
2050 'mixins': ['mixin1', 'mixin2'],
2051 'ex_key':'ex_value'
2052 }
2053
2054 """
2055 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2056 'kvm', 'pool', 'integrity'] # dimension parameters
2057 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2058 'can_use_on_swarming_builders']
2059 for param in params_dict:
2060 # if dimension parameter
2061 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2062 if not 'swarming' in test_info:
2063 return False
2064 swarming = test_info['swarming']
2065 if param in SWARMING_PARAMS:
2066 if not param in swarming:
2067 return False
2068 if not str(swarming[param]) == params_dict[param]:
2069 return False
2070 else:
Garrett Beatyade673d2023-08-04 22:00:252071 if not 'dimensions' in swarming:
Karen Qiane24b7ee2019-02-12 23:37:062072 return False
Garrett Beatyade673d2023-08-04 22:00:252073 dimensions = swarming['dimensions']
Karen Qiane24b7ee2019-02-12 23:37:062074 # only looking at the first dimension set
Garrett Beatyade673d2023-08-04 22:00:252075 if not param in dimensions:
Karen Qiane24b7ee2019-02-12 23:37:062076 return False
Garrett Beatyade673d2023-08-04 22:00:252077 if not dimensions[param] == params_dict[param]:
Karen Qiane24b7ee2019-02-12 23:37:062078 return False
2079
2080 # if flag
2081 elif param.startswith('--'):
2082 if not 'args' in test_info:
2083 return False
2084 if not param in test_info['args']:
2085 return False
2086
2087 # not dimension parameter/flag/mixin
2088 else:
2089 if not param in test_info:
2090 return False
2091 if not test_info[param] == params_dict[param]:
2092 return False
2093 return True
2094 def error_msg(self, msg):
2095 """Prints an error message.
2096
2097 In addition to a catered error message, also prints
2098 out where the user can find more help. Then, program exits.
2099 """
2100 self.print_line(msg + (' If you need more information, ' +
2101 'please run with -h or --help to see valid commands.'))
2102 sys.exit(1)
2103
2104 def find_bots_that_run_test(self, test, bots):
2105 matching_bots = []
2106 for bot in bots:
2107 bot_info = bots[bot]
2108 tests = self.flatten_tests_for_bot(bot_info)
2109 for test_info in tests:
Garrett Beatyffe83c4f2023-09-08 19:07:372110 test_name = test_info['name']
Karen Qiane24b7ee2019-02-12 23:37:062111 if not test_name == test:
2112 continue
2113 matching_bots.append(bot)
2114 return matching_bots
2115
2116 def find_tests_with_params(self, tests, params_dict):
2117 matching_tests = []
2118 for test_name in tests:
2119 test_info = tests[test_name]
2120 if not self.does_test_match(test_info, params_dict):
2121 continue
2122 if not test_name in matching_tests:
2123 matching_tests.append(test_name)
2124 return matching_tests
2125
2126 def flatten_waterfalls_for_query(self, waterfalls):
2127 bots = {}
2128 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012129 waterfall_tests = self.generate_output_tests(waterfall)
2130 for bot in waterfall_tests:
2131 bot_info = waterfall_tests[bot]
2132 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062133 return bots
2134
2135 def flatten_tests_for_bot(self, bot_info):
2136 """Returns a list of flattened tests.
2137
2138 Returns a list of tests not grouped by test category
2139 for a specific bot.
2140 """
2141 TEST_CATS = self.get_test_generator_map().keys()
2142 tests = []
2143 for test_cat in TEST_CATS:
2144 if not test_cat in bot_info:
2145 continue
2146 test_cat_tests = bot_info[test_cat]
2147 tests = tests + test_cat_tests
2148 return tests
2149
2150 def flatten_tests_for_query(self, test_suites):
2151 """Returns a flattened dictionary of tests.
2152
2153 Returns a dictionary of tests associate with their
2154 configuration, not grouped by their test suite.
2155 """
2156 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232157 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062158 for test in test_suite:
2159 test_info = test_suite[test]
2160 test_name = test
Karen Qiane24b7ee2019-02-12 23:37:062161 tests[test_name] = test_info
2162 return tests
2163
2164 def parse_query_filter_params(self, params):
2165 """Parses the filter parameters.
2166
2167 Creates a dictionary from the parameters provided
2168 to filter the bot array.
2169 """
2170 params_dict = {}
2171 for p in params:
2172 # flag
Brian Sheedy0d2300f32024-08-13 23:14:412173 if p.startswith('--'):
Karen Qiane24b7ee2019-02-12 23:37:062174 params_dict[p] = True
2175 else:
Brian Sheedy0d2300f32024-08-13 23:14:412176 pair = p.split(':')
Karen Qiane24b7ee2019-02-12 23:37:062177 if len(pair) != 2:
2178 self.error_msg('Invalid command.')
2179 # regular parameters
Brian Sheedy0d2300f32024-08-13 23:14:412180 if pair[1].lower() == 'true':
Karen Qiane24b7ee2019-02-12 23:37:062181 params_dict[pair[0]] = True
Brian Sheedy0d2300f32024-08-13 23:14:412182 elif pair[1].lower() == 'false':
Karen Qiane24b7ee2019-02-12 23:37:062183 params_dict[pair[0]] = False
2184 else:
2185 params_dict[pair[0]] = pair[1]
2186 return params_dict
2187
2188 def get_test_suites_dict(self, bots):
2189 """Returns a dictionary of bots and their tests.
2190
2191 Returns a dictionary of bots and a list of their associated tests.
2192 """
2193 test_suite_dict = dict()
2194 for bot in bots:
2195 bot_info = bots[bot]
2196 tests = self.flatten_tests_for_bot(bot_info)
2197 test_suite_dict[bot] = tests
2198 return test_suite_dict
2199
2200 def output_query_result(self, result, json_file=None):
2201 """Outputs the result of the query.
2202
2203 If a json file parameter name is provided, then
2204 the result is output into the json file. If not,
2205 then the result is printed to the console.
2206 """
2207 output = json.dumps(result, indent=2)
2208 if json_file:
2209 self.write_file(json_file, output)
2210 else:
2211 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062212
Joshua Hood56c673c2022-03-02 20:29:332213 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062214 def query(self, args):
2215 """Queries tests or bots.
2216
2217 Depending on the arguments provided, outputs a json of
2218 tests or bots matching the appropriate optional parameters provided.
2219 """
2220 # split up query statement
2221 query = args.query.split('/')
2222 self.load_configuration_files()
2223 self.resolve_configuration_files()
2224
2225 # flatten bots json
2226 tests = self.test_suites
2227 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2228
2229 cmd_class = query[0]
2230
2231 # For queries starting with 'bots'
Brian Sheedy0d2300f32024-08-13 23:14:412232 if cmd_class == 'bots':
Karen Qiane24b7ee2019-02-12 23:37:062233 if len(query) == 1:
2234 return self.output_query_result(bots, args.json)
2235 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332236 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062237 if query[1] == 'tests':
2238 test_suites_dict = self.get_test_suites_dict(bots)
2239 return self.output_query_result(test_suites_dict, args.json)
Brian Sheedy0d2300f32024-08-13 23:14:412240 self.error_msg('This query should be in the format: bots/tests.')
Karen Qiane24b7ee2019-02-12 23:37:062241
2242 else:
Brian Sheedy0d2300f32024-08-13 23:14:412243 self.error_msg('This query should have 0 or 1 "/"", found %s instead.' %
2244 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062245
2246 # For queries starting with 'bot'
Brian Sheedy0d2300f32024-08-13 23:14:412247 elif cmd_class == 'bot':
Karen Qiane24b7ee2019-02-12 23:37:062248 if not len(query) == 2 and not len(query) == 3:
Brian Sheedy0d2300f32024-08-13 23:14:412249 self.error_msg('Command should have 1 or 2 "/"", found %s instead.' %
2250 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062251 bot_id = query[1]
2252 if not bot_id in bots:
Brian Sheedy0d2300f32024-08-13 23:14:412253 self.error_msg('No bot named "' + bot_id + '" found.')
Karen Qiane24b7ee2019-02-12 23:37:062254 bot_info = bots[bot_id]
2255 if len(query) == 2:
2256 return self.output_query_result(bot_info, args.json)
2257 if not query[2] == 'tests':
Brian Sheedy0d2300f32024-08-13 23:14:412258 self.error_msg('The query should be in the format:'
2259 'bot/<bot-name>/tests.')
Karen Qiane24b7ee2019-02-12 23:37:062260
2261 bot_tests = self.flatten_tests_for_bot(bot_info)
2262 return self.output_query_result(bot_tests, args.json)
2263
2264 # For queries starting with 'tests'
Brian Sheedy0d2300f32024-08-13 23:14:412265 elif cmd_class == 'tests':
Karen Qiane24b7ee2019-02-12 23:37:062266 if not len(query) == 1 and not len(query) == 2:
Brian Sheedy0d2300f32024-08-13 23:14:412267 self.error_msg('The query should have 0 or 1 "/", found %s instead.' %
2268 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062269 flattened_tests = self.flatten_tests_for_query(tests)
2270 if len(query) == 1:
2271 return self.output_query_result(flattened_tests, args.json)
2272
2273 # create params dict
2274 params = query[1].split('&')
2275 params_dict = self.parse_query_filter_params(params)
2276 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2277 return self.output_query_result(matching_bots)
2278
2279 # For queries starting with 'test'
Brian Sheedy0d2300f32024-08-13 23:14:412280 elif cmd_class == 'test':
Karen Qiane24b7ee2019-02-12 23:37:062281 if not len(query) == 2 and not len(query) == 3:
Brian Sheedy0d2300f32024-08-13 23:14:412282 self.error_msg('The query should have 1 or 2 "/", found %s instead.' %
2283 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062284 test_id = query[1]
2285 if len(query) == 2:
2286 flattened_tests = self.flatten_tests_for_query(tests)
2287 for test in flattened_tests:
2288 if test == test_id:
2289 return self.output_query_result(flattened_tests[test], args.json)
Brian Sheedy0d2300f32024-08-13 23:14:412290 self.error_msg('There is no test named %s.' % test_id)
Karen Qiane24b7ee2019-02-12 23:37:062291 if not query[2] == 'bots':
Brian Sheedy0d2300f32024-08-13 23:14:412292 self.error_msg('The query should be in the format: '
2293 'test/<test-name>/bots')
Karen Qiane24b7ee2019-02-12 23:37:062294 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2295 return self.output_query_result(bots_for_test)
2296
2297 else:
Brian Sheedy0d2300f32024-08-13 23:14:412298 self.error_msg('Your command did not match any valid commands. '
2299 'Try starting with "bots", "bot", "tests", or "test".')
2300
Joshua Hood56c673c2022-03-02 20:29:332301 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282302
Garrett Beaty1afaccc2020-06-25 19:58:152303 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282304 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502305 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062306 elif self.args.query:
2307 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282308 else:
Greg Gutermanf60eb052020-03-12 17:40:012309 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282310 return 0
2311
Brian Sheedy0d2300f32024-08-13 23:14:412312
2313if __name__ == '__main__': # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152314 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2315 sys.exit(generator.main())