blob: 16052bae63e9388025c5131b4f073bc3338a24ad [file] [log] [blame]
Joshua Hood3455e1352022-03-03 23:23:591#!/usr/bin/env vpython3
Avi Drissmandfd880852022-09-15 20:11:092# Copyright 2016 The Chromium Authors
Kenneth Russelleb60cbd22017-12-05 07:54:283# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Script to generate the majority of the JSON files in the src/testing/buildbot
7directory. Maintaining these files by hand is too unwieldy.
8"""
9
10import argparse
11import ast
12import collections
13import copy
John Budorick826d5ed2017-12-28 19:27:3214import difflib
Garrett Beatyd5ca75962020-05-07 16:58:3115import glob
Kenneth Russell8ceeabf2017-12-11 17:53:2816import itertools
Kenneth Russelleb60cbd22017-12-05 07:54:2817import json
18import os
Kenneth Russelleb60cbd22017-12-05 07:54:2819import string
20import sys
21
Brian Sheedya31578e2020-05-18 20:24:3622import buildbot_json_magic_substitutions as magic_substitutions
23
Joshua Hood56c673c2022-03-02 20:29:3324# pylint: disable=super-with-arguments,useless-super-delegation
25
Kenneth Russelleb60cbd22017-12-05 07:54:2826THIS_DIR = os.path.dirname(os.path.abspath(__file__))
27
Brian Sheedyf74819b2021-06-04 01:38:3828BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP = {
29 'android-chromium': '_android_chrome',
30 'android-chromium-monochrome': '_android_monochrome',
Brian Sheedyf74819b2021-06-04 01:38:3831 'android-webview': '_android_webview',
32}
33
Kenneth Russelleb60cbd22017-12-05 07:54:2834
35class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4936 def __init__(self, message):
37 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2838
39
Joshua Hood56c673c2022-03-02 20:29:3340class BaseGenerator(object): # pylint: disable=useless-object-inheritance
Kenneth Russelleb60cbd22017-12-05 07:54:2841 def __init__(self, bb_gen):
42 self.bb_gen = bb_gen
43
Kenneth Russell8ceeabf2017-12-11 17:53:2844 def generate(self, waterfall, tester_name, tester_config, input_tests):
Garrett Beatyffe83c4f2023-09-08 19:07:3745 raise NotImplementedError() # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:2846
47
Kenneth Russell8a386d42018-06-02 09:48:0148class GPUTelemetryTestGenerator(BaseGenerator):
Fabrice de Ganscbd655f2022-08-04 20:15:3049 def __init__(self, bb_gen, is_android_webview=False, is_cast_streaming=False):
Kenneth Russell8a386d42018-06-02 09:48:0150 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5651 self._is_android_webview = is_android_webview
Fabrice de Ganscbd655f2022-08-04 20:15:3052 self._is_cast_streaming = is_cast_streaming
Kenneth Russell8a386d42018-06-02 09:48:0153
54 def generate(self, waterfall, tester_name, tester_config, input_tests):
55 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:2356 for test_name, test_config in sorted(input_tests.items()):
Ben Pastene8e7eb2652022-04-29 19:44:3157 # Variants allow more than one definition for a given test, and is defined
58 # in array format from resolve_variants().
59 if not isinstance(test_config, list):
60 test_config = [test_config]
61
62 for config in test_config:
63 test = self.bb_gen.generate_gpu_telemetry_test(waterfall, tester_name,
64 tester_config, test_name,
65 config,
Fabrice de Ganscbd655f2022-08-04 20:15:3066 self._is_android_webview,
67 self._is_cast_streaming)
Ben Pastene8e7eb2652022-04-29 19:44:3168 if test:
69 isolated_scripts.append(test)
70
Kenneth Russell8a386d42018-06-02 09:48:0171 return isolated_scripts
72
Kenneth Russell8a386d42018-06-02 09:48:0173
Brian Sheedyb6491ba2022-09-26 20:49:4974class SkylabGPUTelemetryTestGenerator(GPUTelemetryTestGenerator):
75 def generate(self, *args, **kwargs):
76 # This should be identical to a regular GPU Telemetry test, but with any
77 # swarming arguments removed.
78 isolated_scripts = super(SkylabGPUTelemetryTestGenerator,
79 self).generate(*args, **kwargs)
80 for test in isolated_scripts:
Xinan Lind9b1d2e72022-11-14 20:57:0281 # chromium_GPU is the Autotest wrapper created for browser GPU tests
82 # run in Skylab.
Xinan Lin1f28a0d2023-03-13 17:39:4183 test['autotest_name'] = 'chromium_Graphics'
Xinan Lind9b1d2e72022-11-14 20:57:0284 # As of 22Q4, Skylab tests are running on a CrOS flavored Autotest
85 # framework and it does not support the sub-args like
86 # extra-browser-args. So we have to pop it out and create a new
87 # key for it. See crrev.com/c/3965359 for details.
88 for idx, arg in enumerate(test.get('args', [])):
89 if '--extra-browser-args' in arg:
90 test['args'].pop(idx)
91 test['extra_browser_args'] = arg.replace('--extra-browser-args=', '')
92 break
Brian Sheedyb6491ba2022-09-26 20:49:4993 return isolated_scripts
94
95
Kenneth Russelleb60cbd22017-12-05 07:54:2896class GTestGenerator(BaseGenerator):
97 def __init__(self, bb_gen):
98 super(GTestGenerator, self).__init__(bb_gen)
99
Kenneth Russell8ceeabf2017-12-11 17:53:28100 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28101 # The relative ordering of some of the tests is important to
102 # minimize differences compared to the handwritten JSON files, since
103 # Python's sorts are stable and there are some tests with the same
104 # key (see gles2_conform_d3d9_test and similar variants). Avoid
105 # losing the order by avoiding coalescing the dictionaries into one.
106 gtests = []
Jamie Madillcf4f8c72021-05-20 19:24:23107 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoon67c3e832020-02-08 07:39:38108 # Variants allow more than one definition for a given test, and is defined
109 # in array format from resolve_variants().
110 if not isinstance(test_config, list):
111 test_config = [test_config]
112
113 for config in test_config:
114 test = self.bb_gen.generate_gtest(
115 waterfall, tester_name, tester_config, test_name, config)
116 if test:
117 # generate_gtest may veto the test generation on this tester.
118 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28119 return gtests
120
Kenneth Russelleb60cbd22017-12-05 07:54:28121
122class IsolatedScriptTestGenerator(BaseGenerator):
123 def __init__(self, bb_gen):
124 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
125
Kenneth Russell8ceeabf2017-12-11 17:53:28126 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28127 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23128 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43129 # Variants allow more than one definition for a given test, and is defined
130 # in array format from resolve_variants().
131 if not isinstance(test_config, list):
132 test_config = [test_config]
133
134 for config in test_config:
135 test = self.bb_gen.generate_isolated_script_test(
136 waterfall, tester_name, tester_config, test_name, config)
137 if test:
138 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28139 return isolated_scripts
140
Kenneth Russelleb60cbd22017-12-05 07:54:28141
142class ScriptGenerator(BaseGenerator):
143 def __init__(self, bb_gen):
144 super(ScriptGenerator, self).__init__(bb_gen)
145
Kenneth Russell8ceeabf2017-12-11 17:53:28146 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28147 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23148 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28149 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28150 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28151 if test:
152 scripts.append(test)
153 return scripts
154
Kenneth Russelleb60cbd22017-12-05 07:54:28155
156class JUnitGenerator(BaseGenerator):
157 def __init__(self, bb_gen):
158 super(JUnitGenerator, self).__init__(bb_gen)
159
Kenneth Russell8ceeabf2017-12-11 17:53:28160 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28161 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23162 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28163 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28164 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28165 if test:
166 scripts.append(test)
167 return scripts
168
Kenneth Russelleb60cbd22017-12-05 07:54:28169
Xinan Lin05fb9c1752020-12-17 00:15:52170class SkylabGenerator(BaseGenerator):
171 def __init__(self, bb_gen):
172 super(SkylabGenerator, self).__init__(bb_gen)
173
174 def generate(self, waterfall, tester_name, tester_config, input_tests):
175 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23176 for test_name, test_config in sorted(input_tests.items()):
Xinan Lin05fb9c1752020-12-17 00:15:52177 for config in test_config:
178 test = self.bb_gen.generate_skylab_test(waterfall, tester_name,
179 tester_config, test_name,
180 config)
181 if test:
182 scripts.append(test)
183 return scripts
184
Xinan Lin05fb9c1752020-12-17 00:15:52185
Jeff Yoon67c3e832020-02-08 07:39:38186def check_compound_references(other_test_suites=None,
187 sub_suite=None,
188 suite=None,
189 target_test_suites=None,
190 test_type=None,
191 **kwargs):
192 """Ensure comound reference's don't target other compounds"""
193 del kwargs
194 if sub_suite in other_test_suites or sub_suite in target_test_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15195 raise BBGenErr('%s may not refer to other composition type test '
196 'suites (error found while processing %s)' %
197 (test_type, suite))
198
Jeff Yoon67c3e832020-02-08 07:39:38199
200def check_basic_references(basic_suites=None,
201 sub_suite=None,
202 suite=None,
203 **kwargs):
204 """Ensure test has a basic suite reference"""
205 del kwargs
206 if sub_suite not in basic_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15207 raise BBGenErr('Unable to find reference to %s while processing %s' %
208 (sub_suite, suite))
209
Jeff Yoon67c3e832020-02-08 07:39:38210
211def check_conflicting_definitions(basic_suites=None,
212 seen_tests=None,
213 sub_suite=None,
214 suite=None,
215 test_type=None,
Garrett Beaty235c1412023-08-29 20:26:29216 target_test_suites=None,
Jeff Yoon67c3e832020-02-08 07:39:38217 **kwargs):
218 """Ensure that if a test is reachable via multiple basic suites,
219 all of them have an identical definition of the tests.
220 """
221 del kwargs
Garrett Beaty235c1412023-08-29 20:26:29222 variants = None
223 if test_type == 'matrix_compound_suites':
224 variants = target_test_suites[suite][sub_suite].get('variants')
225 variants = variants or [None]
Jeff Yoon67c3e832020-02-08 07:39:38226 for test_name in basic_suites[sub_suite]:
Garrett Beaty235c1412023-08-29 20:26:29227 for variant in variants:
228 key = (test_name, variant)
229 if ((seen_sub_suite := seen_tests.get(key)) is not None
230 and basic_suites[sub_suite][test_name] !=
231 basic_suites[seen_sub_suite][test_name]):
232 test_description = (test_name if variant is None else
233 f'{test_name} with variant {variant} applied')
234 raise BBGenErr(
235 'Conflicting test definitions for %s from %s '
236 'and %s in %s (error found while processing %s)' %
237 (test_description, seen_tests[key], sub_suite, test_type, suite))
238 seen_tests[key] = sub_suite
239
Jeff Yoon67c3e832020-02-08 07:39:38240
241def check_matrix_identifier(sub_suite=None,
242 suite=None,
243 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05244 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38245 **kwargs):
246 """Ensure 'idenfitier' is defined for each variant"""
247 del kwargs
248 sub_suite_config = suite_def[sub_suite]
Garrett Beaty2022db42023-08-29 17:22:40249 for variant_name in sub_suite_config.get('variants', []):
250 if variant_name not in all_variants:
251 raise BBGenErr('Missing variant definition for %s in variants.pyl' %
252 variant_name)
253 variant = all_variants[variant_name]
Jeff Yoonda581c32020-03-06 03:56:05254
Jeff Yoon67c3e832020-02-08 07:39:38255 if not 'identifier' in variant:
256 raise BBGenErr('Missing required identifier field in matrix '
257 'compound suite %s, %s' % (suite, sub_suite))
Sven Zhengef0d0872022-04-04 22:13:29258 if variant['identifier'] == '':
259 raise BBGenErr('Identifier field can not be "" in matrix '
260 'compound suite %s, %s' % (suite, sub_suite))
261 if variant['identifier'].strip() != variant['identifier']:
262 raise BBGenErr('Identifier field can not have leading and trailing '
263 'whitespace in matrix compound suite %s, %s' %
264 (suite, sub_suite))
Jeff Yoon67c3e832020-02-08 07:39:38265
266
Joshua Hood56c673c2022-03-02 20:29:33267class BBJSONGenerator(object): # pylint: disable=useless-object-inheritance
Garrett Beaty1afaccc2020-06-25 19:58:15268 def __init__(self, args):
Garrett Beaty1afaccc2020-06-25 19:58:15269 self.args = args
Kenneth Russelleb60cbd22017-12-05 07:54:28270 self.waterfalls = None
271 self.test_suites = None
272 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01273 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41274 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05275 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28276
Garrett Beaty1afaccc2020-06-25 19:58:15277 @staticmethod
278 def parse_args(argv):
279
280 # RawTextHelpFormatter allows for styling of help statement
281 parser = argparse.ArgumentParser(
282 formatter_class=argparse.RawTextHelpFormatter)
283
284 group = parser.add_mutually_exclusive_group()
285 group.add_argument(
286 '-c',
287 '--check',
288 action='store_true',
289 help=
290 'Do consistency checks of configuration and generated files and then '
291 'exit. Used during presubmit. '
292 'Causes the tool to not generate any files.')
293 group.add_argument(
294 '--query',
295 type=str,
296 help=(
297 "Returns raw JSON information of buildbots and tests.\n" +
298 "Examples:\n" + " List all bots (all info):\n" +
299 " --query bots\n\n" +
300 " List all bots and only their associated tests:\n" +
301 " --query bots/tests\n\n" +
302 " List all information about 'bot1' " +
303 "(make sure you have quotes):\n" + " --query bot/'bot1'\n\n" +
304 " List tests running for 'bot1' (make sure you have quotes):\n" +
305 " --query bot/'bot1'/tests\n\n" + " List all tests:\n" +
306 " --query tests\n\n" +
307 " List all tests and the bots running them:\n" +
308 " --query tests/bots\n\n" +
309 " List all tests that satisfy multiple parameters\n" +
310 " (separation of parameters by '&' symbol):\n" +
311 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
312 " List all tests that run with a specific flag:\n" +
313 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
314 " List specific test (make sure you have quotes):\n"
315 " --query test/'test1'\n\n"
316 " List all bots running 'test1' " +
317 "(make sure you have quotes):\n" + " --query test/'test1'/bots"))
318 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47319 '--json',
320 metavar='JSON_FILE_PATH',
321 type=os.path.abspath,
322 help='Outputs results into a json file. Only works with query function.'
323 )
324 parser.add_argument(
Garrett Beaty1afaccc2020-06-25 19:58:15325 '-n',
326 '--new-files',
327 action='store_true',
328 help=
329 'Write output files as .new.json. Useful during development so old and '
330 'new files can be looked at side-by-side.')
Garrett Beatyade673d2023-08-04 22:00:25331 parser.add_argument('--dimension-sets-handling',
332 choices=['disable'],
333 default='disable',
334 help=('This flag no longer has any effect:'
335 ' dimension_sets fields are not allowed'))
Garrett Beaty1afaccc2020-06-25 19:58:15336 parser.add_argument('-v',
337 '--verbose',
338 action='store_true',
339 help='Increases verbosity. Affects consistency checks.')
340 parser.add_argument('waterfall_filters',
341 metavar='waterfalls',
342 type=str,
343 nargs='*',
344 help='Optional list of waterfalls to generate.')
345 parser.add_argument(
346 '--pyl-files-dir',
Garrett Beaty79339e182023-04-10 20:45:47347 type=os.path.abspath,
348 help=('Path to the directory containing the input .pyl files.'
349 ' By default the directory containing this script will be used.'))
Garrett Beaty1afaccc2020-06-25 19:58:15350 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47351 '--output-dir',
352 type=os.path.abspath,
353 help=('Path to the directory to output generated .json files.'
354 'By default, the pyl files directory will be used.'))
Chong Guee622242020-10-28 18:17:35355 parser.add_argument('--isolate-map-file',
356 metavar='PATH',
357 help='path to additional isolate map files.',
Garrett Beaty79339e182023-04-10 20:45:47358 type=os.path.abspath,
Chong Guee622242020-10-28 18:17:35359 default=[],
360 action='append',
361 dest='isolate_map_files')
Garrett Beaty1afaccc2020-06-25 19:58:15362 parser.add_argument(
363 '--infra-config-dir',
364 help='Path to the LUCI services configuration directory',
Garrett Beaty79339e182023-04-10 20:45:47365 type=os.path.abspath,
366 default=os.path.join(os.path.dirname(__file__), '..', '..', 'infra',
367 'config'))
368
Garrett Beaty1afaccc2020-06-25 19:58:15369 args = parser.parse_args(argv)
370 if args.json and not args.query:
371 parser.error(
372 "The --json flag can only be used with --query.") # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:15373
Garrett Beaty79339e182023-04-10 20:45:47374 args.pyl_files_dir = args.pyl_files_dir or THIS_DIR
375 args.output_dir = args.output_dir or args.pyl_files_dir
376
Stephanie Kim572b43c02023-04-13 14:24:13377 def absolute_file_path(filename):
Garrett Beaty79339e182023-04-10 20:45:47378 return os.path.join(args.pyl_files_dir, filename)
379
Stephanie Kim572b43c02023-04-13 14:24:13380 args.waterfalls_pyl_path = absolute_file_path('waterfalls.pyl')
Garrett Beaty96802d02023-07-07 14:18:05381 args.mixins_pyl_path = absolute_file_path('mixins.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13382 args.test_suites_pyl_path = absolute_file_path('test_suites.pyl')
383 args.test_suite_exceptions_pyl_path = absolute_file_path(
Garrett Beaty79339e182023-04-10 20:45:47384 'test_suite_exceptions.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13385 args.gn_isolate_map_pyl_path = absolute_file_path('gn_isolate_map.pyl')
386 args.variants_pyl_path = absolute_file_path('variants.pyl')
387 args.autoshard_exceptions_json_path = absolute_file_path(
388 'autoshard_exceptions.json')
Garrett Beaty79339e182023-04-10 20:45:47389
390 return args
Kenneth Russelleb60cbd22017-12-05 07:54:28391
Stephen Martinis7eb8b612018-09-21 00:17:50392 def print_line(self, line):
393 # Exists so that tests can mock
Jamie Madillcf4f8c72021-05-20 19:24:23394 print(line) # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:50395
Kenneth Russelleb60cbd22017-12-05 07:54:28396 def read_file(self, relative_path):
Garrett Beaty79339e182023-04-10 20:45:47397 with open(relative_path) as fp:
Garrett Beaty1afaccc2020-06-25 19:58:15398 return fp.read()
Kenneth Russelleb60cbd22017-12-05 07:54:28399
Garrett Beaty79339e182023-04-10 20:45:47400 def write_file(self, file_path, contents):
Peter Kastingacd55c12023-08-23 20:19:04401 with open(file_path, 'w', newline='') as fp:
Garrett Beaty79339e182023-04-10 20:45:47402 fp.write(contents)
Zhiling Huangbe008172018-03-08 19:13:11403
Joshua Hood56c673c2022-03-02 20:29:33404 # pylint: disable=inconsistent-return-statements
Garrett Beaty79339e182023-04-10 20:45:47405 def load_pyl_file(self, pyl_file_path):
Kenneth Russelleb60cbd22017-12-05 07:54:28406 try:
Garrett Beaty79339e182023-04-10 20:45:47407 return ast.literal_eval(self.read_file(pyl_file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:28408 except (SyntaxError, ValueError) as e: # pragma: no cover
Josip Sokcevic7110fb382023-06-06 01:05:29409 raise BBGenErr('Failed to parse pyl file "%s": %s' %
410 (pyl_file_path, e)) from e
Joshua Hood56c673c2022-03-02 20:29:33411 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:28412
Kenneth Russell8a386d42018-06-02 09:48:01413 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
414 # Currently it is only mandatory for bots which run GPU tests. Change these to
415 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28416 def is_android(self, tester_config):
417 return tester_config.get('os_type') == 'android'
418
Ben Pastenea9e583b2019-01-16 02:57:26419 def is_chromeos(self, tester_config):
420 return tester_config.get('os_type') == 'chromeos'
421
Chong Guc2ca5d02022-01-11 19:52:17422 def is_fuchsia(self, tester_config):
423 return tester_config.get('os_type') == 'fuchsia'
424
Brian Sheedy781c8ca42021-03-08 22:03:21425 def is_lacros(self, tester_config):
426 return tester_config.get('os_type') == 'lacros'
427
Kenneth Russell8a386d42018-06-02 09:48:01428 def is_linux(self, tester_config):
429 return tester_config.get('os_type') == 'linux'
430
Kai Ninomiya40de9f52019-10-18 21:38:49431 def is_mac(self, tester_config):
432 return tester_config.get('os_type') == 'mac'
433
434 def is_win(self, tester_config):
435 return tester_config.get('os_type') == 'win'
436
437 def is_win64(self, tester_config):
438 return (tester_config.get('os_type') == 'win' and
439 tester_config.get('browser_config') == 'release_x64')
440
Garrett Beatyffe83c4f2023-09-08 19:07:37441 def get_exception_for_test(self, test_config):
442 return self.exceptions.get(test_config['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28443
Garrett Beatyffe83c4f2023-09-08 19:07:37444 def should_run_on_tester(self, waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28445 # Currently, the only reason a test should not run on a given tester is that
446 # it's in the exceptions. (Once the GPU waterfall generation script is
447 # incorporated here, the rules will become more complex.)
Garrett Beatyffe83c4f2023-09-08 19:07:37448 exception = self.get_exception_for_test(test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28449 if not exception:
450 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28451 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28452 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28453 if remove_from:
454 if tester_name in remove_from:
455 return False
456 # TODO(kbr): this code path was added for some tests (including
457 # android_webview_unittests) on one machine (Nougat Phone
458 # Tester) which exists with the same name on two waterfalls,
459 # chromium.android and chromium.fyi; the tests are run on one
460 # but not the other. Once the bots are all uniquely named (a
461 # different ongoing project) this code should be removed.
462 # TODO(kbr): add coverage.
463 return (tester_name + ' ' + waterfall['name']
464 not in remove_from) # pragma: no cover
465 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28466
Garrett Beatyffe83c4f2023-09-08 19:07:37467 def get_test_modifications(self, test, tester_name):
468 exception = self.get_exception_for_test(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28469 if not exception:
470 return None
Nico Weber79dc5f6852018-07-13 19:38:49471 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28472
Garrett Beatyffe83c4f2023-09-08 19:07:37473 def get_test_replacements(self, test, tester_name):
474 exception = self.get_exception_for_test(test)
Brian Sheedye6ea0ee2019-07-11 02:54:37475 if not exception:
476 return None
477 return exception.get('replacements', {}).get(tester_name)
478
Kenneth Russell8a386d42018-06-02 09:48:01479 def merge_command_line_args(self, arr, prefix, splitter):
480 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01481 idx = 0
482 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01483 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01484 while idx < len(arr):
485 flag = arr[idx]
486 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01487 if flag.startswith(prefix):
488 arg = flag[prefix_len:]
489 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01490 if first_idx < 0:
491 first_idx = idx
492 else:
493 delete_current_entry = True
494 if delete_current_entry:
495 del arr[idx]
496 else:
497 idx += 1
498 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01499 arr[first_idx] = prefix + splitter.join(accumulated_args)
500 return arr
501
502 def maybe_fixup_args_array(self, arr):
503 # The incoming array of strings may be an array of command line
504 # arguments. To make it easier to turn on certain features per-bot or
505 # per-test-suite, look specifically for certain flags and merge them
506 # appropriately.
507 # --enable-features=Feature1 --enable-features=Feature2
508 # are merged to:
509 # --enable-features=Feature1,Feature2
510 # and:
511 # --extra-browser-args=arg1 --extra-browser-args=arg2
512 # are merged to:
513 # --extra-browser-args=arg1 arg2
514 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
515 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Yuly Novikov8c487e72020-10-16 20:00:29516 arr = self.merge_command_line_args(arr, '--test-launcher-filter-file=', ';')
Cameron Higgins971f0b92023-01-03 18:05:09517 arr = self.merge_command_line_args(arr, '--extra-app-args=', ',')
Kenneth Russell650995a2018-05-03 21:17:01518 return arr
519
Brian Sheedy910cda82022-07-19 11:58:34520 def substitute_magic_args(self, test_config, tester_name, tester_config):
Brian Sheedya31578e2020-05-18 20:24:36521 """Substitutes any magic substitution args present in |test_config|.
522
523 Substitutions are done in-place.
524
525 See buildbot_json_magic_substitutions.py for more information on this
526 feature.
527
528 Args:
529 test_config: A dict containing a configuration for a specific test on
530 a specific builder, e.g. the output of update_and_cleanup_test.
Brian Sheedy5f173bb2021-11-24 00:45:54531 tester_name: A string containing the name of the tester that |test_config|
532 came from.
Brian Sheedy910cda82022-07-19 11:58:34533 tester_config: A dict containing the configuration for the builder that
534 |test_config| is for.
Brian Sheedya31578e2020-05-18 20:24:36535 """
536 substituted_array = []
Brian Sheedyba13cf522022-09-13 21:00:09537 original_args = test_config.get('args', [])
538 for arg in original_args:
Brian Sheedya31578e2020-05-18 20:24:36539 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
540 function = arg.replace(
541 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
542 if hasattr(magic_substitutions, function):
543 substituted_array.extend(
Brian Sheedy910cda82022-07-19 11:58:34544 getattr(magic_substitutions, function)(test_config, tester_name,
545 tester_config))
Brian Sheedya31578e2020-05-18 20:24:36546 else:
547 raise BBGenErr(
548 'Magic substitution function %s does not exist' % function)
549 else:
550 substituted_array.append(arg)
Brian Sheedyba13cf522022-09-13 21:00:09551 if substituted_array != original_args:
Brian Sheedya31578e2020-05-18 20:24:36552 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
553
Garrett Beaty8d6708c2023-07-20 17:20:41554 def dictionary_merge(self, a, b, path=None):
Kenneth Russelleb60cbd22017-12-05 07:54:28555 """http://stackoverflow.com/questions/7204805/
556 python-dictionaries-of-dictionaries-merge
557 merges b into a
558 """
559 if path is None:
560 path = []
561 for key in b:
Garrett Beaty8d6708c2023-07-20 17:20:41562 if key not in a:
563 if b[key] is not None:
564 a[key] = b[key]
565 continue
566
567 if isinstance(a[key], dict) and isinstance(b[key], dict):
568 self.dictionary_merge(a[key], b[key], path + [str(key)])
569 elif a[key] == b[key]:
570 pass # same leaf value
571 elif isinstance(a[key], list) and isinstance(b[key], list):
Garrett Beatyade673d2023-08-04 22:00:25572 a[key] = a[key] + b[key]
573 if key.endswith('args'):
574 a[key] = self.maybe_fixup_args_array(a[key])
Garrett Beaty8d6708c2023-07-20 17:20:41575 elif b[key] is None:
576 del a[key]
577 else:
Kenneth Russelleb60cbd22017-12-05 07:54:28578 a[key] = b[key]
Garrett Beaty8d6708c2023-07-20 17:20:41579
Kenneth Russelleb60cbd22017-12-05 07:54:28580 return a
581
John Budorickab108712018-09-01 00:12:21582 def initialize_args_for_test(
583 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21584 args = []
585 args.extend(generated_test.get('args', []))
586 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22587
Kenneth Russell8a386d42018-06-02 09:48:01588 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21589 val = generated_test.pop(key, [])
590 if fn(tester_config):
591 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01592
593 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
Brian Sheedy781c8ca42021-03-08 22:03:21594 add_conditional_args('lacros_args', self.is_lacros)
Kenneth Russell8a386d42018-06-02 09:48:01595 add_conditional_args('linux_args', self.is_linux)
596 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36597 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49598 add_conditional_args('mac_args', self.is_mac)
599 add_conditional_args('win_args', self.is_win)
600 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01601
John Budorickab108712018-09-01 00:12:21602 for key in additional_arg_keys or []:
603 args.extend(generated_test.pop(key, []))
604 args.extend(tester_config.get(key, []))
605
606 if args:
607 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01608
Kenneth Russelleb60cbd22017-12-05 07:54:28609 def initialize_swarming_dictionary_for_test(self, generated_test,
610 tester_config):
611 if 'swarming' not in generated_test:
612 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28613 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
614 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38615 'can_use_on_swarming_builders': tester_config.get('use_swarming',
616 True)
Dirk Pranke81ff51c2017-12-09 19:24:28617 })
Kenneth Russelleb60cbd22017-12-05 07:54:28618 if 'swarming' in tester_config:
Kenneth Russelleb60cbd22017-12-05 07:54:28619 self.dictionary_merge(generated_test['swarming'],
620 tester_config['swarming'])
Brian Sheedybc984e242021-04-21 23:44:51621 # Apply any platform-specific Swarming dimensions after the generic ones.
Kenneth Russelleb60cbd22017-12-05 07:54:28622 if 'android_swarming' in generated_test:
623 if self.is_android(tester_config): # pragma: no cover
624 self.dictionary_merge(
625 generated_test['swarming'],
626 generated_test['android_swarming']) # pragma: no cover
627 del generated_test['android_swarming'] # pragma: no cover
Brian Sheedybc984e242021-04-21 23:44:51628 if 'chromeos_swarming' in generated_test:
629 if self.is_chromeos(tester_config): # pragma: no cover
630 self.dictionary_merge(
631 generated_test['swarming'],
632 generated_test['chromeos_swarming']) # pragma: no cover
633 del generated_test['chromeos_swarming'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28634
635 def clean_swarming_dictionary(self, swarming_dict):
636 # Clean out redundant entries from a test's "swarming" dictionary.
637 # This is really only needed to retain 100% parity with the
638 # handwritten JSON files, and can be removed once all the files are
639 # autogenerated.
640 if 'shards' in swarming_dict:
641 if swarming_dict['shards'] == 1: # pragma: no cover
642 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24643 if 'hard_timeout' in swarming_dict:
644 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
645 del swarming_dict['hard_timeout'] # pragma: no cover
Garrett Beatybb18d532023-06-26 22:16:33646 del swarming_dict['can_use_on_swarming_builders']
Kenneth Russelleb60cbd22017-12-05 07:54:28647
Stephen Martinis0382bc12018-09-17 22:29:07648 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
649 waterfall):
650 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01651 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07652 test, waterfall, tester_name, tester_config)
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 Beatyffe83c4f2023-09-08 19:07:37655 modifications = self.get_test_modifications(test, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28656 if modifications:
657 test = self.dictionary_merge(test, modifications)
Garrett Beatybfeff8f2023-06-16 18:57:25658 if (swarming_dict := test.get('swarming')) is not None:
Garrett Beatybb18d532023-06-26 22:16:33659 if swarming_dict.get('can_use_on_swarming_builders'):
Garrett Beatybfeff8f2023-06-16 18:57:25660 self.clean_swarming_dictionary(swarming_dict)
661 else:
662 del test['swarming']
Ben Pastenee012aea42019-05-14 22:32:28663 # Ensure all Android Swarming tests run only on userdebug builds if another
664 # build type was not specified.
665 if 'swarming' in test and self.is_android(tester_config):
Garrett Beatyade673d2023-08-04 22:00:25666 dimensions = test.get('swarming', {}).get('dimensions', {})
667 if (dimensions.get('os') == 'Android'
668 and not dimensions.get('device_os_type')):
669 dimensions['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37670 self.replace_test_args(test, test_name, tester_name)
Garrett Beatyafd33e0f2023-06-23 20:47:57671 if 'args' in test and not test['args']:
672 test.pop('args')
Ben Pastenee012aea42019-05-14 22:32:28673
Kenneth Russelleb60cbd22017-12-05 07:54:28674 return test
675
Brian Sheedye6ea0ee2019-07-11 02:54:37676 def replace_test_args(self, test, test_name, tester_name):
Garrett Beatyffe83c4f2023-09-08 19:07:37677 replacements = self.get_test_replacements(test, tester_name) or {}
Brian Sheedye6ea0ee2019-07-11 02:54:37678 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23679 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37680 if key not in valid_replacement_keys:
681 raise BBGenErr(
682 'Given replacement key %s for %s on %s is not in the list of valid '
683 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23684 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37685 found_key = False
686 for i, test_key in enumerate(test.get(key, [])):
687 # Handle both the key/value being replaced being defined as two
688 # separate items or as key=value.
689 if test_key == replacement_key:
690 found_key = True
691 # Handle flags without values.
692 if replacement_val == None:
693 del test[key][i]
694 else:
695 test[key][i+1] = replacement_val
696 break
Joshua Hood56c673c2022-03-02 20:29:33697 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37698 found_key = True
699 if replacement_val == None:
700 del test[key][i]
701 else:
702 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
703 break
704 if not found_key:
705 raise BBGenErr('Could not find %s in existing list of values for key '
706 '%s in %s on %s' % (replacement_key, key, test_name,
707 tester_name))
708
Shenghua Zhangaba8bad2018-02-07 02:12:09709 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05710 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26711 True):
712 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06713 # are targeting CrOS hardware and so need the special trigger script.
Garrett Beatyade673d2023-08-04 22:00:25714 if 'device_type' in test.get('swarming', {}).get('dimensions', {}):
Ben Pastenea9e583b2019-01-16 02:57:26715 test['trigger_script'] = {
716 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
717 }
Shenghua Zhangaba8bad2018-02-07 02:12:09718
Garrett Beatyffe83c4f2023-09-08 19:07:37719 def add_android_presentation_args(self, tester_config, result):
Ben Pastene858f4be2019-01-09 23:52:09720 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38721 bucket = tester_config.get('results_bucket', 'chromium-result-details')
722 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09723 if (result['swarming']['can_use_on_swarming_builders'] and not
724 tester_config.get('skip_merge_script', False)):
725 result['merge'] = {
Garrett Beatyffe83c4f2023-09-08 19:07:37726 'args': [
727 '--bucket',
728 bucket,
729 '--test-name',
730 result['name'],
731 ],
732 'script': ('//build/android/pylib/results/presentation/'
733 'test_results_presentation.py'),
Ben Pastene858f4be2019-01-09 23:52:09734 }
Ben Pastene858f4be2019-01-09 23:52:09735 if not tester_config.get('skip_output_links', False):
736 result['swarming']['output_links'] = [
737 {
738 'link': [
739 'https://luci-logdog.appspot.com/v/?s',
740 '=android%2Fswarming%2Flogcats%2F',
741 '${TASK_ID}%2F%2B%2Funified_logcats',
742 ],
743 'name': 'shard #${SHARD_INDEX} logcats',
744 },
745 ]
746 if args:
747 result['args'] = args
748
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)
Kenneth Russelleb60cbd22017-12-05 07:54:28757 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21758
759 self.initialize_args_for_test(
760 result, tester_config, additional_arg_keys=['gtest_args'])
Jamie Madilla8be0d72020-10-02 05:24:04761 if self.is_android(tester_config) and tester_config.get(
Yuly Novikov26dd47052021-02-11 00:57:14762 'use_swarming', True):
763 if not test_config.get('use_isolated_scripts_api', False):
764 # TODO(https://crbug.com/1137998) make Android presentation work with
765 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37766 self.add_android_presentation_args(tester_config, result)
Yuly Novikov26dd47052021-02-11 00:57:14767 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42768
Stephen Martinis0382bc12018-09-17 22:29:07769 result = self.update_and_cleanup_test(
770 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09771 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34772 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43773
Garrett Beatybb18d532023-06-26 22:16:33774 if 'swarming' in result and not result.get('merge'):
Jamie Madilla8be0d72020-10-02 05:24:04775 if test_config.get('use_isolated_scripts_api', False):
776 merge_script = 'standard_isolated_script_merge'
777 else:
778 merge_script = 'standard_gtest_merge'
779
Stephen Martinisbc7b7772019-05-01 22:01:43780 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04781 'script': '//testing/merge_scripts/%s.py' % merge_script,
Stephen Martinisbc7b7772019-05-01 22:01:43782 }
Kenneth Russelleb60cbd22017-12-05 07:54:28783 return result
784
785 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
786 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37787 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28788 return None
789 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37790 # Use test_name here instead of test['name'] because test['name'] will be
791 # modified with the variant identifier in a matrix compound suite
Garrett Beatydca3d882023-09-14 23:50:32792 result.setdefault('test', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28793 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01794 self.initialize_args_for_test(result, tester_config)
Yuly Novikov26dd47052021-02-11 00:57:14795 if self.is_android(tester_config) and tester_config.get(
796 'use_swarming', True):
797 if tester_config.get('use_android_presentation', False):
798 # TODO(https://crbug.com/1137998) make Android presentation work with
799 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37800 self.add_android_presentation_args(tester_config, result)
Stephen Martinis0382bc12018-09-17 22:29:07801 result = self.update_and_cleanup_test(
802 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09803 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34804 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17805
Garrett Beatybb18d532023-06-26 22:16:33806 if 'swarming' in result and not result.get('merge'):
Stephen Martinisf50047062019-05-06 22:26:17807 # TODO(https://crbug.com/958376): Consider adding the ability to not have
808 # this default.
809 result['merge'] = {
810 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
Stephen Martinisf50047062019-05-06 22:26:17811 }
Kenneth Russelleb60cbd22017-12-05 07:54:28812 return result
813
814 def generate_script_test(self, waterfall, tester_name, tester_config,
815 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44816 # TODO(https://crbug.com/953072): Remove this check whenever a better
817 # long-term solution is implemented.
818 if (waterfall.get('forbid_script_tests', False) or
819 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
820 raise BBGenErr('Attempted to generate a script test on tester ' +
821 tester_name + ', which explicitly forbids script tests')
Garrett Beatyffe83c4f2023-09-08 19:07:37822 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28823 return None
824 result = {
Garrett Beatyffe83c4f2023-09-08 19:07:37825 'name': test_config['name'],
826 'script': test_config['script'],
Kenneth Russelleb60cbd22017-12-05 07:54:28827 }
Stephen Martinis0382bc12018-09-17 22:29:07828 result = self.update_and_cleanup_test(
829 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34830 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28831 return result
832
833 def generate_junit_test(self, waterfall, tester_name, tester_config,
834 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37835 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28836 return None
John Budorickdef6acb2019-09-17 22:51:09837 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37838 # Use test_name here instead of test['name'] because test['name'] will be
839 # modified with the variant identifier in a matrix compound suite
840 result.setdefault('test', test_name)
John Budorickdef6acb2019-09-17 22:51:09841 self.initialize_args_for_test(result, tester_config)
842 result = self.update_and_cleanup_test(
843 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34844 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28845 return result
846
Xinan Lin05fb9c1752020-12-17 00:15:52847 def generate_skylab_test(self, waterfall, tester_name, tester_config,
848 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37849 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Xinan Lin05fb9c1752020-12-17 00:15:52850 return None
851 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37852 # Use test_name here instead of test['name'] because test['name'] will be
853 # modified with the variant identifier in a matrix compound suite
854 result['test'] = test_name
Xinan Lin05fb9c1752020-12-17 00:15:52855 self.initialize_args_for_test(result, tester_config)
856 result = self.update_and_cleanup_test(result, test_name, tester_name,
857 tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34858 self.substitute_magic_args(result, tester_name, tester_config)
Xinan Lin05fb9c1752020-12-17 00:15:52859 return result
860
Garrett Beaty65d44222023-08-01 17:22:11861 def substitute_gpu_args(self, tester_config, test, args):
Kenneth Russell8a386d42018-06-02 09:48:01862 substitutions = {
863 # Any machine in waterfalls.pyl which desires to run GPU tests
864 # must provide the os_type key.
865 'os_type': tester_config['os_type'],
866 'gpu_vendor_id': '0',
867 'gpu_device_id': '0',
868 }
Garrett Beatyade673d2023-08-04 22:00:25869 dimensions = test.get('swarming', {}).get('dimensions', {})
870 if 'gpu' in dimensions:
871 # First remove the driver version, then split into vendor and device.
872 gpu = dimensions['gpu']
873 if gpu != 'none':
874 gpu = gpu.split('-')[0].split(':')
875 substitutions['gpu_vendor_id'] = gpu[0]
876 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01877 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
878
879 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30880 test_name, test_config, is_android_webview,
881 is_cast_streaming):
Kenneth Russell8a386d42018-06-02 09:48:01882 # These are all just specializations of isolated script tests with
883 # a bunch of boilerplate command line arguments added.
884
885 # The step name must end in 'test' or 'tests' in order for the
886 # results to automatically show up on the flakiness dashboard.
887 # (At least, this was true some time ago.) Continue to use this
888 # naming convention for the time being to minimize changes.
Garrett Beaty235c1412023-08-29 20:26:29889 #
890 # test name is the name of the test without the variant ID added
891 if not (test_name.endswith('test') or test_name.endswith('tests')):
892 raise BBGenErr(
893 f'telemetry test names must end with test or tests, got {test_name}')
Garrett Beatyffe83c4f2023-09-08 19:07:37894 result = self.generate_isolated_script_test(waterfall, tester_name,
895 tester_config, test_name,
896 test_config)
Kenneth Russell8a386d42018-06-02 09:48:01897 if not result:
898 return None
Garrett Beatydca3d882023-09-14 23:50:32899 result['test'] = test_config.get('test') or self.get_default_isolate_name(
900 tester_config, is_android_webview)
Chan Liab7d8dd82020-04-24 23:42:19901
Chan Lia3ad1502020-04-28 05:32:11902 # Populate test_id_prefix.
Garrett Beatydca3d882023-09-14 23:50:32903 gn_entry = self.gn_isolate_map[result['test']]
Chan Li17d969f92020-07-10 00:50:03904 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19905
Kenneth Russell8a386d42018-06-02 09:48:01906 args = result.get('args', [])
Garrett Beatyffe83c4f2023-09-08 19:07:37907 # Use test_name here instead of test['name'] because test['name'] will be
908 # modified with the variant identifier in a matrix compound suite
Kenneth Russell8a386d42018-06-02 09:48:01909 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14910
erikchen6da2d9b2018-08-03 23:01:14911 # These tests upload and download results from cloud storage and therefore
912 # aren't idempotent yet. https://crbug.com/549140.
Garrett Beatybfeff8f2023-06-16 18:57:25913 if 'swarming' in result:
914 result['swarming']['idempotent'] = False
erikchen6da2d9b2018-08-03 23:01:14915
Fabrice de Ganscbd655f2022-08-04 20:15:30916 browser = ''
917 if is_cast_streaming:
918 browser = 'cast-streaming-shell'
919 elif is_android_webview:
920 browser = 'android-webview-instrumentation'
921 else:
922 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:52923
Greg Thompsoncec7d8d2023-01-10 19:11:53924 extra_browser_args = []
925
Brian Sheedy4053a702020-07-28 02:09:52926 # Most platforms require --enable-logging=stderr to get useful browser logs.
927 # However, this actively messes with logging on CrOS (because Chrome's
928 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
929 # in order to see JavaScript console messages. See
930 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
Greg Thompsoncec7d8d2023-01-10 19:11:53931 if self.is_chromeos(tester_config):
932 extra_browser_args.append('--log-level=0')
933 elif not self.is_fuchsia(tester_config) or browser != 'fuchsia-chrome':
934 # Stderr logging is not needed for Chrome browser on Fuchsia, as ordinary
935 # logging via syslog is captured.
936 extra_browser_args.append('--enable-logging=stderr')
937
938 # --expose-gc allows the WebGL conformance tests to more reliably
939 # reproduce GC-related bugs in the V8 bindings.
940 extra_browser_args.append('--js-flags=--expose-gc')
Brian Sheedy4053a702020-07-28 02:09:52941
Kenneth Russell8a386d42018-06-02 09:48:01942 args = [
Bo Liu555a0f92019-03-29 12:11:56943 test_to_run,
944 '--show-stdout',
945 '--browser=%s' % browser,
946 # --passthrough displays more of the logging in Telemetry when
947 # run via typ, in particular some of the warnings about tests
948 # being expected to fail, but passing.
949 '--passthrough',
950 '-v',
Brian Sheedy814e0482022-10-03 23:24:12951 '--stable-jobs',
Greg Thompsoncec7d8d2023-01-10 19:11:53952 '--extra-browser-args=%s' % ' '.join(extra_browser_args),
Brian Sheedy997e4802023-10-18 02:28:13953 '--enforce-browser-version',
Kenneth Russell8a386d42018-06-02 09:48:01954 ] + args
Garrett Beatybfeff8f2023-06-16 18:57:25955 result['args'] = self.maybe_fixup_args_array(
Garrett Beaty65d44222023-08-01 17:22:11956 self.substitute_gpu_args(tester_config, result, args))
Kenneth Russell8a386d42018-06-02 09:48:01957 return result
958
Brian Sheedyf74819b2021-06-04 01:38:38959 def get_default_isolate_name(self, tester_config, is_android_webview):
960 if self.is_android(tester_config):
961 if is_android_webview:
962 return 'telemetry_gpu_integration_test_android_webview'
963 return (
964 'telemetry_gpu_integration_test' +
965 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:33966 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:17967 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:33968 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:38969
Kenneth Russelleb60cbd22017-12-05 07:54:28970 def get_test_generator_map(self):
971 return {
Bo Liu555a0f92019-03-29 12:11:56972 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30973 GPUTelemetryTestGenerator(self, is_android_webview=True),
974 'cast_streaming_tests':
975 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:56976 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30977 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56978 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30979 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56980 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:30981 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56982 'junit_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30983 JUnitGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56984 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:30985 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:52986 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30987 SkylabGenerator(self),
Brian Sheedyb6491ba2022-09-26 20:49:49988 'skylab_gpu_telemetry_tests':
989 SkylabGPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28990 }
991
Kenneth Russell8a386d42018-06-02 09:48:01992 def get_test_type_remapper(self):
993 return {
Fabrice de Gans223272482022-08-08 16:56:57994 # These are a specialization of isolated_scripts with a bunch of
995 # boilerplate command line arguments added to each one.
996 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
997 'cast_streaming_tests': 'isolated_scripts',
998 'gpu_telemetry_tests': 'isolated_scripts',
Brian Sheedyb6491ba2022-09-26 20:49:49999 # These are the same as existing test types, just configured to run
1000 # in Skylab instead of via normal swarming.
1001 'skylab_gpu_telemetry_tests': 'skylab_tests',
Kenneth Russell8a386d42018-06-02 09:48:011002 }
1003
Jeff Yoon67c3e832020-02-08 07:39:381004 def check_composition_type_test_suites(self, test_type,
1005 additional_validators=None):
1006 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1007 validators = [check_compound_references,
1008 check_basic_references,
1009 check_conflicting_definitions]
1010 if additional_validators:
1011 validators += additional_validators
1012
1013 target_suites = self.test_suites.get(test_type, {})
1014 other_test_type = ('compound_suites'
1015 if test_type == 'matrix_compound_suites'
1016 else 'matrix_compound_suites')
1017 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011018 basic_suites = self.test_suites.get('basic_suites', {})
1019
Jamie Madillcf4f8c72021-05-20 19:24:231020 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011021 if suite in basic_suites:
1022 raise BBGenErr('%s names may not duplicate basic test suite names '
1023 '(error found while processsing %s)'
1024 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011025
Jeff Yoon67c3e832020-02-08 07:39:381026 seen_tests = {}
1027 for sub_suite in suite_def:
1028 for validator in validators:
1029 validator(
1030 basic_suites=basic_suites,
1031 other_test_suites=other_suites,
1032 seen_tests=seen_tests,
1033 sub_suite=sub_suite,
1034 suite=suite,
1035 suite_def=suite_def,
1036 target_test_suites=target_suites,
1037 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051038 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381039 )
Kenneth Russelleb60cbd22017-12-05 07:54:281040
Stephen Martinis54d64ad2018-09-21 22:16:201041 def flatten_test_suites(self):
1042 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011043 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1044 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231045 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011046 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201047 self.test_suites = new_test_suites
1048
Chan Lia3ad1502020-04-28 05:32:111049 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231050 for suite in self.test_suites['basic_suites'].values():
1051 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561052 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411053
Garrett Beatydca3d882023-09-14 23:50:321054 isolate_name = test.get('test') or key
Nodir Turakulovfce34292019-12-18 17:05:411055 gn_entry = self.gn_isolate_map.get(isolate_name)
1056 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281057 label = gn_entry['label']
1058
1059 if label.count(':') != 1:
1060 raise BBGenErr(
1061 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1062 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1063 (label, isolate_name))
1064 if label.split(':')[1] != isolate_name:
1065 raise BBGenErr(
1066 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1067 ' label "%s" see http://crbug.com/1071091 for details.' %
1068 (isolate_name, label))
1069
Chan Lia3ad1502020-04-28 05:32:111070 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411071 else: # pragma: no cover
1072 # Some tests do not have an entry gn_isolate_map.pyl, such as
1073 # telemetry tests.
1074 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
1075 pass
1076
Kenneth Russelleb60cbd22017-12-05 07:54:281077 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011078 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201079
Jeff Yoon8154e582019-12-03 23:30:011080 compound_suites = self.test_suites.get('compound_suites', {})
1081 # check_composition_type_test_suites() checks that all basic suites
1082 # referenced by compound suites exist.
1083 basic_suites = self.test_suites.get('basic_suites')
1084
Jamie Madillcf4f8c72021-05-20 19:24:231085 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011086 # Resolve this to a dictionary.
1087 full_suite = {}
1088 for entry in value:
1089 suite = basic_suites[entry]
1090 full_suite.update(suite)
1091 compound_suites[name] = full_suite
1092
Jeff Yoon85fb8df2020-08-20 16:47:431093 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381094 """ Merge variant-defined configurations to each test case definition in a
1095 test suite.
1096
1097 The output maps a unique test name to an array of configurations because
1098 there may exist more than one definition for a test name using variants. The
1099 test name is referenced while mapping machines to test suites, so unpacking
1100 the array is done by the generators.
1101
1102 Args:
1103 basic_test_definition: a {} defined test suite in the format
1104 test_name:test_config
1105 variants: an [] of {} defining configurations to be applied to each test
1106 case in the basic test_definition
1107
1108 Return:
1109 a {} of test_name:[{}], where each {} is a merged configuration
1110 """
1111
1112 # Each test in a basic test suite will have a definition per variant.
1113 test_suite = {}
Garrett Beaty8d6708c2023-07-20 17:20:411114 for variant in variants:
1115 # Unpack the variant from variants.pyl if it's string based.
1116 if isinstance(variant, str):
1117 variant = self.variants[variant]
Jeff Yoonda581c32020-03-06 03:56:051118
Garrett Beaty8d6708c2023-07-20 17:20:411119 # If 'enabled' is set to False, we will not use this variant; otherwise if
1120 # the variant doesn't include 'enabled' variable or 'enabled' is set to
1121 # True, we will use this variant
1122 if not variant.get('enabled', True):
1123 continue
Jeff Yoon67c3e832020-02-08 07:39:381124
Garrett Beaty8d6708c2023-07-20 17:20:411125 # Make a shallow copy of the variant to remove variant-specific fields,
1126 # leaving just mixin fields
1127 variant = copy.copy(variant)
1128 variant.pop('enabled', None)
1129 identifier = variant.pop('identifier')
1130 variant_mixins = variant.pop('mixins', [])
1131 variant_skylab = variant.pop('skylab', {})
Jeff Yoon67c3e832020-02-08 07:39:381132
Garrett Beaty8d6708c2023-07-20 17:20:411133 for test_name, test_config in basic_test_definition.items():
1134 new_test = self.apply_mixin(variant, test_config)
Jeff Yoon67c3e832020-02-08 07:39:381135
Garrett Beaty8d6708c2023-07-20 17:20:411136 new_test['mixins'] = (test_config.get('mixins', []) + variant_mixins +
1137 mixins)
Xinan Lin05fb9c1752020-12-17 00:15:521138
Jeff Yoon67c3e832020-02-08 07:39:381139 # The identifier is used to make the name of the test unique.
1140 # Generators in the recipe uniquely identify a test by it's name, so we
1141 # don't want to have the same name for each variant.
Garrett Beaty235c1412023-08-29 20:26:291142 new_test['name'] = f'{test_name} {identifier}'
Ben Pastene5f231cf22022-05-05 18:03:071143
1144 # Attach the variant identifier to the test config so downstream
1145 # generators can make modifications based on the original name. This
1146 # is mainly used in generate_gpu_telemetry_test().
Garrett Beaty8d6708c2023-07-20 17:20:411147 new_test['variant_id'] = identifier
Ben Pastene5f231cf22022-05-05 18:03:071148
Garrett Beaty8d6708c2023-07-20 17:20:411149 for k, v in variant_skylab.items():
Sven Zheng22ba6312023-10-16 22:59:351150 # cros_chrome_version is the ash chrome version in the cros img in the
1151 # variant of cros_board. We don't want to include it in the final json
1152 # files; so remove it.
Garrett Beaty8d6708c2023-07-20 17:20:411153 if k != 'cros_chrome_version':
1154 new_test[k] = v
1155
Sven Zheng22ba6312023-10-16 22:59:351156 # For skylab, we need to pop the correct `autotest_name`. This field
1157 # defines what wrapper we use in OS infra. e.g. for gtest it's
1158 # https://source.chromium.org/chromiumos/chromiumos/codesearch/+/main:src/third_party/autotest/files/server/site_tests/chromium/chromium.py
1159 if variant_skylab and 'autotest_name' not in new_test:
1160 if 'tast_expr' in test_config:
1161 if 'lacros' in test_config['name']:
1162 new_test['autotest_name'] = 'tast.lacros-from-gcs'
1163 else:
1164 new_test['autotest_name'] = 'tast.chrome-from-gcs'
1165 elif 'benchmark' in test_config:
1166 new_test['autotest_name'] = 'chromium_Telemetry'
1167 else:
1168 new_test['autotest_name'] = 'chromium'
1169
Garrett Beaty8d6708c2023-07-20 17:20:411170 test_suite.setdefault(test_name, []).append(new_test)
1171
Jeff Yoon67c3e832020-02-08 07:39:381172 return test_suite
1173
Jeff Yoon8154e582019-12-03 23:30:011174 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381175 self.check_composition_type_test_suites('matrix_compound_suites',
1176 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011177
1178 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381179 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011180 # referenced by matrix suites exist.
1181 basic_suites = self.test_suites.get('basic_suites')
1182
Garrett Beaty235c1412023-08-29 20:26:291183 for matrix_suite_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011184 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381185
Jamie Madillcf4f8c72021-05-20 19:24:231186 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381187 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1188
Garrett Beaty235c1412023-08-29 20:26:291189 def update_tests(expanded):
1190 for test_name, new_tests in expanded.items():
1191 if not isinstance(new_tests, list):
1192 new_tests = [new_tests]
1193 tests_for_name = full_suite.setdefault(test_name, [])
1194 for t in new_tests:
1195 if t not in tests_for_name:
1196 tests_for_name.append(t)
1197
Garrett Beaty60a7b2a2023-09-13 23:00:401198 if (variants := mtx_test_suite_config.get('variants')):
Jeff Yoon85fb8df2020-08-20 16:47:431199 mixins = mtx_test_suite_config.get('mixins', [])
Garrett Beaty60a7b2a2023-09-13 23:00:401200 result = self.resolve_variants(basic_test_def, variants, mixins)
Garrett Beaty235c1412023-08-29 20:26:291201 update_tests(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271202 else:
1203 suite = basic_suites[test_suite]
Garrett Beaty235c1412023-08-29 20:26:291204 update_tests(suite)
1205 matrix_compound_suites[matrix_suite_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281206
1207 def link_waterfalls_to_test_suites(self):
1208 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231209 for tester_name, tester in waterfall['machines'].items():
1210 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281211 if not value in self.test_suites:
1212 # Hard / impossible to cover this in the unit test.
1213 raise self.unknown_test_suite(
1214 value, tester_name, waterfall['name']) # pragma: no cover
1215 tester['test_suites'][suite] = self.test_suites[value]
1216
1217 def load_configuration_files(self):
Garrett Beaty79339e182023-04-10 20:45:471218 self.waterfalls = self.load_pyl_file(self.args.waterfalls_pyl_path)
1219 self.test_suites = self.load_pyl_file(self.args.test_suites_pyl_path)
1220 self.exceptions = self.load_pyl_file(
1221 self.args.test_suite_exceptions_pyl_path)
1222 self.mixins = self.load_pyl_file(self.args.mixins_pyl_path)
1223 self.gn_isolate_map = self.load_pyl_file(self.args.gn_isolate_map_pyl_path)
Chong Guee622242020-10-28 18:17:351224 for isolate_map in self.args.isolate_map_files:
1225 isolate_map = self.load_pyl_file(isolate_map)
1226 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1227 if duplicates:
1228 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1229 ', '.join(duplicates))
1230 self.gn_isolate_map.update(isolate_map)
1231
Garrett Beaty79339e182023-04-10 20:45:471232 self.variants = self.load_pyl_file(self.args.variants_pyl_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281233
1234 def resolve_configuration_files(self):
Garrett Beaty235c1412023-08-29 20:26:291235 self.resolve_test_names()
Garrett Beatydca3d882023-09-14 23:50:321236 self.resolve_isolate_names()
Garrett Beaty65d44222023-08-01 17:22:111237 self.resolve_dimension_sets()
Chan Lia3ad1502020-04-28 05:32:111238 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281239 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011240 self.resolve_matrix_compound_test_suites()
1241 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281242 self.link_waterfalls_to_test_suites()
1243
Garrett Beaty235c1412023-08-29 20:26:291244 def resolve_test_names(self):
1245 for suite_name, suite in self.test_suites.get('basic_suites').items():
1246 for test_name, test in suite.items():
1247 if 'name' in test:
1248 raise BBGenErr(
1249 f'The name field is set in test {test_name} in basic suite '
1250 f'{suite_name}, this is not supported, the test name is the key '
1251 'within the basic suite')
Garrett Beatyffe83c4f2023-09-08 19:07:371252 # When a test is expanded with variants, this will be overwritten, but
1253 # this ensures every test definition has the name field set
1254 test['name'] = test_name
Garrett Beaty235c1412023-08-29 20:26:291255
Garrett Beatydca3d882023-09-14 23:50:321256 def resolve_isolate_names(self):
1257 for suite_name, suite in self.test_suites.get('basic_suites').items():
1258 for test_name, test in suite.items():
1259 if 'isolate_name' in test:
1260 raise BBGenErr(
1261 f'The isolate_name field is set in test {test_name} in basic '
1262 f'suite {suite_name}, the test field should be used instead')
1263
Garrett Beaty65d44222023-08-01 17:22:111264 def resolve_dimension_sets(self):
Garrett Beaty65d44222023-08-01 17:22:111265
1266 def definitions():
1267 for suite_name, suite in self.test_suites.get('basic_suites', {}).items():
1268 for test_name, test in suite.items():
1269 yield test, f'test {test_name} in basic suite {suite_name}'
1270
1271 for mixin_name, mixin in self.mixins.items():
1272 yield mixin, f'mixin {mixin_name}'
1273
1274 for waterfall in self.waterfalls:
1275 for builder_name, builder in waterfall.get('machines', {}).items():
1276 yield (
1277 builder,
1278 f'builder {builder_name} in waterfall {waterfall["name"]}',
1279 )
1280
1281 for test_name, exceptions in self.exceptions.items():
1282 modifications = exceptions.get('modifications', {})
1283 for builder_name, mods in modifications.items():
1284 yield (
1285 mods,
1286 f'exception for test {test_name} on builder {builder_name}',
1287 )
1288
1289 for definition, location in definitions():
1290 for swarming_attr in (
1291 'swarming',
1292 'android_swarming',
1293 'chromeos_swarming',
1294 ):
1295 if (swarming :=
1296 definition.get(swarming_attr)) and 'dimension_sets' in swarming:
Garrett Beatyade673d2023-08-04 22:00:251297 raise BBGenErr(
1298 f'dimension_sets is no longer supported (set in {location}),'
1299 ' instead, use set dimensions to a single dict')
Garrett Beaty65d44222023-08-01 17:22:111300
Nico Weberd18b8962018-05-16 19:39:381301 def unknown_bot(self, bot_name, waterfall_name):
1302 return BBGenErr(
1303 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1304
Kenneth Russelleb60cbd22017-12-05 07:54:281305 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1306 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381307 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281308 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1309
1310 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1311 return BBGenErr(
1312 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1313 ' on waterfall ' + waterfall_name)
1314
Stephen Martinisb72f6d22018-10-04 23:29:011315 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071316 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321317
1318 Checks in the waterfall, builder, and test objects for mixins.
1319 """
1320 def valid_mixin(mixin_name):
1321 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011322 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321323 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381324
Stephen Martinisb6a50492018-09-12 23:59:321325 def must_be_list(mixins, typ, name):
1326 """Asserts that given mixins are a list."""
1327 if not isinstance(mixins, list):
1328 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1329
Garrett Beatyffe83c4f2023-09-08 19:07:371330 test_name = test['name']
Brian Sheedy7658c982020-01-08 02:27:581331 remove_mixins = set()
1332 if 'remove_mixins' in builder:
1333 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1334 for rm in builder['remove_mixins']:
1335 valid_mixin(rm)
1336 remove_mixins.add(rm)
1337 if 'remove_mixins' in test:
1338 must_be_list(test['remove_mixins'], 'test', test_name)
1339 for rm in test['remove_mixins']:
1340 valid_mixin(rm)
1341 remove_mixins.add(rm)
1342 del test['remove_mixins']
1343
Stephen Martinisb72f6d22018-10-04 23:29:011344 if 'mixins' in waterfall:
1345 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1346 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581347 if mixin in remove_mixins:
1348 continue
Stephen Martinisb6a50492018-09-12 23:59:321349 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531350 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321351
Stephen Martinisb72f6d22018-10-04 23:29:011352 if 'mixins' in builder:
1353 must_be_list(builder['mixins'], 'builder', builder_name)
1354 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581355 if mixin in remove_mixins:
1356 continue
Stephen Martinisb6a50492018-09-12 23:59:321357 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531358 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321359
Stephen Martinisb72f6d22018-10-04 23:29:011360 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071361 return test
1362
Stephen Martinisb72f6d22018-10-04 23:29:011363 must_be_list(test['mixins'], 'test', test_name)
1364 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581365 # We don't bother checking if the given mixin is in remove_mixins here
1366 # since this is already the lowest level, so if a mixin is added here that
1367 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071368 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531369 test = self.apply_mixin(self.mixins[mixin], test, builder)
Jeff Yoon67c3e832020-02-08 07:39:381370 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071371 return test
Stephen Martinisb6a50492018-09-12 23:59:321372
Garrett Beaty8d6708c2023-07-20 17:20:411373 def apply_mixin(self, mixin, test, builder=None):
Stephen Martinisb72f6d22018-10-04 23:29:011374 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321375
Garrett Beaty4c35b142023-06-23 21:01:231376 A mixin is applied by copying all fields from the mixin into the
1377 test with the following exceptions:
1378 * For the various *args keys, the test's existing value (an empty
1379 list if not present) will be extended with the mixin's value.
1380 * The sub-keys of the swarming value will be copied to the test's
1381 swarming value with the following exceptions:
Garrett Beatyade673d2023-08-04 22:00:251382 * For the named_caches sub-keys, the test's existing value (an
1383 empty list if not present) will be extended with the mixin's
1384 value.
1385 * For the dimensions sub-key, the tests's existing value (an empty
1386 dict if not present) will be updated with the mixin's value.
Stephen Martinisb6a50492018-09-12 23:59:321387 """
Garrett Beaty4c35b142023-06-23 21:01:231388
Stephen Martinisb6a50492018-09-12 23:59:321389 new_test = copy.deepcopy(test)
1390 mixin = copy.deepcopy(mixin)
Garrett Beaty8d6708c2023-07-20 17:20:411391
1392 if 'description' in mixin:
1393 description = []
1394 if 'description' in new_test:
1395 description.append(new_test['description'])
1396 description.append(mixin.pop('description'))
1397 new_test['description'] = '\n'.join(description)
1398
Stephen Martinisb72f6d22018-10-04 23:29:011399 if 'swarming' in mixin:
1400 swarming_mixin = mixin['swarming']
1401 new_test.setdefault('swarming', {})
Stephen Martinisb72f6d22018-10-04 23:29:011402 if 'dimensions' in swarming_mixin:
Garrett Beatyade673d2023-08-04 22:00:251403 new_test['swarming'].setdefault('dimensions', {}).update(
1404 swarming_mixin.pop('dimensions'))
Garrett Beaty4c35b142023-06-23 21:01:231405 if 'named_caches' in swarming_mixin:
1406 new_test['swarming'].setdefault('named_caches', []).extend(
1407 swarming_mixin['named_caches'])
1408 del swarming_mixin['named_caches']
Stephen Martinisb72f6d22018-10-04 23:29:011409 # python dict update doesn't do recursion at all. Just hard code the
1410 # nested update we need (mixin['swarming'] shouldn't clobber
1411 # test['swarming'], but should update it).
1412 new_test['swarming'].update(swarming_mixin)
1413 del mixin['swarming']
1414
Garrett Beaty4c35b142023-06-23 21:01:231415 # Array so we can assign to it in a nested scope.
1416 args_need_fixup = ['args' in mixin]
1417
1418 for a in (
1419 'args',
1420 'precommit_args',
1421 'non_precommit_args',
1422 'desktop_args',
1423 'lacros_args',
1424 'linux_args',
1425 'android_args',
1426 'chromeos_args',
1427 'mac_args',
1428 'win_args',
1429 'win64_args',
1430 ):
1431 if (value := mixin.pop(a, None)) is None:
1432 continue
1433 if not isinstance(value, list):
1434 raise BBGenErr(f'"{a}" must be a list')
1435 new_test.setdefault(a, []).extend(value)
1436
Garrett Beaty4c35b142023-06-23 21:01:231437 args = new_test.get('args', [])
Austin Eng148d9f0f2022-02-08 19:18:531438
Garrett Beaty4c35b142023-06-23 21:01:231439 def add_conditional_args(key, fn):
Garrett Beaty8d6708c2023-07-20 17:20:411440 if builder is None:
1441 return
Garrett Beaty4c35b142023-06-23 21:01:231442 val = new_test.pop(key, [])
1443 if val and fn(builder):
1444 args.extend(val)
1445 args_need_fixup[0] = True
Austin Eng148d9f0f2022-02-08 19:18:531446
Garrett Beaty4c35b142023-06-23 21:01:231447 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
1448 add_conditional_args('lacros_args', self.is_lacros)
1449 add_conditional_args('linux_args', self.is_linux)
1450 add_conditional_args('android_args', self.is_android)
1451 add_conditional_args('chromeos_args', self.is_chromeos)
1452 add_conditional_args('mac_args', self.is_mac)
1453 add_conditional_args('win_args', self.is_win)
1454 add_conditional_args('win64_args', self.is_win64)
1455
1456 if args_need_fixup[0]:
1457 new_test['args'] = self.maybe_fixup_args_array(args)
Wezc0e835b702018-10-30 00:38:411458
Stephen Martinisb72f6d22018-10-04 23:29:011459 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321460 return new_test
1461
Greg Gutermanf60eb052020-03-12 17:40:011462 def generate_output_tests(self, waterfall):
1463 """Generates the tests for a waterfall.
1464
1465 Args:
1466 waterfall: a dictionary parsed from a master pyl file
1467 Returns:
1468 A dictionary mapping builders to test specs
1469 """
1470 return {
Jamie Madillcf4f8c72021-05-20 19:24:231471 name: self.get_tests_for_config(waterfall, name, config)
1472 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011473 }
1474
1475 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531476 generator_map = self.get_test_generator_map()
1477 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281478
Greg Gutermanf60eb052020-03-12 17:40:011479 tests = {}
1480 # Copy only well-understood entries in the machine's configuration
1481 # verbatim into the generated JSON.
1482 if 'additional_compile_targets' in config:
1483 tests['additional_compile_targets'] = config[
1484 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231485 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011486 if test_type not in generator_map:
1487 raise self.unknown_test_suite_type(
1488 test_type, name, waterfall['name']) # pragma: no cover
1489 test_generator = generator_map[test_type]
1490 # Let multiple kinds of generators generate the same kinds
1491 # of tests. For example, gpu_telemetry_tests are a
1492 # specialization of isolated_scripts.
1493 new_tests = test_generator.generate(
1494 waterfall, name, config, input_tests)
1495 remapped_test_type = test_type_remapper.get(test_type, test_type)
Garrett Beatyffe83c4f2023-09-08 19:07:371496 tests.setdefault(remapped_test_type, []).extend(new_tests)
1497
1498 for test_type, tests_for_type in tests.items():
1499 if test_type == 'additional_compile_targets':
1500 continue
1501 tests[test_type] = sorted(tests_for_type, key=lambda t: t['name'])
Greg Gutermanf60eb052020-03-12 17:40:011502
1503 return tests
1504
1505 def jsonify(self, all_tests):
1506 return json.dumps(
1507 all_tests, indent=2, separators=(',', ': '),
1508 sort_keys=True) + '\n'
1509
1510 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281511 self.load_configuration_files()
1512 self.resolve_configuration_files()
1513 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011514 result = collections.defaultdict(dict)
1515
Stephanie Kim572b43c02023-04-13 14:24:131516 if os.path.exists(self.args.autoshard_exceptions_json_path):
1517 autoshards = json.loads(
1518 self.read_file(self.args.autoshard_exceptions_json_path))
1519 else:
1520 autoshards = {}
1521
Dirk Pranke6269d302020-10-01 00:14:391522 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011523 for waterfall in self.waterfalls:
1524 for field in required_fields:
1525 # Verify required fields
1526 if field not in waterfall:
1527 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1528
1529 # Handle filter flag, if specified
1530 if filters and waterfall['name'] not in filters:
1531 continue
1532
1533 # Join config files and hardcoded values together
1534 all_tests = self.generate_output_tests(waterfall)
1535 result[waterfall['name']] = all_tests
1536
Stephanie Kim572b43c02023-04-13 14:24:131537 if not autoshards:
1538 continue
1539 for builder, test_spec in all_tests.items():
1540 for target_type, test_list in test_spec.items():
1541 if target_type == 'additional_compile_targets':
1542 continue
1543 for test_dict in test_list:
1544 # Suites that apply variants or other customizations will create
1545 # test_dicts that have "name" value that is different from the
Garrett Beatyffe83c4f2023-09-08 19:07:371546 # "test" value.
Stephanie Kim572b43c02023-04-13 14:24:131547 # e.g. name = vulkan_swiftshader_content_browsertests, but
1548 # test = content_browsertests and
1549 # test_id_prefix = "ninja://content/test:content_browsertests/"
Garrett Beatyffe83c4f2023-09-08 19:07:371550 test_name = test_dict['name']
Stephanie Kim572b43c02023-04-13 14:24:131551 shard_info = autoshards.get(waterfall['name'],
1552 {}).get(builder, {}).get(test_name)
1553 if shard_info:
1554 test_dict['swarming'].update(
1555 {'shards': int(shard_info['shards'])})
1556
Greg Gutermanf60eb052020-03-12 17:40:011557 # Add do not edit warning
1558 for tests in result.values():
1559 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1560 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1561
1562 return result
1563
1564 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281565 suffix = '.json'
1566 if self.args.new_files:
1567 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011568
1569 for filename, contents in result.items():
1570 jsonstr = self.jsonify(contents)
Garrett Beaty79339e182023-04-10 20:45:471571 file_path = os.path.join(self.args.output_dir, filename + suffix)
1572 self.write_file(file_path, jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281573
Nico Weberd18b8962018-05-16 19:39:381574 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161575 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421576 # NOTE: This reference can cause issues; if a file changes there, the
1577 # presubmit here won't be run by default. A manually maintained list there
1578 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1579 # references to configs outside of this directory are added, please change
1580 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1581 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491582
Garrett Beaty7e866fc2021-06-16 14:12:101583 # Get the generated project.pyl so we can check if we should be enforcing
1584 # that the specs are for builders that actually exist
1585 # If not, return None to indicate that we won't enforce that builders in
1586 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491587 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1588 'project.pyl')
1589 if os.path.exists(project_pyl_path):
1590 settings = ast.literal_eval(self.read_file(project_pyl_path))
1591 if not settings.get('validate_source_side_specs_have_builder', True):
1592 return None
1593
Nico Weberd18b8962018-05-16 19:39:381594 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311595 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161596 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1597 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431598 for c in milo_configs:
1599 for l in self.read_file(c).splitlines():
1600 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311601 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431602 continue
1603 # l looks like
1604 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1605 # Extract win_chromium_dbg_ng part.
1606 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381607 return bot_names
1608
Ben Pastene9a010082019-09-25 20:41:371609 def get_internal_waterfalls(self):
1610 # Similar to get_builders_that_do_not_actually_exist above, but for
1611 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201612 return [
Kramer Ge3bf853a2023-04-13 19:39:471613 'chrome', 'chrome.pgo', 'chrome.gpu.fyi', 'internal.chrome.fyi',
Marco Georgaklis333e8386b2023-09-07 22:46:331614 'internal.chromeos.fyi', 'internal.optimization_guide', 'internal.soda'
Yuke Liaoe6c23dd2021-07-28 16:12:201615 ]
Ben Pastene9a010082019-09-25 20:41:371616
Stephen Martinisf83893722018-09-19 00:02:181617 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201618 self.check_input_files_sorting(verbose)
1619
Kenneth Russelleb60cbd22017-12-05 07:54:281620 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011621 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381622 self.check_composition_type_test_suites('matrix_compound_suites',
1623 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111624 self.resolve_test_id_prefixes()
Stephen Martinis54d64ad2018-09-21 22:16:201625 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381626
1627 # All bots should exist.
1628 bot_names = self.get_valid_bot_names()
Garrett Beaty2a02de3c2020-05-15 13:57:351629 if bot_names is not None:
1630 internal_waterfalls = self.get_internal_waterfalls()
1631 for waterfall in self.waterfalls:
1632 # TODO(crbug.com/991417): Remove the need for this exception.
1633 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011634 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351635 for bot_name in waterfall['machines']:
Garrett Beaty2a02de3c2020-05-15 13:57:351636 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581637 if waterfall['name'] in [
1638 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1639 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351640 # TODO(thakis): Remove this once these bots move to luci.
1641 continue # pragma: no cover
1642 if waterfall['name'] in ['tryserver.webrtc',
1643 'webrtc.chromium.fyi.experimental']:
1644 # These waterfalls have their bot configs in a different repo.
1645 # so we don't know about their bot names.
1646 continue # pragma: no cover
1647 if waterfall['name'] in ['client.devtools-frontend.integration',
1648 'tryserver.devtools-frontend',
1649 'chromium.devtools-frontend']:
1650 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201651 if waterfall['name'] in ['client.openscreen.chromium']:
1652 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351653 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381654
Kenneth Russelleb60cbd22017-12-05 07:54:281655 # All test suites must be referenced.
1656 suites_seen = set()
1657 generator_map = self.get_test_generator_map()
1658 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231659 for bot_name, tester in waterfall['machines'].items():
1660 for suite_type, suite in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281661 if suite_type not in generator_map:
1662 raise self.unknown_test_suite_type(suite_type, bot_name,
1663 waterfall['name'])
1664 if suite not in self.test_suites:
1665 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1666 suites_seen.add(suite)
1667 # Since we didn't resolve the configuration files, this set
1668 # includes both composition test suites and regular ones.
1669 resolved_suites = set()
1670 for suite_name in suites_seen:
1671 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011672 for sub_suite in suite:
1673 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281674 resolved_suites.add(suite_name)
1675 # At this point, every key in test_suites.pyl should be referenced.
1676 missing_suites = set(self.test_suites.keys()) - resolved_suites
1677 if missing_suites:
1678 raise BBGenErr('The following test suites were unreferenced by bots on '
1679 'the waterfalls: ' + str(missing_suites))
1680
1681 # All test suite exceptions must refer to bots on the waterfall.
1682 all_bots = set()
1683 missing_bots = set()
1684 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231685 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281686 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281687 # In order to disambiguate between bots with the same name on
1688 # different waterfalls, support has been added to various
1689 # exceptions for concatenating the waterfall name after the bot
1690 # name.
1691 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231692 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381693 removals = (exception.get('remove_from', []) +
1694 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231695 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381696 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281697 if removal not in all_bots:
1698 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411699
Kenneth Russelleb60cbd22017-12-05 07:54:281700 if missing_bots:
1701 raise BBGenErr('The following nonexistent machines were referenced in '
1702 'the test suite exceptions: ' + str(missing_bots))
1703
Garrett Beatyb061e69d2023-06-27 16:15:351704 for name, mixin in self.mixins.items():
1705 if '$mixin_append' in mixin:
1706 raise BBGenErr(
1707 f'$mixin_append is no longer supported (set in mixin "{name}"),'
1708 ' args and named caches specified as normal will be appended')
1709
Stephen Martinis0382bc12018-09-17 22:29:071710 # All mixins must be referenced
1711 seen_mixins = set()
1712 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011713 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231714 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011715 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071716 for suite in self.test_suites.values():
1717 if isinstance(suite, list):
1718 # Don't care about this, it's a composition, which shouldn't include a
1719 # swarming mixin.
1720 continue
1721
1722 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561723 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011724 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071725
Zhaoyang Li9da047d52021-05-10 21:31:441726 for variant in self.variants:
1727 # Unpack the variant from variants.pyl if it's string based.
1728 if isinstance(variant, str):
1729 variant = self.variants[variant]
1730 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1731
Stephen Martinisb72f6d22018-10-04 23:29:011732 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071733 if missing_mixins:
1734 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1735 ' referenced in a waterfall, machine, or test suite.' % (
1736 str(missing_mixins)))
1737
Jeff Yoonda581c32020-03-06 03:56:051738 # All variant references must be referenced
1739 seen_variants = set()
1740 for suite in self.test_suites.values():
1741 if isinstance(suite, list):
1742 continue
1743
1744 for test in suite.values():
1745 if isinstance(test, dict):
1746 for variant in test.get('variants', []):
1747 if isinstance(variant, str):
1748 seen_variants.add(variant)
1749
1750 missing_variants = set(self.variants.keys()) - seen_variants
1751 if missing_variants:
1752 raise BBGenErr('The following variants were unreferenced: %s. They must '
1753 'be referenced in a matrix test suite under the variants '
1754 'key.' % str(missing_variants))
1755
Stephen Martinis54d64ad2018-09-21 22:16:201756
Garrett Beaty79339e182023-04-10 20:45:471757 def type_assert(self, node, typ, file_path, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201758 """Asserts that the Python AST node |node| is of type |typ|.
1759
1760 If verbose is set, it prints out some helpful context lines, showing where
1761 exactly the error occurred in the file.
1762 """
1763 if not isinstance(node, typ):
1764 if verbose:
Garrett Beaty79339e182023-04-10 20:45:471765 lines = [""] + self.read_file(file_path).splitlines()
Stephen Martinis54d64ad2018-09-21 22:16:201766
1767 context = 2
1768 lines_start = max(node.lineno - context, 0)
1769 # Add one to include the last line
1770 lines_end = min(node.lineno + context, len(lines)) + 1
Garrett Beaty79339e182023-04-10 20:45:471771 lines = itertools.chain(
1772 ['== %s ==\n' % file_path],
1773 ["<snip>\n"],
1774 [
1775 '%d %s' % (lines_start + i, line)
1776 for i, line in enumerate(lines[lines_start:lines_start +
1777 context])
1778 ],
1779 ['-' * 80 + '\n'],
1780 ['%d %s' % (node.lineno, lines[node.lineno])],
1781 [
1782 '-' * (node.col_offset + 3) + '^' + '-' *
1783 (80 - node.col_offset - 4) + '\n'
1784 ],
1785 [
1786 '%d %s' % (node.lineno + 1 + i, line)
1787 for i, line in enumerate(lines[node.lineno + 1:lines_end])
1788 ],
1789 ["<snip>\n"],
Stephen Martinis54d64ad2018-09-21 22:16:201790 )
1791 # Print out a useful message when a type assertion fails.
1792 for l in lines:
1793 self.print_line(l.strip())
1794
1795 node_dumped = ast.dump(node, annotate_fields=False)
1796 # If the node is huge, truncate it so everything fits in a terminal
1797 # window.
1798 if len(node_dumped) > 60: # pragma: no cover
1799 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1800 raise BBGenErr(
Garrett Beaty807011ab2023-04-12 00:52:391801 'Invalid .pyl file \'%s\'. Python AST node %r on line %s expected to'
Garrett Beaty79339e182023-04-10 20:45:471802 ' be %s, is %s' %
1803 (file_path, node_dumped, node.lineno, typ, type(node)))
Stephen Martinis54d64ad2018-09-21 22:16:201804
Garrett Beaty79339e182023-04-10 20:45:471805 def check_ast_list_formatted(self,
1806 keys,
1807 file_path,
1808 verbose,
Stephen Martinis1384ff92020-01-07 19:52:151809 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531810 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201811
Stephen Martinis5bef0fc2020-01-06 22:47:531812 Currently only checks to ensure they're correctly sorted, and that there
1813 are no duplicates.
1814
1815 Args:
1816 keys: An python list of AST nodes.
1817
1818 It's a list of AST nodes instead of a list of strings because
1819 when verbose is set, it tries to print out context of where the
1820 diffs are in the file.
Garrett Beaty79339e182023-04-10 20:45:471821 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531822 verbose: If set, print out diff information about how the keys are
1823 incorrectly formatted.
1824 check_sorting: If true, checks if the list is sorted.
1825 Returns:
1826 If the keys are correctly formatted.
1827 """
1828 if not keys:
1829 return True
1830
1831 assert isinstance(keys[0], ast.Str)
1832
1833 keys_strs = [k.s for k in keys]
1834 # Keys to diff against. Used below.
1835 keys_to_diff_against = None
1836 # If the list is properly formatted.
1837 list_formatted = True
1838
1839 # Duplicates are always bad.
1840 if len(set(keys_strs)) != len(keys_strs):
1841 list_formatted = False
1842 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1843
1844 if check_sorting and sorted(keys_strs) != keys_strs:
1845 list_formatted = False
1846 if list_formatted:
1847 return True
1848
1849 if verbose:
1850 line_num = keys[0].lineno
1851 keys = [k.s for k in keys]
1852 if check_sorting:
1853 # If we have duplicates, sorting this will take care of it anyways.
1854 keys_to_diff_against = sorted(set(keys))
1855 # else, keys_to_diff_against is set above already
1856
1857 self.print_line('=' * 80)
1858 self.print_line('(First line of keys is %s)' % line_num)
Garrett Beaty79339e182023-04-10 20:45:471859 for line in difflib.context_diff(keys,
1860 keys_to_diff_against,
1861 fromfile='current (%r)' % file_path,
1862 tofile='sorted',
1863 lineterm=''):
Stephen Martinis5bef0fc2020-01-06 22:47:531864 self.print_line(line)
1865 self.print_line('=' * 80)
1866
1867 return False
1868
Garrett Beaty79339e182023-04-10 20:45:471869 def check_ast_dict_formatted(self, node, file_path, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531870 """Checks if an ast dictionary's keys are correctly formatted.
1871
1872 Just a simple wrapper around check_ast_list_formatted.
1873 Args:
1874 node: An AST node. Assumed to be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471875 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531876 verbose: If set, print out diff information about how the keys are
1877 incorrectly formatted.
1878 check_sorting: If true, checks if the list is sorted.
1879 Returns:
1880 If the dictionary is correctly formatted.
1881 """
Stephen Martinis54d64ad2018-09-21 22:16:201882 keys = []
1883 # The keys of this dict are ordered as ordered in the file; normal python
1884 # dictionary keys are given an arbitrary order, but since we parsed the
1885 # file itself, the order as given in the file is preserved.
1886 for key in node.keys:
Garrett Beaty79339e182023-04-10 20:45:471887 self.type_assert(key, ast.Str, file_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531888 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201889
Garrett Beaty79339e182023-04-10 20:45:471890 return self.check_ast_list_formatted(keys, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181891
1892 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201893 # TODO(https://crbug.com/886993): Add the ability for this script to
1894 # actually format the files, rather than just complain if they're
1895 # incorrectly formatted.
1896 bad_files = set()
Garrett Beaty79339e182023-04-10 20:45:471897
1898 def parse_file(file_path):
Stephen Martinis5bef0fc2020-01-06 22:47:531899 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201900
Stephen Martinis5bef0fc2020-01-06 22:47:531901 Returns an AST node representing the value in the pyl file."""
Garrett Beaty79339e182023-04-10 20:45:471902 parsed = ast.parse(self.read_file(file_path))
Stephen Martinisf83893722018-09-19 00:02:181903
Stephen Martinisf83893722018-09-19 00:02:181904 # Must be a module.
Garrett Beaty79339e182023-04-10 20:45:471905 self.type_assert(parsed, ast.Module, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181906 module = parsed.body
1907
1908 # Only one expression in the module.
Garrett Beaty79339e182023-04-10 20:45:471909 self.type_assert(module, list, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181910 if len(module) != 1: # pragma: no cover
Garrett Beaty79339e182023-04-10 20:45:471911 raise BBGenErr('Invalid .pyl file %s' % file_path)
Stephen Martinisf83893722018-09-19 00:02:181912 expr = module[0]
Garrett Beaty79339e182023-04-10 20:45:471913 self.type_assert(expr, ast.Expr, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181914
Stephen Martinis5bef0fc2020-01-06 22:47:531915 return expr.value
1916
1917 # Handle this separately
Garrett Beaty79339e182023-04-10 20:45:471918 value = parse_file(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531919 # Value should be a list.
Garrett Beaty79339e182023-04-10 20:45:471920 self.type_assert(value, ast.List, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531921
1922 keys = []
Joshua Hood56c673c2022-03-02 20:29:331923 for elm in value.elts:
Garrett Beaty79339e182023-04-10 20:45:471924 self.type_assert(elm, ast.Dict, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531925 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331926 for key, val in zip(elm.keys, elm.values):
Garrett Beaty79339e182023-04-10 20:45:471927 self.type_assert(key, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531928 if key.s == 'machines':
Garrett Beaty79339e182023-04-10 20:45:471929 if not self.check_ast_dict_formatted(
1930 val, self.args.waterfalls_pyl_path, verbose):
1931 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531932
1933 if key.s == "name":
Garrett Beaty79339e182023-04-10 20:45:471934 self.type_assert(val, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531935 waterfall_name = val
1936 assert waterfall_name
1937 keys.append(waterfall_name)
1938
Garrett Beaty79339e182023-04-10 20:45:471939 if not self.check_ast_list_formatted(keys, self.args.waterfalls_pyl_path,
1940 verbose):
1941 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531942
Garrett Beaty79339e182023-04-10 20:45:471943 for file_path in (
1944 self.args.mixins_pyl_path,
1945 self.args.test_suites_pyl_path,
1946 self.args.test_suite_exceptions_pyl_path,
Stephen Martinis5bef0fc2020-01-06 22:47:531947 ):
Garrett Beaty79339e182023-04-10 20:45:471948 value = parse_file(file_path)
Stephen Martinisf83893722018-09-19 00:02:181949 # Value should be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471950 self.type_assert(value, ast.Dict, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181951
Garrett Beaty79339e182023-04-10 20:45:471952 if not self.check_ast_dict_formatted(value, file_path, verbose):
1953 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531954
Garrett Beaty79339e182023-04-10 20:45:471955 if file_path == self.args.test_suites_pyl_path:
Jeff Yoon8154e582019-12-03 23:30:011956 expected_keys = ['basic_suites',
1957 'compound_suites',
1958 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201959 actual_keys = [node.s for node in value.keys]
1960 assert all(key in expected_keys for key in actual_keys), (
Garrett Beaty79339e182023-04-10 20:45:471961 'Invalid %r file; expected keys %r, got %r' %
1962 (file_path, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:331963 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:201964 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011965 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201966 for suite_group in suite_dicts:
Garrett Beaty79339e182023-04-10 20:45:471967 if not self.check_ast_dict_formatted(suite_group, file_path, verbose):
1968 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181969
Stephen Martinis5bef0fc2020-01-06 22:47:531970 for key, suite in zip(value.keys, value.values):
1971 # The compound suites are checked in
1972 # 'check_composition_type_test_suites()'
1973 if key.s == 'basic_suites':
1974 for group in suite.values:
Garrett Beaty79339e182023-04-10 20:45:471975 if not self.check_ast_dict_formatted(group, file_path, verbose):
1976 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531977 break
Stephen Martinis54d64ad2018-09-21 22:16:201978
Garrett Beaty79339e182023-04-10 20:45:471979 elif file_path == self.args.test_suite_exceptions_pyl_path:
Stephen Martinis5bef0fc2020-01-06 22:47:531980 # Check the values for each test.
1981 for test in value.values:
1982 for kind, node in zip(test.keys, test.values):
1983 if isinstance(node, ast.Dict):
Garrett Beaty79339e182023-04-10 20:45:471984 if not self.check_ast_dict_formatted(node, file_path, verbose):
1985 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531986 elif kind.s == 'remove_from':
1987 # Don't care about sorting; these are usually grouped, since the
1988 # same bug can affect multiple builders. Do want to make sure
1989 # there aren't duplicates.
Garrett Beaty79339e182023-04-10 20:45:471990 if not self.check_ast_list_formatted(
1991 node.elts, file_path, verbose, check_sorting=False):
1992 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181993
1994 if bad_files:
1995 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201996 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531997 'unsorted, or have duplicates. Re-run this with --verbose to see '
1998 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181999
Kenneth Russelleb60cbd22017-12-05 07:54:282000 def check_output_file_consistency(self, verbose=False):
2001 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012002 # All waterfalls/bucket .json files must have been written
2003 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:282004 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012005 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:162006 outputs = self.generate_outputs()
2007 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:012008 expected = self.jsonify(expected_contents)
Garrett Beaty79339e182023-04-10 20:45:472009 file_path = os.path.join(self.args.output_dir, filename + '.json')
Ben Pastenef21cda32023-03-30 22:00:572010 current = self.read_file(file_path)
Kenneth Russelleb60cbd22017-12-05 07:54:282011 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:012012 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:322013 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:012014 self.print_line('File ' + filename +
2015 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:322016 'contents:')
2017 for line in difflib.unified_diff(
2018 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:502019 current.splitlines(),
2020 fromfile='expected', tofile='current'):
2021 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:012022
2023 if ungenerated_files:
2024 raise BBGenErr(
2025 'The following files have not been properly '
2026 'autogenerated by generate_buildbot_json.py: ' +
2027 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:282028
Dirk Pranke772f55f2021-04-28 04:51:162029 for builder_group, builders in outputs.items():
2030 for builder, step_types in builders.items():
Garrett Beatydca3d882023-09-14 23:50:322031 for test_type in ('gtest_tests', 'isolated_scripts'):
2032 for step_data in step_types.get(test_type, []):
2033 step_name = step_data['name']
2034 self._check_swarming_config(builder_group, builder, step_name,
2035 step_data)
Dirk Pranke772f55f2021-04-28 04:51:162036
2037 def _check_swarming_config(self, filename, builder, step_name, step_data):
Ben Pastene338f56b2023-03-31 21:24:452038 # TODO(crbug.com/1203436): Ensure all swarming tests specify cpu, not
Dirk Pranke772f55f2021-04-28 04:51:162039 # just mac tests.
Garrett Beatybb18d532023-06-26 22:16:332040 if 'swarming' in step_data:
Garrett Beatyade673d2023-08-04 22:00:252041 dimensions = step_data['swarming'].get('dimensions')
2042 if not dimensions:
Ben Pastene338f56b2023-03-31 21:24:452043 raise BBGenErr('%s: %s / %s : os must be specified for all '
Dirk Pranke772f55f2021-04-28 04:51:162044 'swarmed tests' % (filename, builder, step_name))
Garrett Beatyade673d2023-08-04 22:00:252045 if not dimensions.get('os'):
2046 raise BBGenErr('%s: %s / %s : os must be specified for all '
2047 'swarmed tests' % (filename, builder, step_name))
2048 if 'Mac' in dimensions.get('os') and not dimensions.get('cpu'):
2049 raise BBGenErr('%s: %s / %s : cpu must be specified for mac '
2050 'swarmed tests' % (filename, builder, step_name))
Dirk Pranke772f55f2021-04-28 04:51:162051
Kenneth Russelleb60cbd22017-12-05 07:54:282052 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502053 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282054 self.check_output_file_consistency(verbose) # pragma: no cover
2055
Karen Qiane24b7ee2019-02-12 23:37:062056 def does_test_match(self, test_info, params_dict):
2057 """Checks to see if the test matches the parameters given.
2058
2059 Compares the provided test_info with the params_dict to see
2060 if the bot matches the parameters given. If so, returns True.
2061 Else, returns false.
2062
2063 Args:
2064 test_info (dict): Information about a specific bot provided
2065 in the format shown in waterfalls.pyl
2066 params_dict (dict): Dictionary of parameters and their values
2067 to look for in the bot
2068 Ex: {
2069 'device_os':'android',
2070 '--flag':True,
2071 'mixins': ['mixin1', 'mixin2'],
2072 'ex_key':'ex_value'
2073 }
2074
2075 """
2076 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2077 'kvm', 'pool', 'integrity'] # dimension parameters
2078 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2079 'can_use_on_swarming_builders']
2080 for param in params_dict:
2081 # if dimension parameter
2082 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2083 if not 'swarming' in test_info:
2084 return False
2085 swarming = test_info['swarming']
2086 if param in SWARMING_PARAMS:
2087 if not param in swarming:
2088 return False
2089 if not str(swarming[param]) == params_dict[param]:
2090 return False
2091 else:
Garrett Beatyade673d2023-08-04 22:00:252092 if not 'dimensions' in swarming:
Karen Qiane24b7ee2019-02-12 23:37:062093 return False
Garrett Beatyade673d2023-08-04 22:00:252094 dimensions = swarming['dimensions']
Karen Qiane24b7ee2019-02-12 23:37:062095 # only looking at the first dimension set
Garrett Beatyade673d2023-08-04 22:00:252096 if not param in dimensions:
Karen Qiane24b7ee2019-02-12 23:37:062097 return False
Garrett Beatyade673d2023-08-04 22:00:252098 if not dimensions[param] == params_dict[param]:
Karen Qiane24b7ee2019-02-12 23:37:062099 return False
2100
2101 # if flag
2102 elif param.startswith('--'):
2103 if not 'args' in test_info:
2104 return False
2105 if not param in test_info['args']:
2106 return False
2107
2108 # not dimension parameter/flag/mixin
2109 else:
2110 if not param in test_info:
2111 return False
2112 if not test_info[param] == params_dict[param]:
2113 return False
2114 return True
2115 def error_msg(self, msg):
2116 """Prints an error message.
2117
2118 In addition to a catered error message, also prints
2119 out where the user can find more help. Then, program exits.
2120 """
2121 self.print_line(msg + (' If you need more information, ' +
2122 'please run with -h or --help to see valid commands.'))
2123 sys.exit(1)
2124
2125 def find_bots_that_run_test(self, test, bots):
2126 matching_bots = []
2127 for bot in bots:
2128 bot_info = bots[bot]
2129 tests = self.flatten_tests_for_bot(bot_info)
2130 for test_info in tests:
Garrett Beatyffe83c4f2023-09-08 19:07:372131 test_name = test_info['name']
Karen Qiane24b7ee2019-02-12 23:37:062132 if not test_name == test:
2133 continue
2134 matching_bots.append(bot)
2135 return matching_bots
2136
2137 def find_tests_with_params(self, tests, params_dict):
2138 matching_tests = []
2139 for test_name in tests:
2140 test_info = tests[test_name]
2141 if not self.does_test_match(test_info, params_dict):
2142 continue
2143 if not test_name in matching_tests:
2144 matching_tests.append(test_name)
2145 return matching_tests
2146
2147 def flatten_waterfalls_for_query(self, waterfalls):
2148 bots = {}
2149 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012150 waterfall_tests = self.generate_output_tests(waterfall)
2151 for bot in waterfall_tests:
2152 bot_info = waterfall_tests[bot]
2153 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062154 return bots
2155
2156 def flatten_tests_for_bot(self, bot_info):
2157 """Returns a list of flattened tests.
2158
2159 Returns a list of tests not grouped by test category
2160 for a specific bot.
2161 """
2162 TEST_CATS = self.get_test_generator_map().keys()
2163 tests = []
2164 for test_cat in TEST_CATS:
2165 if not test_cat in bot_info:
2166 continue
2167 test_cat_tests = bot_info[test_cat]
2168 tests = tests + test_cat_tests
2169 return tests
2170
2171 def flatten_tests_for_query(self, test_suites):
2172 """Returns a flattened dictionary of tests.
2173
2174 Returns a dictionary of tests associate with their
2175 configuration, not grouped by their test suite.
2176 """
2177 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232178 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062179 for test in test_suite:
2180 test_info = test_suite[test]
2181 test_name = test
Karen Qiane24b7ee2019-02-12 23:37:062182 tests[test_name] = test_info
2183 return tests
2184
2185 def parse_query_filter_params(self, params):
2186 """Parses the filter parameters.
2187
2188 Creates a dictionary from the parameters provided
2189 to filter the bot array.
2190 """
2191 params_dict = {}
2192 for p in params:
2193 # flag
2194 if p.startswith("--"):
2195 params_dict[p] = True
2196 else:
2197 pair = p.split(":")
2198 if len(pair) != 2:
2199 self.error_msg('Invalid command.')
2200 # regular parameters
2201 if pair[1].lower() == "true":
2202 params_dict[pair[0]] = True
2203 elif pair[1].lower() == "false":
2204 params_dict[pair[0]] = False
2205 else:
2206 params_dict[pair[0]] = pair[1]
2207 return params_dict
2208
2209 def get_test_suites_dict(self, bots):
2210 """Returns a dictionary of bots and their tests.
2211
2212 Returns a dictionary of bots and a list of their associated tests.
2213 """
2214 test_suite_dict = dict()
2215 for bot in bots:
2216 bot_info = bots[bot]
2217 tests = self.flatten_tests_for_bot(bot_info)
2218 test_suite_dict[bot] = tests
2219 return test_suite_dict
2220
2221 def output_query_result(self, result, json_file=None):
2222 """Outputs the result of the query.
2223
2224 If a json file parameter name is provided, then
2225 the result is output into the json file. If not,
2226 then the result is printed to the console.
2227 """
2228 output = json.dumps(result, indent=2)
2229 if json_file:
2230 self.write_file(json_file, output)
2231 else:
2232 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062233
Joshua Hood56c673c2022-03-02 20:29:332234 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062235 def query(self, args):
2236 """Queries tests or bots.
2237
2238 Depending on the arguments provided, outputs a json of
2239 tests or bots matching the appropriate optional parameters provided.
2240 """
2241 # split up query statement
2242 query = args.query.split('/')
2243 self.load_configuration_files()
2244 self.resolve_configuration_files()
2245
2246 # flatten bots json
2247 tests = self.test_suites
2248 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2249
2250 cmd_class = query[0]
2251
2252 # For queries starting with 'bots'
2253 if cmd_class == "bots":
2254 if len(query) == 1:
2255 return self.output_query_result(bots, args.json)
2256 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332257 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062258 if query[1] == 'tests':
2259 test_suites_dict = self.get_test_suites_dict(bots)
2260 return self.output_query_result(test_suites_dict, args.json)
Joshua Hood56c673c2022-03-02 20:29:332261 self.error_msg("This query should be in the format: bots/tests.")
Karen Qiane24b7ee2019-02-12 23:37:062262
2263 else:
2264 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2265 % str(len(query)-1))
2266
2267 # For queries starting with 'bot'
2268 elif cmd_class == "bot":
2269 if not len(query) == 2 and not len(query) == 3:
2270 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2271 % str(len(query)-1))
2272 bot_id = query[1]
2273 if not bot_id in bots:
2274 self.error_msg("No bot named '" + bot_id + "' found.")
2275 bot_info = bots[bot_id]
2276 if len(query) == 2:
2277 return self.output_query_result(bot_info, args.json)
2278 if not query[2] == 'tests':
2279 self.error_msg("The query should be in the format:" +
2280 "bot/<bot-name>/tests.")
2281
2282 bot_tests = self.flatten_tests_for_bot(bot_info)
2283 return self.output_query_result(bot_tests, args.json)
2284
2285 # For queries starting with 'tests'
2286 elif cmd_class == "tests":
2287 if not len(query) == 1 and not len(query) == 2:
2288 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2289 % str(len(query)-1))
2290 flattened_tests = self.flatten_tests_for_query(tests)
2291 if len(query) == 1:
2292 return self.output_query_result(flattened_tests, args.json)
2293
2294 # create params dict
2295 params = query[1].split('&')
2296 params_dict = self.parse_query_filter_params(params)
2297 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2298 return self.output_query_result(matching_bots)
2299
2300 # For queries starting with 'test'
2301 elif cmd_class == "test":
2302 if not len(query) == 2 and not len(query) == 3:
2303 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2304 % str(len(query)-1))
2305 test_id = query[1]
2306 if len(query) == 2:
2307 flattened_tests = self.flatten_tests_for_query(tests)
2308 for test in flattened_tests:
2309 if test == test_id:
2310 return self.output_query_result(flattened_tests[test], args.json)
2311 self.error_msg("There is no test named %s." % test_id)
2312 if not query[2] == 'bots':
2313 self.error_msg("The query should be in the format: " +
2314 "test/<test-name>/bots")
2315 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2316 return self.output_query_result(bots_for_test)
2317
2318 else:
2319 self.error_msg("Your command did not match any valid commands." +
2320 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Joshua Hood56c673c2022-03-02 20:29:332321 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282322
Garrett Beaty1afaccc2020-06-25 19:58:152323 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282324 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502325 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062326 elif self.args.query:
2327 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282328 else:
Greg Gutermanf60eb052020-03-12 17:40:012329 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282330 return 0
2331
2332if __name__ == "__main__": # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152333 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2334 sys.exit(generator.main())