blob: feeb0d17f2210527ae1aec583294f220fdf6af2d [file] [log] [blame]
Joshua Hood3455e1352022-03-03 23:23:591#!/usr/bin/env vpython3
Avi Drissmandfd880852022-09-15 20:11:092# Copyright 2016 The Chromium Authors
Kenneth Russelleb60cbd22017-12-05 07:54:283# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Script to generate the majority of the JSON files in the src/testing/buildbot
7directory. Maintaining these files by hand is too unwieldy.
8"""
9
10import argparse
11import ast
12import collections
13import copy
John Budorick826d5ed2017-12-28 19:27:3214import difflib
Brian Sheedy822e03742024-08-09 18:48:1415import functools
Garrett Beatyd5ca75962020-05-07 16:58:3116import glob
Kenneth Russell8ceeabf2017-12-11 17:53:2817import itertools
Kenneth Russelleb60cbd22017-12-05 07:54:2818import json
19import os
20import string
21import sys
22
Brian Sheedya31578e2020-05-18 20:24:3623import buildbot_json_magic_substitutions as magic_substitutions
24
Joshua Hood56c673c2022-03-02 20:29:3325# pylint: disable=super-with-arguments,useless-super-delegation
26
Kenneth Russelleb60cbd22017-12-05 07:54:2827THIS_DIR = os.path.dirname(os.path.abspath(__file__))
28
Brian Sheedyf74819b2021-06-04 01:38:3829BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP = {
30 'android-chromium': '_android_chrome',
31 'android-chromium-monochrome': '_android_monochrome',
Brian Sheedyf74819b2021-06-04 01:38:3832 'android-webview': '_android_webview',
33}
34
Kenneth Russelleb60cbd22017-12-05 07:54:2835
36class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4937 def __init__(self, message):
38 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2839
40
Joshua Hood56c673c2022-03-02 20:29:3341class BaseGenerator(object): # pylint: disable=useless-object-inheritance
Kenneth Russelleb60cbd22017-12-05 07:54:2842 def __init__(self, bb_gen):
43 self.bb_gen = bb_gen
44
Kenneth Russell8ceeabf2017-12-11 17:53:2845 def generate(self, waterfall, tester_name, tester_config, input_tests):
Garrett Beatyffe83c4f2023-09-08 19:07:3746 raise NotImplementedError() # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:2847
48
Kenneth Russell8a386d42018-06-02 09:48:0149class GPUTelemetryTestGenerator(BaseGenerator):
Xinan Linedcf05b32023-10-19 23:13:5050 def __init__(self,
51 bb_gen,
52 is_android_webview=False,
53 is_cast_streaming=False,
54 is_skylab=False):
Kenneth Russell8a386d42018-06-02 09:48:0155 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5656 self._is_android_webview = is_android_webview
Fabrice de Ganscbd655f2022-08-04 20:15:3057 self._is_cast_streaming = is_cast_streaming
Xinan Linedcf05b32023-10-19 23:13:5058 self._is_skylab = is_skylab
Kenneth Russell8a386d42018-06-02 09:48:0159
60 def generate(self, waterfall, tester_name, tester_config, input_tests):
61 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:2362 for test_name, test_config in sorted(input_tests.items()):
Ben Pastene8e7eb2652022-04-29 19:44:3163 # Variants allow more than one definition for a given test, and is defined
64 # in array format from resolve_variants().
65 if not isinstance(test_config, list):
66 test_config = [test_config]
67
68 for config in test_config:
Xinan Linedcf05b32023-10-19 23:13:5069 test = self.bb_gen.generate_gpu_telemetry_test(
70 waterfall, tester_name, tester_config, test_name, config,
71 self._is_android_webview, self._is_cast_streaming, self._is_skylab)
Ben Pastene8e7eb2652022-04-29 19:44:3172 if test:
73 isolated_scripts.append(test)
74
Kenneth Russell8a386d42018-06-02 09:48:0175 return isolated_scripts
76
Kenneth Russell8a386d42018-06-02 09:48:0177
Brian Sheedyb6491ba2022-09-26 20:49:4978class SkylabGPUTelemetryTestGenerator(GPUTelemetryTestGenerator):
Xinan Linedcf05b32023-10-19 23:13:5079 def __init__(self, bb_gen):
80 super(SkylabGPUTelemetryTestGenerator, self).__init__(bb_gen,
81 is_skylab=True)
82
Brian Sheedyb6491ba2022-09-26 20:49:4983 def generate(self, *args, **kwargs):
84 # This should be identical to a regular GPU Telemetry test, but with any
85 # swarming arguments removed.
86 isolated_scripts = super(SkylabGPUTelemetryTestGenerator,
87 self).generate(*args, **kwargs)
88 for test in isolated_scripts:
Xinan Lind9b1d2e72022-11-14 20:57:0289 # chromium_GPU is the Autotest wrapper created for browser GPU tests
90 # run in Skylab.
Xinan Lin1f28a0d2023-03-13 17:39:4191 test['autotest_name'] = 'chromium_Graphics'
Xinan Lind9b1d2e72022-11-14 20:57:0292 # As of 22Q4, Skylab tests are running on a CrOS flavored Autotest
93 # framework and it does not support the sub-args like
94 # extra-browser-args. So we have to pop it out and create a new
95 # key for it. See crrev.com/c/3965359 for details.
96 for idx, arg in enumerate(test.get('args', [])):
97 if '--extra-browser-args' in arg:
98 test['args'].pop(idx)
99 test['extra_browser_args'] = arg.replace('--extra-browser-args=', '')
100 break
Brian Sheedyb6491ba2022-09-26 20:49:49101 return isolated_scripts
102
103
Kenneth Russelleb60cbd22017-12-05 07:54:28104class GTestGenerator(BaseGenerator):
Kenneth Russell8ceeabf2017-12-11 17:53:28105 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28106 # The relative ordering of some of the tests is important to
107 # minimize differences compared to the handwritten JSON files, since
108 # Python's sorts are stable and there are some tests with the same
109 # key (see gles2_conform_d3d9_test and similar variants). Avoid
110 # losing the order by avoiding coalescing the dictionaries into one.
111 gtests = []
Jamie Madillcf4f8c72021-05-20 19:24:23112 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoon67c3e832020-02-08 07:39:38113 # Variants allow more than one definition for a given test, and is defined
114 # in array format from resolve_variants().
115 if not isinstance(test_config, list):
116 test_config = [test_config]
117
118 for config in test_config:
119 test = self.bb_gen.generate_gtest(
120 waterfall, tester_name, tester_config, test_name, config)
121 if test:
122 # generate_gtest may veto the test generation on this tester.
123 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28124 return gtests
125
Kenneth Russelleb60cbd22017-12-05 07:54:28126
127class IsolatedScriptTestGenerator(BaseGenerator):
Kenneth Russell8ceeabf2017-12-11 17:53:28128 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28129 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23130 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43131 # Variants allow more than one definition for a given test, and is defined
132 # in array format from resolve_variants().
133 if not isinstance(test_config, list):
134 test_config = [test_config]
135
136 for config in test_config:
137 test = self.bb_gen.generate_isolated_script_test(
138 waterfall, tester_name, tester_config, test_name, config)
139 if test:
140 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28141 return isolated_scripts
142
Kenneth Russelleb60cbd22017-12-05 07:54:28143
144class ScriptGenerator(BaseGenerator):
Kenneth Russell8ceeabf2017-12-11 17:53:28145 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28146 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23147 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28148 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28149 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28150 if test:
151 scripts.append(test)
152 return scripts
153
Kenneth Russelleb60cbd22017-12-05 07:54:28154
Xinan Lin05fb9c1752020-12-17 00:15:52155class SkylabGenerator(BaseGenerator):
Xinan Lin05fb9c1752020-12-17 00:15:52156 def generate(self, waterfall, tester_name, tester_config, input_tests):
157 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23158 for test_name, test_config in sorted(input_tests.items()):
Xinan Lin05fb9c1752020-12-17 00:15:52159 for config in test_config:
160 test = self.bb_gen.generate_skylab_test(waterfall, tester_name,
161 tester_config, test_name,
162 config)
163 if test:
164 scripts.append(test)
165 return scripts
166
Xinan Lin05fb9c1752020-12-17 00:15:52167
Jeff Yoon67c3e832020-02-08 07:39:38168def check_compound_references(other_test_suites=None,
169 sub_suite=None,
170 suite=None,
171 target_test_suites=None,
172 test_type=None,
173 **kwargs):
174 """Ensure comound reference's don't target other compounds"""
175 del kwargs
176 if sub_suite in other_test_suites or sub_suite in target_test_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15177 raise BBGenErr('%s may not refer to other composition type test '
178 'suites (error found while processing %s)' %
179 (test_type, suite))
180
Jeff Yoon67c3e832020-02-08 07:39:38181
182def check_basic_references(basic_suites=None,
183 sub_suite=None,
184 suite=None,
185 **kwargs):
186 """Ensure test has a basic suite reference"""
187 del kwargs
188 if sub_suite not in basic_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15189 raise BBGenErr('Unable to find reference to %s while processing %s' %
190 (sub_suite, suite))
191
Jeff Yoon67c3e832020-02-08 07:39:38192
193def check_conflicting_definitions(basic_suites=None,
194 seen_tests=None,
195 sub_suite=None,
196 suite=None,
197 test_type=None,
Garrett Beaty235c1412023-08-29 20:26:29198 target_test_suites=None,
Jeff Yoon67c3e832020-02-08 07:39:38199 **kwargs):
200 """Ensure that if a test is reachable via multiple basic suites,
201 all of them have an identical definition of the tests.
202 """
203 del kwargs
Garrett Beaty235c1412023-08-29 20:26:29204 variants = None
205 if test_type == 'matrix_compound_suites':
206 variants = target_test_suites[suite][sub_suite].get('variants')
207 variants = variants or [None]
Jeff Yoon67c3e832020-02-08 07:39:38208 for test_name in basic_suites[sub_suite]:
Garrett Beaty235c1412023-08-29 20:26:29209 for variant in variants:
210 key = (test_name, variant)
211 if ((seen_sub_suite := seen_tests.get(key)) is not None
212 and basic_suites[sub_suite][test_name] !=
213 basic_suites[seen_sub_suite][test_name]):
214 test_description = (test_name if variant is None else
215 f'{test_name} with variant {variant} applied')
216 raise BBGenErr(
217 'Conflicting test definitions for %s from %s '
218 'and %s in %s (error found while processing %s)' %
219 (test_description, seen_tests[key], sub_suite, test_type, suite))
220 seen_tests[key] = sub_suite
221
Jeff Yoon67c3e832020-02-08 07:39:38222
223def check_matrix_identifier(sub_suite=None,
224 suite=None,
225 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05226 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38227 **kwargs):
228 """Ensure 'idenfitier' is defined for each variant"""
229 del kwargs
230 sub_suite_config = suite_def[sub_suite]
Garrett Beaty2022db42023-08-29 17:22:40231 for variant_name in sub_suite_config.get('variants', []):
232 if variant_name not in all_variants:
233 raise BBGenErr('Missing variant definition for %s in variants.pyl' %
234 variant_name)
235 variant = all_variants[variant_name]
Jeff Yoonda581c32020-03-06 03:56:05236
Jeff Yoon67c3e832020-02-08 07:39:38237 if not 'identifier' in variant:
238 raise BBGenErr('Missing required identifier field in matrix '
239 'compound suite %s, %s' % (suite, sub_suite))
Sven Zhengef0d0872022-04-04 22:13:29240 if variant['identifier'] == '':
241 raise BBGenErr('Identifier field can not be "" in matrix '
242 'compound suite %s, %s' % (suite, sub_suite))
243 if variant['identifier'].strip() != variant['identifier']:
244 raise BBGenErr('Identifier field can not have leading and trailing '
245 'whitespace in matrix compound suite %s, %s' %
246 (suite, sub_suite))
Jeff Yoon67c3e832020-02-08 07:39:38247
248
Joshua Hood56c673c2022-03-02 20:29:33249class BBJSONGenerator(object): # pylint: disable=useless-object-inheritance
Garrett Beaty1afaccc2020-06-25 19:58:15250 def __init__(self, args):
Garrett Beaty1afaccc2020-06-25 19:58:15251 self.args = args
Kenneth Russelleb60cbd22017-12-05 07:54:28252 self.waterfalls = None
253 self.test_suites = None
254 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01255 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41256 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05257 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28258
Garrett Beaty1afaccc2020-06-25 19:58:15259 @staticmethod
260 def parse_args(argv):
261
262 # RawTextHelpFormatter allows for styling of help statement
263 parser = argparse.ArgumentParser(
264 formatter_class=argparse.RawTextHelpFormatter)
265
266 group = parser.add_mutually_exclusive_group()
267 group.add_argument(
268 '-c',
269 '--check',
270 action='store_true',
271 help=
272 'Do consistency checks of configuration and generated files and then '
273 'exit. Used during presubmit. '
274 'Causes the tool to not generate any files.')
275 group.add_argument(
276 '--query',
277 type=str,
Brian Sheedy0d2300f32024-08-13 23:14:41278 help=('Returns raw JSON information of buildbots and tests.\n'
279 'Examples:\n List all bots (all info):\n'
280 ' --query bots\n\n'
281 ' List all bots and only their associated tests:\n'
282 ' --query bots/tests\n\n'
283 ' List all information about "bot1" '
284 '(make sure you have quotes):\n --query bot/"bot1"\n\n'
285 ' List tests running for "bot1" (make sure you have quotes):\n'
286 ' --query bot/"bot1"/tests\n\n List all tests:\n'
287 ' --query tests\n\n'
288 ' List all tests and the bots running them:\n'
289 ' --query tests/bots\n\n'
290 ' List all tests that satisfy multiple parameters\n'
291 ' (separation of parameters by "&" symbol):\n'
292 ' --query tests/"device_os:Android&device_type:hammerhead"\n\n'
293 ' List all tests that run with a specific flag:\n'
294 ' --query bots/"--test-launcher-print-test-studio=always"\n\n'
295 ' List specific test (make sure you have quotes):\n'
296 ' --query test/"test1"\n\n'
297 ' List all bots running "test1" '
298 '(make sure you have quotes):\n --query test/"test1"/bots'))
Garrett Beaty1afaccc2020-06-25 19:58:15299 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47300 '--json',
301 metavar='JSON_FILE_PATH',
302 type=os.path.abspath,
303 help='Outputs results into a json file. Only works with query function.'
304 )
305 parser.add_argument(
Garrett Beaty1afaccc2020-06-25 19:58:15306 '-n',
307 '--new-files',
308 action='store_true',
309 help=
310 'Write output files as .new.json. Useful during development so old and '
311 'new files can be looked at side-by-side.')
Garrett Beatyade673d2023-08-04 22:00:25312 parser.add_argument('--dimension-sets-handling',
313 choices=['disable'],
314 default='disable',
315 help=('This flag no longer has any effect:'
316 ' dimension_sets fields are not allowed'))
Garrett Beaty1afaccc2020-06-25 19:58:15317 parser.add_argument('-v',
318 '--verbose',
319 action='store_true',
320 help='Increases verbosity. Affects consistency checks.')
321 parser.add_argument('waterfall_filters',
322 metavar='waterfalls',
323 type=str,
324 nargs='*',
325 help='Optional list of waterfalls to generate.')
326 parser.add_argument(
327 '--pyl-files-dir',
Garrett Beaty79339e182023-04-10 20:45:47328 type=os.path.abspath,
329 help=('Path to the directory containing the input .pyl files.'
330 ' By default the directory containing this script will be used.'))
Garrett Beaty1afaccc2020-06-25 19:58:15331 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47332 '--output-dir',
333 type=os.path.abspath,
334 help=('Path to the directory to output generated .json files.'
335 'By default, the pyl files directory will be used.'))
Chong Guee622242020-10-28 18:17:35336 parser.add_argument('--isolate-map-file',
337 metavar='PATH',
338 help='path to additional isolate map files.',
Garrett Beaty79339e182023-04-10 20:45:47339 type=os.path.abspath,
Chong Guee622242020-10-28 18:17:35340 default=[],
341 action='append',
342 dest='isolate_map_files')
Garrett Beaty1afaccc2020-06-25 19:58:15343 parser.add_argument(
344 '--infra-config-dir',
345 help='Path to the LUCI services configuration directory',
Garrett Beaty79339e182023-04-10 20:45:47346 type=os.path.abspath,
347 default=os.path.join(os.path.dirname(__file__), '..', '..', 'infra',
348 'config'))
349
Garrett Beaty1afaccc2020-06-25 19:58:15350 args = parser.parse_args(argv)
351 if args.json and not args.query:
352 parser.error(
Brian Sheedy0d2300f32024-08-13 23:14:41353 'The --json flag can only be used with --query.') # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:15354
Garrett Beaty79339e182023-04-10 20:45:47355 args.pyl_files_dir = args.pyl_files_dir or THIS_DIR
356 args.output_dir = args.output_dir or args.pyl_files_dir
357
Garrett Beatyee0e5552024-08-28 18:58:18358 def pyl_dir_path(filename):
Garrett Beaty79339e182023-04-10 20:45:47359 return os.path.join(args.pyl_files_dir, filename)
360
Garrett Beatyee0e5552024-08-28 18:58:18361 args.waterfalls_pyl_path = pyl_dir_path('waterfalls.pyl')
362 args.test_suite_exceptions_pyl_path = pyl_dir_path(
Garrett Beaty79339e182023-04-10 20:45:47363 'test_suite_exceptions.pyl')
Garrett Beaty4999e9792024-04-03 23:29:11364 args.autoshard_exceptions_json_path = os.path.join(
365 args.infra_config_dir, 'targets', 'autoshard_exceptions.json')
Garrett Beaty79339e182023-04-10 20:45:47366
Garrett Beatyee0e5552024-08-28 18:58:18367 if args.pyl_files_dir == THIS_DIR:
368
369 def infra_config_testing_path(filename):
370 return os.path.join(args.infra_config_dir, 'generated', 'testing',
371 filename)
372
373 args.gn_isolate_map_pyl_path = infra_config_testing_path(
374 'gn_isolate_map.pyl')
375 args.mixins_pyl_path = infra_config_testing_path('mixins.pyl')
376 args.test_suites_pyl_path = infra_config_testing_path('test_suites.pyl')
377 args.variants_pyl_path = infra_config_testing_path('variants.pyl')
378 else:
379 args.gn_isolate_map_pyl_path = pyl_dir_path('gn_isolate_map.pyl')
380 args.mixins_pyl_path = pyl_dir_path('mixins.pyl')
381 args.test_suites_pyl_path = pyl_dir_path('test_suites.pyl')
382 args.variants_pyl_path = pyl_dir_path('variants.pyl')
383
Garrett Beaty79339e182023-04-10 20:45:47384 return args
Kenneth Russelleb60cbd22017-12-05 07:54:28385
Stephen Martinis7eb8b612018-09-21 00:17:50386 def print_line(self, line):
387 # Exists so that tests can mock
Jamie Madillcf4f8c72021-05-20 19:24:23388 print(line) # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:50389
Kenneth Russelleb60cbd22017-12-05 07:54:28390 def read_file(self, relative_path):
Garrett Beaty79339e182023-04-10 20:45:47391 with open(relative_path) as fp:
Garrett Beaty1afaccc2020-06-25 19:58:15392 return fp.read()
Kenneth Russelleb60cbd22017-12-05 07:54:28393
Garrett Beaty79339e182023-04-10 20:45:47394 def write_file(self, file_path, contents):
Peter Kastingacd55c12023-08-23 20:19:04395 with open(file_path, 'w', newline='') as fp:
Garrett Beaty79339e182023-04-10 20:45:47396 fp.write(contents)
Zhiling Huangbe008172018-03-08 19:13:11397
Joshua Hood56c673c2022-03-02 20:29:33398 # pylint: disable=inconsistent-return-statements
Garrett Beaty79339e182023-04-10 20:45:47399 def load_pyl_file(self, pyl_file_path):
Kenneth Russelleb60cbd22017-12-05 07:54:28400 try:
Garrett Beaty79339e182023-04-10 20:45:47401 return ast.literal_eval(self.read_file(pyl_file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:28402 except (SyntaxError, ValueError) as e: # pragma: no cover
Josip Sokcevic7110fb382023-06-06 01:05:29403 raise BBGenErr('Failed to parse pyl file "%s": %s' %
404 (pyl_file_path, e)) from e
Joshua Hood56c673c2022-03-02 20:29:33405 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:28406
Kenneth Russell8a386d42018-06-02 09:48:01407 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
408 # Currently it is only mandatory for bots which run GPU tests. Change these to
409 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28410 def is_android(self, tester_config):
411 return tester_config.get('os_type') == 'android'
412
Ben Pastenea9e583b2019-01-16 02:57:26413 def is_chromeos(self, tester_config):
414 return tester_config.get('os_type') == 'chromeos'
415
Chong Guc2ca5d02022-01-11 19:52:17416 def is_fuchsia(self, tester_config):
417 return tester_config.get('os_type') == 'fuchsia'
418
Brian Sheedy781c8ca42021-03-08 22:03:21419 def is_lacros(self, tester_config):
420 return tester_config.get('os_type') == 'lacros'
421
Kenneth Russell8a386d42018-06-02 09:48:01422 def is_linux(self, tester_config):
423 return tester_config.get('os_type') == 'linux'
424
Kai Ninomiya40de9f52019-10-18 21:38:49425 def is_mac(self, tester_config):
426 return tester_config.get('os_type') == 'mac'
427
428 def is_win(self, tester_config):
429 return tester_config.get('os_type') == 'win'
430
431 def is_win64(self, tester_config):
432 return (tester_config.get('os_type') == 'win' and
433 tester_config.get('browser_config') == 'release_x64')
434
Garrett Beatyffe83c4f2023-09-08 19:07:37435 def get_exception_for_test(self, test_config):
436 return self.exceptions.get(test_config['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28437
Garrett Beatyffe83c4f2023-09-08 19:07:37438 def should_run_on_tester(self, waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28439 # Currently, the only reason a test should not run on a given tester is that
440 # it's in the exceptions. (Once the GPU waterfall generation script is
441 # incorporated here, the rules will become more complex.)
Garrett Beatyffe83c4f2023-09-08 19:07:37442 exception = self.get_exception_for_test(test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28443 if not exception:
444 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28445 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28446 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28447 if remove_from:
448 if tester_name in remove_from:
449 return False
450 # TODO(kbr): this code path was added for some tests (including
451 # android_webview_unittests) on one machine (Nougat Phone
452 # Tester) which exists with the same name on two waterfalls,
453 # chromium.android and chromium.fyi; the tests are run on one
454 # but not the other. Once the bots are all uniquely named (a
455 # different ongoing project) this code should be removed.
456 # TODO(kbr): add coverage.
457 return (tester_name + ' ' + waterfall['name']
458 not in remove_from) # pragma: no cover
459 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28460
Garrett Beatyffe83c4f2023-09-08 19:07:37461 def get_test_modifications(self, test, tester_name):
462 exception = self.get_exception_for_test(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28463 if not exception:
464 return None
Nico Weber79dc5f6852018-07-13 19:38:49465 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28466
Garrett Beatyffe83c4f2023-09-08 19:07:37467 def get_test_replacements(self, test, tester_name):
468 exception = self.get_exception_for_test(test)
Brian Sheedye6ea0ee2019-07-11 02:54:37469 if not exception:
470 return None
471 return exception.get('replacements', {}).get(tester_name)
472
Kenneth Russell8a386d42018-06-02 09:48:01473 def merge_command_line_args(self, arr, prefix, splitter):
474 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01475 idx = 0
476 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01477 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01478 while idx < len(arr):
479 flag = arr[idx]
480 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01481 if flag.startswith(prefix):
482 arg = flag[prefix_len:]
483 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01484 if first_idx < 0:
485 first_idx = idx
486 else:
487 delete_current_entry = True
488 if delete_current_entry:
489 del arr[idx]
490 else:
491 idx += 1
492 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01493 arr[first_idx] = prefix + splitter.join(accumulated_args)
494 return arr
495
496 def maybe_fixup_args_array(self, arr):
497 # The incoming array of strings may be an array of command line
498 # arguments. To make it easier to turn on certain features per-bot or
499 # per-test-suite, look specifically for certain flags and merge them
500 # appropriately.
501 # --enable-features=Feature1 --enable-features=Feature2
502 # are merged to:
503 # --enable-features=Feature1,Feature2
504 # and:
505 # --extra-browser-args=arg1 --extra-browser-args=arg2
506 # are merged to:
507 # --extra-browser-args=arg1 arg2
508 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
509 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Yuly Novikov8c487e72020-10-16 20:00:29510 arr = self.merge_command_line_args(arr, '--test-launcher-filter-file=', ';')
Cameron Higgins971f0b92023-01-03 18:05:09511 arr = self.merge_command_line_args(arr, '--extra-app-args=', ',')
Kenneth Russell650995a2018-05-03 21:17:01512 return arr
513
Brian Sheedy910cda82022-07-19 11:58:34514 def substitute_magic_args(self, test_config, tester_name, tester_config):
Brian Sheedya31578e2020-05-18 20:24:36515 """Substitutes any magic substitution args present in |test_config|.
516
517 Substitutions are done in-place.
518
519 See buildbot_json_magic_substitutions.py for more information on this
520 feature.
521
522 Args:
523 test_config: A dict containing a configuration for a specific test on
Garrett Beatye3a606ceb2024-04-30 22:13:13524 a specific builder.
Brian Sheedy5f173bb2021-11-24 00:45:54525 tester_name: A string containing the name of the tester that |test_config|
526 came from.
Brian Sheedy910cda82022-07-19 11:58:34527 tester_config: A dict containing the configuration for the builder that
528 |test_config| is for.
Brian Sheedya31578e2020-05-18 20:24:36529 """
530 substituted_array = []
Brian Sheedyba13cf522022-09-13 21:00:09531 original_args = test_config.get('args', [])
532 for arg in original_args:
Brian Sheedya31578e2020-05-18 20:24:36533 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
534 function = arg.replace(
535 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
536 if hasattr(magic_substitutions, function):
537 substituted_array.extend(
Brian Sheedy910cda82022-07-19 11:58:34538 getattr(magic_substitutions, function)(test_config, tester_name,
539 tester_config))
Brian Sheedya31578e2020-05-18 20:24:36540 else:
541 raise BBGenErr(
542 'Magic substitution function %s does not exist' % function)
543 else:
544 substituted_array.append(arg)
Brian Sheedyba13cf522022-09-13 21:00:09545 if substituted_array != original_args:
Brian Sheedya31578e2020-05-18 20:24:36546 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
547
Garrett Beaty8d6708c2023-07-20 17:20:41548 def dictionary_merge(self, a, b, path=None):
Kenneth Russelleb60cbd22017-12-05 07:54:28549 """http://stackoverflow.com/questions/7204805/
550 python-dictionaries-of-dictionaries-merge
551 merges b into a
552 """
553 if path is None:
554 path = []
555 for key in b:
Garrett Beaty8d6708c2023-07-20 17:20:41556 if key not in a:
557 if b[key] is not None:
558 a[key] = b[key]
559 continue
560
561 if isinstance(a[key], dict) and isinstance(b[key], dict):
562 self.dictionary_merge(a[key], b[key], path + [str(key)])
563 elif a[key] == b[key]:
564 pass # same leaf value
565 elif isinstance(a[key], list) and isinstance(b[key], list):
Garrett Beatyade673d2023-08-04 22:00:25566 a[key] = a[key] + b[key]
567 if key.endswith('args'):
568 a[key] = self.maybe_fixup_args_array(a[key])
Garrett Beaty8d6708c2023-07-20 17:20:41569 elif b[key] is None:
570 del a[key]
571 else:
Kenneth Russelleb60cbd22017-12-05 07:54:28572 a[key] = b[key]
Garrett Beaty8d6708c2023-07-20 17:20:41573
Kenneth Russelleb60cbd22017-12-05 07:54:28574 return a
575
Kenneth Russelleb60cbd22017-12-05 07:54:28576 def clean_swarming_dictionary(self, swarming_dict):
577 # Clean out redundant entries from a test's "swarming" dictionary.
578 # This is really only needed to retain 100% parity with the
579 # handwritten JSON files, and can be removed once all the files are
580 # autogenerated.
581 if 'shards' in swarming_dict:
582 if swarming_dict['shards'] == 1: # pragma: no cover
583 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24584 if 'hard_timeout' in swarming_dict:
585 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
586 del swarming_dict['hard_timeout'] # pragma: no cover
Garrett Beatybb18d532023-06-26 22:16:33587 del swarming_dict['can_use_on_swarming_builders']
Kenneth Russelleb60cbd22017-12-05 07:54:28588
Garrett Beatye3a606ceb2024-04-30 22:13:13589 def resolve_os_conditional_values(self, test, builder):
590 for key, fn in (
591 ('android_swarming', self.is_android),
592 ('chromeos_swarming', self.is_chromeos),
593 ):
594 swarming = test.pop(key, None)
595 if swarming and fn(builder):
596 self.dictionary_merge(test['swarming'], swarming)
597
598 for key, fn in (
599 ('desktop_args', lambda cfg: not self.is_android(cfg)),
600 ('lacros_args', self.is_lacros),
601 ('linux_args', self.is_linux),
602 ('android_args', self.is_android),
603 ('chromeos_args', self.is_chromeos),
604 ('mac_args', self.is_mac),
605 ('win_args', self.is_win),
606 ('win64_args', self.is_win64),
607 ):
608 args = test.pop(key, [])
609 if fn(builder):
610 test.setdefault('args', []).extend(args)
611
612 def apply_common_transformations(self,
613 waterfall,
614 builder_name,
615 builder,
616 test,
617 test_name,
618 *,
619 swarmable=True,
620 supports_args=True):
621 # Initialize the swarming dictionary
622 swarmable = swarmable and builder.get('use_swarming', True)
623 test.setdefault('swarming', {}).setdefault('can_use_on_swarming_builders',
624 swarmable)
625
Garrett Beaty4b9f1752024-09-26 20:02:50626 # Test common mixins are mixins specified in the test declaration itself. To
627 # match the order of expansion in starlark, they take effect before anything
628 # specified in the legacy_test_config.
629 test_common = test.pop('test_common', {})
630 if test_common:
631 test_common_mixins = test_common.pop('mixins', [])
632 self.ensure_valid_mixin_list(test_common_mixins,
633 f'test {test_name} test_common mixins')
634 test_common = self.apply_mixins(test_common, test_common_mixins, [],
635 builder)
636 test = self.apply_mixin(test, test_common, builder)
637
Garrett Beatye3a606ceb2024-04-30 22:13:13638 mixins_to_ignore = test.pop('remove_mixins', [])
639 self.ensure_valid_mixin_list(mixins_to_ignore,
640 f'test {test_name} remove_mixins')
641
Garrett Beatycc184692024-05-01 14:57:09642 # Expand any conditional values
643 self.resolve_os_conditional_values(test, builder)
644
645 # Apply mixins from the test
646 test_mixins = test.pop('mixins', [])
647 self.ensure_valid_mixin_list(test_mixins, f'test {test_name} mixins')
648 test = self.apply_mixins(test, test_mixins, mixins_to_ignore, builder)
649
Garrett Beaty65b7d362024-10-01 16:21:42650 # Apply any variant details
651 variant = test.pop('*variant*', None)
652 if variant is not None:
653 test = self.apply_mixin(variant, test)
654 variant_mixins = test.pop('*variant_mixins*', [])
655 self.ensure_valid_mixin_list(
656 variant_mixins,
657 (f'variant mixins for test {test_name}'
658 f' with variant with identifier{test["variant_id"]}'))
659 test = self.apply_mixins(test, variant_mixins, mixins_to_ignore, builder)
660
Garrett Beatye3a606ceb2024-04-30 22:13:13661 # Add any swarming or args from the builder
662 self.dictionary_merge(test['swarming'], builder.get('swarming', {}))
663 if supports_args:
664 test.setdefault('args', []).extend(builder.get('args', []))
665
Garrett Beatye3a606ceb2024-04-30 22:13:13666 # Apply mixins from the waterfall
667 waterfall_mixins = waterfall.get('mixins', [])
668 self.ensure_valid_mixin_list(waterfall_mixins,
669 f"waterfall {waterfall['name']} mixins")
670 test = self.apply_mixins(test, waterfall_mixins, mixins_to_ignore, builder)
671
672 # Apply mixins from the builder
673 builder_mixins = builder.get('mixins', [])
674 self.ensure_valid_mixin_list(builder_mixins,
Brian Sheedy0d2300f32024-08-13 23:14:41675 f'builder {builder_name} mixins')
Garrett Beatye3a606ceb2024-04-30 22:13:13676 test = self.apply_mixins(test, builder_mixins, mixins_to_ignore, builder)
677
Kenneth Russelleb60cbd22017-12-05 07:54:28678 # See if there are any exceptions that need to be merged into this
679 # test's specification.
Garrett Beatye3a606ceb2024-04-30 22:13:13680 modifications = self.get_test_modifications(test, builder_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28681 if modifications:
682 test = self.dictionary_merge(test, modifications)
Garrett Beatye3a606ceb2024-04-30 22:13:13683
684 # Clean up the swarming entry or remove it if it's unnecessary
Garrett Beatybfeff8f2023-06-16 18:57:25685 if (swarming_dict := test.get('swarming')) is not None:
Garrett Beatybb18d532023-06-26 22:16:33686 if swarming_dict.get('can_use_on_swarming_builders'):
Garrett Beatybfeff8f2023-06-16 18:57:25687 self.clean_swarming_dictionary(swarming_dict)
688 else:
689 del test['swarming']
Garrett Beatye3a606ceb2024-04-30 22:13:13690
Ben Pastenee012aea42019-05-14 22:32:28691 # Ensure all Android Swarming tests run only on userdebug builds if another
692 # build type was not specified.
Garrett Beatye3a606ceb2024-04-30 22:13:13693 if 'swarming' in test and self.is_android(builder):
Garrett Beatyade673d2023-08-04 22:00:25694 dimensions = test.get('swarming', {}).get('dimensions', {})
695 if (dimensions.get('os') == 'Android'
696 and not dimensions.get('device_os_type')):
697 dimensions['device_os_type'] = 'userdebug'
Garrett Beatye3a606ceb2024-04-30 22:13:13698
699 # Apply any replacements specified for the test for the builder
700 self.replace_test_args(test, test_name, builder_name)
701
702 # Remove args if it is empty
703 if 'args' in test:
704 if not test['args']:
705 del test['args']
706 else:
707 # Replace any magic arguments with their actual value
708 self.substitute_magic_args(test, builder_name, builder)
709
710 test['args'] = self.maybe_fixup_args_array(test['args'])
Ben Pastenee012aea42019-05-14 22:32:28711
Kenneth Russelleb60cbd22017-12-05 07:54:28712 return test
713
Brian Sheedye6ea0ee2019-07-11 02:54:37714 def replace_test_args(self, test, test_name, tester_name):
Garrett Beatyffe83c4f2023-09-08 19:07:37715 replacements = self.get_test_replacements(test, tester_name) or {}
Brian Sheedye6ea0ee2019-07-11 02:54:37716 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23717 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37718 if key not in valid_replacement_keys:
719 raise BBGenErr(
720 'Given replacement key %s for %s on %s is not in the list of valid '
721 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23722 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37723 found_key = False
724 for i, test_key in enumerate(test.get(key, [])):
725 # Handle both the key/value being replaced being defined as two
726 # separate items or as key=value.
727 if test_key == replacement_key:
728 found_key = True
729 # Handle flags without values.
Brian Sheedy822e03742024-08-09 18:48:14730 if replacement_val is None:
Brian Sheedye6ea0ee2019-07-11 02:54:37731 del test[key][i]
732 else:
733 test[key][i+1] = replacement_val
734 break
Joshua Hood56c673c2022-03-02 20:29:33735 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37736 found_key = True
Brian Sheedy822e03742024-08-09 18:48:14737 if replacement_val is None:
Brian Sheedye6ea0ee2019-07-11 02:54:37738 del test[key][i]
739 else:
740 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
741 break
742 if not found_key:
743 raise BBGenErr('Could not find %s in existing list of values for key '
744 '%s in %s on %s' % (replacement_key, key, test_name,
745 tester_name))
746
Shenghua Zhangaba8bad2018-02-07 02:12:09747 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05748 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26749 True):
750 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06751 # are targeting CrOS hardware and so need the special trigger script.
Garrett Beatyade673d2023-08-04 22:00:25752 if 'device_type' in test.get('swarming', {}).get('dimensions', {}):
Ben Pastenea9e583b2019-01-16 02:57:26753 test['trigger_script'] = {
754 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
755 }
Shenghua Zhangaba8bad2018-02-07 02:12:09756
Garrett Beatyffe83c4f2023-09-08 19:07:37757 def add_android_presentation_args(self, tester_config, result):
John Budorick262ae112019-07-12 19:24:38758 bucket = tester_config.get('results_bucket', 'chromium-result-details')
Garrett Beaty94af4272024-04-17 18:06:14759 result.setdefault('args', []).append('--gs-results-bucket=%s' % bucket)
760
761 if ('swarming' in result and 'merge' not in 'result'
762 and not tester_config.get('skip_merge_script', False)):
Ben Pastene858f4be2019-01-09 23:52:09763 result['merge'] = {
Garrett Beatyffe83c4f2023-09-08 19:07:37764 'args': [
765 '--bucket',
766 bucket,
767 '--test-name',
768 result['name'],
769 ],
770 'script': ('//build/android/pylib/results/presentation/'
771 'test_results_presentation.py'),
Ben Pastene858f4be2019-01-09 23:52:09772 }
Ben Pastene858f4be2019-01-09 23:52:09773
Kenneth Russelleb60cbd22017-12-05 07:54:28774 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
775 test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37776 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28777 return None
778 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37779 # Use test_name here instead of test['name'] because test['name'] will be
780 # modified with the variant identifier in a matrix compound suite
781 result.setdefault('test', test_name)
John Budorickab108712018-09-01 00:12:21782
Garrett Beatye3a606ceb2024-04-30 22:13:13783 result = self.apply_common_transformations(waterfall, tester_name,
784 tester_config, result, test_name)
Garrett Beaty94af4272024-04-17 18:06:14785 if self.is_android(tester_config) and 'swarming' in result:
786 if not result.get('use_isolated_scripts_api', False):
Alison Gale71bd8f152024-04-26 22:38:20787 # TODO(crbug.com/40725094) make Android presentation work with
Yuly Novikov26dd47052021-02-11 00:57:14788 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37789 self.add_android_presentation_args(tester_config, result)
Yuly Novikov26dd47052021-02-11 00:57:14790 result['args'] = result.get('args', []) + ['--recover-devices']
Shenghua Zhangaba8bad2018-02-07 02:12:09791 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43792
Garrett Beatybb18d532023-06-26 22:16:33793 if 'swarming' in result and not result.get('merge'):
Jamie Madilla8be0d72020-10-02 05:24:04794 if test_config.get('use_isolated_scripts_api', False):
795 merge_script = 'standard_isolated_script_merge'
796 else:
797 merge_script = 'standard_gtest_merge'
798
Stephen Martinisbc7b7772019-05-01 22:01:43799 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04800 'script': '//testing/merge_scripts/%s.py' % merge_script,
Stephen Martinisbc7b7772019-05-01 22:01:43801 }
Kenneth Russelleb60cbd22017-12-05 07:54:28802 return result
803
804 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
805 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37806 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28807 return None
808 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37809 # Use test_name here instead of test['name'] because test['name'] will be
810 # modified with the variant identifier in a matrix compound suite
Garrett Beatydca3d882023-09-14 23:50:32811 result.setdefault('test', test_name)
Garrett Beatye3a606ceb2024-04-30 22:13:13812 result = self.apply_common_transformations(waterfall, tester_name,
813 tester_config, result, test_name)
Garrett Beaty94af4272024-04-17 18:06:14814 if self.is_android(tester_config) and 'swarming' in result:
Yuly Novikov26dd47052021-02-11 00:57:14815 if tester_config.get('use_android_presentation', False):
Alison Gale71bd8f152024-04-26 22:38:20816 # TODO(crbug.com/40725094) make Android presentation work with
Yuly Novikov26dd47052021-02-11 00:57:14817 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37818 self.add_android_presentation_args(tester_config, result)
Shenghua Zhangaba8bad2018-02-07 02:12:09819 self.add_common_test_properties(result, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17820
Garrett Beatybb18d532023-06-26 22:16:33821 if 'swarming' in result and not result.get('merge'):
Alison Gale923a33e2024-04-22 23:34:28822 # TODO(crbug.com/41456107): Consider adding the ability to not have
Stephen Martinisf50047062019-05-06 22:26:17823 # this default.
824 result['merge'] = {
825 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
Stephen Martinisf50047062019-05-06 22:26:17826 }
Kenneth Russelleb60cbd22017-12-05 07:54:28827 return result
828
Garrett Beaty938560e32024-09-26 18:57:35829 _SCRIPT_FIELDS = ('name', 'script', 'args', 'precommit_args',
830 'non_precommit_args', 'resultdb')
831
Kenneth Russelleb60cbd22017-12-05 07:54:28832 def generate_script_test(self, waterfall, tester_name, tester_config,
833 test_name, test_config):
Alison Gale47d1537d2024-04-19 21:31:46834 # TODO(crbug.com/40623237): Remove this check whenever a better
Brian Sheedy158cd0f2019-04-26 01:12:44835 # long-term solution is implemented.
836 if (waterfall.get('forbid_script_tests', False) or
837 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
838 raise BBGenErr('Attempted to generate a script test on tester ' +
839 tester_name + ', which explicitly forbids script tests')
Garrett Beatyffe83c4f2023-09-08 19:07:37840 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28841 return None
Garrett Beaty938560e32024-09-26 18:57:35842 result = copy.deepcopy(test_config)
Garrett Beatye3a606ceb2024-04-30 22:13:13843 result = self.apply_common_transformations(waterfall,
844 tester_name,
845 tester_config,
846 result,
847 test_name,
848 swarmable=False,
849 supports_args=False)
Garrett Beaty938560e32024-09-26 18:57:35850 result = {k: result[k] for k in self._SCRIPT_FIELDS if k in result}
Kenneth Russelleb60cbd22017-12-05 07:54:28851 return result
852
Xinan Lin05fb9c1752020-12-17 00:15:52853 def generate_skylab_test(self, waterfall, tester_name, tester_config,
854 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37855 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Xinan Lin05fb9c1752020-12-17 00:15:52856 return None
857 result = copy.deepcopy(test_config)
Brian Sheedy67937ad12024-03-06 22:53:55858 result.setdefault('test', test_name)
Struan Shrimpton08baa3c2024-08-09 17:21:45859 result['run_cft'] = True
yoshiki iguchid1664ef2024-03-28 19:16:52860
861 if 'cros_board' in result or 'cros_board' in tester_config:
862 result['cros_board'] = tester_config.get('cros_board') or result.get(
863 'cros_board')
864 else:
Brian Sheedy0d2300f32024-08-13 23:14:41865 raise BBGenErr('skylab tests must specify cros_board.')
yoshiki iguchid1664ef2024-03-28 19:16:52866 if 'cros_model' in result or 'cros_model' in tester_config:
867 result['cros_model'] = tester_config.get('cros_model') or result.get(
868 'cros_model')
869 if 'dut_pool' in result or 'cros_dut_pool' in tester_config:
870 result['dut_pool'] = tester_config.get('cros_dut_pool') or result.get(
871 'dut_pool')
Qijiang Fan9032762d2024-06-25 06:02:24872 if 'cros_build_target' in result or 'cros_build_target' in tester_config:
873 result['cros_build_target'] = tester_config.get(
874 'cros_build_target') or result.get('cros_build_target')
yoshiki iguchid1664ef2024-03-28 19:16:52875
yoshiki iguchia5f87c7d2024-06-19 02:48:34876 # Skylab tests enable the shard-level-retry by default.
877 if ('shard_level_retries_on_ctp' in result
878 or 'shard_level_retries_on_ctp' in tester_config):
879 result['shard_level_retries_on_ctp'] = (
880 tester_config.get('shard_level_retries_on_ctp')
881 or result.get('shard_level_retries_on_ctp'))
Qijiang Fan84a0286b2024-06-25 06:44:08882 elif result.get('experiment_percentage') != 100:
yoshiki iguchia5f87c7d2024-06-19 02:48:34883 result['shard_level_retries_on_ctp'] = 1
884
Garrett Beatye3a606ceb2024-04-30 22:13:13885 result = self.apply_common_transformations(waterfall,
886 tester_name,
887 tester_config,
888 result,
889 test_name,
890 swarmable=False)
Xinan Lin05fb9c1752020-12-17 00:15:52891 return result
892
Garrett Beaty65d44222023-08-01 17:22:11893 def substitute_gpu_args(self, tester_config, test, args):
Kenneth Russell8a386d42018-06-02 09:48:01894 substitutions = {
895 # Any machine in waterfalls.pyl which desires to run GPU tests
896 # must provide the os_type key.
897 'os_type': tester_config['os_type'],
898 'gpu_vendor_id': '0',
899 'gpu_device_id': '0',
900 }
Garrett Beatyade673d2023-08-04 22:00:25901 dimensions = test.get('swarming', {}).get('dimensions', {})
902 if 'gpu' in dimensions:
903 # First remove the driver version, then split into vendor and device.
904 gpu = dimensions['gpu']
905 if gpu != 'none':
906 gpu = gpu.split('-')[0].split(':')
907 substitutions['gpu_vendor_id'] = gpu[0]
908 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01909 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
910
Garrett Beaty7436fb72024-08-07 20:20:58911 # LINT.IfChange(gpu_telemetry_test)
912
Kenneth Russell8a386d42018-06-02 09:48:01913 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30914 test_name, test_config, is_android_webview,
Xinan Linedcf05b32023-10-19 23:13:50915 is_cast_streaming, is_skylab):
Kenneth Russell8a386d42018-06-02 09:48:01916 # These are all just specializations of isolated script tests with
917 # a bunch of boilerplate command line arguments added.
918
919 # The step name must end in 'test' or 'tests' in order for the
920 # results to automatically show up on the flakiness dashboard.
921 # (At least, this was true some time ago.) Continue to use this
922 # naming convention for the time being to minimize changes.
Garrett Beaty235c1412023-08-29 20:26:29923 #
924 # test name is the name of the test without the variant ID added
925 if not (test_name.endswith('test') or test_name.endswith('tests')):
926 raise BBGenErr(
927 f'telemetry test names must end with test or tests, got {test_name}')
Garrett Beatyffe83c4f2023-09-08 19:07:37928 result = self.generate_isolated_script_test(waterfall, tester_name,
929 tester_config, test_name,
930 test_config)
Kenneth Russell8a386d42018-06-02 09:48:01931 if not result:
932 return None
Garrett Beatydca3d882023-09-14 23:50:32933 result['test'] = test_config.get('test') or self.get_default_isolate_name(
934 tester_config, is_android_webview)
Chan Liab7d8dd82020-04-24 23:42:19935
Chan Lia3ad1502020-04-28 05:32:11936 # Populate test_id_prefix.
Garrett Beatydca3d882023-09-14 23:50:32937 gn_entry = self.gn_isolate_map[result['test']]
Chan Li17d969f92020-07-10 00:50:03938 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19939
Kenneth Russell8a386d42018-06-02 09:48:01940 args = result.get('args', [])
Garrett Beatyffe83c4f2023-09-08 19:07:37941 # Use test_name here instead of test['name'] because test['name'] will be
942 # modified with the variant identifier in a matrix compound suite
Kenneth Russell8a386d42018-06-02 09:48:01943 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14944
945 # These tests upload and download results from cloud storage and therefore
946 # aren't idempotent yet. https://crbug.com/549140.
Garrett Beatybfeff8f2023-06-16 18:57:25947 if 'swarming' in result:
948 result['swarming']['idempotent'] = False
erikchen6da2d9b2018-08-03 23:01:14949
Fabrice de Ganscbd655f2022-08-04 20:15:30950 browser = ''
951 if is_cast_streaming:
952 browser = 'cast-streaming-shell'
953 elif is_android_webview:
954 browser = 'android-webview-instrumentation'
955 else:
956 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:52957
Greg Thompsoncec7d8d2023-01-10 19:11:53958 extra_browser_args = []
959
Brian Sheedy4053a702020-07-28 02:09:52960 # Most platforms require --enable-logging=stderr to get useful browser logs.
961 # However, this actively messes with logging on CrOS (because Chrome's
962 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
963 # in order to see JavaScript console messages. See
964 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
Greg Thompsoncec7d8d2023-01-10 19:11:53965 if self.is_chromeos(tester_config):
966 extra_browser_args.append('--log-level=0')
967 elif not self.is_fuchsia(tester_config) or browser != 'fuchsia-chrome':
968 # Stderr logging is not needed for Chrome browser on Fuchsia, as ordinary
969 # logging via syslog is captured.
970 extra_browser_args.append('--enable-logging=stderr')
971
972 # --expose-gc allows the WebGL conformance tests to more reliably
973 # reproduce GC-related bugs in the V8 bindings.
974 extra_browser_args.append('--js-flags=--expose-gc')
Brian Sheedy4053a702020-07-28 02:09:52975
Xinan Linedcf05b32023-10-19 23:13:50976 # Skylab supports sharding, so reuse swarming's shard config.
977 if is_skylab and 'shards' not in result and test_config.get(
978 'swarming', {}).get('shards'):
979 result['shards'] = test_config['swarming']['shards']
980
Kenneth Russell8a386d42018-06-02 09:48:01981 args = [
Bo Liu555a0f92019-03-29 12:11:56982 test_to_run,
983 '--show-stdout',
984 '--browser=%s' % browser,
985 # --passthrough displays more of the logging in Telemetry when
986 # run via typ, in particular some of the warnings about tests
987 # being expected to fail, but passing.
988 '--passthrough',
989 '-v',
Brian Sheedy814e0482022-10-03 23:24:12990 '--stable-jobs',
Greg Thompsoncec7d8d2023-01-10 19:11:53991 '--extra-browser-args=%s' % ' '.join(extra_browser_args),
Brian Sheedy997e4802023-10-18 02:28:13992 '--enforce-browser-version',
Kenneth Russell8a386d42018-06-02 09:48:01993 ] + args
Garrett Beatybfeff8f2023-06-16 18:57:25994 result['args'] = self.maybe_fixup_args_array(
Garrett Beaty65d44222023-08-01 17:22:11995 self.substitute_gpu_args(tester_config, result, args))
Kenneth Russell8a386d42018-06-02 09:48:01996 return result
997
Garrett Beaty7436fb72024-08-07 20:20:58998 # pylint: disable=line-too-long
999 # LINT.ThenChange(//infra/config/lib/targets-internal/test-types/gpu_telemetry_test.star)
1000 # pylint: enable=line-too-long
1001
Brian Sheedyf74819b2021-06-04 01:38:381002 def get_default_isolate_name(self, tester_config, is_android_webview):
1003 if self.is_android(tester_config):
1004 if is_android_webview:
1005 return 'telemetry_gpu_integration_test_android_webview'
1006 return (
1007 'telemetry_gpu_integration_test' +
1008 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:331009 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:171010 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:331011 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:381012
Kenneth Russelleb60cbd22017-12-05 07:54:281013 def get_test_generator_map(self):
1014 return {
Bo Liu555a0f92019-03-29 12:11:561015 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301016 GPUTelemetryTestGenerator(self, is_android_webview=True),
1017 'cast_streaming_tests':
1018 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:561019 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301020 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561021 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301022 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561023 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301024 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561025 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301026 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:521027 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301028 SkylabGenerator(self),
Brian Sheedyb6491ba2022-09-26 20:49:491029 'skylab_gpu_telemetry_tests':
1030 SkylabGPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281031 }
1032
Kenneth Russell8a386d42018-06-02 09:48:011033 def get_test_type_remapper(self):
1034 return {
Fabrice de Gans223272482022-08-08 16:56:571035 # These are a specialization of isolated_scripts with a bunch of
1036 # boilerplate command line arguments added to each one.
1037 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
1038 'cast_streaming_tests': 'isolated_scripts',
1039 'gpu_telemetry_tests': 'isolated_scripts',
Brian Sheedyb6491ba2022-09-26 20:49:491040 # These are the same as existing test types, just configured to run
1041 # in Skylab instead of via normal swarming.
1042 'skylab_gpu_telemetry_tests': 'skylab_tests',
Kenneth Russell8a386d42018-06-02 09:48:011043 }
1044
Jeff Yoon67c3e832020-02-08 07:39:381045 def check_composition_type_test_suites(self, test_type,
1046 additional_validators=None):
1047 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1048 validators = [check_compound_references,
1049 check_basic_references,
1050 check_conflicting_definitions]
1051 if additional_validators:
1052 validators += additional_validators
1053
1054 target_suites = self.test_suites.get(test_type, {})
1055 other_test_type = ('compound_suites'
1056 if test_type == 'matrix_compound_suites'
1057 else 'matrix_compound_suites')
1058 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011059 basic_suites = self.test_suites.get('basic_suites', {})
1060
Jamie Madillcf4f8c72021-05-20 19:24:231061 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011062 if suite in basic_suites:
1063 raise BBGenErr('%s names may not duplicate basic test suite names '
1064 '(error found while processsing %s)'
1065 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011066
Jeff Yoon67c3e832020-02-08 07:39:381067 seen_tests = {}
1068 for sub_suite in suite_def:
1069 for validator in validators:
1070 validator(
1071 basic_suites=basic_suites,
1072 other_test_suites=other_suites,
1073 seen_tests=seen_tests,
1074 sub_suite=sub_suite,
1075 suite=suite,
1076 suite_def=suite_def,
1077 target_test_suites=target_suites,
1078 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051079 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381080 )
Kenneth Russelleb60cbd22017-12-05 07:54:281081
Stephen Martinis54d64ad2018-09-21 22:16:201082 def flatten_test_suites(self):
1083 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011084 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1085 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231086 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011087 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201088 self.test_suites = new_test_suites
1089
Chan Lia3ad1502020-04-28 05:32:111090 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231091 for suite in self.test_suites['basic_suites'].values():
1092 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561093 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411094
Garrett Beatydca3d882023-09-14 23:50:321095 isolate_name = test.get('test') or key
Nodir Turakulovfce34292019-12-18 17:05:411096 gn_entry = self.gn_isolate_map.get(isolate_name)
1097 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281098 label = gn_entry['label']
1099
1100 if label.count(':') != 1:
1101 raise BBGenErr(
1102 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1103 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1104 (label, isolate_name))
1105 if label.split(':')[1] != isolate_name:
1106 raise BBGenErr(
1107 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1108 ' label "%s" see http://crbug.com/1071091 for details.' %
1109 (isolate_name, label))
1110
Chan Lia3ad1502020-04-28 05:32:111111 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411112 else: # pragma: no cover
1113 # Some tests do not have an entry gn_isolate_map.pyl, such as
1114 # telemetry tests.
Alison Gale47d1537d2024-04-19 21:31:461115 # TODO(crbug.com/40112160): require an entry in gn_isolate_map.
Nodir Turakulovfce34292019-12-18 17:05:411116 pass
1117
Kenneth Russelleb60cbd22017-12-05 07:54:281118 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011119 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201120
Jeff Yoon8154e582019-12-03 23:30:011121 compound_suites = self.test_suites.get('compound_suites', {})
1122 # check_composition_type_test_suites() checks that all basic suites
1123 # referenced by compound suites exist.
1124 basic_suites = self.test_suites.get('basic_suites')
1125
Jamie Madillcf4f8c72021-05-20 19:24:231126 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011127 # Resolve this to a dictionary.
1128 full_suite = {}
1129 for entry in value:
1130 suite = basic_suites[entry]
1131 full_suite.update(suite)
1132 compound_suites[name] = full_suite
1133
Jeff Yoon85fb8df2020-08-20 16:47:431134 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381135 """ Merge variant-defined configurations to each test case definition in a
1136 test suite.
1137
1138 The output maps a unique test name to an array of configurations because
1139 there may exist more than one definition for a test name using variants. The
1140 test name is referenced while mapping machines to test suites, so unpacking
1141 the array is done by the generators.
1142
1143 Args:
1144 basic_test_definition: a {} defined test suite in the format
1145 test_name:test_config
1146 variants: an [] of {} defining configurations to be applied to each test
1147 case in the basic test_definition
1148
1149 Return:
1150 a {} of test_name:[{}], where each {} is a merged configuration
1151 """
1152
1153 # Each test in a basic test suite will have a definition per variant.
1154 test_suite = {}
Garrett Beaty8d6708c2023-07-20 17:20:411155 for variant in variants:
1156 # Unpack the variant from variants.pyl if it's string based.
1157 if isinstance(variant, str):
1158 variant = self.variants[variant]
Jeff Yoonda581c32020-03-06 03:56:051159
Garrett Beaty8d6708c2023-07-20 17:20:411160 # If 'enabled' is set to False, we will not use this variant; otherwise if
1161 # the variant doesn't include 'enabled' variable or 'enabled' is set to
1162 # True, we will use this variant
1163 if not variant.get('enabled', True):
1164 continue
Jeff Yoon67c3e832020-02-08 07:39:381165
Garrett Beaty8d6708c2023-07-20 17:20:411166 # Make a shallow copy of the variant to remove variant-specific fields,
1167 # leaving just mixin fields
1168 variant = copy.copy(variant)
1169 variant.pop('enabled', None)
1170 identifier = variant.pop('identifier')
1171 variant_mixins = variant.pop('mixins', [])
1172 variant_skylab = variant.pop('skylab', {})
Jeff Yoon67c3e832020-02-08 07:39:381173
Garrett Beaty8d6708c2023-07-20 17:20:411174 for test_name, test_config in basic_test_definition.items():
Garrett Beaty65b7d362024-10-01 16:21:421175 new_test = copy.copy(test_config)
Xinan Lin05fb9c1752020-12-17 00:15:521176
Jeff Yoon67c3e832020-02-08 07:39:381177 # The identifier is used to make the name of the test unique.
1178 # Generators in the recipe uniquely identify a test by it's name, so we
1179 # don't want to have the same name for each variant.
Garrett Beaty235c1412023-08-29 20:26:291180 new_test['name'] = f'{test_name} {identifier}'
Ben Pastene5f231cf22022-05-05 18:03:071181
1182 # Attach the variant identifier to the test config so downstream
1183 # generators can make modifications based on the original name. This
1184 # is mainly used in generate_gpu_telemetry_test().
Garrett Beaty8d6708c2023-07-20 17:20:411185 new_test['variant_id'] = identifier
Ben Pastene5f231cf22022-05-05 18:03:071186
Garrett Beaty65b7d362024-10-01 16:21:421187 # Save the variant details and mixins to be applied in
1188 # apply_common_transformations to match the order that starlark will
1189 # apply things
1190 new_test['*variant*'] = variant
1191 new_test['*variant_mixins*'] = variant_mixins + mixins
1192
1193 # TODO: crbug.com/40258588 - When skylab support is implemented in
1194 # starlark, these fields should be incorporated into mixins and handled
1195 # consistently with other fields
Garrett Beaty8d6708c2023-07-20 17:20:411196 for k, v in variant_skylab.items():
Sven Zheng22ba6312023-10-16 22:59:351197 # cros_chrome_version is the ash chrome version in the cros img in the
1198 # variant of cros_board. We don't want to include it in the final json
1199 # files; so remove it.
Garrett Beaty8d6708c2023-07-20 17:20:411200 if k != 'cros_chrome_version':
1201 new_test[k] = v
1202
Sven Zheng22ba6312023-10-16 22:59:351203 # For skylab, we need to pop the correct `autotest_name`. This field
1204 # defines what wrapper we use in OS infra. e.g. for gtest it's
1205 # https://source.chromium.org/chromiumos/chromiumos/codesearch/+/main:src/third_party/autotest/files/server/site_tests/chromium/chromium.py
1206 if variant_skylab and 'autotest_name' not in new_test:
1207 if 'tast_expr' in test_config:
1208 if 'lacros' in test_config['name']:
1209 new_test['autotest_name'] = 'tast.lacros-from-gcs'
1210 else:
1211 new_test['autotest_name'] = 'tast.chrome-from-gcs'
1212 elif 'benchmark' in test_config:
1213 new_test['autotest_name'] = 'chromium_Telemetry'
1214 else:
1215 new_test['autotest_name'] = 'chromium'
1216
Garrett Beaty8d6708c2023-07-20 17:20:411217 test_suite.setdefault(test_name, []).append(new_test)
1218
Jeff Yoon67c3e832020-02-08 07:39:381219 return test_suite
1220
Jeff Yoon8154e582019-12-03 23:30:011221 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381222 self.check_composition_type_test_suites('matrix_compound_suites',
1223 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011224
1225 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381226 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011227 # referenced by matrix suites exist.
1228 basic_suites = self.test_suites.get('basic_suites')
1229
Brian Sheedy822e03742024-08-09 18:48:141230 def update_tests_uncurried(full_suite, expanded):
1231 for test_name, new_tests in expanded.items():
1232 if not isinstance(new_tests, list):
1233 new_tests = [new_tests]
1234 tests_for_name = full_suite.setdefault(test_name, [])
1235 for t in new_tests:
1236 if t not in tests_for_name:
1237 tests_for_name.append(t)
1238
Garrett Beaty235c1412023-08-29 20:26:291239 for matrix_suite_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011240 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381241
Jamie Madillcf4f8c72021-05-20 19:24:231242 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381243 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1244
Brian Sheedy822e03742024-08-09 18:48:141245 update_tests = functools.partial(update_tests_uncurried, full_suite)
Garrett Beaty235c1412023-08-29 20:26:291246
Garrett Beaty60a7b2a2023-09-13 23:00:401247 if (variants := mtx_test_suite_config.get('variants')):
Jeff Yoon85fb8df2020-08-20 16:47:431248 mixins = mtx_test_suite_config.get('mixins', [])
Garrett Beaty60a7b2a2023-09-13 23:00:401249 result = self.resolve_variants(basic_test_def, variants, mixins)
Garrett Beaty235c1412023-08-29 20:26:291250 update_tests(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271251 else:
1252 suite = basic_suites[test_suite]
Garrett Beaty235c1412023-08-29 20:26:291253 update_tests(suite)
1254 matrix_compound_suites[matrix_suite_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281255
1256 def link_waterfalls_to_test_suites(self):
1257 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231258 for tester_name, tester in waterfall['machines'].items():
1259 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281260 if not value in self.test_suites:
1261 # Hard / impossible to cover this in the unit test.
1262 raise self.unknown_test_suite(
1263 value, tester_name, waterfall['name']) # pragma: no cover
1264 tester['test_suites'][suite] = self.test_suites[value]
1265
1266 def load_configuration_files(self):
Garrett Beaty79339e182023-04-10 20:45:471267 self.waterfalls = self.load_pyl_file(self.args.waterfalls_pyl_path)
1268 self.test_suites = self.load_pyl_file(self.args.test_suites_pyl_path)
1269 self.exceptions = self.load_pyl_file(
1270 self.args.test_suite_exceptions_pyl_path)
1271 self.mixins = self.load_pyl_file(self.args.mixins_pyl_path)
1272 self.gn_isolate_map = self.load_pyl_file(self.args.gn_isolate_map_pyl_path)
Chong Guee622242020-10-28 18:17:351273 for isolate_map in self.args.isolate_map_files:
1274 isolate_map = self.load_pyl_file(isolate_map)
1275 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1276 if duplicates:
1277 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1278 ', '.join(duplicates))
1279 self.gn_isolate_map.update(isolate_map)
1280
Garrett Beaty79339e182023-04-10 20:45:471281 self.variants = self.load_pyl_file(self.args.variants_pyl_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281282
1283 def resolve_configuration_files(self):
Garrett Beaty086b3402024-09-25 23:45:341284 self.resolve_mixins()
Garrett Beaty235c1412023-08-29 20:26:291285 self.resolve_test_names()
Garrett Beatydca3d882023-09-14 23:50:321286 self.resolve_isolate_names()
Garrett Beaty65d44222023-08-01 17:22:111287 self.resolve_dimension_sets()
Chan Lia3ad1502020-04-28 05:32:111288 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281289 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011290 self.resolve_matrix_compound_test_suites()
1291 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281292 self.link_waterfalls_to_test_suites()
1293
Garrett Beaty086b3402024-09-25 23:45:341294 def resolve_mixins(self):
1295 for mixin in self.mixins.values():
1296 mixin.pop('fail_if_unused', None)
1297
Garrett Beaty235c1412023-08-29 20:26:291298 def resolve_test_names(self):
1299 for suite_name, suite in self.test_suites.get('basic_suites').items():
1300 for test_name, test in suite.items():
1301 if 'name' in test:
1302 raise BBGenErr(
1303 f'The name field is set in test {test_name} in basic suite '
1304 f'{suite_name}, this is not supported, the test name is the key '
1305 'within the basic suite')
Garrett Beatyffe83c4f2023-09-08 19:07:371306 # When a test is expanded with variants, this will be overwritten, but
1307 # this ensures every test definition has the name field set
1308 test['name'] = test_name
Garrett Beaty235c1412023-08-29 20:26:291309
Garrett Beatydca3d882023-09-14 23:50:321310 def resolve_isolate_names(self):
1311 for suite_name, suite in self.test_suites.get('basic_suites').items():
1312 for test_name, test in suite.items():
1313 if 'isolate_name' in test:
1314 raise BBGenErr(
1315 f'The isolate_name field is set in test {test_name} in basic '
1316 f'suite {suite_name}, the test field should be used instead')
1317
Garrett Beaty65d44222023-08-01 17:22:111318 def resolve_dimension_sets(self):
Garrett Beaty65d44222023-08-01 17:22:111319
1320 def definitions():
1321 for suite_name, suite in self.test_suites.get('basic_suites', {}).items():
1322 for test_name, test in suite.items():
1323 yield test, f'test {test_name} in basic suite {suite_name}'
1324
1325 for mixin_name, mixin in self.mixins.items():
1326 yield mixin, f'mixin {mixin_name}'
1327
1328 for waterfall in self.waterfalls:
1329 for builder_name, builder in waterfall.get('machines', {}).items():
1330 yield (
1331 builder,
1332 f'builder {builder_name} in waterfall {waterfall["name"]}',
1333 )
1334
1335 for test_name, exceptions in self.exceptions.items():
1336 modifications = exceptions.get('modifications', {})
1337 for builder_name, mods in modifications.items():
1338 yield (
1339 mods,
1340 f'exception for test {test_name} on builder {builder_name}',
1341 )
1342
1343 for definition, location in definitions():
1344 for swarming_attr in (
1345 'swarming',
1346 'android_swarming',
1347 'chromeos_swarming',
1348 ):
1349 if (swarming :=
1350 definition.get(swarming_attr)) and 'dimension_sets' in swarming:
Garrett Beatyade673d2023-08-04 22:00:251351 raise BBGenErr(
1352 f'dimension_sets is no longer supported (set in {location}),'
1353 ' instead, use set dimensions to a single dict')
Garrett Beaty65d44222023-08-01 17:22:111354
Nico Weberd18b8962018-05-16 19:39:381355 def unknown_bot(self, bot_name, waterfall_name):
1356 return BBGenErr(
1357 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1358
Kenneth Russelleb60cbd22017-12-05 07:54:281359 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1360 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381361 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281362 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1363
1364 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1365 return BBGenErr(
1366 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1367 ' on waterfall ' + waterfall_name)
1368
Garrett Beatye3a606ceb2024-04-30 22:13:131369 def ensure_valid_mixin_list(self, mixins, location):
1370 if not isinstance(mixins, list):
1371 raise BBGenErr(
1372 f"got '{mixins}', should be a list of mixin names: {location}")
1373 for mixin in mixins:
1374 if not mixin in self.mixins:
1375 raise BBGenErr(f'bad mixin {mixin}: {location}')
Stephen Martinisb6a50492018-09-12 23:59:321376
Garrett Beatye3a606ceb2024-04-30 22:13:131377 def apply_mixins(self, test, mixins, mixins_to_ignore, builder=None):
1378 for mixin in mixins:
1379 if mixin not in mixins_to_ignore:
Austin Eng148d9f0f2022-02-08 19:18:531380 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinis0382bc12018-09-17 22:29:071381 return test
Stephen Martinisb6a50492018-09-12 23:59:321382
Garrett Beaty8d6708c2023-07-20 17:20:411383 def apply_mixin(self, mixin, test, builder=None):
Stephen Martinisb72f6d22018-10-04 23:29:011384 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321385
Garrett Beaty4c35b142023-06-23 21:01:231386 A mixin is applied by copying all fields from the mixin into the
1387 test with the following exceptions:
1388 * For the various *args keys, the test's existing value (an empty
1389 list if not present) will be extended with the mixin's value.
1390 * The sub-keys of the swarming value will be copied to the test's
1391 swarming value with the following exceptions:
Garrett Beatyade673d2023-08-04 22:00:251392 * For the named_caches sub-keys, the test's existing value (an
1393 empty list if not present) will be extended with the mixin's
1394 value.
1395 * For the dimensions sub-key, the tests's existing value (an empty
1396 dict if not present) will be updated with the mixin's value.
Stephen Martinisb6a50492018-09-12 23:59:321397 """
Garrett Beaty4c35b142023-06-23 21:01:231398
Stephen Martinisb6a50492018-09-12 23:59:321399 new_test = copy.deepcopy(test)
1400 mixin = copy.deepcopy(mixin)
Garrett Beaty8d6708c2023-07-20 17:20:411401
1402 if 'description' in mixin:
1403 description = []
1404 if 'description' in new_test:
1405 description.append(new_test['description'])
1406 description.append(mixin.pop('description'))
1407 new_test['description'] = '\n'.join(description)
1408
Stephen Martinisb72f6d22018-10-04 23:29:011409 if 'swarming' in mixin:
1410 swarming_mixin = mixin['swarming']
1411 new_test.setdefault('swarming', {})
1412 if 'dimensions' in swarming_mixin:
Garrett Beatyade673d2023-08-04 22:00:251413 new_test['swarming'].setdefault('dimensions', {}).update(
1414 swarming_mixin.pop('dimensions'))
Garrett Beaty4c35b142023-06-23 21:01:231415 if 'named_caches' in swarming_mixin:
1416 new_test['swarming'].setdefault('named_caches', []).extend(
1417 swarming_mixin['named_caches'])
1418 del swarming_mixin['named_caches']
Stephen Martinisb72f6d22018-10-04 23:29:011419 # python dict update doesn't do recursion at all. Just hard code the
1420 # nested update we need (mixin['swarming'] shouldn't clobber
1421 # test['swarming'], but should update it).
1422 new_test['swarming'].update(swarming_mixin)
1423 del mixin['swarming']
1424
Garrett Beatye3a606ceb2024-04-30 22:13:131425 for a in ('args', 'precommit_args', 'non_precommit_args'):
Garrett Beaty4c35b142023-06-23 21:01:231426 if (value := mixin.pop(a, None)) is None:
1427 continue
1428 if not isinstance(value, list):
1429 raise BBGenErr(f'"{a}" must be a list')
1430 new_test.setdefault(a, []).extend(value)
1431
Garrett Beatye3a606ceb2024-04-30 22:13:131432 # At this point, all keys that require merging are taken care of, so the
1433 # remaining entries can be copied over. The os-conditional entries will be
1434 # resolved immediately after and they are resolved before any mixins are
1435 # applied, so there's are no concerns about overwriting the corresponding
1436 # entry in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011437 new_test.update(mixin)
Garrett Beatye3a606ceb2024-04-30 22:13:131438 if builder:
1439 self.resolve_os_conditional_values(new_test, builder)
1440
1441 if 'args' in new_test:
1442 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1443
Stephen Martinisb6a50492018-09-12 23:59:321444 return new_test
1445
Greg Gutermanf60eb052020-03-12 17:40:011446 def generate_output_tests(self, waterfall):
1447 """Generates the tests for a waterfall.
1448
1449 Args:
1450 waterfall: a dictionary parsed from a master pyl file
1451 Returns:
1452 A dictionary mapping builders to test specs
1453 """
1454 return {
Jamie Madillcf4f8c72021-05-20 19:24:231455 name: self.get_tests_for_config(waterfall, name, config)
1456 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011457 }
1458
1459 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531460 generator_map = self.get_test_generator_map()
1461 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281462
Greg Gutermanf60eb052020-03-12 17:40:011463 tests = {}
1464 # Copy only well-understood entries in the machine's configuration
1465 # verbatim into the generated JSON.
1466 if 'additional_compile_targets' in config:
1467 tests['additional_compile_targets'] = config[
1468 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231469 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011470 if test_type not in generator_map:
1471 raise self.unknown_test_suite_type(
1472 test_type, name, waterfall['name']) # pragma: no cover
1473 test_generator = generator_map[test_type]
1474 # Let multiple kinds of generators generate the same kinds
1475 # of tests. For example, gpu_telemetry_tests are a
1476 # specialization of isolated_scripts.
1477 new_tests = test_generator.generate(
1478 waterfall, name, config, input_tests)
1479 remapped_test_type = test_type_remapper.get(test_type, test_type)
Garrett Beatyffe83c4f2023-09-08 19:07:371480 tests.setdefault(remapped_test_type, []).extend(new_tests)
1481
1482 for test_type, tests_for_type in tests.items():
1483 if test_type == 'additional_compile_targets':
1484 continue
1485 tests[test_type] = sorted(tests_for_type, key=lambda t: t['name'])
Greg Gutermanf60eb052020-03-12 17:40:011486
1487 return tests
1488
1489 def jsonify(self, all_tests):
1490 return json.dumps(
1491 all_tests, indent=2, separators=(',', ': '),
1492 sort_keys=True) + '\n'
1493
1494 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281495 self.load_configuration_files()
1496 self.resolve_configuration_files()
1497 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011498 result = collections.defaultdict(dict)
1499
Stephanie Kim572b43c02023-04-13 14:24:131500 if os.path.exists(self.args.autoshard_exceptions_json_path):
1501 autoshards = json.loads(
1502 self.read_file(self.args.autoshard_exceptions_json_path))
1503 else:
1504 autoshards = {}
1505
Dirk Pranke6269d302020-10-01 00:14:391506 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011507 for waterfall in self.waterfalls:
1508 for field in required_fields:
1509 # Verify required fields
1510 if field not in waterfall:
Brian Sheedy0d2300f32024-08-13 23:14:411511 raise BBGenErr('Waterfall %s has no %s' % (waterfall['name'], field))
Greg Gutermanf60eb052020-03-12 17:40:011512
1513 # Handle filter flag, if specified
1514 if filters and waterfall['name'] not in filters:
1515 continue
1516
1517 # Join config files and hardcoded values together
1518 all_tests = self.generate_output_tests(waterfall)
1519 result[waterfall['name']] = all_tests
1520
Stephanie Kim572b43c02023-04-13 14:24:131521 if not autoshards:
1522 continue
1523 for builder, test_spec in all_tests.items():
1524 for target_type, test_list in test_spec.items():
1525 if target_type == 'additional_compile_targets':
1526 continue
1527 for test_dict in test_list:
1528 # Suites that apply variants or other customizations will create
1529 # test_dicts that have "name" value that is different from the
Garrett Beatyffe83c4f2023-09-08 19:07:371530 # "test" value.
Stephanie Kim572b43c02023-04-13 14:24:131531 # e.g. name = vulkan_swiftshader_content_browsertests, but
1532 # test = content_browsertests and
1533 # test_id_prefix = "ninja://content/test:content_browsertests/"
Garrett Beatyffe83c4f2023-09-08 19:07:371534 test_name = test_dict['name']
Stephanie Kim572b43c02023-04-13 14:24:131535 shard_info = autoshards.get(waterfall['name'],
1536 {}).get(builder, {}).get(test_name)
1537 if shard_info:
1538 test_dict['swarming'].update(
1539 {'shards': int(shard_info['shards'])})
1540
Greg Gutermanf60eb052020-03-12 17:40:011541 # Add do not edit warning
1542 for tests in result.values():
1543 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1544 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1545
1546 return result
1547
1548 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281549 suffix = '.json'
1550 if self.args.new_files:
1551 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011552
1553 for filename, contents in result.items():
1554 jsonstr = self.jsonify(contents)
Garrett Beaty79339e182023-04-10 20:45:471555 file_path = os.path.join(self.args.output_dir, filename + suffix)
1556 self.write_file(file_path, jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281557
Nico Weberd18b8962018-05-16 19:39:381558 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161559 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421560 # NOTE: This reference can cause issues; if a file changes there, the
1561 # presubmit here won't be run by default. A manually maintained list there
1562 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1563 # references to configs outside of this directory are added, please change
1564 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1565 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491566
Garrett Beaty7e866fc2021-06-16 14:12:101567 # Get the generated project.pyl so we can check if we should be enforcing
1568 # that the specs are for builders that actually exist
1569 # If not, return None to indicate that we won't enforce that builders in
1570 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491571 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1572 'project.pyl')
1573 if os.path.exists(project_pyl_path):
1574 settings = ast.literal_eval(self.read_file(project_pyl_path))
1575 if not settings.get('validate_source_side_specs_have_builder', True):
1576 return None
1577
Nico Weberd18b8962018-05-16 19:39:381578 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311579 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161580 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1581 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431582 for c in milo_configs:
1583 for l in self.read_file(c).splitlines():
1584 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311585 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431586 continue
1587 # l looks like
1588 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1589 # Extract win_chromium_dbg_ng part.
1590 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381591 return bot_names
1592
Ben Pastene9a010082019-09-25 20:41:371593 def get_internal_waterfalls(self):
1594 # Similar to get_builders_that_do_not_actually_exist above, but for
1595 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201596 return [
Kramer Ge3bf853a2023-04-13 19:39:471597 'chrome', 'chrome.pgo', 'chrome.gpu.fyi', 'internal.chrome.fyi',
yoshiki iguchi4de608082024-03-14 00:33:361598 'internal.chromeos.fyi', 'internal.optimization_guide', 'internal.soda',
1599 'chromeos.preuprev'
Yuke Liaoe6c23dd2021-07-28 16:12:201600 ]
Ben Pastene9a010082019-09-25 20:41:371601
Stephen Martinisf83893722018-09-19 00:02:181602 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201603 self.check_input_files_sorting(verbose)
1604
Kenneth Russelleb60cbd22017-12-05 07:54:281605 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011606 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381607 self.check_composition_type_test_suites('matrix_compound_suites',
1608 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111609 self.resolve_test_id_prefixes()
Garrett Beaty1ead4a52023-12-07 19:16:421610
1611 # All test suites must be referenced. Check this before flattening the test
1612 # suites so that we can transitively check the basic suites for compound
1613 # suites and matrix compound suites (otherwise we would determine a basic
1614 # suite is used if it shared a name with a test present in a basic suite
1615 # that is used).
1616 all_suites = set(
1617 itertools.chain(*(self.test_suites.get(a, {}) for a in (
1618 'basic_suites',
1619 'compound_suites',
1620 'matrix_compound_suites',
1621 ))))
1622 unused_suites = set(all_suites)
1623 generator_map = self.get_test_generator_map()
1624 for waterfall in self.waterfalls:
1625 for bot_name, tester in waterfall['machines'].items():
1626 for suite_type, suite in tester.get('test_suites', {}).items():
1627 if suite_type not in generator_map:
1628 raise self.unknown_test_suite_type(suite_type, bot_name,
1629 waterfall['name'])
1630 if suite not in all_suites:
1631 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1632 unused_suites.discard(suite)
1633 # For each compound suite or matrix compound suite, if the suite was used,
1634 # remove all of the basic suites that it composes from the set of unused
1635 # suites
1636 for a in ('compound_suites', 'matrix_compound_suites'):
1637 for suite, sub_suites in self.test_suites.get(a, {}).items():
1638 if suite not in unused_suites:
1639 unused_suites.difference_update(sub_suites)
1640 if unused_suites:
1641 raise BBGenErr('The following test suites were unreferenced by bots on '
1642 'the waterfalls: ' + str(unused_suites))
1643
Stephen Martinis54d64ad2018-09-21 22:16:201644 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381645
1646 # All bots should exist.
1647 bot_names = self.get_valid_bot_names()
Garrett Beaty2a02de3c2020-05-15 13:57:351648 if bot_names is not None:
1649 internal_waterfalls = self.get_internal_waterfalls()
1650 for waterfall in self.waterfalls:
Alison Gale923a33e2024-04-22 23:34:281651 # TODO(crbug.com/41474799): Remove the need for this exception.
Garrett Beaty2a02de3c2020-05-15 13:57:351652 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011653 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351654 for bot_name in waterfall['machines']:
Garrett Beaty2a02de3c2020-05-15 13:57:351655 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581656 if waterfall['name'] in [
1657 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1658 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351659 # TODO(thakis): Remove this once these bots move to luci.
1660 continue # pragma: no cover
1661 if waterfall['name'] in ['tryserver.webrtc',
1662 'webrtc.chromium.fyi.experimental']:
1663 # These waterfalls have their bot configs in a different repo.
1664 # so we don't know about their bot names.
1665 continue # pragma: no cover
1666 if waterfall['name'] in ['client.devtools-frontend.integration',
1667 'tryserver.devtools-frontend',
1668 'chromium.devtools-frontend']:
1669 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201670 if waterfall['name'] in ['client.openscreen.chromium']:
1671 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351672 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381673
Kenneth Russelleb60cbd22017-12-05 07:54:281674 # All test suite exceptions must refer to bots on the waterfall.
1675 all_bots = set()
1676 missing_bots = set()
1677 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231678 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281679 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281680 # In order to disambiguate between bots with the same name on
1681 # different waterfalls, support has been added to various
1682 # exceptions for concatenating the waterfall name after the bot
1683 # name.
1684 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231685 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381686 removals = (exception.get('remove_from', []) +
1687 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231688 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381689 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281690 if removal not in all_bots:
1691 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411692
Kenneth Russelleb60cbd22017-12-05 07:54:281693 if missing_bots:
1694 raise BBGenErr('The following nonexistent machines were referenced in '
1695 'the test suite exceptions: ' + str(missing_bots))
1696
Garrett Beatyb061e69d2023-06-27 16:15:351697 for name, mixin in self.mixins.items():
1698 if '$mixin_append' in mixin:
1699 raise BBGenErr(
1700 f'$mixin_append is no longer supported (set in mixin "{name}"),'
1701 ' args and named caches specified as normal will be appended')
1702
Stephen Martinis0382bc12018-09-17 22:29:071703 # All mixins must be referenced
1704 seen_mixins = set()
1705 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011706 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231707 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011708 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071709 for suite in self.test_suites.values():
1710 if isinstance(suite, list):
1711 # Don't care about this, it's a composition, which shouldn't include a
1712 # swarming mixin.
1713 continue
1714
1715 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561716 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011717 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Garrett Beaty4b9f1752024-09-26 20:02:501718 seen_mixins = seen_mixins.union(
1719 test.get('test_common', {}).get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071720
Zhaoyang Li9da047d52021-05-10 21:31:441721 for variant in self.variants:
1722 # Unpack the variant from variants.pyl if it's string based.
1723 if isinstance(variant, str):
1724 variant = self.variants[variant]
1725 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1726
Garrett Beaty086b3402024-09-25 23:45:341727 missing_mixins = set()
1728 for name, mixin_value in self.mixins.items():
1729 if name not in seen_mixins and mixin_value.get('fail_if_unused', True):
1730 missing_mixins.add(name)
Stephen Martinis0382bc12018-09-17 22:29:071731 if missing_mixins:
1732 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1733 ' referenced in a waterfall, machine, or test suite.' % (
1734 str(missing_mixins)))
1735
Jeff Yoonda581c32020-03-06 03:56:051736 # All variant references must be referenced
1737 seen_variants = set()
1738 for suite in self.test_suites.values():
1739 if isinstance(suite, list):
1740 continue
1741
1742 for test in suite.values():
1743 if isinstance(test, dict):
1744 for variant in test.get('variants', []):
1745 if isinstance(variant, str):
1746 seen_variants.add(variant)
1747
1748 missing_variants = set(self.variants.keys()) - seen_variants
1749 if missing_variants:
1750 raise BBGenErr('The following variants were unreferenced: %s. They must '
1751 'be referenced in a matrix test suite under the variants '
1752 'key.' % str(missing_variants))
1753
Stephen Martinis54d64ad2018-09-21 22:16:201754
Garrett Beaty79339e182023-04-10 20:45:471755 def type_assert(self, node, typ, file_path, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201756 """Asserts that the Python AST node |node| is of type |typ|.
1757
1758 If verbose is set, it prints out some helpful context lines, showing where
1759 exactly the error occurred in the file.
1760 """
1761 if not isinstance(node, typ):
1762 if verbose:
Brian Sheedy0d2300f32024-08-13 23:14:411763 lines = [''] + self.read_file(file_path).splitlines()
Stephen Martinis54d64ad2018-09-21 22:16:201764
1765 context = 2
1766 lines_start = max(node.lineno - context, 0)
1767 # Add one to include the last line
1768 lines_end = min(node.lineno + context, len(lines)) + 1
Garrett Beaty79339e182023-04-10 20:45:471769 lines = itertools.chain(
1770 ['== %s ==\n' % file_path],
Brian Sheedy0d2300f32024-08-13 23:14:411771 ['<snip>\n'],
Garrett Beaty79339e182023-04-10 20:45:471772 [
1773 '%d %s' % (lines_start + i, line)
1774 for i, line in enumerate(lines[lines_start:lines_start +
1775 context])
1776 ],
1777 ['-' * 80 + '\n'],
1778 ['%d %s' % (node.lineno, lines[node.lineno])],
1779 [
1780 '-' * (node.col_offset + 3) + '^' + '-' *
1781 (80 - node.col_offset - 4) + '\n'
1782 ],
1783 [
1784 '%d %s' % (node.lineno + 1 + i, line)
1785 for i, line in enumerate(lines[node.lineno + 1:lines_end])
1786 ],
Brian Sheedy0d2300f32024-08-13 23:14:411787 ['<snip>\n'],
Stephen Martinis54d64ad2018-09-21 22:16:201788 )
1789 # Print out a useful message when a type assertion fails.
1790 for l in lines:
1791 self.print_line(l.strip())
1792
1793 node_dumped = ast.dump(node, annotate_fields=False)
1794 # If the node is huge, truncate it so everything fits in a terminal
1795 # window.
1796 if len(node_dumped) > 60: # pragma: no cover
1797 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1798 raise BBGenErr(
Brian Sheedy0d2300f32024-08-13 23:14:411799 "Invalid .pyl file '%s'. Python AST node %r on line %s expected to"
Garrett Beaty79339e182023-04-10 20:45:471800 ' be %s, is %s' %
1801 (file_path, node_dumped, node.lineno, typ, type(node)))
Stephen Martinis54d64ad2018-09-21 22:16:201802
Garrett Beaty79339e182023-04-10 20:45:471803 def check_ast_list_formatted(self,
1804 keys,
1805 file_path,
1806 verbose,
Stephen Martinis1384ff92020-01-07 19:52:151807 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531808 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201809
Stephen Martinis5bef0fc2020-01-06 22:47:531810 Currently only checks to ensure they're correctly sorted, and that there
1811 are no duplicates.
1812
1813 Args:
1814 keys: An python list of AST nodes.
1815
1816 It's a list of AST nodes instead of a list of strings because
1817 when verbose is set, it tries to print out context of where the
1818 diffs are in the file.
Garrett Beaty79339e182023-04-10 20:45:471819 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531820 verbose: If set, print out diff information about how the keys are
1821 incorrectly formatted.
1822 check_sorting: If true, checks if the list is sorted.
1823 Returns:
1824 If the keys are correctly formatted.
1825 """
1826 if not keys:
1827 return True
1828
1829 assert isinstance(keys[0], ast.Str)
1830
1831 keys_strs = [k.s for k in keys]
1832 # Keys to diff against. Used below.
1833 keys_to_diff_against = None
1834 # If the list is properly formatted.
1835 list_formatted = True
1836
1837 # Duplicates are always bad.
1838 if len(set(keys_strs)) != len(keys_strs):
1839 list_formatted = False
1840 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1841
1842 if check_sorting and sorted(keys_strs) != keys_strs:
1843 list_formatted = False
1844 if list_formatted:
1845 return True
1846
1847 if verbose:
1848 line_num = keys[0].lineno
1849 keys = [k.s for k in keys]
1850 if check_sorting:
1851 # If we have duplicates, sorting this will take care of it anyways.
1852 keys_to_diff_against = sorted(set(keys))
1853 # else, keys_to_diff_against is set above already
1854
1855 self.print_line('=' * 80)
1856 self.print_line('(First line of keys is %s)' % line_num)
Garrett Beaty79339e182023-04-10 20:45:471857 for line in difflib.context_diff(keys,
1858 keys_to_diff_against,
1859 fromfile='current (%r)' % file_path,
1860 tofile='sorted',
1861 lineterm=''):
Stephen Martinis5bef0fc2020-01-06 22:47:531862 self.print_line(line)
1863 self.print_line('=' * 80)
1864
1865 return False
1866
Garrett Beaty79339e182023-04-10 20:45:471867 def check_ast_dict_formatted(self, node, file_path, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531868 """Checks if an ast dictionary's keys are correctly formatted.
1869
1870 Just a simple wrapper around check_ast_list_formatted.
1871 Args:
1872 node: An AST node. Assumed to be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471873 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531874 verbose: If set, print out diff information about how the keys are
1875 incorrectly formatted.
1876 check_sorting: If true, checks if the list is sorted.
1877 Returns:
1878 If the dictionary is correctly formatted.
1879 """
Stephen Martinis54d64ad2018-09-21 22:16:201880 keys = []
1881 # The keys of this dict are ordered as ordered in the file; normal python
1882 # dictionary keys are given an arbitrary order, but since we parsed the
1883 # file itself, the order as given in the file is preserved.
1884 for key in node.keys:
Garrett Beaty79339e182023-04-10 20:45:471885 self.type_assert(key, ast.Str, file_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531886 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201887
Garrett Beaty79339e182023-04-10 20:45:471888 return self.check_ast_list_formatted(keys, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181889
1890 def check_input_files_sorting(self, verbose=False):
Alison Gale923a33e2024-04-22 23:34:281891 # TODO(crbug.com/41415841): Add the ability for this script to
Stephen Martinis54d64ad2018-09-21 22:16:201892 # actually format the files, rather than just complain if they're
1893 # incorrectly formatted.
1894 bad_files = set()
Garrett Beaty79339e182023-04-10 20:45:471895
1896 def parse_file(file_path):
Stephen Martinis5bef0fc2020-01-06 22:47:531897 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201898
Stephen Martinis5bef0fc2020-01-06 22:47:531899 Returns an AST node representing the value in the pyl file."""
Garrett Beaty79339e182023-04-10 20:45:471900 parsed = ast.parse(self.read_file(file_path))
Stephen Martinisf83893722018-09-19 00:02:181901
Stephen Martinisf83893722018-09-19 00:02:181902 # Must be a module.
Garrett Beaty79339e182023-04-10 20:45:471903 self.type_assert(parsed, ast.Module, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181904 module = parsed.body
1905
1906 # Only one expression in the module.
Garrett Beaty79339e182023-04-10 20:45:471907 self.type_assert(module, list, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181908 if len(module) != 1: # pragma: no cover
Garrett Beaty79339e182023-04-10 20:45:471909 raise BBGenErr('Invalid .pyl file %s' % file_path)
Stephen Martinisf83893722018-09-19 00:02:181910 expr = module[0]
Garrett Beaty79339e182023-04-10 20:45:471911 self.type_assert(expr, ast.Expr, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181912
Stephen Martinis5bef0fc2020-01-06 22:47:531913 return expr.value
1914
1915 # Handle this separately
Garrett Beaty79339e182023-04-10 20:45:471916 value = parse_file(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531917 # Value should be a list.
Garrett Beaty79339e182023-04-10 20:45:471918 self.type_assert(value, ast.List, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531919
1920 keys = []
Joshua Hood56c673c2022-03-02 20:29:331921 for elm in value.elts:
Garrett Beaty79339e182023-04-10 20:45:471922 self.type_assert(elm, ast.Dict, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531923 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331924 for key, val in zip(elm.keys, elm.values):
Garrett Beaty79339e182023-04-10 20:45:471925 self.type_assert(key, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531926 if key.s == 'machines':
Garrett Beaty79339e182023-04-10 20:45:471927 if not self.check_ast_dict_formatted(
1928 val, self.args.waterfalls_pyl_path, verbose):
1929 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531930
Brian Sheedy0d2300f32024-08-13 23:14:411931 if key.s == 'name':
Garrett Beaty79339e182023-04-10 20:45:471932 self.type_assert(val, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531933 waterfall_name = val
1934 assert waterfall_name
1935 keys.append(waterfall_name)
1936
Garrett Beaty79339e182023-04-10 20:45:471937 if not self.check_ast_list_formatted(keys, self.args.waterfalls_pyl_path,
1938 verbose):
1939 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531940
Garrett Beaty79339e182023-04-10 20:45:471941 for file_path in (
1942 self.args.mixins_pyl_path,
1943 self.args.test_suites_pyl_path,
1944 self.args.test_suite_exceptions_pyl_path,
Stephen Martinis5bef0fc2020-01-06 22:47:531945 ):
Garrett Beaty79339e182023-04-10 20:45:471946 value = parse_file(file_path)
Stephen Martinisf83893722018-09-19 00:02:181947 # Value should be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471948 self.type_assert(value, ast.Dict, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181949
Garrett Beaty79339e182023-04-10 20:45:471950 if not self.check_ast_dict_formatted(value, file_path, verbose):
1951 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531952
Garrett Beaty79339e182023-04-10 20:45:471953 if file_path == self.args.test_suites_pyl_path:
Jeff Yoon8154e582019-12-03 23:30:011954 expected_keys = ['basic_suites',
1955 'compound_suites',
1956 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201957 actual_keys = [node.s for node in value.keys]
1958 assert all(key in expected_keys for key in actual_keys), (
Garrett Beaty79339e182023-04-10 20:45:471959 'Invalid %r file; expected keys %r, got %r' %
1960 (file_path, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:331961 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:201962 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011963 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201964 for suite_group in suite_dicts:
Garrett Beaty79339e182023-04-10 20:45:471965 if not self.check_ast_dict_formatted(suite_group, file_path, verbose):
1966 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181967
Stephen Martinis5bef0fc2020-01-06 22:47:531968 for key, suite in zip(value.keys, value.values):
1969 # The compound suites are checked in
1970 # 'check_composition_type_test_suites()'
1971 if key.s == 'basic_suites':
1972 for group in suite.values:
Garrett Beaty79339e182023-04-10 20:45:471973 if not self.check_ast_dict_formatted(group, file_path, verbose):
1974 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531975 break
Stephen Martinis54d64ad2018-09-21 22:16:201976
Garrett Beaty79339e182023-04-10 20:45:471977 elif file_path == self.args.test_suite_exceptions_pyl_path:
Stephen Martinis5bef0fc2020-01-06 22:47:531978 # Check the values for each test.
1979 for test in value.values:
1980 for kind, node in zip(test.keys, test.values):
1981 if isinstance(node, ast.Dict):
Garrett Beaty79339e182023-04-10 20:45:471982 if not self.check_ast_dict_formatted(node, file_path, verbose):
1983 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531984 elif kind.s == 'remove_from':
1985 # Don't care about sorting; these are usually grouped, since the
1986 # same bug can affect multiple builders. Do want to make sure
1987 # there aren't duplicates.
Garrett Beaty79339e182023-04-10 20:45:471988 if not self.check_ast_list_formatted(
1989 node.elts, file_path, verbose, check_sorting=False):
1990 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181991
1992 if bad_files:
1993 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201994 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531995 'unsorted, or have duplicates. Re-run this with --verbose to see '
1996 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181997
Kenneth Russelleb60cbd22017-12-05 07:54:281998 def check_output_file_consistency(self, verbose=False):
1999 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012000 # All waterfalls/bucket .json files must have been written
2001 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:282002 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012003 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:162004 outputs = self.generate_outputs()
2005 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:012006 expected = self.jsonify(expected_contents)
Garrett Beaty79339e182023-04-10 20:45:472007 file_path = os.path.join(self.args.output_dir, filename + '.json')
Ben Pastenef21cda32023-03-30 22:00:572008 current = self.read_file(file_path)
Kenneth Russelleb60cbd22017-12-05 07:54:282009 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:012010 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:322011 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:012012 self.print_line('File ' + filename +
2013 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:322014 'contents:')
2015 for line in difflib.unified_diff(
2016 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:502017 current.splitlines(),
2018 fromfile='expected', tofile='current'):
2019 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:012020
2021 if ungenerated_files:
2022 raise BBGenErr(
2023 'The following files have not been properly '
2024 'autogenerated by generate_buildbot_json.py: ' +
2025 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:282026
Dirk Pranke772f55f2021-04-28 04:51:162027 for builder_group, builders in outputs.items():
2028 for builder, step_types in builders.items():
Garrett Beatydca3d882023-09-14 23:50:322029 for test_type in ('gtest_tests', 'isolated_scripts'):
2030 for step_data in step_types.get(test_type, []):
2031 step_name = step_data['name']
2032 self._check_swarming_config(builder_group, builder, step_name,
2033 step_data)
Dirk Pranke772f55f2021-04-28 04:51:162034
2035 def _check_swarming_config(self, filename, builder, step_name, step_data):
Alison Gale47d1537d2024-04-19 21:31:462036 # TODO(crbug.com/40179524): Ensure all swarming tests specify cpu, not
Dirk Pranke772f55f2021-04-28 04:51:162037 # just mac tests.
Garrett Beatybb18d532023-06-26 22:16:332038 if 'swarming' in step_data:
Garrett Beatyade673d2023-08-04 22:00:252039 dimensions = step_data['swarming'].get('dimensions')
2040 if not dimensions:
Tatsuhisa Yamaguchif1878d52023-11-06 06:02:252041 raise BBGenErr('%s: %s / %s : dimensions must be specified for all '
Dirk Pranke772f55f2021-04-28 04:51:162042 'swarmed tests' % (filename, builder, step_name))
Garrett Beatyade673d2023-08-04 22:00:252043 if not dimensions.get('os'):
2044 raise BBGenErr('%s: %s / %s : os must be specified for all '
2045 'swarmed tests' % (filename, builder, step_name))
2046 if 'Mac' in dimensions.get('os') and not dimensions.get('cpu'):
2047 raise BBGenErr('%s: %s / %s : cpu must be specified for mac '
2048 'swarmed tests' % (filename, builder, step_name))
Dirk Pranke772f55f2021-04-28 04:51:162049
Kenneth Russelleb60cbd22017-12-05 07:54:282050 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502051 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282052 self.check_output_file_consistency(verbose) # pragma: no cover
2053
Karen Qiane24b7ee2019-02-12 23:37:062054 def does_test_match(self, test_info, params_dict):
2055 """Checks to see if the test matches the parameters given.
2056
2057 Compares the provided test_info with the params_dict to see
2058 if the bot matches the parameters given. If so, returns True.
2059 Else, returns false.
2060
2061 Args:
2062 test_info (dict): Information about a specific bot provided
2063 in the format shown in waterfalls.pyl
2064 params_dict (dict): Dictionary of parameters and their values
2065 to look for in the bot
2066 Ex: {
2067 'device_os':'android',
2068 '--flag':True,
2069 'mixins': ['mixin1', 'mixin2'],
2070 'ex_key':'ex_value'
2071 }
2072
2073 """
2074 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2075 'kvm', 'pool', 'integrity'] # dimension parameters
2076 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2077 'can_use_on_swarming_builders']
2078 for param in params_dict:
2079 # if dimension parameter
2080 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2081 if not 'swarming' in test_info:
2082 return False
2083 swarming = test_info['swarming']
2084 if param in SWARMING_PARAMS:
2085 if not param in swarming:
2086 return False
2087 if not str(swarming[param]) == params_dict[param]:
2088 return False
2089 else:
Garrett Beatyade673d2023-08-04 22:00:252090 if not 'dimensions' in swarming:
Karen Qiane24b7ee2019-02-12 23:37:062091 return False
Garrett Beatyade673d2023-08-04 22:00:252092 dimensions = swarming['dimensions']
Karen Qiane24b7ee2019-02-12 23:37:062093 # only looking at the first dimension set
Garrett Beatyade673d2023-08-04 22:00:252094 if not param in dimensions:
Karen Qiane24b7ee2019-02-12 23:37:062095 return False
Garrett Beatyade673d2023-08-04 22:00:252096 if not dimensions[param] == params_dict[param]:
Karen Qiane24b7ee2019-02-12 23:37:062097 return False
2098
2099 # if flag
2100 elif param.startswith('--'):
2101 if not 'args' in test_info:
2102 return False
2103 if not param in test_info['args']:
2104 return False
2105
2106 # not dimension parameter/flag/mixin
2107 else:
2108 if not param in test_info:
2109 return False
2110 if not test_info[param] == params_dict[param]:
2111 return False
2112 return True
2113 def error_msg(self, msg):
2114 """Prints an error message.
2115
2116 In addition to a catered error message, also prints
2117 out where the user can find more help. Then, program exits.
2118 """
2119 self.print_line(msg + (' If you need more information, ' +
2120 'please run with -h or --help to see valid commands.'))
2121 sys.exit(1)
2122
2123 def find_bots_that_run_test(self, test, bots):
2124 matching_bots = []
2125 for bot in bots:
2126 bot_info = bots[bot]
2127 tests = self.flatten_tests_for_bot(bot_info)
2128 for test_info in tests:
Garrett Beatyffe83c4f2023-09-08 19:07:372129 test_name = test_info['name']
Karen Qiane24b7ee2019-02-12 23:37:062130 if not test_name == test:
2131 continue
2132 matching_bots.append(bot)
2133 return matching_bots
2134
2135 def find_tests_with_params(self, tests, params_dict):
2136 matching_tests = []
2137 for test_name in tests:
2138 test_info = tests[test_name]
2139 if not self.does_test_match(test_info, params_dict):
2140 continue
2141 if not test_name in matching_tests:
2142 matching_tests.append(test_name)
2143 return matching_tests
2144
2145 def flatten_waterfalls_for_query(self, waterfalls):
2146 bots = {}
2147 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012148 waterfall_tests = self.generate_output_tests(waterfall)
2149 for bot in waterfall_tests:
2150 bot_info = waterfall_tests[bot]
2151 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062152 return bots
2153
2154 def flatten_tests_for_bot(self, bot_info):
2155 """Returns a list of flattened tests.
2156
2157 Returns a list of tests not grouped by test category
2158 for a specific bot.
2159 """
2160 TEST_CATS = self.get_test_generator_map().keys()
2161 tests = []
2162 for test_cat in TEST_CATS:
2163 if not test_cat in bot_info:
2164 continue
2165 test_cat_tests = bot_info[test_cat]
2166 tests = tests + test_cat_tests
2167 return tests
2168
2169 def flatten_tests_for_query(self, test_suites):
2170 """Returns a flattened dictionary of tests.
2171
2172 Returns a dictionary of tests associate with their
2173 configuration, not grouped by their test suite.
2174 """
2175 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232176 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062177 for test in test_suite:
2178 test_info = test_suite[test]
2179 test_name = test
Karen Qiane24b7ee2019-02-12 23:37:062180 tests[test_name] = test_info
2181 return tests
2182
2183 def parse_query_filter_params(self, params):
2184 """Parses the filter parameters.
2185
2186 Creates a dictionary from the parameters provided
2187 to filter the bot array.
2188 """
2189 params_dict = {}
2190 for p in params:
2191 # flag
Brian Sheedy0d2300f32024-08-13 23:14:412192 if p.startswith('--'):
Karen Qiane24b7ee2019-02-12 23:37:062193 params_dict[p] = True
2194 else:
Brian Sheedy0d2300f32024-08-13 23:14:412195 pair = p.split(':')
Karen Qiane24b7ee2019-02-12 23:37:062196 if len(pair) != 2:
2197 self.error_msg('Invalid command.')
2198 # regular parameters
Brian Sheedy0d2300f32024-08-13 23:14:412199 if pair[1].lower() == 'true':
Karen Qiane24b7ee2019-02-12 23:37:062200 params_dict[pair[0]] = True
Brian Sheedy0d2300f32024-08-13 23:14:412201 elif pair[1].lower() == 'false':
Karen Qiane24b7ee2019-02-12 23:37:062202 params_dict[pair[0]] = False
2203 else:
2204 params_dict[pair[0]] = pair[1]
2205 return params_dict
2206
2207 def get_test_suites_dict(self, bots):
2208 """Returns a dictionary of bots and their tests.
2209
2210 Returns a dictionary of bots and a list of their associated tests.
2211 """
2212 test_suite_dict = dict()
2213 for bot in bots:
2214 bot_info = bots[bot]
2215 tests = self.flatten_tests_for_bot(bot_info)
2216 test_suite_dict[bot] = tests
2217 return test_suite_dict
2218
2219 def output_query_result(self, result, json_file=None):
2220 """Outputs the result of the query.
2221
2222 If a json file parameter name is provided, then
2223 the result is output into the json file. If not,
2224 then the result is printed to the console.
2225 """
2226 output = json.dumps(result, indent=2)
2227 if json_file:
2228 self.write_file(json_file, output)
2229 else:
2230 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062231
Joshua Hood56c673c2022-03-02 20:29:332232 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062233 def query(self, args):
2234 """Queries tests or bots.
2235
2236 Depending on the arguments provided, outputs a json of
2237 tests or bots matching the appropriate optional parameters provided.
2238 """
2239 # split up query statement
2240 query = args.query.split('/')
2241 self.load_configuration_files()
2242 self.resolve_configuration_files()
2243
2244 # flatten bots json
2245 tests = self.test_suites
2246 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2247
2248 cmd_class = query[0]
2249
2250 # For queries starting with 'bots'
Brian Sheedy0d2300f32024-08-13 23:14:412251 if cmd_class == 'bots':
Karen Qiane24b7ee2019-02-12 23:37:062252 if len(query) == 1:
2253 return self.output_query_result(bots, args.json)
2254 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332255 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062256 if query[1] == 'tests':
2257 test_suites_dict = self.get_test_suites_dict(bots)
2258 return self.output_query_result(test_suites_dict, args.json)
Brian Sheedy0d2300f32024-08-13 23:14:412259 self.error_msg('This query should be in the format: bots/tests.')
Karen Qiane24b7ee2019-02-12 23:37:062260
2261 else:
Brian Sheedy0d2300f32024-08-13 23:14:412262 self.error_msg('This query should have 0 or 1 "/"", found %s instead.' %
2263 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062264
2265 # For queries starting with 'bot'
Brian Sheedy0d2300f32024-08-13 23:14:412266 elif cmd_class == 'bot':
Karen Qiane24b7ee2019-02-12 23:37:062267 if not len(query) == 2 and not len(query) == 3:
Brian Sheedy0d2300f32024-08-13 23:14:412268 self.error_msg('Command should have 1 or 2 "/"", found %s instead.' %
2269 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062270 bot_id = query[1]
2271 if not bot_id in bots:
Brian Sheedy0d2300f32024-08-13 23:14:412272 self.error_msg('No bot named "' + bot_id + '" found.')
Karen Qiane24b7ee2019-02-12 23:37:062273 bot_info = bots[bot_id]
2274 if len(query) == 2:
2275 return self.output_query_result(bot_info, args.json)
2276 if not query[2] == 'tests':
Brian Sheedy0d2300f32024-08-13 23:14:412277 self.error_msg('The query should be in the format:'
2278 'bot/<bot-name>/tests.')
Karen Qiane24b7ee2019-02-12 23:37:062279
2280 bot_tests = self.flatten_tests_for_bot(bot_info)
2281 return self.output_query_result(bot_tests, args.json)
2282
2283 # For queries starting with 'tests'
Brian Sheedy0d2300f32024-08-13 23:14:412284 elif cmd_class == 'tests':
Karen Qiane24b7ee2019-02-12 23:37:062285 if not len(query) == 1 and not len(query) == 2:
Brian Sheedy0d2300f32024-08-13 23:14:412286 self.error_msg('The query should have 0 or 1 "/", found %s instead.' %
2287 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062288 flattened_tests = self.flatten_tests_for_query(tests)
2289 if len(query) == 1:
2290 return self.output_query_result(flattened_tests, args.json)
2291
2292 # create params dict
2293 params = query[1].split('&')
2294 params_dict = self.parse_query_filter_params(params)
2295 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2296 return self.output_query_result(matching_bots)
2297
2298 # For queries starting with 'test'
Brian Sheedy0d2300f32024-08-13 23:14:412299 elif cmd_class == 'test':
Karen Qiane24b7ee2019-02-12 23:37:062300 if not len(query) == 2 and not len(query) == 3:
Brian Sheedy0d2300f32024-08-13 23:14:412301 self.error_msg('The query should have 1 or 2 "/", found %s instead.' %
2302 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062303 test_id = query[1]
2304 if len(query) == 2:
2305 flattened_tests = self.flatten_tests_for_query(tests)
2306 for test in flattened_tests:
2307 if test == test_id:
2308 return self.output_query_result(flattened_tests[test], args.json)
Brian Sheedy0d2300f32024-08-13 23:14:412309 self.error_msg('There is no test named %s.' % test_id)
Karen Qiane24b7ee2019-02-12 23:37:062310 if not query[2] == 'bots':
Brian Sheedy0d2300f32024-08-13 23:14:412311 self.error_msg('The query should be in the format: '
2312 'test/<test-name>/bots')
Karen Qiane24b7ee2019-02-12 23:37:062313 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2314 return self.output_query_result(bots_for_test)
2315
2316 else:
Brian Sheedy0d2300f32024-08-13 23:14:412317 self.error_msg('Your command did not match any valid commands. '
2318 'Try starting with "bots", "bot", "tests", or "test".')
2319
Joshua Hood56c673c2022-03-02 20:29:332320 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282321
Garrett Beaty1afaccc2020-06-25 19:58:152322 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282323 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502324 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062325 elif self.args.query:
2326 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282327 else:
Greg Gutermanf60eb052020-03-12 17:40:012328 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282329 return 0
2330
Brian Sheedy0d2300f32024-08-13 23:14:412331
2332if __name__ == '__main__': # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152333 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2334 sys.exit(generator.main())