blob: c17b53cc320fe650192fbbc729c934bc0e0e6c74 [file] [log] [blame]
Joshua Hood3455e1352022-03-03 23:23:591#!/usr/bin/env vpython3
Avi Drissmandfd880852022-09-15 20:11:092# Copyright 2016 The Chromium Authors
Kenneth Russelleb60cbd22017-12-05 07:54:283# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Script to generate the majority of the JSON files in the src/testing/buildbot
7directory. Maintaining these files by hand is too unwieldy.
8"""
9
10import argparse
11import ast
12import collections
13import copy
John Budorick826d5ed2017-12-28 19:27:3214import difflib
Garrett Beatyd5ca75962020-05-07 16:58:3115import glob
Kenneth Russell8ceeabf2017-12-11 17:53:2816import itertools
Kenneth Russelleb60cbd22017-12-05 07:54:2817import json
18import os
19import string
20import sys
21
Brian Sheedya31578e2020-05-18 20:24:3622import buildbot_json_magic_substitutions as magic_substitutions
23
Joshua Hood56c673c2022-03-02 20:29:3324# pylint: disable=super-with-arguments,useless-super-delegation
25
Kenneth Russelleb60cbd22017-12-05 07:54:2826THIS_DIR = os.path.dirname(os.path.abspath(__file__))
27
Brian Sheedyf74819b2021-06-04 01:38:3828BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP = {
29 'android-chromium': '_android_chrome',
30 'android-chromium-monochrome': '_android_monochrome',
Brian Sheedyf74819b2021-06-04 01:38:3831 'android-webview': '_android_webview',
32}
33
Kenneth Russelleb60cbd22017-12-05 07:54:2834
35class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4936 def __init__(self, message):
37 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2838
39
Joshua Hood56c673c2022-03-02 20:29:3340class BaseGenerator(object): # pylint: disable=useless-object-inheritance
Kenneth Russelleb60cbd22017-12-05 07:54:2841 def __init__(self, bb_gen):
42 self.bb_gen = bb_gen
43
Kenneth Russell8ceeabf2017-12-11 17:53:2844 def generate(self, waterfall, tester_name, tester_config, input_tests):
Garrett Beatyffe83c4f2023-09-08 19:07:3745 raise NotImplementedError() # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:2846
47
Kenneth Russell8a386d42018-06-02 09:48:0148class GPUTelemetryTestGenerator(BaseGenerator):
Xinan Linedcf05b32023-10-19 23:13:5049 def __init__(self,
50 bb_gen,
51 is_android_webview=False,
52 is_cast_streaming=False,
53 is_skylab=False):
Kenneth Russell8a386d42018-06-02 09:48:0154 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5655 self._is_android_webview = is_android_webview
Fabrice de Ganscbd655f2022-08-04 20:15:3056 self._is_cast_streaming = is_cast_streaming
Xinan Linedcf05b32023-10-19 23:13:5057 self._is_skylab = is_skylab
Kenneth Russell8a386d42018-06-02 09:48:0158
59 def generate(self, waterfall, tester_name, tester_config, input_tests):
60 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:2361 for test_name, test_config in sorted(input_tests.items()):
Ben Pastene8e7eb2652022-04-29 19:44:3162 # Variants allow more than one definition for a given test, and is defined
63 # in array format from resolve_variants().
64 if not isinstance(test_config, list):
65 test_config = [test_config]
66
67 for config in test_config:
Xinan Linedcf05b32023-10-19 23:13:5068 test = self.bb_gen.generate_gpu_telemetry_test(
69 waterfall, tester_name, tester_config, test_name, config,
70 self._is_android_webview, self._is_cast_streaming, self._is_skylab)
Ben Pastene8e7eb2652022-04-29 19:44:3171 if test:
72 isolated_scripts.append(test)
73
Kenneth Russell8a386d42018-06-02 09:48:0174 return isolated_scripts
75
Kenneth Russell8a386d42018-06-02 09:48:0176
Brian Sheedyb6491ba2022-09-26 20:49:4977class SkylabGPUTelemetryTestGenerator(GPUTelemetryTestGenerator):
Xinan Linedcf05b32023-10-19 23:13:5078 def __init__(self, bb_gen):
79 super(SkylabGPUTelemetryTestGenerator, self).__init__(bb_gen,
80 is_skylab=True)
81
Brian Sheedyb6491ba2022-09-26 20:49:4982 def generate(self, *args, **kwargs):
83 # This should be identical to a regular GPU Telemetry test, but with any
84 # swarming arguments removed.
85 isolated_scripts = super(SkylabGPUTelemetryTestGenerator,
86 self).generate(*args, **kwargs)
87 for test in isolated_scripts:
Xinan Lind9b1d2e72022-11-14 20:57:0288 # chromium_GPU is the Autotest wrapper created for browser GPU tests
89 # run in Skylab.
Xinan Lin1f28a0d2023-03-13 17:39:4190 test['autotest_name'] = 'chromium_Graphics'
Xinan Lind9b1d2e72022-11-14 20:57:0291 # As of 22Q4, Skylab tests are running on a CrOS flavored Autotest
92 # framework and it does not support the sub-args like
93 # extra-browser-args. So we have to pop it out and create a new
94 # key for it. See crrev.com/c/3965359 for details.
95 for idx, arg in enumerate(test.get('args', [])):
96 if '--extra-browser-args' in arg:
97 test['args'].pop(idx)
98 test['extra_browser_args'] = arg.replace('--extra-browser-args=', '')
99 break
Brian Sheedyb6491ba2022-09-26 20:49:49100 return isolated_scripts
101
102
Kenneth Russelleb60cbd22017-12-05 07:54:28103class GTestGenerator(BaseGenerator):
Kenneth Russell8ceeabf2017-12-11 17:53:28104 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28105 # The relative ordering of some of the tests is important to
106 # minimize differences compared to the handwritten JSON files, since
107 # Python's sorts are stable and there are some tests with the same
108 # key (see gles2_conform_d3d9_test and similar variants). Avoid
109 # losing the order by avoiding coalescing the dictionaries into one.
110 gtests = []
Jamie Madillcf4f8c72021-05-20 19:24:23111 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoon67c3e832020-02-08 07:39:38112 # Variants allow more than one definition for a given test, and is defined
113 # in array format from resolve_variants().
114 if not isinstance(test_config, list):
115 test_config = [test_config]
116
117 for config in test_config:
118 test = self.bb_gen.generate_gtest(
119 waterfall, tester_name, tester_config, test_name, config)
120 if test:
121 # generate_gtest may veto the test generation on this tester.
122 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28123 return gtests
124
Kenneth Russelleb60cbd22017-12-05 07:54:28125
126class IsolatedScriptTestGenerator(BaseGenerator):
Kenneth Russell8ceeabf2017-12-11 17:53:28127 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28128 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23129 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43130 # Variants allow more than one definition for a given test, and is defined
131 # in array format from resolve_variants().
132 if not isinstance(test_config, list):
133 test_config = [test_config]
134
135 for config in test_config:
136 test = self.bb_gen.generate_isolated_script_test(
137 waterfall, tester_name, tester_config, test_name, config)
138 if test:
139 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28140 return isolated_scripts
141
Kenneth Russelleb60cbd22017-12-05 07:54:28142
143class ScriptGenerator(BaseGenerator):
Kenneth Russell8ceeabf2017-12-11 17:53:28144 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28145 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23146 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28147 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28148 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28149 if test:
150 scripts.append(test)
151 return scripts
152
Kenneth Russelleb60cbd22017-12-05 07:54:28153
154class JUnitGenerator(BaseGenerator):
Kenneth Russell8ceeabf2017-12-11 17:53:28155 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28156 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23157 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28158 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28159 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28160 if test:
161 scripts.append(test)
162 return scripts
163
Kenneth Russelleb60cbd22017-12-05 07:54:28164
Xinan Lin05fb9c1752020-12-17 00:15:52165class SkylabGenerator(BaseGenerator):
Xinan Lin05fb9c1752020-12-17 00:15:52166 def generate(self, waterfall, tester_name, tester_config, input_tests):
167 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23168 for test_name, test_config in sorted(input_tests.items()):
Xinan Lin05fb9c1752020-12-17 00:15:52169 for config in test_config:
170 test = self.bb_gen.generate_skylab_test(waterfall, tester_name,
171 tester_config, test_name,
172 config)
173 if test:
174 scripts.append(test)
175 return scripts
176
Xinan Lin05fb9c1752020-12-17 00:15:52177
Jeff Yoon67c3e832020-02-08 07:39:38178def check_compound_references(other_test_suites=None,
179 sub_suite=None,
180 suite=None,
181 target_test_suites=None,
182 test_type=None,
183 **kwargs):
184 """Ensure comound reference's don't target other compounds"""
185 del kwargs
186 if sub_suite in other_test_suites or sub_suite in target_test_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15187 raise BBGenErr('%s may not refer to other composition type test '
188 'suites (error found while processing %s)' %
189 (test_type, suite))
190
Jeff Yoon67c3e832020-02-08 07:39:38191
192def check_basic_references(basic_suites=None,
193 sub_suite=None,
194 suite=None,
195 **kwargs):
196 """Ensure test has a basic suite reference"""
197 del kwargs
198 if sub_suite not in basic_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15199 raise BBGenErr('Unable to find reference to %s while processing %s' %
200 (sub_suite, suite))
201
Jeff Yoon67c3e832020-02-08 07:39:38202
203def check_conflicting_definitions(basic_suites=None,
204 seen_tests=None,
205 sub_suite=None,
206 suite=None,
207 test_type=None,
Garrett Beaty235c1412023-08-29 20:26:29208 target_test_suites=None,
Jeff Yoon67c3e832020-02-08 07:39:38209 **kwargs):
210 """Ensure that if a test is reachable via multiple basic suites,
211 all of them have an identical definition of the tests.
212 """
213 del kwargs
Garrett Beaty235c1412023-08-29 20:26:29214 variants = None
215 if test_type == 'matrix_compound_suites':
216 variants = target_test_suites[suite][sub_suite].get('variants')
217 variants = variants or [None]
Jeff Yoon67c3e832020-02-08 07:39:38218 for test_name in basic_suites[sub_suite]:
Garrett Beaty235c1412023-08-29 20:26:29219 for variant in variants:
220 key = (test_name, variant)
221 if ((seen_sub_suite := seen_tests.get(key)) is not None
222 and basic_suites[sub_suite][test_name] !=
223 basic_suites[seen_sub_suite][test_name]):
224 test_description = (test_name if variant is None else
225 f'{test_name} with variant {variant} applied')
226 raise BBGenErr(
227 'Conflicting test definitions for %s from %s '
228 'and %s in %s (error found while processing %s)' %
229 (test_description, seen_tests[key], sub_suite, test_type, suite))
230 seen_tests[key] = sub_suite
231
Jeff Yoon67c3e832020-02-08 07:39:38232
233def check_matrix_identifier(sub_suite=None,
234 suite=None,
235 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05236 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38237 **kwargs):
238 """Ensure 'idenfitier' is defined for each variant"""
239 del kwargs
240 sub_suite_config = suite_def[sub_suite]
Garrett Beaty2022db42023-08-29 17:22:40241 for variant_name in sub_suite_config.get('variants', []):
242 if variant_name not in all_variants:
243 raise BBGenErr('Missing variant definition for %s in variants.pyl' %
244 variant_name)
245 variant = all_variants[variant_name]
Jeff Yoonda581c32020-03-06 03:56:05246
Jeff Yoon67c3e832020-02-08 07:39:38247 if not 'identifier' in variant:
248 raise BBGenErr('Missing required identifier field in matrix '
249 'compound suite %s, %s' % (suite, sub_suite))
Sven Zhengef0d0872022-04-04 22:13:29250 if variant['identifier'] == '':
251 raise BBGenErr('Identifier field can not be "" in matrix '
252 'compound suite %s, %s' % (suite, sub_suite))
253 if variant['identifier'].strip() != variant['identifier']:
254 raise BBGenErr('Identifier field can not have leading and trailing '
255 'whitespace in matrix compound suite %s, %s' %
256 (suite, sub_suite))
Jeff Yoon67c3e832020-02-08 07:39:38257
258
Joshua Hood56c673c2022-03-02 20:29:33259class BBJSONGenerator(object): # pylint: disable=useless-object-inheritance
Garrett Beaty1afaccc2020-06-25 19:58:15260 def __init__(self, args):
Garrett Beaty1afaccc2020-06-25 19:58:15261 self.args = args
Kenneth Russelleb60cbd22017-12-05 07:54:28262 self.waterfalls = None
263 self.test_suites = None
264 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01265 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41266 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05267 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28268
Garrett Beaty1afaccc2020-06-25 19:58:15269 @staticmethod
270 def parse_args(argv):
271
272 # RawTextHelpFormatter allows for styling of help statement
273 parser = argparse.ArgumentParser(
274 formatter_class=argparse.RawTextHelpFormatter)
275
276 group = parser.add_mutually_exclusive_group()
277 group.add_argument(
278 '-c',
279 '--check',
280 action='store_true',
281 help=
282 'Do consistency checks of configuration and generated files and then '
283 'exit. Used during presubmit. '
284 'Causes the tool to not generate any files.')
285 group.add_argument(
286 '--query',
287 type=str,
288 help=(
289 "Returns raw JSON information of buildbots and tests.\n" +
290 "Examples:\n" + " List all bots (all info):\n" +
291 " --query bots\n\n" +
292 " List all bots and only their associated tests:\n" +
293 " --query bots/tests\n\n" +
294 " List all information about 'bot1' " +
295 "(make sure you have quotes):\n" + " --query bot/'bot1'\n\n" +
296 " List tests running for 'bot1' (make sure you have quotes):\n" +
297 " --query bot/'bot1'/tests\n\n" + " List all tests:\n" +
298 " --query tests\n\n" +
299 " List all tests and the bots running them:\n" +
300 " --query tests/bots\n\n" +
301 " List all tests that satisfy multiple parameters\n" +
302 " (separation of parameters by '&' symbol):\n" +
303 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
304 " List all tests that run with a specific flag:\n" +
305 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
306 " List specific test (make sure you have quotes):\n"
307 " --query test/'test1'\n\n"
308 " List all bots running 'test1' " +
309 "(make sure you have quotes):\n" + " --query test/'test1'/bots"))
310 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47311 '--json',
312 metavar='JSON_FILE_PATH',
313 type=os.path.abspath,
314 help='Outputs results into a json file. Only works with query function.'
315 )
316 parser.add_argument(
Garrett Beaty1afaccc2020-06-25 19:58:15317 '-n',
318 '--new-files',
319 action='store_true',
320 help=
321 'Write output files as .new.json. Useful during development so old and '
322 'new files can be looked at side-by-side.')
Garrett Beatyade673d2023-08-04 22:00:25323 parser.add_argument('--dimension-sets-handling',
324 choices=['disable'],
325 default='disable',
326 help=('This flag no longer has any effect:'
327 ' dimension_sets fields are not allowed'))
Garrett Beaty1afaccc2020-06-25 19:58:15328 parser.add_argument('-v',
329 '--verbose',
330 action='store_true',
331 help='Increases verbosity. Affects consistency checks.')
332 parser.add_argument('waterfall_filters',
333 metavar='waterfalls',
334 type=str,
335 nargs='*',
336 help='Optional list of waterfalls to generate.')
337 parser.add_argument(
338 '--pyl-files-dir',
Garrett Beaty79339e182023-04-10 20:45:47339 type=os.path.abspath,
340 help=('Path to the directory containing the input .pyl files.'
341 ' By default the directory containing this script will be used.'))
Garrett Beaty1afaccc2020-06-25 19:58:15342 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47343 '--output-dir',
344 type=os.path.abspath,
345 help=('Path to the directory to output generated .json files.'
346 'By default, the pyl files directory will be used.'))
Chong Guee622242020-10-28 18:17:35347 parser.add_argument('--isolate-map-file',
348 metavar='PATH',
349 help='path to additional isolate map files.',
Garrett Beaty79339e182023-04-10 20:45:47350 type=os.path.abspath,
Chong Guee622242020-10-28 18:17:35351 default=[],
352 action='append',
353 dest='isolate_map_files')
Garrett Beaty1afaccc2020-06-25 19:58:15354 parser.add_argument(
355 '--infra-config-dir',
356 help='Path to the LUCI services configuration directory',
Garrett Beaty79339e182023-04-10 20:45:47357 type=os.path.abspath,
358 default=os.path.join(os.path.dirname(__file__), '..', '..', 'infra',
359 'config'))
360
Garrett Beaty1afaccc2020-06-25 19:58:15361 args = parser.parse_args(argv)
362 if args.json and not args.query:
363 parser.error(
364 "The --json flag can only be used with --query.") # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:15365
Garrett Beaty79339e182023-04-10 20:45:47366 args.pyl_files_dir = args.pyl_files_dir or THIS_DIR
367 args.output_dir = args.output_dir or args.pyl_files_dir
368
Stephanie Kim572b43c02023-04-13 14:24:13369 def absolute_file_path(filename):
Garrett Beaty79339e182023-04-10 20:45:47370 return os.path.join(args.pyl_files_dir, filename)
371
Stephanie Kim572b43c02023-04-13 14:24:13372 args.waterfalls_pyl_path = absolute_file_path('waterfalls.pyl')
Garrett Beaty96802d02023-07-07 14:18:05373 args.mixins_pyl_path = absolute_file_path('mixins.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13374 args.test_suites_pyl_path = absolute_file_path('test_suites.pyl')
375 args.test_suite_exceptions_pyl_path = absolute_file_path(
Garrett Beaty79339e182023-04-10 20:45:47376 'test_suite_exceptions.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13377 args.gn_isolate_map_pyl_path = absolute_file_path('gn_isolate_map.pyl')
378 args.variants_pyl_path = absolute_file_path('variants.pyl')
Garrett Beaty4999e9792024-04-03 23:29:11379 args.autoshard_exceptions_json_path = os.path.join(
380 args.infra_config_dir, 'targets', 'autoshard_exceptions.json')
Garrett Beaty79339e182023-04-10 20:45:47381
382 return args
Kenneth Russelleb60cbd22017-12-05 07:54:28383
Stephen Martinis7eb8b612018-09-21 00:17:50384 def print_line(self, line):
385 # Exists so that tests can mock
Jamie Madillcf4f8c72021-05-20 19:24:23386 print(line) # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:50387
Kenneth Russelleb60cbd22017-12-05 07:54:28388 def read_file(self, relative_path):
Garrett Beaty79339e182023-04-10 20:45:47389 with open(relative_path) as fp:
Garrett Beaty1afaccc2020-06-25 19:58:15390 return fp.read()
Kenneth Russelleb60cbd22017-12-05 07:54:28391
Garrett Beaty79339e182023-04-10 20:45:47392 def write_file(self, file_path, contents):
Peter Kastingacd55c12023-08-23 20:19:04393 with open(file_path, 'w', newline='') as fp:
Garrett Beaty79339e182023-04-10 20:45:47394 fp.write(contents)
Zhiling Huangbe008172018-03-08 19:13:11395
Joshua Hood56c673c2022-03-02 20:29:33396 # pylint: disable=inconsistent-return-statements
Garrett Beaty79339e182023-04-10 20:45:47397 def load_pyl_file(self, pyl_file_path):
Kenneth Russelleb60cbd22017-12-05 07:54:28398 try:
Garrett Beaty79339e182023-04-10 20:45:47399 return ast.literal_eval(self.read_file(pyl_file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:28400 except (SyntaxError, ValueError) as e: # pragma: no cover
Josip Sokcevic7110fb382023-06-06 01:05:29401 raise BBGenErr('Failed to parse pyl file "%s": %s' %
402 (pyl_file_path, e)) from e
Joshua Hood56c673c2022-03-02 20:29:33403 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:28404
Kenneth Russell8a386d42018-06-02 09:48:01405 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
406 # Currently it is only mandatory for bots which run GPU tests. Change these to
407 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28408 def is_android(self, tester_config):
409 return tester_config.get('os_type') == 'android'
410
Ben Pastenea9e583b2019-01-16 02:57:26411 def is_chromeos(self, tester_config):
412 return tester_config.get('os_type') == 'chromeos'
413
Chong Guc2ca5d02022-01-11 19:52:17414 def is_fuchsia(self, tester_config):
415 return tester_config.get('os_type') == 'fuchsia'
416
Brian Sheedy781c8ca42021-03-08 22:03:21417 def is_lacros(self, tester_config):
418 return tester_config.get('os_type') == 'lacros'
419
Kenneth Russell8a386d42018-06-02 09:48:01420 def is_linux(self, tester_config):
421 return tester_config.get('os_type') == 'linux'
422
Kai Ninomiya40de9f52019-10-18 21:38:49423 def is_mac(self, tester_config):
424 return tester_config.get('os_type') == 'mac'
425
426 def is_win(self, tester_config):
427 return tester_config.get('os_type') == 'win'
428
429 def is_win64(self, tester_config):
430 return (tester_config.get('os_type') == 'win' and
431 tester_config.get('browser_config') == 'release_x64')
432
Garrett Beatyffe83c4f2023-09-08 19:07:37433 def get_exception_for_test(self, test_config):
434 return self.exceptions.get(test_config['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28435
Garrett Beatyffe83c4f2023-09-08 19:07:37436 def should_run_on_tester(self, waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28437 # Currently, the only reason a test should not run on a given tester is that
438 # it's in the exceptions. (Once the GPU waterfall generation script is
439 # incorporated here, the rules will become more complex.)
Garrett Beatyffe83c4f2023-09-08 19:07:37440 exception = self.get_exception_for_test(test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28441 if not exception:
442 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28443 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28444 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28445 if remove_from:
446 if tester_name in remove_from:
447 return False
448 # TODO(kbr): this code path was added for some tests (including
449 # android_webview_unittests) on one machine (Nougat Phone
450 # Tester) which exists with the same name on two waterfalls,
451 # chromium.android and chromium.fyi; the tests are run on one
452 # but not the other. Once the bots are all uniquely named (a
453 # different ongoing project) this code should be removed.
454 # TODO(kbr): add coverage.
455 return (tester_name + ' ' + waterfall['name']
456 not in remove_from) # pragma: no cover
457 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28458
Garrett Beatyffe83c4f2023-09-08 19:07:37459 def get_test_modifications(self, test, tester_name):
460 exception = self.get_exception_for_test(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28461 if not exception:
462 return None
Nico Weber79dc5f6852018-07-13 19:38:49463 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28464
Garrett Beatyffe83c4f2023-09-08 19:07:37465 def get_test_replacements(self, test, tester_name):
466 exception = self.get_exception_for_test(test)
Brian Sheedye6ea0ee2019-07-11 02:54:37467 if not exception:
468 return None
469 return exception.get('replacements', {}).get(tester_name)
470
Kenneth Russell8a386d42018-06-02 09:48:01471 def merge_command_line_args(self, arr, prefix, splitter):
472 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01473 idx = 0
474 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01475 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01476 while idx < len(arr):
477 flag = arr[idx]
478 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01479 if flag.startswith(prefix):
480 arg = flag[prefix_len:]
481 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01482 if first_idx < 0:
483 first_idx = idx
484 else:
485 delete_current_entry = True
486 if delete_current_entry:
487 del arr[idx]
488 else:
489 idx += 1
490 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01491 arr[first_idx] = prefix + splitter.join(accumulated_args)
492 return arr
493
494 def maybe_fixup_args_array(self, arr):
495 # The incoming array of strings may be an array of command line
496 # arguments. To make it easier to turn on certain features per-bot or
497 # per-test-suite, look specifically for certain flags and merge them
498 # appropriately.
499 # --enable-features=Feature1 --enable-features=Feature2
500 # are merged to:
501 # --enable-features=Feature1,Feature2
502 # and:
503 # --extra-browser-args=arg1 --extra-browser-args=arg2
504 # are merged to:
505 # --extra-browser-args=arg1 arg2
506 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
507 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Yuly Novikov8c487e72020-10-16 20:00:29508 arr = self.merge_command_line_args(arr, '--test-launcher-filter-file=', ';')
Cameron Higgins971f0b92023-01-03 18:05:09509 arr = self.merge_command_line_args(arr, '--extra-app-args=', ',')
Kenneth Russell650995a2018-05-03 21:17:01510 return arr
511
Brian Sheedy910cda82022-07-19 11:58:34512 def substitute_magic_args(self, test_config, tester_name, tester_config):
Brian Sheedya31578e2020-05-18 20:24:36513 """Substitutes any magic substitution args present in |test_config|.
514
515 Substitutions are done in-place.
516
517 See buildbot_json_magic_substitutions.py for more information on this
518 feature.
519
520 Args:
521 test_config: A dict containing a configuration for a specific test on
Garrett Beatye3a606ceb2024-04-30 22:13:13522 a specific builder.
Brian Sheedy5f173bb2021-11-24 00:45:54523 tester_name: A string containing the name of the tester that |test_config|
524 came from.
Brian Sheedy910cda82022-07-19 11:58:34525 tester_config: A dict containing the configuration for the builder that
526 |test_config| is for.
Brian Sheedya31578e2020-05-18 20:24:36527 """
528 substituted_array = []
Brian Sheedyba13cf522022-09-13 21:00:09529 original_args = test_config.get('args', [])
530 for arg in original_args:
Brian Sheedya31578e2020-05-18 20:24:36531 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
532 function = arg.replace(
533 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
534 if hasattr(magic_substitutions, function):
535 substituted_array.extend(
Brian Sheedy910cda82022-07-19 11:58:34536 getattr(magic_substitutions, function)(test_config, tester_name,
537 tester_config))
Brian Sheedya31578e2020-05-18 20:24:36538 else:
539 raise BBGenErr(
540 'Magic substitution function %s does not exist' % function)
541 else:
542 substituted_array.append(arg)
Brian Sheedyba13cf522022-09-13 21:00:09543 if substituted_array != original_args:
Brian Sheedya31578e2020-05-18 20:24:36544 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
545
Garrett Beaty8d6708c2023-07-20 17:20:41546 def dictionary_merge(self, a, b, path=None):
Kenneth Russelleb60cbd22017-12-05 07:54:28547 """http://stackoverflow.com/questions/7204805/
548 python-dictionaries-of-dictionaries-merge
549 merges b into a
550 """
551 if path is None:
552 path = []
553 for key in b:
Garrett Beaty8d6708c2023-07-20 17:20:41554 if key not in a:
555 if b[key] is not None:
556 a[key] = b[key]
557 continue
558
559 if isinstance(a[key], dict) and isinstance(b[key], dict):
560 self.dictionary_merge(a[key], b[key], path + [str(key)])
561 elif a[key] == b[key]:
562 pass # same leaf value
563 elif isinstance(a[key], list) and isinstance(b[key], list):
Garrett Beatyade673d2023-08-04 22:00:25564 a[key] = a[key] + b[key]
565 if key.endswith('args'):
566 a[key] = self.maybe_fixup_args_array(a[key])
Garrett Beaty8d6708c2023-07-20 17:20:41567 elif b[key] is None:
568 del a[key]
569 else:
Kenneth Russelleb60cbd22017-12-05 07:54:28570 a[key] = b[key]
Garrett Beaty8d6708c2023-07-20 17:20:41571
Kenneth Russelleb60cbd22017-12-05 07:54:28572 return a
573
Kenneth Russelleb60cbd22017-12-05 07:54:28574 def clean_swarming_dictionary(self, swarming_dict):
575 # Clean out redundant entries from a test's "swarming" dictionary.
576 # This is really only needed to retain 100% parity with the
577 # handwritten JSON files, and can be removed once all the files are
578 # autogenerated.
579 if 'shards' in swarming_dict:
580 if swarming_dict['shards'] == 1: # pragma: no cover
581 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24582 if 'hard_timeout' in swarming_dict:
583 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
584 del swarming_dict['hard_timeout'] # pragma: no cover
Garrett Beatybb18d532023-06-26 22:16:33585 del swarming_dict['can_use_on_swarming_builders']
Kenneth Russelleb60cbd22017-12-05 07:54:28586
Garrett Beatye3a606ceb2024-04-30 22:13:13587 def resolve_os_conditional_values(self, test, builder):
588 for key, fn in (
589 ('android_swarming', self.is_android),
590 ('chromeos_swarming', self.is_chromeos),
591 ):
592 swarming = test.pop(key, None)
593 if swarming and fn(builder):
594 self.dictionary_merge(test['swarming'], swarming)
595
596 for key, fn in (
597 ('desktop_args', lambda cfg: not self.is_android(cfg)),
598 ('lacros_args', self.is_lacros),
599 ('linux_args', self.is_linux),
600 ('android_args', self.is_android),
601 ('chromeos_args', self.is_chromeos),
602 ('mac_args', self.is_mac),
603 ('win_args', self.is_win),
604 ('win64_args', self.is_win64),
605 ):
606 args = test.pop(key, [])
607 if fn(builder):
608 test.setdefault('args', []).extend(args)
609
610 def apply_common_transformations(self,
611 waterfall,
612 builder_name,
613 builder,
614 test,
615 test_name,
616 *,
617 swarmable=True,
618 supports_args=True):
619 # Initialize the swarming dictionary
620 swarmable = swarmable and builder.get('use_swarming', True)
621 test.setdefault('swarming', {}).setdefault('can_use_on_swarming_builders',
622 swarmable)
623
624 mixins_to_ignore = test.pop('remove_mixins', [])
625 self.ensure_valid_mixin_list(mixins_to_ignore,
626 f'test {test_name} remove_mixins')
627
Garrett Beatycc184692024-05-01 14:57:09628 # Expand any conditional values
629 self.resolve_os_conditional_values(test, builder)
630
631 # Apply mixins from the test
632 test_mixins = test.pop('mixins', [])
633 self.ensure_valid_mixin_list(test_mixins, f'test {test_name} mixins')
634 test = self.apply_mixins(test, test_mixins, mixins_to_ignore, builder)
635
Garrett Beatye3a606ceb2024-04-30 22:13:13636 # Add any swarming or args from the builder
637 self.dictionary_merge(test['swarming'], builder.get('swarming', {}))
638 if supports_args:
639 test.setdefault('args', []).extend(builder.get('args', []))
640
Garrett Beatye3a606ceb2024-04-30 22:13:13641 # Apply mixins from the waterfall
642 waterfall_mixins = waterfall.get('mixins', [])
643 self.ensure_valid_mixin_list(waterfall_mixins,
644 f"waterfall {waterfall['name']} mixins")
645 test = self.apply_mixins(test, waterfall_mixins, mixins_to_ignore, builder)
646
647 # Apply mixins from the builder
648 builder_mixins = builder.get('mixins', [])
649 self.ensure_valid_mixin_list(builder_mixins,
650 f"builder {builder_name} mixins")
651 test = self.apply_mixins(test, builder_mixins, mixins_to_ignore, builder)
652
Kenneth Russelleb60cbd22017-12-05 07:54:28653 # See if there are any exceptions that need to be merged into this
654 # test's specification.
Garrett Beatye3a606ceb2024-04-30 22:13:13655 modifications = self.get_test_modifications(test, builder_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28656 if modifications:
657 test = self.dictionary_merge(test, modifications)
Garrett Beatye3a606ceb2024-04-30 22:13:13658
659 # Clean up the swarming entry or remove it if it's unnecessary
Garrett Beatybfeff8f2023-06-16 18:57:25660 if (swarming_dict := test.get('swarming')) is not None:
Garrett Beatybb18d532023-06-26 22:16:33661 if swarming_dict.get('can_use_on_swarming_builders'):
Garrett Beatybfeff8f2023-06-16 18:57:25662 self.clean_swarming_dictionary(swarming_dict)
663 else:
664 del test['swarming']
Garrett Beatye3a606ceb2024-04-30 22:13:13665
Ben Pastenee012aea42019-05-14 22:32:28666 # Ensure all Android Swarming tests run only on userdebug builds if another
667 # build type was not specified.
Garrett Beatye3a606ceb2024-04-30 22:13:13668 if 'swarming' in test and self.is_android(builder):
Garrett Beatyade673d2023-08-04 22:00:25669 dimensions = test.get('swarming', {}).get('dimensions', {})
670 if (dimensions.get('os') == 'Android'
671 and not dimensions.get('device_os_type')):
672 dimensions['device_os_type'] = 'userdebug'
Garrett Beatye3a606ceb2024-04-30 22:13:13673
674 # Apply any replacements specified for the test for the builder
675 self.replace_test_args(test, test_name, builder_name)
676
677 # Remove args if it is empty
678 if 'args' in test:
679 if not test['args']:
680 del test['args']
681 else:
682 # Replace any magic arguments with their actual value
683 self.substitute_magic_args(test, builder_name, builder)
684
685 test['args'] = self.maybe_fixup_args_array(test['args'])
Ben Pastenee012aea42019-05-14 22:32:28686
Kenneth Russelleb60cbd22017-12-05 07:54:28687 return test
688
Brian Sheedye6ea0ee2019-07-11 02:54:37689 def replace_test_args(self, test, test_name, tester_name):
Garrett Beatyffe83c4f2023-09-08 19:07:37690 replacements = self.get_test_replacements(test, tester_name) or {}
Brian Sheedye6ea0ee2019-07-11 02:54:37691 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23692 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37693 if key not in valid_replacement_keys:
694 raise BBGenErr(
695 'Given replacement key %s for %s on %s is not in the list of valid '
696 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23697 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37698 found_key = False
699 for i, test_key in enumerate(test.get(key, [])):
700 # Handle both the key/value being replaced being defined as two
701 # separate items or as key=value.
702 if test_key == replacement_key:
703 found_key = True
704 # Handle flags without values.
705 if replacement_val == None:
706 del test[key][i]
707 else:
708 test[key][i+1] = replacement_val
709 break
Joshua Hood56c673c2022-03-02 20:29:33710 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37711 found_key = True
712 if replacement_val == None:
713 del test[key][i]
714 else:
715 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
716 break
717 if not found_key:
718 raise BBGenErr('Could not find %s in existing list of values for key '
719 '%s in %s on %s' % (replacement_key, key, test_name,
720 tester_name))
721
Shenghua Zhangaba8bad2018-02-07 02:12:09722 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05723 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26724 True):
725 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06726 # are targeting CrOS hardware and so need the special trigger script.
Garrett Beatyade673d2023-08-04 22:00:25727 if 'device_type' in test.get('swarming', {}).get('dimensions', {}):
Ben Pastenea9e583b2019-01-16 02:57:26728 test['trigger_script'] = {
729 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
730 }
Shenghua Zhangaba8bad2018-02-07 02:12:09731
Garrett Beatyffe83c4f2023-09-08 19:07:37732 def add_android_presentation_args(self, tester_config, result):
John Budorick262ae112019-07-12 19:24:38733 bucket = tester_config.get('results_bucket', 'chromium-result-details')
Garrett Beaty94af4272024-04-17 18:06:14734 result.setdefault('args', []).append('--gs-results-bucket=%s' % bucket)
735
736 if ('swarming' in result and 'merge' not in 'result'
737 and not tester_config.get('skip_merge_script', False)):
Ben Pastene858f4be2019-01-09 23:52:09738 result['merge'] = {
Garrett Beatyffe83c4f2023-09-08 19:07:37739 'args': [
740 '--bucket',
741 bucket,
742 '--test-name',
743 result['name'],
744 ],
745 'script': ('//build/android/pylib/results/presentation/'
746 'test_results_presentation.py'),
Ben Pastene858f4be2019-01-09 23:52:09747 }
Ben Pastene858f4be2019-01-09 23:52:09748
Kenneth Russelleb60cbd22017-12-05 07:54:28749 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
750 test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37751 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28752 return None
753 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37754 # Use test_name here instead of test['name'] because test['name'] will be
755 # modified with the variant identifier in a matrix compound suite
756 result.setdefault('test', test_name)
John Budorickab108712018-09-01 00:12:21757
Garrett Beatye3a606ceb2024-04-30 22:13:13758 result = self.apply_common_transformations(waterfall, tester_name,
759 tester_config, result, test_name)
Garrett Beaty94af4272024-04-17 18:06:14760 if self.is_android(tester_config) and 'swarming' in result:
761 if not result.get('use_isolated_scripts_api', False):
Alison Gale71bd8f152024-04-26 22:38:20762 # TODO(crbug.com/40725094) make Android presentation work with
Yuly Novikov26dd47052021-02-11 00:57:14763 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37764 self.add_android_presentation_args(tester_config, result)
Yuly Novikov26dd47052021-02-11 00:57:14765 result['args'] = result.get('args', []) + ['--recover-devices']
Shenghua Zhangaba8bad2018-02-07 02:12:09766 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43767
Garrett Beatybb18d532023-06-26 22:16:33768 if 'swarming' in result and not result.get('merge'):
Jamie Madilla8be0d72020-10-02 05:24:04769 if test_config.get('use_isolated_scripts_api', False):
770 merge_script = 'standard_isolated_script_merge'
771 else:
772 merge_script = 'standard_gtest_merge'
773
Stephen Martinisbc7b7772019-05-01 22:01:43774 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04775 'script': '//testing/merge_scripts/%s.py' % merge_script,
Stephen Martinisbc7b7772019-05-01 22:01:43776 }
Kenneth Russelleb60cbd22017-12-05 07:54:28777 return result
778
779 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
780 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37781 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28782 return None
783 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37784 # Use test_name here instead of test['name'] because test['name'] will be
785 # modified with the variant identifier in a matrix compound suite
Garrett Beatydca3d882023-09-14 23:50:32786 result.setdefault('test', test_name)
Garrett Beatye3a606ceb2024-04-30 22:13:13787 result = self.apply_common_transformations(waterfall, tester_name,
788 tester_config, result, test_name)
Garrett Beaty94af4272024-04-17 18:06:14789 if self.is_android(tester_config) and 'swarming' in result:
Yuly Novikov26dd47052021-02-11 00:57:14790 if tester_config.get('use_android_presentation', False):
Alison Gale71bd8f152024-04-26 22:38:20791 # TODO(crbug.com/40725094) make Android presentation work with
Yuly Novikov26dd47052021-02-11 00:57:14792 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37793 self.add_android_presentation_args(tester_config, result)
Shenghua Zhangaba8bad2018-02-07 02:12:09794 self.add_common_test_properties(result, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17795
Garrett Beatybb18d532023-06-26 22:16:33796 if 'swarming' in result and not result.get('merge'):
Alison Gale923a33e2024-04-22 23:34:28797 # TODO(crbug.com/41456107): Consider adding the ability to not have
Stephen Martinisf50047062019-05-06 22:26:17798 # this default.
799 result['merge'] = {
800 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
Stephen Martinisf50047062019-05-06 22:26:17801 }
Kenneth Russelleb60cbd22017-12-05 07:54:28802 return result
803
804 def generate_script_test(self, waterfall, tester_name, tester_config,
805 test_name, test_config):
Alison Gale47d1537d2024-04-19 21:31:46806 # TODO(crbug.com/40623237): Remove this check whenever a better
Brian Sheedy158cd0f2019-04-26 01:12:44807 # long-term solution is implemented.
808 if (waterfall.get('forbid_script_tests', False) or
809 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
810 raise BBGenErr('Attempted to generate a script test on tester ' +
811 tester_name + ', which explicitly forbids script tests')
Garrett Beatyffe83c4f2023-09-08 19:07:37812 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28813 return None
814 result = {
Garrett Beatyffe83c4f2023-09-08 19:07:37815 'name': test_config['name'],
816 'script': test_config['script'],
Kenneth Russelleb60cbd22017-12-05 07:54:28817 }
Garrett Beatye3a606ceb2024-04-30 22:13:13818 result = self.apply_common_transformations(waterfall,
819 tester_name,
820 tester_config,
821 result,
822 test_name,
823 swarmable=False,
824 supports_args=False)
Kenneth Russelleb60cbd22017-12-05 07:54:28825 return result
826
827 def generate_junit_test(self, waterfall, tester_name, tester_config,
828 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37829 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28830 return None
John Budorickdef6acb2019-09-17 22:51:09831 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37832 # Use test_name here instead of test['name'] because test['name'] will be
833 # modified with the variant identifier in a matrix compound suite
834 result.setdefault('test', test_name)
Garrett Beatye3a606ceb2024-04-30 22:13:13835 result = self.apply_common_transformations(waterfall,
836 tester_name,
837 tester_config,
838 result,
839 test_name,
840 swarmable=False)
Kenneth Russelleb60cbd22017-12-05 07:54:28841 return result
842
Xinan Lin05fb9c1752020-12-17 00:15:52843 def generate_skylab_test(self, waterfall, tester_name, tester_config,
844 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37845 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Xinan Lin05fb9c1752020-12-17 00:15:52846 return None
847 result = copy.deepcopy(test_config)
Brian Sheedy67937ad12024-03-06 22:53:55848 result.setdefault('test', test_name)
Struan Shrimpton08baa3c2024-08-09 17:21:45849 result['run_cft'] = True
yoshiki iguchid1664ef2024-03-28 19:16:52850
851 if 'cros_board' in result or 'cros_board' in tester_config:
852 result['cros_board'] = tester_config.get('cros_board') or result.get(
853 'cros_board')
854 else:
855 raise BBGenErr("skylab tests must specify cros_board.")
856 if 'cros_model' in result or 'cros_model' in tester_config:
857 result['cros_model'] = tester_config.get('cros_model') or result.get(
858 'cros_model')
859 if 'dut_pool' in result or 'cros_dut_pool' in tester_config:
860 result['dut_pool'] = tester_config.get('cros_dut_pool') or result.get(
861 'dut_pool')
Qijiang Fan9032762d2024-06-25 06:02:24862 if 'cros_build_target' in result or 'cros_build_target' in tester_config:
863 result['cros_build_target'] = tester_config.get(
864 'cros_build_target') or result.get('cros_build_target')
yoshiki iguchid1664ef2024-03-28 19:16:52865
yoshiki iguchia5f87c7d2024-06-19 02:48:34866 # Skylab tests enable the shard-level-retry by default.
867 if ('shard_level_retries_on_ctp' in result
868 or 'shard_level_retries_on_ctp' in tester_config):
869 result['shard_level_retries_on_ctp'] = (
870 tester_config.get('shard_level_retries_on_ctp')
871 or result.get('shard_level_retries_on_ctp'))
Qijiang Fan84a0286b2024-06-25 06:44:08872 elif result.get('experiment_percentage') != 100:
yoshiki iguchia5f87c7d2024-06-19 02:48:34873 result['shard_level_retries_on_ctp'] = 1
874
Garrett Beatye3a606ceb2024-04-30 22:13:13875 result = self.apply_common_transformations(waterfall,
876 tester_name,
877 tester_config,
878 result,
879 test_name,
880 swarmable=False)
Xinan Lin05fb9c1752020-12-17 00:15:52881 return result
882
Garrett Beaty65d44222023-08-01 17:22:11883 def substitute_gpu_args(self, tester_config, test, args):
Kenneth Russell8a386d42018-06-02 09:48:01884 substitutions = {
885 # Any machine in waterfalls.pyl which desires to run GPU tests
886 # must provide the os_type key.
887 'os_type': tester_config['os_type'],
888 'gpu_vendor_id': '0',
889 'gpu_device_id': '0',
890 }
Garrett Beatyade673d2023-08-04 22:00:25891 dimensions = test.get('swarming', {}).get('dimensions', {})
892 if 'gpu' in dimensions:
893 # First remove the driver version, then split into vendor and device.
894 gpu = dimensions['gpu']
895 if gpu != 'none':
896 gpu = gpu.split('-')[0].split(':')
897 substitutions['gpu_vendor_id'] = gpu[0]
898 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01899 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
900
Garrett Beaty7436fb72024-08-07 20:20:58901 # LINT.IfChange(gpu_telemetry_test)
902
Kenneth Russell8a386d42018-06-02 09:48:01903 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30904 test_name, test_config, is_android_webview,
Xinan Linedcf05b32023-10-19 23:13:50905 is_cast_streaming, is_skylab):
Kenneth Russell8a386d42018-06-02 09:48:01906 # These are all just specializations of isolated script tests with
907 # a bunch of boilerplate command line arguments added.
908
909 # The step name must end in 'test' or 'tests' in order for the
910 # results to automatically show up on the flakiness dashboard.
911 # (At least, this was true some time ago.) Continue to use this
912 # naming convention for the time being to minimize changes.
Garrett Beaty235c1412023-08-29 20:26:29913 #
914 # test name is the name of the test without the variant ID added
915 if not (test_name.endswith('test') or test_name.endswith('tests')):
916 raise BBGenErr(
917 f'telemetry test names must end with test or tests, got {test_name}')
Garrett Beatyffe83c4f2023-09-08 19:07:37918 result = self.generate_isolated_script_test(waterfall, tester_name,
919 tester_config, test_name,
920 test_config)
Kenneth Russell8a386d42018-06-02 09:48:01921 if not result:
922 return None
Garrett Beatydca3d882023-09-14 23:50:32923 result['test'] = test_config.get('test') or self.get_default_isolate_name(
924 tester_config, is_android_webview)
Chan Liab7d8dd82020-04-24 23:42:19925
Chan Lia3ad1502020-04-28 05:32:11926 # Populate test_id_prefix.
Garrett Beatydca3d882023-09-14 23:50:32927 gn_entry = self.gn_isolate_map[result['test']]
Chan Li17d969f92020-07-10 00:50:03928 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19929
Kenneth Russell8a386d42018-06-02 09:48:01930 args = result.get('args', [])
Garrett Beatyffe83c4f2023-09-08 19:07:37931 # Use test_name here instead of test['name'] because test['name'] will be
932 # modified with the variant identifier in a matrix compound suite
Kenneth Russell8a386d42018-06-02 09:48:01933 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14934
935 # These tests upload and download results from cloud storage and therefore
936 # aren't idempotent yet. https://crbug.com/549140.
Garrett Beatybfeff8f2023-06-16 18:57:25937 if 'swarming' in result:
938 result['swarming']['idempotent'] = False
erikchen6da2d9b2018-08-03 23:01:14939
Fabrice de Ganscbd655f2022-08-04 20:15:30940 browser = ''
941 if is_cast_streaming:
942 browser = 'cast-streaming-shell'
943 elif is_android_webview:
944 browser = 'android-webview-instrumentation'
945 else:
946 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:52947
Greg Thompsoncec7d8d2023-01-10 19:11:53948 extra_browser_args = []
949
Brian Sheedy4053a702020-07-28 02:09:52950 # Most platforms require --enable-logging=stderr to get useful browser logs.
951 # However, this actively messes with logging on CrOS (because Chrome's
952 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
953 # in order to see JavaScript console messages. See
954 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
Greg Thompsoncec7d8d2023-01-10 19:11:53955 if self.is_chromeos(tester_config):
956 extra_browser_args.append('--log-level=0')
957 elif not self.is_fuchsia(tester_config) or browser != 'fuchsia-chrome':
958 # Stderr logging is not needed for Chrome browser on Fuchsia, as ordinary
959 # logging via syslog is captured.
960 extra_browser_args.append('--enable-logging=stderr')
961
962 # --expose-gc allows the WebGL conformance tests to more reliably
963 # reproduce GC-related bugs in the V8 bindings.
964 extra_browser_args.append('--js-flags=--expose-gc')
Brian Sheedy4053a702020-07-28 02:09:52965
Xinan Linedcf05b32023-10-19 23:13:50966 # Skylab supports sharding, so reuse swarming's shard config.
967 if is_skylab and 'shards' not in result and test_config.get(
968 'swarming', {}).get('shards'):
969 result['shards'] = test_config['swarming']['shards']
970
Kenneth Russell8a386d42018-06-02 09:48:01971 args = [
Bo Liu555a0f92019-03-29 12:11:56972 test_to_run,
973 '--show-stdout',
974 '--browser=%s' % browser,
975 # --passthrough displays more of the logging in Telemetry when
976 # run via typ, in particular some of the warnings about tests
977 # being expected to fail, but passing.
978 '--passthrough',
979 '-v',
Brian Sheedy814e0482022-10-03 23:24:12980 '--stable-jobs',
Greg Thompsoncec7d8d2023-01-10 19:11:53981 '--extra-browser-args=%s' % ' '.join(extra_browser_args),
Brian Sheedy997e4802023-10-18 02:28:13982 '--enforce-browser-version',
Kenneth Russell8a386d42018-06-02 09:48:01983 ] + args
Garrett Beatybfeff8f2023-06-16 18:57:25984 result['args'] = self.maybe_fixup_args_array(
Garrett Beaty65d44222023-08-01 17:22:11985 self.substitute_gpu_args(tester_config, result, args))
Kenneth Russell8a386d42018-06-02 09:48:01986 return result
987
Garrett Beaty7436fb72024-08-07 20:20:58988 # pylint: disable=line-too-long
989 # LINT.ThenChange(//infra/config/lib/targets-internal/test-types/gpu_telemetry_test.star)
990 # pylint: enable=line-too-long
991
Brian Sheedyf74819b2021-06-04 01:38:38992 def get_default_isolate_name(self, tester_config, is_android_webview):
993 if self.is_android(tester_config):
994 if is_android_webview:
995 return 'telemetry_gpu_integration_test_android_webview'
996 return (
997 'telemetry_gpu_integration_test' +
998 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:33999 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:171000 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:331001 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:381002
Kenneth Russelleb60cbd22017-12-05 07:54:281003 def get_test_generator_map(self):
1004 return {
Bo Liu555a0f92019-03-29 12:11:561005 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301006 GPUTelemetryTestGenerator(self, is_android_webview=True),
1007 'cast_streaming_tests':
1008 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:561009 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301010 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561011 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301012 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561013 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301014 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561015 'junit_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301016 JUnitGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561017 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301018 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:521019 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301020 SkylabGenerator(self),
Brian Sheedyb6491ba2022-09-26 20:49:491021 'skylab_gpu_telemetry_tests':
1022 SkylabGPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281023 }
1024
Kenneth Russell8a386d42018-06-02 09:48:011025 def get_test_type_remapper(self):
1026 return {
Fabrice de Gans223272482022-08-08 16:56:571027 # These are a specialization of isolated_scripts with a bunch of
1028 # boilerplate command line arguments added to each one.
1029 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
1030 'cast_streaming_tests': 'isolated_scripts',
1031 'gpu_telemetry_tests': 'isolated_scripts',
Brian Sheedyb6491ba2022-09-26 20:49:491032 # These are the same as existing test types, just configured to run
1033 # in Skylab instead of via normal swarming.
1034 'skylab_gpu_telemetry_tests': 'skylab_tests',
Kenneth Russell8a386d42018-06-02 09:48:011035 }
1036
Jeff Yoon67c3e832020-02-08 07:39:381037 def check_composition_type_test_suites(self, test_type,
1038 additional_validators=None):
1039 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1040 validators = [check_compound_references,
1041 check_basic_references,
1042 check_conflicting_definitions]
1043 if additional_validators:
1044 validators += additional_validators
1045
1046 target_suites = self.test_suites.get(test_type, {})
1047 other_test_type = ('compound_suites'
1048 if test_type == 'matrix_compound_suites'
1049 else 'matrix_compound_suites')
1050 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011051 basic_suites = self.test_suites.get('basic_suites', {})
1052
Jamie Madillcf4f8c72021-05-20 19:24:231053 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011054 if suite in basic_suites:
1055 raise BBGenErr('%s names may not duplicate basic test suite names '
1056 '(error found while processsing %s)'
1057 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011058
Jeff Yoon67c3e832020-02-08 07:39:381059 seen_tests = {}
1060 for sub_suite in suite_def:
1061 for validator in validators:
1062 validator(
1063 basic_suites=basic_suites,
1064 other_test_suites=other_suites,
1065 seen_tests=seen_tests,
1066 sub_suite=sub_suite,
1067 suite=suite,
1068 suite_def=suite_def,
1069 target_test_suites=target_suites,
1070 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051071 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381072 )
Kenneth Russelleb60cbd22017-12-05 07:54:281073
Stephen Martinis54d64ad2018-09-21 22:16:201074 def flatten_test_suites(self):
1075 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011076 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1077 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231078 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011079 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201080 self.test_suites = new_test_suites
1081
Chan Lia3ad1502020-04-28 05:32:111082 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231083 for suite in self.test_suites['basic_suites'].values():
1084 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561085 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411086
Garrett Beatydca3d882023-09-14 23:50:321087 isolate_name = test.get('test') or key
Nodir Turakulovfce34292019-12-18 17:05:411088 gn_entry = self.gn_isolate_map.get(isolate_name)
1089 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281090 label = gn_entry['label']
1091
1092 if label.count(':') != 1:
1093 raise BBGenErr(
1094 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1095 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1096 (label, isolate_name))
1097 if label.split(':')[1] != isolate_name:
1098 raise BBGenErr(
1099 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1100 ' label "%s" see http://crbug.com/1071091 for details.' %
1101 (isolate_name, label))
1102
Chan Lia3ad1502020-04-28 05:32:111103 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411104 else: # pragma: no cover
1105 # Some tests do not have an entry gn_isolate_map.pyl, such as
1106 # telemetry tests.
Alison Gale47d1537d2024-04-19 21:31:461107 # TODO(crbug.com/40112160): require an entry in gn_isolate_map.
Nodir Turakulovfce34292019-12-18 17:05:411108 pass
1109
Kenneth Russelleb60cbd22017-12-05 07:54:281110 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011111 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201112
Jeff Yoon8154e582019-12-03 23:30:011113 compound_suites = self.test_suites.get('compound_suites', {})
1114 # check_composition_type_test_suites() checks that all basic suites
1115 # referenced by compound suites exist.
1116 basic_suites = self.test_suites.get('basic_suites')
1117
Jamie Madillcf4f8c72021-05-20 19:24:231118 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011119 # Resolve this to a dictionary.
1120 full_suite = {}
1121 for entry in value:
1122 suite = basic_suites[entry]
1123 full_suite.update(suite)
1124 compound_suites[name] = full_suite
1125
Jeff Yoon85fb8df2020-08-20 16:47:431126 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381127 """ Merge variant-defined configurations to each test case definition in a
1128 test suite.
1129
1130 The output maps a unique test name to an array of configurations because
1131 there may exist more than one definition for a test name using variants. The
1132 test name is referenced while mapping machines to test suites, so unpacking
1133 the array is done by the generators.
1134
1135 Args:
1136 basic_test_definition: a {} defined test suite in the format
1137 test_name:test_config
1138 variants: an [] of {} defining configurations to be applied to each test
1139 case in the basic test_definition
1140
1141 Return:
1142 a {} of test_name:[{}], where each {} is a merged configuration
1143 """
1144
1145 # Each test in a basic test suite will have a definition per variant.
1146 test_suite = {}
Garrett Beaty8d6708c2023-07-20 17:20:411147 for variant in variants:
1148 # Unpack the variant from variants.pyl if it's string based.
1149 if isinstance(variant, str):
1150 variant = self.variants[variant]
Jeff Yoonda581c32020-03-06 03:56:051151
Garrett Beaty8d6708c2023-07-20 17:20:411152 # If 'enabled' is set to False, we will not use this variant; otherwise if
1153 # the variant doesn't include 'enabled' variable or 'enabled' is set to
1154 # True, we will use this variant
1155 if not variant.get('enabled', True):
1156 continue
Jeff Yoon67c3e832020-02-08 07:39:381157
Garrett Beaty8d6708c2023-07-20 17:20:411158 # Make a shallow copy of the variant to remove variant-specific fields,
1159 # leaving just mixin fields
1160 variant = copy.copy(variant)
1161 variant.pop('enabled', None)
1162 identifier = variant.pop('identifier')
1163 variant_mixins = variant.pop('mixins', [])
1164 variant_skylab = variant.pop('skylab', {})
Jeff Yoon67c3e832020-02-08 07:39:381165
Garrett Beaty8d6708c2023-07-20 17:20:411166 for test_name, test_config in basic_test_definition.items():
1167 new_test = self.apply_mixin(variant, test_config)
Jeff Yoon67c3e832020-02-08 07:39:381168
Garrett Beaty8d6708c2023-07-20 17:20:411169 new_test['mixins'] = (test_config.get('mixins', []) + variant_mixins +
1170 mixins)
Xinan Lin05fb9c1752020-12-17 00:15:521171
Jeff Yoon67c3e832020-02-08 07:39:381172 # The identifier is used to make the name of the test unique.
1173 # Generators in the recipe uniquely identify a test by it's name, so we
1174 # don't want to have the same name for each variant.
Garrett Beaty235c1412023-08-29 20:26:291175 new_test['name'] = f'{test_name} {identifier}'
Ben Pastene5f231cf22022-05-05 18:03:071176
1177 # Attach the variant identifier to the test config so downstream
1178 # generators can make modifications based on the original name. This
1179 # is mainly used in generate_gpu_telemetry_test().
Garrett Beaty8d6708c2023-07-20 17:20:411180 new_test['variant_id'] = identifier
Ben Pastene5f231cf22022-05-05 18:03:071181
Garrett Beaty8d6708c2023-07-20 17:20:411182 for k, v in variant_skylab.items():
Sven Zheng22ba6312023-10-16 22:59:351183 # cros_chrome_version is the ash chrome version in the cros img in the
1184 # variant of cros_board. We don't want to include it in the final json
1185 # files; so remove it.
Garrett Beaty8d6708c2023-07-20 17:20:411186 if k != 'cros_chrome_version':
1187 new_test[k] = v
1188
Sven Zheng22ba6312023-10-16 22:59:351189 # For skylab, we need to pop the correct `autotest_name`. This field
1190 # defines what wrapper we use in OS infra. e.g. for gtest it's
1191 # https://source.chromium.org/chromiumos/chromiumos/codesearch/+/main:src/third_party/autotest/files/server/site_tests/chromium/chromium.py
1192 if variant_skylab and 'autotest_name' not in new_test:
1193 if 'tast_expr' in test_config:
1194 if 'lacros' in test_config['name']:
1195 new_test['autotest_name'] = 'tast.lacros-from-gcs'
1196 else:
1197 new_test['autotest_name'] = 'tast.chrome-from-gcs'
1198 elif 'benchmark' in test_config:
1199 new_test['autotest_name'] = 'chromium_Telemetry'
1200 else:
1201 new_test['autotest_name'] = 'chromium'
1202
Garrett Beaty8d6708c2023-07-20 17:20:411203 test_suite.setdefault(test_name, []).append(new_test)
1204
Jeff Yoon67c3e832020-02-08 07:39:381205 return test_suite
1206
Jeff Yoon8154e582019-12-03 23:30:011207 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381208 self.check_composition_type_test_suites('matrix_compound_suites',
1209 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011210
1211 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381212 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011213 # referenced by matrix suites exist.
1214 basic_suites = self.test_suites.get('basic_suites')
1215
Garrett Beaty235c1412023-08-29 20:26:291216 for matrix_suite_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011217 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381218
Jamie Madillcf4f8c72021-05-20 19:24:231219 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381220 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1221
Garrett Beaty235c1412023-08-29 20:26:291222 def update_tests(expanded):
1223 for test_name, new_tests in expanded.items():
1224 if not isinstance(new_tests, list):
1225 new_tests = [new_tests]
1226 tests_for_name = full_suite.setdefault(test_name, [])
1227 for t in new_tests:
1228 if t not in tests_for_name:
1229 tests_for_name.append(t)
1230
Garrett Beaty60a7b2a2023-09-13 23:00:401231 if (variants := mtx_test_suite_config.get('variants')):
Jeff Yoon85fb8df2020-08-20 16:47:431232 mixins = mtx_test_suite_config.get('mixins', [])
Garrett Beaty60a7b2a2023-09-13 23:00:401233 result = self.resolve_variants(basic_test_def, variants, mixins)
Garrett Beaty235c1412023-08-29 20:26:291234 update_tests(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271235 else:
1236 suite = basic_suites[test_suite]
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 Beaty235c1412023-08-29 20:26:291268 self.resolve_test_names()
Garrett Beatydca3d882023-09-14 23:50:321269 self.resolve_isolate_names()
Garrett Beaty65d44222023-08-01 17:22:111270 self.resolve_dimension_sets()
Chan Lia3ad1502020-04-28 05:32:111271 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281272 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011273 self.resolve_matrix_compound_test_suites()
1274 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281275 self.link_waterfalls_to_test_suites()
1276
Garrett Beaty235c1412023-08-29 20:26:291277 def resolve_test_names(self):
1278 for suite_name, suite in self.test_suites.get('basic_suites').items():
1279 for test_name, test in suite.items():
1280 if 'name' in test:
1281 raise BBGenErr(
1282 f'The name field is set in test {test_name} in basic suite '
1283 f'{suite_name}, this is not supported, the test name is the key '
1284 'within the basic suite')
Garrett Beatyffe83c4f2023-09-08 19:07:371285 # When a test is expanded with variants, this will be overwritten, but
1286 # this ensures every test definition has the name field set
1287 test['name'] = test_name
Garrett Beaty235c1412023-08-29 20:26:291288
Garrett Beatydca3d882023-09-14 23:50:321289 def resolve_isolate_names(self):
1290 for suite_name, suite in self.test_suites.get('basic_suites').items():
1291 for test_name, test in suite.items():
1292 if 'isolate_name' in test:
1293 raise BBGenErr(
1294 f'The isolate_name field is set in test {test_name} in basic '
1295 f'suite {suite_name}, the test field should be used instead')
1296
Garrett Beaty65d44222023-08-01 17:22:111297 def resolve_dimension_sets(self):
Garrett Beaty65d44222023-08-01 17:22:111298
1299 def definitions():
1300 for suite_name, suite in self.test_suites.get('basic_suites', {}).items():
1301 for test_name, test in suite.items():
1302 yield test, f'test {test_name} in basic suite {suite_name}'
1303
1304 for mixin_name, mixin in self.mixins.items():
1305 yield mixin, f'mixin {mixin_name}'
1306
1307 for waterfall in self.waterfalls:
1308 for builder_name, builder in waterfall.get('machines', {}).items():
1309 yield (
1310 builder,
1311 f'builder {builder_name} in waterfall {waterfall["name"]}',
1312 )
1313
1314 for test_name, exceptions in self.exceptions.items():
1315 modifications = exceptions.get('modifications', {})
1316 for builder_name, mods in modifications.items():
1317 yield (
1318 mods,
1319 f'exception for test {test_name} on builder {builder_name}',
1320 )
1321
1322 for definition, location in definitions():
1323 for swarming_attr in (
1324 'swarming',
1325 'android_swarming',
1326 'chromeos_swarming',
1327 ):
1328 if (swarming :=
1329 definition.get(swarming_attr)) and 'dimension_sets' in swarming:
Garrett Beatyade673d2023-08-04 22:00:251330 raise BBGenErr(
1331 f'dimension_sets is no longer supported (set in {location}),'
1332 ' instead, use set dimensions to a single dict')
Garrett Beaty65d44222023-08-01 17:22:111333
Nico Weberd18b8962018-05-16 19:39:381334 def unknown_bot(self, bot_name, waterfall_name):
1335 return BBGenErr(
1336 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1337
Kenneth Russelleb60cbd22017-12-05 07:54:281338 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1339 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381340 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281341 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1342
1343 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1344 return BBGenErr(
1345 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1346 ' on waterfall ' + waterfall_name)
1347
Garrett Beatye3a606ceb2024-04-30 22:13:131348 def ensure_valid_mixin_list(self, mixins, location):
1349 if not isinstance(mixins, list):
1350 raise BBGenErr(
1351 f"got '{mixins}', should be a list of mixin names: {location}")
1352 for mixin in mixins:
1353 if not mixin in self.mixins:
1354 raise BBGenErr(f'bad mixin {mixin}: {location}')
Stephen Martinisb6a50492018-09-12 23:59:321355
Garrett Beatye3a606ceb2024-04-30 22:13:131356 def apply_mixins(self, test, mixins, mixins_to_ignore, builder=None):
1357 for mixin in mixins:
1358 if mixin not in mixins_to_ignore:
Austin Eng148d9f0f2022-02-08 19:18:531359 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinis0382bc12018-09-17 22:29:071360 return test
Stephen Martinisb6a50492018-09-12 23:59:321361
Garrett Beaty8d6708c2023-07-20 17:20:411362 def apply_mixin(self, mixin, test, builder=None):
Stephen Martinisb72f6d22018-10-04 23:29:011363 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321364
Garrett Beaty4c35b142023-06-23 21:01:231365 A mixin is applied by copying all fields from the mixin into the
1366 test with the following exceptions:
1367 * For the various *args keys, the test's existing value (an empty
1368 list if not present) will be extended with the mixin's value.
1369 * The sub-keys of the swarming value will be copied to the test's
1370 swarming value with the following exceptions:
Garrett Beatyade673d2023-08-04 22:00:251371 * For the named_caches sub-keys, the test's existing value (an
1372 empty list if not present) will be extended with the mixin's
1373 value.
1374 * For the dimensions sub-key, the tests's existing value (an empty
1375 dict if not present) will be updated with the mixin's value.
Stephen Martinisb6a50492018-09-12 23:59:321376 """
Garrett Beaty4c35b142023-06-23 21:01:231377
Stephen Martinisb6a50492018-09-12 23:59:321378 new_test = copy.deepcopy(test)
1379 mixin = copy.deepcopy(mixin)
Garrett Beaty8d6708c2023-07-20 17:20:411380
1381 if 'description' in mixin:
1382 description = []
1383 if 'description' in new_test:
1384 description.append(new_test['description'])
1385 description.append(mixin.pop('description'))
1386 new_test['description'] = '\n'.join(description)
1387
Stephen Martinisb72f6d22018-10-04 23:29:011388 if 'swarming' in mixin:
1389 swarming_mixin = mixin['swarming']
1390 new_test.setdefault('swarming', {})
1391 if 'dimensions' in swarming_mixin:
Garrett Beatyade673d2023-08-04 22:00:251392 new_test['swarming'].setdefault('dimensions', {}).update(
1393 swarming_mixin.pop('dimensions'))
Garrett Beaty4c35b142023-06-23 21:01:231394 if 'named_caches' in swarming_mixin:
1395 new_test['swarming'].setdefault('named_caches', []).extend(
1396 swarming_mixin['named_caches'])
1397 del swarming_mixin['named_caches']
Stephen Martinisb72f6d22018-10-04 23:29:011398 # python dict update doesn't do recursion at all. Just hard code the
1399 # nested update we need (mixin['swarming'] shouldn't clobber
1400 # test['swarming'], but should update it).
1401 new_test['swarming'].update(swarming_mixin)
1402 del mixin['swarming']
1403
Garrett Beatye3a606ceb2024-04-30 22:13:131404 for a in ('args', 'precommit_args', 'non_precommit_args'):
Garrett Beaty4c35b142023-06-23 21:01:231405 if (value := mixin.pop(a, None)) is None:
1406 continue
1407 if not isinstance(value, list):
1408 raise BBGenErr(f'"{a}" must be a list')
1409 new_test.setdefault(a, []).extend(value)
1410
Garrett Beatye3a606ceb2024-04-30 22:13:131411 # At this point, all keys that require merging are taken care of, so the
1412 # remaining entries can be copied over. The os-conditional entries will be
1413 # resolved immediately after and they are resolved before any mixins are
1414 # applied, so there's are no concerns about overwriting the corresponding
1415 # entry in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011416 new_test.update(mixin)
Garrett Beatye3a606ceb2024-04-30 22:13:131417 if builder:
1418 self.resolve_os_conditional_values(new_test, builder)
1419
1420 if 'args' in new_test:
1421 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1422
Stephen Martinisb6a50492018-09-12 23:59:321423 return new_test
1424
Greg Gutermanf60eb052020-03-12 17:40:011425 def generate_output_tests(self, waterfall):
1426 """Generates the tests for a waterfall.
1427
1428 Args:
1429 waterfall: a dictionary parsed from a master pyl file
1430 Returns:
1431 A dictionary mapping builders to test specs
1432 """
1433 return {
Jamie Madillcf4f8c72021-05-20 19:24:231434 name: self.get_tests_for_config(waterfall, name, config)
1435 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011436 }
1437
1438 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531439 generator_map = self.get_test_generator_map()
1440 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281441
Greg Gutermanf60eb052020-03-12 17:40:011442 tests = {}
1443 # Copy only well-understood entries in the machine's configuration
1444 # verbatim into the generated JSON.
1445 if 'additional_compile_targets' in config:
1446 tests['additional_compile_targets'] = config[
1447 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231448 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011449 if test_type not in generator_map:
1450 raise self.unknown_test_suite_type(
1451 test_type, name, waterfall['name']) # pragma: no cover
1452 test_generator = generator_map[test_type]
1453 # Let multiple kinds of generators generate the same kinds
1454 # of tests. For example, gpu_telemetry_tests are a
1455 # specialization of isolated_scripts.
1456 new_tests = test_generator.generate(
1457 waterfall, name, config, input_tests)
1458 remapped_test_type = test_type_remapper.get(test_type, test_type)
Garrett Beatyffe83c4f2023-09-08 19:07:371459 tests.setdefault(remapped_test_type, []).extend(new_tests)
1460
1461 for test_type, tests_for_type in tests.items():
1462 if test_type == 'additional_compile_targets':
1463 continue
1464 tests[test_type] = sorted(tests_for_type, key=lambda t: t['name'])
Greg Gutermanf60eb052020-03-12 17:40:011465
1466 return tests
1467
1468 def jsonify(self, all_tests):
1469 return json.dumps(
1470 all_tests, indent=2, separators=(',', ': '),
1471 sort_keys=True) + '\n'
1472
1473 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281474 self.load_configuration_files()
1475 self.resolve_configuration_files()
1476 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011477 result = collections.defaultdict(dict)
1478
Stephanie Kim572b43c02023-04-13 14:24:131479 if os.path.exists(self.args.autoshard_exceptions_json_path):
1480 autoshards = json.loads(
1481 self.read_file(self.args.autoshard_exceptions_json_path))
1482 else:
1483 autoshards = {}
1484
Dirk Pranke6269d302020-10-01 00:14:391485 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011486 for waterfall in self.waterfalls:
1487 for field in required_fields:
1488 # Verify required fields
1489 if field not in waterfall:
1490 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1491
1492 # Handle filter flag, if specified
1493 if filters and waterfall['name'] not in filters:
1494 continue
1495
1496 # Join config files and hardcoded values together
1497 all_tests = self.generate_output_tests(waterfall)
1498 result[waterfall['name']] = all_tests
1499
Stephanie Kim572b43c02023-04-13 14:24:131500 if not autoshards:
1501 continue
1502 for builder, test_spec in all_tests.items():
1503 for target_type, test_list in test_spec.items():
1504 if target_type == 'additional_compile_targets':
1505 continue
1506 for test_dict in test_list:
1507 # Suites that apply variants or other customizations will create
1508 # test_dicts that have "name" value that is different from the
Garrett Beatyffe83c4f2023-09-08 19:07:371509 # "test" value.
Stephanie Kim572b43c02023-04-13 14:24:131510 # e.g. name = vulkan_swiftshader_content_browsertests, but
1511 # test = content_browsertests and
1512 # test_id_prefix = "ninja://content/test:content_browsertests/"
Garrett Beatyffe83c4f2023-09-08 19:07:371513 test_name = test_dict['name']
Stephanie Kim572b43c02023-04-13 14:24:131514 shard_info = autoshards.get(waterfall['name'],
1515 {}).get(builder, {}).get(test_name)
1516 if shard_info:
1517 test_dict['swarming'].update(
1518 {'shards': int(shard_info['shards'])})
1519
Greg Gutermanf60eb052020-03-12 17:40:011520 # Add do not edit warning
1521 for tests in result.values():
1522 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1523 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1524
1525 return result
1526
1527 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281528 suffix = '.json'
1529 if self.args.new_files:
1530 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011531
1532 for filename, contents in result.items():
1533 jsonstr = self.jsonify(contents)
Garrett Beaty79339e182023-04-10 20:45:471534 file_path = os.path.join(self.args.output_dir, filename + suffix)
1535 self.write_file(file_path, jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281536
Nico Weberd18b8962018-05-16 19:39:381537 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161538 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421539 # NOTE: This reference can cause issues; if a file changes there, the
1540 # presubmit here won't be run by default. A manually maintained list there
1541 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1542 # references to configs outside of this directory are added, please change
1543 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1544 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491545
Garrett Beaty7e866fc2021-06-16 14:12:101546 # Get the generated project.pyl so we can check if we should be enforcing
1547 # that the specs are for builders that actually exist
1548 # If not, return None to indicate that we won't enforce that builders in
1549 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491550 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1551 'project.pyl')
1552 if os.path.exists(project_pyl_path):
1553 settings = ast.literal_eval(self.read_file(project_pyl_path))
1554 if not settings.get('validate_source_side_specs_have_builder', True):
1555 return None
1556
Nico Weberd18b8962018-05-16 19:39:381557 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311558 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161559 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1560 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431561 for c in milo_configs:
1562 for l in self.read_file(c).splitlines():
1563 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311564 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431565 continue
1566 # l looks like
1567 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1568 # Extract win_chromium_dbg_ng part.
1569 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381570 return bot_names
1571
Ben Pastene9a010082019-09-25 20:41:371572 def get_internal_waterfalls(self):
1573 # Similar to get_builders_that_do_not_actually_exist above, but for
1574 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201575 return [
Kramer Ge3bf853a2023-04-13 19:39:471576 'chrome', 'chrome.pgo', 'chrome.gpu.fyi', 'internal.chrome.fyi',
yoshiki iguchi4de608082024-03-14 00:33:361577 'internal.chromeos.fyi', 'internal.optimization_guide', 'internal.soda',
1578 'chromeos.preuprev'
Yuke Liaoe6c23dd2021-07-28 16:12:201579 ]
Ben Pastene9a010082019-09-25 20:41:371580
Stephen Martinisf83893722018-09-19 00:02:181581 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201582 self.check_input_files_sorting(verbose)
1583
Kenneth Russelleb60cbd22017-12-05 07:54:281584 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011585 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381586 self.check_composition_type_test_suites('matrix_compound_suites',
1587 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111588 self.resolve_test_id_prefixes()
Garrett Beaty1ead4a52023-12-07 19:16:421589
1590 # All test suites must be referenced. Check this before flattening the test
1591 # suites so that we can transitively check the basic suites for compound
1592 # suites and matrix compound suites (otherwise we would determine a basic
1593 # suite is used if it shared a name with a test present in a basic suite
1594 # that is used).
1595 all_suites = set(
1596 itertools.chain(*(self.test_suites.get(a, {}) for a in (
1597 'basic_suites',
1598 'compound_suites',
1599 'matrix_compound_suites',
1600 ))))
1601 unused_suites = set(all_suites)
1602 generator_map = self.get_test_generator_map()
1603 for waterfall in self.waterfalls:
1604 for bot_name, tester in waterfall['machines'].items():
1605 for suite_type, suite in tester.get('test_suites', {}).items():
1606 if suite_type not in generator_map:
1607 raise self.unknown_test_suite_type(suite_type, bot_name,
1608 waterfall['name'])
1609 if suite not in all_suites:
1610 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1611 unused_suites.discard(suite)
1612 # For each compound suite or matrix compound suite, if the suite was used,
1613 # remove all of the basic suites that it composes from the set of unused
1614 # suites
1615 for a in ('compound_suites', 'matrix_compound_suites'):
1616 for suite, sub_suites in self.test_suites.get(a, {}).items():
1617 if suite not in unused_suites:
1618 unused_suites.difference_update(sub_suites)
1619 if unused_suites:
1620 raise BBGenErr('The following test suites were unreferenced by bots on '
1621 'the waterfalls: ' + str(unused_suites))
1622
Stephen Martinis54d64ad2018-09-21 22:16:201623 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381624
1625 # All bots should exist.
1626 bot_names = self.get_valid_bot_names()
Garrett Beaty2a02de3c2020-05-15 13:57:351627 if bot_names is not None:
1628 internal_waterfalls = self.get_internal_waterfalls()
1629 for waterfall in self.waterfalls:
Alison Gale923a33e2024-04-22 23:34:281630 # TODO(crbug.com/41474799): Remove the need for this exception.
Garrett Beaty2a02de3c2020-05-15 13:57:351631 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011632 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351633 for bot_name in waterfall['machines']:
Garrett Beaty2a02de3c2020-05-15 13:57:351634 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581635 if waterfall['name'] in [
1636 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1637 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351638 # TODO(thakis): Remove this once these bots move to luci.
1639 continue # pragma: no cover
1640 if waterfall['name'] in ['tryserver.webrtc',
1641 'webrtc.chromium.fyi.experimental']:
1642 # These waterfalls have their bot configs in a different repo.
1643 # so we don't know about their bot names.
1644 continue # pragma: no cover
1645 if waterfall['name'] in ['client.devtools-frontend.integration',
1646 'tryserver.devtools-frontend',
1647 'chromium.devtools-frontend']:
1648 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201649 if waterfall['name'] in ['client.openscreen.chromium']:
1650 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351651 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381652
Kenneth Russelleb60cbd22017-12-05 07:54:281653 # All test suite exceptions must refer to bots on the waterfall.
1654 all_bots = set()
1655 missing_bots = set()
1656 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231657 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281658 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281659 # In order to disambiguate between bots with the same name on
1660 # different waterfalls, support has been added to various
1661 # exceptions for concatenating the waterfall name after the bot
1662 # name.
1663 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231664 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381665 removals = (exception.get('remove_from', []) +
1666 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231667 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381668 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281669 if removal not in all_bots:
1670 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411671
Kenneth Russelleb60cbd22017-12-05 07:54:281672 if missing_bots:
1673 raise BBGenErr('The following nonexistent machines were referenced in '
1674 'the test suite exceptions: ' + str(missing_bots))
1675
Garrett Beatyb061e69d2023-06-27 16:15:351676 for name, mixin in self.mixins.items():
1677 if '$mixin_append' in mixin:
1678 raise BBGenErr(
1679 f'$mixin_append is no longer supported (set in mixin "{name}"),'
1680 ' args and named caches specified as normal will be appended')
1681
Stephen Martinis0382bc12018-09-17 22:29:071682 # All mixins must be referenced
1683 seen_mixins = set()
1684 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011685 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231686 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011687 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071688 for suite in self.test_suites.values():
1689 if isinstance(suite, list):
1690 # Don't care about this, it's a composition, which shouldn't include a
1691 # swarming mixin.
1692 continue
1693
1694 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561695 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011696 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071697
Zhaoyang Li9da047d52021-05-10 21:31:441698 for variant in self.variants:
1699 # Unpack the variant from variants.pyl if it's string based.
1700 if isinstance(variant, str):
1701 variant = self.variants[variant]
1702 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1703
Stephen Martinisb72f6d22018-10-04 23:29:011704 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071705 if missing_mixins:
1706 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1707 ' referenced in a waterfall, machine, or test suite.' % (
1708 str(missing_mixins)))
1709
Jeff Yoonda581c32020-03-06 03:56:051710 # All variant references must be referenced
1711 seen_variants = set()
1712 for suite in self.test_suites.values():
1713 if isinstance(suite, list):
1714 continue
1715
1716 for test in suite.values():
1717 if isinstance(test, dict):
1718 for variant in test.get('variants', []):
1719 if isinstance(variant, str):
1720 seen_variants.add(variant)
1721
1722 missing_variants = set(self.variants.keys()) - seen_variants
1723 if missing_variants:
1724 raise BBGenErr('The following variants were unreferenced: %s. They must '
1725 'be referenced in a matrix test suite under the variants '
1726 'key.' % str(missing_variants))
1727
Stephen Martinis54d64ad2018-09-21 22:16:201728
Garrett Beaty79339e182023-04-10 20:45:471729 def type_assert(self, node, typ, file_path, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201730 """Asserts that the Python AST node |node| is of type |typ|.
1731
1732 If verbose is set, it prints out some helpful context lines, showing where
1733 exactly the error occurred in the file.
1734 """
1735 if not isinstance(node, typ):
1736 if verbose:
Garrett Beaty79339e182023-04-10 20:45:471737 lines = [""] + self.read_file(file_path).splitlines()
Stephen Martinis54d64ad2018-09-21 22:16:201738
1739 context = 2
1740 lines_start = max(node.lineno - context, 0)
1741 # Add one to include the last line
1742 lines_end = min(node.lineno + context, len(lines)) + 1
Garrett Beaty79339e182023-04-10 20:45:471743 lines = itertools.chain(
1744 ['== %s ==\n' % file_path],
1745 ["<snip>\n"],
1746 [
1747 '%d %s' % (lines_start + i, line)
1748 for i, line in enumerate(lines[lines_start:lines_start +
1749 context])
1750 ],
1751 ['-' * 80 + '\n'],
1752 ['%d %s' % (node.lineno, lines[node.lineno])],
1753 [
1754 '-' * (node.col_offset + 3) + '^' + '-' *
1755 (80 - node.col_offset - 4) + '\n'
1756 ],
1757 [
1758 '%d %s' % (node.lineno + 1 + i, line)
1759 for i, line in enumerate(lines[node.lineno + 1:lines_end])
1760 ],
1761 ["<snip>\n"],
Stephen Martinis54d64ad2018-09-21 22:16:201762 )
1763 # Print out a useful message when a type assertion fails.
1764 for l in lines:
1765 self.print_line(l.strip())
1766
1767 node_dumped = ast.dump(node, annotate_fields=False)
1768 # If the node is huge, truncate it so everything fits in a terminal
1769 # window.
1770 if len(node_dumped) > 60: # pragma: no cover
1771 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1772 raise BBGenErr(
Garrett Beaty807011ab2023-04-12 00:52:391773 'Invalid .pyl file \'%s\'. Python AST node %r on line %s expected to'
Garrett Beaty79339e182023-04-10 20:45:471774 ' be %s, is %s' %
1775 (file_path, node_dumped, node.lineno, typ, type(node)))
Stephen Martinis54d64ad2018-09-21 22:16:201776
Garrett Beaty79339e182023-04-10 20:45:471777 def check_ast_list_formatted(self,
1778 keys,
1779 file_path,
1780 verbose,
Stephen Martinis1384ff92020-01-07 19:52:151781 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531782 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201783
Stephen Martinis5bef0fc2020-01-06 22:47:531784 Currently only checks to ensure they're correctly sorted, and that there
1785 are no duplicates.
1786
1787 Args:
1788 keys: An python list of AST nodes.
1789
1790 It's a list of AST nodes instead of a list of strings because
1791 when verbose is set, it tries to print out context of where the
1792 diffs are in the file.
Garrett Beaty79339e182023-04-10 20:45:471793 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531794 verbose: If set, print out diff information about how the keys are
1795 incorrectly formatted.
1796 check_sorting: If true, checks if the list is sorted.
1797 Returns:
1798 If the keys are correctly formatted.
1799 """
1800 if not keys:
1801 return True
1802
1803 assert isinstance(keys[0], ast.Str)
1804
1805 keys_strs = [k.s for k in keys]
1806 # Keys to diff against. Used below.
1807 keys_to_diff_against = None
1808 # If the list is properly formatted.
1809 list_formatted = True
1810
1811 # Duplicates are always bad.
1812 if len(set(keys_strs)) != len(keys_strs):
1813 list_formatted = False
1814 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1815
1816 if check_sorting and sorted(keys_strs) != keys_strs:
1817 list_formatted = False
1818 if list_formatted:
1819 return True
1820
1821 if verbose:
1822 line_num = keys[0].lineno
1823 keys = [k.s for k in keys]
1824 if check_sorting:
1825 # If we have duplicates, sorting this will take care of it anyways.
1826 keys_to_diff_against = sorted(set(keys))
1827 # else, keys_to_diff_against is set above already
1828
1829 self.print_line('=' * 80)
1830 self.print_line('(First line of keys is %s)' % line_num)
Garrett Beaty79339e182023-04-10 20:45:471831 for line in difflib.context_diff(keys,
1832 keys_to_diff_against,
1833 fromfile='current (%r)' % file_path,
1834 tofile='sorted',
1835 lineterm=''):
Stephen Martinis5bef0fc2020-01-06 22:47:531836 self.print_line(line)
1837 self.print_line('=' * 80)
1838
1839 return False
1840
Garrett Beaty79339e182023-04-10 20:45:471841 def check_ast_dict_formatted(self, node, file_path, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531842 """Checks if an ast dictionary's keys are correctly formatted.
1843
1844 Just a simple wrapper around check_ast_list_formatted.
1845 Args:
1846 node: An AST node. Assumed to be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471847 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531848 verbose: If set, print out diff information about how the keys are
1849 incorrectly formatted.
1850 check_sorting: If true, checks if the list is sorted.
1851 Returns:
1852 If the dictionary is correctly formatted.
1853 """
Stephen Martinis54d64ad2018-09-21 22:16:201854 keys = []
1855 # The keys of this dict are ordered as ordered in the file; normal python
1856 # dictionary keys are given an arbitrary order, but since we parsed the
1857 # file itself, the order as given in the file is preserved.
1858 for key in node.keys:
Garrett Beaty79339e182023-04-10 20:45:471859 self.type_assert(key, ast.Str, file_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531860 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201861
Garrett Beaty79339e182023-04-10 20:45:471862 return self.check_ast_list_formatted(keys, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181863
1864 def check_input_files_sorting(self, verbose=False):
Alison Gale923a33e2024-04-22 23:34:281865 # TODO(crbug.com/41415841): Add the ability for this script to
Stephen Martinis54d64ad2018-09-21 22:16:201866 # actually format the files, rather than just complain if they're
1867 # incorrectly formatted.
1868 bad_files = set()
Garrett Beaty79339e182023-04-10 20:45:471869
1870 def parse_file(file_path):
Stephen Martinis5bef0fc2020-01-06 22:47:531871 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201872
Stephen Martinis5bef0fc2020-01-06 22:47:531873 Returns an AST node representing the value in the pyl file."""
Garrett Beaty79339e182023-04-10 20:45:471874 parsed = ast.parse(self.read_file(file_path))
Stephen Martinisf83893722018-09-19 00:02:181875
Stephen Martinisf83893722018-09-19 00:02:181876 # Must be a module.
Garrett Beaty79339e182023-04-10 20:45:471877 self.type_assert(parsed, ast.Module, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181878 module = parsed.body
1879
1880 # Only one expression in the module.
Garrett Beaty79339e182023-04-10 20:45:471881 self.type_assert(module, list, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181882 if len(module) != 1: # pragma: no cover
Garrett Beaty79339e182023-04-10 20:45:471883 raise BBGenErr('Invalid .pyl file %s' % file_path)
Stephen Martinisf83893722018-09-19 00:02:181884 expr = module[0]
Garrett Beaty79339e182023-04-10 20:45:471885 self.type_assert(expr, ast.Expr, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181886
Stephen Martinis5bef0fc2020-01-06 22:47:531887 return expr.value
1888
1889 # Handle this separately
Garrett Beaty79339e182023-04-10 20:45:471890 value = parse_file(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531891 # Value should be a list.
Garrett Beaty79339e182023-04-10 20:45:471892 self.type_assert(value, ast.List, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531893
1894 keys = []
Joshua Hood56c673c2022-03-02 20:29:331895 for elm in value.elts:
Garrett Beaty79339e182023-04-10 20:45:471896 self.type_assert(elm, ast.Dict, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531897 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331898 for key, val in zip(elm.keys, elm.values):
Garrett Beaty79339e182023-04-10 20:45:471899 self.type_assert(key, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531900 if key.s == 'machines':
Garrett Beaty79339e182023-04-10 20:45:471901 if not self.check_ast_dict_formatted(
1902 val, self.args.waterfalls_pyl_path, verbose):
1903 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531904
1905 if key.s == "name":
Garrett Beaty79339e182023-04-10 20:45:471906 self.type_assert(val, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531907 waterfall_name = val
1908 assert waterfall_name
1909 keys.append(waterfall_name)
1910
Garrett Beaty79339e182023-04-10 20:45:471911 if not self.check_ast_list_formatted(keys, self.args.waterfalls_pyl_path,
1912 verbose):
1913 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531914
Garrett Beaty79339e182023-04-10 20:45:471915 for file_path in (
1916 self.args.mixins_pyl_path,
1917 self.args.test_suites_pyl_path,
1918 self.args.test_suite_exceptions_pyl_path,
Stephen Martinis5bef0fc2020-01-06 22:47:531919 ):
Garrett Beaty79339e182023-04-10 20:45:471920 value = parse_file(file_path)
Stephen Martinisf83893722018-09-19 00:02:181921 # Value should be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471922 self.type_assert(value, ast.Dict, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181923
Garrett Beaty79339e182023-04-10 20:45:471924 if not self.check_ast_dict_formatted(value, file_path, verbose):
1925 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531926
Garrett Beaty79339e182023-04-10 20:45:471927 if file_path == self.args.test_suites_pyl_path:
Jeff Yoon8154e582019-12-03 23:30:011928 expected_keys = ['basic_suites',
1929 'compound_suites',
1930 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201931 actual_keys = [node.s for node in value.keys]
1932 assert all(key in expected_keys for key in actual_keys), (
Garrett Beaty79339e182023-04-10 20:45:471933 'Invalid %r file; expected keys %r, got %r' %
1934 (file_path, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:331935 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:201936 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011937 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201938 for suite_group in suite_dicts:
Garrett Beaty79339e182023-04-10 20:45:471939 if not self.check_ast_dict_formatted(suite_group, file_path, verbose):
1940 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181941
Stephen Martinis5bef0fc2020-01-06 22:47:531942 for key, suite in zip(value.keys, value.values):
1943 # The compound suites are checked in
1944 # 'check_composition_type_test_suites()'
1945 if key.s == 'basic_suites':
1946 for group in suite.values:
Garrett Beaty79339e182023-04-10 20:45:471947 if not self.check_ast_dict_formatted(group, file_path, verbose):
1948 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531949 break
Stephen Martinis54d64ad2018-09-21 22:16:201950
Garrett Beaty79339e182023-04-10 20:45:471951 elif file_path == self.args.test_suite_exceptions_pyl_path:
Stephen Martinis5bef0fc2020-01-06 22:47:531952 # Check the values for each test.
1953 for test in value.values:
1954 for kind, node in zip(test.keys, test.values):
1955 if isinstance(node, ast.Dict):
Garrett Beaty79339e182023-04-10 20:45:471956 if not self.check_ast_dict_formatted(node, file_path, verbose):
1957 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531958 elif kind.s == 'remove_from':
1959 # Don't care about sorting; these are usually grouped, since the
1960 # same bug can affect multiple builders. Do want to make sure
1961 # there aren't duplicates.
Garrett Beaty79339e182023-04-10 20:45:471962 if not self.check_ast_list_formatted(
1963 node.elts, file_path, verbose, check_sorting=False):
1964 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181965
1966 if bad_files:
1967 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201968 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531969 'unsorted, or have duplicates. Re-run this with --verbose to see '
1970 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181971
Kenneth Russelleb60cbd22017-12-05 07:54:281972 def check_output_file_consistency(self, verbose=False):
1973 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011974 # All waterfalls/bucket .json files must have been written
1975 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:281976 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011977 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:161978 outputs = self.generate_outputs()
1979 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:011980 expected = self.jsonify(expected_contents)
Garrett Beaty79339e182023-04-10 20:45:471981 file_path = os.path.join(self.args.output_dir, filename + '.json')
Ben Pastenef21cda32023-03-30 22:00:571982 current = self.read_file(file_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281983 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:011984 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:321985 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:011986 self.print_line('File ' + filename +
1987 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321988 'contents:')
1989 for line in difflib.unified_diff(
1990 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501991 current.splitlines(),
1992 fromfile='expected', tofile='current'):
1993 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:011994
1995 if ungenerated_files:
1996 raise BBGenErr(
1997 'The following files have not been properly '
1998 'autogenerated by generate_buildbot_json.py: ' +
1999 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:282000
Dirk Pranke772f55f2021-04-28 04:51:162001 for builder_group, builders in outputs.items():
2002 for builder, step_types in builders.items():
Garrett Beatydca3d882023-09-14 23:50:322003 for test_type in ('gtest_tests', 'isolated_scripts'):
2004 for step_data in step_types.get(test_type, []):
2005 step_name = step_data['name']
2006 self._check_swarming_config(builder_group, builder, step_name,
2007 step_data)
Dirk Pranke772f55f2021-04-28 04:51:162008
2009 def _check_swarming_config(self, filename, builder, step_name, step_data):
Alison Gale47d1537d2024-04-19 21:31:462010 # TODO(crbug.com/40179524): Ensure all swarming tests specify cpu, not
Dirk Pranke772f55f2021-04-28 04:51:162011 # just mac tests.
Garrett Beatybb18d532023-06-26 22:16:332012 if 'swarming' in step_data:
Garrett Beatyade673d2023-08-04 22:00:252013 dimensions = step_data['swarming'].get('dimensions')
2014 if not dimensions:
Tatsuhisa Yamaguchif1878d52023-11-06 06:02:252015 raise BBGenErr('%s: %s / %s : dimensions must be specified for all '
Dirk Pranke772f55f2021-04-28 04:51:162016 'swarmed tests' % (filename, builder, step_name))
Garrett Beatyade673d2023-08-04 22:00:252017 if not dimensions.get('os'):
2018 raise BBGenErr('%s: %s / %s : os must be specified for all '
2019 'swarmed tests' % (filename, builder, step_name))
2020 if 'Mac' in dimensions.get('os') and not dimensions.get('cpu'):
2021 raise BBGenErr('%s: %s / %s : cpu must be specified for mac '
2022 'swarmed tests' % (filename, builder, step_name))
Dirk Pranke772f55f2021-04-28 04:51:162023
Kenneth Russelleb60cbd22017-12-05 07:54:282024 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502025 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282026 self.check_output_file_consistency(verbose) # pragma: no cover
2027
Karen Qiane24b7ee2019-02-12 23:37:062028 def does_test_match(self, test_info, params_dict):
2029 """Checks to see if the test matches the parameters given.
2030
2031 Compares the provided test_info with the params_dict to see
2032 if the bot matches the parameters given. If so, returns True.
2033 Else, returns false.
2034
2035 Args:
2036 test_info (dict): Information about a specific bot provided
2037 in the format shown in waterfalls.pyl
2038 params_dict (dict): Dictionary of parameters and their values
2039 to look for in the bot
2040 Ex: {
2041 'device_os':'android',
2042 '--flag':True,
2043 'mixins': ['mixin1', 'mixin2'],
2044 'ex_key':'ex_value'
2045 }
2046
2047 """
2048 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2049 'kvm', 'pool', 'integrity'] # dimension parameters
2050 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2051 'can_use_on_swarming_builders']
2052 for param in params_dict:
2053 # if dimension parameter
2054 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2055 if not 'swarming' in test_info:
2056 return False
2057 swarming = test_info['swarming']
2058 if param in SWARMING_PARAMS:
2059 if not param in swarming:
2060 return False
2061 if not str(swarming[param]) == params_dict[param]:
2062 return False
2063 else:
Garrett Beatyade673d2023-08-04 22:00:252064 if not 'dimensions' in swarming:
Karen Qiane24b7ee2019-02-12 23:37:062065 return False
Garrett Beatyade673d2023-08-04 22:00:252066 dimensions = swarming['dimensions']
Karen Qiane24b7ee2019-02-12 23:37:062067 # only looking at the first dimension set
Garrett Beatyade673d2023-08-04 22:00:252068 if not param in dimensions:
Karen Qiane24b7ee2019-02-12 23:37:062069 return False
Garrett Beatyade673d2023-08-04 22:00:252070 if not dimensions[param] == params_dict[param]:
Karen Qiane24b7ee2019-02-12 23:37:062071 return False
2072
2073 # if flag
2074 elif param.startswith('--'):
2075 if not 'args' in test_info:
2076 return False
2077 if not param in test_info['args']:
2078 return False
2079
2080 # not dimension parameter/flag/mixin
2081 else:
2082 if not param in test_info:
2083 return False
2084 if not test_info[param] == params_dict[param]:
2085 return False
2086 return True
2087 def error_msg(self, msg):
2088 """Prints an error message.
2089
2090 In addition to a catered error message, also prints
2091 out where the user can find more help. Then, program exits.
2092 """
2093 self.print_line(msg + (' If you need more information, ' +
2094 'please run with -h or --help to see valid commands.'))
2095 sys.exit(1)
2096
2097 def find_bots_that_run_test(self, test, bots):
2098 matching_bots = []
2099 for bot in bots:
2100 bot_info = bots[bot]
2101 tests = self.flatten_tests_for_bot(bot_info)
2102 for test_info in tests:
Garrett Beatyffe83c4f2023-09-08 19:07:372103 test_name = test_info['name']
Karen Qiane24b7ee2019-02-12 23:37:062104 if not test_name == test:
2105 continue
2106 matching_bots.append(bot)
2107 return matching_bots
2108
2109 def find_tests_with_params(self, tests, params_dict):
2110 matching_tests = []
2111 for test_name in tests:
2112 test_info = tests[test_name]
2113 if not self.does_test_match(test_info, params_dict):
2114 continue
2115 if not test_name in matching_tests:
2116 matching_tests.append(test_name)
2117 return matching_tests
2118
2119 def flatten_waterfalls_for_query(self, waterfalls):
2120 bots = {}
2121 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012122 waterfall_tests = self.generate_output_tests(waterfall)
2123 for bot in waterfall_tests:
2124 bot_info = waterfall_tests[bot]
2125 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062126 return bots
2127
2128 def flatten_tests_for_bot(self, bot_info):
2129 """Returns a list of flattened tests.
2130
2131 Returns a list of tests not grouped by test category
2132 for a specific bot.
2133 """
2134 TEST_CATS = self.get_test_generator_map().keys()
2135 tests = []
2136 for test_cat in TEST_CATS:
2137 if not test_cat in bot_info:
2138 continue
2139 test_cat_tests = bot_info[test_cat]
2140 tests = tests + test_cat_tests
2141 return tests
2142
2143 def flatten_tests_for_query(self, test_suites):
2144 """Returns a flattened dictionary of tests.
2145
2146 Returns a dictionary of tests associate with their
2147 configuration, not grouped by their test suite.
2148 """
2149 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232150 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062151 for test in test_suite:
2152 test_info = test_suite[test]
2153 test_name = test
Karen Qiane24b7ee2019-02-12 23:37:062154 tests[test_name] = test_info
2155 return tests
2156
2157 def parse_query_filter_params(self, params):
2158 """Parses the filter parameters.
2159
2160 Creates a dictionary from the parameters provided
2161 to filter the bot array.
2162 """
2163 params_dict = {}
2164 for p in params:
2165 # flag
2166 if p.startswith("--"):
2167 params_dict[p] = True
2168 else:
2169 pair = p.split(":")
2170 if len(pair) != 2:
2171 self.error_msg('Invalid command.')
2172 # regular parameters
2173 if pair[1].lower() == "true":
2174 params_dict[pair[0]] = True
2175 elif pair[1].lower() == "false":
2176 params_dict[pair[0]] = False
2177 else:
2178 params_dict[pair[0]] = pair[1]
2179 return params_dict
2180
2181 def get_test_suites_dict(self, bots):
2182 """Returns a dictionary of bots and their tests.
2183
2184 Returns a dictionary of bots and a list of their associated tests.
2185 """
2186 test_suite_dict = dict()
2187 for bot in bots:
2188 bot_info = bots[bot]
2189 tests = self.flatten_tests_for_bot(bot_info)
2190 test_suite_dict[bot] = tests
2191 return test_suite_dict
2192
2193 def output_query_result(self, result, json_file=None):
2194 """Outputs the result of the query.
2195
2196 If a json file parameter name is provided, then
2197 the result is output into the json file. If not,
2198 then the result is printed to the console.
2199 """
2200 output = json.dumps(result, indent=2)
2201 if json_file:
2202 self.write_file(json_file, output)
2203 else:
2204 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062205
Joshua Hood56c673c2022-03-02 20:29:332206 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062207 def query(self, args):
2208 """Queries tests or bots.
2209
2210 Depending on the arguments provided, outputs a json of
2211 tests or bots matching the appropriate optional parameters provided.
2212 """
2213 # split up query statement
2214 query = args.query.split('/')
2215 self.load_configuration_files()
2216 self.resolve_configuration_files()
2217
2218 # flatten bots json
2219 tests = self.test_suites
2220 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2221
2222 cmd_class = query[0]
2223
2224 # For queries starting with 'bots'
2225 if cmd_class == "bots":
2226 if len(query) == 1:
2227 return self.output_query_result(bots, args.json)
2228 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332229 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062230 if query[1] == 'tests':
2231 test_suites_dict = self.get_test_suites_dict(bots)
2232 return self.output_query_result(test_suites_dict, args.json)
Joshua Hood56c673c2022-03-02 20:29:332233 self.error_msg("This query should be in the format: bots/tests.")
Karen Qiane24b7ee2019-02-12 23:37:062234
2235 else:
2236 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2237 % str(len(query)-1))
2238
2239 # For queries starting with 'bot'
2240 elif cmd_class == "bot":
2241 if not len(query) == 2 and not len(query) == 3:
2242 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2243 % str(len(query)-1))
2244 bot_id = query[1]
2245 if not bot_id in bots:
2246 self.error_msg("No bot named '" + bot_id + "' found.")
2247 bot_info = bots[bot_id]
2248 if len(query) == 2:
2249 return self.output_query_result(bot_info, args.json)
2250 if not query[2] == 'tests':
2251 self.error_msg("The query should be in the format:" +
2252 "bot/<bot-name>/tests.")
2253
2254 bot_tests = self.flatten_tests_for_bot(bot_info)
2255 return self.output_query_result(bot_tests, args.json)
2256
2257 # For queries starting with 'tests'
2258 elif cmd_class == "tests":
2259 if not len(query) == 1 and not len(query) == 2:
2260 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2261 % str(len(query)-1))
2262 flattened_tests = self.flatten_tests_for_query(tests)
2263 if len(query) == 1:
2264 return self.output_query_result(flattened_tests, args.json)
2265
2266 # create params dict
2267 params = query[1].split('&')
2268 params_dict = self.parse_query_filter_params(params)
2269 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2270 return self.output_query_result(matching_bots)
2271
2272 # For queries starting with 'test'
2273 elif cmd_class == "test":
2274 if not len(query) == 2 and not len(query) == 3:
2275 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2276 % str(len(query)-1))
2277 test_id = query[1]
2278 if len(query) == 2:
2279 flattened_tests = self.flatten_tests_for_query(tests)
2280 for test in flattened_tests:
2281 if test == test_id:
2282 return self.output_query_result(flattened_tests[test], args.json)
2283 self.error_msg("There is no test named %s." % test_id)
2284 if not query[2] == 'bots':
2285 self.error_msg("The query should be in the format: " +
2286 "test/<test-name>/bots")
2287 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2288 return self.output_query_result(bots_for_test)
2289
2290 else:
2291 self.error_msg("Your command did not match any valid commands." +
2292 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Joshua Hood56c673c2022-03-02 20:29:332293 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282294
Garrett Beaty1afaccc2020-06-25 19:58:152295 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282296 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502297 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062298 elif self.args.query:
2299 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282300 else:
Greg Gutermanf60eb052020-03-12 17:40:012301 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282302 return 0
2303
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())