blob: 764faeadead171559da9868ca958e733ffdbe4c3 [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
689 # For skylab, we need to pop the correct `autotest_name`. This field
690 # defines what wrapper we use in OS infra. e.g. for gtest it's
691 # https://source.chromium.org/chromiumos/chromiumos/codesearch/+/main:src/third_party/autotest/files/server/site_tests/chromium/chromium.py
692 if 'autotest_name' not in test:
693 if 'tast_expr' in test:
694 if 'lacros' in test['name']:
695 test['autotest_name'] = 'tast.lacros-from-gcs'
696 else:
697 test['autotest_name'] = 'tast.chrome-from-gcs'
698 elif 'benchmark' in test:
699 test['autotest_name'] = 'chromium_Telemetry'
700 else:
701 test['autotest_name'] = 'chromium'
702
Garrett Beatye3a606ceb2024-04-30 22:13:13703 # Apply any replacements specified for the test for the builder
704 self.replace_test_args(test, test_name, builder_name)
705
706 # Remove args if it is empty
707 if 'args' in test:
708 if not test['args']:
709 del test['args']
710 else:
711 # Replace any magic arguments with their actual value
712 self.substitute_magic_args(test, builder_name, builder)
713
714 test['args'] = self.maybe_fixup_args_array(test['args'])
Ben Pastenee012aea42019-05-14 22:32:28715
Kenneth Russelleb60cbd22017-12-05 07:54:28716 return test
717
Brian Sheedye6ea0ee2019-07-11 02:54:37718 def replace_test_args(self, test, test_name, tester_name):
Garrett Beatyffe83c4f2023-09-08 19:07:37719 replacements = self.get_test_replacements(test, tester_name) or {}
Brian Sheedye6ea0ee2019-07-11 02:54:37720 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23721 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37722 if key not in valid_replacement_keys:
723 raise BBGenErr(
724 'Given replacement key %s for %s on %s is not in the list of valid '
725 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23726 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37727 found_key = False
728 for i, test_key in enumerate(test.get(key, [])):
729 # Handle both the key/value being replaced being defined as two
730 # separate items or as key=value.
731 if test_key == replacement_key:
732 found_key = True
733 # Handle flags without values.
Brian Sheedy822e03742024-08-09 18:48:14734 if replacement_val is None:
Brian Sheedye6ea0ee2019-07-11 02:54:37735 del test[key][i]
736 else:
737 test[key][i+1] = replacement_val
738 break
Joshua Hood56c673c2022-03-02 20:29:33739 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37740 found_key = True
Brian Sheedy822e03742024-08-09 18:48:14741 if replacement_val is None:
Brian Sheedye6ea0ee2019-07-11 02:54:37742 del test[key][i]
743 else:
744 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
745 break
746 if not found_key:
747 raise BBGenErr('Could not find %s in existing list of values for key '
748 '%s in %s on %s' % (replacement_key, key, test_name,
749 tester_name))
750
Shenghua Zhangaba8bad2018-02-07 02:12:09751 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05752 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26753 True):
754 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06755 # are targeting CrOS hardware and so need the special trigger script.
Garrett Beatyade673d2023-08-04 22:00:25756 if 'device_type' in test.get('swarming', {}).get('dimensions', {}):
Ben Pastenea9e583b2019-01-16 02:57:26757 test['trigger_script'] = {
758 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
759 }
Shenghua Zhangaba8bad2018-02-07 02:12:09760
Garrett Beatyffe83c4f2023-09-08 19:07:37761 def add_android_presentation_args(self, tester_config, result):
John Budorick262ae112019-07-12 19:24:38762 bucket = tester_config.get('results_bucket', 'chromium-result-details')
Garrett Beaty94af4272024-04-17 18:06:14763 result.setdefault('args', []).append('--gs-results-bucket=%s' % bucket)
764
765 if ('swarming' in result and 'merge' not in 'result'
766 and not tester_config.get('skip_merge_script', False)):
Ben Pastene858f4be2019-01-09 23:52:09767 result['merge'] = {
Garrett Beatyffe83c4f2023-09-08 19:07:37768 'args': [
769 '--bucket',
770 bucket,
771 '--test-name',
772 result['name'],
773 ],
774 'script': ('//build/android/pylib/results/presentation/'
775 'test_results_presentation.py'),
Ben Pastene858f4be2019-01-09 23:52:09776 }
Ben Pastene858f4be2019-01-09 23:52:09777
Kenneth Russelleb60cbd22017-12-05 07:54:28778 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
779 test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37780 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28781 return None
782 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37783 # Use test_name here instead of test['name'] because test['name'] will be
784 # modified with the variant identifier in a matrix compound suite
785 result.setdefault('test', test_name)
John Budorickab108712018-09-01 00:12:21786
Garrett Beatye3a606ceb2024-04-30 22:13:13787 result = self.apply_common_transformations(waterfall, tester_name,
788 tester_config, result, test_name)
Garrett Beaty94af4272024-04-17 18:06:14789 if self.is_android(tester_config) and 'swarming' in result:
790 if not result.get('use_isolated_scripts_api', False):
Alison Gale71bd8f152024-04-26 22:38:20791 # TODO(crbug.com/40725094) make Android presentation work with
Yuly Novikov26dd47052021-02-11 00:57:14792 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37793 self.add_android_presentation_args(tester_config, result)
Yuly Novikov26dd47052021-02-11 00:57:14794 result['args'] = result.get('args', []) + ['--recover-devices']
Shenghua Zhangaba8bad2018-02-07 02:12:09795 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43796
Garrett Beatybb18d532023-06-26 22:16:33797 if 'swarming' in result and not result.get('merge'):
Jamie Madilla8be0d72020-10-02 05:24:04798 if test_config.get('use_isolated_scripts_api', False):
799 merge_script = 'standard_isolated_script_merge'
800 else:
801 merge_script = 'standard_gtest_merge'
802
Stephen Martinisbc7b7772019-05-01 22:01:43803 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04804 'script': '//testing/merge_scripts/%s.py' % merge_script,
Stephen Martinisbc7b7772019-05-01 22:01:43805 }
Kenneth Russelleb60cbd22017-12-05 07:54:28806 return result
807
808 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
809 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37810 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28811 return None
812 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37813 # Use test_name here instead of test['name'] because test['name'] will be
814 # modified with the variant identifier in a matrix compound suite
Garrett Beatydca3d882023-09-14 23:50:32815 result.setdefault('test', test_name)
Garrett Beatye3a606ceb2024-04-30 22:13:13816 result = self.apply_common_transformations(waterfall, tester_name,
817 tester_config, result, test_name)
Garrett Beaty94af4272024-04-17 18:06:14818 if self.is_android(tester_config) and 'swarming' in result:
Yuly Novikov26dd47052021-02-11 00:57:14819 if tester_config.get('use_android_presentation', False):
Alison Gale71bd8f152024-04-26 22:38:20820 # TODO(crbug.com/40725094) make Android presentation work with
Yuly Novikov26dd47052021-02-11 00:57:14821 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37822 self.add_android_presentation_args(tester_config, result)
Shenghua Zhangaba8bad2018-02-07 02:12:09823 self.add_common_test_properties(result, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17824
Garrett Beatybb18d532023-06-26 22:16:33825 if 'swarming' in result and not result.get('merge'):
Alison Gale923a33e2024-04-22 23:34:28826 # TODO(crbug.com/41456107): Consider adding the ability to not have
Stephen Martinisf50047062019-05-06 22:26:17827 # this default.
828 result['merge'] = {
829 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
Stephen Martinisf50047062019-05-06 22:26:17830 }
Kenneth Russelleb60cbd22017-12-05 07:54:28831 return result
832
Garrett Beaty938560e32024-09-26 18:57:35833 _SCRIPT_FIELDS = ('name', 'script', 'args', 'precommit_args',
834 'non_precommit_args', 'resultdb')
835
Kenneth Russelleb60cbd22017-12-05 07:54:28836 def generate_script_test(self, waterfall, tester_name, tester_config,
837 test_name, test_config):
Alison Gale47d1537d2024-04-19 21:31:46838 # TODO(crbug.com/40623237): Remove this check whenever a better
Brian Sheedy158cd0f2019-04-26 01:12:44839 # long-term solution is implemented.
840 if (waterfall.get('forbid_script_tests', False) or
841 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
842 raise BBGenErr('Attempted to generate a script test on tester ' +
843 tester_name + ', which explicitly forbids script tests')
Garrett Beatyffe83c4f2023-09-08 19:07:37844 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28845 return None
Garrett Beaty938560e32024-09-26 18:57:35846 result = copy.deepcopy(test_config)
Garrett Beatye3a606ceb2024-04-30 22:13:13847 result = self.apply_common_transformations(waterfall,
848 tester_name,
849 tester_config,
850 result,
851 test_name,
852 swarmable=False,
853 supports_args=False)
Garrett Beaty938560e32024-09-26 18:57:35854 result = {k: result[k] for k in self._SCRIPT_FIELDS if k in result}
Kenneth Russelleb60cbd22017-12-05 07:54:28855 return result
856
Xinan Lin05fb9c1752020-12-17 00:15:52857 def generate_skylab_test(self, waterfall, tester_name, tester_config,
858 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37859 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Xinan Lin05fb9c1752020-12-17 00:15:52860 return None
861 result = copy.deepcopy(test_config)
Brian Sheedy67937ad12024-03-06 22:53:55862 result.setdefault('test', test_name)
yoshiki iguchid1664ef2024-03-28 19:16:52863
Garrett Beaty050fc4c2025-01-09 19:44:50864 skylab = result.setdefault('skylab', {})
865 if result.get('experiment_percentage') != 100:
866 skylab.setdefault('shard_level_retries_on_ctp', 1)
yoshiki iguchid1664ef2024-03-28 19:16:52867
Garrett Beaty050fc4c2025-01-09 19:44:50868 for src, dst in (
869 ('cros_board', 'cros_board'),
870 ('cros_model', 'cros_model'),
871 ('cros_dut_pool', 'dut_pool'),
872 ('cros_build_target', 'cros_build_target'),
873 ('shard_level_retries_on_ctp', 'shard_level_retries_on_ctp'),
874 ):
875 if src in tester_config:
876 skylab[dst] = tester_config[src]
yoshiki iguchia5f87c7d2024-06-19 02:48:34877
Garrett Beatye3a606ceb2024-04-30 22:13:13878 result = self.apply_common_transformations(waterfall,
879 tester_name,
880 tester_config,
881 result,
882 test_name,
883 swarmable=False)
Garrett Beaty050fc4c2025-01-09 19:44:50884
885 if 'cros_board' not in result:
886 raise BBGenErr('skylab tests must specify cros_board.')
887
Xinan Lin05fb9c1752020-12-17 00:15:52888 return result
889
Garrett Beaty65d44222023-08-01 17:22:11890 def substitute_gpu_args(self, tester_config, test, args):
Kenneth Russell8a386d42018-06-02 09:48:01891 substitutions = {
892 # Any machine in waterfalls.pyl which desires to run GPU tests
893 # must provide the os_type key.
894 'os_type': tester_config['os_type'],
895 'gpu_vendor_id': '0',
896 'gpu_device_id': '0',
897 }
Garrett Beatyade673d2023-08-04 22:00:25898 dimensions = test.get('swarming', {}).get('dimensions', {})
899 if 'gpu' in dimensions:
900 # First remove the driver version, then split into vendor and device.
901 gpu = dimensions['gpu']
902 if gpu != 'none':
903 gpu = gpu.split('-')[0].split(':')
904 substitutions['gpu_vendor_id'] = gpu[0]
905 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01906 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
907
Garrett Beaty7436fb72024-08-07 20:20:58908 # LINT.IfChange(gpu_telemetry_test)
909
Kenneth Russell8a386d42018-06-02 09:48:01910 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30911 test_name, test_config, is_android_webview,
Xinan Linedcf05b32023-10-19 23:13:50912 is_cast_streaming, is_skylab):
Kenneth Russell8a386d42018-06-02 09:48:01913 # These are all just specializations of isolated script tests with
914 # a bunch of boilerplate command line arguments added.
915
916 # The step name must end in 'test' or 'tests' in order for the
917 # results to automatically show up on the flakiness dashboard.
918 # (At least, this was true some time ago.) Continue to use this
919 # naming convention for the time being to minimize changes.
Garrett Beaty235c1412023-08-29 20:26:29920 #
921 # test name is the name of the test without the variant ID added
922 if not (test_name.endswith('test') or test_name.endswith('tests')):
923 raise BBGenErr(
924 f'telemetry test names must end with test or tests, got {test_name}')
Garrett Beatyffe83c4f2023-09-08 19:07:37925 result = self.generate_isolated_script_test(waterfall, tester_name,
926 tester_config, test_name,
927 test_config)
Kenneth Russell8a386d42018-06-02 09:48:01928 if not result:
929 return None
Garrett Beatydca3d882023-09-14 23:50:32930 result['test'] = test_config.get('test') or self.get_default_isolate_name(
931 tester_config, is_android_webview)
Chan Liab7d8dd82020-04-24 23:42:19932
Chan Lia3ad1502020-04-28 05:32:11933 # Populate test_id_prefix.
Garrett Beatydca3d882023-09-14 23:50:32934 gn_entry = self.gn_isolate_map[result['test']]
Chan Li17d969f92020-07-10 00:50:03935 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19936
Kenneth Russell8a386d42018-06-02 09:48:01937 args = result.get('args', [])
Garrett Beatyffe83c4f2023-09-08 19:07:37938 # Use test_name here instead of test['name'] because test['name'] will be
939 # modified with the variant identifier in a matrix compound suite
Kenneth Russell8a386d42018-06-02 09:48:01940 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14941
942 # These tests upload and download results from cloud storage and therefore
943 # aren't idempotent yet. https://crbug.com/549140.
Garrett Beatybfeff8f2023-06-16 18:57:25944 if 'swarming' in result:
945 result['swarming']['idempotent'] = False
erikchen6da2d9b2018-08-03 23:01:14946
Fabrice de Ganscbd655f2022-08-04 20:15:30947 browser = ''
948 if is_cast_streaming:
949 browser = 'cast-streaming-shell'
950 elif is_android_webview:
951 browser = 'android-webview-instrumentation'
952 else:
953 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:52954
Greg Thompsoncec7d8d2023-01-10 19:11:53955 extra_browser_args = []
956
Brian Sheedy4053a702020-07-28 02:09:52957 # Most platforms require --enable-logging=stderr to get useful browser logs.
958 # However, this actively messes with logging on CrOS (because Chrome's
959 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
960 # in order to see JavaScript console messages. See
961 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
Greg Thompsoncec7d8d2023-01-10 19:11:53962 if self.is_chromeos(tester_config):
963 extra_browser_args.append('--log-level=0')
964 elif not self.is_fuchsia(tester_config) or browser != 'fuchsia-chrome':
965 # Stderr logging is not needed for Chrome browser on Fuchsia, as ordinary
966 # logging via syslog is captured.
967 extra_browser_args.append('--enable-logging=stderr')
968
969 # --expose-gc allows the WebGL conformance tests to more reliably
970 # reproduce GC-related bugs in the V8 bindings.
971 extra_browser_args.append('--js-flags=--expose-gc')
Brian Sheedy4053a702020-07-28 02:09:52972
Xinan Linedcf05b32023-10-19 23:13:50973 # Skylab supports sharding, so reuse swarming's shard config.
974 if is_skylab and 'shards' not in result and test_config.get(
975 'swarming', {}).get('shards'):
976 result['shards'] = test_config['swarming']['shards']
977
Kenneth Russell8a386d42018-06-02 09:48:01978 args = [
Bo Liu555a0f92019-03-29 12:11:56979 test_to_run,
980 '--show-stdout',
981 '--browser=%s' % browser,
982 # --passthrough displays more of the logging in Telemetry when
983 # run via typ, in particular some of the warnings about tests
984 # being expected to fail, but passing.
985 '--passthrough',
986 '-v',
Brian Sheedy814e0482022-10-03 23:24:12987 '--stable-jobs',
Greg Thompsoncec7d8d2023-01-10 19:11:53988 '--extra-browser-args=%s' % ' '.join(extra_browser_args),
Brian Sheedy997e4802023-10-18 02:28:13989 '--enforce-browser-version',
Kenneth Russell8a386d42018-06-02 09:48:01990 ] + args
Garrett Beatybfeff8f2023-06-16 18:57:25991 result['args'] = self.maybe_fixup_args_array(
Garrett Beaty65d44222023-08-01 17:22:11992 self.substitute_gpu_args(tester_config, result, args))
Kenneth Russell8a386d42018-06-02 09:48:01993 return result
994
Garrett Beaty7436fb72024-08-07 20:20:58995 # pylint: disable=line-too-long
996 # LINT.ThenChange(//infra/config/lib/targets-internal/test-types/gpu_telemetry_test.star)
997 # pylint: enable=line-too-long
998
Brian Sheedyf74819b2021-06-04 01:38:38999 def get_default_isolate_name(self, tester_config, is_android_webview):
1000 if self.is_android(tester_config):
1001 if is_android_webview:
1002 return 'telemetry_gpu_integration_test_android_webview'
1003 return (
1004 'telemetry_gpu_integration_test' +
1005 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:331006 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:171007 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:331008 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:381009
Kenneth Russelleb60cbd22017-12-05 07:54:281010 def get_test_generator_map(self):
1011 return {
Bo Liu555a0f92019-03-29 12:11:561012 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301013 GPUTelemetryTestGenerator(self, is_android_webview=True),
1014 'cast_streaming_tests':
1015 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:561016 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301017 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561018 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301019 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561020 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301021 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561022 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301023 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:521024 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301025 SkylabGenerator(self),
Brian Sheedyb6491ba2022-09-26 20:49:491026 'skylab_gpu_telemetry_tests':
1027 SkylabGPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281028 }
1029
Kenneth Russell8a386d42018-06-02 09:48:011030 def get_test_type_remapper(self):
1031 return {
Fabrice de Gans223272482022-08-08 16:56:571032 # These are a specialization of isolated_scripts with a bunch of
1033 # boilerplate command line arguments added to each one.
1034 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
1035 'cast_streaming_tests': 'isolated_scripts',
1036 'gpu_telemetry_tests': 'isolated_scripts',
Brian Sheedyb6491ba2022-09-26 20:49:491037 # These are the same as existing test types, just configured to run
1038 # in Skylab instead of via normal swarming.
1039 'skylab_gpu_telemetry_tests': 'skylab_tests',
Kenneth Russell8a386d42018-06-02 09:48:011040 }
1041
Jeff Yoon67c3e832020-02-08 07:39:381042 def check_composition_type_test_suites(self, test_type,
1043 additional_validators=None):
1044 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1045 validators = [check_compound_references,
1046 check_basic_references,
1047 check_conflicting_definitions]
1048 if additional_validators:
1049 validators += additional_validators
1050
1051 target_suites = self.test_suites.get(test_type, {})
1052 other_test_type = ('compound_suites'
1053 if test_type == 'matrix_compound_suites'
1054 else 'matrix_compound_suites')
1055 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011056 basic_suites = self.test_suites.get('basic_suites', {})
1057
Jamie Madillcf4f8c72021-05-20 19:24:231058 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011059 if suite in basic_suites:
1060 raise BBGenErr('%s names may not duplicate basic test suite names '
1061 '(error found while processsing %s)'
1062 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011063
Jeff Yoon67c3e832020-02-08 07:39:381064 seen_tests = {}
1065 for sub_suite in suite_def:
1066 for validator in validators:
1067 validator(
1068 basic_suites=basic_suites,
1069 other_test_suites=other_suites,
1070 seen_tests=seen_tests,
1071 sub_suite=sub_suite,
1072 suite=suite,
1073 suite_def=suite_def,
1074 target_test_suites=target_suites,
1075 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051076 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381077 )
Kenneth Russelleb60cbd22017-12-05 07:54:281078
Stephen Martinis54d64ad2018-09-21 22:16:201079 def flatten_test_suites(self):
1080 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011081 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1082 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231083 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011084 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201085 self.test_suites = new_test_suites
1086
Chan Lia3ad1502020-04-28 05:32:111087 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231088 for suite in self.test_suites['basic_suites'].values():
1089 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561090 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411091
Garrett Beatydca3d882023-09-14 23:50:321092 isolate_name = test.get('test') or key
Nodir Turakulovfce34292019-12-18 17:05:411093 gn_entry = self.gn_isolate_map.get(isolate_name)
1094 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281095 label = gn_entry['label']
1096
1097 if label.count(':') != 1:
1098 raise BBGenErr(
1099 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1100 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1101 (label, isolate_name))
1102 if label.split(':')[1] != isolate_name:
1103 raise BBGenErr(
1104 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1105 ' label "%s" see http://crbug.com/1071091 for details.' %
1106 (isolate_name, label))
1107
Chan Lia3ad1502020-04-28 05:32:111108 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411109 else: # pragma: no cover
1110 # Some tests do not have an entry gn_isolate_map.pyl, such as
1111 # telemetry tests.
Alison Gale47d1537d2024-04-19 21:31:461112 # TODO(crbug.com/40112160): require an entry in gn_isolate_map.
Nodir Turakulovfce34292019-12-18 17:05:411113 pass
1114
Kenneth Russelleb60cbd22017-12-05 07:54:281115 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011116 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201117
Jeff Yoon8154e582019-12-03 23:30:011118 compound_suites = self.test_suites.get('compound_suites', {})
1119 # check_composition_type_test_suites() checks that all basic suites
1120 # referenced by compound suites exist.
1121 basic_suites = self.test_suites.get('basic_suites')
1122
Jamie Madillcf4f8c72021-05-20 19:24:231123 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011124 # Resolve this to a dictionary.
1125 full_suite = {}
1126 for entry in value:
1127 suite = basic_suites[entry]
1128 full_suite.update(suite)
1129 compound_suites[name] = full_suite
1130
Jeff Yoon85fb8df2020-08-20 16:47:431131 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381132 """ Merge variant-defined configurations to each test case definition in a
1133 test suite.
1134
1135 The output maps a unique test name to an array of configurations because
1136 there may exist more than one definition for a test name using variants. The
1137 test name is referenced while mapping machines to test suites, so unpacking
1138 the array is done by the generators.
1139
1140 Args:
1141 basic_test_definition: a {} defined test suite in the format
1142 test_name:test_config
1143 variants: an [] of {} defining configurations to be applied to each test
1144 case in the basic test_definition
1145
1146 Return:
1147 a {} of test_name:[{}], where each {} is a merged configuration
1148 """
1149
1150 # Each test in a basic test suite will have a definition per variant.
1151 test_suite = {}
Garrett Beaty8d6708c2023-07-20 17:20:411152 for variant in variants:
1153 # Unpack the variant from variants.pyl if it's string based.
1154 if isinstance(variant, str):
1155 variant = self.variants[variant]
Jeff Yoonda581c32020-03-06 03:56:051156
Garrett Beaty8d6708c2023-07-20 17:20:411157 # If 'enabled' is set to False, we will not use this variant; otherwise if
1158 # the variant doesn't include 'enabled' variable or 'enabled' is set to
1159 # True, we will use this variant
1160 if not variant.get('enabled', True):
1161 continue
Jeff Yoon67c3e832020-02-08 07:39:381162
Garrett Beaty8d6708c2023-07-20 17:20:411163 # Make a shallow copy of the variant to remove variant-specific fields,
1164 # leaving just mixin fields
1165 variant = copy.copy(variant)
1166 variant.pop('enabled', None)
1167 identifier = variant.pop('identifier')
1168 variant_mixins = variant.pop('mixins', [])
Jeff Yoon67c3e832020-02-08 07:39:381169
Garrett Beaty8d6708c2023-07-20 17:20:411170 for test_name, test_config in basic_test_definition.items():
Garrett Beaty65b7d362024-10-01 16:21:421171 new_test = copy.copy(test_config)
Xinan Lin05fb9c1752020-12-17 00:15:521172
Jeff Yoon67c3e832020-02-08 07:39:381173 # The identifier is used to make the name of the test unique.
1174 # Generators in the recipe uniquely identify a test by it's name, so we
1175 # don't want to have the same name for each variant.
Garrett Beaty235c1412023-08-29 20:26:291176 new_test['name'] = f'{test_name} {identifier}'
Ben Pastene5f231cf22022-05-05 18:03:071177
1178 # Attach the variant identifier to the test config so downstream
1179 # generators can make modifications based on the original name. This
1180 # is mainly used in generate_gpu_telemetry_test().
Garrett Beaty8d6708c2023-07-20 17:20:411181 new_test['variant_id'] = identifier
Ben Pastene5f231cf22022-05-05 18:03:071182
Garrett Beaty65b7d362024-10-01 16:21:421183 # Save the variant details and mixins to be applied in
1184 # apply_common_transformations to match the order that starlark will
1185 # apply things
1186 new_test['*variant*'] = variant
1187 new_test['*variant_mixins*'] = variant_mixins + mixins
1188
Garrett Beaty8d6708c2023-07-20 17:20:411189 test_suite.setdefault(test_name, []).append(new_test)
1190
Jeff Yoon67c3e832020-02-08 07:39:381191 return test_suite
1192
Jeff Yoon8154e582019-12-03 23:30:011193 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381194 self.check_composition_type_test_suites('matrix_compound_suites',
1195 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011196
1197 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381198 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011199 # referenced by matrix suites exist.
1200 basic_suites = self.test_suites.get('basic_suites')
1201
Brian Sheedy822e03742024-08-09 18:48:141202 def update_tests_uncurried(full_suite, expanded):
1203 for test_name, new_tests in expanded.items():
1204 if not isinstance(new_tests, list):
1205 new_tests = [new_tests]
1206 tests_for_name = full_suite.setdefault(test_name, [])
1207 for t in new_tests:
1208 if t not in tests_for_name:
1209 tests_for_name.append(t)
1210
Garrett Beaty235c1412023-08-29 20:26:291211 for matrix_suite_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011212 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381213
Jamie Madillcf4f8c72021-05-20 19:24:231214 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381215 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1216
Brian Sheedy822e03742024-08-09 18:48:141217 update_tests = functools.partial(update_tests_uncurried, full_suite)
Garrett Beaty235c1412023-08-29 20:26:291218
Garrett Beaty73c4cd42024-10-04 17:55:081219 mixins = mtx_test_suite_config.get('mixins', [])
Garrett Beaty60a7b2a2023-09-13 23:00:401220 if (variants := mtx_test_suite_config.get('variants')):
Garrett Beaty60a7b2a2023-09-13 23:00:401221 result = self.resolve_variants(basic_test_def, variants, mixins)
Garrett Beaty235c1412023-08-29 20:26:291222 update_tests(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271223 else:
Garrett Beaty73c4cd42024-10-04 17:55:081224 suite = copy.deepcopy(basic_suites[test_suite])
1225 for test_config in suite.values():
1226 test_config['mixins'] = test_config.get('mixins', []) + mixins
Garrett Beaty235c1412023-08-29 20:26:291227 update_tests(suite)
1228 matrix_compound_suites[matrix_suite_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281229
1230 def link_waterfalls_to_test_suites(self):
1231 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231232 for tester_name, tester in waterfall['machines'].items():
1233 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281234 if not value in self.test_suites:
1235 # Hard / impossible to cover this in the unit test.
1236 raise self.unknown_test_suite(
1237 value, tester_name, waterfall['name']) # pragma: no cover
1238 tester['test_suites'][suite] = self.test_suites[value]
1239
1240 def load_configuration_files(self):
Garrett Beaty79339e182023-04-10 20:45:471241 self.waterfalls = self.load_pyl_file(self.args.waterfalls_pyl_path)
1242 self.test_suites = self.load_pyl_file(self.args.test_suites_pyl_path)
1243 self.exceptions = self.load_pyl_file(
1244 self.args.test_suite_exceptions_pyl_path)
1245 self.mixins = self.load_pyl_file(self.args.mixins_pyl_path)
1246 self.gn_isolate_map = self.load_pyl_file(self.args.gn_isolate_map_pyl_path)
Chong Guee622242020-10-28 18:17:351247 for isolate_map in self.args.isolate_map_files:
1248 isolate_map = self.load_pyl_file(isolate_map)
1249 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1250 if duplicates:
1251 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1252 ', '.join(duplicates))
1253 self.gn_isolate_map.update(isolate_map)
1254
Garrett Beaty79339e182023-04-10 20:45:471255 self.variants = self.load_pyl_file(self.args.variants_pyl_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281256
1257 def resolve_configuration_files(self):
Garrett Beaty086b3402024-09-25 23:45:341258 self.resolve_mixins()
Garrett Beaty235c1412023-08-29 20:26:291259 self.resolve_test_names()
Garrett Beatydca3d882023-09-14 23:50:321260 self.resolve_isolate_names()
Garrett Beaty65d44222023-08-01 17:22:111261 self.resolve_dimension_sets()
Chan Lia3ad1502020-04-28 05:32:111262 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281263 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011264 self.resolve_matrix_compound_test_suites()
1265 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281266 self.link_waterfalls_to_test_suites()
1267
Garrett Beaty086b3402024-09-25 23:45:341268 def resolve_mixins(self):
1269 for mixin in self.mixins.values():
1270 mixin.pop('fail_if_unused', None)
1271
Garrett Beaty235c1412023-08-29 20:26:291272 def resolve_test_names(self):
1273 for suite_name, suite in self.test_suites.get('basic_suites').items():
1274 for test_name, test in suite.items():
1275 if 'name' in test:
1276 raise BBGenErr(
1277 f'The name field is set in test {test_name} in basic suite '
1278 f'{suite_name}, this is not supported, the test name is the key '
1279 'within the basic suite')
Garrett Beatyffe83c4f2023-09-08 19:07:371280 # When a test is expanded with variants, this will be overwritten, but
1281 # this ensures every test definition has the name field set
1282 test['name'] = test_name
Garrett Beaty235c1412023-08-29 20:26:291283
Garrett Beatydca3d882023-09-14 23:50:321284 def resolve_isolate_names(self):
1285 for suite_name, suite in self.test_suites.get('basic_suites').items():
1286 for test_name, test in suite.items():
1287 if 'isolate_name' in test:
1288 raise BBGenErr(
1289 f'The isolate_name field is set in test {test_name} in basic '
1290 f'suite {suite_name}, the test field should be used instead')
1291
Garrett Beaty65d44222023-08-01 17:22:111292 def resolve_dimension_sets(self):
Garrett Beaty65d44222023-08-01 17:22:111293
1294 def definitions():
1295 for suite_name, suite in self.test_suites.get('basic_suites', {}).items():
1296 for test_name, test in suite.items():
1297 yield test, f'test {test_name} in basic suite {suite_name}'
1298
1299 for mixin_name, mixin in self.mixins.items():
1300 yield mixin, f'mixin {mixin_name}'
1301
1302 for waterfall in self.waterfalls:
1303 for builder_name, builder in waterfall.get('machines', {}).items():
1304 yield (
1305 builder,
1306 f'builder {builder_name} in waterfall {waterfall["name"]}',
1307 )
1308
1309 for test_name, exceptions in self.exceptions.items():
1310 modifications = exceptions.get('modifications', {})
1311 for builder_name, mods in modifications.items():
1312 yield (
1313 mods,
1314 f'exception for test {test_name} on builder {builder_name}',
1315 )
1316
1317 for definition, location in definitions():
1318 for swarming_attr in (
1319 'swarming',
1320 'android_swarming',
1321 'chromeos_swarming',
1322 ):
1323 if (swarming :=
1324 definition.get(swarming_attr)) and 'dimension_sets' in swarming:
Garrett Beatyade673d2023-08-04 22:00:251325 raise BBGenErr(
1326 f'dimension_sets is no longer supported (set in {location}),'
1327 ' instead, use set dimensions to a single dict')
Garrett Beaty65d44222023-08-01 17:22:111328
Nico Weberd18b8962018-05-16 19:39:381329 def unknown_bot(self, bot_name, waterfall_name):
1330 return BBGenErr(
1331 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1332
Kenneth Russelleb60cbd22017-12-05 07:54:281333 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1334 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381335 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281336 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1337
1338 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1339 return BBGenErr(
1340 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1341 ' on waterfall ' + waterfall_name)
1342
Garrett Beatye3a606ceb2024-04-30 22:13:131343 def ensure_valid_mixin_list(self, mixins, location):
1344 if not isinstance(mixins, list):
1345 raise BBGenErr(
1346 f"got '{mixins}', should be a list of mixin names: {location}")
1347 for mixin in mixins:
1348 if not mixin in self.mixins:
1349 raise BBGenErr(f'bad mixin {mixin}: {location}')
Stephen Martinisb6a50492018-09-12 23:59:321350
Garrett Beatye3a606ceb2024-04-30 22:13:131351 def apply_mixins(self, test, mixins, mixins_to_ignore, builder=None):
1352 for mixin in mixins:
1353 if mixin not in mixins_to_ignore:
Austin Eng148d9f0f2022-02-08 19:18:531354 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinis0382bc12018-09-17 22:29:071355 return test
Stephen Martinisb6a50492018-09-12 23:59:321356
Garrett Beaty8d6708c2023-07-20 17:20:411357 def apply_mixin(self, mixin, test, builder=None):
Stephen Martinisb72f6d22018-10-04 23:29:011358 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321359
Garrett Beaty4c35b142023-06-23 21:01:231360 A mixin is applied by copying all fields from the mixin into the
1361 test with the following exceptions:
1362 * For the various *args keys, the test's existing value (an empty
1363 list if not present) will be extended with the mixin's value.
1364 * The sub-keys of the swarming value will be copied to the test's
1365 swarming value with the following exceptions:
Garrett Beatyade673d2023-08-04 22:00:251366 * For the named_caches sub-keys, the test's existing value (an
1367 empty list if not present) will be extended with the mixin's
1368 value.
1369 * For the dimensions sub-key, the tests's existing value (an empty
1370 dict if not present) will be updated with the mixin's value.
Stephen Martinisb6a50492018-09-12 23:59:321371 """
Garrett Beaty4c35b142023-06-23 21:01:231372
Stephen Martinisb6a50492018-09-12 23:59:321373 new_test = copy.deepcopy(test)
1374 mixin = copy.deepcopy(mixin)
Garrett Beaty8d6708c2023-07-20 17:20:411375
1376 if 'description' in mixin:
1377 description = []
1378 if 'description' in new_test:
1379 description.append(new_test['description'])
1380 description.append(mixin.pop('description'))
1381 new_test['description'] = '\n'.join(description)
1382
Stephen Martinisb72f6d22018-10-04 23:29:011383 if 'swarming' in mixin:
Garrett Beaty1b271622024-10-01 22:30:251384 self.merge_swarming(new_test.setdefault('swarming', {}),
1385 mixin.pop('swarming'))
Stephen Martinisb72f6d22018-10-04 23:29:011386
Garrett Beaty050fc4c2025-01-09 19:44:501387 if 'skylab' in mixin:
1388 new_test.setdefault('skylab', {}).update(mixin.pop('skylab'))
1389
Garrett Beatye3a606ceb2024-04-30 22:13:131390 for a in ('args', 'precommit_args', 'non_precommit_args'):
Garrett Beaty4c35b142023-06-23 21:01:231391 if (value := mixin.pop(a, None)) is None:
1392 continue
1393 if not isinstance(value, list):
1394 raise BBGenErr(f'"{a}" must be a list')
1395 new_test.setdefault(a, []).extend(value)
1396
Garrett Beatye3a606ceb2024-04-30 22:13:131397 # At this point, all keys that require merging are taken care of, so the
1398 # remaining entries can be copied over. The os-conditional entries will be
1399 # resolved immediately after and they are resolved before any mixins are
1400 # applied, so there's are no concerns about overwriting the corresponding
1401 # entry in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011402 new_test.update(mixin)
Garrett Beatye3a606ceb2024-04-30 22:13:131403 if builder:
1404 self.resolve_os_conditional_values(new_test, builder)
1405
1406 if 'args' in new_test:
1407 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1408
Stephen Martinisb6a50492018-09-12 23:59:321409 return new_test
1410
Greg Gutermanf60eb052020-03-12 17:40:011411 def generate_output_tests(self, waterfall):
1412 """Generates the tests for a waterfall.
1413
1414 Args:
1415 waterfall: a dictionary parsed from a master pyl file
1416 Returns:
1417 A dictionary mapping builders to test specs
1418 """
1419 return {
Jamie Madillcf4f8c72021-05-20 19:24:231420 name: self.get_tests_for_config(waterfall, name, config)
1421 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011422 }
1423
1424 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531425 generator_map = self.get_test_generator_map()
1426 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281427
Greg Gutermanf60eb052020-03-12 17:40:011428 tests = {}
1429 # Copy only well-understood entries in the machine's configuration
1430 # verbatim into the generated JSON.
1431 if 'additional_compile_targets' in config:
1432 tests['additional_compile_targets'] = config[
1433 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231434 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011435 if test_type not in generator_map:
1436 raise self.unknown_test_suite_type(
1437 test_type, name, waterfall['name']) # pragma: no cover
1438 test_generator = generator_map[test_type]
1439 # Let multiple kinds of generators generate the same kinds
1440 # of tests. For example, gpu_telemetry_tests are a
1441 # specialization of isolated_scripts.
1442 new_tests = test_generator.generate(
1443 waterfall, name, config, input_tests)
1444 remapped_test_type = test_type_remapper.get(test_type, test_type)
Garrett Beatyffe83c4f2023-09-08 19:07:371445 tests.setdefault(remapped_test_type, []).extend(new_tests)
1446
1447 for test_type, tests_for_type in tests.items():
1448 if test_type == 'additional_compile_targets':
1449 continue
1450 tests[test_type] = sorted(tests_for_type, key=lambda t: t['name'])
Greg Gutermanf60eb052020-03-12 17:40:011451
1452 return tests
1453
1454 def jsonify(self, all_tests):
1455 return json.dumps(
1456 all_tests, indent=2, separators=(',', ': '),
1457 sort_keys=True) + '\n'
1458
1459 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281460 self.load_configuration_files()
1461 self.resolve_configuration_files()
1462 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011463 result = collections.defaultdict(dict)
1464
Stephanie Kim572b43c02023-04-13 14:24:131465 if os.path.exists(self.args.autoshard_exceptions_json_path):
1466 autoshards = json.loads(
1467 self.read_file(self.args.autoshard_exceptions_json_path))
1468 else:
1469 autoshards = {}
1470
Dirk Pranke6269d302020-10-01 00:14:391471 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011472 for waterfall in self.waterfalls:
1473 for field in required_fields:
1474 # Verify required fields
1475 if field not in waterfall:
Brian Sheedy0d2300f32024-08-13 23:14:411476 raise BBGenErr('Waterfall %s has no %s' % (waterfall['name'], field))
Greg Gutermanf60eb052020-03-12 17:40:011477
1478 # Handle filter flag, if specified
1479 if filters and waterfall['name'] not in filters:
1480 continue
1481
1482 # Join config files and hardcoded values together
1483 all_tests = self.generate_output_tests(waterfall)
1484 result[waterfall['name']] = all_tests
1485
Stephanie Kim572b43c02023-04-13 14:24:131486 if not autoshards:
1487 continue
1488 for builder, test_spec in all_tests.items():
1489 for target_type, test_list in test_spec.items():
1490 if target_type == 'additional_compile_targets':
1491 continue
1492 for test_dict in test_list:
1493 # Suites that apply variants or other customizations will create
1494 # test_dicts that have "name" value that is different from the
Garrett Beatyffe83c4f2023-09-08 19:07:371495 # "test" value.
Stephanie Kim572b43c02023-04-13 14:24:131496 # e.g. name = vulkan_swiftshader_content_browsertests, but
1497 # test = content_browsertests and
1498 # test_id_prefix = "ninja://content/test:content_browsertests/"
Garrett Beatyffe83c4f2023-09-08 19:07:371499 test_name = test_dict['name']
Stephanie Kim572b43c02023-04-13 14:24:131500 shard_info = autoshards.get(waterfall['name'],
1501 {}).get(builder, {}).get(test_name)
1502 if shard_info:
1503 test_dict['swarming'].update(
1504 {'shards': int(shard_info['shards'])})
1505
Greg Gutermanf60eb052020-03-12 17:40:011506 # Add do not edit warning
1507 for tests in result.values():
1508 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1509 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1510
1511 return result
1512
1513 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281514 suffix = '.json'
1515 if self.args.new_files:
1516 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011517
1518 for filename, contents in result.items():
1519 jsonstr = self.jsonify(contents)
Garrett Beaty79339e182023-04-10 20:45:471520 file_path = os.path.join(self.args.output_dir, filename + suffix)
1521 self.write_file(file_path, jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281522
Nico Weberd18b8962018-05-16 19:39:381523 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161524 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421525 # NOTE: This reference can cause issues; if a file changes there, the
1526 # presubmit here won't be run by default. A manually maintained list there
1527 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1528 # references to configs outside of this directory are added, please change
1529 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1530 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491531
Garrett Beaty7e866fc2021-06-16 14:12:101532 # Get the generated project.pyl so we can check if we should be enforcing
1533 # that the specs are for builders that actually exist
1534 # If not, return None to indicate that we won't enforce that builders in
1535 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491536 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1537 'project.pyl')
1538 if os.path.exists(project_pyl_path):
1539 settings = ast.literal_eval(self.read_file(project_pyl_path))
1540 if not settings.get('validate_source_side_specs_have_builder', True):
1541 return None
1542
Nico Weberd18b8962018-05-16 19:39:381543 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311544 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161545 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1546 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431547 for c in milo_configs:
1548 for l in self.read_file(c).splitlines():
1549 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311550 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431551 continue
1552 # l looks like
1553 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1554 # Extract win_chromium_dbg_ng part.
1555 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381556 return bot_names
1557
Ben Pastene9a010082019-09-25 20:41:371558 def get_internal_waterfalls(self):
1559 # Similar to get_builders_that_do_not_actually_exist above, but for
1560 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201561 return [
Kramer Ge3bf853a2023-04-13 19:39:471562 'chrome', 'chrome.pgo', 'chrome.gpu.fyi', 'internal.chrome.fyi',
Ming-Ying Chungc71698b2025-03-07 19:11:511563 'internal.chromeos.fyi', 'internal.optimization_guide',
1564 'internal.translatekit', 'internal.soda', 'chromeos.preuprev'
Yuke Liaoe6c23dd2021-07-28 16:12:201565 ]
Ben Pastene9a010082019-09-25 20:41:371566
Stephen Martinisf83893722018-09-19 00:02:181567 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201568 self.check_input_files_sorting(verbose)
1569
Kenneth Russelleb60cbd22017-12-05 07:54:281570 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011571 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381572 self.check_composition_type_test_suites('matrix_compound_suites',
1573 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111574 self.resolve_test_id_prefixes()
Garrett Beaty1ead4a52023-12-07 19:16:421575
1576 # All test suites must be referenced. Check this before flattening the test
1577 # suites so that we can transitively check the basic suites for compound
1578 # suites and matrix compound suites (otherwise we would determine a basic
1579 # suite is used if it shared a name with a test present in a basic suite
1580 # that is used).
1581 all_suites = set(
1582 itertools.chain(*(self.test_suites.get(a, {}) for a in (
1583 'basic_suites',
1584 'compound_suites',
1585 'matrix_compound_suites',
1586 ))))
1587 unused_suites = set(all_suites)
1588 generator_map = self.get_test_generator_map()
1589 for waterfall in self.waterfalls:
1590 for bot_name, tester in waterfall['machines'].items():
1591 for suite_type, suite in tester.get('test_suites', {}).items():
1592 if suite_type not in generator_map:
1593 raise self.unknown_test_suite_type(suite_type, bot_name,
1594 waterfall['name'])
1595 if suite not in all_suites:
1596 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1597 unused_suites.discard(suite)
1598 # For each compound suite or matrix compound suite, if the suite was used,
1599 # remove all of the basic suites that it composes from the set of unused
1600 # suites
1601 for a in ('compound_suites', 'matrix_compound_suites'):
1602 for suite, sub_suites in self.test_suites.get(a, {}).items():
1603 if suite not in unused_suites:
1604 unused_suites.difference_update(sub_suites)
1605 if unused_suites:
1606 raise BBGenErr('The following test suites were unreferenced by bots on '
1607 'the waterfalls: ' + str(unused_suites))
1608
Stephen Martinis54d64ad2018-09-21 22:16:201609 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381610
1611 # All bots should exist.
1612 bot_names = self.get_valid_bot_names()
Garrett Beaty2a02de3c2020-05-15 13:57:351613 if bot_names is not None:
1614 internal_waterfalls = self.get_internal_waterfalls()
1615 for waterfall in self.waterfalls:
Alison Gale923a33e2024-04-22 23:34:281616 # TODO(crbug.com/41474799): Remove the need for this exception.
Garrett Beaty2a02de3c2020-05-15 13:57:351617 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011618 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351619 for bot_name in waterfall['machines']:
Garrett Beaty2a02de3c2020-05-15 13:57:351620 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581621 if waterfall['name'] in [
1622 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1623 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351624 # TODO(thakis): Remove this once these bots move to luci.
1625 continue # pragma: no cover
1626 if waterfall['name'] in ['tryserver.webrtc',
1627 'webrtc.chromium.fyi.experimental']:
1628 # These waterfalls have their bot configs in a different repo.
1629 # so we don't know about their bot names.
1630 continue # pragma: no cover
1631 if waterfall['name'] in ['client.devtools-frontend.integration',
1632 'tryserver.devtools-frontend',
1633 'chromium.devtools-frontend']:
1634 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201635 if waterfall['name'] in ['client.openscreen.chromium']:
1636 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351637 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381638
Kenneth Russelleb60cbd22017-12-05 07:54:281639 # All test suite exceptions must refer to bots on the waterfall.
1640 all_bots = set()
1641 missing_bots = set()
1642 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231643 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281644 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281645 # In order to disambiguate between bots with the same name on
1646 # different waterfalls, support has been added to various
1647 # exceptions for concatenating the waterfall name after the bot
1648 # name.
1649 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231650 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381651 removals = (exception.get('remove_from', []) +
1652 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231653 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381654 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281655 if removal not in all_bots:
1656 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411657
Kenneth Russelleb60cbd22017-12-05 07:54:281658 if missing_bots:
1659 raise BBGenErr('The following nonexistent machines were referenced in '
1660 'the test suite exceptions: ' + str(missing_bots))
1661
Garrett Beatyb061e69d2023-06-27 16:15:351662 for name, mixin in self.mixins.items():
1663 if '$mixin_append' in mixin:
1664 raise BBGenErr(
1665 f'$mixin_append is no longer supported (set in mixin "{name}"),'
1666 ' args and named caches specified as normal will be appended')
1667
Garrett Beatyac3dc962024-10-11 17:30:351668 # All variant references must be referenced
1669 seen_variants = set()
1670 for suite in self.test_suites.values():
1671 if isinstance(suite, list):
1672 continue
1673
1674 for test in suite.values():
1675 if isinstance(test, dict):
1676 for variant in test.get('variants', []):
1677 if isinstance(variant, str):
1678 seen_variants.add(variant)
1679
1680 missing_variants = set(self.variants.keys()) - seen_variants
1681 if missing_variants:
1682 raise BBGenErr('The following variants were unreferenced: %s. They must '
1683 'be referenced in a matrix test suite under the variants '
1684 'key.' % str(missing_variants))
1685
Stephen Martinis0382bc12018-09-17 22:29:071686 # All mixins must be referenced
1687 seen_mixins = set()
1688 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011689 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231690 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011691 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071692 for suite in self.test_suites.values():
1693 if isinstance(suite, list):
1694 # Don't care about this, it's a composition, which shouldn't include a
1695 # swarming mixin.
1696 continue
1697
1698 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561699 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011700 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Garrett Beaty4b9f1752024-09-26 20:02:501701 seen_mixins = seen_mixins.union(
1702 test.get('test_common', {}).get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071703
Zhaoyang Li9da047d52021-05-10 21:31:441704 for variant in self.variants:
1705 # Unpack the variant from variants.pyl if it's string based.
1706 if isinstance(variant, str):
1707 variant = self.variants[variant]
1708 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1709
Garrett Beaty086b3402024-09-25 23:45:341710 missing_mixins = set()
1711 for name, mixin_value in self.mixins.items():
1712 if name not in seen_mixins and mixin_value.get('fail_if_unused', True):
1713 missing_mixins.add(name)
Stephen Martinis0382bc12018-09-17 22:29:071714 if missing_mixins:
1715 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1716 ' referenced in a waterfall, machine, or test suite.' % (
1717 str(missing_mixins)))
1718
Stephen Martinis54d64ad2018-09-21 22:16:201719
Garrett Beaty79339e182023-04-10 20:45:471720 def type_assert(self, node, typ, file_path, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201721 """Asserts that the Python AST node |node| is of type |typ|.
1722
1723 If verbose is set, it prints out some helpful context lines, showing where
1724 exactly the error occurred in the file.
1725 """
1726 if not isinstance(node, typ):
1727 if verbose:
Brian Sheedy0d2300f32024-08-13 23:14:411728 lines = [''] + self.read_file(file_path).splitlines()
Stephen Martinis54d64ad2018-09-21 22:16:201729
1730 context = 2
1731 lines_start = max(node.lineno - context, 0)
1732 # Add one to include the last line
1733 lines_end = min(node.lineno + context, len(lines)) + 1
Garrett Beaty79339e182023-04-10 20:45:471734 lines = itertools.chain(
1735 ['== %s ==\n' % file_path],
Brian Sheedy0d2300f32024-08-13 23:14:411736 ['<snip>\n'],
Garrett Beaty79339e182023-04-10 20:45:471737 [
1738 '%d %s' % (lines_start + i, line)
1739 for i, line in enumerate(lines[lines_start:lines_start +
1740 context])
1741 ],
1742 ['-' * 80 + '\n'],
1743 ['%d %s' % (node.lineno, lines[node.lineno])],
1744 [
1745 '-' * (node.col_offset + 3) + '^' + '-' *
1746 (80 - node.col_offset - 4) + '\n'
1747 ],
1748 [
1749 '%d %s' % (node.lineno + 1 + i, line)
1750 for i, line in enumerate(lines[node.lineno + 1:lines_end])
1751 ],
Brian Sheedy0d2300f32024-08-13 23:14:411752 ['<snip>\n'],
Stephen Martinis54d64ad2018-09-21 22:16:201753 )
1754 # Print out a useful message when a type assertion fails.
1755 for l in lines:
1756 self.print_line(l.strip())
1757
1758 node_dumped = ast.dump(node, annotate_fields=False)
1759 # If the node is huge, truncate it so everything fits in a terminal
1760 # window.
1761 if len(node_dumped) > 60: # pragma: no cover
1762 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1763 raise BBGenErr(
Brian Sheedy0d2300f32024-08-13 23:14:411764 "Invalid .pyl file '%s'. Python AST node %r on line %s expected to"
Garrett Beaty79339e182023-04-10 20:45:471765 ' be %s, is %s' %
1766 (file_path, node_dumped, node.lineno, typ, type(node)))
Stephen Martinis54d64ad2018-09-21 22:16:201767
Garrett Beaty79339e182023-04-10 20:45:471768 def check_ast_list_formatted(self,
1769 keys,
1770 file_path,
1771 verbose,
Stephen Martinis1384ff92020-01-07 19:52:151772 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531773 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201774
Stephen Martinis5bef0fc2020-01-06 22:47:531775 Currently only checks to ensure they're correctly sorted, and that there
1776 are no duplicates.
1777
1778 Args:
1779 keys: An python list of AST nodes.
1780
1781 It's a list of AST nodes instead of a list of strings because
1782 when verbose is set, it tries to print out context of where the
1783 diffs are in the file.
Garrett Beaty79339e182023-04-10 20:45:471784 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531785 verbose: If set, print out diff information about how the keys are
1786 incorrectly formatted.
1787 check_sorting: If true, checks if the list is sorted.
1788 Returns:
1789 If the keys are correctly formatted.
1790 """
1791 if not keys:
1792 return True
1793
1794 assert isinstance(keys[0], ast.Str)
1795
1796 keys_strs = [k.s for k in keys]
1797 # Keys to diff against. Used below.
1798 keys_to_diff_against = None
1799 # If the list is properly formatted.
1800 list_formatted = True
1801
1802 # Duplicates are always bad.
1803 if len(set(keys_strs)) != len(keys_strs):
1804 list_formatted = False
1805 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1806
1807 if check_sorting and sorted(keys_strs) != keys_strs:
1808 list_formatted = False
1809 if list_formatted:
1810 return True
1811
1812 if verbose:
1813 line_num = keys[0].lineno
1814 keys = [k.s for k in keys]
1815 if check_sorting:
1816 # If we have duplicates, sorting this will take care of it anyways.
1817 keys_to_diff_against = sorted(set(keys))
1818 # else, keys_to_diff_against is set above already
1819
1820 self.print_line('=' * 80)
1821 self.print_line('(First line of keys is %s)' % line_num)
Garrett Beaty79339e182023-04-10 20:45:471822 for line in difflib.context_diff(keys,
1823 keys_to_diff_against,
1824 fromfile='current (%r)' % file_path,
1825 tofile='sorted',
1826 lineterm=''):
Stephen Martinis5bef0fc2020-01-06 22:47:531827 self.print_line(line)
1828 self.print_line('=' * 80)
1829
1830 return False
1831
Garrett Beaty79339e182023-04-10 20:45:471832 def check_ast_dict_formatted(self, node, file_path, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531833 """Checks if an ast dictionary's keys are correctly formatted.
1834
1835 Just a simple wrapper around check_ast_list_formatted.
1836 Args:
1837 node: An AST node. Assumed to be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471838 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531839 verbose: If set, print out diff information about how the keys are
1840 incorrectly formatted.
1841 check_sorting: If true, checks if the list is sorted.
1842 Returns:
1843 If the dictionary is correctly formatted.
1844 """
Stephen Martinis54d64ad2018-09-21 22:16:201845 keys = []
1846 # The keys of this dict are ordered as ordered in the file; normal python
1847 # dictionary keys are given an arbitrary order, but since we parsed the
1848 # file itself, the order as given in the file is preserved.
1849 for key in node.keys:
Garrett Beaty79339e182023-04-10 20:45:471850 self.type_assert(key, ast.Str, file_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531851 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201852
Garrett Beaty79339e182023-04-10 20:45:471853 return self.check_ast_list_formatted(keys, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181854
1855 def check_input_files_sorting(self, verbose=False):
Alison Gale923a33e2024-04-22 23:34:281856 # TODO(crbug.com/41415841): Add the ability for this script to
Stephen Martinis54d64ad2018-09-21 22:16:201857 # actually format the files, rather than just complain if they're
1858 # incorrectly formatted.
1859 bad_files = set()
Garrett Beaty79339e182023-04-10 20:45:471860
1861 def parse_file(file_path):
Stephen Martinis5bef0fc2020-01-06 22:47:531862 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201863
Stephen Martinis5bef0fc2020-01-06 22:47:531864 Returns an AST node representing the value in the pyl file."""
Garrett Beaty79339e182023-04-10 20:45:471865 parsed = ast.parse(self.read_file(file_path))
Stephen Martinisf83893722018-09-19 00:02:181866
Stephen Martinisf83893722018-09-19 00:02:181867 # Must be a module.
Garrett Beaty79339e182023-04-10 20:45:471868 self.type_assert(parsed, ast.Module, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181869 module = parsed.body
1870
1871 # Only one expression in the module.
Garrett Beaty79339e182023-04-10 20:45:471872 self.type_assert(module, list, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181873 if len(module) != 1: # pragma: no cover
Garrett Beaty79339e182023-04-10 20:45:471874 raise BBGenErr('Invalid .pyl file %s' % file_path)
Stephen Martinisf83893722018-09-19 00:02:181875 expr = module[0]
Garrett Beaty79339e182023-04-10 20:45:471876 self.type_assert(expr, ast.Expr, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181877
Stephen Martinis5bef0fc2020-01-06 22:47:531878 return expr.value
1879
1880 # Handle this separately
Garrett Beaty79339e182023-04-10 20:45:471881 value = parse_file(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531882 # Value should be a list.
Garrett Beaty79339e182023-04-10 20:45:471883 self.type_assert(value, ast.List, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531884
1885 keys = []
Joshua Hood56c673c2022-03-02 20:29:331886 for elm in value.elts:
Garrett Beaty79339e182023-04-10 20:45:471887 self.type_assert(elm, ast.Dict, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531888 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331889 for key, val in zip(elm.keys, elm.values):
Garrett Beaty79339e182023-04-10 20:45:471890 self.type_assert(key, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531891 if key.s == 'machines':
Garrett Beaty79339e182023-04-10 20:45:471892 if not self.check_ast_dict_formatted(
1893 val, self.args.waterfalls_pyl_path, verbose):
1894 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531895
Brian Sheedy0d2300f32024-08-13 23:14:411896 if key.s == 'name':
Garrett Beaty79339e182023-04-10 20:45:471897 self.type_assert(val, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531898 waterfall_name = val
1899 assert waterfall_name
1900 keys.append(waterfall_name)
1901
Garrett Beaty79339e182023-04-10 20:45:471902 if not self.check_ast_list_formatted(keys, self.args.waterfalls_pyl_path,
1903 verbose):
1904 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531905
Garrett Beaty79339e182023-04-10 20:45:471906 for file_path in (
1907 self.args.mixins_pyl_path,
1908 self.args.test_suites_pyl_path,
1909 self.args.test_suite_exceptions_pyl_path,
Stephen Martinis5bef0fc2020-01-06 22:47:531910 ):
Garrett Beaty79339e182023-04-10 20:45:471911 value = parse_file(file_path)
Stephen Martinisf83893722018-09-19 00:02:181912 # Value should be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471913 self.type_assert(value, ast.Dict, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181914
Garrett Beaty79339e182023-04-10 20:45:471915 if not self.check_ast_dict_formatted(value, file_path, verbose):
1916 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531917
Garrett Beaty79339e182023-04-10 20:45:471918 if file_path == self.args.test_suites_pyl_path:
Jeff Yoon8154e582019-12-03 23:30:011919 expected_keys = ['basic_suites',
1920 'compound_suites',
1921 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201922 actual_keys = [node.s for node in value.keys]
1923 assert all(key in expected_keys for key in actual_keys), (
Garrett Beaty79339e182023-04-10 20:45:471924 'Invalid %r file; expected keys %r, got %r' %
1925 (file_path, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:331926 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:201927 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011928 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201929 for suite_group in suite_dicts:
Garrett Beaty79339e182023-04-10 20:45:471930 if not self.check_ast_dict_formatted(suite_group, file_path, verbose):
1931 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181932
Stephen Martinis5bef0fc2020-01-06 22:47:531933 for key, suite in zip(value.keys, value.values):
1934 # The compound suites are checked in
1935 # 'check_composition_type_test_suites()'
1936 if key.s == 'basic_suites':
1937 for group in suite.values:
Garrett Beaty79339e182023-04-10 20:45:471938 if not self.check_ast_dict_formatted(group, file_path, verbose):
1939 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531940 break
Stephen Martinis54d64ad2018-09-21 22:16:201941
Garrett Beaty79339e182023-04-10 20:45:471942 elif file_path == self.args.test_suite_exceptions_pyl_path:
Stephen Martinis5bef0fc2020-01-06 22:47:531943 # Check the values for each test.
1944 for test in value.values:
1945 for kind, node in zip(test.keys, test.values):
1946 if isinstance(node, ast.Dict):
Garrett Beaty79339e182023-04-10 20:45:471947 if not self.check_ast_dict_formatted(node, file_path, verbose):
1948 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531949 elif kind.s == 'remove_from':
1950 # Don't care about sorting; these are usually grouped, since the
1951 # same bug can affect multiple builders. Do want to make sure
1952 # there aren't duplicates.
Garrett Beaty79339e182023-04-10 20:45:471953 if not self.check_ast_list_formatted(
1954 node.elts, file_path, verbose, check_sorting=False):
1955 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181956
1957 if bad_files:
1958 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201959 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531960 'unsorted, or have duplicates. Re-run this with --verbose to see '
1961 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181962
Kenneth Russelleb60cbd22017-12-05 07:54:281963 def check_output_file_consistency(self, verbose=False):
1964 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011965 # All waterfalls/bucket .json files must have been written
1966 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:281967 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011968 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:161969 outputs = self.generate_outputs()
1970 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:011971 expected = self.jsonify(expected_contents)
Garrett Beaty79339e182023-04-10 20:45:471972 file_path = os.path.join(self.args.output_dir, filename + '.json')
Ben Pastenef21cda32023-03-30 22:00:571973 current = self.read_file(file_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281974 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:011975 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:321976 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:011977 self.print_line('File ' + filename +
1978 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321979 'contents:')
1980 for line in difflib.unified_diff(
1981 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501982 current.splitlines(),
1983 fromfile='expected', tofile='current'):
1984 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:011985
1986 if ungenerated_files:
1987 raise BBGenErr(
1988 'The following files have not been properly '
1989 'autogenerated by generate_buildbot_json.py: ' +
1990 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:281991
Dirk Pranke772f55f2021-04-28 04:51:161992 for builder_group, builders in outputs.items():
1993 for builder, step_types in builders.items():
Garrett Beatydca3d882023-09-14 23:50:321994 for test_type in ('gtest_tests', 'isolated_scripts'):
1995 for step_data in step_types.get(test_type, []):
1996 step_name = step_data['name']
1997 self._check_swarming_config(builder_group, builder, step_name,
1998 step_data)
Dirk Pranke772f55f2021-04-28 04:51:161999
2000 def _check_swarming_config(self, filename, builder, step_name, step_data):
Alison Gale47d1537d2024-04-19 21:31:462001 # TODO(crbug.com/40179524): Ensure all swarming tests specify cpu, not
Dirk Pranke772f55f2021-04-28 04:51:162002 # just mac tests.
Garrett Beatybb18d532023-06-26 22:16:332003 if 'swarming' in step_data:
Garrett Beatyade673d2023-08-04 22:00:252004 dimensions = step_data['swarming'].get('dimensions')
2005 if not dimensions:
Tatsuhisa Yamaguchif1878d52023-11-06 06:02:252006 raise BBGenErr('%s: %s / %s : dimensions must be specified for all '
Dirk Pranke772f55f2021-04-28 04:51:162007 'swarmed tests' % (filename, builder, step_name))
Garrett Beatyade673d2023-08-04 22:00:252008 if not dimensions.get('os'):
2009 raise BBGenErr('%s: %s / %s : os must be specified for all '
2010 'swarmed tests' % (filename, builder, step_name))
2011 if 'Mac' in dimensions.get('os') and not dimensions.get('cpu'):
2012 raise BBGenErr('%s: %s / %s : cpu must be specified for mac '
2013 'swarmed tests' % (filename, builder, step_name))
Dirk Pranke772f55f2021-04-28 04:51:162014
Kenneth Russelleb60cbd22017-12-05 07:54:282015 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502016 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282017 self.check_output_file_consistency(verbose) # pragma: no cover
2018
Karen Qiane24b7ee2019-02-12 23:37:062019 def does_test_match(self, test_info, params_dict):
2020 """Checks to see if the test matches the parameters given.
2021
2022 Compares the provided test_info with the params_dict to see
2023 if the bot matches the parameters given. If so, returns True.
2024 Else, returns false.
2025
2026 Args:
2027 test_info (dict): Information about a specific bot provided
2028 in the format shown in waterfalls.pyl
2029 params_dict (dict): Dictionary of parameters and their values
2030 to look for in the bot
2031 Ex: {
2032 'device_os':'android',
2033 '--flag':True,
2034 'mixins': ['mixin1', 'mixin2'],
2035 'ex_key':'ex_value'
2036 }
2037
2038 """
2039 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2040 'kvm', 'pool', 'integrity'] # dimension parameters
2041 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2042 'can_use_on_swarming_builders']
2043 for param in params_dict:
2044 # if dimension parameter
2045 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2046 if not 'swarming' in test_info:
2047 return False
2048 swarming = test_info['swarming']
2049 if param in SWARMING_PARAMS:
2050 if not param in swarming:
2051 return False
2052 if not str(swarming[param]) == params_dict[param]:
2053 return False
2054 else:
Garrett Beatyade673d2023-08-04 22:00:252055 if not 'dimensions' in swarming:
Karen Qiane24b7ee2019-02-12 23:37:062056 return False
Garrett Beatyade673d2023-08-04 22:00:252057 dimensions = swarming['dimensions']
Karen Qiane24b7ee2019-02-12 23:37:062058 # only looking at the first dimension set
Garrett Beatyade673d2023-08-04 22:00:252059 if not param in dimensions:
Karen Qiane24b7ee2019-02-12 23:37:062060 return False
Garrett Beatyade673d2023-08-04 22:00:252061 if not dimensions[param] == params_dict[param]:
Karen Qiane24b7ee2019-02-12 23:37:062062 return False
2063
2064 # if flag
2065 elif param.startswith('--'):
2066 if not 'args' in test_info:
2067 return False
2068 if not param in test_info['args']:
2069 return False
2070
2071 # not dimension parameter/flag/mixin
2072 else:
2073 if not param in test_info:
2074 return False
2075 if not test_info[param] == params_dict[param]:
2076 return False
2077 return True
2078 def error_msg(self, msg):
2079 """Prints an error message.
2080
2081 In addition to a catered error message, also prints
2082 out where the user can find more help. Then, program exits.
2083 """
2084 self.print_line(msg + (' If you need more information, ' +
2085 'please run with -h or --help to see valid commands.'))
2086 sys.exit(1)
2087
2088 def find_bots_that_run_test(self, test, bots):
2089 matching_bots = []
2090 for bot in bots:
2091 bot_info = bots[bot]
2092 tests = self.flatten_tests_for_bot(bot_info)
2093 for test_info in tests:
Garrett Beatyffe83c4f2023-09-08 19:07:372094 test_name = test_info['name']
Karen Qiane24b7ee2019-02-12 23:37:062095 if not test_name == test:
2096 continue
2097 matching_bots.append(bot)
2098 return matching_bots
2099
2100 def find_tests_with_params(self, tests, params_dict):
2101 matching_tests = []
2102 for test_name in tests:
2103 test_info = tests[test_name]
2104 if not self.does_test_match(test_info, params_dict):
2105 continue
2106 if not test_name in matching_tests:
2107 matching_tests.append(test_name)
2108 return matching_tests
2109
2110 def flatten_waterfalls_for_query(self, waterfalls):
2111 bots = {}
2112 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012113 waterfall_tests = self.generate_output_tests(waterfall)
2114 for bot in waterfall_tests:
2115 bot_info = waterfall_tests[bot]
2116 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062117 return bots
2118
2119 def flatten_tests_for_bot(self, bot_info):
2120 """Returns a list of flattened tests.
2121
2122 Returns a list of tests not grouped by test category
2123 for a specific bot.
2124 """
2125 TEST_CATS = self.get_test_generator_map().keys()
2126 tests = []
2127 for test_cat in TEST_CATS:
2128 if not test_cat in bot_info:
2129 continue
2130 test_cat_tests = bot_info[test_cat]
2131 tests = tests + test_cat_tests
2132 return tests
2133
2134 def flatten_tests_for_query(self, test_suites):
2135 """Returns a flattened dictionary of tests.
2136
2137 Returns a dictionary of tests associate with their
2138 configuration, not grouped by their test suite.
2139 """
2140 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232141 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062142 for test in test_suite:
2143 test_info = test_suite[test]
2144 test_name = test
Karen Qiane24b7ee2019-02-12 23:37:062145 tests[test_name] = test_info
2146 return tests
2147
2148 def parse_query_filter_params(self, params):
2149 """Parses the filter parameters.
2150
2151 Creates a dictionary from the parameters provided
2152 to filter the bot array.
2153 """
2154 params_dict = {}
2155 for p in params:
2156 # flag
Brian Sheedy0d2300f32024-08-13 23:14:412157 if p.startswith('--'):
Karen Qiane24b7ee2019-02-12 23:37:062158 params_dict[p] = True
2159 else:
Brian Sheedy0d2300f32024-08-13 23:14:412160 pair = p.split(':')
Karen Qiane24b7ee2019-02-12 23:37:062161 if len(pair) != 2:
2162 self.error_msg('Invalid command.')
2163 # regular parameters
Brian Sheedy0d2300f32024-08-13 23:14:412164 if pair[1].lower() == 'true':
Karen Qiane24b7ee2019-02-12 23:37:062165 params_dict[pair[0]] = True
Brian Sheedy0d2300f32024-08-13 23:14:412166 elif pair[1].lower() == 'false':
Karen Qiane24b7ee2019-02-12 23:37:062167 params_dict[pair[0]] = False
2168 else:
2169 params_dict[pair[0]] = pair[1]
2170 return params_dict
2171
2172 def get_test_suites_dict(self, bots):
2173 """Returns a dictionary of bots and their tests.
2174
2175 Returns a dictionary of bots and a list of their associated tests.
2176 """
2177 test_suite_dict = dict()
2178 for bot in bots:
2179 bot_info = bots[bot]
2180 tests = self.flatten_tests_for_bot(bot_info)
2181 test_suite_dict[bot] = tests
2182 return test_suite_dict
2183
2184 def output_query_result(self, result, json_file=None):
2185 """Outputs the result of the query.
2186
2187 If a json file parameter name is provided, then
2188 the result is output into the json file. If not,
2189 then the result is printed to the console.
2190 """
2191 output = json.dumps(result, indent=2)
2192 if json_file:
2193 self.write_file(json_file, output)
2194 else:
2195 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062196
Joshua Hood56c673c2022-03-02 20:29:332197 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062198 def query(self, args):
2199 """Queries tests or bots.
2200
2201 Depending on the arguments provided, outputs a json of
2202 tests or bots matching the appropriate optional parameters provided.
2203 """
2204 # split up query statement
2205 query = args.query.split('/')
2206 self.load_configuration_files()
2207 self.resolve_configuration_files()
2208
2209 # flatten bots json
2210 tests = self.test_suites
2211 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2212
2213 cmd_class = query[0]
2214
2215 # For queries starting with 'bots'
Brian Sheedy0d2300f32024-08-13 23:14:412216 if cmd_class == 'bots':
Karen Qiane24b7ee2019-02-12 23:37:062217 if len(query) == 1:
2218 return self.output_query_result(bots, args.json)
2219 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332220 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062221 if query[1] == 'tests':
2222 test_suites_dict = self.get_test_suites_dict(bots)
2223 return self.output_query_result(test_suites_dict, args.json)
Brian Sheedy0d2300f32024-08-13 23:14:412224 self.error_msg('This query should be in the format: bots/tests.')
Karen Qiane24b7ee2019-02-12 23:37:062225
2226 else:
Brian Sheedy0d2300f32024-08-13 23:14:412227 self.error_msg('This query should have 0 or 1 "/"", found %s instead.' %
2228 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062229
2230 # For queries starting with 'bot'
Brian Sheedy0d2300f32024-08-13 23:14:412231 elif cmd_class == 'bot':
Karen Qiane24b7ee2019-02-12 23:37:062232 if not len(query) == 2 and not len(query) == 3:
Brian Sheedy0d2300f32024-08-13 23:14:412233 self.error_msg('Command should have 1 or 2 "/"", found %s instead.' %
2234 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062235 bot_id = query[1]
2236 if not bot_id in bots:
Brian Sheedy0d2300f32024-08-13 23:14:412237 self.error_msg('No bot named "' + bot_id + '" found.')
Karen Qiane24b7ee2019-02-12 23:37:062238 bot_info = bots[bot_id]
2239 if len(query) == 2:
2240 return self.output_query_result(bot_info, args.json)
2241 if not query[2] == 'tests':
Brian Sheedy0d2300f32024-08-13 23:14:412242 self.error_msg('The query should be in the format:'
2243 'bot/<bot-name>/tests.')
Karen Qiane24b7ee2019-02-12 23:37:062244
2245 bot_tests = self.flatten_tests_for_bot(bot_info)
2246 return self.output_query_result(bot_tests, args.json)
2247
2248 # For queries starting with 'tests'
Brian Sheedy0d2300f32024-08-13 23:14:412249 elif cmd_class == 'tests':
Karen Qiane24b7ee2019-02-12 23:37:062250 if not len(query) == 1 and not len(query) == 2:
Brian Sheedy0d2300f32024-08-13 23:14:412251 self.error_msg('The query should have 0 or 1 "/", found %s instead.' %
2252 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062253 flattened_tests = self.flatten_tests_for_query(tests)
2254 if len(query) == 1:
2255 return self.output_query_result(flattened_tests, args.json)
2256
2257 # create params dict
2258 params = query[1].split('&')
2259 params_dict = self.parse_query_filter_params(params)
2260 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2261 return self.output_query_result(matching_bots)
2262
2263 # For queries starting with 'test'
Brian Sheedy0d2300f32024-08-13 23:14:412264 elif cmd_class == 'test':
Karen Qiane24b7ee2019-02-12 23:37:062265 if not len(query) == 2 and not len(query) == 3:
Brian Sheedy0d2300f32024-08-13 23:14:412266 self.error_msg('The query should have 1 or 2 "/", found %s instead.' %
2267 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062268 test_id = query[1]
2269 if len(query) == 2:
2270 flattened_tests = self.flatten_tests_for_query(tests)
2271 for test in flattened_tests:
2272 if test == test_id:
2273 return self.output_query_result(flattened_tests[test], args.json)
Brian Sheedy0d2300f32024-08-13 23:14:412274 self.error_msg('There is no test named %s.' % test_id)
Karen Qiane24b7ee2019-02-12 23:37:062275 if not query[2] == 'bots':
Brian Sheedy0d2300f32024-08-13 23:14:412276 self.error_msg('The query should be in the format: '
2277 'test/<test-name>/bots')
Karen Qiane24b7ee2019-02-12 23:37:062278 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2279 return self.output_query_result(bots_for_test)
2280
2281 else:
Brian Sheedy0d2300f32024-08-13 23:14:412282 self.error_msg('Your command did not match any valid commands. '
2283 'Try starting with "bots", "bot", "tests", or "test".')
2284
Joshua Hood56c673c2022-03-02 20:29:332285 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282286
Garrett Beaty1afaccc2020-06-25 19:58:152287 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282288 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502289 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062290 elif self.args.query:
2291 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282292 else:
Greg Gutermanf60eb052020-03-12 17:40:012293 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282294 return 0
2295
Brian Sheedy0d2300f32024-08-13 23:14:412296
2297if __name__ == '__main__': # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152298 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2299 sys.exit(generator.main())