blob: afe09f665528d736257d62f59ab501a952e220ea [file] [log] [blame]
Joshua Hood3455e1352022-03-03 23:23:591#!/usr/bin/env vpython3
Avi Drissmandfd880852022-09-15 20:11:092# Copyright 2016 The Chromium Authors
Kenneth Russelleb60cbd22017-12-05 07:54:283# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Script to generate the majority of the JSON files in the src/testing/buildbot
7directory. Maintaining these files by hand is too unwieldy.
8"""
9
10import argparse
11import ast
12import collections
13import copy
John Budorick826d5ed2017-12-28 19:27:3214import difflib
Garrett Beatyd5ca75962020-05-07 16:58:3115import glob
Kenneth Russell8ceeabf2017-12-11 17:53:2816import itertools
Kenneth Russelleb60cbd22017-12-05 07:54:2817import json
18import os
Kenneth Russelleb60cbd22017-12-05 07:54:2819import string
20import sys
21
Brian Sheedya31578e2020-05-18 20:24:3622import buildbot_json_magic_substitutions as magic_substitutions
23
Joshua Hood56c673c2022-03-02 20:29:3324# pylint: disable=super-with-arguments,useless-super-delegation
25
Kenneth Russelleb60cbd22017-12-05 07:54:2826THIS_DIR = os.path.dirname(os.path.abspath(__file__))
27
Brian Sheedyf74819b2021-06-04 01:38:3828BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP = {
29 'android-chromium': '_android_chrome',
30 'android-chromium-monochrome': '_android_monochrome',
Brian Sheedyf74819b2021-06-04 01:38:3831 'android-webview': '_android_webview',
32}
33
Kenneth Russelleb60cbd22017-12-05 07:54:2834
35class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4936 def __init__(self, message):
37 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2838
39
Joshua Hood56c673c2022-03-02 20:29:3340class BaseGenerator(object): # pylint: disable=useless-object-inheritance
Kenneth Russelleb60cbd22017-12-05 07:54:2841 def __init__(self, bb_gen):
42 self.bb_gen = bb_gen
43
Kenneth Russell8ceeabf2017-12-11 17:53:2844 def generate(self, waterfall, tester_name, tester_config, input_tests):
Garrett Beatyffe83c4f2023-09-08 19:07:3745 raise NotImplementedError() # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:2846
47
Kenneth Russell8a386d42018-06-02 09:48:0148class GPUTelemetryTestGenerator(BaseGenerator):
Xinan Linedcf05b32023-10-19 23:13:5049 def __init__(self,
50 bb_gen,
51 is_android_webview=False,
52 is_cast_streaming=False,
53 is_skylab=False):
Kenneth Russell8a386d42018-06-02 09:48:0154 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5655 self._is_android_webview = is_android_webview
Fabrice de Ganscbd655f2022-08-04 20:15:3056 self._is_cast_streaming = is_cast_streaming
Xinan Linedcf05b32023-10-19 23:13:5057 self._is_skylab = is_skylab
Kenneth Russell8a386d42018-06-02 09:48:0158
59 def generate(self, waterfall, tester_name, tester_config, input_tests):
60 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:2361 for test_name, test_config in sorted(input_tests.items()):
Ben Pastene8e7eb2652022-04-29 19:44:3162 # Variants allow more than one definition for a given test, and is defined
63 # in array format from resolve_variants().
64 if not isinstance(test_config, list):
65 test_config = [test_config]
66
67 for config in test_config:
Xinan Linedcf05b32023-10-19 23:13:5068 test = self.bb_gen.generate_gpu_telemetry_test(
69 waterfall, tester_name, tester_config, test_name, config,
70 self._is_android_webview, self._is_cast_streaming, self._is_skylab)
Ben Pastene8e7eb2652022-04-29 19:44:3171 if test:
72 isolated_scripts.append(test)
73
Kenneth Russell8a386d42018-06-02 09:48:0174 return isolated_scripts
75
Kenneth Russell8a386d42018-06-02 09:48:0176
Brian Sheedyb6491ba2022-09-26 20:49:4977class SkylabGPUTelemetryTestGenerator(GPUTelemetryTestGenerator):
Xinan Linedcf05b32023-10-19 23:13:5078 def __init__(self, bb_gen):
79 super(SkylabGPUTelemetryTestGenerator, self).__init__(bb_gen,
80 is_skylab=True)
81
Brian Sheedyb6491ba2022-09-26 20:49:4982 def generate(self, *args, **kwargs):
83 # This should be identical to a regular GPU Telemetry test, but with any
84 # swarming arguments removed.
85 isolated_scripts = super(SkylabGPUTelemetryTestGenerator,
86 self).generate(*args, **kwargs)
87 for test in isolated_scripts:
Xinan Lind9b1d2e72022-11-14 20:57:0288 # chromium_GPU is the Autotest wrapper created for browser GPU tests
89 # run in Skylab.
Xinan Lin1f28a0d2023-03-13 17:39:4190 test['autotest_name'] = 'chromium_Graphics'
Xinan Lind9b1d2e72022-11-14 20:57:0291 # As of 22Q4, Skylab tests are running on a CrOS flavored Autotest
92 # framework and it does not support the sub-args like
93 # extra-browser-args. So we have to pop it out and create a new
94 # key for it. See crrev.com/c/3965359 for details.
95 for idx, arg in enumerate(test.get('args', [])):
96 if '--extra-browser-args' in arg:
97 test['args'].pop(idx)
98 test['extra_browser_args'] = arg.replace('--extra-browser-args=', '')
99 break
Brian Sheedyb6491ba2022-09-26 20:49:49100 return isolated_scripts
101
102
Kenneth Russelleb60cbd22017-12-05 07:54:28103class GTestGenerator(BaseGenerator):
104 def __init__(self, bb_gen):
105 super(GTestGenerator, self).__init__(bb_gen)
106
Kenneth Russell8ceeabf2017-12-11 17:53:28107 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28108 # The relative ordering of some of the tests is important to
109 # minimize differences compared to the handwritten JSON files, since
110 # Python's sorts are stable and there are some tests with the same
111 # key (see gles2_conform_d3d9_test and similar variants). Avoid
112 # losing the order by avoiding coalescing the dictionaries into one.
113 gtests = []
Jamie Madillcf4f8c72021-05-20 19:24:23114 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoon67c3e832020-02-08 07:39:38115 # Variants allow more than one definition for a given test, and is defined
116 # in array format from resolve_variants().
117 if not isinstance(test_config, list):
118 test_config = [test_config]
119
120 for config in test_config:
121 test = self.bb_gen.generate_gtest(
122 waterfall, tester_name, tester_config, test_name, config)
123 if test:
124 # generate_gtest may veto the test generation on this tester.
125 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28126 return gtests
127
Kenneth Russelleb60cbd22017-12-05 07:54:28128
129class IsolatedScriptTestGenerator(BaseGenerator):
130 def __init__(self, bb_gen):
131 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
132
Kenneth Russell8ceeabf2017-12-11 17:53:28133 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28134 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23135 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43136 # Variants allow more than one definition for a given test, and is defined
137 # in array format from resolve_variants().
138 if not isinstance(test_config, list):
139 test_config = [test_config]
140
141 for config in test_config:
142 test = self.bb_gen.generate_isolated_script_test(
143 waterfall, tester_name, tester_config, test_name, config)
144 if test:
145 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28146 return isolated_scripts
147
Kenneth Russelleb60cbd22017-12-05 07:54:28148
149class ScriptGenerator(BaseGenerator):
150 def __init__(self, bb_gen):
151 super(ScriptGenerator, self).__init__(bb_gen)
152
Kenneth Russell8ceeabf2017-12-11 17:53:28153 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28154 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23155 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28156 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28157 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28158 if test:
159 scripts.append(test)
160 return scripts
161
Kenneth Russelleb60cbd22017-12-05 07:54:28162
163class JUnitGenerator(BaseGenerator):
164 def __init__(self, bb_gen):
165 super(JUnitGenerator, self).__init__(bb_gen)
166
Kenneth Russell8ceeabf2017-12-11 17:53:28167 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28168 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23169 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28170 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28171 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28172 if test:
173 scripts.append(test)
174 return scripts
175
Kenneth Russelleb60cbd22017-12-05 07:54:28176
Xinan Lin05fb9c1752020-12-17 00:15:52177class SkylabGenerator(BaseGenerator):
178 def __init__(self, bb_gen):
179 super(SkylabGenerator, self).__init__(bb_gen)
180
181 def generate(self, waterfall, tester_name, tester_config, input_tests):
182 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23183 for test_name, test_config in sorted(input_tests.items()):
Xinan Lin05fb9c1752020-12-17 00:15:52184 for config in test_config:
185 test = self.bb_gen.generate_skylab_test(waterfall, tester_name,
186 tester_config, test_name,
187 config)
188 if test:
189 scripts.append(test)
190 return scripts
191
Xinan Lin05fb9c1752020-12-17 00:15:52192
Jeff Yoon67c3e832020-02-08 07:39:38193def check_compound_references(other_test_suites=None,
194 sub_suite=None,
195 suite=None,
196 target_test_suites=None,
197 test_type=None,
198 **kwargs):
199 """Ensure comound reference's don't target other compounds"""
200 del kwargs
201 if sub_suite in other_test_suites or sub_suite in target_test_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15202 raise BBGenErr('%s may not refer to other composition type test '
203 'suites (error found while processing %s)' %
204 (test_type, suite))
205
Jeff Yoon67c3e832020-02-08 07:39:38206
207def check_basic_references(basic_suites=None,
208 sub_suite=None,
209 suite=None,
210 **kwargs):
211 """Ensure test has a basic suite reference"""
212 del kwargs
213 if sub_suite not in basic_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15214 raise BBGenErr('Unable to find reference to %s while processing %s' %
215 (sub_suite, suite))
216
Jeff Yoon67c3e832020-02-08 07:39:38217
218def check_conflicting_definitions(basic_suites=None,
219 seen_tests=None,
220 sub_suite=None,
221 suite=None,
222 test_type=None,
Garrett Beaty235c1412023-08-29 20:26:29223 target_test_suites=None,
Jeff Yoon67c3e832020-02-08 07:39:38224 **kwargs):
225 """Ensure that if a test is reachable via multiple basic suites,
226 all of them have an identical definition of the tests.
227 """
228 del kwargs
Garrett Beaty235c1412023-08-29 20:26:29229 variants = None
230 if test_type == 'matrix_compound_suites':
231 variants = target_test_suites[suite][sub_suite].get('variants')
232 variants = variants or [None]
Jeff Yoon67c3e832020-02-08 07:39:38233 for test_name in basic_suites[sub_suite]:
Garrett Beaty235c1412023-08-29 20:26:29234 for variant in variants:
235 key = (test_name, variant)
236 if ((seen_sub_suite := seen_tests.get(key)) is not None
237 and basic_suites[sub_suite][test_name] !=
238 basic_suites[seen_sub_suite][test_name]):
239 test_description = (test_name if variant is None else
240 f'{test_name} with variant {variant} applied')
241 raise BBGenErr(
242 'Conflicting test definitions for %s from %s '
243 'and %s in %s (error found while processing %s)' %
244 (test_description, seen_tests[key], sub_suite, test_type, suite))
245 seen_tests[key] = sub_suite
246
Jeff Yoon67c3e832020-02-08 07:39:38247
248def check_matrix_identifier(sub_suite=None,
249 suite=None,
250 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05251 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38252 **kwargs):
253 """Ensure 'idenfitier' is defined for each variant"""
254 del kwargs
255 sub_suite_config = suite_def[sub_suite]
Garrett Beaty2022db42023-08-29 17:22:40256 for variant_name in sub_suite_config.get('variants', []):
257 if variant_name not in all_variants:
258 raise BBGenErr('Missing variant definition for %s in variants.pyl' %
259 variant_name)
260 variant = all_variants[variant_name]
Jeff Yoonda581c32020-03-06 03:56:05261
Jeff Yoon67c3e832020-02-08 07:39:38262 if not 'identifier' in variant:
263 raise BBGenErr('Missing required identifier field in matrix '
264 'compound suite %s, %s' % (suite, sub_suite))
Sven Zhengef0d0872022-04-04 22:13:29265 if variant['identifier'] == '':
266 raise BBGenErr('Identifier field can not be "" in matrix '
267 'compound suite %s, %s' % (suite, sub_suite))
268 if variant['identifier'].strip() != variant['identifier']:
269 raise BBGenErr('Identifier field can not have leading and trailing '
270 'whitespace in matrix compound suite %s, %s' %
271 (suite, sub_suite))
Jeff Yoon67c3e832020-02-08 07:39:38272
273
Joshua Hood56c673c2022-03-02 20:29:33274class BBJSONGenerator(object): # pylint: disable=useless-object-inheritance
Garrett Beaty1afaccc2020-06-25 19:58:15275 def __init__(self, args):
Garrett Beaty1afaccc2020-06-25 19:58:15276 self.args = args
Kenneth Russelleb60cbd22017-12-05 07:54:28277 self.waterfalls = None
278 self.test_suites = None
279 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01280 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41281 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05282 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28283
Garrett Beaty1afaccc2020-06-25 19:58:15284 @staticmethod
285 def parse_args(argv):
286
287 # RawTextHelpFormatter allows for styling of help statement
288 parser = argparse.ArgumentParser(
289 formatter_class=argparse.RawTextHelpFormatter)
290
291 group = parser.add_mutually_exclusive_group()
292 group.add_argument(
293 '-c',
294 '--check',
295 action='store_true',
296 help=
297 'Do consistency checks of configuration and generated files and then '
298 'exit. Used during presubmit. '
299 'Causes the tool to not generate any files.')
300 group.add_argument(
301 '--query',
302 type=str,
303 help=(
304 "Returns raw JSON information of buildbots and tests.\n" +
305 "Examples:\n" + " List all bots (all info):\n" +
306 " --query bots\n\n" +
307 " List all bots and only their associated tests:\n" +
308 " --query bots/tests\n\n" +
309 " List all information about 'bot1' " +
310 "(make sure you have quotes):\n" + " --query bot/'bot1'\n\n" +
311 " List tests running for 'bot1' (make sure you have quotes):\n" +
312 " --query bot/'bot1'/tests\n\n" + " List all tests:\n" +
313 " --query tests\n\n" +
314 " List all tests and the bots running them:\n" +
315 " --query tests/bots\n\n" +
316 " List all tests that satisfy multiple parameters\n" +
317 " (separation of parameters by '&' symbol):\n" +
318 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
319 " List all tests that run with a specific flag:\n" +
320 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
321 " List specific test (make sure you have quotes):\n"
322 " --query test/'test1'\n\n"
323 " List all bots running 'test1' " +
324 "(make sure you have quotes):\n" + " --query test/'test1'/bots"))
325 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47326 '--json',
327 metavar='JSON_FILE_PATH',
328 type=os.path.abspath,
329 help='Outputs results into a json file. Only works with query function.'
330 )
331 parser.add_argument(
Garrett Beaty1afaccc2020-06-25 19:58:15332 '-n',
333 '--new-files',
334 action='store_true',
335 help=
336 'Write output files as .new.json. Useful during development so old and '
337 'new files can be looked at side-by-side.')
Garrett Beatyade673d2023-08-04 22:00:25338 parser.add_argument('--dimension-sets-handling',
339 choices=['disable'],
340 default='disable',
341 help=('This flag no longer has any effect:'
342 ' dimension_sets fields are not allowed'))
Garrett Beaty1afaccc2020-06-25 19:58:15343 parser.add_argument('-v',
344 '--verbose',
345 action='store_true',
346 help='Increases verbosity. Affects consistency checks.')
347 parser.add_argument('waterfall_filters',
348 metavar='waterfalls',
349 type=str,
350 nargs='*',
351 help='Optional list of waterfalls to generate.')
352 parser.add_argument(
353 '--pyl-files-dir',
Garrett Beaty79339e182023-04-10 20:45:47354 type=os.path.abspath,
355 help=('Path to the directory containing the input .pyl files.'
356 ' By default the directory containing this script will be used.'))
Garrett Beaty1afaccc2020-06-25 19:58:15357 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47358 '--output-dir',
359 type=os.path.abspath,
360 help=('Path to the directory to output generated .json files.'
361 'By default, the pyl files directory will be used.'))
Chong Guee622242020-10-28 18:17:35362 parser.add_argument('--isolate-map-file',
363 metavar='PATH',
364 help='path to additional isolate map files.',
Garrett Beaty79339e182023-04-10 20:45:47365 type=os.path.abspath,
Chong Guee622242020-10-28 18:17:35366 default=[],
367 action='append',
368 dest='isolate_map_files')
Garrett Beaty1afaccc2020-06-25 19:58:15369 parser.add_argument(
370 '--infra-config-dir',
371 help='Path to the LUCI services configuration directory',
Garrett Beaty79339e182023-04-10 20:45:47372 type=os.path.abspath,
373 default=os.path.join(os.path.dirname(__file__), '..', '..', 'infra',
374 'config'))
375
Garrett Beaty1afaccc2020-06-25 19:58:15376 args = parser.parse_args(argv)
377 if args.json and not args.query:
378 parser.error(
379 "The --json flag can only be used with --query.") # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:15380
Garrett Beaty79339e182023-04-10 20:45:47381 args.pyl_files_dir = args.pyl_files_dir or THIS_DIR
382 args.output_dir = args.output_dir or args.pyl_files_dir
383
Stephanie Kim572b43c02023-04-13 14:24:13384 def absolute_file_path(filename):
Garrett Beaty79339e182023-04-10 20:45:47385 return os.path.join(args.pyl_files_dir, filename)
386
Stephanie Kim572b43c02023-04-13 14:24:13387 args.waterfalls_pyl_path = absolute_file_path('waterfalls.pyl')
Garrett Beaty96802d02023-07-07 14:18:05388 args.mixins_pyl_path = absolute_file_path('mixins.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13389 args.test_suites_pyl_path = absolute_file_path('test_suites.pyl')
390 args.test_suite_exceptions_pyl_path = absolute_file_path(
Garrett Beaty79339e182023-04-10 20:45:47391 'test_suite_exceptions.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13392 args.gn_isolate_map_pyl_path = absolute_file_path('gn_isolate_map.pyl')
393 args.variants_pyl_path = absolute_file_path('variants.pyl')
Garrett Beaty4999e9792024-04-03 23:29:11394 args.autoshard_exceptions_json_path = os.path.join(
395 args.infra_config_dir, 'targets', 'autoshard_exceptions.json')
Garrett Beaty79339e182023-04-10 20:45:47396
397 return args
Kenneth Russelleb60cbd22017-12-05 07:54:28398
Stephen Martinis7eb8b612018-09-21 00:17:50399 def print_line(self, line):
400 # Exists so that tests can mock
Jamie Madillcf4f8c72021-05-20 19:24:23401 print(line) # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:50402
Kenneth Russelleb60cbd22017-12-05 07:54:28403 def read_file(self, relative_path):
Garrett Beaty79339e182023-04-10 20:45:47404 with open(relative_path) as fp:
Garrett Beaty1afaccc2020-06-25 19:58:15405 return fp.read()
Kenneth Russelleb60cbd22017-12-05 07:54:28406
Garrett Beaty79339e182023-04-10 20:45:47407 def write_file(self, file_path, contents):
Peter Kastingacd55c12023-08-23 20:19:04408 with open(file_path, 'w', newline='') as fp:
Garrett Beaty79339e182023-04-10 20:45:47409 fp.write(contents)
Zhiling Huangbe008172018-03-08 19:13:11410
Joshua Hood56c673c2022-03-02 20:29:33411 # pylint: disable=inconsistent-return-statements
Garrett Beaty79339e182023-04-10 20:45:47412 def load_pyl_file(self, pyl_file_path):
Kenneth Russelleb60cbd22017-12-05 07:54:28413 try:
Garrett Beaty79339e182023-04-10 20:45:47414 return ast.literal_eval(self.read_file(pyl_file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:28415 except (SyntaxError, ValueError) as e: # pragma: no cover
Josip Sokcevic7110fb382023-06-06 01:05:29416 raise BBGenErr('Failed to parse pyl file "%s": %s' %
417 (pyl_file_path, e)) from e
Joshua Hood56c673c2022-03-02 20:29:33418 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:28419
Kenneth Russell8a386d42018-06-02 09:48:01420 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
421 # Currently it is only mandatory for bots which run GPU tests. Change these to
422 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28423 def is_android(self, tester_config):
424 return tester_config.get('os_type') == 'android'
425
Ben Pastenea9e583b2019-01-16 02:57:26426 def is_chromeos(self, tester_config):
427 return tester_config.get('os_type') == 'chromeos'
428
Chong Guc2ca5d02022-01-11 19:52:17429 def is_fuchsia(self, tester_config):
430 return tester_config.get('os_type') == 'fuchsia'
431
Brian Sheedy781c8ca42021-03-08 22:03:21432 def is_lacros(self, tester_config):
433 return tester_config.get('os_type') == 'lacros'
434
Kenneth Russell8a386d42018-06-02 09:48:01435 def is_linux(self, tester_config):
436 return tester_config.get('os_type') == 'linux'
437
Kai Ninomiya40de9f52019-10-18 21:38:49438 def is_mac(self, tester_config):
439 return tester_config.get('os_type') == 'mac'
440
441 def is_win(self, tester_config):
442 return tester_config.get('os_type') == 'win'
443
444 def is_win64(self, tester_config):
445 return (tester_config.get('os_type') == 'win' and
446 tester_config.get('browser_config') == 'release_x64')
447
Garrett Beatyffe83c4f2023-09-08 19:07:37448 def get_exception_for_test(self, test_config):
449 return self.exceptions.get(test_config['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28450
Garrett Beatyffe83c4f2023-09-08 19:07:37451 def should_run_on_tester(self, waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28452 # Currently, the only reason a test should not run on a given tester is that
453 # it's in the exceptions. (Once the GPU waterfall generation script is
454 # incorporated here, the rules will become more complex.)
Garrett Beatyffe83c4f2023-09-08 19:07:37455 exception = self.get_exception_for_test(test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28456 if not exception:
457 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28458 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28459 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28460 if remove_from:
461 if tester_name in remove_from:
462 return False
463 # TODO(kbr): this code path was added for some tests (including
464 # android_webview_unittests) on one machine (Nougat Phone
465 # Tester) which exists with the same name on two waterfalls,
466 # chromium.android and chromium.fyi; the tests are run on one
467 # but not the other. Once the bots are all uniquely named (a
468 # different ongoing project) this code should be removed.
469 # TODO(kbr): add coverage.
470 return (tester_name + ' ' + waterfall['name']
471 not in remove_from) # pragma: no cover
472 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28473
Garrett Beatyffe83c4f2023-09-08 19:07:37474 def get_test_modifications(self, test, tester_name):
475 exception = self.get_exception_for_test(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28476 if not exception:
477 return None
Nico Weber79dc5f6852018-07-13 19:38:49478 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28479
Garrett Beatyffe83c4f2023-09-08 19:07:37480 def get_test_replacements(self, test, tester_name):
481 exception = self.get_exception_for_test(test)
Brian Sheedye6ea0ee2019-07-11 02:54:37482 if not exception:
483 return None
484 return exception.get('replacements', {}).get(tester_name)
485
Kenneth Russell8a386d42018-06-02 09:48:01486 def merge_command_line_args(self, arr, prefix, splitter):
487 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01488 idx = 0
489 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01490 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01491 while idx < len(arr):
492 flag = arr[idx]
493 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01494 if flag.startswith(prefix):
495 arg = flag[prefix_len:]
496 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01497 if first_idx < 0:
498 first_idx = idx
499 else:
500 delete_current_entry = True
501 if delete_current_entry:
502 del arr[idx]
503 else:
504 idx += 1
505 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01506 arr[first_idx] = prefix + splitter.join(accumulated_args)
507 return arr
508
509 def maybe_fixup_args_array(self, arr):
510 # The incoming array of strings may be an array of command line
511 # arguments. To make it easier to turn on certain features per-bot or
512 # per-test-suite, look specifically for certain flags and merge them
513 # appropriately.
514 # --enable-features=Feature1 --enable-features=Feature2
515 # are merged to:
516 # --enable-features=Feature1,Feature2
517 # and:
518 # --extra-browser-args=arg1 --extra-browser-args=arg2
519 # are merged to:
520 # --extra-browser-args=arg1 arg2
521 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
522 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Yuly Novikov8c487e72020-10-16 20:00:29523 arr = self.merge_command_line_args(arr, '--test-launcher-filter-file=', ';')
Cameron Higgins971f0b92023-01-03 18:05:09524 arr = self.merge_command_line_args(arr, '--extra-app-args=', ',')
Kenneth Russell650995a2018-05-03 21:17:01525 return arr
526
Brian Sheedy910cda82022-07-19 11:58:34527 def substitute_magic_args(self, test_config, tester_name, tester_config):
Brian Sheedya31578e2020-05-18 20:24:36528 """Substitutes any magic substitution args present in |test_config|.
529
530 Substitutions are done in-place.
531
532 See buildbot_json_magic_substitutions.py for more information on this
533 feature.
534
535 Args:
536 test_config: A dict containing a configuration for a specific test on
537 a specific builder, e.g. the output of update_and_cleanup_test.
Brian Sheedy5f173bb2021-11-24 00:45:54538 tester_name: A string containing the name of the tester that |test_config|
539 came from.
Brian Sheedy910cda82022-07-19 11:58:34540 tester_config: A dict containing the configuration for the builder that
541 |test_config| is for.
Brian Sheedya31578e2020-05-18 20:24:36542 """
543 substituted_array = []
Brian Sheedyba13cf522022-09-13 21:00:09544 original_args = test_config.get('args', [])
545 for arg in original_args:
Brian Sheedya31578e2020-05-18 20:24:36546 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
547 function = arg.replace(
548 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
549 if hasattr(magic_substitutions, function):
550 substituted_array.extend(
Brian Sheedy910cda82022-07-19 11:58:34551 getattr(magic_substitutions, function)(test_config, tester_name,
552 tester_config))
Brian Sheedya31578e2020-05-18 20:24:36553 else:
554 raise BBGenErr(
555 'Magic substitution function %s does not exist' % function)
556 else:
557 substituted_array.append(arg)
Brian Sheedyba13cf522022-09-13 21:00:09558 if substituted_array != original_args:
Brian Sheedya31578e2020-05-18 20:24:36559 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
560
Garrett Beaty8d6708c2023-07-20 17:20:41561 def dictionary_merge(self, a, b, path=None):
Kenneth Russelleb60cbd22017-12-05 07:54:28562 """http://stackoverflow.com/questions/7204805/
563 python-dictionaries-of-dictionaries-merge
564 merges b into a
565 """
566 if path is None:
567 path = []
568 for key in b:
Garrett Beaty8d6708c2023-07-20 17:20:41569 if key not in a:
570 if b[key] is not None:
571 a[key] = b[key]
572 continue
573
574 if isinstance(a[key], dict) and isinstance(b[key], dict):
575 self.dictionary_merge(a[key], b[key], path + [str(key)])
576 elif a[key] == b[key]:
577 pass # same leaf value
578 elif isinstance(a[key], list) and isinstance(b[key], list):
Garrett Beatyade673d2023-08-04 22:00:25579 a[key] = a[key] + b[key]
580 if key.endswith('args'):
581 a[key] = self.maybe_fixup_args_array(a[key])
Garrett Beaty8d6708c2023-07-20 17:20:41582 elif b[key] is None:
583 del a[key]
584 else:
Kenneth Russelleb60cbd22017-12-05 07:54:28585 a[key] = b[key]
Garrett Beaty8d6708c2023-07-20 17:20:41586
Kenneth Russelleb60cbd22017-12-05 07:54:28587 return a
588
Garrett Beatyafb41c92024-04-24 17:00:37589 def initialize_args_for_test(self, generated_test, tester_config):
John Budorickab108712018-09-01 00:12:21590 args = []
591 args.extend(generated_test.get('args', []))
592 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22593
Kenneth Russell8a386d42018-06-02 09:48:01594 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21595 val = generated_test.pop(key, [])
596 if fn(tester_config):
597 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01598
599 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
Brian Sheedy781c8ca42021-03-08 22:03:21600 add_conditional_args('lacros_args', self.is_lacros)
Kenneth Russell8a386d42018-06-02 09:48:01601 add_conditional_args('linux_args', self.is_linux)
602 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36603 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49604 add_conditional_args('mac_args', self.is_mac)
605 add_conditional_args('win_args', self.is_win)
606 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01607
John Budorickab108712018-09-01 00:12:21608 if args:
609 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01610
Kenneth Russelleb60cbd22017-12-05 07:54:28611 def initialize_swarming_dictionary_for_test(self, generated_test,
612 tester_config):
613 if 'swarming' not in generated_test:
614 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28615 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
616 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38617 'can_use_on_swarming_builders': tester_config.get('use_swarming',
618 True)
Dirk Pranke81ff51c2017-12-09 19:24:28619 })
Kenneth Russelleb60cbd22017-12-05 07:54:28620 if 'swarming' in tester_config:
Kenneth Russelleb60cbd22017-12-05 07:54:28621 self.dictionary_merge(generated_test['swarming'],
622 tester_config['swarming'])
Brian Sheedybc984e242021-04-21 23:44:51623 # Apply any platform-specific Swarming dimensions after the generic ones.
Kenneth Russelleb60cbd22017-12-05 07:54:28624 if 'android_swarming' in generated_test:
625 if self.is_android(tester_config): # pragma: no cover
626 self.dictionary_merge(
627 generated_test['swarming'],
628 generated_test['android_swarming']) # pragma: no cover
629 del generated_test['android_swarming'] # pragma: no cover
Brian Sheedybc984e242021-04-21 23:44:51630 if 'chromeos_swarming' in generated_test:
631 if self.is_chromeos(tester_config): # pragma: no cover
632 self.dictionary_merge(
633 generated_test['swarming'],
634 generated_test['chromeos_swarming']) # pragma: no cover
635 del generated_test['chromeos_swarming'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28636
637 def clean_swarming_dictionary(self, swarming_dict):
638 # Clean out redundant entries from a test's "swarming" dictionary.
639 # This is really only needed to retain 100% parity with the
640 # handwritten JSON files, and can be removed once all the files are
641 # autogenerated.
642 if 'shards' in swarming_dict:
643 if swarming_dict['shards'] == 1: # pragma: no cover
644 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24645 if 'hard_timeout' in swarming_dict:
646 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
647 del swarming_dict['hard_timeout'] # pragma: no cover
Garrett Beatybb18d532023-06-26 22:16:33648 del swarming_dict['can_use_on_swarming_builders']
Kenneth Russelleb60cbd22017-12-05 07:54:28649
Stephen Martinis0382bc12018-09-17 22:29:07650 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
651 waterfall):
652 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01653 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07654 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28655 # See if there are any exceptions that need to be merged into this
656 # test's specification.
Garrett Beatyffe83c4f2023-09-08 19:07:37657 modifications = self.get_test_modifications(test, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28658 if modifications:
659 test = self.dictionary_merge(test, modifications)
Garrett Beatybfeff8f2023-06-16 18:57:25660 if (swarming_dict := test.get('swarming')) is not None:
Garrett Beatybb18d532023-06-26 22:16:33661 if swarming_dict.get('can_use_on_swarming_builders'):
Garrett Beatybfeff8f2023-06-16 18:57:25662 self.clean_swarming_dictionary(swarming_dict)
663 else:
664 del test['swarming']
Ben Pastenee012aea42019-05-14 22:32:28665 # Ensure all Android Swarming tests run only on userdebug builds if another
666 # build type was not specified.
667 if 'swarming' in test and self.is_android(tester_config):
Garrett Beatyade673d2023-08-04 22:00:25668 dimensions = test.get('swarming', {}).get('dimensions', {})
669 if (dimensions.get('os') == 'Android'
670 and not dimensions.get('device_os_type')):
671 dimensions['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37672 self.replace_test_args(test, test_name, tester_name)
Garrett Beatyafd33e0f2023-06-23 20:47:57673 if 'args' in test and not test['args']:
674 test.pop('args')
Ben Pastenee012aea42019-05-14 22:32:28675
Kenneth Russelleb60cbd22017-12-05 07:54:28676 return test
677
Brian Sheedye6ea0ee2019-07-11 02:54:37678 def replace_test_args(self, test, test_name, tester_name):
Garrett Beatyffe83c4f2023-09-08 19:07:37679 replacements = self.get_test_replacements(test, tester_name) or {}
Brian Sheedye6ea0ee2019-07-11 02:54:37680 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23681 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37682 if key not in valid_replacement_keys:
683 raise BBGenErr(
684 'Given replacement key %s for %s on %s is not in the list of valid '
685 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23686 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37687 found_key = False
688 for i, test_key in enumerate(test.get(key, [])):
689 # Handle both the key/value being replaced being defined as two
690 # separate items or as key=value.
691 if test_key == replacement_key:
692 found_key = True
693 # Handle flags without values.
694 if replacement_val == None:
695 del test[key][i]
696 else:
697 test[key][i+1] = replacement_val
698 break
Joshua Hood56c673c2022-03-02 20:29:33699 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37700 found_key = True
701 if replacement_val == None:
702 del test[key][i]
703 else:
704 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
705 break
706 if not found_key:
707 raise BBGenErr('Could not find %s in existing list of values for key '
708 '%s in %s on %s' % (replacement_key, key, test_name,
709 tester_name))
710
Shenghua Zhangaba8bad2018-02-07 02:12:09711 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05712 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26713 True):
714 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06715 # are targeting CrOS hardware and so need the special trigger script.
Garrett Beatyade673d2023-08-04 22:00:25716 if 'device_type' in test.get('swarming', {}).get('dimensions', {}):
Ben Pastenea9e583b2019-01-16 02:57:26717 test['trigger_script'] = {
718 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
719 }
Shenghua Zhangaba8bad2018-02-07 02:12:09720
Garrett Beatyffe83c4f2023-09-08 19:07:37721 def add_android_presentation_args(self, tester_config, result):
John Budorick262ae112019-07-12 19:24:38722 bucket = tester_config.get('results_bucket', 'chromium-result-details')
Garrett Beaty94af4272024-04-17 18:06:14723 result.setdefault('args', []).append('--gs-results-bucket=%s' % bucket)
724
725 if ('swarming' in result and 'merge' not in 'result'
726 and not tester_config.get('skip_merge_script', False)):
Ben Pastene858f4be2019-01-09 23:52:09727 result['merge'] = {
Garrett Beatyffe83c4f2023-09-08 19:07:37728 'args': [
729 '--bucket',
730 bucket,
731 '--test-name',
732 result['name'],
733 ],
734 'script': ('//build/android/pylib/results/presentation/'
735 'test_results_presentation.py'),
Ben Pastene858f4be2019-01-09 23:52:09736 }
Ben Pastene858f4be2019-01-09 23:52:09737
Kenneth Russelleb60cbd22017-12-05 07:54:28738 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
739 test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37740 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28741 return None
742 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37743 # Use test_name here instead of test['name'] because test['name'] will be
744 # modified with the variant identifier in a matrix compound suite
745 result.setdefault('test', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28746 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21747
Garrett Beatyafb41c92024-04-24 17:00:37748 self.initialize_args_for_test(result, tester_config)
Garrett Beaty94af4272024-04-17 18:06:14749 result = self.update_and_cleanup_test(result, test_name, tester_name,
750 tester_config, waterfall)
751 if self.is_android(tester_config) and 'swarming' in result:
752 if not result.get('use_isolated_scripts_api', False):
Alison Gale71bd8f152024-04-26 22:38:20753 # TODO(crbug.com/40725094) make Android presentation work with
Yuly Novikov26dd47052021-02-11 00:57:14754 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37755 self.add_android_presentation_args(tester_config, result)
Yuly Novikov26dd47052021-02-11 00:57:14756 result['args'] = result.get('args', []) + ['--recover-devices']
Shenghua Zhangaba8bad2018-02-07 02:12:09757 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34758 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43759
Garrett Beatybb18d532023-06-26 22:16:33760 if 'swarming' in result and not result.get('merge'):
Jamie Madilla8be0d72020-10-02 05:24:04761 if test_config.get('use_isolated_scripts_api', False):
762 merge_script = 'standard_isolated_script_merge'
763 else:
764 merge_script = 'standard_gtest_merge'
765
Stephen Martinisbc7b7772019-05-01 22:01:43766 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04767 'script': '//testing/merge_scripts/%s.py' % merge_script,
Stephen Martinisbc7b7772019-05-01 22:01:43768 }
Kenneth Russelleb60cbd22017-12-05 07:54:28769 return result
770
771 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
772 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37773 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28774 return None
775 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37776 # Use test_name here instead of test['name'] because test['name'] will be
777 # modified with the variant identifier in a matrix compound suite
Garrett Beatydca3d882023-09-14 23:50:32778 result.setdefault('test', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28779 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01780 self.initialize_args_for_test(result, tester_config)
Garrett Beaty94af4272024-04-17 18:06:14781 result = self.update_and_cleanup_test(result, test_name, tester_name,
782 tester_config, waterfall)
783 if self.is_android(tester_config) and 'swarming' in result:
Yuly Novikov26dd47052021-02-11 00:57:14784 if tester_config.get('use_android_presentation', False):
Alison Gale71bd8f152024-04-26 22:38:20785 # TODO(crbug.com/40725094) make Android presentation work with
Yuly Novikov26dd47052021-02-11 00:57:14786 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37787 self.add_android_presentation_args(tester_config, result)
Shenghua Zhangaba8bad2018-02-07 02:12:09788 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34789 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17790
Garrett Beatybb18d532023-06-26 22:16:33791 if 'swarming' in result and not result.get('merge'):
Alison Gale923a33e2024-04-22 23:34:28792 # TODO(crbug.com/41456107): Consider adding the ability to not have
Stephen Martinisf50047062019-05-06 22:26:17793 # this default.
794 result['merge'] = {
795 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
Stephen Martinisf50047062019-05-06 22:26:17796 }
Kenneth Russelleb60cbd22017-12-05 07:54:28797 return result
798
799 def generate_script_test(self, waterfall, tester_name, tester_config,
800 test_name, test_config):
Alison Gale47d1537d2024-04-19 21:31:46801 # TODO(crbug.com/40623237): Remove this check whenever a better
Brian Sheedy158cd0f2019-04-26 01:12:44802 # long-term solution is implemented.
803 if (waterfall.get('forbid_script_tests', False) or
804 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
805 raise BBGenErr('Attempted to generate a script test on tester ' +
806 tester_name + ', which explicitly forbids script tests')
Garrett Beatyffe83c4f2023-09-08 19:07:37807 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28808 return None
809 result = {
Garrett Beatyffe83c4f2023-09-08 19:07:37810 'name': test_config['name'],
811 'script': test_config['script'],
Kenneth Russelleb60cbd22017-12-05 07:54:28812 }
Stephen Martinis0382bc12018-09-17 22:29:07813 result = self.update_and_cleanup_test(
814 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34815 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28816 return result
817
818 def generate_junit_test(self, waterfall, tester_name, tester_config,
819 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37820 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28821 return None
John Budorickdef6acb2019-09-17 22:51:09822 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37823 # Use test_name here instead of test['name'] because test['name'] will be
824 # modified with the variant identifier in a matrix compound suite
825 result.setdefault('test', test_name)
John Budorickdef6acb2019-09-17 22:51:09826 self.initialize_args_for_test(result, tester_config)
827 result = self.update_and_cleanup_test(
828 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34829 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28830 return result
831
Xinan Lin05fb9c1752020-12-17 00:15:52832 def generate_skylab_test(self, waterfall, tester_name, tester_config,
833 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37834 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Xinan Lin05fb9c1752020-12-17 00:15:52835 return None
836 result = copy.deepcopy(test_config)
Brian Sheedy67937ad12024-03-06 22:53:55837 result.setdefault('test', test_name)
yoshiki iguchid1664ef2024-03-28 19:16:52838
839 if 'cros_board' in result or 'cros_board' in tester_config:
840 result['cros_board'] = tester_config.get('cros_board') or result.get(
841 'cros_board')
842 else:
843 raise BBGenErr("skylab tests must specify cros_board.")
844 if 'cros_model' in result or 'cros_model' in tester_config:
845 result['cros_model'] = tester_config.get('cros_model') or result.get(
846 'cros_model')
847 if 'dut_pool' in result or 'cros_dut_pool' in tester_config:
848 result['dut_pool'] = tester_config.get('cros_dut_pool') or result.get(
849 'dut_pool')
850
Xinan Lin05fb9c1752020-12-17 00:15:52851 self.initialize_args_for_test(result, tester_config)
852 result = self.update_and_cleanup_test(result, test_name, tester_name,
853 tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34854 self.substitute_magic_args(result, tester_name, tester_config)
Xinan Lin05fb9c1752020-12-17 00:15:52855 return result
856
Garrett Beaty65d44222023-08-01 17:22:11857 def substitute_gpu_args(self, tester_config, test, args):
Kenneth Russell8a386d42018-06-02 09:48:01858 substitutions = {
859 # Any machine in waterfalls.pyl which desires to run GPU tests
860 # must provide the os_type key.
861 'os_type': tester_config['os_type'],
862 'gpu_vendor_id': '0',
863 'gpu_device_id': '0',
864 }
Garrett Beatyade673d2023-08-04 22:00:25865 dimensions = test.get('swarming', {}).get('dimensions', {})
866 if 'gpu' in dimensions:
867 # First remove the driver version, then split into vendor and device.
868 gpu = dimensions['gpu']
869 if gpu != 'none':
870 gpu = gpu.split('-')[0].split(':')
871 substitutions['gpu_vendor_id'] = gpu[0]
872 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01873 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
874
875 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30876 test_name, test_config, is_android_webview,
Xinan Linedcf05b32023-10-19 23:13:50877 is_cast_streaming, is_skylab):
Kenneth Russell8a386d42018-06-02 09:48:01878 # These are all just specializations of isolated script tests with
879 # a bunch of boilerplate command line arguments added.
880
881 # The step name must end in 'test' or 'tests' in order for the
882 # results to automatically show up on the flakiness dashboard.
883 # (At least, this was true some time ago.) Continue to use this
884 # naming convention for the time being to minimize changes.
Garrett Beaty235c1412023-08-29 20:26:29885 #
886 # test name is the name of the test without the variant ID added
887 if not (test_name.endswith('test') or test_name.endswith('tests')):
888 raise BBGenErr(
889 f'telemetry test names must end with test or tests, got {test_name}')
Garrett Beatyffe83c4f2023-09-08 19:07:37890 result = self.generate_isolated_script_test(waterfall, tester_name,
891 tester_config, test_name,
892 test_config)
Kenneth Russell8a386d42018-06-02 09:48:01893 if not result:
894 return None
Garrett Beatydca3d882023-09-14 23:50:32895 result['test'] = test_config.get('test') or self.get_default_isolate_name(
896 tester_config, is_android_webview)
Chan Liab7d8dd82020-04-24 23:42:19897
Chan Lia3ad1502020-04-28 05:32:11898 # Populate test_id_prefix.
Garrett Beatydca3d882023-09-14 23:50:32899 gn_entry = self.gn_isolate_map[result['test']]
Chan Li17d969f92020-07-10 00:50:03900 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19901
Kenneth Russell8a386d42018-06-02 09:48:01902 args = result.get('args', [])
Garrett Beatyffe83c4f2023-09-08 19:07:37903 # Use test_name here instead of test['name'] because test['name'] will be
904 # modified with the variant identifier in a matrix compound suite
Kenneth Russell8a386d42018-06-02 09:48:01905 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14906
erikchen6da2d9b2018-08-03 23:01:14907 # These tests upload and download results from cloud storage and therefore
908 # aren't idempotent yet. https://crbug.com/549140.
Garrett Beatybfeff8f2023-06-16 18:57:25909 if 'swarming' in result:
910 result['swarming']['idempotent'] = False
erikchen6da2d9b2018-08-03 23:01:14911
Fabrice de Ganscbd655f2022-08-04 20:15:30912 browser = ''
913 if is_cast_streaming:
914 browser = 'cast-streaming-shell'
915 elif is_android_webview:
916 browser = 'android-webview-instrumentation'
917 else:
918 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:52919
Greg Thompsoncec7d8d2023-01-10 19:11:53920 extra_browser_args = []
921
Brian Sheedy4053a702020-07-28 02:09:52922 # Most platforms require --enable-logging=stderr to get useful browser logs.
923 # However, this actively messes with logging on CrOS (because Chrome's
924 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
925 # in order to see JavaScript console messages. See
926 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
Greg Thompsoncec7d8d2023-01-10 19:11:53927 if self.is_chromeos(tester_config):
928 extra_browser_args.append('--log-level=0')
929 elif not self.is_fuchsia(tester_config) or browser != 'fuchsia-chrome':
930 # Stderr logging is not needed for Chrome browser on Fuchsia, as ordinary
931 # logging via syslog is captured.
932 extra_browser_args.append('--enable-logging=stderr')
933
934 # --expose-gc allows the WebGL conformance tests to more reliably
935 # reproduce GC-related bugs in the V8 bindings.
936 extra_browser_args.append('--js-flags=--expose-gc')
Brian Sheedy4053a702020-07-28 02:09:52937
Xinan Linedcf05b32023-10-19 23:13:50938 # Skylab supports sharding, so reuse swarming's shard config.
939 if is_skylab and 'shards' not in result and test_config.get(
940 'swarming', {}).get('shards'):
941 result['shards'] = test_config['swarming']['shards']
942
Kenneth Russell8a386d42018-06-02 09:48:01943 args = [
Bo Liu555a0f92019-03-29 12:11:56944 test_to_run,
945 '--show-stdout',
946 '--browser=%s' % browser,
947 # --passthrough displays more of the logging in Telemetry when
948 # run via typ, in particular some of the warnings about tests
949 # being expected to fail, but passing.
950 '--passthrough',
951 '-v',
Brian Sheedy814e0482022-10-03 23:24:12952 '--stable-jobs',
Greg Thompsoncec7d8d2023-01-10 19:11:53953 '--extra-browser-args=%s' % ' '.join(extra_browser_args),
Brian Sheedy997e4802023-10-18 02:28:13954 '--enforce-browser-version',
Kenneth Russell8a386d42018-06-02 09:48:01955 ] + args
Garrett Beatybfeff8f2023-06-16 18:57:25956 result['args'] = self.maybe_fixup_args_array(
Garrett Beaty65d44222023-08-01 17:22:11957 self.substitute_gpu_args(tester_config, result, args))
Kenneth Russell8a386d42018-06-02 09:48:01958 return result
959
Brian Sheedyf74819b2021-06-04 01:38:38960 def get_default_isolate_name(self, tester_config, is_android_webview):
961 if self.is_android(tester_config):
962 if is_android_webview:
963 return 'telemetry_gpu_integration_test_android_webview'
964 return (
965 'telemetry_gpu_integration_test' +
966 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:33967 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:17968 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:33969 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:38970
Kenneth Russelleb60cbd22017-12-05 07:54:28971 def get_test_generator_map(self):
972 return {
Bo Liu555a0f92019-03-29 12:11:56973 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30974 GPUTelemetryTestGenerator(self, is_android_webview=True),
975 'cast_streaming_tests':
976 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:56977 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30978 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56979 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30980 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56981 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:30982 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56983 'junit_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30984 JUnitGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56985 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:30986 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:52987 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30988 SkylabGenerator(self),
Brian Sheedyb6491ba2022-09-26 20:49:49989 'skylab_gpu_telemetry_tests':
990 SkylabGPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28991 }
992
Kenneth Russell8a386d42018-06-02 09:48:01993 def get_test_type_remapper(self):
994 return {
Fabrice de Gans223272482022-08-08 16:56:57995 # These are a specialization of isolated_scripts with a bunch of
996 # boilerplate command line arguments added to each one.
997 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
998 'cast_streaming_tests': 'isolated_scripts',
999 'gpu_telemetry_tests': 'isolated_scripts',
Brian Sheedyb6491ba2022-09-26 20:49:491000 # These are the same as existing test types, just configured to run
1001 # in Skylab instead of via normal swarming.
1002 'skylab_gpu_telemetry_tests': 'skylab_tests',
Kenneth Russell8a386d42018-06-02 09:48:011003 }
1004
Jeff Yoon67c3e832020-02-08 07:39:381005 def check_composition_type_test_suites(self, test_type,
1006 additional_validators=None):
1007 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1008 validators = [check_compound_references,
1009 check_basic_references,
1010 check_conflicting_definitions]
1011 if additional_validators:
1012 validators += additional_validators
1013
1014 target_suites = self.test_suites.get(test_type, {})
1015 other_test_type = ('compound_suites'
1016 if test_type == 'matrix_compound_suites'
1017 else 'matrix_compound_suites')
1018 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011019 basic_suites = self.test_suites.get('basic_suites', {})
1020
Jamie Madillcf4f8c72021-05-20 19:24:231021 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011022 if suite in basic_suites:
1023 raise BBGenErr('%s names may not duplicate basic test suite names '
1024 '(error found while processsing %s)'
1025 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011026
Jeff Yoon67c3e832020-02-08 07:39:381027 seen_tests = {}
1028 for sub_suite in suite_def:
1029 for validator in validators:
1030 validator(
1031 basic_suites=basic_suites,
1032 other_test_suites=other_suites,
1033 seen_tests=seen_tests,
1034 sub_suite=sub_suite,
1035 suite=suite,
1036 suite_def=suite_def,
1037 target_test_suites=target_suites,
1038 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051039 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381040 )
Kenneth Russelleb60cbd22017-12-05 07:54:281041
Stephen Martinis54d64ad2018-09-21 22:16:201042 def flatten_test_suites(self):
1043 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011044 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1045 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231046 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011047 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201048 self.test_suites = new_test_suites
1049
Chan Lia3ad1502020-04-28 05:32:111050 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231051 for suite in self.test_suites['basic_suites'].values():
1052 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561053 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411054
Garrett Beatydca3d882023-09-14 23:50:321055 isolate_name = test.get('test') or key
Nodir Turakulovfce34292019-12-18 17:05:411056 gn_entry = self.gn_isolate_map.get(isolate_name)
1057 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281058 label = gn_entry['label']
1059
1060 if label.count(':') != 1:
1061 raise BBGenErr(
1062 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1063 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1064 (label, isolate_name))
1065 if label.split(':')[1] != isolate_name:
1066 raise BBGenErr(
1067 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1068 ' label "%s" see http://crbug.com/1071091 for details.' %
1069 (isolate_name, label))
1070
Chan Lia3ad1502020-04-28 05:32:111071 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411072 else: # pragma: no cover
1073 # Some tests do not have an entry gn_isolate_map.pyl, such as
1074 # telemetry tests.
Alison Gale47d1537d2024-04-19 21:31:461075 # TODO(crbug.com/40112160): require an entry in gn_isolate_map.
Nodir Turakulovfce34292019-12-18 17:05:411076 pass
1077
Kenneth Russelleb60cbd22017-12-05 07:54:281078 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011079 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201080
Jeff Yoon8154e582019-12-03 23:30:011081 compound_suites = self.test_suites.get('compound_suites', {})
1082 # check_composition_type_test_suites() checks that all basic suites
1083 # referenced by compound suites exist.
1084 basic_suites = self.test_suites.get('basic_suites')
1085
Jamie Madillcf4f8c72021-05-20 19:24:231086 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011087 # Resolve this to a dictionary.
1088 full_suite = {}
1089 for entry in value:
1090 suite = basic_suites[entry]
1091 full_suite.update(suite)
1092 compound_suites[name] = full_suite
1093
Jeff Yoon85fb8df2020-08-20 16:47:431094 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381095 """ Merge variant-defined configurations to each test case definition in a
1096 test suite.
1097
1098 The output maps a unique test name to an array of configurations because
1099 there may exist more than one definition for a test name using variants. The
1100 test name is referenced while mapping machines to test suites, so unpacking
1101 the array is done by the generators.
1102
1103 Args:
1104 basic_test_definition: a {} defined test suite in the format
1105 test_name:test_config
1106 variants: an [] of {} defining configurations to be applied to each test
1107 case in the basic test_definition
1108
1109 Return:
1110 a {} of test_name:[{}], where each {} is a merged configuration
1111 """
1112
1113 # Each test in a basic test suite will have a definition per variant.
1114 test_suite = {}
Garrett Beaty8d6708c2023-07-20 17:20:411115 for variant in variants:
1116 # Unpack the variant from variants.pyl if it's string based.
1117 if isinstance(variant, str):
1118 variant = self.variants[variant]
Jeff Yoonda581c32020-03-06 03:56:051119
Garrett Beaty8d6708c2023-07-20 17:20:411120 # If 'enabled' is set to False, we will not use this variant; otherwise if
1121 # the variant doesn't include 'enabled' variable or 'enabled' is set to
1122 # True, we will use this variant
1123 if not variant.get('enabled', True):
1124 continue
Jeff Yoon67c3e832020-02-08 07:39:381125
Garrett Beaty8d6708c2023-07-20 17:20:411126 # Make a shallow copy of the variant to remove variant-specific fields,
1127 # leaving just mixin fields
1128 variant = copy.copy(variant)
1129 variant.pop('enabled', None)
1130 identifier = variant.pop('identifier')
1131 variant_mixins = variant.pop('mixins', [])
1132 variant_skylab = variant.pop('skylab', {})
Jeff Yoon67c3e832020-02-08 07:39:381133
Garrett Beaty8d6708c2023-07-20 17:20:411134 for test_name, test_config in basic_test_definition.items():
1135 new_test = self.apply_mixin(variant, test_config)
Jeff Yoon67c3e832020-02-08 07:39:381136
Garrett Beaty8d6708c2023-07-20 17:20:411137 new_test['mixins'] = (test_config.get('mixins', []) + variant_mixins +
1138 mixins)
Xinan Lin05fb9c1752020-12-17 00:15:521139
Jeff Yoon67c3e832020-02-08 07:39:381140 # The identifier is used to make the name of the test unique.
1141 # Generators in the recipe uniquely identify a test by it's name, so we
1142 # don't want to have the same name for each variant.
Garrett Beaty235c1412023-08-29 20:26:291143 new_test['name'] = f'{test_name} {identifier}'
Ben Pastene5f231cf22022-05-05 18:03:071144
1145 # Attach the variant identifier to the test config so downstream
1146 # generators can make modifications based on the original name. This
1147 # is mainly used in generate_gpu_telemetry_test().
Garrett Beaty8d6708c2023-07-20 17:20:411148 new_test['variant_id'] = identifier
Ben Pastene5f231cf22022-05-05 18:03:071149
Garrett Beaty8d6708c2023-07-20 17:20:411150 for k, v in variant_skylab.items():
Sven Zheng22ba6312023-10-16 22:59:351151 # cros_chrome_version is the ash chrome version in the cros img in the
1152 # variant of cros_board. We don't want to include it in the final json
1153 # files; so remove it.
Garrett Beaty8d6708c2023-07-20 17:20:411154 if k != 'cros_chrome_version':
1155 new_test[k] = v
1156
Sven Zheng22ba6312023-10-16 22:59:351157 # For skylab, we need to pop the correct `autotest_name`. This field
1158 # defines what wrapper we use in OS infra. e.g. for gtest it's
1159 # https://source.chromium.org/chromiumos/chromiumos/codesearch/+/main:src/third_party/autotest/files/server/site_tests/chromium/chromium.py
1160 if variant_skylab and 'autotest_name' not in new_test:
1161 if 'tast_expr' in test_config:
1162 if 'lacros' in test_config['name']:
1163 new_test['autotest_name'] = 'tast.lacros-from-gcs'
1164 else:
1165 new_test['autotest_name'] = 'tast.chrome-from-gcs'
1166 elif 'benchmark' in test_config:
1167 new_test['autotest_name'] = 'chromium_Telemetry'
1168 else:
1169 new_test['autotest_name'] = 'chromium'
1170
Garrett Beaty8d6708c2023-07-20 17:20:411171 test_suite.setdefault(test_name, []).append(new_test)
1172
Jeff Yoon67c3e832020-02-08 07:39:381173 return test_suite
1174
Jeff Yoon8154e582019-12-03 23:30:011175 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381176 self.check_composition_type_test_suites('matrix_compound_suites',
1177 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011178
1179 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381180 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011181 # referenced by matrix suites exist.
1182 basic_suites = self.test_suites.get('basic_suites')
1183
Garrett Beaty235c1412023-08-29 20:26:291184 for matrix_suite_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011185 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381186
Jamie Madillcf4f8c72021-05-20 19:24:231187 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381188 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1189
Garrett Beaty235c1412023-08-29 20:26:291190 def update_tests(expanded):
1191 for test_name, new_tests in expanded.items():
1192 if not isinstance(new_tests, list):
1193 new_tests = [new_tests]
1194 tests_for_name = full_suite.setdefault(test_name, [])
1195 for t in new_tests:
1196 if t not in tests_for_name:
1197 tests_for_name.append(t)
1198
Garrett Beaty60a7b2a2023-09-13 23:00:401199 if (variants := mtx_test_suite_config.get('variants')):
Jeff Yoon85fb8df2020-08-20 16:47:431200 mixins = mtx_test_suite_config.get('mixins', [])
Garrett Beaty60a7b2a2023-09-13 23:00:401201 result = self.resolve_variants(basic_test_def, variants, mixins)
Garrett Beaty235c1412023-08-29 20:26:291202 update_tests(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271203 else:
1204 suite = basic_suites[test_suite]
Garrett Beaty235c1412023-08-29 20:26:291205 update_tests(suite)
1206 matrix_compound_suites[matrix_suite_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281207
1208 def link_waterfalls_to_test_suites(self):
1209 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231210 for tester_name, tester in waterfall['machines'].items():
1211 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281212 if not value in self.test_suites:
1213 # Hard / impossible to cover this in the unit test.
1214 raise self.unknown_test_suite(
1215 value, tester_name, waterfall['name']) # pragma: no cover
1216 tester['test_suites'][suite] = self.test_suites[value]
1217
1218 def load_configuration_files(self):
Garrett Beaty79339e182023-04-10 20:45:471219 self.waterfalls = self.load_pyl_file(self.args.waterfalls_pyl_path)
1220 self.test_suites = self.load_pyl_file(self.args.test_suites_pyl_path)
1221 self.exceptions = self.load_pyl_file(
1222 self.args.test_suite_exceptions_pyl_path)
1223 self.mixins = self.load_pyl_file(self.args.mixins_pyl_path)
1224 self.gn_isolate_map = self.load_pyl_file(self.args.gn_isolate_map_pyl_path)
Chong Guee622242020-10-28 18:17:351225 for isolate_map in self.args.isolate_map_files:
1226 isolate_map = self.load_pyl_file(isolate_map)
1227 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1228 if duplicates:
1229 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1230 ', '.join(duplicates))
1231 self.gn_isolate_map.update(isolate_map)
1232
Garrett Beaty79339e182023-04-10 20:45:471233 self.variants = self.load_pyl_file(self.args.variants_pyl_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281234
1235 def resolve_configuration_files(self):
Garrett Beaty235c1412023-08-29 20:26:291236 self.resolve_test_names()
Garrett Beatydca3d882023-09-14 23:50:321237 self.resolve_isolate_names()
Garrett Beaty65d44222023-08-01 17:22:111238 self.resolve_dimension_sets()
Chan Lia3ad1502020-04-28 05:32:111239 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281240 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011241 self.resolve_matrix_compound_test_suites()
1242 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281243 self.link_waterfalls_to_test_suites()
1244
Garrett Beaty235c1412023-08-29 20:26:291245 def resolve_test_names(self):
1246 for suite_name, suite in self.test_suites.get('basic_suites').items():
1247 for test_name, test in suite.items():
1248 if 'name' in test:
1249 raise BBGenErr(
1250 f'The name field is set in test {test_name} in basic suite '
1251 f'{suite_name}, this is not supported, the test name is the key '
1252 'within the basic suite')
Garrett Beatyffe83c4f2023-09-08 19:07:371253 # When a test is expanded with variants, this will be overwritten, but
1254 # this ensures every test definition has the name field set
1255 test['name'] = test_name
Garrett Beaty235c1412023-08-29 20:26:291256
Garrett Beatydca3d882023-09-14 23:50:321257 def resolve_isolate_names(self):
1258 for suite_name, suite in self.test_suites.get('basic_suites').items():
1259 for test_name, test in suite.items():
1260 if 'isolate_name' in test:
1261 raise BBGenErr(
1262 f'The isolate_name field is set in test {test_name} in basic '
1263 f'suite {suite_name}, the test field should be used instead')
1264
Garrett Beaty65d44222023-08-01 17:22:111265 def resolve_dimension_sets(self):
Garrett Beaty65d44222023-08-01 17:22:111266
1267 def definitions():
1268 for suite_name, suite in self.test_suites.get('basic_suites', {}).items():
1269 for test_name, test in suite.items():
1270 yield test, f'test {test_name} in basic suite {suite_name}'
1271
1272 for mixin_name, mixin in self.mixins.items():
1273 yield mixin, f'mixin {mixin_name}'
1274
1275 for waterfall in self.waterfalls:
1276 for builder_name, builder in waterfall.get('machines', {}).items():
1277 yield (
1278 builder,
1279 f'builder {builder_name} in waterfall {waterfall["name"]}',
1280 )
1281
1282 for test_name, exceptions in self.exceptions.items():
1283 modifications = exceptions.get('modifications', {})
1284 for builder_name, mods in modifications.items():
1285 yield (
1286 mods,
1287 f'exception for test {test_name} on builder {builder_name}',
1288 )
1289
1290 for definition, location in definitions():
1291 for swarming_attr in (
1292 'swarming',
1293 'android_swarming',
1294 'chromeos_swarming',
1295 ):
1296 if (swarming :=
1297 definition.get(swarming_attr)) and 'dimension_sets' in swarming:
Garrett Beatyade673d2023-08-04 22:00:251298 raise BBGenErr(
1299 f'dimension_sets is no longer supported (set in {location}),'
1300 ' instead, use set dimensions to a single dict')
Garrett Beaty65d44222023-08-01 17:22:111301
Nico Weberd18b8962018-05-16 19:39:381302 def unknown_bot(self, bot_name, waterfall_name):
1303 return BBGenErr(
1304 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1305
Kenneth Russelleb60cbd22017-12-05 07:54:281306 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1307 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381308 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281309 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1310
1311 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1312 return BBGenErr(
1313 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1314 ' on waterfall ' + waterfall_name)
1315
Stephen Martinisb72f6d22018-10-04 23:29:011316 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071317 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321318
1319 Checks in the waterfall, builder, and test objects for mixins.
1320 """
1321 def valid_mixin(mixin_name):
1322 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011323 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321324 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381325
Stephen Martinisb6a50492018-09-12 23:59:321326 def must_be_list(mixins, typ, name):
1327 """Asserts that given mixins are a list."""
1328 if not isinstance(mixins, list):
1329 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1330
Garrett Beatyffe83c4f2023-09-08 19:07:371331 test_name = test['name']
Brian Sheedy7658c982020-01-08 02:27:581332 remove_mixins = set()
1333 if 'remove_mixins' in builder:
1334 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1335 for rm in builder['remove_mixins']:
1336 valid_mixin(rm)
1337 remove_mixins.add(rm)
1338 if 'remove_mixins' in test:
1339 must_be_list(test['remove_mixins'], 'test', test_name)
1340 for rm in test['remove_mixins']:
1341 valid_mixin(rm)
1342 remove_mixins.add(rm)
1343 del test['remove_mixins']
1344
Stephen Martinisb72f6d22018-10-04 23:29:011345 if 'mixins' in waterfall:
1346 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1347 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581348 if mixin in remove_mixins:
1349 continue
Stephen Martinisb6a50492018-09-12 23:59:321350 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531351 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321352
Stephen Martinisb72f6d22018-10-04 23:29:011353 if 'mixins' in builder:
1354 must_be_list(builder['mixins'], 'builder', builder_name)
1355 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581356 if mixin in remove_mixins:
1357 continue
Stephen Martinisb6a50492018-09-12 23:59:321358 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531359 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321360
Stephen Martinisb72f6d22018-10-04 23:29:011361 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071362 return test
1363
Stephen Martinisb72f6d22018-10-04 23:29:011364 must_be_list(test['mixins'], 'test', test_name)
1365 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581366 # We don't bother checking if the given mixin is in remove_mixins here
1367 # since this is already the lowest level, so if a mixin is added here that
1368 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071369 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531370 test = self.apply_mixin(self.mixins[mixin], test, builder)
Jeff Yoon67c3e832020-02-08 07:39:381371 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071372 return test
Stephen Martinisb6a50492018-09-12 23:59:321373
Garrett Beaty8d6708c2023-07-20 17:20:411374 def apply_mixin(self, mixin, test, builder=None):
Stephen Martinisb72f6d22018-10-04 23:29:011375 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321376
Garrett Beaty4c35b142023-06-23 21:01:231377 A mixin is applied by copying all fields from the mixin into the
1378 test with the following exceptions:
1379 * For the various *args keys, the test's existing value (an empty
1380 list if not present) will be extended with the mixin's value.
1381 * The sub-keys of the swarming value will be copied to the test's
1382 swarming value with the following exceptions:
Garrett Beatyade673d2023-08-04 22:00:251383 * For the named_caches sub-keys, the test's existing value (an
1384 empty list if not present) will be extended with the mixin's
1385 value.
1386 * For the dimensions sub-key, the tests's existing value (an empty
1387 dict if not present) will be updated with the mixin's value.
Stephen Martinisb6a50492018-09-12 23:59:321388 """
Garrett Beaty4c35b142023-06-23 21:01:231389
Stephen Martinisb6a50492018-09-12 23:59:321390 new_test = copy.deepcopy(test)
1391 mixin = copy.deepcopy(mixin)
Garrett Beaty8d6708c2023-07-20 17:20:411392
1393 if 'description' in mixin:
1394 description = []
1395 if 'description' in new_test:
1396 description.append(new_test['description'])
1397 description.append(mixin.pop('description'))
1398 new_test['description'] = '\n'.join(description)
1399
Stephen Martinisb72f6d22018-10-04 23:29:011400 if 'swarming' in mixin:
1401 swarming_mixin = mixin['swarming']
1402 new_test.setdefault('swarming', {})
Stephen Martinisb72f6d22018-10-04 23:29:011403 if 'dimensions' in swarming_mixin:
Garrett Beatyade673d2023-08-04 22:00:251404 new_test['swarming'].setdefault('dimensions', {}).update(
1405 swarming_mixin.pop('dimensions'))
Garrett Beaty4c35b142023-06-23 21:01:231406 if 'named_caches' in swarming_mixin:
1407 new_test['swarming'].setdefault('named_caches', []).extend(
1408 swarming_mixin['named_caches'])
1409 del swarming_mixin['named_caches']
Stephen Martinisb72f6d22018-10-04 23:29:011410 # python dict update doesn't do recursion at all. Just hard code the
1411 # nested update we need (mixin['swarming'] shouldn't clobber
1412 # test['swarming'], but should update it).
1413 new_test['swarming'].update(swarming_mixin)
1414 del mixin['swarming']
1415
Garrett Beaty4c35b142023-06-23 21:01:231416 # Array so we can assign to it in a nested scope.
1417 args_need_fixup = ['args' in mixin]
1418
1419 for a in (
1420 'args',
1421 'precommit_args',
1422 'non_precommit_args',
1423 'desktop_args',
1424 'lacros_args',
1425 'linux_args',
1426 'android_args',
1427 'chromeos_args',
1428 'mac_args',
1429 'win_args',
1430 'win64_args',
1431 ):
1432 if (value := mixin.pop(a, None)) is None:
1433 continue
1434 if not isinstance(value, list):
1435 raise BBGenErr(f'"{a}" must be a list')
1436 new_test.setdefault(a, []).extend(value)
1437
Garrett Beaty4c35b142023-06-23 21:01:231438 args = new_test.get('args', [])
Austin Eng148d9f0f2022-02-08 19:18:531439
Garrett Beaty4c35b142023-06-23 21:01:231440 def add_conditional_args(key, fn):
Garrett Beaty8d6708c2023-07-20 17:20:411441 if builder is None:
1442 return
Garrett Beaty4c35b142023-06-23 21:01:231443 val = new_test.pop(key, [])
1444 if val and fn(builder):
1445 args.extend(val)
1446 args_need_fixup[0] = True
Austin Eng148d9f0f2022-02-08 19:18:531447
Garrett Beaty4c35b142023-06-23 21:01:231448 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
1449 add_conditional_args('lacros_args', self.is_lacros)
1450 add_conditional_args('linux_args', self.is_linux)
1451 add_conditional_args('android_args', self.is_android)
1452 add_conditional_args('chromeos_args', self.is_chromeos)
1453 add_conditional_args('mac_args', self.is_mac)
1454 add_conditional_args('win_args', self.is_win)
1455 add_conditional_args('win64_args', self.is_win64)
1456
1457 if args_need_fixup[0]:
1458 new_test['args'] = self.maybe_fixup_args_array(args)
Wezc0e835b702018-10-30 00:38:411459
Stephen Martinisb72f6d22018-10-04 23:29:011460 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321461 return new_test
1462
Greg Gutermanf60eb052020-03-12 17:40:011463 def generate_output_tests(self, waterfall):
1464 """Generates the tests for a waterfall.
1465
1466 Args:
1467 waterfall: a dictionary parsed from a master pyl file
1468 Returns:
1469 A dictionary mapping builders to test specs
1470 """
1471 return {
Jamie Madillcf4f8c72021-05-20 19:24:231472 name: self.get_tests_for_config(waterfall, name, config)
1473 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011474 }
1475
1476 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531477 generator_map = self.get_test_generator_map()
1478 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281479
Greg Gutermanf60eb052020-03-12 17:40:011480 tests = {}
1481 # Copy only well-understood entries in the machine's configuration
1482 # verbatim into the generated JSON.
1483 if 'additional_compile_targets' in config:
1484 tests['additional_compile_targets'] = config[
1485 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231486 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011487 if test_type not in generator_map:
1488 raise self.unknown_test_suite_type(
1489 test_type, name, waterfall['name']) # pragma: no cover
1490 test_generator = generator_map[test_type]
1491 # Let multiple kinds of generators generate the same kinds
1492 # of tests. For example, gpu_telemetry_tests are a
1493 # specialization of isolated_scripts.
1494 new_tests = test_generator.generate(
1495 waterfall, name, config, input_tests)
1496 remapped_test_type = test_type_remapper.get(test_type, test_type)
Garrett Beatyffe83c4f2023-09-08 19:07:371497 tests.setdefault(remapped_test_type, []).extend(new_tests)
1498
1499 for test_type, tests_for_type in tests.items():
1500 if test_type == 'additional_compile_targets':
1501 continue
1502 tests[test_type] = sorted(tests_for_type, key=lambda t: t['name'])
Greg Gutermanf60eb052020-03-12 17:40:011503
1504 return tests
1505
1506 def jsonify(self, all_tests):
1507 return json.dumps(
1508 all_tests, indent=2, separators=(',', ': '),
1509 sort_keys=True) + '\n'
1510
1511 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281512 self.load_configuration_files()
1513 self.resolve_configuration_files()
1514 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011515 result = collections.defaultdict(dict)
1516
Stephanie Kim572b43c02023-04-13 14:24:131517 if os.path.exists(self.args.autoshard_exceptions_json_path):
1518 autoshards = json.loads(
1519 self.read_file(self.args.autoshard_exceptions_json_path))
1520 else:
1521 autoshards = {}
1522
Dirk Pranke6269d302020-10-01 00:14:391523 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011524 for waterfall in self.waterfalls:
1525 for field in required_fields:
1526 # Verify required fields
1527 if field not in waterfall:
1528 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1529
1530 # Handle filter flag, if specified
1531 if filters and waterfall['name'] not in filters:
1532 continue
1533
1534 # Join config files and hardcoded values together
1535 all_tests = self.generate_output_tests(waterfall)
1536 result[waterfall['name']] = all_tests
1537
Stephanie Kim572b43c02023-04-13 14:24:131538 if not autoshards:
1539 continue
1540 for builder, test_spec in all_tests.items():
1541 for target_type, test_list in test_spec.items():
1542 if target_type == 'additional_compile_targets':
1543 continue
1544 for test_dict in test_list:
1545 # Suites that apply variants or other customizations will create
1546 # test_dicts that have "name" value that is different from the
Garrett Beatyffe83c4f2023-09-08 19:07:371547 # "test" value.
Stephanie Kim572b43c02023-04-13 14:24:131548 # e.g. name = vulkan_swiftshader_content_browsertests, but
1549 # test = content_browsertests and
1550 # test_id_prefix = "ninja://content/test:content_browsertests/"
Garrett Beatyffe83c4f2023-09-08 19:07:371551 test_name = test_dict['name']
Stephanie Kim572b43c02023-04-13 14:24:131552 shard_info = autoshards.get(waterfall['name'],
1553 {}).get(builder, {}).get(test_name)
1554 if shard_info:
1555 test_dict['swarming'].update(
1556 {'shards': int(shard_info['shards'])})
1557
Greg Gutermanf60eb052020-03-12 17:40:011558 # Add do not edit warning
1559 for tests in result.values():
1560 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1561 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1562
1563 return result
1564
1565 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281566 suffix = '.json'
1567 if self.args.new_files:
1568 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011569
1570 for filename, contents in result.items():
1571 jsonstr = self.jsonify(contents)
Garrett Beaty79339e182023-04-10 20:45:471572 file_path = os.path.join(self.args.output_dir, filename + suffix)
1573 self.write_file(file_path, jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281574
Nico Weberd18b8962018-05-16 19:39:381575 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161576 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421577 # NOTE: This reference can cause issues; if a file changes there, the
1578 # presubmit here won't be run by default. A manually maintained list there
1579 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1580 # references to configs outside of this directory are added, please change
1581 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1582 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491583
Garrett Beaty7e866fc2021-06-16 14:12:101584 # Get the generated project.pyl so we can check if we should be enforcing
1585 # that the specs are for builders that actually exist
1586 # If not, return None to indicate that we won't enforce that builders in
1587 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491588 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1589 'project.pyl')
1590 if os.path.exists(project_pyl_path):
1591 settings = ast.literal_eval(self.read_file(project_pyl_path))
1592 if not settings.get('validate_source_side_specs_have_builder', True):
1593 return None
1594
Nico Weberd18b8962018-05-16 19:39:381595 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311596 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161597 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1598 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431599 for c in milo_configs:
1600 for l in self.read_file(c).splitlines():
1601 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311602 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431603 continue
1604 # l looks like
1605 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1606 # Extract win_chromium_dbg_ng part.
1607 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381608 return bot_names
1609
Ben Pastene9a010082019-09-25 20:41:371610 def get_internal_waterfalls(self):
1611 # Similar to get_builders_that_do_not_actually_exist above, but for
1612 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201613 return [
Kramer Ge3bf853a2023-04-13 19:39:471614 'chrome', 'chrome.pgo', 'chrome.gpu.fyi', 'internal.chrome.fyi',
yoshiki iguchi4de608082024-03-14 00:33:361615 'internal.chromeos.fyi', 'internal.optimization_guide', 'internal.soda',
1616 'chromeos.preuprev'
Yuke Liaoe6c23dd2021-07-28 16:12:201617 ]
Ben Pastene9a010082019-09-25 20:41:371618
Stephen Martinisf83893722018-09-19 00:02:181619 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201620 self.check_input_files_sorting(verbose)
1621
Kenneth Russelleb60cbd22017-12-05 07:54:281622 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011623 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381624 self.check_composition_type_test_suites('matrix_compound_suites',
1625 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111626 self.resolve_test_id_prefixes()
Garrett Beaty1ead4a52023-12-07 19:16:421627
1628 # All test suites must be referenced. Check this before flattening the test
1629 # suites so that we can transitively check the basic suites for compound
1630 # suites and matrix compound suites (otherwise we would determine a basic
1631 # suite is used if it shared a name with a test present in a basic suite
1632 # that is used).
1633 all_suites = set(
1634 itertools.chain(*(self.test_suites.get(a, {}) for a in (
1635 'basic_suites',
1636 'compound_suites',
1637 'matrix_compound_suites',
1638 ))))
1639 unused_suites = set(all_suites)
1640 generator_map = self.get_test_generator_map()
1641 for waterfall in self.waterfalls:
1642 for bot_name, tester in waterfall['machines'].items():
1643 for suite_type, suite in tester.get('test_suites', {}).items():
1644 if suite_type not in generator_map:
1645 raise self.unknown_test_suite_type(suite_type, bot_name,
1646 waterfall['name'])
1647 if suite not in all_suites:
1648 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1649 unused_suites.discard(suite)
1650 # For each compound suite or matrix compound suite, if the suite was used,
1651 # remove all of the basic suites that it composes from the set of unused
1652 # suites
1653 for a in ('compound_suites', 'matrix_compound_suites'):
1654 for suite, sub_suites in self.test_suites.get(a, {}).items():
1655 if suite not in unused_suites:
1656 unused_suites.difference_update(sub_suites)
1657 if unused_suites:
1658 raise BBGenErr('The following test suites were unreferenced by bots on '
1659 'the waterfalls: ' + str(unused_suites))
1660
Stephen Martinis54d64ad2018-09-21 22:16:201661 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381662
1663 # All bots should exist.
1664 bot_names = self.get_valid_bot_names()
Garrett Beaty2a02de3c2020-05-15 13:57:351665 if bot_names is not None:
1666 internal_waterfalls = self.get_internal_waterfalls()
1667 for waterfall in self.waterfalls:
Alison Gale923a33e2024-04-22 23:34:281668 # TODO(crbug.com/41474799): Remove the need for this exception.
Garrett Beaty2a02de3c2020-05-15 13:57:351669 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011670 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351671 for bot_name in waterfall['machines']:
Garrett Beaty2a02de3c2020-05-15 13:57:351672 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581673 if waterfall['name'] in [
1674 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1675 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351676 # TODO(thakis): Remove this once these bots move to luci.
1677 continue # pragma: no cover
1678 if waterfall['name'] in ['tryserver.webrtc',
1679 'webrtc.chromium.fyi.experimental']:
1680 # These waterfalls have their bot configs in a different repo.
1681 # so we don't know about their bot names.
1682 continue # pragma: no cover
1683 if waterfall['name'] in ['client.devtools-frontend.integration',
1684 'tryserver.devtools-frontend',
1685 'chromium.devtools-frontend']:
1686 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201687 if waterfall['name'] in ['client.openscreen.chromium']:
1688 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351689 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381690
Kenneth Russelleb60cbd22017-12-05 07:54:281691 # All test suite exceptions must refer to bots on the waterfall.
1692 all_bots = set()
1693 missing_bots = set()
1694 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231695 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281696 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281697 # In order to disambiguate between bots with the same name on
1698 # different waterfalls, support has been added to various
1699 # exceptions for concatenating the waterfall name after the bot
1700 # name.
1701 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231702 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381703 removals = (exception.get('remove_from', []) +
1704 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231705 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381706 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281707 if removal not in all_bots:
1708 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411709
Kenneth Russelleb60cbd22017-12-05 07:54:281710 if missing_bots:
1711 raise BBGenErr('The following nonexistent machines were referenced in '
1712 'the test suite exceptions: ' + str(missing_bots))
1713
Garrett Beatyb061e69d2023-06-27 16:15:351714 for name, mixin in self.mixins.items():
1715 if '$mixin_append' in mixin:
1716 raise BBGenErr(
1717 f'$mixin_append is no longer supported (set in mixin "{name}"),'
1718 ' args and named caches specified as normal will be appended')
1719
Stephen Martinis0382bc12018-09-17 22:29:071720 # All mixins must be referenced
1721 seen_mixins = set()
1722 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011723 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231724 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011725 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071726 for suite in self.test_suites.values():
1727 if isinstance(suite, list):
1728 # Don't care about this, it's a composition, which shouldn't include a
1729 # swarming mixin.
1730 continue
1731
1732 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561733 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011734 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071735
Zhaoyang Li9da047d52021-05-10 21:31:441736 for variant in self.variants:
1737 # Unpack the variant from variants.pyl if it's string based.
1738 if isinstance(variant, str):
1739 variant = self.variants[variant]
1740 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1741
Stephen Martinisb72f6d22018-10-04 23:29:011742 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071743 if missing_mixins:
1744 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1745 ' referenced in a waterfall, machine, or test suite.' % (
1746 str(missing_mixins)))
1747
Jeff Yoonda581c32020-03-06 03:56:051748 # All variant references must be referenced
1749 seen_variants = set()
1750 for suite in self.test_suites.values():
1751 if isinstance(suite, list):
1752 continue
1753
1754 for test in suite.values():
1755 if isinstance(test, dict):
1756 for variant in test.get('variants', []):
1757 if isinstance(variant, str):
1758 seen_variants.add(variant)
1759
1760 missing_variants = set(self.variants.keys()) - seen_variants
1761 if missing_variants:
1762 raise BBGenErr('The following variants were unreferenced: %s. They must '
1763 'be referenced in a matrix test suite under the variants '
1764 'key.' % str(missing_variants))
1765
Stephen Martinis54d64ad2018-09-21 22:16:201766
Garrett Beaty79339e182023-04-10 20:45:471767 def type_assert(self, node, typ, file_path, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201768 """Asserts that the Python AST node |node| is of type |typ|.
1769
1770 If verbose is set, it prints out some helpful context lines, showing where
1771 exactly the error occurred in the file.
1772 """
1773 if not isinstance(node, typ):
1774 if verbose:
Garrett Beaty79339e182023-04-10 20:45:471775 lines = [""] + self.read_file(file_path).splitlines()
Stephen Martinis54d64ad2018-09-21 22:16:201776
1777 context = 2
1778 lines_start = max(node.lineno - context, 0)
1779 # Add one to include the last line
1780 lines_end = min(node.lineno + context, len(lines)) + 1
Garrett Beaty79339e182023-04-10 20:45:471781 lines = itertools.chain(
1782 ['== %s ==\n' % file_path],
1783 ["<snip>\n"],
1784 [
1785 '%d %s' % (lines_start + i, line)
1786 for i, line in enumerate(lines[lines_start:lines_start +
1787 context])
1788 ],
1789 ['-' * 80 + '\n'],
1790 ['%d %s' % (node.lineno, lines[node.lineno])],
1791 [
1792 '-' * (node.col_offset + 3) + '^' + '-' *
1793 (80 - node.col_offset - 4) + '\n'
1794 ],
1795 [
1796 '%d %s' % (node.lineno + 1 + i, line)
1797 for i, line in enumerate(lines[node.lineno + 1:lines_end])
1798 ],
1799 ["<snip>\n"],
Stephen Martinis54d64ad2018-09-21 22:16:201800 )
1801 # Print out a useful message when a type assertion fails.
1802 for l in lines:
1803 self.print_line(l.strip())
1804
1805 node_dumped = ast.dump(node, annotate_fields=False)
1806 # If the node is huge, truncate it so everything fits in a terminal
1807 # window.
1808 if len(node_dumped) > 60: # pragma: no cover
1809 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1810 raise BBGenErr(
Garrett Beaty807011ab2023-04-12 00:52:391811 'Invalid .pyl file \'%s\'. Python AST node %r on line %s expected to'
Garrett Beaty79339e182023-04-10 20:45:471812 ' be %s, is %s' %
1813 (file_path, node_dumped, node.lineno, typ, type(node)))
Stephen Martinis54d64ad2018-09-21 22:16:201814
Garrett Beaty79339e182023-04-10 20:45:471815 def check_ast_list_formatted(self,
1816 keys,
1817 file_path,
1818 verbose,
Stephen Martinis1384ff92020-01-07 19:52:151819 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531820 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201821
Stephen Martinis5bef0fc2020-01-06 22:47:531822 Currently only checks to ensure they're correctly sorted, and that there
1823 are no duplicates.
1824
1825 Args:
1826 keys: An python list of AST nodes.
1827
1828 It's a list of AST nodes instead of a list of strings because
1829 when verbose is set, it tries to print out context of where the
1830 diffs are in the file.
Garrett Beaty79339e182023-04-10 20:45:471831 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531832 verbose: If set, print out diff information about how the keys are
1833 incorrectly formatted.
1834 check_sorting: If true, checks if the list is sorted.
1835 Returns:
1836 If the keys are correctly formatted.
1837 """
1838 if not keys:
1839 return True
1840
1841 assert isinstance(keys[0], ast.Str)
1842
1843 keys_strs = [k.s for k in keys]
1844 # Keys to diff against. Used below.
1845 keys_to_diff_against = None
1846 # If the list is properly formatted.
1847 list_formatted = True
1848
1849 # Duplicates are always bad.
1850 if len(set(keys_strs)) != len(keys_strs):
1851 list_formatted = False
1852 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1853
1854 if check_sorting and sorted(keys_strs) != keys_strs:
1855 list_formatted = False
1856 if list_formatted:
1857 return True
1858
1859 if verbose:
1860 line_num = keys[0].lineno
1861 keys = [k.s for k in keys]
1862 if check_sorting:
1863 # If we have duplicates, sorting this will take care of it anyways.
1864 keys_to_diff_against = sorted(set(keys))
1865 # else, keys_to_diff_against is set above already
1866
1867 self.print_line('=' * 80)
1868 self.print_line('(First line of keys is %s)' % line_num)
Garrett Beaty79339e182023-04-10 20:45:471869 for line in difflib.context_diff(keys,
1870 keys_to_diff_against,
1871 fromfile='current (%r)' % file_path,
1872 tofile='sorted',
1873 lineterm=''):
Stephen Martinis5bef0fc2020-01-06 22:47:531874 self.print_line(line)
1875 self.print_line('=' * 80)
1876
1877 return False
1878
Garrett Beaty79339e182023-04-10 20:45:471879 def check_ast_dict_formatted(self, node, file_path, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531880 """Checks if an ast dictionary's keys are correctly formatted.
1881
1882 Just a simple wrapper around check_ast_list_formatted.
1883 Args:
1884 node: An AST node. Assumed to be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471885 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531886 verbose: If set, print out diff information about how the keys are
1887 incorrectly formatted.
1888 check_sorting: If true, checks if the list is sorted.
1889 Returns:
1890 If the dictionary is correctly formatted.
1891 """
Stephen Martinis54d64ad2018-09-21 22:16:201892 keys = []
1893 # The keys of this dict are ordered as ordered in the file; normal python
1894 # dictionary keys are given an arbitrary order, but since we parsed the
1895 # file itself, the order as given in the file is preserved.
1896 for key in node.keys:
Garrett Beaty79339e182023-04-10 20:45:471897 self.type_assert(key, ast.Str, file_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531898 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201899
Garrett Beaty79339e182023-04-10 20:45:471900 return self.check_ast_list_formatted(keys, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181901
1902 def check_input_files_sorting(self, verbose=False):
Alison Gale923a33e2024-04-22 23:34:281903 # TODO(crbug.com/41415841): Add the ability for this script to
Stephen Martinis54d64ad2018-09-21 22:16:201904 # actually format the files, rather than just complain if they're
1905 # incorrectly formatted.
1906 bad_files = set()
Garrett Beaty79339e182023-04-10 20:45:471907
1908 def parse_file(file_path):
Stephen Martinis5bef0fc2020-01-06 22:47:531909 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201910
Stephen Martinis5bef0fc2020-01-06 22:47:531911 Returns an AST node representing the value in the pyl file."""
Garrett Beaty79339e182023-04-10 20:45:471912 parsed = ast.parse(self.read_file(file_path))
Stephen Martinisf83893722018-09-19 00:02:181913
Stephen Martinisf83893722018-09-19 00:02:181914 # Must be a module.
Garrett Beaty79339e182023-04-10 20:45:471915 self.type_assert(parsed, ast.Module, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181916 module = parsed.body
1917
1918 # Only one expression in the module.
Garrett Beaty79339e182023-04-10 20:45:471919 self.type_assert(module, list, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181920 if len(module) != 1: # pragma: no cover
Garrett Beaty79339e182023-04-10 20:45:471921 raise BBGenErr('Invalid .pyl file %s' % file_path)
Stephen Martinisf83893722018-09-19 00:02:181922 expr = module[0]
Garrett Beaty79339e182023-04-10 20:45:471923 self.type_assert(expr, ast.Expr, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181924
Stephen Martinis5bef0fc2020-01-06 22:47:531925 return expr.value
1926
1927 # Handle this separately
Garrett Beaty79339e182023-04-10 20:45:471928 value = parse_file(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531929 # Value should be a list.
Garrett Beaty79339e182023-04-10 20:45:471930 self.type_assert(value, ast.List, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531931
1932 keys = []
Joshua Hood56c673c2022-03-02 20:29:331933 for elm in value.elts:
Garrett Beaty79339e182023-04-10 20:45:471934 self.type_assert(elm, ast.Dict, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531935 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331936 for key, val in zip(elm.keys, elm.values):
Garrett Beaty79339e182023-04-10 20:45:471937 self.type_assert(key, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531938 if key.s == 'machines':
Garrett Beaty79339e182023-04-10 20:45:471939 if not self.check_ast_dict_formatted(
1940 val, self.args.waterfalls_pyl_path, verbose):
1941 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531942
1943 if key.s == "name":
Garrett Beaty79339e182023-04-10 20:45:471944 self.type_assert(val, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531945 waterfall_name = val
1946 assert waterfall_name
1947 keys.append(waterfall_name)
1948
Garrett Beaty79339e182023-04-10 20:45:471949 if not self.check_ast_list_formatted(keys, self.args.waterfalls_pyl_path,
1950 verbose):
1951 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531952
Garrett Beaty79339e182023-04-10 20:45:471953 for file_path in (
1954 self.args.mixins_pyl_path,
1955 self.args.test_suites_pyl_path,
1956 self.args.test_suite_exceptions_pyl_path,
Stephen Martinis5bef0fc2020-01-06 22:47:531957 ):
Garrett Beaty79339e182023-04-10 20:45:471958 value = parse_file(file_path)
Stephen Martinisf83893722018-09-19 00:02:181959 # Value should be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471960 self.type_assert(value, ast.Dict, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181961
Garrett Beaty79339e182023-04-10 20:45:471962 if not self.check_ast_dict_formatted(value, file_path, verbose):
1963 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531964
Garrett Beaty79339e182023-04-10 20:45:471965 if file_path == self.args.test_suites_pyl_path:
Jeff Yoon8154e582019-12-03 23:30:011966 expected_keys = ['basic_suites',
1967 'compound_suites',
1968 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201969 actual_keys = [node.s for node in value.keys]
1970 assert all(key in expected_keys for key in actual_keys), (
Garrett Beaty79339e182023-04-10 20:45:471971 'Invalid %r file; expected keys %r, got %r' %
1972 (file_path, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:331973 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:201974 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011975 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201976 for suite_group in suite_dicts:
Garrett Beaty79339e182023-04-10 20:45:471977 if not self.check_ast_dict_formatted(suite_group, file_path, verbose):
1978 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181979
Stephen Martinis5bef0fc2020-01-06 22:47:531980 for key, suite in zip(value.keys, value.values):
1981 # The compound suites are checked in
1982 # 'check_composition_type_test_suites()'
1983 if key.s == 'basic_suites':
1984 for group in suite.values:
Garrett Beaty79339e182023-04-10 20:45:471985 if not self.check_ast_dict_formatted(group, file_path, verbose):
1986 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531987 break
Stephen Martinis54d64ad2018-09-21 22:16:201988
Garrett Beaty79339e182023-04-10 20:45:471989 elif file_path == self.args.test_suite_exceptions_pyl_path:
Stephen Martinis5bef0fc2020-01-06 22:47:531990 # Check the values for each test.
1991 for test in value.values:
1992 for kind, node in zip(test.keys, test.values):
1993 if isinstance(node, ast.Dict):
Garrett Beaty79339e182023-04-10 20:45:471994 if not self.check_ast_dict_formatted(node, file_path, verbose):
1995 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531996 elif kind.s == 'remove_from':
1997 # Don't care about sorting; these are usually grouped, since the
1998 # same bug can affect multiple builders. Do want to make sure
1999 # there aren't duplicates.
Garrett Beaty79339e182023-04-10 20:45:472000 if not self.check_ast_list_formatted(
2001 node.elts, file_path, verbose, check_sorting=False):
2002 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:182003
2004 if bad_files:
2005 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:202006 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:532007 'unsorted, or have duplicates. Re-run this with --verbose to see '
2008 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:182009
Kenneth Russelleb60cbd22017-12-05 07:54:282010 def check_output_file_consistency(self, verbose=False):
2011 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012012 # All waterfalls/bucket .json files must have been written
2013 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:282014 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012015 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:162016 outputs = self.generate_outputs()
2017 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:012018 expected = self.jsonify(expected_contents)
Garrett Beaty79339e182023-04-10 20:45:472019 file_path = os.path.join(self.args.output_dir, filename + '.json')
Ben Pastenef21cda32023-03-30 22:00:572020 current = self.read_file(file_path)
Kenneth Russelleb60cbd22017-12-05 07:54:282021 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:012022 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:322023 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:012024 self.print_line('File ' + filename +
2025 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:322026 'contents:')
2027 for line in difflib.unified_diff(
2028 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:502029 current.splitlines(),
2030 fromfile='expected', tofile='current'):
2031 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:012032
2033 if ungenerated_files:
2034 raise BBGenErr(
2035 'The following files have not been properly '
2036 'autogenerated by generate_buildbot_json.py: ' +
2037 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:282038
Dirk Pranke772f55f2021-04-28 04:51:162039 for builder_group, builders in outputs.items():
2040 for builder, step_types in builders.items():
Garrett Beatydca3d882023-09-14 23:50:322041 for test_type in ('gtest_tests', 'isolated_scripts'):
2042 for step_data in step_types.get(test_type, []):
2043 step_name = step_data['name']
2044 self._check_swarming_config(builder_group, builder, step_name,
2045 step_data)
Dirk Pranke772f55f2021-04-28 04:51:162046
2047 def _check_swarming_config(self, filename, builder, step_name, step_data):
Alison Gale47d1537d2024-04-19 21:31:462048 # TODO(crbug.com/40179524): Ensure all swarming tests specify cpu, not
Dirk Pranke772f55f2021-04-28 04:51:162049 # just mac tests.
Garrett Beatybb18d532023-06-26 22:16:332050 if 'swarming' in step_data:
Garrett Beatyade673d2023-08-04 22:00:252051 dimensions = step_data['swarming'].get('dimensions')
2052 if not dimensions:
Tatsuhisa Yamaguchif1878d52023-11-06 06:02:252053 raise BBGenErr('%s: %s / %s : dimensions must be specified for all '
Dirk Pranke772f55f2021-04-28 04:51:162054 'swarmed tests' % (filename, builder, step_name))
Garrett Beatyade673d2023-08-04 22:00:252055 if not dimensions.get('os'):
2056 raise BBGenErr('%s: %s / %s : os must be specified for all '
2057 'swarmed tests' % (filename, builder, step_name))
2058 if 'Mac' in dimensions.get('os') and not dimensions.get('cpu'):
2059 raise BBGenErr('%s: %s / %s : cpu must be specified for mac '
2060 'swarmed tests' % (filename, builder, step_name))
Dirk Pranke772f55f2021-04-28 04:51:162061
Kenneth Russelleb60cbd22017-12-05 07:54:282062 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502063 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282064 self.check_output_file_consistency(verbose) # pragma: no cover
2065
Karen Qiane24b7ee2019-02-12 23:37:062066 def does_test_match(self, test_info, params_dict):
2067 """Checks to see if the test matches the parameters given.
2068
2069 Compares the provided test_info with the params_dict to see
2070 if the bot matches the parameters given. If so, returns True.
2071 Else, returns false.
2072
2073 Args:
2074 test_info (dict): Information about a specific bot provided
2075 in the format shown in waterfalls.pyl
2076 params_dict (dict): Dictionary of parameters and their values
2077 to look for in the bot
2078 Ex: {
2079 'device_os':'android',
2080 '--flag':True,
2081 'mixins': ['mixin1', 'mixin2'],
2082 'ex_key':'ex_value'
2083 }
2084
2085 """
2086 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2087 'kvm', 'pool', 'integrity'] # dimension parameters
2088 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2089 'can_use_on_swarming_builders']
2090 for param in params_dict:
2091 # if dimension parameter
2092 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2093 if not 'swarming' in test_info:
2094 return False
2095 swarming = test_info['swarming']
2096 if param in SWARMING_PARAMS:
2097 if not param in swarming:
2098 return False
2099 if not str(swarming[param]) == params_dict[param]:
2100 return False
2101 else:
Garrett Beatyade673d2023-08-04 22:00:252102 if not 'dimensions' in swarming:
Karen Qiane24b7ee2019-02-12 23:37:062103 return False
Garrett Beatyade673d2023-08-04 22:00:252104 dimensions = swarming['dimensions']
Karen Qiane24b7ee2019-02-12 23:37:062105 # only looking at the first dimension set
Garrett Beatyade673d2023-08-04 22:00:252106 if not param in dimensions:
Karen Qiane24b7ee2019-02-12 23:37:062107 return False
Garrett Beatyade673d2023-08-04 22:00:252108 if not dimensions[param] == params_dict[param]:
Karen Qiane24b7ee2019-02-12 23:37:062109 return False
2110
2111 # if flag
2112 elif param.startswith('--'):
2113 if not 'args' in test_info:
2114 return False
2115 if not param in test_info['args']:
2116 return False
2117
2118 # not dimension parameter/flag/mixin
2119 else:
2120 if not param in test_info:
2121 return False
2122 if not test_info[param] == params_dict[param]:
2123 return False
2124 return True
2125 def error_msg(self, msg):
2126 """Prints an error message.
2127
2128 In addition to a catered error message, also prints
2129 out where the user can find more help. Then, program exits.
2130 """
2131 self.print_line(msg + (' If you need more information, ' +
2132 'please run with -h or --help to see valid commands.'))
2133 sys.exit(1)
2134
2135 def find_bots_that_run_test(self, test, bots):
2136 matching_bots = []
2137 for bot in bots:
2138 bot_info = bots[bot]
2139 tests = self.flatten_tests_for_bot(bot_info)
2140 for test_info in tests:
Garrett Beatyffe83c4f2023-09-08 19:07:372141 test_name = test_info['name']
Karen Qiane24b7ee2019-02-12 23:37:062142 if not test_name == test:
2143 continue
2144 matching_bots.append(bot)
2145 return matching_bots
2146
2147 def find_tests_with_params(self, tests, params_dict):
2148 matching_tests = []
2149 for test_name in tests:
2150 test_info = tests[test_name]
2151 if not self.does_test_match(test_info, params_dict):
2152 continue
2153 if not test_name in matching_tests:
2154 matching_tests.append(test_name)
2155 return matching_tests
2156
2157 def flatten_waterfalls_for_query(self, waterfalls):
2158 bots = {}
2159 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012160 waterfall_tests = self.generate_output_tests(waterfall)
2161 for bot in waterfall_tests:
2162 bot_info = waterfall_tests[bot]
2163 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062164 return bots
2165
2166 def flatten_tests_for_bot(self, bot_info):
2167 """Returns a list of flattened tests.
2168
2169 Returns a list of tests not grouped by test category
2170 for a specific bot.
2171 """
2172 TEST_CATS = self.get_test_generator_map().keys()
2173 tests = []
2174 for test_cat in TEST_CATS:
2175 if not test_cat in bot_info:
2176 continue
2177 test_cat_tests = bot_info[test_cat]
2178 tests = tests + test_cat_tests
2179 return tests
2180
2181 def flatten_tests_for_query(self, test_suites):
2182 """Returns a flattened dictionary of tests.
2183
2184 Returns a dictionary of tests associate with their
2185 configuration, not grouped by their test suite.
2186 """
2187 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232188 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062189 for test in test_suite:
2190 test_info = test_suite[test]
2191 test_name = test
Karen Qiane24b7ee2019-02-12 23:37:062192 tests[test_name] = test_info
2193 return tests
2194
2195 def parse_query_filter_params(self, params):
2196 """Parses the filter parameters.
2197
2198 Creates a dictionary from the parameters provided
2199 to filter the bot array.
2200 """
2201 params_dict = {}
2202 for p in params:
2203 # flag
2204 if p.startswith("--"):
2205 params_dict[p] = True
2206 else:
2207 pair = p.split(":")
2208 if len(pair) != 2:
2209 self.error_msg('Invalid command.')
2210 # regular parameters
2211 if pair[1].lower() == "true":
2212 params_dict[pair[0]] = True
2213 elif pair[1].lower() == "false":
2214 params_dict[pair[0]] = False
2215 else:
2216 params_dict[pair[0]] = pair[1]
2217 return params_dict
2218
2219 def get_test_suites_dict(self, bots):
2220 """Returns a dictionary of bots and their tests.
2221
2222 Returns a dictionary of bots and a list of their associated tests.
2223 """
2224 test_suite_dict = dict()
2225 for bot in bots:
2226 bot_info = bots[bot]
2227 tests = self.flatten_tests_for_bot(bot_info)
2228 test_suite_dict[bot] = tests
2229 return test_suite_dict
2230
2231 def output_query_result(self, result, json_file=None):
2232 """Outputs the result of the query.
2233
2234 If a json file parameter name is provided, then
2235 the result is output into the json file. If not,
2236 then the result is printed to the console.
2237 """
2238 output = json.dumps(result, indent=2)
2239 if json_file:
2240 self.write_file(json_file, output)
2241 else:
2242 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062243
Joshua Hood56c673c2022-03-02 20:29:332244 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062245 def query(self, args):
2246 """Queries tests or bots.
2247
2248 Depending on the arguments provided, outputs a json of
2249 tests or bots matching the appropriate optional parameters provided.
2250 """
2251 # split up query statement
2252 query = args.query.split('/')
2253 self.load_configuration_files()
2254 self.resolve_configuration_files()
2255
2256 # flatten bots json
2257 tests = self.test_suites
2258 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2259
2260 cmd_class = query[0]
2261
2262 # For queries starting with 'bots'
2263 if cmd_class == "bots":
2264 if len(query) == 1:
2265 return self.output_query_result(bots, args.json)
2266 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332267 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062268 if query[1] == 'tests':
2269 test_suites_dict = self.get_test_suites_dict(bots)
2270 return self.output_query_result(test_suites_dict, args.json)
Joshua Hood56c673c2022-03-02 20:29:332271 self.error_msg("This query should be in the format: bots/tests.")
Karen Qiane24b7ee2019-02-12 23:37:062272
2273 else:
2274 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2275 % str(len(query)-1))
2276
2277 # For queries starting with 'bot'
2278 elif cmd_class == "bot":
2279 if not len(query) == 2 and not len(query) == 3:
2280 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2281 % str(len(query)-1))
2282 bot_id = query[1]
2283 if not bot_id in bots:
2284 self.error_msg("No bot named '" + bot_id + "' found.")
2285 bot_info = bots[bot_id]
2286 if len(query) == 2:
2287 return self.output_query_result(bot_info, args.json)
2288 if not query[2] == 'tests':
2289 self.error_msg("The query should be in the format:" +
2290 "bot/<bot-name>/tests.")
2291
2292 bot_tests = self.flatten_tests_for_bot(bot_info)
2293 return self.output_query_result(bot_tests, args.json)
2294
2295 # For queries starting with 'tests'
2296 elif cmd_class == "tests":
2297 if not len(query) == 1 and not len(query) == 2:
2298 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2299 % str(len(query)-1))
2300 flattened_tests = self.flatten_tests_for_query(tests)
2301 if len(query) == 1:
2302 return self.output_query_result(flattened_tests, args.json)
2303
2304 # create params dict
2305 params = query[1].split('&')
2306 params_dict = self.parse_query_filter_params(params)
2307 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2308 return self.output_query_result(matching_bots)
2309
2310 # For queries starting with 'test'
2311 elif cmd_class == "test":
2312 if not len(query) == 2 and not len(query) == 3:
2313 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2314 % str(len(query)-1))
2315 test_id = query[1]
2316 if len(query) == 2:
2317 flattened_tests = self.flatten_tests_for_query(tests)
2318 for test in flattened_tests:
2319 if test == test_id:
2320 return self.output_query_result(flattened_tests[test], args.json)
2321 self.error_msg("There is no test named %s." % test_id)
2322 if not query[2] == 'bots':
2323 self.error_msg("The query should be in the format: " +
2324 "test/<test-name>/bots")
2325 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2326 return self.output_query_result(bots_for_test)
2327
2328 else:
2329 self.error_msg("Your command did not match any valid commands." +
2330 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Joshua Hood56c673c2022-03-02 20:29:332331 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282332
Garrett Beaty1afaccc2020-06-25 19:58:152333 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282334 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502335 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062336 elif self.args.query:
2337 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282338 else:
Greg Gutermanf60eb052020-03-12 17:40:012339 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282340 return 0
2341
2342if __name__ == "__main__": # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152343 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2344 sys.exit(generator.main())