blob: 85e6cd82c3eb8e301d23cad1c716a14ce4aeaf43 [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 Beaty1b271622024-10-01 22:30:25548 @staticmethod
549 def merge_swarming(swarming1, swarming2):
550 swarming2 = dict(swarming2)
551 if 'dimensions' in swarming2:
552 swarming1.setdefault('dimensions', {}).update(swarming2.pop('dimensions'))
553 if 'named_caches' in swarming2:
554 named_caches = swarming1.setdefault('named_caches', [])
555 named_caches.extend(swarming2.pop('named_caches'))
556 swarming1.update(swarming2)
Kenneth Russelleb60cbd22017-12-05 07:54:28557
Kenneth Russelleb60cbd22017-12-05 07:54:28558 def clean_swarming_dictionary(self, swarming_dict):
559 # Clean out redundant entries from a test's "swarming" dictionary.
560 # This is really only needed to retain 100% parity with the
561 # handwritten JSON files, and can be removed once all the files are
562 # autogenerated.
563 if 'shards' in swarming_dict:
564 if swarming_dict['shards'] == 1: # pragma: no cover
565 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24566 if 'hard_timeout' in swarming_dict:
567 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
568 del swarming_dict['hard_timeout'] # pragma: no cover
Garrett Beatybb18d532023-06-26 22:16:33569 del swarming_dict['can_use_on_swarming_builders']
Kenneth Russelleb60cbd22017-12-05 07:54:28570
Garrett Beatye3a606ceb2024-04-30 22:13:13571 def resolve_os_conditional_values(self, test, builder):
572 for key, fn in (
573 ('android_swarming', self.is_android),
574 ('chromeos_swarming', self.is_chromeos),
575 ):
576 swarming = test.pop(key, None)
577 if swarming and fn(builder):
Garrett Beaty1b271622024-10-01 22:30:25578 self.merge_swarming(test['swarming'], swarming)
Garrett Beatye3a606ceb2024-04-30 22:13:13579
580 for key, fn in (
581 ('desktop_args', lambda cfg: not self.is_android(cfg)),
582 ('lacros_args', self.is_lacros),
583 ('linux_args', self.is_linux),
584 ('android_args', self.is_android),
585 ('chromeos_args', self.is_chromeos),
586 ('mac_args', self.is_mac),
587 ('win_args', self.is_win),
588 ('win64_args', self.is_win64),
589 ):
590 args = test.pop(key, [])
591 if fn(builder):
592 test.setdefault('args', []).extend(args)
593
594 def apply_common_transformations(self,
595 waterfall,
596 builder_name,
597 builder,
598 test,
599 test_name,
600 *,
601 swarmable=True,
602 supports_args=True):
603 # Initialize the swarming dictionary
604 swarmable = swarmable and builder.get('use_swarming', True)
605 test.setdefault('swarming', {}).setdefault('can_use_on_swarming_builders',
606 swarmable)
607
Garrett Beaty4b9f1752024-09-26 20:02:50608 # Test common mixins are mixins specified in the test declaration itself. To
609 # match the order of expansion in starlark, they take effect before anything
610 # specified in the legacy_test_config.
611 test_common = test.pop('test_common', {})
612 if test_common:
613 test_common_mixins = test_common.pop('mixins', [])
614 self.ensure_valid_mixin_list(test_common_mixins,
615 f'test {test_name} test_common mixins')
616 test_common = self.apply_mixins(test_common, test_common_mixins, [],
617 builder)
618 test = self.apply_mixin(test, test_common, builder)
619
Garrett Beatye3a606ceb2024-04-30 22:13:13620 mixins_to_ignore = test.pop('remove_mixins', [])
621 self.ensure_valid_mixin_list(mixins_to_ignore,
622 f'test {test_name} remove_mixins')
623
Garrett Beatycc184692024-05-01 14:57:09624 # Expand any conditional values
625 self.resolve_os_conditional_values(test, builder)
626
627 # Apply mixins from the test
628 test_mixins = test.pop('mixins', [])
629 self.ensure_valid_mixin_list(test_mixins, f'test {test_name} mixins')
630 test = self.apply_mixins(test, test_mixins, mixins_to_ignore, builder)
631
Garrett Beaty65b7d362024-10-01 16:21:42632 # Apply any variant details
633 variant = test.pop('*variant*', None)
634 if variant is not None:
635 test = self.apply_mixin(variant, test)
636 variant_mixins = test.pop('*variant_mixins*', [])
637 self.ensure_valid_mixin_list(
638 variant_mixins,
639 (f'variant mixins for test {test_name}'
640 f' with variant with identifier{test["variant_id"]}'))
641 test = self.apply_mixins(test, variant_mixins, mixins_to_ignore, builder)
642
Garrett Beatye3a606ceb2024-04-30 22:13:13643 # Add any swarming or args from the builder
Garrett Beaty1b271622024-10-01 22:30:25644 self.merge_swarming(test['swarming'], builder.get('swarming', {}))
Garrett Beatye3a606ceb2024-04-30 22:13:13645 if supports_args:
646 test.setdefault('args', []).extend(builder.get('args', []))
647
Garrett Beatye3a606ceb2024-04-30 22:13:13648 # Apply mixins from the waterfall
649 waterfall_mixins = waterfall.get('mixins', [])
650 self.ensure_valid_mixin_list(waterfall_mixins,
651 f"waterfall {waterfall['name']} mixins")
652 test = self.apply_mixins(test, waterfall_mixins, mixins_to_ignore, builder)
653
654 # Apply mixins from the builder
655 builder_mixins = builder.get('mixins', [])
656 self.ensure_valid_mixin_list(builder_mixins,
Brian Sheedy0d2300f32024-08-13 23:14:41657 f'builder {builder_name} mixins')
Garrett Beatye3a606ceb2024-04-30 22:13:13658 test = self.apply_mixins(test, builder_mixins, mixins_to_ignore, builder)
659
Kenneth Russelleb60cbd22017-12-05 07:54:28660 # See if there are any exceptions that need to be merged into this
661 # test's specification.
Garrett Beatye3a606ceb2024-04-30 22:13:13662 modifications = self.get_test_modifications(test, builder_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28663 if modifications:
Garrett Beaty1b271622024-10-01 22:30:25664 test = self.apply_mixin(modifications, test, builder)
Garrett Beatye3a606ceb2024-04-30 22:13:13665
666 # Clean up the swarming entry or remove it if it's unnecessary
Garrett Beatybfeff8f2023-06-16 18:57:25667 if (swarming_dict := test.get('swarming')) is not None:
Garrett Beatybb18d532023-06-26 22:16:33668 if swarming_dict.get('can_use_on_swarming_builders'):
Garrett Beatybfeff8f2023-06-16 18:57:25669 self.clean_swarming_dictionary(swarming_dict)
670 else:
671 del test['swarming']
Garrett Beatye3a606ceb2024-04-30 22:13:13672
Ben Pastenee012aea42019-05-14 22:32:28673 # Ensure all Android Swarming tests run only on userdebug builds if another
674 # build type was not specified.
Garrett Beatye3a606ceb2024-04-30 22:13:13675 if 'swarming' in test and self.is_android(builder):
Garrett Beatyade673d2023-08-04 22:00:25676 dimensions = test.get('swarming', {}).get('dimensions', {})
677 if (dimensions.get('os') == 'Android'
678 and not dimensions.get('device_os_type')):
679 dimensions['device_os_type'] = 'userdebug'
Garrett Beatye3a606ceb2024-04-30 22:13:13680
681 # Apply any replacements specified for the test for the builder
682 self.replace_test_args(test, test_name, builder_name)
683
684 # Remove args if it is empty
685 if 'args' in test:
686 if not test['args']:
687 del test['args']
688 else:
689 # Replace any magic arguments with their actual value
690 self.substitute_magic_args(test, builder_name, builder)
691
692 test['args'] = self.maybe_fixup_args_array(test['args'])
Ben Pastenee012aea42019-05-14 22:32:28693
Kenneth Russelleb60cbd22017-12-05 07:54:28694 return test
695
Brian Sheedye6ea0ee2019-07-11 02:54:37696 def replace_test_args(self, test, test_name, tester_name):
Garrett Beatyffe83c4f2023-09-08 19:07:37697 replacements = self.get_test_replacements(test, tester_name) or {}
Brian Sheedye6ea0ee2019-07-11 02:54:37698 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23699 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37700 if key not in valid_replacement_keys:
701 raise BBGenErr(
702 'Given replacement key %s for %s on %s is not in the list of valid '
703 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23704 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37705 found_key = False
706 for i, test_key in enumerate(test.get(key, [])):
707 # Handle both the key/value being replaced being defined as two
708 # separate items or as key=value.
709 if test_key == replacement_key:
710 found_key = True
711 # Handle flags without values.
Brian Sheedy822e03742024-08-09 18:48:14712 if replacement_val is None:
Brian Sheedye6ea0ee2019-07-11 02:54:37713 del test[key][i]
714 else:
715 test[key][i+1] = replacement_val
716 break
Joshua Hood56c673c2022-03-02 20:29:33717 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37718 found_key = True
Brian Sheedy822e03742024-08-09 18:48:14719 if replacement_val is None:
Brian Sheedye6ea0ee2019-07-11 02:54:37720 del test[key][i]
721 else:
722 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
723 break
724 if not found_key:
725 raise BBGenErr('Could not find %s in existing list of values for key '
726 '%s in %s on %s' % (replacement_key, key, test_name,
727 tester_name))
728
Shenghua Zhangaba8bad2018-02-07 02:12:09729 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05730 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26731 True):
732 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06733 # are targeting CrOS hardware and so need the special trigger script.
Garrett Beatyade673d2023-08-04 22:00:25734 if 'device_type' in test.get('swarming', {}).get('dimensions', {}):
Ben Pastenea9e583b2019-01-16 02:57:26735 test['trigger_script'] = {
736 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
737 }
Shenghua Zhangaba8bad2018-02-07 02:12:09738
Garrett Beatyffe83c4f2023-09-08 19:07:37739 def add_android_presentation_args(self, tester_config, result):
John Budorick262ae112019-07-12 19:24:38740 bucket = tester_config.get('results_bucket', 'chromium-result-details')
Garrett Beaty94af4272024-04-17 18:06:14741 result.setdefault('args', []).append('--gs-results-bucket=%s' % bucket)
742
743 if ('swarming' in result and 'merge' not in 'result'
744 and not tester_config.get('skip_merge_script', False)):
Ben Pastene858f4be2019-01-09 23:52:09745 result['merge'] = {
Garrett Beatyffe83c4f2023-09-08 19:07:37746 'args': [
747 '--bucket',
748 bucket,
749 '--test-name',
750 result['name'],
751 ],
752 'script': ('//build/android/pylib/results/presentation/'
753 'test_results_presentation.py'),
Ben Pastene858f4be2019-01-09 23:52:09754 }
Ben Pastene858f4be2019-01-09 23:52:09755
Kenneth Russelleb60cbd22017-12-05 07:54:28756 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
757 test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37758 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28759 return None
760 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37761 # Use test_name here instead of test['name'] because test['name'] will be
762 # modified with the variant identifier in a matrix compound suite
763 result.setdefault('test', test_name)
John Budorickab108712018-09-01 00:12:21764
Garrett Beatye3a606ceb2024-04-30 22:13:13765 result = self.apply_common_transformations(waterfall, tester_name,
766 tester_config, result, test_name)
Garrett Beaty94af4272024-04-17 18:06:14767 if self.is_android(tester_config) and 'swarming' in result:
768 if not result.get('use_isolated_scripts_api', False):
Alison Gale71bd8f152024-04-26 22:38:20769 # TODO(crbug.com/40725094) make Android presentation work with
Yuly Novikov26dd47052021-02-11 00:57:14770 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37771 self.add_android_presentation_args(tester_config, result)
Yuly Novikov26dd47052021-02-11 00:57:14772 result['args'] = result.get('args', []) + ['--recover-devices']
Shenghua Zhangaba8bad2018-02-07 02:12:09773 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43774
Garrett Beatybb18d532023-06-26 22:16:33775 if 'swarming' in result and not result.get('merge'):
Jamie Madilla8be0d72020-10-02 05:24:04776 if test_config.get('use_isolated_scripts_api', False):
777 merge_script = 'standard_isolated_script_merge'
778 else:
779 merge_script = 'standard_gtest_merge'
780
Stephen Martinisbc7b7772019-05-01 22:01:43781 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04782 'script': '//testing/merge_scripts/%s.py' % merge_script,
Stephen Martinisbc7b7772019-05-01 22:01:43783 }
Kenneth Russelleb60cbd22017-12-05 07:54:28784 return result
785
786 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
787 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37788 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28789 return None
790 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37791 # Use test_name here instead of test['name'] because test['name'] will be
792 # modified with the variant identifier in a matrix compound suite
Garrett Beatydca3d882023-09-14 23:50:32793 result.setdefault('test', test_name)
Garrett Beatye3a606ceb2024-04-30 22:13:13794 result = self.apply_common_transformations(waterfall, tester_name,
795 tester_config, result, test_name)
Garrett Beaty94af4272024-04-17 18:06:14796 if self.is_android(tester_config) and 'swarming' in result:
Yuly Novikov26dd47052021-02-11 00:57:14797 if tester_config.get('use_android_presentation', False):
Alison Gale71bd8f152024-04-26 22:38:20798 # TODO(crbug.com/40725094) make Android presentation work with
Yuly Novikov26dd47052021-02-11 00:57:14799 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37800 self.add_android_presentation_args(tester_config, result)
Shenghua Zhangaba8bad2018-02-07 02:12:09801 self.add_common_test_properties(result, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17802
Garrett Beatybb18d532023-06-26 22:16:33803 if 'swarming' in result and not result.get('merge'):
Alison Gale923a33e2024-04-22 23:34:28804 # TODO(crbug.com/41456107): Consider adding the ability to not have
Stephen Martinisf50047062019-05-06 22:26:17805 # this default.
806 result['merge'] = {
807 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
Stephen Martinisf50047062019-05-06 22:26:17808 }
Kenneth Russelleb60cbd22017-12-05 07:54:28809 return result
810
Garrett Beaty938560e32024-09-26 18:57:35811 _SCRIPT_FIELDS = ('name', 'script', 'args', 'precommit_args',
812 'non_precommit_args', 'resultdb')
813
Kenneth Russelleb60cbd22017-12-05 07:54:28814 def generate_script_test(self, waterfall, tester_name, tester_config,
815 test_name, test_config):
Alison Gale47d1537d2024-04-19 21:31:46816 # TODO(crbug.com/40623237): Remove this check whenever a better
Brian Sheedy158cd0f2019-04-26 01:12:44817 # long-term solution is implemented.
818 if (waterfall.get('forbid_script_tests', False) or
819 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
820 raise BBGenErr('Attempted to generate a script test on tester ' +
821 tester_name + ', which explicitly forbids script tests')
Garrett Beatyffe83c4f2023-09-08 19:07:37822 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28823 return None
Garrett Beaty938560e32024-09-26 18:57:35824 result = copy.deepcopy(test_config)
Garrett Beatye3a606ceb2024-04-30 22:13:13825 result = self.apply_common_transformations(waterfall,
826 tester_name,
827 tester_config,
828 result,
829 test_name,
830 swarmable=False,
831 supports_args=False)
Garrett Beaty938560e32024-09-26 18:57:35832 result = {k: result[k] for k in self._SCRIPT_FIELDS if k in result}
Kenneth Russelleb60cbd22017-12-05 07:54:28833 return result
834
Xinan Lin05fb9c1752020-12-17 00:15:52835 def generate_skylab_test(self, waterfall, tester_name, tester_config,
836 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37837 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Xinan Lin05fb9c1752020-12-17 00:15:52838 return None
839 result = copy.deepcopy(test_config)
Brian Sheedy67937ad12024-03-06 22:53:55840 result.setdefault('test', test_name)
Struan Shrimpton08baa3c2024-08-09 17:21:45841 result['run_cft'] = True
yoshiki iguchid1664ef2024-03-28 19:16:52842
843 if 'cros_board' in result or 'cros_board' in tester_config:
844 result['cros_board'] = tester_config.get('cros_board') or result.get(
845 'cros_board')
846 else:
Brian Sheedy0d2300f32024-08-13 23:14:41847 raise BBGenErr('skylab tests must specify cros_board.')
yoshiki iguchid1664ef2024-03-28 19:16:52848 if 'cros_model' in result or 'cros_model' in tester_config:
849 result['cros_model'] = tester_config.get('cros_model') or result.get(
850 'cros_model')
851 if 'dut_pool' in result or 'cros_dut_pool' in tester_config:
852 result['dut_pool'] = tester_config.get('cros_dut_pool') or result.get(
853 'dut_pool')
Qijiang Fan9032762d2024-06-25 06:02:24854 if 'cros_build_target' in result or 'cros_build_target' in tester_config:
855 result['cros_build_target'] = tester_config.get(
856 'cros_build_target') or result.get('cros_build_target')
yoshiki iguchid1664ef2024-03-28 19:16:52857
yoshiki iguchia5f87c7d2024-06-19 02:48:34858 # Skylab tests enable the shard-level-retry by default.
859 if ('shard_level_retries_on_ctp' in result
860 or 'shard_level_retries_on_ctp' in tester_config):
861 result['shard_level_retries_on_ctp'] = (
862 tester_config.get('shard_level_retries_on_ctp')
863 or result.get('shard_level_retries_on_ctp'))
Qijiang Fan84a0286b2024-06-25 06:44:08864 elif result.get('experiment_percentage') != 100:
yoshiki iguchia5f87c7d2024-06-19 02:48:34865 result['shard_level_retries_on_ctp'] = 1
866
Garrett Beatye3a606ceb2024-04-30 22:13:13867 result = self.apply_common_transformations(waterfall,
868 tester_name,
869 tester_config,
870 result,
871 test_name,
872 swarmable=False)
Xinan Lin05fb9c1752020-12-17 00:15:52873 return result
874
Garrett Beaty65d44222023-08-01 17:22:11875 def substitute_gpu_args(self, tester_config, test, args):
Kenneth Russell8a386d42018-06-02 09:48:01876 substitutions = {
877 # Any machine in waterfalls.pyl which desires to run GPU tests
878 # must provide the os_type key.
879 'os_type': tester_config['os_type'],
880 'gpu_vendor_id': '0',
881 'gpu_device_id': '0',
882 }
Garrett Beatyade673d2023-08-04 22:00:25883 dimensions = test.get('swarming', {}).get('dimensions', {})
884 if 'gpu' in dimensions:
885 # First remove the driver version, then split into vendor and device.
886 gpu = dimensions['gpu']
887 if gpu != 'none':
888 gpu = gpu.split('-')[0].split(':')
889 substitutions['gpu_vendor_id'] = gpu[0]
890 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01891 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
892
Garrett Beaty7436fb72024-08-07 20:20:58893 # LINT.IfChange(gpu_telemetry_test)
894
Kenneth Russell8a386d42018-06-02 09:48:01895 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30896 test_name, test_config, is_android_webview,
Xinan Linedcf05b32023-10-19 23:13:50897 is_cast_streaming, is_skylab):
Kenneth Russell8a386d42018-06-02 09:48:01898 # These are all just specializations of isolated script tests with
899 # a bunch of boilerplate command line arguments added.
900
901 # The step name must end in 'test' or 'tests' in order for the
902 # results to automatically show up on the flakiness dashboard.
903 # (At least, this was true some time ago.) Continue to use this
904 # naming convention for the time being to minimize changes.
Garrett Beaty235c1412023-08-29 20:26:29905 #
906 # test name is the name of the test without the variant ID added
907 if not (test_name.endswith('test') or test_name.endswith('tests')):
908 raise BBGenErr(
909 f'telemetry test names must end with test or tests, got {test_name}')
Garrett Beatyffe83c4f2023-09-08 19:07:37910 result = self.generate_isolated_script_test(waterfall, tester_name,
911 tester_config, test_name,
912 test_config)
Kenneth Russell8a386d42018-06-02 09:48:01913 if not result:
914 return None
Garrett Beatydca3d882023-09-14 23:50:32915 result['test'] = test_config.get('test') or self.get_default_isolate_name(
916 tester_config, is_android_webview)
Chan Liab7d8dd82020-04-24 23:42:19917
Chan Lia3ad1502020-04-28 05:32:11918 # Populate test_id_prefix.
Garrett Beatydca3d882023-09-14 23:50:32919 gn_entry = self.gn_isolate_map[result['test']]
Chan Li17d969f92020-07-10 00:50:03920 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19921
Kenneth Russell8a386d42018-06-02 09:48:01922 args = result.get('args', [])
Garrett Beatyffe83c4f2023-09-08 19:07:37923 # Use test_name here instead of test['name'] because test['name'] will be
924 # modified with the variant identifier in a matrix compound suite
Kenneth Russell8a386d42018-06-02 09:48:01925 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14926
927 # These tests upload and download results from cloud storage and therefore
928 # aren't idempotent yet. https://crbug.com/549140.
Garrett Beatybfeff8f2023-06-16 18:57:25929 if 'swarming' in result:
930 result['swarming']['idempotent'] = False
erikchen6da2d9b2018-08-03 23:01:14931
Fabrice de Ganscbd655f2022-08-04 20:15:30932 browser = ''
933 if is_cast_streaming:
934 browser = 'cast-streaming-shell'
935 elif is_android_webview:
936 browser = 'android-webview-instrumentation'
937 else:
938 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:52939
Greg Thompsoncec7d8d2023-01-10 19:11:53940 extra_browser_args = []
941
Brian Sheedy4053a702020-07-28 02:09:52942 # Most platforms require --enable-logging=stderr to get useful browser logs.
943 # However, this actively messes with logging on CrOS (because Chrome's
944 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
945 # in order to see JavaScript console messages. See
946 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
Greg Thompsoncec7d8d2023-01-10 19:11:53947 if self.is_chromeos(tester_config):
948 extra_browser_args.append('--log-level=0')
949 elif not self.is_fuchsia(tester_config) or browser != 'fuchsia-chrome':
950 # Stderr logging is not needed for Chrome browser on Fuchsia, as ordinary
951 # logging via syslog is captured.
952 extra_browser_args.append('--enable-logging=stderr')
953
954 # --expose-gc allows the WebGL conformance tests to more reliably
955 # reproduce GC-related bugs in the V8 bindings.
956 extra_browser_args.append('--js-flags=--expose-gc')
Brian Sheedy4053a702020-07-28 02:09:52957
Xinan Linedcf05b32023-10-19 23:13:50958 # Skylab supports sharding, so reuse swarming's shard config.
959 if is_skylab and 'shards' not in result and test_config.get(
960 'swarming', {}).get('shards'):
961 result['shards'] = test_config['swarming']['shards']
962
Kenneth Russell8a386d42018-06-02 09:48:01963 args = [
Bo Liu555a0f92019-03-29 12:11:56964 test_to_run,
965 '--show-stdout',
966 '--browser=%s' % browser,
967 # --passthrough displays more of the logging in Telemetry when
968 # run via typ, in particular some of the warnings about tests
969 # being expected to fail, but passing.
970 '--passthrough',
971 '-v',
Brian Sheedy814e0482022-10-03 23:24:12972 '--stable-jobs',
Greg Thompsoncec7d8d2023-01-10 19:11:53973 '--extra-browser-args=%s' % ' '.join(extra_browser_args),
Brian Sheedy997e48062023-10-18 02:28:13974 '--enforce-browser-version',
Kenneth Russell8a386d42018-06-02 09:48:01975 ] + args
Garrett Beatybfeff8f2023-06-16 18:57:25976 result['args'] = self.maybe_fixup_args_array(
Garrett Beaty65d44222023-08-01 17:22:11977 self.substitute_gpu_args(tester_config, result, args))
Kenneth Russell8a386d42018-06-02 09:48:01978 return result
979
Garrett Beaty7436fb72024-08-07 20:20:58980 # pylint: disable=line-too-long
981 # LINT.ThenChange(//infra/config/lib/targets-internal/test-types/gpu_telemetry_test.star)
982 # pylint: enable=line-too-long
983
Brian Sheedyf74819b2021-06-04 01:38:38984 def get_default_isolate_name(self, tester_config, is_android_webview):
985 if self.is_android(tester_config):
986 if is_android_webview:
987 return 'telemetry_gpu_integration_test_android_webview'
988 return (
989 'telemetry_gpu_integration_test' +
990 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:33991 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:17992 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:33993 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:38994
Kenneth Russelleb60cbd22017-12-05 07:54:28995 def get_test_generator_map(self):
996 return {
Bo Liu555a0f92019-03-29 12:11:56997 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30998 GPUTelemetryTestGenerator(self, is_android_webview=True),
999 'cast_streaming_tests':
1000 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:561001 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301002 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561003 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301004 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561005 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301006 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561007 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301008 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:521009 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301010 SkylabGenerator(self),
Brian Sheedyb6491ba2022-09-26 20:49:491011 'skylab_gpu_telemetry_tests':
1012 SkylabGPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281013 }
1014
Kenneth Russell8a386d42018-06-02 09:48:011015 def get_test_type_remapper(self):
1016 return {
Fabrice de Gans223272482022-08-08 16:56:571017 # These are a specialization of isolated_scripts with a bunch of
1018 # boilerplate command line arguments added to each one.
1019 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
1020 'cast_streaming_tests': 'isolated_scripts',
1021 'gpu_telemetry_tests': 'isolated_scripts',
Brian Sheedyb6491ba2022-09-26 20:49:491022 # These are the same as existing test types, just configured to run
1023 # in Skylab instead of via normal swarming.
1024 'skylab_gpu_telemetry_tests': 'skylab_tests',
Kenneth Russell8a386d42018-06-02 09:48:011025 }
1026
Jeff Yoon67c3e832020-02-08 07:39:381027 def check_composition_type_test_suites(self, test_type,
1028 additional_validators=None):
1029 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1030 validators = [check_compound_references,
1031 check_basic_references,
1032 check_conflicting_definitions]
1033 if additional_validators:
1034 validators += additional_validators
1035
1036 target_suites = self.test_suites.get(test_type, {})
1037 other_test_type = ('compound_suites'
1038 if test_type == 'matrix_compound_suites'
1039 else 'matrix_compound_suites')
1040 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011041 basic_suites = self.test_suites.get('basic_suites', {})
1042
Jamie Madillcf4f8c72021-05-20 19:24:231043 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011044 if suite in basic_suites:
1045 raise BBGenErr('%s names may not duplicate basic test suite names '
1046 '(error found while processsing %s)'
1047 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011048
Jeff Yoon67c3e832020-02-08 07:39:381049 seen_tests = {}
1050 for sub_suite in suite_def:
1051 for validator in validators:
1052 validator(
1053 basic_suites=basic_suites,
1054 other_test_suites=other_suites,
1055 seen_tests=seen_tests,
1056 sub_suite=sub_suite,
1057 suite=suite,
1058 suite_def=suite_def,
1059 target_test_suites=target_suites,
1060 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051061 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381062 )
Kenneth Russelleb60cbd22017-12-05 07:54:281063
Stephen Martinis54d64ad2018-09-21 22:16:201064 def flatten_test_suites(self):
1065 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011066 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1067 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231068 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011069 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201070 self.test_suites = new_test_suites
1071
Chan Lia3ad1502020-04-28 05:32:111072 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231073 for suite in self.test_suites['basic_suites'].values():
1074 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561075 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411076
Garrett Beatydca3d882023-09-14 23:50:321077 isolate_name = test.get('test') or key
Nodir Turakulovfce34292019-12-18 17:05:411078 gn_entry = self.gn_isolate_map.get(isolate_name)
1079 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281080 label = gn_entry['label']
1081
1082 if label.count(':') != 1:
1083 raise BBGenErr(
1084 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1085 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1086 (label, isolate_name))
1087 if label.split(':')[1] != isolate_name:
1088 raise BBGenErr(
1089 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1090 ' label "%s" see http://crbug.com/1071091 for details.' %
1091 (isolate_name, label))
1092
Chan Lia3ad1502020-04-28 05:32:111093 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411094 else: # pragma: no cover
1095 # Some tests do not have an entry gn_isolate_map.pyl, such as
1096 # telemetry tests.
Alison Gale47d1537d2024-04-19 21:31:461097 # TODO(crbug.com/40112160): require an entry in gn_isolate_map.
Nodir Turakulovfce34292019-12-18 17:05:411098 pass
1099
Kenneth Russelleb60cbd22017-12-05 07:54:281100 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011101 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201102
Jeff Yoon8154e582019-12-03 23:30:011103 compound_suites = self.test_suites.get('compound_suites', {})
1104 # check_composition_type_test_suites() checks that all basic suites
1105 # referenced by compound suites exist.
1106 basic_suites = self.test_suites.get('basic_suites')
1107
Jamie Madillcf4f8c72021-05-20 19:24:231108 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011109 # Resolve this to a dictionary.
1110 full_suite = {}
1111 for entry in value:
1112 suite = basic_suites[entry]
1113 full_suite.update(suite)
1114 compound_suites[name] = full_suite
1115
Jeff Yoon85fb8df2020-08-20 16:47:431116 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381117 """ Merge variant-defined configurations to each test case definition in a
1118 test suite.
1119
1120 The output maps a unique test name to an array of configurations because
1121 there may exist more than one definition for a test name using variants. The
1122 test name is referenced while mapping machines to test suites, so unpacking
1123 the array is done by the generators.
1124
1125 Args:
1126 basic_test_definition: a {} defined test suite in the format
1127 test_name:test_config
1128 variants: an [] of {} defining configurations to be applied to each test
1129 case in the basic test_definition
1130
1131 Return:
1132 a {} of test_name:[{}], where each {} is a merged configuration
1133 """
1134
1135 # Each test in a basic test suite will have a definition per variant.
1136 test_suite = {}
Garrett Beaty8d6708c2023-07-20 17:20:411137 for variant in variants:
1138 # Unpack the variant from variants.pyl if it's string based.
1139 if isinstance(variant, str):
1140 variant = self.variants[variant]
Jeff Yoonda581c32020-03-06 03:56:051141
Garrett Beaty8d6708c2023-07-20 17:20:411142 # If 'enabled' is set to False, we will not use this variant; otherwise if
1143 # the variant doesn't include 'enabled' variable or 'enabled' is set to
1144 # True, we will use this variant
1145 if not variant.get('enabled', True):
1146 continue
Jeff Yoon67c3e832020-02-08 07:39:381147
Garrett Beaty8d6708c2023-07-20 17:20:411148 # Make a shallow copy of the variant to remove variant-specific fields,
1149 # leaving just mixin fields
1150 variant = copy.copy(variant)
1151 variant.pop('enabled', None)
1152 identifier = variant.pop('identifier')
1153 variant_mixins = variant.pop('mixins', [])
1154 variant_skylab = variant.pop('skylab', {})
Jeff Yoon67c3e832020-02-08 07:39:381155
Garrett Beaty8d6708c2023-07-20 17:20:411156 for test_name, test_config in basic_test_definition.items():
Garrett Beaty65b7d362024-10-01 16:21:421157 new_test = copy.copy(test_config)
Xinan Lin05fb9c1752020-12-17 00:15:521158
Jeff Yoon67c3e832020-02-08 07:39:381159 # The identifier is used to make the name of the test unique.
1160 # Generators in the recipe uniquely identify a test by it's name, so we
1161 # don't want to have the same name for each variant.
Garrett Beaty235c1412023-08-29 20:26:291162 new_test['name'] = f'{test_name} {identifier}'
Ben Pastene5f231cf22022-05-05 18:03:071163
1164 # Attach the variant identifier to the test config so downstream
1165 # generators can make modifications based on the original name. This
1166 # is mainly used in generate_gpu_telemetry_test().
Garrett Beaty8d6708c2023-07-20 17:20:411167 new_test['variant_id'] = identifier
Ben Pastene5f231cf22022-05-05 18:03:071168
Garrett Beaty65b7d362024-10-01 16:21:421169 # Save the variant details and mixins to be applied in
1170 # apply_common_transformations to match the order that starlark will
1171 # apply things
1172 new_test['*variant*'] = variant
1173 new_test['*variant_mixins*'] = variant_mixins + mixins
1174
1175 # TODO: crbug.com/40258588 - When skylab support is implemented in
1176 # starlark, these fields should be incorporated into mixins and handled
1177 # consistently with other fields
Garrett Beaty8d6708c2023-07-20 17:20:411178 for k, v in variant_skylab.items():
Sven Zheng22ba6312023-10-16 22:59:351179 # cros_chrome_version is the ash chrome version in the cros img in the
1180 # variant of cros_board. We don't want to include it in the final json
1181 # files; so remove it.
Garrett Beaty8d6708c2023-07-20 17:20:411182 if k != 'cros_chrome_version':
1183 new_test[k] = v
1184
Sven Zheng22ba6312023-10-16 22:59:351185 # For skylab, we need to pop the correct `autotest_name`. This field
1186 # defines what wrapper we use in OS infra. e.g. for gtest it's
1187 # https://source.chromium.org/chromiumos/chromiumos/codesearch/+/main:src/third_party/autotest/files/server/site_tests/chromium/chromium.py
1188 if variant_skylab and 'autotest_name' not in new_test:
1189 if 'tast_expr' in test_config:
1190 if 'lacros' in test_config['name']:
1191 new_test['autotest_name'] = 'tast.lacros-from-gcs'
1192 else:
1193 new_test['autotest_name'] = 'tast.chrome-from-gcs'
1194 elif 'benchmark' in test_config:
1195 new_test['autotest_name'] = 'chromium_Telemetry'
1196 else:
1197 new_test['autotest_name'] = 'chromium'
1198
Garrett Beaty8d6708c2023-07-20 17:20:411199 test_suite.setdefault(test_name, []).append(new_test)
1200
Jeff Yoon67c3e832020-02-08 07:39:381201 return test_suite
1202
Jeff Yoon8154e582019-12-03 23:30:011203 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381204 self.check_composition_type_test_suites('matrix_compound_suites',
1205 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011206
1207 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381208 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011209 # referenced by matrix suites exist.
1210 basic_suites = self.test_suites.get('basic_suites')
1211
Brian Sheedy822e03742024-08-09 18:48:141212 def update_tests_uncurried(full_suite, expanded):
1213 for test_name, new_tests in expanded.items():
1214 if not isinstance(new_tests, list):
1215 new_tests = [new_tests]
1216 tests_for_name = full_suite.setdefault(test_name, [])
1217 for t in new_tests:
1218 if t not in tests_for_name:
1219 tests_for_name.append(t)
1220
Garrett Beaty235c1412023-08-29 20:26:291221 for matrix_suite_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011222 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381223
Jamie Madillcf4f8c72021-05-20 19:24:231224 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381225 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1226
Brian Sheedy822e03742024-08-09 18:48:141227 update_tests = functools.partial(update_tests_uncurried, full_suite)
Garrett Beaty235c1412023-08-29 20:26:291228
Garrett Beaty73c4cd42024-10-04 17:55:081229 mixins = mtx_test_suite_config.get('mixins', [])
Garrett Beaty60a7b2a2023-09-13 23:00:401230 if (variants := mtx_test_suite_config.get('variants')):
Garrett Beaty60a7b2a2023-09-13 23:00:401231 result = self.resolve_variants(basic_test_def, variants, mixins)
Garrett Beaty235c1412023-08-29 20:26:291232 update_tests(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271233 else:
Garrett Beaty73c4cd42024-10-04 17:55:081234 suite = copy.deepcopy(basic_suites[test_suite])
1235 for test_config in suite.values():
1236 test_config['mixins'] = test_config.get('mixins', []) + mixins
Garrett Beaty235c1412023-08-29 20:26:291237 update_tests(suite)
1238 matrix_compound_suites[matrix_suite_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281239
1240 def link_waterfalls_to_test_suites(self):
1241 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231242 for tester_name, tester in waterfall['machines'].items():
1243 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281244 if not value in self.test_suites:
1245 # Hard / impossible to cover this in the unit test.
1246 raise self.unknown_test_suite(
1247 value, tester_name, waterfall['name']) # pragma: no cover
1248 tester['test_suites'][suite] = self.test_suites[value]
1249
1250 def load_configuration_files(self):
Garrett Beaty79339e182023-04-10 20:45:471251 self.waterfalls = self.load_pyl_file(self.args.waterfalls_pyl_path)
1252 self.test_suites = self.load_pyl_file(self.args.test_suites_pyl_path)
1253 self.exceptions = self.load_pyl_file(
1254 self.args.test_suite_exceptions_pyl_path)
1255 self.mixins = self.load_pyl_file(self.args.mixins_pyl_path)
1256 self.gn_isolate_map = self.load_pyl_file(self.args.gn_isolate_map_pyl_path)
Chong Guee622242020-10-28 18:17:351257 for isolate_map in self.args.isolate_map_files:
1258 isolate_map = self.load_pyl_file(isolate_map)
1259 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1260 if duplicates:
1261 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1262 ', '.join(duplicates))
1263 self.gn_isolate_map.update(isolate_map)
1264
Garrett Beaty79339e182023-04-10 20:45:471265 self.variants = self.load_pyl_file(self.args.variants_pyl_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281266
1267 def resolve_configuration_files(self):
Garrett Beaty086b3402024-09-25 23:45:341268 self.resolve_mixins()
Garrett Beaty235c1412023-08-29 20:26:291269 self.resolve_test_names()
Garrett Beatydca3d882023-09-14 23:50:321270 self.resolve_isolate_names()
Garrett Beaty65d44222023-08-01 17:22:111271 self.resolve_dimension_sets()
Chan Lia3ad1502020-04-28 05:32:111272 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281273 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011274 self.resolve_matrix_compound_test_suites()
1275 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281276 self.link_waterfalls_to_test_suites()
1277
Garrett Beaty086b3402024-09-25 23:45:341278 def resolve_mixins(self):
1279 for mixin in self.mixins.values():
1280 mixin.pop('fail_if_unused', None)
1281
Garrett Beaty235c1412023-08-29 20:26:291282 def resolve_test_names(self):
1283 for suite_name, suite in self.test_suites.get('basic_suites').items():
1284 for test_name, test in suite.items():
1285 if 'name' in test:
1286 raise BBGenErr(
1287 f'The name field is set in test {test_name} in basic suite '
1288 f'{suite_name}, this is not supported, the test name is the key '
1289 'within the basic suite')
Garrett Beatyffe83c4f2023-09-08 19:07:371290 # When a test is expanded with variants, this will be overwritten, but
1291 # this ensures every test definition has the name field set
1292 test['name'] = test_name
Garrett Beaty235c1412023-08-29 20:26:291293
Garrett Beatydca3d882023-09-14 23:50:321294 def resolve_isolate_names(self):
1295 for suite_name, suite in self.test_suites.get('basic_suites').items():
1296 for test_name, test in suite.items():
1297 if 'isolate_name' in test:
1298 raise BBGenErr(
1299 f'The isolate_name field is set in test {test_name} in basic '
1300 f'suite {suite_name}, the test field should be used instead')
1301
Garrett Beaty65d44222023-08-01 17:22:111302 def resolve_dimension_sets(self):
Garrett Beaty65d44222023-08-01 17:22:111303
1304 def definitions():
1305 for suite_name, suite in self.test_suites.get('basic_suites', {}).items():
1306 for test_name, test in suite.items():
1307 yield test, f'test {test_name} in basic suite {suite_name}'
1308
1309 for mixin_name, mixin in self.mixins.items():
1310 yield mixin, f'mixin {mixin_name}'
1311
1312 for waterfall in self.waterfalls:
1313 for builder_name, builder in waterfall.get('machines', {}).items():
1314 yield (
1315 builder,
1316 f'builder {builder_name} in waterfall {waterfall["name"]}',
1317 )
1318
1319 for test_name, exceptions in self.exceptions.items():
1320 modifications = exceptions.get('modifications', {})
1321 for builder_name, mods in modifications.items():
1322 yield (
1323 mods,
1324 f'exception for test {test_name} on builder {builder_name}',
1325 )
1326
1327 for definition, location in definitions():
1328 for swarming_attr in (
1329 'swarming',
1330 'android_swarming',
1331 'chromeos_swarming',
1332 ):
1333 if (swarming :=
1334 definition.get(swarming_attr)) and 'dimension_sets' in swarming:
Garrett Beatyade673d2023-08-04 22:00:251335 raise BBGenErr(
1336 f'dimension_sets is no longer supported (set in {location}),'
1337 ' instead, use set dimensions to a single dict')
Garrett Beaty65d44222023-08-01 17:22:111338
Nico Weberd18b8962018-05-16 19:39:381339 def unknown_bot(self, bot_name, waterfall_name):
1340 return BBGenErr(
1341 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1342
Kenneth Russelleb60cbd22017-12-05 07:54:281343 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1344 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381345 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281346 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1347
1348 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1349 return BBGenErr(
1350 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1351 ' on waterfall ' + waterfall_name)
1352
Garrett Beatye3a606ceb2024-04-30 22:13:131353 def ensure_valid_mixin_list(self, mixins, location):
1354 if not isinstance(mixins, list):
1355 raise BBGenErr(
1356 f"got '{mixins}', should be a list of mixin names: {location}")
1357 for mixin in mixins:
1358 if not mixin in self.mixins:
1359 raise BBGenErr(f'bad mixin {mixin}: {location}')
Stephen Martinisb6a50492018-09-12 23:59:321360
Garrett Beatye3a606ceb2024-04-30 22:13:131361 def apply_mixins(self, test, mixins, mixins_to_ignore, builder=None):
1362 for mixin in mixins:
1363 if mixin not in mixins_to_ignore:
Austin Eng148d9f0f2022-02-08 19:18:531364 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinis0382bc12018-09-17 22:29:071365 return test
Stephen Martinisb6a50492018-09-12 23:59:321366
Garrett Beaty8d6708c2023-07-20 17:20:411367 def apply_mixin(self, mixin, test, builder=None):
Stephen Martinisb72f6d22018-10-04 23:29:011368 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321369
Garrett Beaty4c35b142023-06-23 21:01:231370 A mixin is applied by copying all fields from the mixin into the
1371 test with the following exceptions:
1372 * For the various *args keys, the test's existing value (an empty
1373 list if not present) will be extended with the mixin's value.
1374 * The sub-keys of the swarming value will be copied to the test's
1375 swarming value with the following exceptions:
Garrett Beatyade673d2023-08-04 22:00:251376 * For the named_caches sub-keys, the test's existing value (an
1377 empty list if not present) will be extended with the mixin's
1378 value.
1379 * For the dimensions sub-key, the tests's existing value (an empty
1380 dict if not present) will be updated with the mixin's value.
Stephen Martinisb6a50492018-09-12 23:59:321381 """
Garrett Beaty4c35b142023-06-23 21:01:231382
Stephen Martinisb6a50492018-09-12 23:59:321383 new_test = copy.deepcopy(test)
1384 mixin = copy.deepcopy(mixin)
Garrett Beaty8d6708c2023-07-20 17:20:411385
1386 if 'description' in mixin:
1387 description = []
1388 if 'description' in new_test:
1389 description.append(new_test['description'])
1390 description.append(mixin.pop('description'))
1391 new_test['description'] = '\n'.join(description)
1392
Stephen Martinisb72f6d22018-10-04 23:29:011393 if 'swarming' in mixin:
Garrett Beaty1b271622024-10-01 22:30:251394 self.merge_swarming(new_test.setdefault('swarming', {}),
1395 mixin.pop('swarming'))
Stephen Martinisb72f6d22018-10-04 23:29:011396
Garrett Beatye3a606ceb2024-04-30 22:13:131397 for a in ('args', 'precommit_args', 'non_precommit_args'):
Garrett Beaty4c35b142023-06-23 21:01:231398 if (value := mixin.pop(a, None)) is None:
1399 continue
1400 if not isinstance(value, list):
1401 raise BBGenErr(f'"{a}" must be a list')
1402 new_test.setdefault(a, []).extend(value)
1403
Garrett Beatye3a606ceb2024-04-30 22:13:131404 # At this point, all keys that require merging are taken care of, so the
1405 # remaining entries can be copied over. The os-conditional entries will be
1406 # resolved immediately after and they are resolved before any mixins are
1407 # applied, so there's are no concerns about overwriting the corresponding
1408 # entry in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011409 new_test.update(mixin)
Garrett Beatye3a606ceb2024-04-30 22:13:131410 if builder:
1411 self.resolve_os_conditional_values(new_test, builder)
1412
1413 if 'args' in new_test:
1414 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1415
Stephen Martinisb6a50492018-09-12 23:59:321416 return new_test
1417
Greg Gutermanf60eb052020-03-12 17:40:011418 def generate_output_tests(self, waterfall):
1419 """Generates the tests for a waterfall.
1420
1421 Args:
1422 waterfall: a dictionary parsed from a master pyl file
1423 Returns:
1424 A dictionary mapping builders to test specs
1425 """
1426 return {
Jamie Madillcf4f8c72021-05-20 19:24:231427 name: self.get_tests_for_config(waterfall, name, config)
1428 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011429 }
1430
1431 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531432 generator_map = self.get_test_generator_map()
1433 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281434
Greg Gutermanf60eb052020-03-12 17:40:011435 tests = {}
1436 # Copy only well-understood entries in the machine's configuration
1437 # verbatim into the generated JSON.
1438 if 'additional_compile_targets' in config:
1439 tests['additional_compile_targets'] = config[
1440 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231441 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011442 if test_type not in generator_map:
1443 raise self.unknown_test_suite_type(
1444 test_type, name, waterfall['name']) # pragma: no cover
1445 test_generator = generator_map[test_type]
1446 # Let multiple kinds of generators generate the same kinds
1447 # of tests. For example, gpu_telemetry_tests are a
1448 # specialization of isolated_scripts.
1449 new_tests = test_generator.generate(
1450 waterfall, name, config, input_tests)
1451 remapped_test_type = test_type_remapper.get(test_type, test_type)
Garrett Beatyffe83c4f2023-09-08 19:07:371452 tests.setdefault(remapped_test_type, []).extend(new_tests)
1453
1454 for test_type, tests_for_type in tests.items():
1455 if test_type == 'additional_compile_targets':
1456 continue
1457 tests[test_type] = sorted(tests_for_type, key=lambda t: t['name'])
Greg Gutermanf60eb052020-03-12 17:40:011458
1459 return tests
1460
1461 def jsonify(self, all_tests):
1462 return json.dumps(
1463 all_tests, indent=2, separators=(',', ': '),
1464 sort_keys=True) + '\n'
1465
1466 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281467 self.load_configuration_files()
1468 self.resolve_configuration_files()
1469 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011470 result = collections.defaultdict(dict)
1471
Stephanie Kim572b43c02023-04-13 14:24:131472 if os.path.exists(self.args.autoshard_exceptions_json_path):
1473 autoshards = json.loads(
1474 self.read_file(self.args.autoshard_exceptions_json_path))
1475 else:
1476 autoshards = {}
1477
Dirk Pranke6269d302020-10-01 00:14:391478 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011479 for waterfall in self.waterfalls:
1480 for field in required_fields:
1481 # Verify required fields
1482 if field not in waterfall:
Brian Sheedy0d2300f32024-08-13 23:14:411483 raise BBGenErr('Waterfall %s has no %s' % (waterfall['name'], field))
Greg Gutermanf60eb052020-03-12 17:40:011484
1485 # Handle filter flag, if specified
1486 if filters and waterfall['name'] not in filters:
1487 continue
1488
1489 # Join config files and hardcoded values together
1490 all_tests = self.generate_output_tests(waterfall)
1491 result[waterfall['name']] = all_tests
1492
Stephanie Kim572b43c02023-04-13 14:24:131493 if not autoshards:
1494 continue
1495 for builder, test_spec in all_tests.items():
1496 for target_type, test_list in test_spec.items():
1497 if target_type == 'additional_compile_targets':
1498 continue
1499 for test_dict in test_list:
1500 # Suites that apply variants or other customizations will create
1501 # test_dicts that have "name" value that is different from the
Garrett Beatyffe83c4f2023-09-08 19:07:371502 # "test" value.
Stephanie Kim572b43c02023-04-13 14:24:131503 # e.g. name = vulkan_swiftshader_content_browsertests, but
1504 # test = content_browsertests and
1505 # test_id_prefix = "ninja://content/test:content_browsertests/"
Garrett Beatyffe83c4f2023-09-08 19:07:371506 test_name = test_dict['name']
Stephanie Kim572b43c02023-04-13 14:24:131507 shard_info = autoshards.get(waterfall['name'],
1508 {}).get(builder, {}).get(test_name)
1509 if shard_info:
1510 test_dict['swarming'].update(
1511 {'shards': int(shard_info['shards'])})
1512
Greg Gutermanf60eb052020-03-12 17:40:011513 # Add do not edit warning
1514 for tests in result.values():
1515 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1516 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1517
1518 return result
1519
1520 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281521 suffix = '.json'
1522 if self.args.new_files:
1523 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011524
1525 for filename, contents in result.items():
1526 jsonstr = self.jsonify(contents)
Garrett Beaty79339e182023-04-10 20:45:471527 file_path = os.path.join(self.args.output_dir, filename + suffix)
1528 self.write_file(file_path, jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281529
Nico Weberd18b8962018-05-16 19:39:381530 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161531 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421532 # NOTE: This reference can cause issues; if a file changes there, the
1533 # presubmit here won't be run by default. A manually maintained list there
1534 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1535 # references to configs outside of this directory are added, please change
1536 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1537 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491538
Garrett Beaty7e866fc2021-06-16 14:12:101539 # Get the generated project.pyl so we can check if we should be enforcing
1540 # that the specs are for builders that actually exist
1541 # If not, return None to indicate that we won't enforce that builders in
1542 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491543 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1544 'project.pyl')
1545 if os.path.exists(project_pyl_path):
1546 settings = ast.literal_eval(self.read_file(project_pyl_path))
1547 if not settings.get('validate_source_side_specs_have_builder', True):
1548 return None
1549
Nico Weberd18b8962018-05-16 19:39:381550 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311551 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161552 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1553 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431554 for c in milo_configs:
1555 for l in self.read_file(c).splitlines():
1556 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311557 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431558 continue
1559 # l looks like
1560 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1561 # Extract win_chromium_dbg_ng part.
1562 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381563 return bot_names
1564
Ben Pastene9a010082019-09-25 20:41:371565 def get_internal_waterfalls(self):
1566 # Similar to get_builders_that_do_not_actually_exist above, but for
1567 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201568 return [
Kramer Ge3bf853a2023-04-13 19:39:471569 'chrome', 'chrome.pgo', 'chrome.gpu.fyi', 'internal.chrome.fyi',
yoshiki iguchi4de608082024-03-14 00:33:361570 'internal.chromeos.fyi', 'internal.optimization_guide', 'internal.soda',
1571 'chromeos.preuprev'
Yuke Liaoe6c23dd2021-07-28 16:12:201572 ]
Ben Pastene9a010082019-09-25 20:41:371573
Stephen Martinisf83893722018-09-19 00:02:181574 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201575 self.check_input_files_sorting(verbose)
1576
Kenneth Russelleb60cbd22017-12-05 07:54:281577 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011578 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381579 self.check_composition_type_test_suites('matrix_compound_suites',
1580 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111581 self.resolve_test_id_prefixes()
Garrett Beaty1ead4a52023-12-07 19:16:421582
1583 # All test suites must be referenced. Check this before flattening the test
1584 # suites so that we can transitively check the basic suites for compound
1585 # suites and matrix compound suites (otherwise we would determine a basic
1586 # suite is used if it shared a name with a test present in a basic suite
1587 # that is used).
1588 all_suites = set(
1589 itertools.chain(*(self.test_suites.get(a, {}) for a in (
1590 'basic_suites',
1591 'compound_suites',
1592 'matrix_compound_suites',
1593 ))))
1594 unused_suites = set(all_suites)
1595 generator_map = self.get_test_generator_map()
1596 for waterfall in self.waterfalls:
1597 for bot_name, tester in waterfall['machines'].items():
1598 for suite_type, suite in tester.get('test_suites', {}).items():
1599 if suite_type not in generator_map:
1600 raise self.unknown_test_suite_type(suite_type, bot_name,
1601 waterfall['name'])
1602 if suite not in all_suites:
1603 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1604 unused_suites.discard(suite)
1605 # For each compound suite or matrix compound suite, if the suite was used,
1606 # remove all of the basic suites that it composes from the set of unused
1607 # suites
1608 for a in ('compound_suites', 'matrix_compound_suites'):
1609 for suite, sub_suites in self.test_suites.get(a, {}).items():
1610 if suite not in unused_suites:
1611 unused_suites.difference_update(sub_suites)
1612 if unused_suites:
1613 raise BBGenErr('The following test suites were unreferenced by bots on '
1614 'the waterfalls: ' + str(unused_suites))
1615
Stephen Martinis54d64ad2018-09-21 22:16:201616 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381617
1618 # All bots should exist.
1619 bot_names = self.get_valid_bot_names()
Garrett Beaty2a02de3c2020-05-15 13:57:351620 if bot_names is not None:
1621 internal_waterfalls = self.get_internal_waterfalls()
1622 for waterfall in self.waterfalls:
Alison Gale923a33e2024-04-22 23:34:281623 # TODO(crbug.com/41474799): Remove the need for this exception.
Garrett Beaty2a02de3c2020-05-15 13:57:351624 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011625 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351626 for bot_name in waterfall['machines']:
Garrett Beaty2a02de3c2020-05-15 13:57:351627 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581628 if waterfall['name'] in [
1629 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1630 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351631 # TODO(thakis): Remove this once these bots move to luci.
1632 continue # pragma: no cover
1633 if waterfall['name'] in ['tryserver.webrtc',
1634 'webrtc.chromium.fyi.experimental']:
1635 # These waterfalls have their bot configs in a different repo.
1636 # so we don't know about their bot names.
1637 continue # pragma: no cover
1638 if waterfall['name'] in ['client.devtools-frontend.integration',
1639 'tryserver.devtools-frontend',
1640 'chromium.devtools-frontend']:
1641 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201642 if waterfall['name'] in ['client.openscreen.chromium']:
1643 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351644 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381645
Kenneth Russelleb60cbd22017-12-05 07:54:281646 # All test suite exceptions must refer to bots on the waterfall.
1647 all_bots = set()
1648 missing_bots = set()
1649 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231650 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281651 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281652 # In order to disambiguate between bots with the same name on
1653 # different waterfalls, support has been added to various
1654 # exceptions for concatenating the waterfall name after the bot
1655 # name.
1656 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231657 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381658 removals = (exception.get('remove_from', []) +
1659 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231660 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381661 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281662 if removal not in all_bots:
1663 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411664
Kenneth Russelleb60cbd22017-12-05 07:54:281665 if missing_bots:
1666 raise BBGenErr('The following nonexistent machines were referenced in '
1667 'the test suite exceptions: ' + str(missing_bots))
1668
Garrett Beatyb061e69d2023-06-27 16:15:351669 for name, mixin in self.mixins.items():
1670 if '$mixin_append' in mixin:
1671 raise BBGenErr(
1672 f'$mixin_append is no longer supported (set in mixin "{name}"),'
1673 ' args and named caches specified as normal will be appended')
1674
Stephen Martinis0382bc12018-09-17 22:29:071675 # All mixins must be referenced
1676 seen_mixins = set()
1677 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011678 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231679 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011680 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071681 for suite in self.test_suites.values():
1682 if isinstance(suite, list):
1683 # Don't care about this, it's a composition, which shouldn't include a
1684 # swarming mixin.
1685 continue
1686
1687 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561688 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011689 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Garrett Beaty4b9f1752024-09-26 20:02:501690 seen_mixins = seen_mixins.union(
1691 test.get('test_common', {}).get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071692
Zhaoyang Li9da047d52021-05-10 21:31:441693 for variant in self.variants:
1694 # Unpack the variant from variants.pyl if it's string based.
1695 if isinstance(variant, str):
1696 variant = self.variants[variant]
1697 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1698
Garrett Beaty086b3402024-09-25 23:45:341699 missing_mixins = set()
1700 for name, mixin_value in self.mixins.items():
1701 if name not in seen_mixins and mixin_value.get('fail_if_unused', True):
1702 missing_mixins.add(name)
Stephen Martinis0382bc12018-09-17 22:29:071703 if missing_mixins:
1704 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1705 ' referenced in a waterfall, machine, or test suite.' % (
1706 str(missing_mixins)))
1707
Jeff Yoonda581c32020-03-06 03:56:051708 # All variant references must be referenced
1709 seen_variants = set()
1710 for suite in self.test_suites.values():
1711 if isinstance(suite, list):
1712 continue
1713
1714 for test in suite.values():
1715 if isinstance(test, dict):
1716 for variant in test.get('variants', []):
1717 if isinstance(variant, str):
1718 seen_variants.add(variant)
1719
1720 missing_variants = set(self.variants.keys()) - seen_variants
1721 if missing_variants:
1722 raise BBGenErr('The following variants were unreferenced: %s. They must '
1723 'be referenced in a matrix test suite under the variants '
1724 'key.' % str(missing_variants))
1725
Stephen Martinis54d64ad2018-09-21 22:16:201726
Garrett Beaty79339e182023-04-10 20:45:471727 def type_assert(self, node, typ, file_path, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201728 """Asserts that the Python AST node |node| is of type |typ|.
1729
1730 If verbose is set, it prints out some helpful context lines, showing where
1731 exactly the error occurred in the file.
1732 """
1733 if not isinstance(node, typ):
1734 if verbose:
Brian Sheedy0d2300f32024-08-13 23:14:411735 lines = [''] + self.read_file(file_path).splitlines()
Stephen Martinis54d64ad2018-09-21 22:16:201736
1737 context = 2
1738 lines_start = max(node.lineno - context, 0)
1739 # Add one to include the last line
1740 lines_end = min(node.lineno + context, len(lines)) + 1
Garrett Beaty79339e182023-04-10 20:45:471741 lines = itertools.chain(
1742 ['== %s ==\n' % file_path],
Brian Sheedy0d2300f32024-08-13 23:14:411743 ['<snip>\n'],
Garrett Beaty79339e182023-04-10 20:45:471744 [
1745 '%d %s' % (lines_start + i, line)
1746 for i, line in enumerate(lines[lines_start:lines_start +
1747 context])
1748 ],
1749 ['-' * 80 + '\n'],
1750 ['%d %s' % (node.lineno, lines[node.lineno])],
1751 [
1752 '-' * (node.col_offset + 3) + '^' + '-' *
1753 (80 - node.col_offset - 4) + '\n'
1754 ],
1755 [
1756 '%d %s' % (node.lineno + 1 + i, line)
1757 for i, line in enumerate(lines[node.lineno + 1:lines_end])
1758 ],
Brian Sheedy0d2300f32024-08-13 23:14:411759 ['<snip>\n'],
Stephen Martinis54d64ad2018-09-21 22:16:201760 )
1761 # Print out a useful message when a type assertion fails.
1762 for l in lines:
1763 self.print_line(l.strip())
1764
1765 node_dumped = ast.dump(node, annotate_fields=False)
1766 # If the node is huge, truncate it so everything fits in a terminal
1767 # window.
1768 if len(node_dumped) > 60: # pragma: no cover
1769 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1770 raise BBGenErr(
Brian Sheedy0d2300f32024-08-13 23:14:411771 "Invalid .pyl file '%s'. Python AST node %r on line %s expected to"
Garrett Beaty79339e182023-04-10 20:45:471772 ' be %s, is %s' %
1773 (file_path, node_dumped, node.lineno, typ, type(node)))
Stephen Martinis54d64ad2018-09-21 22:16:201774
Garrett Beaty79339e182023-04-10 20:45:471775 def check_ast_list_formatted(self,
1776 keys,
1777 file_path,
1778 verbose,
Stephen Martinis1384ff92020-01-07 19:52:151779 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531780 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201781
Stephen Martinis5bef0fc2020-01-06 22:47:531782 Currently only checks to ensure they're correctly sorted, and that there
1783 are no duplicates.
1784
1785 Args:
1786 keys: An python list of AST nodes.
1787
1788 It's a list of AST nodes instead of a list of strings because
1789 when verbose is set, it tries to print out context of where the
1790 diffs are in the file.
Garrett Beaty79339e182023-04-10 20:45:471791 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531792 verbose: If set, print out diff information about how the keys are
1793 incorrectly formatted.
1794 check_sorting: If true, checks if the list is sorted.
1795 Returns:
1796 If the keys are correctly formatted.
1797 """
1798 if not keys:
1799 return True
1800
1801 assert isinstance(keys[0], ast.Str)
1802
1803 keys_strs = [k.s for k in keys]
1804 # Keys to diff against. Used below.
1805 keys_to_diff_against = None
1806 # If the list is properly formatted.
1807 list_formatted = True
1808
1809 # Duplicates are always bad.
1810 if len(set(keys_strs)) != len(keys_strs):
1811 list_formatted = False
1812 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1813
1814 if check_sorting and sorted(keys_strs) != keys_strs:
1815 list_formatted = False
1816 if list_formatted:
1817 return True
1818
1819 if verbose:
1820 line_num = keys[0].lineno
1821 keys = [k.s for k in keys]
1822 if check_sorting:
1823 # If we have duplicates, sorting this will take care of it anyways.
1824 keys_to_diff_against = sorted(set(keys))
1825 # else, keys_to_diff_against is set above already
1826
1827 self.print_line('=' * 80)
1828 self.print_line('(First line of keys is %s)' % line_num)
Garrett Beaty79339e182023-04-10 20:45:471829 for line in difflib.context_diff(keys,
1830 keys_to_diff_against,
1831 fromfile='current (%r)' % file_path,
1832 tofile='sorted',
1833 lineterm=''):
Stephen Martinis5bef0fc2020-01-06 22:47:531834 self.print_line(line)
1835 self.print_line('=' * 80)
1836
1837 return False
1838
Garrett Beaty79339e182023-04-10 20:45:471839 def check_ast_dict_formatted(self, node, file_path, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531840 """Checks if an ast dictionary's keys are correctly formatted.
1841
1842 Just a simple wrapper around check_ast_list_formatted.
1843 Args:
1844 node: An AST node. Assumed to be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471845 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531846 verbose: If set, print out diff information about how the keys are
1847 incorrectly formatted.
1848 check_sorting: If true, checks if the list is sorted.
1849 Returns:
1850 If the dictionary is correctly formatted.
1851 """
Stephen Martinis54d64ad2018-09-21 22:16:201852 keys = []
1853 # The keys of this dict are ordered as ordered in the file; normal python
1854 # dictionary keys are given an arbitrary order, but since we parsed the
1855 # file itself, the order as given in the file is preserved.
1856 for key in node.keys:
Garrett Beaty79339e182023-04-10 20:45:471857 self.type_assert(key, ast.Str, file_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531858 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201859
Garrett Beaty79339e182023-04-10 20:45:471860 return self.check_ast_list_formatted(keys, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181861
1862 def check_input_files_sorting(self, verbose=False):
Alison Gale923a33e2024-04-22 23:34:281863 # TODO(crbug.com/41415841): Add the ability for this script to
Stephen Martinis54d64ad2018-09-21 22:16:201864 # actually format the files, rather than just complain if they're
1865 # incorrectly formatted.
1866 bad_files = set()
Garrett Beaty79339e182023-04-10 20:45:471867
1868 def parse_file(file_path):
Stephen Martinis5bef0fc2020-01-06 22:47:531869 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201870
Stephen Martinis5bef0fc2020-01-06 22:47:531871 Returns an AST node representing the value in the pyl file."""
Garrett Beaty79339e182023-04-10 20:45:471872 parsed = ast.parse(self.read_file(file_path))
Stephen Martinisf83893722018-09-19 00:02:181873
Stephen Martinisf83893722018-09-19 00:02:181874 # Must be a module.
Garrett Beaty79339e182023-04-10 20:45:471875 self.type_assert(parsed, ast.Module, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181876 module = parsed.body
1877
1878 # Only one expression in the module.
Garrett Beaty79339e182023-04-10 20:45:471879 self.type_assert(module, list, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181880 if len(module) != 1: # pragma: no cover
Garrett Beaty79339e182023-04-10 20:45:471881 raise BBGenErr('Invalid .pyl file %s' % file_path)
Stephen Martinisf83893722018-09-19 00:02:181882 expr = module[0]
Garrett Beaty79339e182023-04-10 20:45:471883 self.type_assert(expr, ast.Expr, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181884
Stephen Martinis5bef0fc2020-01-06 22:47:531885 return expr.value
1886
1887 # Handle this separately
Garrett Beaty79339e182023-04-10 20:45:471888 value = parse_file(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531889 # Value should be a list.
Garrett Beaty79339e182023-04-10 20:45:471890 self.type_assert(value, ast.List, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531891
1892 keys = []
Joshua Hood56c673c2022-03-02 20:29:331893 for elm in value.elts:
Garrett Beaty79339e182023-04-10 20:45:471894 self.type_assert(elm, ast.Dict, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531895 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331896 for key, val in zip(elm.keys, elm.values):
Garrett Beaty79339e182023-04-10 20:45:471897 self.type_assert(key, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531898 if key.s == 'machines':
Garrett Beaty79339e182023-04-10 20:45:471899 if not self.check_ast_dict_formatted(
1900 val, self.args.waterfalls_pyl_path, verbose):
1901 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531902
Brian Sheedy0d2300f32024-08-13 23:14:411903 if key.s == 'name':
Garrett Beaty79339e182023-04-10 20:45:471904 self.type_assert(val, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531905 waterfall_name = val
1906 assert waterfall_name
1907 keys.append(waterfall_name)
1908
Garrett Beaty79339e182023-04-10 20:45:471909 if not self.check_ast_list_formatted(keys, self.args.waterfalls_pyl_path,
1910 verbose):
1911 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531912
Garrett Beaty79339e182023-04-10 20:45:471913 for file_path in (
1914 self.args.mixins_pyl_path,
1915 self.args.test_suites_pyl_path,
1916 self.args.test_suite_exceptions_pyl_path,
Stephen Martinis5bef0fc2020-01-06 22:47:531917 ):
Garrett Beaty79339e182023-04-10 20:45:471918 value = parse_file(file_path)
Stephen Martinisf83893722018-09-19 00:02:181919 # Value should be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471920 self.type_assert(value, ast.Dict, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181921
Garrett Beaty79339e182023-04-10 20:45:471922 if not self.check_ast_dict_formatted(value, file_path, verbose):
1923 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531924
Garrett Beaty79339e182023-04-10 20:45:471925 if file_path == self.args.test_suites_pyl_path:
Jeff Yoon8154e582019-12-03 23:30:011926 expected_keys = ['basic_suites',
1927 'compound_suites',
1928 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201929 actual_keys = [node.s for node in value.keys]
1930 assert all(key in expected_keys for key in actual_keys), (
Garrett Beaty79339e182023-04-10 20:45:471931 'Invalid %r file; expected keys %r, got %r' %
1932 (file_path, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:331933 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:201934 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011935 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201936 for suite_group in suite_dicts:
Garrett Beaty79339e182023-04-10 20:45:471937 if not self.check_ast_dict_formatted(suite_group, file_path, verbose):
1938 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181939
Stephen Martinis5bef0fc2020-01-06 22:47:531940 for key, suite in zip(value.keys, value.values):
1941 # The compound suites are checked in
1942 # 'check_composition_type_test_suites()'
1943 if key.s == 'basic_suites':
1944 for group in suite.values:
Garrett Beaty79339e182023-04-10 20:45:471945 if not self.check_ast_dict_formatted(group, file_path, verbose):
1946 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531947 break
Stephen Martinis54d64ad2018-09-21 22:16:201948
Garrett Beaty79339e182023-04-10 20:45:471949 elif file_path == self.args.test_suite_exceptions_pyl_path:
Stephen Martinis5bef0fc2020-01-06 22:47:531950 # Check the values for each test.
1951 for test in value.values:
1952 for kind, node in zip(test.keys, test.values):
1953 if isinstance(node, ast.Dict):
Garrett Beaty79339e182023-04-10 20:45:471954 if not self.check_ast_dict_formatted(node, file_path, verbose):
1955 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531956 elif kind.s == 'remove_from':
1957 # Don't care about sorting; these are usually grouped, since the
1958 # same bug can affect multiple builders. Do want to make sure
1959 # there aren't duplicates.
Garrett Beaty79339e182023-04-10 20:45:471960 if not self.check_ast_list_formatted(
1961 node.elts, file_path, verbose, check_sorting=False):
1962 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181963
1964 if bad_files:
1965 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201966 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531967 'unsorted, or have duplicates. Re-run this with --verbose to see '
1968 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181969
Kenneth Russelleb60cbd22017-12-05 07:54:281970 def check_output_file_consistency(self, verbose=False):
1971 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011972 # All waterfalls/bucket .json files must have been written
1973 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:281974 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011975 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:161976 outputs = self.generate_outputs()
1977 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:011978 expected = self.jsonify(expected_contents)
Garrett Beaty79339e182023-04-10 20:45:471979 file_path = os.path.join(self.args.output_dir, filename + '.json')
Ben Pastenef21cda32023-03-30 22:00:571980 current = self.read_file(file_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281981 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:011982 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:321983 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:011984 self.print_line('File ' + filename +
1985 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321986 'contents:')
1987 for line in difflib.unified_diff(
1988 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501989 current.splitlines(),
1990 fromfile='expected', tofile='current'):
1991 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:011992
1993 if ungenerated_files:
1994 raise BBGenErr(
1995 'The following files have not been properly '
1996 'autogenerated by generate_buildbot_json.py: ' +
1997 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:281998
Dirk Pranke772f55f2021-04-28 04:51:161999 for builder_group, builders in outputs.items():
2000 for builder, step_types in builders.items():
Garrett Beatydca3d882023-09-14 23:50:322001 for test_type in ('gtest_tests', 'isolated_scripts'):
2002 for step_data in step_types.get(test_type, []):
2003 step_name = step_data['name']
2004 self._check_swarming_config(builder_group, builder, step_name,
2005 step_data)
Dirk Pranke772f55f2021-04-28 04:51:162006
2007 def _check_swarming_config(self, filename, builder, step_name, step_data):
Alison Gale47d1537d2024-04-19 21:31:462008 # TODO(crbug.com/40179524): Ensure all swarming tests specify cpu, not
Dirk Pranke772f55f2021-04-28 04:51:162009 # just mac tests.
Garrett Beatybb18d532023-06-26 22:16:332010 if 'swarming' in step_data:
Garrett Beatyade673d2023-08-04 22:00:252011 dimensions = step_data['swarming'].get('dimensions')
2012 if not dimensions:
Tatsuhisa Yamaguchif1878d52023-11-06 06:02:252013 raise BBGenErr('%s: %s / %s : dimensions must be specified for all '
Dirk Pranke772f55f2021-04-28 04:51:162014 'swarmed tests' % (filename, builder, step_name))
Garrett Beatyade673d2023-08-04 22:00:252015 if not dimensions.get('os'):
2016 raise BBGenErr('%s: %s / %s : os must be specified for all '
2017 'swarmed tests' % (filename, builder, step_name))
2018 if 'Mac' in dimensions.get('os') and not dimensions.get('cpu'):
2019 raise BBGenErr('%s: %s / %s : cpu must be specified for mac '
2020 'swarmed tests' % (filename, builder, step_name))
Dirk Pranke772f55f2021-04-28 04:51:162021
Kenneth Russelleb60cbd22017-12-05 07:54:282022 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502023 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282024 self.check_output_file_consistency(verbose) # pragma: no cover
2025
Karen Qiane24b7ee2019-02-12 23:37:062026 def does_test_match(self, test_info, params_dict):
2027 """Checks to see if the test matches the parameters given.
2028
2029 Compares the provided test_info with the params_dict to see
2030 if the bot matches the parameters given. If so, returns True.
2031 Else, returns false.
2032
2033 Args:
2034 test_info (dict): Information about a specific bot provided
2035 in the format shown in waterfalls.pyl
2036 params_dict (dict): Dictionary of parameters and their values
2037 to look for in the bot
2038 Ex: {
2039 'device_os':'android',
2040 '--flag':True,
2041 'mixins': ['mixin1', 'mixin2'],
2042 'ex_key':'ex_value'
2043 }
2044
2045 """
2046 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2047 'kvm', 'pool', 'integrity'] # dimension parameters
2048 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2049 'can_use_on_swarming_builders']
2050 for param in params_dict:
2051 # if dimension parameter
2052 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2053 if not 'swarming' in test_info:
2054 return False
2055 swarming = test_info['swarming']
2056 if param in SWARMING_PARAMS:
2057 if not param in swarming:
2058 return False
2059 if not str(swarming[param]) == params_dict[param]:
2060 return False
2061 else:
Garrett Beatyade673d2023-08-04 22:00:252062 if not 'dimensions' in swarming:
Karen Qiane24b7ee2019-02-12 23:37:062063 return False
Garrett Beatyade673d2023-08-04 22:00:252064 dimensions = swarming['dimensions']
Karen Qiane24b7ee2019-02-12 23:37:062065 # only looking at the first dimension set
Garrett Beatyade673d2023-08-04 22:00:252066 if not param in dimensions:
Karen Qiane24b7ee2019-02-12 23:37:062067 return False
Garrett Beatyade673d2023-08-04 22:00:252068 if not dimensions[param] == params_dict[param]:
Karen Qiane24b7ee2019-02-12 23:37:062069 return False
2070
2071 # if flag
2072 elif param.startswith('--'):
2073 if not 'args' in test_info:
2074 return False
2075 if not param in test_info['args']:
2076 return False
2077
2078 # not dimension parameter/flag/mixin
2079 else:
2080 if not param in test_info:
2081 return False
2082 if not test_info[param] == params_dict[param]:
2083 return False
2084 return True
2085 def error_msg(self, msg):
2086 """Prints an error message.
2087
2088 In addition to a catered error message, also prints
2089 out where the user can find more help. Then, program exits.
2090 """
2091 self.print_line(msg + (' If you need more information, ' +
2092 'please run with -h or --help to see valid commands.'))
2093 sys.exit(1)
2094
2095 def find_bots_that_run_test(self, test, bots):
2096 matching_bots = []
2097 for bot in bots:
2098 bot_info = bots[bot]
2099 tests = self.flatten_tests_for_bot(bot_info)
2100 for test_info in tests:
Garrett Beatyffe83c4f2023-09-08 19:07:372101 test_name = test_info['name']
Karen Qiane24b7ee2019-02-12 23:37:062102 if not test_name == test:
2103 continue
2104 matching_bots.append(bot)
2105 return matching_bots
2106
2107 def find_tests_with_params(self, tests, params_dict):
2108 matching_tests = []
2109 for test_name in tests:
2110 test_info = tests[test_name]
2111 if not self.does_test_match(test_info, params_dict):
2112 continue
2113 if not test_name in matching_tests:
2114 matching_tests.append(test_name)
2115 return matching_tests
2116
2117 def flatten_waterfalls_for_query(self, waterfalls):
2118 bots = {}
2119 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012120 waterfall_tests = self.generate_output_tests(waterfall)
2121 for bot in waterfall_tests:
2122 bot_info = waterfall_tests[bot]
2123 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062124 return bots
2125
2126 def flatten_tests_for_bot(self, bot_info):
2127 """Returns a list of flattened tests.
2128
2129 Returns a list of tests not grouped by test category
2130 for a specific bot.
2131 """
2132 TEST_CATS = self.get_test_generator_map().keys()
2133 tests = []
2134 for test_cat in TEST_CATS:
2135 if not test_cat in bot_info:
2136 continue
2137 test_cat_tests = bot_info[test_cat]
2138 tests = tests + test_cat_tests
2139 return tests
2140
2141 def flatten_tests_for_query(self, test_suites):
2142 """Returns a flattened dictionary of tests.
2143
2144 Returns a dictionary of tests associate with their
2145 configuration, not grouped by their test suite.
2146 """
2147 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232148 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062149 for test in test_suite:
2150 test_info = test_suite[test]
2151 test_name = test
Karen Qiane24b7ee2019-02-12 23:37:062152 tests[test_name] = test_info
2153 return tests
2154
2155 def parse_query_filter_params(self, params):
2156 """Parses the filter parameters.
2157
2158 Creates a dictionary from the parameters provided
2159 to filter the bot array.
2160 """
2161 params_dict = {}
2162 for p in params:
2163 # flag
Brian Sheedy0d2300f32024-08-13 23:14:412164 if p.startswith('--'):
Karen Qiane24b7ee2019-02-12 23:37:062165 params_dict[p] = True
2166 else:
Brian Sheedy0d2300f32024-08-13 23:14:412167 pair = p.split(':')
Karen Qiane24b7ee2019-02-12 23:37:062168 if len(pair) != 2:
2169 self.error_msg('Invalid command.')
2170 # regular parameters
Brian Sheedy0d2300f32024-08-13 23:14:412171 if pair[1].lower() == 'true':
Karen Qiane24b7ee2019-02-12 23:37:062172 params_dict[pair[0]] = True
Brian Sheedy0d2300f32024-08-13 23:14:412173 elif pair[1].lower() == 'false':
Karen Qiane24b7ee2019-02-12 23:37:062174 params_dict[pair[0]] = False
2175 else:
2176 params_dict[pair[0]] = pair[1]
2177 return params_dict
2178
2179 def get_test_suites_dict(self, bots):
2180 """Returns a dictionary of bots and their tests.
2181
2182 Returns a dictionary of bots and a list of their associated tests.
2183 """
2184 test_suite_dict = dict()
2185 for bot in bots:
2186 bot_info = bots[bot]
2187 tests = self.flatten_tests_for_bot(bot_info)
2188 test_suite_dict[bot] = tests
2189 return test_suite_dict
2190
2191 def output_query_result(self, result, json_file=None):
2192 """Outputs the result of the query.
2193
2194 If a json file parameter name is provided, then
2195 the result is output into the json file. If not,
2196 then the result is printed to the console.
2197 """
2198 output = json.dumps(result, indent=2)
2199 if json_file:
2200 self.write_file(json_file, output)
2201 else:
2202 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062203
Joshua Hood56c673c2022-03-02 20:29:332204 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062205 def query(self, args):
2206 """Queries tests or bots.
2207
2208 Depending on the arguments provided, outputs a json of
2209 tests or bots matching the appropriate optional parameters provided.
2210 """
2211 # split up query statement
2212 query = args.query.split('/')
2213 self.load_configuration_files()
2214 self.resolve_configuration_files()
2215
2216 # flatten bots json
2217 tests = self.test_suites
2218 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2219
2220 cmd_class = query[0]
2221
2222 # For queries starting with 'bots'
Brian Sheedy0d2300f32024-08-13 23:14:412223 if cmd_class == 'bots':
Karen Qiane24b7ee2019-02-12 23:37:062224 if len(query) == 1:
2225 return self.output_query_result(bots, args.json)
2226 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332227 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062228 if query[1] == 'tests':
2229 test_suites_dict = self.get_test_suites_dict(bots)
2230 return self.output_query_result(test_suites_dict, args.json)
Brian Sheedy0d2300f32024-08-13 23:14:412231 self.error_msg('This query should be in the format: bots/tests.')
Karen Qiane24b7ee2019-02-12 23:37:062232
2233 else:
Brian Sheedy0d2300f32024-08-13 23:14:412234 self.error_msg('This query should have 0 or 1 "/"", found %s instead.' %
2235 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062236
2237 # For queries starting with 'bot'
Brian Sheedy0d2300f32024-08-13 23:14:412238 elif cmd_class == 'bot':
Karen Qiane24b7ee2019-02-12 23:37:062239 if not len(query) == 2 and not len(query) == 3:
Brian Sheedy0d2300f32024-08-13 23:14:412240 self.error_msg('Command should have 1 or 2 "/"", found %s instead.' %
2241 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062242 bot_id = query[1]
2243 if not bot_id in bots:
Brian Sheedy0d2300f32024-08-13 23:14:412244 self.error_msg('No bot named "' + bot_id + '" found.')
Karen Qiane24b7ee2019-02-12 23:37:062245 bot_info = bots[bot_id]
2246 if len(query) == 2:
2247 return self.output_query_result(bot_info, args.json)
2248 if not query[2] == 'tests':
Brian Sheedy0d2300f32024-08-13 23:14:412249 self.error_msg('The query should be in the format:'
2250 'bot/<bot-name>/tests.')
Karen Qiane24b7ee2019-02-12 23:37:062251
2252 bot_tests = self.flatten_tests_for_bot(bot_info)
2253 return self.output_query_result(bot_tests, args.json)
2254
2255 # For queries starting with 'tests'
Brian Sheedy0d2300f32024-08-13 23:14:412256 elif cmd_class == 'tests':
Karen Qiane24b7ee2019-02-12 23:37:062257 if not len(query) == 1 and not len(query) == 2:
Brian Sheedy0d2300f32024-08-13 23:14:412258 self.error_msg('The query should have 0 or 1 "/", found %s instead.' %
2259 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062260 flattened_tests = self.flatten_tests_for_query(tests)
2261 if len(query) == 1:
2262 return self.output_query_result(flattened_tests, args.json)
2263
2264 # create params dict
2265 params = query[1].split('&')
2266 params_dict = self.parse_query_filter_params(params)
2267 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2268 return self.output_query_result(matching_bots)
2269
2270 # For queries starting with 'test'
Brian Sheedy0d2300f32024-08-13 23:14:412271 elif cmd_class == 'test':
Karen Qiane24b7ee2019-02-12 23:37:062272 if not len(query) == 2 and not len(query) == 3:
Brian Sheedy0d2300f32024-08-13 23:14:412273 self.error_msg('The query should have 1 or 2 "/", found %s instead.' %
2274 str(len(query) - 1))
Karen Qiane24b7ee2019-02-12 23:37:062275 test_id = query[1]
2276 if len(query) == 2:
2277 flattened_tests = self.flatten_tests_for_query(tests)
2278 for test in flattened_tests:
2279 if test == test_id:
2280 return self.output_query_result(flattened_tests[test], args.json)
Brian Sheedy0d2300f32024-08-13 23:14:412281 self.error_msg('There is no test named %s.' % test_id)
Karen Qiane24b7ee2019-02-12 23:37:062282 if not query[2] == 'bots':
Brian Sheedy0d2300f32024-08-13 23:14:412283 self.error_msg('The query should be in the format: '
2284 'test/<test-name>/bots')
Karen Qiane24b7ee2019-02-12 23:37:062285 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2286 return self.output_query_result(bots_for_test)
2287
2288 else:
Brian Sheedy0d2300f32024-08-13 23:14:412289 self.error_msg('Your command did not match any valid commands. '
2290 'Try starting with "bots", "bot", "tests", or "test".')
2291
Joshua Hood56c673c2022-03-02 20:29:332292 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282293
Garrett Beaty1afaccc2020-06-25 19:58:152294 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282295 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502296 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062297 elif self.args.query:
2298 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282299 else:
Greg Gutermanf60eb052020-03-12 17:40:012300 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282301 return 0
2302
Brian Sheedy0d2300f32024-08-13 23:14:412303
2304if __name__ == '__main__': # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152305 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2306 sys.exit(generator.main())