blob: 8a5cf8efdfa8c0fb9ac12caff20db74ea50e4952 [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
John Budorickab108712018-09-01 00:12:21589 def initialize_args_for_test(
590 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21591 args = []
592 args.extend(generated_test.get('args', []))
593 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22594
Kenneth Russell8a386d42018-06-02 09:48:01595 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21596 val = generated_test.pop(key, [])
597 if fn(tester_config):
598 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01599
600 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
Brian Sheedy781c8ca42021-03-08 22:03:21601 add_conditional_args('lacros_args', self.is_lacros)
Kenneth Russell8a386d42018-06-02 09:48:01602 add_conditional_args('linux_args', self.is_linux)
603 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36604 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49605 add_conditional_args('mac_args', self.is_mac)
606 add_conditional_args('win_args', self.is_win)
607 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01608
John Budorickab108712018-09-01 00:12:21609 for key in additional_arg_keys or []:
610 args.extend(generated_test.pop(key, []))
611 args.extend(tester_config.get(key, []))
612
613 if args:
614 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01615
Kenneth Russelleb60cbd22017-12-05 07:54:28616 def initialize_swarming_dictionary_for_test(self, generated_test,
617 tester_config):
618 if 'swarming' not in generated_test:
619 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28620 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
621 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38622 'can_use_on_swarming_builders': tester_config.get('use_swarming',
623 True)
Dirk Pranke81ff51c2017-12-09 19:24:28624 })
Kenneth Russelleb60cbd22017-12-05 07:54:28625 if 'swarming' in tester_config:
Kenneth Russelleb60cbd22017-12-05 07:54:28626 self.dictionary_merge(generated_test['swarming'],
627 tester_config['swarming'])
Brian Sheedybc984e242021-04-21 23:44:51628 # Apply any platform-specific Swarming dimensions after the generic ones.
Kenneth Russelleb60cbd22017-12-05 07:54:28629 if 'android_swarming' in generated_test:
630 if self.is_android(tester_config): # pragma: no cover
631 self.dictionary_merge(
632 generated_test['swarming'],
633 generated_test['android_swarming']) # pragma: no cover
634 del generated_test['android_swarming'] # pragma: no cover
Brian Sheedybc984e242021-04-21 23:44:51635 if 'chromeos_swarming' in generated_test:
636 if self.is_chromeos(tester_config): # pragma: no cover
637 self.dictionary_merge(
638 generated_test['swarming'],
639 generated_test['chromeos_swarming']) # pragma: no cover
640 del generated_test['chromeos_swarming'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28641
642 def clean_swarming_dictionary(self, swarming_dict):
643 # Clean out redundant entries from a test's "swarming" dictionary.
644 # This is really only needed to retain 100% parity with the
645 # handwritten JSON files, and can be removed once all the files are
646 # autogenerated.
647 if 'shards' in swarming_dict:
648 if swarming_dict['shards'] == 1: # pragma: no cover
649 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24650 if 'hard_timeout' in swarming_dict:
651 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
652 del swarming_dict['hard_timeout'] # pragma: no cover
Garrett Beatybb18d532023-06-26 22:16:33653 del swarming_dict['can_use_on_swarming_builders']
Kenneth Russelleb60cbd22017-12-05 07:54:28654
Stephen Martinis0382bc12018-09-17 22:29:07655 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
656 waterfall):
657 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01658 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07659 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28660 # See if there are any exceptions that need to be merged into this
661 # test's specification.
Garrett Beatyffe83c4f2023-09-08 19:07:37662 modifications = self.get_test_modifications(test, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28663 if modifications:
664 test = self.dictionary_merge(test, modifications)
Garrett Beatybfeff8f2023-06-16 18:57:25665 if (swarming_dict := test.get('swarming')) is not None:
Garrett Beatybb18d532023-06-26 22:16:33666 if swarming_dict.get('can_use_on_swarming_builders'):
Garrett Beatybfeff8f2023-06-16 18:57:25667 self.clean_swarming_dictionary(swarming_dict)
668 else:
669 del test['swarming']
Ben Pastenee012aea42019-05-14 22:32:28670 # Ensure all Android Swarming tests run only on userdebug builds if another
671 # build type was not specified.
672 if 'swarming' in test and self.is_android(tester_config):
Garrett Beatyade673d2023-08-04 22:00:25673 dimensions = test.get('swarming', {}).get('dimensions', {})
674 if (dimensions.get('os') == 'Android'
675 and not dimensions.get('device_os_type')):
676 dimensions['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37677 self.replace_test_args(test, test_name, tester_name)
Garrett Beatyafd33e0f2023-06-23 20:47:57678 if 'args' in test and not test['args']:
679 test.pop('args')
Ben Pastenee012aea42019-05-14 22:32:28680
Kenneth Russelleb60cbd22017-12-05 07:54:28681 return test
682
Brian Sheedye6ea0ee2019-07-11 02:54:37683 def replace_test_args(self, test, test_name, tester_name):
Garrett Beatyffe83c4f2023-09-08 19:07:37684 replacements = self.get_test_replacements(test, tester_name) or {}
Brian Sheedye6ea0ee2019-07-11 02:54:37685 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23686 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37687 if key not in valid_replacement_keys:
688 raise BBGenErr(
689 'Given replacement key %s for %s on %s is not in the list of valid '
690 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23691 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37692 found_key = False
693 for i, test_key in enumerate(test.get(key, [])):
694 # Handle both the key/value being replaced being defined as two
695 # separate items or as key=value.
696 if test_key == replacement_key:
697 found_key = True
698 # Handle flags without values.
699 if replacement_val == None:
700 del test[key][i]
701 else:
702 test[key][i+1] = replacement_val
703 break
Joshua Hood56c673c2022-03-02 20:29:33704 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37705 found_key = True
706 if replacement_val == None:
707 del test[key][i]
708 else:
709 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
710 break
711 if not found_key:
712 raise BBGenErr('Could not find %s in existing list of values for key '
713 '%s in %s on %s' % (replacement_key, key, test_name,
714 tester_name))
715
Shenghua Zhangaba8bad2018-02-07 02:12:09716 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05717 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26718 True):
719 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06720 # are targeting CrOS hardware and so need the special trigger script.
Garrett Beatyade673d2023-08-04 22:00:25721 if 'device_type' in test.get('swarming', {}).get('dimensions', {}):
Ben Pastenea9e583b2019-01-16 02:57:26722 test['trigger_script'] = {
723 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
724 }
Shenghua Zhangaba8bad2018-02-07 02:12:09725
Garrett Beatyffe83c4f2023-09-08 19:07:37726 def add_android_presentation_args(self, tester_config, result):
Ben Pastene858f4be2019-01-09 23:52:09727 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38728 bucket = tester_config.get('results_bucket', 'chromium-result-details')
729 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09730 if (result['swarming']['can_use_on_swarming_builders'] and not
731 tester_config.get('skip_merge_script', False)):
732 result['merge'] = {
Garrett Beatyffe83c4f2023-09-08 19:07:37733 'args': [
734 '--bucket',
735 bucket,
736 '--test-name',
737 result['name'],
738 ],
739 'script': ('//build/android/pylib/results/presentation/'
740 'test_results_presentation.py'),
Ben Pastene858f4be2019-01-09 23:52:09741 }
Ben Pastene858f4be2019-01-09 23:52:09742 if args:
743 result['args'] = args
744
Kenneth Russelleb60cbd22017-12-05 07:54:28745 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
746 test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37747 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28748 return None
749 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37750 # Use test_name here instead of test['name'] because test['name'] will be
751 # modified with the variant identifier in a matrix compound suite
752 result.setdefault('test', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28753 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21754
755 self.initialize_args_for_test(
756 result, tester_config, additional_arg_keys=['gtest_args'])
Jamie Madilla8be0d72020-10-02 05:24:04757 if self.is_android(tester_config) and tester_config.get(
Yuly Novikov26dd47052021-02-11 00:57:14758 'use_swarming', True):
759 if not test_config.get('use_isolated_scripts_api', False):
760 # TODO(https://crbug.com/1137998) make Android presentation work with
761 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37762 self.add_android_presentation_args(tester_config, result)
Yuly Novikov26dd47052021-02-11 00:57:14763 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42764
Stephen Martinis0382bc12018-09-17 22:29:07765 result = self.update_and_cleanup_test(
766 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09767 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34768 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43769
Garrett Beatybb18d532023-06-26 22:16:33770 if 'swarming' in result and not result.get('merge'):
Jamie Madilla8be0d72020-10-02 05:24:04771 if test_config.get('use_isolated_scripts_api', False):
772 merge_script = 'standard_isolated_script_merge'
773 else:
774 merge_script = 'standard_gtest_merge'
775
Stephen Martinisbc7b7772019-05-01 22:01:43776 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04777 'script': '//testing/merge_scripts/%s.py' % merge_script,
Stephen Martinisbc7b7772019-05-01 22:01:43778 }
Kenneth Russelleb60cbd22017-12-05 07:54:28779 return result
780
781 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
782 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37783 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28784 return None
785 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37786 # Use test_name here instead of test['name'] because test['name'] will be
787 # modified with the variant identifier in a matrix compound suite
Garrett Beatydca3d882023-09-14 23:50:32788 result.setdefault('test', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28789 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01790 self.initialize_args_for_test(result, tester_config)
Yuly Novikov26dd47052021-02-11 00:57:14791 if self.is_android(tester_config) and tester_config.get(
792 'use_swarming', True):
793 if tester_config.get('use_android_presentation', False):
794 # TODO(https://crbug.com/1137998) make Android presentation work with
795 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37796 self.add_android_presentation_args(tester_config, result)
Stephen Martinis0382bc12018-09-17 22:29:07797 result = self.update_and_cleanup_test(
798 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09799 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34800 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17801
Garrett Beatybb18d532023-06-26 22:16:33802 if 'swarming' in result and not result.get('merge'):
Stephen Martinisf50047062019-05-06 22:26:17803 # TODO(https://crbug.com/958376): Consider adding the ability to not have
804 # this default.
805 result['merge'] = {
806 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
Stephen Martinisf50047062019-05-06 22:26:17807 }
Kenneth Russelleb60cbd22017-12-05 07:54:28808 return result
809
810 def generate_script_test(self, waterfall, tester_name, tester_config,
811 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44812 # TODO(https://crbug.com/953072): Remove this check whenever a better
813 # long-term solution is implemented.
814 if (waterfall.get('forbid_script_tests', False) or
815 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
816 raise BBGenErr('Attempted to generate a script test on tester ' +
817 tester_name + ', which explicitly forbids script tests')
Garrett Beatyffe83c4f2023-09-08 19:07:37818 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28819 return None
820 result = {
Garrett Beatyffe83c4f2023-09-08 19:07:37821 'name': test_config['name'],
822 'script': test_config['script'],
Kenneth Russelleb60cbd22017-12-05 07:54:28823 }
Stephen Martinis0382bc12018-09-17 22:29:07824 result = self.update_and_cleanup_test(
825 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34826 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28827 return result
828
829 def generate_junit_test(self, waterfall, tester_name, tester_config,
830 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37831 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28832 return None
John Budorickdef6acb2019-09-17 22:51:09833 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37834 # Use test_name here instead of test['name'] because test['name'] will be
835 # modified with the variant identifier in a matrix compound suite
836 result.setdefault('test', test_name)
John Budorickdef6acb2019-09-17 22:51:09837 self.initialize_args_for_test(result, tester_config)
838 result = self.update_and_cleanup_test(
839 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34840 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28841 return result
842
Xinan Lin05fb9c1752020-12-17 00:15:52843 def generate_skylab_test(self, waterfall, tester_name, tester_config,
844 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37845 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Xinan Lin05fb9c1752020-12-17 00:15:52846 return None
847 result = copy.deepcopy(test_config)
Brian Sheedy67937ad12024-03-06 22:53:55848 result.setdefault('test', test_name)
yoshiki iguchid1664ef2024-03-28 19:16:52849
850 if 'cros_board' in result or 'cros_board' in tester_config:
851 result['cros_board'] = tester_config.get('cros_board') or result.get(
852 'cros_board')
853 else:
854 raise BBGenErr("skylab tests must specify cros_board.")
855 if 'cros_model' in result or 'cros_model' in tester_config:
856 result['cros_model'] = tester_config.get('cros_model') or result.get(
857 'cros_model')
858 if 'dut_pool' in result or 'cros_dut_pool' in tester_config:
859 result['dut_pool'] = tester_config.get('cros_dut_pool') or result.get(
860 'dut_pool')
861
Xinan Lin05fb9c1752020-12-17 00:15:52862 self.initialize_args_for_test(result, tester_config)
863 result = self.update_and_cleanup_test(result, test_name, tester_name,
864 tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34865 self.substitute_magic_args(result, tester_name, tester_config)
Xinan Lin05fb9c1752020-12-17 00:15:52866 return result
867
Garrett Beaty65d44222023-08-01 17:22:11868 def substitute_gpu_args(self, tester_config, test, args):
Kenneth Russell8a386d42018-06-02 09:48:01869 substitutions = {
870 # Any machine in waterfalls.pyl which desires to run GPU tests
871 # must provide the os_type key.
872 'os_type': tester_config['os_type'],
873 'gpu_vendor_id': '0',
874 'gpu_device_id': '0',
875 }
Garrett Beatyade673d2023-08-04 22:00:25876 dimensions = test.get('swarming', {}).get('dimensions', {})
877 if 'gpu' in dimensions:
878 # First remove the driver version, then split into vendor and device.
879 gpu = dimensions['gpu']
880 if gpu != 'none':
881 gpu = gpu.split('-')[0].split(':')
882 substitutions['gpu_vendor_id'] = gpu[0]
883 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01884 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
885
886 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30887 test_name, test_config, is_android_webview,
Xinan Linedcf05b32023-10-19 23:13:50888 is_cast_streaming, is_skylab):
Kenneth Russell8a386d42018-06-02 09:48:01889 # These are all just specializations of isolated script tests with
890 # a bunch of boilerplate command line arguments added.
891
892 # The step name must end in 'test' or 'tests' in order for the
893 # results to automatically show up on the flakiness dashboard.
894 # (At least, this was true some time ago.) Continue to use this
895 # naming convention for the time being to minimize changes.
Garrett Beaty235c1412023-08-29 20:26:29896 #
897 # test name is the name of the test without the variant ID added
898 if not (test_name.endswith('test') or test_name.endswith('tests')):
899 raise BBGenErr(
900 f'telemetry test names must end with test or tests, got {test_name}')
Garrett Beatyffe83c4f2023-09-08 19:07:37901 result = self.generate_isolated_script_test(waterfall, tester_name,
902 tester_config, test_name,
903 test_config)
Kenneth Russell8a386d42018-06-02 09:48:01904 if not result:
905 return None
Garrett Beatydca3d882023-09-14 23:50:32906 result['test'] = test_config.get('test') or self.get_default_isolate_name(
907 tester_config, is_android_webview)
Chan Liab7d8dd82020-04-24 23:42:19908
Chan Lia3ad1502020-04-28 05:32:11909 # Populate test_id_prefix.
Garrett Beatydca3d882023-09-14 23:50:32910 gn_entry = self.gn_isolate_map[result['test']]
Chan Li17d969f92020-07-10 00:50:03911 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19912
Kenneth Russell8a386d42018-06-02 09:48:01913 args = result.get('args', [])
Garrett Beatyffe83c4f2023-09-08 19:07:37914 # Use test_name here instead of test['name'] because test['name'] will be
915 # modified with the variant identifier in a matrix compound suite
Kenneth Russell8a386d42018-06-02 09:48:01916 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14917
erikchen6da2d9b2018-08-03 23:01:14918 # These tests upload and download results from cloud storage and therefore
919 # aren't idempotent yet. https://crbug.com/549140.
Garrett Beatybfeff8f2023-06-16 18:57:25920 if 'swarming' in result:
921 result['swarming']['idempotent'] = False
erikchen6da2d9b2018-08-03 23:01:14922
Fabrice de Ganscbd655f2022-08-04 20:15:30923 browser = ''
924 if is_cast_streaming:
925 browser = 'cast-streaming-shell'
926 elif is_android_webview:
927 browser = 'android-webview-instrumentation'
928 else:
929 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:52930
Greg Thompsoncec7d8d2023-01-10 19:11:53931 extra_browser_args = []
932
Brian Sheedy4053a702020-07-28 02:09:52933 # Most platforms require --enable-logging=stderr to get useful browser logs.
934 # However, this actively messes with logging on CrOS (because Chrome's
935 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
936 # in order to see JavaScript console messages. See
937 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
Greg Thompsoncec7d8d2023-01-10 19:11:53938 if self.is_chromeos(tester_config):
939 extra_browser_args.append('--log-level=0')
940 elif not self.is_fuchsia(tester_config) or browser != 'fuchsia-chrome':
941 # Stderr logging is not needed for Chrome browser on Fuchsia, as ordinary
942 # logging via syslog is captured.
943 extra_browser_args.append('--enable-logging=stderr')
944
945 # --expose-gc allows the WebGL conformance tests to more reliably
946 # reproduce GC-related bugs in the V8 bindings.
947 extra_browser_args.append('--js-flags=--expose-gc')
Brian Sheedy4053a702020-07-28 02:09:52948
Xinan Linedcf05b32023-10-19 23:13:50949 # Skylab supports sharding, so reuse swarming's shard config.
950 if is_skylab and 'shards' not in result and test_config.get(
951 'swarming', {}).get('shards'):
952 result['shards'] = test_config['swarming']['shards']
953
Kenneth Russell8a386d42018-06-02 09:48:01954 args = [
Bo Liu555a0f92019-03-29 12:11:56955 test_to_run,
956 '--show-stdout',
957 '--browser=%s' % browser,
958 # --passthrough displays more of the logging in Telemetry when
959 # run via typ, in particular some of the warnings about tests
960 # being expected to fail, but passing.
961 '--passthrough',
962 '-v',
Brian Sheedy814e0482022-10-03 23:24:12963 '--stable-jobs',
Greg Thompsoncec7d8d2023-01-10 19:11:53964 '--extra-browser-args=%s' % ' '.join(extra_browser_args),
Brian Sheedy997e4802023-10-18 02:28:13965 '--enforce-browser-version',
Kenneth Russell8a386d42018-06-02 09:48:01966 ] + args
Garrett Beatybfeff8f2023-06-16 18:57:25967 result['args'] = self.maybe_fixup_args_array(
Garrett Beaty65d44222023-08-01 17:22:11968 self.substitute_gpu_args(tester_config, result, args))
Kenneth Russell8a386d42018-06-02 09:48:01969 return result
970
Brian Sheedyf74819b2021-06-04 01:38:38971 def get_default_isolate_name(self, tester_config, is_android_webview):
972 if self.is_android(tester_config):
973 if is_android_webview:
974 return 'telemetry_gpu_integration_test_android_webview'
975 return (
976 'telemetry_gpu_integration_test' +
977 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:33978 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:17979 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:33980 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:38981
Kenneth Russelleb60cbd22017-12-05 07:54:28982 def get_test_generator_map(self):
983 return {
Bo Liu555a0f92019-03-29 12:11:56984 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30985 GPUTelemetryTestGenerator(self, is_android_webview=True),
986 'cast_streaming_tests':
987 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:56988 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30989 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56990 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30991 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56992 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:30993 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56994 'junit_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30995 JUnitGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56996 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:30997 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:52998 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30999 SkylabGenerator(self),
Brian Sheedyb6491ba2022-09-26 20:49:491000 'skylab_gpu_telemetry_tests':
1001 SkylabGPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281002 }
1003
Kenneth Russell8a386d42018-06-02 09:48:011004 def get_test_type_remapper(self):
1005 return {
Fabrice de Gans223272482022-08-08 16:56:571006 # These are a specialization of isolated_scripts with a bunch of
1007 # boilerplate command line arguments added to each one.
1008 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
1009 'cast_streaming_tests': 'isolated_scripts',
1010 'gpu_telemetry_tests': 'isolated_scripts',
Brian Sheedyb6491ba2022-09-26 20:49:491011 # These are the same as existing test types, just configured to run
1012 # in Skylab instead of via normal swarming.
1013 'skylab_gpu_telemetry_tests': 'skylab_tests',
Kenneth Russell8a386d42018-06-02 09:48:011014 }
1015
Jeff Yoon67c3e832020-02-08 07:39:381016 def check_composition_type_test_suites(self, test_type,
1017 additional_validators=None):
1018 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1019 validators = [check_compound_references,
1020 check_basic_references,
1021 check_conflicting_definitions]
1022 if additional_validators:
1023 validators += additional_validators
1024
1025 target_suites = self.test_suites.get(test_type, {})
1026 other_test_type = ('compound_suites'
1027 if test_type == 'matrix_compound_suites'
1028 else 'matrix_compound_suites')
1029 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011030 basic_suites = self.test_suites.get('basic_suites', {})
1031
Jamie Madillcf4f8c72021-05-20 19:24:231032 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011033 if suite in basic_suites:
1034 raise BBGenErr('%s names may not duplicate basic test suite names '
1035 '(error found while processsing %s)'
1036 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011037
Jeff Yoon67c3e832020-02-08 07:39:381038 seen_tests = {}
1039 for sub_suite in suite_def:
1040 for validator in validators:
1041 validator(
1042 basic_suites=basic_suites,
1043 other_test_suites=other_suites,
1044 seen_tests=seen_tests,
1045 sub_suite=sub_suite,
1046 suite=suite,
1047 suite_def=suite_def,
1048 target_test_suites=target_suites,
1049 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051050 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381051 )
Kenneth Russelleb60cbd22017-12-05 07:54:281052
Stephen Martinis54d64ad2018-09-21 22:16:201053 def flatten_test_suites(self):
1054 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011055 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1056 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231057 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011058 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201059 self.test_suites = new_test_suites
1060
Chan Lia3ad1502020-04-28 05:32:111061 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231062 for suite in self.test_suites['basic_suites'].values():
1063 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561064 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411065
Garrett Beatydca3d882023-09-14 23:50:321066 isolate_name = test.get('test') or key
Nodir Turakulovfce34292019-12-18 17:05:411067 gn_entry = self.gn_isolate_map.get(isolate_name)
1068 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281069 label = gn_entry['label']
1070
1071 if label.count(':') != 1:
1072 raise BBGenErr(
1073 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1074 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1075 (label, isolate_name))
1076 if label.split(':')[1] != isolate_name:
1077 raise BBGenErr(
1078 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1079 ' label "%s" see http://crbug.com/1071091 for details.' %
1080 (isolate_name, label))
1081
Chan Lia3ad1502020-04-28 05:32:111082 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411083 else: # pragma: no cover
1084 # Some tests do not have an entry gn_isolate_map.pyl, such as
1085 # telemetry tests.
1086 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
1087 pass
1088
Kenneth Russelleb60cbd22017-12-05 07:54:281089 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011090 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201091
Jeff Yoon8154e582019-12-03 23:30:011092 compound_suites = self.test_suites.get('compound_suites', {})
1093 # check_composition_type_test_suites() checks that all basic suites
1094 # referenced by compound suites exist.
1095 basic_suites = self.test_suites.get('basic_suites')
1096
Jamie Madillcf4f8c72021-05-20 19:24:231097 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011098 # Resolve this to a dictionary.
1099 full_suite = {}
1100 for entry in value:
1101 suite = basic_suites[entry]
1102 full_suite.update(suite)
1103 compound_suites[name] = full_suite
1104
Jeff Yoon85fb8df2020-08-20 16:47:431105 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381106 """ Merge variant-defined configurations to each test case definition in a
1107 test suite.
1108
1109 The output maps a unique test name to an array of configurations because
1110 there may exist more than one definition for a test name using variants. The
1111 test name is referenced while mapping machines to test suites, so unpacking
1112 the array is done by the generators.
1113
1114 Args:
1115 basic_test_definition: a {} defined test suite in the format
1116 test_name:test_config
1117 variants: an [] of {} defining configurations to be applied to each test
1118 case in the basic test_definition
1119
1120 Return:
1121 a {} of test_name:[{}], where each {} is a merged configuration
1122 """
1123
1124 # Each test in a basic test suite will have a definition per variant.
1125 test_suite = {}
Garrett Beaty8d6708c2023-07-20 17:20:411126 for variant in variants:
1127 # Unpack the variant from variants.pyl if it's string based.
1128 if isinstance(variant, str):
1129 variant = self.variants[variant]
Jeff Yoonda581c32020-03-06 03:56:051130
Garrett Beaty8d6708c2023-07-20 17:20:411131 # If 'enabled' is set to False, we will not use this variant; otherwise if
1132 # the variant doesn't include 'enabled' variable or 'enabled' is set to
1133 # True, we will use this variant
1134 if not variant.get('enabled', True):
1135 continue
Jeff Yoon67c3e832020-02-08 07:39:381136
Garrett Beaty8d6708c2023-07-20 17:20:411137 # Make a shallow copy of the variant to remove variant-specific fields,
1138 # leaving just mixin fields
1139 variant = copy.copy(variant)
1140 variant.pop('enabled', None)
1141 identifier = variant.pop('identifier')
1142 variant_mixins = variant.pop('mixins', [])
1143 variant_skylab = variant.pop('skylab', {})
Jeff Yoon67c3e832020-02-08 07:39:381144
Garrett Beaty8d6708c2023-07-20 17:20:411145 for test_name, test_config in basic_test_definition.items():
1146 new_test = self.apply_mixin(variant, test_config)
Jeff Yoon67c3e832020-02-08 07:39:381147
Garrett Beaty8d6708c2023-07-20 17:20:411148 new_test['mixins'] = (test_config.get('mixins', []) + variant_mixins +
1149 mixins)
Xinan Lin05fb9c1752020-12-17 00:15:521150
Jeff Yoon67c3e832020-02-08 07:39:381151 # The identifier is used to make the name of the test unique.
1152 # Generators in the recipe uniquely identify a test by it's name, so we
1153 # don't want to have the same name for each variant.
Garrett Beaty235c1412023-08-29 20:26:291154 new_test['name'] = f'{test_name} {identifier}'
Ben Pastene5f231cf22022-05-05 18:03:071155
1156 # Attach the variant identifier to the test config so downstream
1157 # generators can make modifications based on the original name. This
1158 # is mainly used in generate_gpu_telemetry_test().
Garrett Beaty8d6708c2023-07-20 17:20:411159 new_test['variant_id'] = identifier
Ben Pastene5f231cf22022-05-05 18:03:071160
Garrett Beaty8d6708c2023-07-20 17:20:411161 for k, v in variant_skylab.items():
Sven Zheng22ba6312023-10-16 22:59:351162 # cros_chrome_version is the ash chrome version in the cros img in the
1163 # variant of cros_board. We don't want to include it in the final json
1164 # files; so remove it.
Garrett Beaty8d6708c2023-07-20 17:20:411165 if k != 'cros_chrome_version':
1166 new_test[k] = v
1167
Sven Zheng22ba6312023-10-16 22:59:351168 # For skylab, we need to pop the correct `autotest_name`. This field
1169 # defines what wrapper we use in OS infra. e.g. for gtest it's
1170 # https://source.chromium.org/chromiumos/chromiumos/codesearch/+/main:src/third_party/autotest/files/server/site_tests/chromium/chromium.py
1171 if variant_skylab and 'autotest_name' not in new_test:
1172 if 'tast_expr' in test_config:
1173 if 'lacros' in test_config['name']:
1174 new_test['autotest_name'] = 'tast.lacros-from-gcs'
1175 else:
1176 new_test['autotest_name'] = 'tast.chrome-from-gcs'
1177 elif 'benchmark' in test_config:
1178 new_test['autotest_name'] = 'chromium_Telemetry'
1179 else:
1180 new_test['autotest_name'] = 'chromium'
1181
Garrett Beaty8d6708c2023-07-20 17:20:411182 test_suite.setdefault(test_name, []).append(new_test)
1183
Jeff Yoon67c3e832020-02-08 07:39:381184 return test_suite
1185
Jeff Yoon8154e582019-12-03 23:30:011186 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381187 self.check_composition_type_test_suites('matrix_compound_suites',
1188 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011189
1190 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381191 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011192 # referenced by matrix suites exist.
1193 basic_suites = self.test_suites.get('basic_suites')
1194
Garrett Beaty235c1412023-08-29 20:26:291195 for matrix_suite_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011196 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381197
Jamie Madillcf4f8c72021-05-20 19:24:231198 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381199 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1200
Garrett Beaty235c1412023-08-29 20:26:291201 def update_tests(expanded):
1202 for test_name, new_tests in expanded.items():
1203 if not isinstance(new_tests, list):
1204 new_tests = [new_tests]
1205 tests_for_name = full_suite.setdefault(test_name, [])
1206 for t in new_tests:
1207 if t not in tests_for_name:
1208 tests_for_name.append(t)
1209
Garrett Beaty60a7b2a2023-09-13 23:00:401210 if (variants := mtx_test_suite_config.get('variants')):
Jeff Yoon85fb8df2020-08-20 16:47:431211 mixins = mtx_test_suite_config.get('mixins', [])
Garrett Beaty60a7b2a2023-09-13 23:00:401212 result = self.resolve_variants(basic_test_def, variants, mixins)
Garrett Beaty235c1412023-08-29 20:26:291213 update_tests(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271214 else:
1215 suite = basic_suites[test_suite]
Garrett Beaty235c1412023-08-29 20:26:291216 update_tests(suite)
1217 matrix_compound_suites[matrix_suite_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281218
1219 def link_waterfalls_to_test_suites(self):
1220 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231221 for tester_name, tester in waterfall['machines'].items():
1222 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281223 if not value in self.test_suites:
1224 # Hard / impossible to cover this in the unit test.
1225 raise self.unknown_test_suite(
1226 value, tester_name, waterfall['name']) # pragma: no cover
1227 tester['test_suites'][suite] = self.test_suites[value]
1228
1229 def load_configuration_files(self):
Garrett Beaty79339e182023-04-10 20:45:471230 self.waterfalls = self.load_pyl_file(self.args.waterfalls_pyl_path)
1231 self.test_suites = self.load_pyl_file(self.args.test_suites_pyl_path)
1232 self.exceptions = self.load_pyl_file(
1233 self.args.test_suite_exceptions_pyl_path)
1234 self.mixins = self.load_pyl_file(self.args.mixins_pyl_path)
1235 self.gn_isolate_map = self.load_pyl_file(self.args.gn_isolate_map_pyl_path)
Chong Guee622242020-10-28 18:17:351236 for isolate_map in self.args.isolate_map_files:
1237 isolate_map = self.load_pyl_file(isolate_map)
1238 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1239 if duplicates:
1240 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1241 ', '.join(duplicates))
1242 self.gn_isolate_map.update(isolate_map)
1243
Garrett Beaty79339e182023-04-10 20:45:471244 self.variants = self.load_pyl_file(self.args.variants_pyl_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281245
1246 def resolve_configuration_files(self):
Garrett Beaty235c1412023-08-29 20:26:291247 self.resolve_test_names()
Garrett Beatydca3d882023-09-14 23:50:321248 self.resolve_isolate_names()
Garrett Beaty65d44222023-08-01 17:22:111249 self.resolve_dimension_sets()
Chan Lia3ad1502020-04-28 05:32:111250 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281251 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011252 self.resolve_matrix_compound_test_suites()
1253 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281254 self.link_waterfalls_to_test_suites()
1255
Garrett Beaty235c1412023-08-29 20:26:291256 def resolve_test_names(self):
1257 for suite_name, suite in self.test_suites.get('basic_suites').items():
1258 for test_name, test in suite.items():
1259 if 'name' in test:
1260 raise BBGenErr(
1261 f'The name field is set in test {test_name} in basic suite '
1262 f'{suite_name}, this is not supported, the test name is the key '
1263 'within the basic suite')
Garrett Beatyffe83c4f2023-09-08 19:07:371264 # When a test is expanded with variants, this will be overwritten, but
1265 # this ensures every test definition has the name field set
1266 test['name'] = test_name
Garrett Beaty235c1412023-08-29 20:26:291267
Garrett Beatydca3d882023-09-14 23:50:321268 def resolve_isolate_names(self):
1269 for suite_name, suite in self.test_suites.get('basic_suites').items():
1270 for test_name, test in suite.items():
1271 if 'isolate_name' in test:
1272 raise BBGenErr(
1273 f'The isolate_name field is set in test {test_name} in basic '
1274 f'suite {suite_name}, the test field should be used instead')
1275
Garrett Beaty65d44222023-08-01 17:22:111276 def resolve_dimension_sets(self):
Garrett Beaty65d44222023-08-01 17:22:111277
1278 def definitions():
1279 for suite_name, suite in self.test_suites.get('basic_suites', {}).items():
1280 for test_name, test in suite.items():
1281 yield test, f'test {test_name} in basic suite {suite_name}'
1282
1283 for mixin_name, mixin in self.mixins.items():
1284 yield mixin, f'mixin {mixin_name}'
1285
1286 for waterfall in self.waterfalls:
1287 for builder_name, builder in waterfall.get('machines', {}).items():
1288 yield (
1289 builder,
1290 f'builder {builder_name} in waterfall {waterfall["name"]}',
1291 )
1292
1293 for test_name, exceptions in self.exceptions.items():
1294 modifications = exceptions.get('modifications', {})
1295 for builder_name, mods in modifications.items():
1296 yield (
1297 mods,
1298 f'exception for test {test_name} on builder {builder_name}',
1299 )
1300
1301 for definition, location in definitions():
1302 for swarming_attr in (
1303 'swarming',
1304 'android_swarming',
1305 'chromeos_swarming',
1306 ):
1307 if (swarming :=
1308 definition.get(swarming_attr)) and 'dimension_sets' in swarming:
Garrett Beatyade673d2023-08-04 22:00:251309 raise BBGenErr(
1310 f'dimension_sets is no longer supported (set in {location}),'
1311 ' instead, use set dimensions to a single dict')
Garrett Beaty65d44222023-08-01 17:22:111312
Nico Weberd18b8962018-05-16 19:39:381313 def unknown_bot(self, bot_name, waterfall_name):
1314 return BBGenErr(
1315 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1316
Kenneth Russelleb60cbd22017-12-05 07:54:281317 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1318 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381319 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281320 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1321
1322 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1323 return BBGenErr(
1324 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1325 ' on waterfall ' + waterfall_name)
1326
Stephen Martinisb72f6d22018-10-04 23:29:011327 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071328 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321329
1330 Checks in the waterfall, builder, and test objects for mixins.
1331 """
1332 def valid_mixin(mixin_name):
1333 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011334 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321335 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381336
Stephen Martinisb6a50492018-09-12 23:59:321337 def must_be_list(mixins, typ, name):
1338 """Asserts that given mixins are a list."""
1339 if not isinstance(mixins, list):
1340 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1341
Garrett Beatyffe83c4f2023-09-08 19:07:371342 test_name = test['name']
Brian Sheedy7658c982020-01-08 02:27:581343 remove_mixins = set()
1344 if 'remove_mixins' in builder:
1345 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1346 for rm in builder['remove_mixins']:
1347 valid_mixin(rm)
1348 remove_mixins.add(rm)
1349 if 'remove_mixins' in test:
1350 must_be_list(test['remove_mixins'], 'test', test_name)
1351 for rm in test['remove_mixins']:
1352 valid_mixin(rm)
1353 remove_mixins.add(rm)
1354 del test['remove_mixins']
1355
Stephen Martinisb72f6d22018-10-04 23:29:011356 if 'mixins' in waterfall:
1357 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1358 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581359 if mixin in remove_mixins:
1360 continue
Stephen Martinisb6a50492018-09-12 23:59:321361 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531362 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321363
Stephen Martinisb72f6d22018-10-04 23:29:011364 if 'mixins' in builder:
1365 must_be_list(builder['mixins'], 'builder', builder_name)
1366 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581367 if mixin in remove_mixins:
1368 continue
Stephen Martinisb6a50492018-09-12 23:59:321369 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531370 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321371
Stephen Martinisb72f6d22018-10-04 23:29:011372 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071373 return test
1374
Stephen Martinisb72f6d22018-10-04 23:29:011375 must_be_list(test['mixins'], 'test', test_name)
1376 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581377 # We don't bother checking if the given mixin is in remove_mixins here
1378 # since this is already the lowest level, so if a mixin is added here that
1379 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071380 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531381 test = self.apply_mixin(self.mixins[mixin], test, builder)
Jeff Yoon67c3e832020-02-08 07:39:381382 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071383 return test
Stephen Martinisb6a50492018-09-12 23:59:321384
Garrett Beaty8d6708c2023-07-20 17:20:411385 def apply_mixin(self, mixin, test, builder=None):
Stephen Martinisb72f6d22018-10-04 23:29:011386 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321387
Garrett Beaty4c35b142023-06-23 21:01:231388 A mixin is applied by copying all fields from the mixin into the
1389 test with the following exceptions:
1390 * For the various *args keys, the test's existing value (an empty
1391 list if not present) will be extended with the mixin's value.
1392 * The sub-keys of the swarming value will be copied to the test's
1393 swarming value with the following exceptions:
Garrett Beatyade673d2023-08-04 22:00:251394 * For the named_caches sub-keys, the test's existing value (an
1395 empty list if not present) will be extended with the mixin's
1396 value.
1397 * For the dimensions sub-key, the tests's existing value (an empty
1398 dict if not present) will be updated with the mixin's value.
Stephen Martinisb6a50492018-09-12 23:59:321399 """
Garrett Beaty4c35b142023-06-23 21:01:231400
Stephen Martinisb6a50492018-09-12 23:59:321401 new_test = copy.deepcopy(test)
1402 mixin = copy.deepcopy(mixin)
Garrett Beaty8d6708c2023-07-20 17:20:411403
1404 if 'description' in mixin:
1405 description = []
1406 if 'description' in new_test:
1407 description.append(new_test['description'])
1408 description.append(mixin.pop('description'))
1409 new_test['description'] = '\n'.join(description)
1410
Stephen Martinisb72f6d22018-10-04 23:29:011411 if 'swarming' in mixin:
1412 swarming_mixin = mixin['swarming']
1413 new_test.setdefault('swarming', {})
Stephen Martinisb72f6d22018-10-04 23:29:011414 if 'dimensions' in swarming_mixin:
Garrett Beatyade673d2023-08-04 22:00:251415 new_test['swarming'].setdefault('dimensions', {}).update(
1416 swarming_mixin.pop('dimensions'))
Garrett Beaty4c35b142023-06-23 21:01:231417 if 'named_caches' in swarming_mixin:
1418 new_test['swarming'].setdefault('named_caches', []).extend(
1419 swarming_mixin['named_caches'])
1420 del swarming_mixin['named_caches']
Stephen Martinisb72f6d22018-10-04 23:29:011421 # python dict update doesn't do recursion at all. Just hard code the
1422 # nested update we need (mixin['swarming'] shouldn't clobber
1423 # test['swarming'], but should update it).
1424 new_test['swarming'].update(swarming_mixin)
1425 del mixin['swarming']
1426
Garrett Beaty4c35b142023-06-23 21:01:231427 # Array so we can assign to it in a nested scope.
1428 args_need_fixup = ['args' in mixin]
1429
1430 for a in (
1431 'args',
1432 'precommit_args',
1433 'non_precommit_args',
1434 'desktop_args',
1435 'lacros_args',
1436 'linux_args',
1437 'android_args',
1438 'chromeos_args',
1439 'mac_args',
1440 'win_args',
1441 'win64_args',
1442 ):
1443 if (value := mixin.pop(a, None)) is None:
1444 continue
1445 if not isinstance(value, list):
1446 raise BBGenErr(f'"{a}" must be a list')
1447 new_test.setdefault(a, []).extend(value)
1448
Garrett Beaty4c35b142023-06-23 21:01:231449 args = new_test.get('args', [])
Austin Eng148d9f0f2022-02-08 19:18:531450
Garrett Beaty4c35b142023-06-23 21:01:231451 def add_conditional_args(key, fn):
Garrett Beaty8d6708c2023-07-20 17:20:411452 if builder is None:
1453 return
Garrett Beaty4c35b142023-06-23 21:01:231454 val = new_test.pop(key, [])
1455 if val and fn(builder):
1456 args.extend(val)
1457 args_need_fixup[0] = True
Austin Eng148d9f0f2022-02-08 19:18:531458
Garrett Beaty4c35b142023-06-23 21:01:231459 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
1460 add_conditional_args('lacros_args', self.is_lacros)
1461 add_conditional_args('linux_args', self.is_linux)
1462 add_conditional_args('android_args', self.is_android)
1463 add_conditional_args('chromeos_args', self.is_chromeos)
1464 add_conditional_args('mac_args', self.is_mac)
1465 add_conditional_args('win_args', self.is_win)
1466 add_conditional_args('win64_args', self.is_win64)
1467
1468 if args_need_fixup[0]:
1469 new_test['args'] = self.maybe_fixup_args_array(args)
Wezc0e835b702018-10-30 00:38:411470
Stephen Martinisb72f6d22018-10-04 23:29:011471 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321472 return new_test
1473
Greg Gutermanf60eb052020-03-12 17:40:011474 def generate_output_tests(self, waterfall):
1475 """Generates the tests for a waterfall.
1476
1477 Args:
1478 waterfall: a dictionary parsed from a master pyl file
1479 Returns:
1480 A dictionary mapping builders to test specs
1481 """
1482 return {
Jamie Madillcf4f8c72021-05-20 19:24:231483 name: self.get_tests_for_config(waterfall, name, config)
1484 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011485 }
1486
1487 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531488 generator_map = self.get_test_generator_map()
1489 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281490
Greg Gutermanf60eb052020-03-12 17:40:011491 tests = {}
1492 # Copy only well-understood entries in the machine's configuration
1493 # verbatim into the generated JSON.
1494 if 'additional_compile_targets' in config:
1495 tests['additional_compile_targets'] = config[
1496 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231497 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011498 if test_type not in generator_map:
1499 raise self.unknown_test_suite_type(
1500 test_type, name, waterfall['name']) # pragma: no cover
1501 test_generator = generator_map[test_type]
1502 # Let multiple kinds of generators generate the same kinds
1503 # of tests. For example, gpu_telemetry_tests are a
1504 # specialization of isolated_scripts.
1505 new_tests = test_generator.generate(
1506 waterfall, name, config, input_tests)
1507 remapped_test_type = test_type_remapper.get(test_type, test_type)
Garrett Beatyffe83c4f2023-09-08 19:07:371508 tests.setdefault(remapped_test_type, []).extend(new_tests)
1509
1510 for test_type, tests_for_type in tests.items():
1511 if test_type == 'additional_compile_targets':
1512 continue
1513 tests[test_type] = sorted(tests_for_type, key=lambda t: t['name'])
Greg Gutermanf60eb052020-03-12 17:40:011514
1515 return tests
1516
1517 def jsonify(self, all_tests):
1518 return json.dumps(
1519 all_tests, indent=2, separators=(',', ': '),
1520 sort_keys=True) + '\n'
1521
1522 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281523 self.load_configuration_files()
1524 self.resolve_configuration_files()
1525 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011526 result = collections.defaultdict(dict)
1527
Stephanie Kim572b43c02023-04-13 14:24:131528 if os.path.exists(self.args.autoshard_exceptions_json_path):
1529 autoshards = json.loads(
1530 self.read_file(self.args.autoshard_exceptions_json_path))
1531 else:
1532 autoshards = {}
1533
Dirk Pranke6269d302020-10-01 00:14:391534 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011535 for waterfall in self.waterfalls:
1536 for field in required_fields:
1537 # Verify required fields
1538 if field not in waterfall:
1539 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1540
1541 # Handle filter flag, if specified
1542 if filters and waterfall['name'] not in filters:
1543 continue
1544
1545 # Join config files and hardcoded values together
1546 all_tests = self.generate_output_tests(waterfall)
1547 result[waterfall['name']] = all_tests
1548
Stephanie Kim572b43c02023-04-13 14:24:131549 if not autoshards:
1550 continue
1551 for builder, test_spec in all_tests.items():
1552 for target_type, test_list in test_spec.items():
1553 if target_type == 'additional_compile_targets':
1554 continue
1555 for test_dict in test_list:
1556 # Suites that apply variants or other customizations will create
1557 # test_dicts that have "name" value that is different from the
Garrett Beatyffe83c4f2023-09-08 19:07:371558 # "test" value.
Stephanie Kim572b43c02023-04-13 14:24:131559 # e.g. name = vulkan_swiftshader_content_browsertests, but
1560 # test = content_browsertests and
1561 # test_id_prefix = "ninja://content/test:content_browsertests/"
Garrett Beatyffe83c4f2023-09-08 19:07:371562 test_name = test_dict['name']
Stephanie Kim572b43c02023-04-13 14:24:131563 shard_info = autoshards.get(waterfall['name'],
1564 {}).get(builder, {}).get(test_name)
1565 if shard_info:
1566 test_dict['swarming'].update(
1567 {'shards': int(shard_info['shards'])})
1568
Greg Gutermanf60eb052020-03-12 17:40:011569 # Add do not edit warning
1570 for tests in result.values():
1571 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1572 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1573
1574 return result
1575
1576 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281577 suffix = '.json'
1578 if self.args.new_files:
1579 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011580
1581 for filename, contents in result.items():
1582 jsonstr = self.jsonify(contents)
Garrett Beaty79339e182023-04-10 20:45:471583 file_path = os.path.join(self.args.output_dir, filename + suffix)
1584 self.write_file(file_path, jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281585
Nico Weberd18b8962018-05-16 19:39:381586 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161587 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421588 # NOTE: This reference can cause issues; if a file changes there, the
1589 # presubmit here won't be run by default. A manually maintained list there
1590 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1591 # references to configs outside of this directory are added, please change
1592 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1593 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491594
Garrett Beaty7e866fc2021-06-16 14:12:101595 # Get the generated project.pyl so we can check if we should be enforcing
1596 # that the specs are for builders that actually exist
1597 # If not, return None to indicate that we won't enforce that builders in
1598 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491599 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1600 'project.pyl')
1601 if os.path.exists(project_pyl_path):
1602 settings = ast.literal_eval(self.read_file(project_pyl_path))
1603 if not settings.get('validate_source_side_specs_have_builder', True):
1604 return None
1605
Nico Weberd18b8962018-05-16 19:39:381606 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311607 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161608 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1609 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431610 for c in milo_configs:
1611 for l in self.read_file(c).splitlines():
1612 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311613 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431614 continue
1615 # l looks like
1616 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1617 # Extract win_chromium_dbg_ng part.
1618 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381619 return bot_names
1620
Ben Pastene9a010082019-09-25 20:41:371621 def get_internal_waterfalls(self):
1622 # Similar to get_builders_that_do_not_actually_exist above, but for
1623 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201624 return [
Kramer Ge3bf853a2023-04-13 19:39:471625 'chrome', 'chrome.pgo', 'chrome.gpu.fyi', 'internal.chrome.fyi',
yoshiki iguchi4de608082024-03-14 00:33:361626 'internal.chromeos.fyi', 'internal.optimization_guide', 'internal.soda',
1627 'chromeos.preuprev'
Yuke Liaoe6c23dd2021-07-28 16:12:201628 ]
Ben Pastene9a010082019-09-25 20:41:371629
Stephen Martinisf83893722018-09-19 00:02:181630 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201631 self.check_input_files_sorting(verbose)
1632
Kenneth Russelleb60cbd22017-12-05 07:54:281633 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011634 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381635 self.check_composition_type_test_suites('matrix_compound_suites',
1636 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111637 self.resolve_test_id_prefixes()
Garrett Beaty1ead4a52023-12-07 19:16:421638
1639 # All test suites must be referenced. Check this before flattening the test
1640 # suites so that we can transitively check the basic suites for compound
1641 # suites and matrix compound suites (otherwise we would determine a basic
1642 # suite is used if it shared a name with a test present in a basic suite
1643 # that is used).
1644 all_suites = set(
1645 itertools.chain(*(self.test_suites.get(a, {}) for a in (
1646 'basic_suites',
1647 'compound_suites',
1648 'matrix_compound_suites',
1649 ))))
1650 unused_suites = set(all_suites)
1651 generator_map = self.get_test_generator_map()
1652 for waterfall in self.waterfalls:
1653 for bot_name, tester in waterfall['machines'].items():
1654 for suite_type, suite in tester.get('test_suites', {}).items():
1655 if suite_type not in generator_map:
1656 raise self.unknown_test_suite_type(suite_type, bot_name,
1657 waterfall['name'])
1658 if suite not in all_suites:
1659 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1660 unused_suites.discard(suite)
1661 # For each compound suite or matrix compound suite, if the suite was used,
1662 # remove all of the basic suites that it composes from the set of unused
1663 # suites
1664 for a in ('compound_suites', 'matrix_compound_suites'):
1665 for suite, sub_suites in self.test_suites.get(a, {}).items():
1666 if suite not in unused_suites:
1667 unused_suites.difference_update(sub_suites)
1668 if unused_suites:
1669 raise BBGenErr('The following test suites were unreferenced by bots on '
1670 'the waterfalls: ' + str(unused_suites))
1671
Stephen Martinis54d64ad2018-09-21 22:16:201672 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381673
1674 # All bots should exist.
1675 bot_names = self.get_valid_bot_names()
Garrett Beaty2a02de3c2020-05-15 13:57:351676 if bot_names is not None:
1677 internal_waterfalls = self.get_internal_waterfalls()
1678 for waterfall in self.waterfalls:
1679 # TODO(crbug.com/991417): Remove the need for this exception.
1680 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011681 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351682 for bot_name in waterfall['machines']:
Garrett Beaty2a02de3c2020-05-15 13:57:351683 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581684 if waterfall['name'] in [
1685 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1686 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351687 # TODO(thakis): Remove this once these bots move to luci.
1688 continue # pragma: no cover
1689 if waterfall['name'] in ['tryserver.webrtc',
1690 'webrtc.chromium.fyi.experimental']:
1691 # These waterfalls have their bot configs in a different repo.
1692 # so we don't know about their bot names.
1693 continue # pragma: no cover
1694 if waterfall['name'] in ['client.devtools-frontend.integration',
1695 'tryserver.devtools-frontend',
1696 'chromium.devtools-frontend']:
1697 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201698 if waterfall['name'] in ['client.openscreen.chromium']:
1699 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351700 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381701
Kenneth Russelleb60cbd22017-12-05 07:54:281702 # All test suite exceptions must refer to bots on the waterfall.
1703 all_bots = set()
1704 missing_bots = set()
1705 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231706 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281707 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281708 # In order to disambiguate between bots with the same name on
1709 # different waterfalls, support has been added to various
1710 # exceptions for concatenating the waterfall name after the bot
1711 # name.
1712 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231713 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381714 removals = (exception.get('remove_from', []) +
1715 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231716 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381717 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281718 if removal not in all_bots:
1719 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411720
Kenneth Russelleb60cbd22017-12-05 07:54:281721 if missing_bots:
1722 raise BBGenErr('The following nonexistent machines were referenced in '
1723 'the test suite exceptions: ' + str(missing_bots))
1724
Garrett Beatyb061e69d2023-06-27 16:15:351725 for name, mixin in self.mixins.items():
1726 if '$mixin_append' in mixin:
1727 raise BBGenErr(
1728 f'$mixin_append is no longer supported (set in mixin "{name}"),'
1729 ' args and named caches specified as normal will be appended')
1730
Stephen Martinis0382bc12018-09-17 22:29:071731 # All mixins must be referenced
1732 seen_mixins = set()
1733 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011734 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231735 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011736 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071737 for suite in self.test_suites.values():
1738 if isinstance(suite, list):
1739 # Don't care about this, it's a composition, which shouldn't include a
1740 # swarming mixin.
1741 continue
1742
1743 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561744 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011745 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071746
Zhaoyang Li9da047d52021-05-10 21:31:441747 for variant in self.variants:
1748 # Unpack the variant from variants.pyl if it's string based.
1749 if isinstance(variant, str):
1750 variant = self.variants[variant]
1751 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1752
Stephen Martinisb72f6d22018-10-04 23:29:011753 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071754 if missing_mixins:
1755 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1756 ' referenced in a waterfall, machine, or test suite.' % (
1757 str(missing_mixins)))
1758
Jeff Yoonda581c32020-03-06 03:56:051759 # All variant references must be referenced
1760 seen_variants = set()
1761 for suite in self.test_suites.values():
1762 if isinstance(suite, list):
1763 continue
1764
1765 for test in suite.values():
1766 if isinstance(test, dict):
1767 for variant in test.get('variants', []):
1768 if isinstance(variant, str):
1769 seen_variants.add(variant)
1770
1771 missing_variants = set(self.variants.keys()) - seen_variants
1772 if missing_variants:
1773 raise BBGenErr('The following variants were unreferenced: %s. They must '
1774 'be referenced in a matrix test suite under the variants '
1775 'key.' % str(missing_variants))
1776
Stephen Martinis54d64ad2018-09-21 22:16:201777
Garrett Beaty79339e182023-04-10 20:45:471778 def type_assert(self, node, typ, file_path, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201779 """Asserts that the Python AST node |node| is of type |typ|.
1780
1781 If verbose is set, it prints out some helpful context lines, showing where
1782 exactly the error occurred in the file.
1783 """
1784 if not isinstance(node, typ):
1785 if verbose:
Garrett Beaty79339e182023-04-10 20:45:471786 lines = [""] + self.read_file(file_path).splitlines()
Stephen Martinis54d64ad2018-09-21 22:16:201787
1788 context = 2
1789 lines_start = max(node.lineno - context, 0)
1790 # Add one to include the last line
1791 lines_end = min(node.lineno + context, len(lines)) + 1
Garrett Beaty79339e182023-04-10 20:45:471792 lines = itertools.chain(
1793 ['== %s ==\n' % file_path],
1794 ["<snip>\n"],
1795 [
1796 '%d %s' % (lines_start + i, line)
1797 for i, line in enumerate(lines[lines_start:lines_start +
1798 context])
1799 ],
1800 ['-' * 80 + '\n'],
1801 ['%d %s' % (node.lineno, lines[node.lineno])],
1802 [
1803 '-' * (node.col_offset + 3) + '^' + '-' *
1804 (80 - node.col_offset - 4) + '\n'
1805 ],
1806 [
1807 '%d %s' % (node.lineno + 1 + i, line)
1808 for i, line in enumerate(lines[node.lineno + 1:lines_end])
1809 ],
1810 ["<snip>\n"],
Stephen Martinis54d64ad2018-09-21 22:16:201811 )
1812 # Print out a useful message when a type assertion fails.
1813 for l in lines:
1814 self.print_line(l.strip())
1815
1816 node_dumped = ast.dump(node, annotate_fields=False)
1817 # If the node is huge, truncate it so everything fits in a terminal
1818 # window.
1819 if len(node_dumped) > 60: # pragma: no cover
1820 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1821 raise BBGenErr(
Garrett Beaty807011ab2023-04-12 00:52:391822 'Invalid .pyl file \'%s\'. Python AST node %r on line %s expected to'
Garrett Beaty79339e182023-04-10 20:45:471823 ' be %s, is %s' %
1824 (file_path, node_dumped, node.lineno, typ, type(node)))
Stephen Martinis54d64ad2018-09-21 22:16:201825
Garrett Beaty79339e182023-04-10 20:45:471826 def check_ast_list_formatted(self,
1827 keys,
1828 file_path,
1829 verbose,
Stephen Martinis1384ff92020-01-07 19:52:151830 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531831 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201832
Stephen Martinis5bef0fc2020-01-06 22:47:531833 Currently only checks to ensure they're correctly sorted, and that there
1834 are no duplicates.
1835
1836 Args:
1837 keys: An python list of AST nodes.
1838
1839 It's a list of AST nodes instead of a list of strings because
1840 when verbose is set, it tries to print out context of where the
1841 diffs are in the file.
Garrett Beaty79339e182023-04-10 20:45:471842 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531843 verbose: If set, print out diff information about how the keys are
1844 incorrectly formatted.
1845 check_sorting: If true, checks if the list is sorted.
1846 Returns:
1847 If the keys are correctly formatted.
1848 """
1849 if not keys:
1850 return True
1851
1852 assert isinstance(keys[0], ast.Str)
1853
1854 keys_strs = [k.s for k in keys]
1855 # Keys to diff against. Used below.
1856 keys_to_diff_against = None
1857 # If the list is properly formatted.
1858 list_formatted = True
1859
1860 # Duplicates are always bad.
1861 if len(set(keys_strs)) != len(keys_strs):
1862 list_formatted = False
1863 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1864
1865 if check_sorting and sorted(keys_strs) != keys_strs:
1866 list_formatted = False
1867 if list_formatted:
1868 return True
1869
1870 if verbose:
1871 line_num = keys[0].lineno
1872 keys = [k.s for k in keys]
1873 if check_sorting:
1874 # If we have duplicates, sorting this will take care of it anyways.
1875 keys_to_diff_against = sorted(set(keys))
1876 # else, keys_to_diff_against is set above already
1877
1878 self.print_line('=' * 80)
1879 self.print_line('(First line of keys is %s)' % line_num)
Garrett Beaty79339e182023-04-10 20:45:471880 for line in difflib.context_diff(keys,
1881 keys_to_diff_against,
1882 fromfile='current (%r)' % file_path,
1883 tofile='sorted',
1884 lineterm=''):
Stephen Martinis5bef0fc2020-01-06 22:47:531885 self.print_line(line)
1886 self.print_line('=' * 80)
1887
1888 return False
1889
Garrett Beaty79339e182023-04-10 20:45:471890 def check_ast_dict_formatted(self, node, file_path, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531891 """Checks if an ast dictionary's keys are correctly formatted.
1892
1893 Just a simple wrapper around check_ast_list_formatted.
1894 Args:
1895 node: An AST node. Assumed to be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471896 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531897 verbose: If set, print out diff information about how the keys are
1898 incorrectly formatted.
1899 check_sorting: If true, checks if the list is sorted.
1900 Returns:
1901 If the dictionary is correctly formatted.
1902 """
Stephen Martinis54d64ad2018-09-21 22:16:201903 keys = []
1904 # The keys of this dict are ordered as ordered in the file; normal python
1905 # dictionary keys are given an arbitrary order, but since we parsed the
1906 # file itself, the order as given in the file is preserved.
1907 for key in node.keys:
Garrett Beaty79339e182023-04-10 20:45:471908 self.type_assert(key, ast.Str, file_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531909 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201910
Garrett Beaty79339e182023-04-10 20:45:471911 return self.check_ast_list_formatted(keys, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181912
1913 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201914 # TODO(https://crbug.com/886993): Add the ability for this script to
1915 # actually format the files, rather than just complain if they're
1916 # incorrectly formatted.
1917 bad_files = set()
Garrett Beaty79339e182023-04-10 20:45:471918
1919 def parse_file(file_path):
Stephen Martinis5bef0fc2020-01-06 22:47:531920 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201921
Stephen Martinis5bef0fc2020-01-06 22:47:531922 Returns an AST node representing the value in the pyl file."""
Garrett Beaty79339e182023-04-10 20:45:471923 parsed = ast.parse(self.read_file(file_path))
Stephen Martinisf83893722018-09-19 00:02:181924
Stephen Martinisf83893722018-09-19 00:02:181925 # Must be a module.
Garrett Beaty79339e182023-04-10 20:45:471926 self.type_assert(parsed, ast.Module, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181927 module = parsed.body
1928
1929 # Only one expression in the module.
Garrett Beaty79339e182023-04-10 20:45:471930 self.type_assert(module, list, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181931 if len(module) != 1: # pragma: no cover
Garrett Beaty79339e182023-04-10 20:45:471932 raise BBGenErr('Invalid .pyl file %s' % file_path)
Stephen Martinisf83893722018-09-19 00:02:181933 expr = module[0]
Garrett Beaty79339e182023-04-10 20:45:471934 self.type_assert(expr, ast.Expr, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181935
Stephen Martinis5bef0fc2020-01-06 22:47:531936 return expr.value
1937
1938 # Handle this separately
Garrett Beaty79339e182023-04-10 20:45:471939 value = parse_file(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531940 # Value should be a list.
Garrett Beaty79339e182023-04-10 20:45:471941 self.type_assert(value, ast.List, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531942
1943 keys = []
Joshua Hood56c673c2022-03-02 20:29:331944 for elm in value.elts:
Garrett Beaty79339e182023-04-10 20:45:471945 self.type_assert(elm, ast.Dict, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531946 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331947 for key, val in zip(elm.keys, elm.values):
Garrett Beaty79339e182023-04-10 20:45:471948 self.type_assert(key, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531949 if key.s == 'machines':
Garrett Beaty79339e182023-04-10 20:45:471950 if not self.check_ast_dict_formatted(
1951 val, self.args.waterfalls_pyl_path, verbose):
1952 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531953
1954 if key.s == "name":
Garrett Beaty79339e182023-04-10 20:45:471955 self.type_assert(val, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531956 waterfall_name = val
1957 assert waterfall_name
1958 keys.append(waterfall_name)
1959
Garrett Beaty79339e182023-04-10 20:45:471960 if not self.check_ast_list_formatted(keys, self.args.waterfalls_pyl_path,
1961 verbose):
1962 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531963
Garrett Beaty79339e182023-04-10 20:45:471964 for file_path in (
1965 self.args.mixins_pyl_path,
1966 self.args.test_suites_pyl_path,
1967 self.args.test_suite_exceptions_pyl_path,
Stephen Martinis5bef0fc2020-01-06 22:47:531968 ):
Garrett Beaty79339e182023-04-10 20:45:471969 value = parse_file(file_path)
Stephen Martinisf83893722018-09-19 00:02:181970 # Value should be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471971 self.type_assert(value, ast.Dict, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181972
Garrett Beaty79339e182023-04-10 20:45:471973 if not self.check_ast_dict_formatted(value, file_path, verbose):
1974 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531975
Garrett Beaty79339e182023-04-10 20:45:471976 if file_path == self.args.test_suites_pyl_path:
Jeff Yoon8154e582019-12-03 23:30:011977 expected_keys = ['basic_suites',
1978 'compound_suites',
1979 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201980 actual_keys = [node.s for node in value.keys]
1981 assert all(key in expected_keys for key in actual_keys), (
Garrett Beaty79339e182023-04-10 20:45:471982 'Invalid %r file; expected keys %r, got %r' %
1983 (file_path, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:331984 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:201985 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011986 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201987 for suite_group in suite_dicts:
Garrett Beaty79339e182023-04-10 20:45:471988 if not self.check_ast_dict_formatted(suite_group, file_path, verbose):
1989 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181990
Stephen Martinis5bef0fc2020-01-06 22:47:531991 for key, suite in zip(value.keys, value.values):
1992 # The compound suites are checked in
1993 # 'check_composition_type_test_suites()'
1994 if key.s == 'basic_suites':
1995 for group in suite.values:
Garrett Beaty79339e182023-04-10 20:45:471996 if not self.check_ast_dict_formatted(group, file_path, verbose):
1997 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531998 break
Stephen Martinis54d64ad2018-09-21 22:16:201999
Garrett Beaty79339e182023-04-10 20:45:472000 elif file_path == self.args.test_suite_exceptions_pyl_path:
Stephen Martinis5bef0fc2020-01-06 22:47:532001 # Check the values for each test.
2002 for test in value.values:
2003 for kind, node in zip(test.keys, test.values):
2004 if isinstance(node, ast.Dict):
Garrett Beaty79339e182023-04-10 20:45:472005 if not self.check_ast_dict_formatted(node, file_path, verbose):
2006 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:532007 elif kind.s == 'remove_from':
2008 # Don't care about sorting; these are usually grouped, since the
2009 # same bug can affect multiple builders. Do want to make sure
2010 # there aren't duplicates.
Garrett Beaty79339e182023-04-10 20:45:472011 if not self.check_ast_list_formatted(
2012 node.elts, file_path, verbose, check_sorting=False):
2013 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:182014
2015 if bad_files:
2016 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:202017 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:532018 'unsorted, or have duplicates. Re-run this with --verbose to see '
2019 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:182020
Kenneth Russelleb60cbd22017-12-05 07:54:282021 def check_output_file_consistency(self, verbose=False):
2022 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012023 # All waterfalls/bucket .json files must have been written
2024 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:282025 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012026 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:162027 outputs = self.generate_outputs()
2028 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:012029 expected = self.jsonify(expected_contents)
Garrett Beaty79339e182023-04-10 20:45:472030 file_path = os.path.join(self.args.output_dir, filename + '.json')
Ben Pastenef21cda32023-03-30 22:00:572031 current = self.read_file(file_path)
Kenneth Russelleb60cbd22017-12-05 07:54:282032 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:012033 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:322034 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:012035 self.print_line('File ' + filename +
2036 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:322037 'contents:')
2038 for line in difflib.unified_diff(
2039 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:502040 current.splitlines(),
2041 fromfile='expected', tofile='current'):
2042 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:012043
2044 if ungenerated_files:
2045 raise BBGenErr(
2046 'The following files have not been properly '
2047 'autogenerated by generate_buildbot_json.py: ' +
2048 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:282049
Dirk Pranke772f55f2021-04-28 04:51:162050 for builder_group, builders in outputs.items():
2051 for builder, step_types in builders.items():
Garrett Beatydca3d882023-09-14 23:50:322052 for test_type in ('gtest_tests', 'isolated_scripts'):
2053 for step_data in step_types.get(test_type, []):
2054 step_name = step_data['name']
2055 self._check_swarming_config(builder_group, builder, step_name,
2056 step_data)
Dirk Pranke772f55f2021-04-28 04:51:162057
2058 def _check_swarming_config(self, filename, builder, step_name, step_data):
Ben Pastene338f56b2023-03-31 21:24:452059 # TODO(crbug.com/1203436): Ensure all swarming tests specify cpu, not
Dirk Pranke772f55f2021-04-28 04:51:162060 # just mac tests.
Garrett Beatybb18d532023-06-26 22:16:332061 if 'swarming' in step_data:
Garrett Beatyade673d2023-08-04 22:00:252062 dimensions = step_data['swarming'].get('dimensions')
2063 if not dimensions:
Tatsuhisa Yamaguchif1878d52023-11-06 06:02:252064 raise BBGenErr('%s: %s / %s : dimensions must be specified for all '
Dirk Pranke772f55f2021-04-28 04:51:162065 'swarmed tests' % (filename, builder, step_name))
Garrett Beatyade673d2023-08-04 22:00:252066 if not dimensions.get('os'):
2067 raise BBGenErr('%s: %s / %s : os must be specified for all '
2068 'swarmed tests' % (filename, builder, step_name))
2069 if 'Mac' in dimensions.get('os') and not dimensions.get('cpu'):
2070 raise BBGenErr('%s: %s / %s : cpu must be specified for mac '
2071 'swarmed tests' % (filename, builder, step_name))
Dirk Pranke772f55f2021-04-28 04:51:162072
Kenneth Russelleb60cbd22017-12-05 07:54:282073 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502074 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282075 self.check_output_file_consistency(verbose) # pragma: no cover
2076
Karen Qiane24b7ee2019-02-12 23:37:062077 def does_test_match(self, test_info, params_dict):
2078 """Checks to see if the test matches the parameters given.
2079
2080 Compares the provided test_info with the params_dict to see
2081 if the bot matches the parameters given. If so, returns True.
2082 Else, returns false.
2083
2084 Args:
2085 test_info (dict): Information about a specific bot provided
2086 in the format shown in waterfalls.pyl
2087 params_dict (dict): Dictionary of parameters and their values
2088 to look for in the bot
2089 Ex: {
2090 'device_os':'android',
2091 '--flag':True,
2092 'mixins': ['mixin1', 'mixin2'],
2093 'ex_key':'ex_value'
2094 }
2095
2096 """
2097 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2098 'kvm', 'pool', 'integrity'] # dimension parameters
2099 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2100 'can_use_on_swarming_builders']
2101 for param in params_dict:
2102 # if dimension parameter
2103 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2104 if not 'swarming' in test_info:
2105 return False
2106 swarming = test_info['swarming']
2107 if param in SWARMING_PARAMS:
2108 if not param in swarming:
2109 return False
2110 if not str(swarming[param]) == params_dict[param]:
2111 return False
2112 else:
Garrett Beatyade673d2023-08-04 22:00:252113 if not 'dimensions' in swarming:
Karen Qiane24b7ee2019-02-12 23:37:062114 return False
Garrett Beatyade673d2023-08-04 22:00:252115 dimensions = swarming['dimensions']
Karen Qiane24b7ee2019-02-12 23:37:062116 # only looking at the first dimension set
Garrett Beatyade673d2023-08-04 22:00:252117 if not param in dimensions:
Karen Qiane24b7ee2019-02-12 23:37:062118 return False
Garrett Beatyade673d2023-08-04 22:00:252119 if not dimensions[param] == params_dict[param]:
Karen Qiane24b7ee2019-02-12 23:37:062120 return False
2121
2122 # if flag
2123 elif param.startswith('--'):
2124 if not 'args' in test_info:
2125 return False
2126 if not param in test_info['args']:
2127 return False
2128
2129 # not dimension parameter/flag/mixin
2130 else:
2131 if not param in test_info:
2132 return False
2133 if not test_info[param] == params_dict[param]:
2134 return False
2135 return True
2136 def error_msg(self, msg):
2137 """Prints an error message.
2138
2139 In addition to a catered error message, also prints
2140 out where the user can find more help. Then, program exits.
2141 """
2142 self.print_line(msg + (' If you need more information, ' +
2143 'please run with -h or --help to see valid commands.'))
2144 sys.exit(1)
2145
2146 def find_bots_that_run_test(self, test, bots):
2147 matching_bots = []
2148 for bot in bots:
2149 bot_info = bots[bot]
2150 tests = self.flatten_tests_for_bot(bot_info)
2151 for test_info in tests:
Garrett Beatyffe83c4f2023-09-08 19:07:372152 test_name = test_info['name']
Karen Qiane24b7ee2019-02-12 23:37:062153 if not test_name == test:
2154 continue
2155 matching_bots.append(bot)
2156 return matching_bots
2157
2158 def find_tests_with_params(self, tests, params_dict):
2159 matching_tests = []
2160 for test_name in tests:
2161 test_info = tests[test_name]
2162 if not self.does_test_match(test_info, params_dict):
2163 continue
2164 if not test_name in matching_tests:
2165 matching_tests.append(test_name)
2166 return matching_tests
2167
2168 def flatten_waterfalls_for_query(self, waterfalls):
2169 bots = {}
2170 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012171 waterfall_tests = self.generate_output_tests(waterfall)
2172 for bot in waterfall_tests:
2173 bot_info = waterfall_tests[bot]
2174 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062175 return bots
2176
2177 def flatten_tests_for_bot(self, bot_info):
2178 """Returns a list of flattened tests.
2179
2180 Returns a list of tests not grouped by test category
2181 for a specific bot.
2182 """
2183 TEST_CATS = self.get_test_generator_map().keys()
2184 tests = []
2185 for test_cat in TEST_CATS:
2186 if not test_cat in bot_info:
2187 continue
2188 test_cat_tests = bot_info[test_cat]
2189 tests = tests + test_cat_tests
2190 return tests
2191
2192 def flatten_tests_for_query(self, test_suites):
2193 """Returns a flattened dictionary of tests.
2194
2195 Returns a dictionary of tests associate with their
2196 configuration, not grouped by their test suite.
2197 """
2198 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232199 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062200 for test in test_suite:
2201 test_info = test_suite[test]
2202 test_name = test
Karen Qiane24b7ee2019-02-12 23:37:062203 tests[test_name] = test_info
2204 return tests
2205
2206 def parse_query_filter_params(self, params):
2207 """Parses the filter parameters.
2208
2209 Creates a dictionary from the parameters provided
2210 to filter the bot array.
2211 """
2212 params_dict = {}
2213 for p in params:
2214 # flag
2215 if p.startswith("--"):
2216 params_dict[p] = True
2217 else:
2218 pair = p.split(":")
2219 if len(pair) != 2:
2220 self.error_msg('Invalid command.')
2221 # regular parameters
2222 if pair[1].lower() == "true":
2223 params_dict[pair[0]] = True
2224 elif pair[1].lower() == "false":
2225 params_dict[pair[0]] = False
2226 else:
2227 params_dict[pair[0]] = pair[1]
2228 return params_dict
2229
2230 def get_test_suites_dict(self, bots):
2231 """Returns a dictionary of bots and their tests.
2232
2233 Returns a dictionary of bots and a list of their associated tests.
2234 """
2235 test_suite_dict = dict()
2236 for bot in bots:
2237 bot_info = bots[bot]
2238 tests = self.flatten_tests_for_bot(bot_info)
2239 test_suite_dict[bot] = tests
2240 return test_suite_dict
2241
2242 def output_query_result(self, result, json_file=None):
2243 """Outputs the result of the query.
2244
2245 If a json file parameter name is provided, then
2246 the result is output into the json file. If not,
2247 then the result is printed to the console.
2248 """
2249 output = json.dumps(result, indent=2)
2250 if json_file:
2251 self.write_file(json_file, output)
2252 else:
2253 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062254
Joshua Hood56c673c2022-03-02 20:29:332255 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062256 def query(self, args):
2257 """Queries tests or bots.
2258
2259 Depending on the arguments provided, outputs a json of
2260 tests or bots matching the appropriate optional parameters provided.
2261 """
2262 # split up query statement
2263 query = args.query.split('/')
2264 self.load_configuration_files()
2265 self.resolve_configuration_files()
2266
2267 # flatten bots json
2268 tests = self.test_suites
2269 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2270
2271 cmd_class = query[0]
2272
2273 # For queries starting with 'bots'
2274 if cmd_class == "bots":
2275 if len(query) == 1:
2276 return self.output_query_result(bots, args.json)
2277 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332278 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062279 if query[1] == 'tests':
2280 test_suites_dict = self.get_test_suites_dict(bots)
2281 return self.output_query_result(test_suites_dict, args.json)
Joshua Hood56c673c2022-03-02 20:29:332282 self.error_msg("This query should be in the format: bots/tests.")
Karen Qiane24b7ee2019-02-12 23:37:062283
2284 else:
2285 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2286 % str(len(query)-1))
2287
2288 # For queries starting with 'bot'
2289 elif cmd_class == "bot":
2290 if not len(query) == 2 and not len(query) == 3:
2291 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2292 % str(len(query)-1))
2293 bot_id = query[1]
2294 if not bot_id in bots:
2295 self.error_msg("No bot named '" + bot_id + "' found.")
2296 bot_info = bots[bot_id]
2297 if len(query) == 2:
2298 return self.output_query_result(bot_info, args.json)
2299 if not query[2] == 'tests':
2300 self.error_msg("The query should be in the format:" +
2301 "bot/<bot-name>/tests.")
2302
2303 bot_tests = self.flatten_tests_for_bot(bot_info)
2304 return self.output_query_result(bot_tests, args.json)
2305
2306 # For queries starting with 'tests'
2307 elif cmd_class == "tests":
2308 if not len(query) == 1 and not len(query) == 2:
2309 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2310 % str(len(query)-1))
2311 flattened_tests = self.flatten_tests_for_query(tests)
2312 if len(query) == 1:
2313 return self.output_query_result(flattened_tests, args.json)
2314
2315 # create params dict
2316 params = query[1].split('&')
2317 params_dict = self.parse_query_filter_params(params)
2318 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2319 return self.output_query_result(matching_bots)
2320
2321 # For queries starting with 'test'
2322 elif cmd_class == "test":
2323 if not len(query) == 2 and not len(query) == 3:
2324 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2325 % str(len(query)-1))
2326 test_id = query[1]
2327 if len(query) == 2:
2328 flattened_tests = self.flatten_tests_for_query(tests)
2329 for test in flattened_tests:
2330 if test == test_id:
2331 return self.output_query_result(flattened_tests[test], args.json)
2332 self.error_msg("There is no test named %s." % test_id)
2333 if not query[2] == 'bots':
2334 self.error_msg("The query should be in the format: " +
2335 "test/<test-name>/bots")
2336 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2337 return self.output_query_result(bots_for_test)
2338
2339 else:
2340 self.error_msg("Your command did not match any valid commands." +
2341 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Joshua Hood56c673c2022-03-02 20:29:332342 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282343
Garrett Beaty1afaccc2020-06-25 19:58:152344 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282345 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502346 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062347 elif self.args.query:
2348 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282349 else:
Greg Gutermanf60eb052020-03-12 17:40:012350 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282351 return 0
2352
2353if __name__ == "__main__": # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152354 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2355 sys.exit(generator.main())