blob: 1e85564eeba6c95386a42ce3ea9736613ba9681f [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:
Brian Sheedyc860f022022-09-30 23:32:1781 if 'isolate_name' in test:
82 test['test'] = test['isolate_name']
83 del test['isolate_name']
Xinan Lind9b1d2e72022-11-14 20:57:0284 # chromium_GPU is the Autotest wrapper created for browser GPU tests
85 # run in Skylab.
Xinan Lin1f28a0d2023-03-13 17:39:4186 test['autotest_name'] = 'chromium_Graphics'
Xinan Lind9b1d2e72022-11-14 20:57:0287 # As of 22Q4, Skylab tests are running on a CrOS flavored Autotest
88 # framework and it does not support the sub-args like
89 # extra-browser-args. So we have to pop it out and create a new
90 # key for it. See crrev.com/c/3965359 for details.
91 for idx, arg in enumerate(test.get('args', [])):
92 if '--extra-browser-args' in arg:
93 test['args'].pop(idx)
94 test['extra_browser_args'] = arg.replace('--extra-browser-args=', '')
95 break
Brian Sheedyb6491ba2022-09-26 20:49:4996 return isolated_scripts
97
98
Kenneth Russelleb60cbd22017-12-05 07:54:2899class GTestGenerator(BaseGenerator):
100 def __init__(self, bb_gen):
101 super(GTestGenerator, self).__init__(bb_gen)
102
Kenneth Russell8ceeabf2017-12-11 17:53:28103 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28104 # The relative ordering of some of the tests is important to
105 # minimize differences compared to the handwritten JSON files, since
106 # Python's sorts are stable and there are some tests with the same
107 # key (see gles2_conform_d3d9_test and similar variants). Avoid
108 # losing the order by avoiding coalescing the dictionaries into one.
109 gtests = []
Jamie Madillcf4f8c72021-05-20 19:24:23110 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoon67c3e832020-02-08 07:39:38111 # Variants allow more than one definition for a given test, and is defined
112 # in array format from resolve_variants().
113 if not isinstance(test_config, list):
114 test_config = [test_config]
115
116 for config in test_config:
117 test = self.bb_gen.generate_gtest(
118 waterfall, tester_name, tester_config, test_name, config)
119 if test:
120 # generate_gtest may veto the test generation on this tester.
121 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28122 return gtests
123
Kenneth Russelleb60cbd22017-12-05 07:54:28124
125class IsolatedScriptTestGenerator(BaseGenerator):
126 def __init__(self, bb_gen):
127 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
128
Kenneth Russell8ceeabf2017-12-11 17:53:28129 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28130 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23131 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43132 # Variants allow more than one definition for a given test, and is defined
133 # in array format from resolve_variants().
134 if not isinstance(test_config, list):
135 test_config = [test_config]
136
137 for config in test_config:
138 test = self.bb_gen.generate_isolated_script_test(
139 waterfall, tester_name, tester_config, test_name, config)
140 if test:
141 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28142 return isolated_scripts
143
Kenneth Russelleb60cbd22017-12-05 07:54:28144
145class ScriptGenerator(BaseGenerator):
146 def __init__(self, bb_gen):
147 super(ScriptGenerator, self).__init__(bb_gen)
148
Kenneth Russell8ceeabf2017-12-11 17:53:28149 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28150 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23151 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28152 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28153 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28154 if test:
155 scripts.append(test)
156 return scripts
157
Kenneth Russelleb60cbd22017-12-05 07:54:28158
159class JUnitGenerator(BaseGenerator):
160 def __init__(self, bb_gen):
161 super(JUnitGenerator, self).__init__(bb_gen)
162
Kenneth Russell8ceeabf2017-12-11 17:53:28163 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28164 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23165 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28166 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28167 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28168 if test:
169 scripts.append(test)
170 return scripts
171
Kenneth Russelleb60cbd22017-12-05 07:54:28172
Xinan Lin05fb9c1752020-12-17 00:15:52173class SkylabGenerator(BaseGenerator):
174 def __init__(self, bb_gen):
175 super(SkylabGenerator, self).__init__(bb_gen)
176
177 def generate(self, waterfall, tester_name, tester_config, input_tests):
178 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23179 for test_name, test_config in sorted(input_tests.items()):
Xinan Lin05fb9c1752020-12-17 00:15:52180 for config in test_config:
181 test = self.bb_gen.generate_skylab_test(waterfall, tester_name,
182 tester_config, test_name,
183 config)
184 if test:
185 scripts.append(test)
186 return scripts
187
Xinan Lin05fb9c1752020-12-17 00:15:52188
Jeff Yoon67c3e832020-02-08 07:39:38189def check_compound_references(other_test_suites=None,
190 sub_suite=None,
191 suite=None,
192 target_test_suites=None,
193 test_type=None,
194 **kwargs):
195 """Ensure comound reference's don't target other compounds"""
196 del kwargs
197 if sub_suite in other_test_suites or sub_suite in target_test_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15198 raise BBGenErr('%s may not refer to other composition type test '
199 'suites (error found while processing %s)' %
200 (test_type, suite))
201
Jeff Yoon67c3e832020-02-08 07:39:38202
203def check_basic_references(basic_suites=None,
204 sub_suite=None,
205 suite=None,
206 **kwargs):
207 """Ensure test has a basic suite reference"""
208 del kwargs
209 if sub_suite not in basic_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15210 raise BBGenErr('Unable to find reference to %s while processing %s' %
211 (sub_suite, suite))
212
Jeff Yoon67c3e832020-02-08 07:39:38213
214def check_conflicting_definitions(basic_suites=None,
215 seen_tests=None,
216 sub_suite=None,
217 suite=None,
218 test_type=None,
Garrett Beaty235c1412023-08-29 20:26:29219 target_test_suites=None,
Jeff Yoon67c3e832020-02-08 07:39:38220 **kwargs):
221 """Ensure that if a test is reachable via multiple basic suites,
222 all of them have an identical definition of the tests.
223 """
224 del kwargs
Garrett Beaty235c1412023-08-29 20:26:29225 variants = None
226 if test_type == 'matrix_compound_suites':
227 variants = target_test_suites[suite][sub_suite].get('variants')
228 variants = variants or [None]
Jeff Yoon67c3e832020-02-08 07:39:38229 for test_name in basic_suites[sub_suite]:
Garrett Beaty235c1412023-08-29 20:26:29230 for variant in variants:
231 key = (test_name, variant)
232 if ((seen_sub_suite := seen_tests.get(key)) is not None
233 and basic_suites[sub_suite][test_name] !=
234 basic_suites[seen_sub_suite][test_name]):
235 test_description = (test_name if variant is None else
236 f'{test_name} with variant {variant} applied')
237 raise BBGenErr(
238 'Conflicting test definitions for %s from %s '
239 'and %s in %s (error found while processing %s)' %
240 (test_description, seen_tests[key], sub_suite, test_type, suite))
241 seen_tests[key] = sub_suite
242
Jeff Yoon67c3e832020-02-08 07:39:38243
244def check_matrix_identifier(sub_suite=None,
245 suite=None,
246 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05247 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38248 **kwargs):
249 """Ensure 'idenfitier' is defined for each variant"""
250 del kwargs
251 sub_suite_config = suite_def[sub_suite]
Garrett Beaty2022db42023-08-29 17:22:40252 for variant_name in sub_suite_config.get('variants', []):
253 if variant_name not in all_variants:
254 raise BBGenErr('Missing variant definition for %s in variants.pyl' %
255 variant_name)
256 variant = all_variants[variant_name]
Jeff Yoonda581c32020-03-06 03:56:05257
Jeff Yoon67c3e832020-02-08 07:39:38258 if not 'identifier' in variant:
259 raise BBGenErr('Missing required identifier field in matrix '
260 'compound suite %s, %s' % (suite, sub_suite))
Sven Zhengef0d0872022-04-04 22:13:29261 if variant['identifier'] == '':
262 raise BBGenErr('Identifier field can not be "" in matrix '
263 'compound suite %s, %s' % (suite, sub_suite))
264 if variant['identifier'].strip() != variant['identifier']:
265 raise BBGenErr('Identifier field can not have leading and trailing '
266 'whitespace in matrix compound suite %s, %s' %
267 (suite, sub_suite))
Jeff Yoon67c3e832020-02-08 07:39:38268
269
Joshua Hood56c673c2022-03-02 20:29:33270class BBJSONGenerator(object): # pylint: disable=useless-object-inheritance
Garrett Beaty1afaccc2020-06-25 19:58:15271 def __init__(self, args):
Garrett Beaty1afaccc2020-06-25 19:58:15272 self.args = args
Kenneth Russelleb60cbd22017-12-05 07:54:28273 self.waterfalls = None
274 self.test_suites = None
275 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01276 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41277 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05278 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28279
Garrett Beaty1afaccc2020-06-25 19:58:15280 @staticmethod
281 def parse_args(argv):
282
283 # RawTextHelpFormatter allows for styling of help statement
284 parser = argparse.ArgumentParser(
285 formatter_class=argparse.RawTextHelpFormatter)
286
287 group = parser.add_mutually_exclusive_group()
288 group.add_argument(
289 '-c',
290 '--check',
291 action='store_true',
292 help=
293 'Do consistency checks of configuration and generated files and then '
294 'exit. Used during presubmit. '
295 'Causes the tool to not generate any files.')
296 group.add_argument(
297 '--query',
298 type=str,
299 help=(
300 "Returns raw JSON information of buildbots and tests.\n" +
301 "Examples:\n" + " List all bots (all info):\n" +
302 " --query bots\n\n" +
303 " List all bots and only their associated tests:\n" +
304 " --query bots/tests\n\n" +
305 " List all information about 'bot1' " +
306 "(make sure you have quotes):\n" + " --query bot/'bot1'\n\n" +
307 " List tests running for 'bot1' (make sure you have quotes):\n" +
308 " --query bot/'bot1'/tests\n\n" + " List all tests:\n" +
309 " --query tests\n\n" +
310 " List all tests and the bots running them:\n" +
311 " --query tests/bots\n\n" +
312 " List all tests that satisfy multiple parameters\n" +
313 " (separation of parameters by '&' symbol):\n" +
314 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
315 " List all tests that run with a specific flag:\n" +
316 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
317 " List specific test (make sure you have quotes):\n"
318 " --query test/'test1'\n\n"
319 " List all bots running 'test1' " +
320 "(make sure you have quotes):\n" + " --query test/'test1'/bots"))
321 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47322 '--json',
323 metavar='JSON_FILE_PATH',
324 type=os.path.abspath,
325 help='Outputs results into a json file. Only works with query function.'
326 )
327 parser.add_argument(
Garrett Beaty1afaccc2020-06-25 19:58:15328 '-n',
329 '--new-files',
330 action='store_true',
331 help=
332 'Write output files as .new.json. Useful during development so old and '
333 'new files can be looked at side-by-side.')
Garrett Beatyade673d2023-08-04 22:00:25334 parser.add_argument('--dimension-sets-handling',
335 choices=['disable'],
336 default='disable',
337 help=('This flag no longer has any effect:'
338 ' dimension_sets fields are not allowed'))
Garrett Beaty1afaccc2020-06-25 19:58:15339 parser.add_argument('-v',
340 '--verbose',
341 action='store_true',
342 help='Increases verbosity. Affects consistency checks.')
343 parser.add_argument('waterfall_filters',
344 metavar='waterfalls',
345 type=str,
346 nargs='*',
347 help='Optional list of waterfalls to generate.')
348 parser.add_argument(
349 '--pyl-files-dir',
Garrett Beaty79339e182023-04-10 20:45:47350 type=os.path.abspath,
351 help=('Path to the directory containing the input .pyl files.'
352 ' By default the directory containing this script will be used.'))
Garrett Beaty1afaccc2020-06-25 19:58:15353 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47354 '--output-dir',
355 type=os.path.abspath,
356 help=('Path to the directory to output generated .json files.'
357 'By default, the pyl files directory will be used.'))
Chong Guee622242020-10-28 18:17:35358 parser.add_argument('--isolate-map-file',
359 metavar='PATH',
360 help='path to additional isolate map files.',
Garrett Beaty79339e182023-04-10 20:45:47361 type=os.path.abspath,
Chong Guee622242020-10-28 18:17:35362 default=[],
363 action='append',
364 dest='isolate_map_files')
Garrett Beaty1afaccc2020-06-25 19:58:15365 parser.add_argument(
366 '--infra-config-dir',
367 help='Path to the LUCI services configuration directory',
Garrett Beaty79339e182023-04-10 20:45:47368 type=os.path.abspath,
369 default=os.path.join(os.path.dirname(__file__), '..', '..', 'infra',
370 'config'))
371
Garrett Beaty1afaccc2020-06-25 19:58:15372 args = parser.parse_args(argv)
373 if args.json and not args.query:
374 parser.error(
375 "The --json flag can only be used with --query.") # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:15376
Garrett Beaty79339e182023-04-10 20:45:47377 args.pyl_files_dir = args.pyl_files_dir or THIS_DIR
378 args.output_dir = args.output_dir or args.pyl_files_dir
379
Stephanie Kim572b43c02023-04-13 14:24:13380 def absolute_file_path(filename):
Garrett Beaty79339e182023-04-10 20:45:47381 return os.path.join(args.pyl_files_dir, filename)
382
Stephanie Kim572b43c02023-04-13 14:24:13383 args.waterfalls_pyl_path = absolute_file_path('waterfalls.pyl')
Garrett Beaty96802d02023-07-07 14:18:05384 args.mixins_pyl_path = absolute_file_path('mixins.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13385 args.test_suites_pyl_path = absolute_file_path('test_suites.pyl')
386 args.test_suite_exceptions_pyl_path = absolute_file_path(
Garrett Beaty79339e182023-04-10 20:45:47387 'test_suite_exceptions.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13388 args.gn_isolate_map_pyl_path = absolute_file_path('gn_isolate_map.pyl')
389 args.variants_pyl_path = absolute_file_path('variants.pyl')
390 args.autoshard_exceptions_json_path = absolute_file_path(
391 'autoshard_exceptions.json')
Garrett Beaty79339e182023-04-10 20:45:47392
393 return args
Kenneth Russelleb60cbd22017-12-05 07:54:28394
Stephen Martinis7eb8b612018-09-21 00:17:50395 def print_line(self, line):
396 # Exists so that tests can mock
Jamie Madillcf4f8c72021-05-20 19:24:23397 print(line) # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:50398
Kenneth Russelleb60cbd22017-12-05 07:54:28399 def read_file(self, relative_path):
Garrett Beaty79339e182023-04-10 20:45:47400 with open(relative_path) as fp:
Garrett Beaty1afaccc2020-06-25 19:58:15401 return fp.read()
Kenneth Russelleb60cbd22017-12-05 07:54:28402
Garrett Beaty79339e182023-04-10 20:45:47403 def write_file(self, file_path, contents):
Peter Kastingacd55c12023-08-23 20:19:04404 with open(file_path, 'w', newline='') as fp:
Garrett Beaty79339e182023-04-10 20:45:47405 fp.write(contents)
Zhiling Huangbe008172018-03-08 19:13:11406
Joshua Hood56c673c2022-03-02 20:29:33407 # pylint: disable=inconsistent-return-statements
Garrett Beaty79339e182023-04-10 20:45:47408 def load_pyl_file(self, pyl_file_path):
Kenneth Russelleb60cbd22017-12-05 07:54:28409 try:
Garrett Beaty79339e182023-04-10 20:45:47410 return ast.literal_eval(self.read_file(pyl_file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:28411 except (SyntaxError, ValueError) as e: # pragma: no cover
Josip Sokcevic7110fb382023-06-06 01:05:29412 raise BBGenErr('Failed to parse pyl file "%s": %s' %
413 (pyl_file_path, e)) from e
Joshua Hood56c673c2022-03-02 20:29:33414 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:28415
Kenneth Russell8a386d42018-06-02 09:48:01416 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
417 # Currently it is only mandatory for bots which run GPU tests. Change these to
418 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28419 def is_android(self, tester_config):
420 return tester_config.get('os_type') == 'android'
421
Ben Pastenea9e583b2019-01-16 02:57:26422 def is_chromeos(self, tester_config):
423 return tester_config.get('os_type') == 'chromeos'
424
Chong Guc2ca5d02022-01-11 19:52:17425 def is_fuchsia(self, tester_config):
426 return tester_config.get('os_type') == 'fuchsia'
427
Brian Sheedy781c8ca42021-03-08 22:03:21428 def is_lacros(self, tester_config):
429 return tester_config.get('os_type') == 'lacros'
430
Kenneth Russell8a386d42018-06-02 09:48:01431 def is_linux(self, tester_config):
432 return tester_config.get('os_type') == 'linux'
433
Kai Ninomiya40de9f52019-10-18 21:38:49434 def is_mac(self, tester_config):
435 return tester_config.get('os_type') == 'mac'
436
437 def is_win(self, tester_config):
438 return tester_config.get('os_type') == 'win'
439
440 def is_win64(self, tester_config):
441 return (tester_config.get('os_type') == 'win' and
442 tester_config.get('browser_config') == 'release_x64')
443
Garrett Beatyffe83c4f2023-09-08 19:07:37444 def get_exception_for_test(self, test_config):
445 return self.exceptions.get(test_config['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28446
Garrett Beatyffe83c4f2023-09-08 19:07:37447 def should_run_on_tester(self, waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28448 # Currently, the only reason a test should not run on a given tester is that
449 # it's in the exceptions. (Once the GPU waterfall generation script is
450 # incorporated here, the rules will become more complex.)
Garrett Beatyffe83c4f2023-09-08 19:07:37451 exception = self.get_exception_for_test(test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28452 if not exception:
453 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28454 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28455 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28456 if remove_from:
457 if tester_name in remove_from:
458 return False
459 # TODO(kbr): this code path was added for some tests (including
460 # android_webview_unittests) on one machine (Nougat Phone
461 # Tester) which exists with the same name on two waterfalls,
462 # chromium.android and chromium.fyi; the tests are run on one
463 # but not the other. Once the bots are all uniquely named (a
464 # different ongoing project) this code should be removed.
465 # TODO(kbr): add coverage.
466 return (tester_name + ' ' + waterfall['name']
467 not in remove_from) # pragma: no cover
468 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28469
Garrett Beatyffe83c4f2023-09-08 19:07:37470 def get_test_modifications(self, test, tester_name):
471 exception = self.get_exception_for_test(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28472 if not exception:
473 return None
Nico Weber79dc5f6852018-07-13 19:38:49474 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28475
Garrett Beatyffe83c4f2023-09-08 19:07:37476 def get_test_replacements(self, test, tester_name):
477 exception = self.get_exception_for_test(test)
Brian Sheedye6ea0ee2019-07-11 02:54:37478 if not exception:
479 return None
480 return exception.get('replacements', {}).get(tester_name)
481
Kenneth Russell8a386d42018-06-02 09:48:01482 def merge_command_line_args(self, arr, prefix, splitter):
483 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01484 idx = 0
485 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01486 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01487 while idx < len(arr):
488 flag = arr[idx]
489 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01490 if flag.startswith(prefix):
491 arg = flag[prefix_len:]
492 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01493 if first_idx < 0:
494 first_idx = idx
495 else:
496 delete_current_entry = True
497 if delete_current_entry:
498 del arr[idx]
499 else:
500 idx += 1
501 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01502 arr[first_idx] = prefix + splitter.join(accumulated_args)
503 return arr
504
505 def maybe_fixup_args_array(self, arr):
506 # The incoming array of strings may be an array of command line
507 # arguments. To make it easier to turn on certain features per-bot or
508 # per-test-suite, look specifically for certain flags and merge them
509 # appropriately.
510 # --enable-features=Feature1 --enable-features=Feature2
511 # are merged to:
512 # --enable-features=Feature1,Feature2
513 # and:
514 # --extra-browser-args=arg1 --extra-browser-args=arg2
515 # are merged to:
516 # --extra-browser-args=arg1 arg2
517 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
518 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Yuly Novikov8c487e72020-10-16 20:00:29519 arr = self.merge_command_line_args(arr, '--test-launcher-filter-file=', ';')
Cameron Higgins971f0b92023-01-03 18:05:09520 arr = self.merge_command_line_args(arr, '--extra-app-args=', ',')
Kenneth Russell650995a2018-05-03 21:17:01521 return arr
522
Brian Sheedy910cda82022-07-19 11:58:34523 def substitute_magic_args(self, test_config, tester_name, tester_config):
Brian Sheedya31578e2020-05-18 20:24:36524 """Substitutes any magic substitution args present in |test_config|.
525
526 Substitutions are done in-place.
527
528 See buildbot_json_magic_substitutions.py for more information on this
529 feature.
530
531 Args:
532 test_config: A dict containing a configuration for a specific test on
533 a specific builder, e.g. the output of update_and_cleanup_test.
Brian Sheedy5f173bb2021-11-24 00:45:54534 tester_name: A string containing the name of the tester that |test_config|
535 came from.
Brian Sheedy910cda82022-07-19 11:58:34536 tester_config: A dict containing the configuration for the builder that
537 |test_config| is for.
Brian Sheedya31578e2020-05-18 20:24:36538 """
539 substituted_array = []
Brian Sheedyba13cf522022-09-13 21:00:09540 original_args = test_config.get('args', [])
541 for arg in original_args:
Brian Sheedya31578e2020-05-18 20:24:36542 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
543 function = arg.replace(
544 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
545 if hasattr(magic_substitutions, function):
546 substituted_array.extend(
Brian Sheedy910cda82022-07-19 11:58:34547 getattr(magic_substitutions, function)(test_config, tester_name,
548 tester_config))
Brian Sheedya31578e2020-05-18 20:24:36549 else:
550 raise BBGenErr(
551 'Magic substitution function %s does not exist' % function)
552 else:
553 substituted_array.append(arg)
Brian Sheedyba13cf522022-09-13 21:00:09554 if substituted_array != original_args:
Brian Sheedya31578e2020-05-18 20:24:36555 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
556
Garrett Beaty8d6708c2023-07-20 17:20:41557 def dictionary_merge(self, a, b, path=None):
Kenneth Russelleb60cbd22017-12-05 07:54:28558 """http://stackoverflow.com/questions/7204805/
559 python-dictionaries-of-dictionaries-merge
560 merges b into a
561 """
562 if path is None:
563 path = []
564 for key in b:
Garrett Beaty8d6708c2023-07-20 17:20:41565 if key not in a:
566 if b[key] is not None:
567 a[key] = b[key]
568 continue
569
570 if isinstance(a[key], dict) and isinstance(b[key], dict):
571 self.dictionary_merge(a[key], b[key], path + [str(key)])
572 elif a[key] == b[key]:
573 pass # same leaf value
574 elif isinstance(a[key], list) and isinstance(b[key], list):
Garrett Beatyade673d2023-08-04 22:00:25575 a[key] = a[key] + b[key]
576 if key.endswith('args'):
577 a[key] = self.maybe_fixup_args_array(a[key])
Garrett Beaty8d6708c2023-07-20 17:20:41578 elif b[key] is None:
579 del a[key]
580 else:
Kenneth Russelleb60cbd22017-12-05 07:54:28581 a[key] = b[key]
Garrett Beaty8d6708c2023-07-20 17:20:41582
Kenneth Russelleb60cbd22017-12-05 07:54:28583 return a
584
John Budorickab108712018-09-01 00:12:21585 def initialize_args_for_test(
586 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21587 args = []
588 args.extend(generated_test.get('args', []))
589 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22590
Kenneth Russell8a386d42018-06-02 09:48:01591 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21592 val = generated_test.pop(key, [])
593 if fn(tester_config):
594 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01595
596 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
Brian Sheedy781c8ca42021-03-08 22:03:21597 add_conditional_args('lacros_args', self.is_lacros)
Kenneth Russell8a386d42018-06-02 09:48:01598 add_conditional_args('linux_args', self.is_linux)
599 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36600 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49601 add_conditional_args('mac_args', self.is_mac)
602 add_conditional_args('win_args', self.is_win)
603 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01604
John Budorickab108712018-09-01 00:12:21605 for key in additional_arg_keys or []:
606 args.extend(generated_test.pop(key, []))
607 args.extend(tester_config.get(key, []))
608
609 if args:
610 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01611
Kenneth Russelleb60cbd22017-12-05 07:54:28612 def initialize_swarming_dictionary_for_test(self, generated_test,
613 tester_config):
614 if 'swarming' not in generated_test:
615 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28616 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
617 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38618 'can_use_on_swarming_builders': tester_config.get('use_swarming',
619 True)
Dirk Pranke81ff51c2017-12-09 19:24:28620 })
Kenneth Russelleb60cbd22017-12-05 07:54:28621 if 'swarming' in tester_config:
Kenneth Russelleb60cbd22017-12-05 07:54:28622 self.dictionary_merge(generated_test['swarming'],
623 tester_config['swarming'])
Brian Sheedybc984e242021-04-21 23:44:51624 # Apply any platform-specific Swarming dimensions after the generic ones.
Kenneth Russelleb60cbd22017-12-05 07:54:28625 if 'android_swarming' in generated_test:
626 if self.is_android(tester_config): # pragma: no cover
627 self.dictionary_merge(
628 generated_test['swarming'],
629 generated_test['android_swarming']) # pragma: no cover
630 del generated_test['android_swarming'] # pragma: no cover
Brian Sheedybc984e242021-04-21 23:44:51631 if 'chromeos_swarming' in generated_test:
632 if self.is_chromeos(tester_config): # pragma: no cover
633 self.dictionary_merge(
634 generated_test['swarming'],
635 generated_test['chromeos_swarming']) # pragma: no cover
636 del generated_test['chromeos_swarming'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28637
638 def clean_swarming_dictionary(self, swarming_dict):
639 # Clean out redundant entries from a test's "swarming" dictionary.
640 # This is really only needed to retain 100% parity with the
641 # handwritten JSON files, and can be removed once all the files are
642 # autogenerated.
643 if 'shards' in swarming_dict:
644 if swarming_dict['shards'] == 1: # pragma: no cover
645 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24646 if 'hard_timeout' in swarming_dict:
647 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
648 del swarming_dict['hard_timeout'] # pragma: no cover
Garrett Beatybb18d532023-06-26 22:16:33649 del swarming_dict['can_use_on_swarming_builders']
Kenneth Russelleb60cbd22017-12-05 07:54:28650
Stephen Martinis0382bc12018-09-17 22:29:07651 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
652 waterfall):
653 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01654 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07655 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28656 # See if there are any exceptions that need to be merged into this
657 # test's specification.
Garrett Beatyffe83c4f2023-09-08 19:07:37658 modifications = self.get_test_modifications(test, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28659 if modifications:
660 test = self.dictionary_merge(test, modifications)
Garrett Beatybfeff8f2023-06-16 18:57:25661 if (swarming_dict := test.get('swarming')) is not None:
Garrett Beatybb18d532023-06-26 22:16:33662 if swarming_dict.get('can_use_on_swarming_builders'):
Garrett Beatybfeff8f2023-06-16 18:57:25663 self.clean_swarming_dictionary(swarming_dict)
664 else:
665 del test['swarming']
Ben Pastenee012aea42019-05-14 22:32:28666 # Ensure all Android Swarming tests run only on userdebug builds if another
667 # build type was not specified.
668 if 'swarming' in test and self.is_android(tester_config):
Garrett Beatyade673d2023-08-04 22:00:25669 dimensions = test.get('swarming', {}).get('dimensions', {})
670 if (dimensions.get('os') == 'Android'
671 and not dimensions.get('device_os_type')):
672 dimensions['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37673 self.replace_test_args(test, test_name, tester_name)
Garrett Beatyafd33e0f2023-06-23 20:47:57674 if 'args' in test and not test['args']:
675 test.pop('args')
Ben Pastenee012aea42019-05-14 22:32:28676
Kenneth Russelleb60cbd22017-12-05 07:54:28677 return test
678
Brian Sheedye6ea0ee2019-07-11 02:54:37679 def replace_test_args(self, test, test_name, tester_name):
Garrett Beatyffe83c4f2023-09-08 19:07:37680 replacements = self.get_test_replacements(test, tester_name) or {}
Brian Sheedye6ea0ee2019-07-11 02:54:37681 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23682 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37683 if key not in valid_replacement_keys:
684 raise BBGenErr(
685 'Given replacement key %s for %s on %s is not in the list of valid '
686 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23687 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37688 found_key = False
689 for i, test_key in enumerate(test.get(key, [])):
690 # Handle both the key/value being replaced being defined as two
691 # separate items or as key=value.
692 if test_key == replacement_key:
693 found_key = True
694 # Handle flags without values.
695 if replacement_val == None:
696 del test[key][i]
697 else:
698 test[key][i+1] = replacement_val
699 break
Joshua Hood56c673c2022-03-02 20:29:33700 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37701 found_key = True
702 if replacement_val == None:
703 del test[key][i]
704 else:
705 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
706 break
707 if not found_key:
708 raise BBGenErr('Could not find %s in existing list of values for key '
709 '%s in %s on %s' % (replacement_key, key, test_name,
710 tester_name))
711
Shenghua Zhangaba8bad2018-02-07 02:12:09712 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05713 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26714 True):
715 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06716 # are targeting CrOS hardware and so need the special trigger script.
Garrett Beatyade673d2023-08-04 22:00:25717 if 'device_type' in test.get('swarming', {}).get('dimensions', {}):
Ben Pastenea9e583b2019-01-16 02:57:26718 test['trigger_script'] = {
719 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
720 }
Shenghua Zhangaba8bad2018-02-07 02:12:09721
Garrett Beatyffe83c4f2023-09-08 19:07:37722 def add_android_presentation_args(self, tester_config, result):
Ben Pastene858f4be2019-01-09 23:52:09723 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38724 bucket = tester_config.get('results_bucket', 'chromium-result-details')
725 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09726 if (result['swarming']['can_use_on_swarming_builders'] and not
727 tester_config.get('skip_merge_script', False)):
728 result['merge'] = {
Garrett Beatyffe83c4f2023-09-08 19:07:37729 'args': [
730 '--bucket',
731 bucket,
732 '--test-name',
733 result['name'],
734 ],
735 'script': ('//build/android/pylib/results/presentation/'
736 'test_results_presentation.py'),
Ben Pastene858f4be2019-01-09 23:52:09737 }
Ben Pastene858f4be2019-01-09 23:52:09738 if not tester_config.get('skip_output_links', False):
739 result['swarming']['output_links'] = [
740 {
741 'link': [
742 'https://luci-logdog.appspot.com/v/?s',
743 '=android%2Fswarming%2Flogcats%2F',
744 '${TASK_ID}%2F%2B%2Funified_logcats',
745 ],
746 'name': 'shard #${SHARD_INDEX} logcats',
747 },
748 ]
749 if args:
750 result['args'] = args
751
Kenneth Russelleb60cbd22017-12-05 07:54:28752 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
753 test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37754 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28755 return None
756 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37757 # Use test_name here instead of test['name'] because test['name'] will be
758 # modified with the variant identifier in a matrix compound suite
759 result.setdefault('test', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28760 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21761
762 self.initialize_args_for_test(
763 result, tester_config, additional_arg_keys=['gtest_args'])
Jamie Madilla8be0d72020-10-02 05:24:04764 if self.is_android(tester_config) and tester_config.get(
Yuly Novikov26dd47052021-02-11 00:57:14765 'use_swarming', True):
766 if not test_config.get('use_isolated_scripts_api', False):
767 # TODO(https://crbug.com/1137998) make Android presentation work with
768 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37769 self.add_android_presentation_args(tester_config, result)
Yuly Novikov26dd47052021-02-11 00:57:14770 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42771
Stephen Martinis0382bc12018-09-17 22:29:07772 result = self.update_and_cleanup_test(
773 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09774 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34775 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43776
Garrett Beatybb18d532023-06-26 22:16:33777 if 'swarming' in result and not result.get('merge'):
Jamie Madilla8be0d72020-10-02 05:24:04778 if test_config.get('use_isolated_scripts_api', False):
779 merge_script = 'standard_isolated_script_merge'
780 else:
781 merge_script = 'standard_gtest_merge'
782
Stephen Martinisbc7b7772019-05-01 22:01:43783 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04784 'script': '//testing/merge_scripts/%s.py' % merge_script,
Stephen Martinisbc7b7772019-05-01 22:01:43785 }
Kenneth Russelleb60cbd22017-12-05 07:54:28786 return result
787
788 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
789 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37790 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28791 return None
792 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37793 # Use test_name here instead of test['name'] because test['name'] will be
794 # modified with the variant identifier in a matrix compound suite
Kenneth Russelleb60cbd22017-12-05 07:54:28795 result['isolate_name'] = result.get('isolate_name', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28796 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01797 self.initialize_args_for_test(result, tester_config)
Yuly Novikov26dd47052021-02-11 00:57:14798 if self.is_android(tester_config) and tester_config.get(
799 'use_swarming', True):
800 if tester_config.get('use_android_presentation', False):
801 # TODO(https://crbug.com/1137998) make Android presentation work with
802 # isolated scripts in test_results_presentation.py merge script
Garrett Beatyffe83c4f2023-09-08 19:07:37803 self.add_android_presentation_args(tester_config, result)
Stephen Martinis0382bc12018-09-17 22:29:07804 result = self.update_and_cleanup_test(
805 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09806 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34807 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17808
Garrett Beatybb18d532023-06-26 22:16:33809 if 'swarming' in result and not result.get('merge'):
Stephen Martinisf50047062019-05-06 22:26:17810 # TODO(https://crbug.com/958376): Consider adding the ability to not have
811 # this default.
812 result['merge'] = {
813 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
Stephen Martinisf50047062019-05-06 22:26:17814 }
Kenneth Russelleb60cbd22017-12-05 07:54:28815 return result
816
817 def generate_script_test(self, waterfall, tester_name, tester_config,
818 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44819 # TODO(https://crbug.com/953072): Remove this check whenever a better
820 # long-term solution is implemented.
821 if (waterfall.get('forbid_script_tests', False) or
822 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
823 raise BBGenErr('Attempted to generate a script test on tester ' +
824 tester_name + ', which explicitly forbids script tests')
Garrett Beatyffe83c4f2023-09-08 19:07:37825 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28826 return None
827 result = {
Garrett Beatyffe83c4f2023-09-08 19:07:37828 'name': test_config['name'],
829 'script': test_config['script'],
Kenneth Russelleb60cbd22017-12-05 07:54:28830 }
Stephen Martinis0382bc12018-09-17 22:29:07831 result = self.update_and_cleanup_test(
832 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34833 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28834 return result
835
836 def generate_junit_test(self, waterfall, tester_name, tester_config,
837 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37838 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28839 return None
John Budorickdef6acb2019-09-17 22:51:09840 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37841 # Use test_name here instead of test['name'] because test['name'] will be
842 # modified with the variant identifier in a matrix compound suite
843 result.setdefault('test', test_name)
John Budorickdef6acb2019-09-17 22:51:09844 self.initialize_args_for_test(result, tester_config)
845 result = self.update_and_cleanup_test(
846 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34847 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28848 return result
849
Xinan Lin05fb9c1752020-12-17 00:15:52850 def generate_skylab_test(self, waterfall, tester_name, tester_config,
851 test_name, test_config):
Garrett Beatyffe83c4f2023-09-08 19:07:37852 if not self.should_run_on_tester(waterfall, tester_name, test_config):
Xinan Lin05fb9c1752020-12-17 00:15:52853 return None
854 result = copy.deepcopy(test_config)
Garrett Beatyffe83c4f2023-09-08 19:07:37855 # Use test_name here instead of test['name'] because test['name'] will be
856 # modified with the variant identifier in a matrix compound suite
857 result['test'] = test_name
Xinan Lin05fb9c1752020-12-17 00:15:52858 self.initialize_args_for_test(result, tester_config)
859 result = self.update_and_cleanup_test(result, test_name, tester_name,
860 tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34861 self.substitute_magic_args(result, tester_name, tester_config)
Xinan Lin05fb9c1752020-12-17 00:15:52862 return result
863
Garrett Beaty65d44222023-08-01 17:22:11864 def substitute_gpu_args(self, tester_config, test, args):
Kenneth Russell8a386d42018-06-02 09:48:01865 substitutions = {
866 # Any machine in waterfalls.pyl which desires to run GPU tests
867 # must provide the os_type key.
868 'os_type': tester_config['os_type'],
869 'gpu_vendor_id': '0',
870 'gpu_device_id': '0',
871 }
Garrett Beatyade673d2023-08-04 22:00:25872 dimensions = test.get('swarming', {}).get('dimensions', {})
873 if 'gpu' in dimensions:
874 # First remove the driver version, then split into vendor and device.
875 gpu = dimensions['gpu']
876 if gpu != 'none':
877 gpu = gpu.split('-')[0].split(':')
878 substitutions['gpu_vendor_id'] = gpu[0]
879 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01880 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
881
882 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30883 test_name, test_config, is_android_webview,
884 is_cast_streaming):
Kenneth Russell8a386d42018-06-02 09:48:01885 # These are all just specializations of isolated script tests with
886 # a bunch of boilerplate command line arguments added.
887
888 # The step name must end in 'test' or 'tests' in order for the
889 # results to automatically show up on the flakiness dashboard.
890 # (At least, this was true some time ago.) Continue to use this
891 # naming convention for the time being to minimize changes.
Garrett Beaty235c1412023-08-29 20:26:29892 #
893 # test name is the name of the test without the variant ID added
894 if not (test_name.endswith('test') or test_name.endswith('tests')):
895 raise BBGenErr(
896 f'telemetry test names must end with test or tests, got {test_name}')
Garrett Beatyffe83c4f2023-09-08 19:07:37897 result = self.generate_isolated_script_test(waterfall, tester_name,
898 tester_config, test_name,
899 test_config)
Kenneth Russell8a386d42018-06-02 09:48:01900 if not result:
901 return None
Chong Gub75754b32020-03-13 16:39:20902 result['isolate_name'] = test_config.get(
Brian Sheedyf74819b2021-06-04 01:38:38903 'isolate_name',
904 self.get_default_isolate_name(tester_config, is_android_webview))
Chan Liab7d8dd82020-04-24 23:42:19905
Chan Lia3ad1502020-04-28 05:32:11906 # Populate test_id_prefix.
Brian Sheedyf74819b2021-06-04 01:38:38907 gn_entry = self.gn_isolate_map[result['isolate_name']]
Chan Li17d969f92020-07-10 00:50:03908 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19909
Kenneth Russell8a386d42018-06-02 09:48:01910 args = result.get('args', [])
Garrett Beatyffe83c4f2023-09-08 19:07:37911 # Use test_name here instead of test['name'] because test['name'] will be
912 # modified with the variant identifier in a matrix compound suite
Kenneth Russell8a386d42018-06-02 09:48:01913 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14914
erikchen6da2d9b2018-08-03 23:01:14915 # These tests upload and download results from cloud storage and therefore
916 # aren't idempotent yet. https://crbug.com/549140.
Garrett Beatybfeff8f2023-06-16 18:57:25917 if 'swarming' in result:
918 result['swarming']['idempotent'] = False
erikchen6da2d9b2018-08-03 23:01:14919
Kenneth Russell44910c32018-12-03 23:35:11920 # The GPU tests act much like integration tests for the entire browser, and
921 # tend to uncover flakiness bugs more readily than other test suites. In
922 # order to surface any flakiness more readily to the developer of the CL
923 # which is introducing it, we disable retries with patch on the commit
924 # queue.
925 result['should_retry_with_patch'] = False
926
Fabrice de Ganscbd655f2022-08-04 20:15:30927 browser = ''
928 if is_cast_streaming:
929 browser = 'cast-streaming-shell'
930 elif is_android_webview:
931 browser = 'android-webview-instrumentation'
932 else:
933 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:52934
Greg Thompsoncec7d8d2023-01-10 19:11:53935 extra_browser_args = []
936
Brian Sheedy4053a702020-07-28 02:09:52937 # Most platforms require --enable-logging=stderr to get useful browser logs.
938 # However, this actively messes with logging on CrOS (because Chrome's
939 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
940 # in order to see JavaScript console messages. See
941 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
Greg Thompsoncec7d8d2023-01-10 19:11:53942 if self.is_chromeos(tester_config):
943 extra_browser_args.append('--log-level=0')
944 elif not self.is_fuchsia(tester_config) or browser != 'fuchsia-chrome':
945 # Stderr logging is not needed for Chrome browser on Fuchsia, as ordinary
946 # logging via syslog is captured.
947 extra_browser_args.append('--enable-logging=stderr')
948
949 # --expose-gc allows the WebGL conformance tests to more reliably
950 # reproduce GC-related bugs in the V8 bindings.
951 extra_browser_args.append('--js-flags=--expose-gc')
Brian Sheedy4053a702020-07-28 02:09:52952
Kenneth Russell8a386d42018-06-02 09:48:01953 args = [
Bo Liu555a0f92019-03-29 12:11:56954 test_to_run,
955 '--show-stdout',
956 '--browser=%s' % browser,
957 # --passthrough displays more of the logging in Telemetry when
958 # run via typ, in particular some of the warnings about tests
959 # being expected to fail, but passing.
960 '--passthrough',
961 '-v',
Brian Sheedy814e0482022-10-03 23:24:12962 '--stable-jobs',
Greg Thompsoncec7d8d2023-01-10 19:11:53963 '--extra-browser-args=%s' % ' '.join(extra_browser_args),
Kenneth Russell8a386d42018-06-02 09:48:01964 ] + args
Garrett Beatybfeff8f2023-06-16 18:57:25965 result['args'] = self.maybe_fixup_args_array(
Garrett Beaty65d44222023-08-01 17:22:11966 self.substitute_gpu_args(tester_config, result, args))
Kenneth Russell8a386d42018-06-02 09:48:01967 return result
968
Brian Sheedyf74819b2021-06-04 01:38:38969 def get_default_isolate_name(self, tester_config, is_android_webview):
970 if self.is_android(tester_config):
971 if is_android_webview:
972 return 'telemetry_gpu_integration_test_android_webview'
973 return (
974 'telemetry_gpu_integration_test' +
975 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:33976 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:17977 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:33978 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:38979
Kenneth Russelleb60cbd22017-12-05 07:54:28980 def get_test_generator_map(self):
981 return {
Bo Liu555a0f92019-03-29 12:11:56982 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30983 GPUTelemetryTestGenerator(self, is_android_webview=True),
984 'cast_streaming_tests':
985 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:56986 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30987 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56988 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30989 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56990 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:30991 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56992 'junit_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30993 JUnitGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56994 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:30995 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:52996 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:30997 SkylabGenerator(self),
Brian Sheedyb6491ba2022-09-26 20:49:49998 'skylab_gpu_telemetry_tests':
999 SkylabGPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281000 }
1001
Kenneth Russell8a386d42018-06-02 09:48:011002 def get_test_type_remapper(self):
1003 return {
Fabrice de Gans223272482022-08-08 16:56:571004 # These are a specialization of isolated_scripts with a bunch of
1005 # boilerplate command line arguments added to each one.
1006 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
1007 'cast_streaming_tests': 'isolated_scripts',
1008 'gpu_telemetry_tests': 'isolated_scripts',
Brian Sheedyb6491ba2022-09-26 20:49:491009 # These are the same as existing test types, just configured to run
1010 # in Skylab instead of via normal swarming.
1011 'skylab_gpu_telemetry_tests': 'skylab_tests',
Kenneth Russell8a386d42018-06-02 09:48:011012 }
1013
Jeff Yoon67c3e832020-02-08 07:39:381014 def check_composition_type_test_suites(self, test_type,
1015 additional_validators=None):
1016 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1017 validators = [check_compound_references,
1018 check_basic_references,
1019 check_conflicting_definitions]
1020 if additional_validators:
1021 validators += additional_validators
1022
1023 target_suites = self.test_suites.get(test_type, {})
1024 other_test_type = ('compound_suites'
1025 if test_type == 'matrix_compound_suites'
1026 else 'matrix_compound_suites')
1027 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011028 basic_suites = self.test_suites.get('basic_suites', {})
1029
Jamie Madillcf4f8c72021-05-20 19:24:231030 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011031 if suite in basic_suites:
1032 raise BBGenErr('%s names may not duplicate basic test suite names '
1033 '(error found while processsing %s)'
1034 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011035
Jeff Yoon67c3e832020-02-08 07:39:381036 seen_tests = {}
1037 for sub_suite in suite_def:
1038 for validator in validators:
1039 validator(
1040 basic_suites=basic_suites,
1041 other_test_suites=other_suites,
1042 seen_tests=seen_tests,
1043 sub_suite=sub_suite,
1044 suite=suite,
1045 suite_def=suite_def,
1046 target_test_suites=target_suites,
1047 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051048 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381049 )
Kenneth Russelleb60cbd22017-12-05 07:54:281050
Stephen Martinis54d64ad2018-09-21 22:16:201051 def flatten_test_suites(self):
1052 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011053 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1054 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231055 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011056 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201057 self.test_suites = new_test_suites
1058
Chan Lia3ad1502020-04-28 05:32:111059 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231060 for suite in self.test_suites['basic_suites'].values():
1061 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561062 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411063
1064 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
John Palmera8515fca2021-05-20 03:35:321065 # https://source.chromium.org/chromium/chromium/tools/build/+/main:scripts/slave/recipe_modules/chromium_tests/generators.py;l=89;drc=14c062ba0eb418d3c4623dde41a753241b9df06b
Nodir Turakulovfce34292019-12-18 17:05:411066 # TODO(crbug.com/1035124): clean this up.
1067 isolate_name = test.get('test') or test.get('isolate_name') or key
1068 gn_entry = self.gn_isolate_map.get(isolate_name)
1069 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281070 label = gn_entry['label']
1071
1072 if label.count(':') != 1:
1073 raise BBGenErr(
1074 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1075 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1076 (label, isolate_name))
1077 if label.split(':')[1] != isolate_name:
1078 raise BBGenErr(
1079 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1080 ' label "%s" see http://crbug.com/1071091 for details.' %
1081 (isolate_name, label))
1082
Chan Lia3ad1502020-04-28 05:32:111083 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411084 else: # pragma: no cover
1085 # Some tests do not have an entry gn_isolate_map.pyl, such as
1086 # telemetry tests.
1087 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
1088 pass
1089
Kenneth Russelleb60cbd22017-12-05 07:54:281090 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011091 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201092
Jeff Yoon8154e582019-12-03 23:30:011093 compound_suites = self.test_suites.get('compound_suites', {})
1094 # check_composition_type_test_suites() checks that all basic suites
1095 # referenced by compound suites exist.
1096 basic_suites = self.test_suites.get('basic_suites')
1097
Jamie Madillcf4f8c72021-05-20 19:24:231098 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011099 # Resolve this to a dictionary.
1100 full_suite = {}
1101 for entry in value:
1102 suite = basic_suites[entry]
1103 full_suite.update(suite)
1104 compound_suites[name] = full_suite
1105
Jeff Yoon85fb8df2020-08-20 16:47:431106 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381107 """ Merge variant-defined configurations to each test case definition in a
1108 test suite.
1109
1110 The output maps a unique test name to an array of configurations because
1111 there may exist more than one definition for a test name using variants. The
1112 test name is referenced while mapping machines to test suites, so unpacking
1113 the array is done by the generators.
1114
1115 Args:
1116 basic_test_definition: a {} defined test suite in the format
1117 test_name:test_config
1118 variants: an [] of {} defining configurations to be applied to each test
1119 case in the basic test_definition
1120
1121 Return:
1122 a {} of test_name:[{}], where each {} is a merged configuration
1123 """
1124
1125 # Each test in a basic test suite will have a definition per variant.
1126 test_suite = {}
Garrett Beaty8d6708c2023-07-20 17:20:411127 for variant in variants:
1128 # Unpack the variant from variants.pyl if it's string based.
1129 if isinstance(variant, str):
1130 variant = self.variants[variant]
Jeff Yoonda581c32020-03-06 03:56:051131
Garrett Beaty8d6708c2023-07-20 17:20:411132 # If 'enabled' is set to False, we will not use this variant; otherwise if
1133 # the variant doesn't include 'enabled' variable or 'enabled' is set to
1134 # True, we will use this variant
1135 if not variant.get('enabled', True):
1136 continue
Jeff Yoon67c3e832020-02-08 07:39:381137
Garrett Beaty8d6708c2023-07-20 17:20:411138 # Make a shallow copy of the variant to remove variant-specific fields,
1139 # leaving just mixin fields
1140 variant = copy.copy(variant)
1141 variant.pop('enabled', None)
1142 identifier = variant.pop('identifier')
1143 variant_mixins = variant.pop('mixins', [])
1144 variant_skylab = variant.pop('skylab', {})
Jeff Yoon67c3e832020-02-08 07:39:381145
Garrett Beaty8d6708c2023-07-20 17:20:411146 for test_name, test_config in basic_test_definition.items():
1147 new_test = self.apply_mixin(variant, test_config)
Jeff Yoon67c3e832020-02-08 07:39:381148
Garrett Beaty8d6708c2023-07-20 17:20:411149 new_test['mixins'] = (test_config.get('mixins', []) + variant_mixins +
1150 mixins)
Xinan Lin05fb9c1752020-12-17 00:15:521151
Jeff Yoon67c3e832020-02-08 07:39:381152 # The identifier is used to make the name of the test unique.
1153 # Generators in the recipe uniquely identify a test by it's name, so we
1154 # don't want to have the same name for each variant.
Garrett Beaty235c1412023-08-29 20:26:291155 new_test['name'] = f'{test_name} {identifier}'
Ben Pastene5f231cf22022-05-05 18:03:071156
1157 # Attach the variant identifier to the test config so downstream
1158 # generators can make modifications based on the original name. This
1159 # is mainly used in generate_gpu_telemetry_test().
Garrett Beaty8d6708c2023-07-20 17:20:411160 new_test['variant_id'] = identifier
Ben Pastene5f231cf22022-05-05 18:03:071161
Garrett Beaty8d6708c2023-07-20 17:20:411162 # cros_chrome_version is the ash chrome version in the cros img in the
1163 # variant of cros_board. We don't want to include it in the final json
1164 # files; so remove it.
1165 for k, v in variant_skylab.items():
1166 if k != 'cros_chrome_version':
1167 new_test[k] = v
1168
1169 test_suite.setdefault(test_name, []).append(new_test)
1170
Jeff Yoon67c3e832020-02-08 07:39:381171 return test_suite
1172
Jeff Yoon8154e582019-12-03 23:30:011173 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381174 self.check_composition_type_test_suites('matrix_compound_suites',
1175 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011176
1177 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381178 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011179 # referenced by matrix suites exist.
1180 basic_suites = self.test_suites.get('basic_suites')
1181
Garrett Beaty235c1412023-08-29 20:26:291182 for matrix_suite_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011183 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381184
Jamie Madillcf4f8c72021-05-20 19:24:231185 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381186 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1187
Garrett Beaty235c1412023-08-29 20:26:291188 def update_tests(expanded):
1189 for test_name, new_tests in expanded.items():
1190 if not isinstance(new_tests, list):
1191 new_tests = [new_tests]
1192 tests_for_name = full_suite.setdefault(test_name, [])
1193 for t in new_tests:
1194 if t not in tests_for_name:
1195 tests_for_name.append(t)
1196
Garrett Beaty60a7b2a2023-09-13 23:00:401197 if (variants := mtx_test_suite_config.get('variants')):
Jeff Yoon85fb8df2020-08-20 16:47:431198 mixins = mtx_test_suite_config.get('mixins', [])
Garrett Beaty60a7b2a2023-09-13 23:00:401199 result = self.resolve_variants(basic_test_def, variants, mixins)
Garrett Beaty235c1412023-08-29 20:26:291200 update_tests(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271201 else:
1202 suite = basic_suites[test_suite]
Garrett Beaty235c1412023-08-29 20:26:291203 update_tests(suite)
1204 matrix_compound_suites[matrix_suite_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281205
1206 def link_waterfalls_to_test_suites(self):
1207 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231208 for tester_name, tester in waterfall['machines'].items():
1209 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281210 if not value in self.test_suites:
1211 # Hard / impossible to cover this in the unit test.
1212 raise self.unknown_test_suite(
1213 value, tester_name, waterfall['name']) # pragma: no cover
1214 tester['test_suites'][suite] = self.test_suites[value]
1215
1216 def load_configuration_files(self):
Garrett Beaty79339e182023-04-10 20:45:471217 self.waterfalls = self.load_pyl_file(self.args.waterfalls_pyl_path)
1218 self.test_suites = self.load_pyl_file(self.args.test_suites_pyl_path)
1219 self.exceptions = self.load_pyl_file(
1220 self.args.test_suite_exceptions_pyl_path)
1221 self.mixins = self.load_pyl_file(self.args.mixins_pyl_path)
1222 self.gn_isolate_map = self.load_pyl_file(self.args.gn_isolate_map_pyl_path)
Chong Guee622242020-10-28 18:17:351223 for isolate_map in self.args.isolate_map_files:
1224 isolate_map = self.load_pyl_file(isolate_map)
1225 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1226 if duplicates:
1227 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1228 ', '.join(duplicates))
1229 self.gn_isolate_map.update(isolate_map)
1230
Garrett Beaty79339e182023-04-10 20:45:471231 self.variants = self.load_pyl_file(self.args.variants_pyl_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281232
1233 def resolve_configuration_files(self):
Garrett Beaty235c1412023-08-29 20:26:291234 self.resolve_test_names()
Garrett Beaty65d44222023-08-01 17:22:111235 self.resolve_dimension_sets()
Chan Lia3ad1502020-04-28 05:32:111236 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281237 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011238 self.resolve_matrix_compound_test_suites()
1239 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281240 self.link_waterfalls_to_test_suites()
1241
Garrett Beaty235c1412023-08-29 20:26:291242 def resolve_test_names(self):
1243 for suite_name, suite in self.test_suites.get('basic_suites').items():
1244 for test_name, test in suite.items():
1245 if 'name' in test:
1246 raise BBGenErr(
1247 f'The name field is set in test {test_name} in basic suite '
1248 f'{suite_name}, this is not supported, the test name is the key '
1249 'within the basic suite')
Garrett Beatyffe83c4f2023-09-08 19:07:371250 # When a test is expanded with variants, this will be overwritten, but
1251 # this ensures every test definition has the name field set
1252 test['name'] = test_name
Garrett Beaty235c1412023-08-29 20:26:291253
Garrett Beaty65d44222023-08-01 17:22:111254 def resolve_dimension_sets(self):
Garrett Beaty65d44222023-08-01 17:22:111255
1256 def definitions():
1257 for suite_name, suite in self.test_suites.get('basic_suites', {}).items():
1258 for test_name, test in suite.items():
1259 yield test, f'test {test_name} in basic suite {suite_name}'
1260
1261 for mixin_name, mixin in self.mixins.items():
1262 yield mixin, f'mixin {mixin_name}'
1263
1264 for waterfall in self.waterfalls:
1265 for builder_name, builder in waterfall.get('machines', {}).items():
1266 yield (
1267 builder,
1268 f'builder {builder_name} in waterfall {waterfall["name"]}',
1269 )
1270
1271 for test_name, exceptions in self.exceptions.items():
1272 modifications = exceptions.get('modifications', {})
1273 for builder_name, mods in modifications.items():
1274 yield (
1275 mods,
1276 f'exception for test {test_name} on builder {builder_name}',
1277 )
1278
1279 for definition, location in definitions():
1280 for swarming_attr in (
1281 'swarming',
1282 'android_swarming',
1283 'chromeos_swarming',
1284 ):
1285 if (swarming :=
1286 definition.get(swarming_attr)) and 'dimension_sets' in swarming:
Garrett Beatyade673d2023-08-04 22:00:251287 raise BBGenErr(
1288 f'dimension_sets is no longer supported (set in {location}),'
1289 ' instead, use set dimensions to a single dict')
Garrett Beaty65d44222023-08-01 17:22:111290
Nico Weberd18b8962018-05-16 19:39:381291 def unknown_bot(self, bot_name, waterfall_name):
1292 return BBGenErr(
1293 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1294
Kenneth Russelleb60cbd22017-12-05 07:54:281295 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1296 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381297 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281298 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1299
1300 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1301 return BBGenErr(
1302 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1303 ' on waterfall ' + waterfall_name)
1304
Stephen Martinisb72f6d22018-10-04 23:29:011305 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071306 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321307
1308 Checks in the waterfall, builder, and test objects for mixins.
1309 """
1310 def valid_mixin(mixin_name):
1311 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011312 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321313 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381314
Stephen Martinisb6a50492018-09-12 23:59:321315 def must_be_list(mixins, typ, name):
1316 """Asserts that given mixins are a list."""
1317 if not isinstance(mixins, list):
1318 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1319
Garrett Beatyffe83c4f2023-09-08 19:07:371320 test_name = test['name']
Brian Sheedy7658c982020-01-08 02:27:581321 remove_mixins = set()
1322 if 'remove_mixins' in builder:
1323 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1324 for rm in builder['remove_mixins']:
1325 valid_mixin(rm)
1326 remove_mixins.add(rm)
1327 if 'remove_mixins' in test:
1328 must_be_list(test['remove_mixins'], 'test', test_name)
1329 for rm in test['remove_mixins']:
1330 valid_mixin(rm)
1331 remove_mixins.add(rm)
1332 del test['remove_mixins']
1333
Stephen Martinisb72f6d22018-10-04 23:29:011334 if 'mixins' in waterfall:
1335 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1336 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581337 if mixin in remove_mixins:
1338 continue
Stephen Martinisb6a50492018-09-12 23:59:321339 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531340 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321341
Stephen Martinisb72f6d22018-10-04 23:29:011342 if 'mixins' in builder:
1343 must_be_list(builder['mixins'], 'builder', builder_name)
1344 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581345 if mixin in remove_mixins:
1346 continue
Stephen Martinisb6a50492018-09-12 23:59:321347 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531348 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321349
Stephen Martinisb72f6d22018-10-04 23:29:011350 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071351 return test
1352
Stephen Martinisb72f6d22018-10-04 23:29:011353 must_be_list(test['mixins'], 'test', test_name)
1354 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581355 # We don't bother checking if the given mixin is in remove_mixins here
1356 # since this is already the lowest level, so if a mixin is added here that
1357 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071358 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531359 test = self.apply_mixin(self.mixins[mixin], test, builder)
Jeff Yoon67c3e832020-02-08 07:39:381360 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071361 return test
Stephen Martinisb6a50492018-09-12 23:59:321362
Garrett Beaty8d6708c2023-07-20 17:20:411363 def apply_mixin(self, mixin, test, builder=None):
Stephen Martinisb72f6d22018-10-04 23:29:011364 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321365
Garrett Beaty4c35b142023-06-23 21:01:231366 A mixin is applied by copying all fields from the mixin into the
1367 test with the following exceptions:
1368 * For the various *args keys, the test's existing value (an empty
1369 list if not present) will be extended with the mixin's value.
1370 * The sub-keys of the swarming value will be copied to the test's
1371 swarming value with the following exceptions:
Garrett Beatyade673d2023-08-04 22:00:251372 * For the named_caches sub-keys, the test's existing value (an
1373 empty list if not present) will be extended with the mixin's
1374 value.
1375 * For the dimensions sub-key, the tests's existing value (an empty
1376 dict if not present) will be updated with the mixin's value.
Stephen Martinisb6a50492018-09-12 23:59:321377 """
Garrett Beaty4c35b142023-06-23 21:01:231378
Stephen Martinisb6a50492018-09-12 23:59:321379 new_test = copy.deepcopy(test)
1380 mixin = copy.deepcopy(mixin)
Garrett Beaty8d6708c2023-07-20 17:20:411381
1382 if 'description' in mixin:
1383 description = []
1384 if 'description' in new_test:
1385 description.append(new_test['description'])
1386 description.append(mixin.pop('description'))
1387 new_test['description'] = '\n'.join(description)
1388
Stephen Martinisb72f6d22018-10-04 23:29:011389 if 'swarming' in mixin:
1390 swarming_mixin = mixin['swarming']
1391 new_test.setdefault('swarming', {})
Stephen Martinisb72f6d22018-10-04 23:29:011392 if 'dimensions' in swarming_mixin:
Garrett Beatyade673d2023-08-04 22:00:251393 new_test['swarming'].setdefault('dimensions', {}).update(
1394 swarming_mixin.pop('dimensions'))
Garrett Beaty4c35b142023-06-23 21:01:231395 if 'named_caches' in swarming_mixin:
1396 new_test['swarming'].setdefault('named_caches', []).extend(
1397 swarming_mixin['named_caches'])
1398 del swarming_mixin['named_caches']
Stephen Martinisb72f6d22018-10-04 23:29:011399 # python dict update doesn't do recursion at all. Just hard code the
1400 # nested update we need (mixin['swarming'] shouldn't clobber
1401 # test['swarming'], but should update it).
1402 new_test['swarming'].update(swarming_mixin)
1403 del mixin['swarming']
1404
Garrett Beaty4c35b142023-06-23 21:01:231405 # Array so we can assign to it in a nested scope.
1406 args_need_fixup = ['args' in mixin]
1407
1408 for a in (
1409 'args',
1410 'precommit_args',
1411 'non_precommit_args',
1412 'desktop_args',
1413 'lacros_args',
1414 'linux_args',
1415 'android_args',
1416 'chromeos_args',
1417 'mac_args',
1418 'win_args',
1419 'win64_args',
1420 ):
1421 if (value := mixin.pop(a, None)) is None:
1422 continue
1423 if not isinstance(value, list):
1424 raise BBGenErr(f'"{a}" must be a list')
1425 new_test.setdefault(a, []).extend(value)
1426
Garrett Beaty4c35b142023-06-23 21:01:231427 args = new_test.get('args', [])
Austin Eng148d9f0f2022-02-08 19:18:531428
Garrett Beaty4c35b142023-06-23 21:01:231429 def add_conditional_args(key, fn):
Garrett Beaty8d6708c2023-07-20 17:20:411430 if builder is None:
1431 return
Garrett Beaty4c35b142023-06-23 21:01:231432 val = new_test.pop(key, [])
1433 if val and fn(builder):
1434 args.extend(val)
1435 args_need_fixup[0] = True
Austin Eng148d9f0f2022-02-08 19:18:531436
Garrett Beaty4c35b142023-06-23 21:01:231437 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
1438 add_conditional_args('lacros_args', self.is_lacros)
1439 add_conditional_args('linux_args', self.is_linux)
1440 add_conditional_args('android_args', self.is_android)
1441 add_conditional_args('chromeos_args', self.is_chromeos)
1442 add_conditional_args('mac_args', self.is_mac)
1443 add_conditional_args('win_args', self.is_win)
1444 add_conditional_args('win64_args', self.is_win64)
1445
1446 if args_need_fixup[0]:
1447 new_test['args'] = self.maybe_fixup_args_array(args)
Wezc0e835b702018-10-30 00:38:411448
Stephen Martinisb72f6d22018-10-04 23:29:011449 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321450 return new_test
1451
Greg Gutermanf60eb052020-03-12 17:40:011452 def generate_output_tests(self, waterfall):
1453 """Generates the tests for a waterfall.
1454
1455 Args:
1456 waterfall: a dictionary parsed from a master pyl file
1457 Returns:
1458 A dictionary mapping builders to test specs
1459 """
1460 return {
Jamie Madillcf4f8c72021-05-20 19:24:231461 name: self.get_tests_for_config(waterfall, name, config)
1462 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011463 }
1464
1465 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531466 generator_map = self.get_test_generator_map()
1467 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281468
Greg Gutermanf60eb052020-03-12 17:40:011469 tests = {}
1470 # Copy only well-understood entries in the machine's configuration
1471 # verbatim into the generated JSON.
1472 if 'additional_compile_targets' in config:
1473 tests['additional_compile_targets'] = config[
1474 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231475 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011476 if test_type not in generator_map:
1477 raise self.unknown_test_suite_type(
1478 test_type, name, waterfall['name']) # pragma: no cover
1479 test_generator = generator_map[test_type]
1480 # Let multiple kinds of generators generate the same kinds
1481 # of tests. For example, gpu_telemetry_tests are a
1482 # specialization of isolated_scripts.
1483 new_tests = test_generator.generate(
1484 waterfall, name, config, input_tests)
1485 remapped_test_type = test_type_remapper.get(test_type, test_type)
Garrett Beatyffe83c4f2023-09-08 19:07:371486 tests.setdefault(remapped_test_type, []).extend(new_tests)
1487
1488 for test_type, tests_for_type in tests.items():
1489 if test_type == 'additional_compile_targets':
1490 continue
1491 tests[test_type] = sorted(tests_for_type, key=lambda t: t['name'])
Greg Gutermanf60eb052020-03-12 17:40:011492
1493 return tests
1494
1495 def jsonify(self, all_tests):
1496 return json.dumps(
1497 all_tests, indent=2, separators=(',', ': '),
1498 sort_keys=True) + '\n'
1499
1500 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281501 self.load_configuration_files()
1502 self.resolve_configuration_files()
1503 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011504 result = collections.defaultdict(dict)
1505
Stephanie Kim572b43c02023-04-13 14:24:131506 if os.path.exists(self.args.autoshard_exceptions_json_path):
1507 autoshards = json.loads(
1508 self.read_file(self.args.autoshard_exceptions_json_path))
1509 else:
1510 autoshards = {}
1511
Dirk Pranke6269d302020-10-01 00:14:391512 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011513 for waterfall in self.waterfalls:
1514 for field in required_fields:
1515 # Verify required fields
1516 if field not in waterfall:
1517 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1518
1519 # Handle filter flag, if specified
1520 if filters and waterfall['name'] not in filters:
1521 continue
1522
1523 # Join config files and hardcoded values together
1524 all_tests = self.generate_output_tests(waterfall)
1525 result[waterfall['name']] = all_tests
1526
Stephanie Kim572b43c02023-04-13 14:24:131527 if not autoshards:
1528 continue
1529 for builder, test_spec in all_tests.items():
1530 for target_type, test_list in test_spec.items():
1531 if target_type == 'additional_compile_targets':
1532 continue
1533 for test_dict in test_list:
1534 # Suites that apply variants or other customizations will create
1535 # test_dicts that have "name" value that is different from the
Garrett Beatyffe83c4f2023-09-08 19:07:371536 # "test" value.
Stephanie Kim572b43c02023-04-13 14:24:131537 # e.g. name = vulkan_swiftshader_content_browsertests, but
1538 # test = content_browsertests and
1539 # test_id_prefix = "ninja://content/test:content_browsertests/"
Garrett Beatyffe83c4f2023-09-08 19:07:371540 test_name = test_dict['name']
Stephanie Kim572b43c02023-04-13 14:24:131541 shard_info = autoshards.get(waterfall['name'],
1542 {}).get(builder, {}).get(test_name)
1543 if shard_info:
1544 test_dict['swarming'].update(
1545 {'shards': int(shard_info['shards'])})
1546
Greg Gutermanf60eb052020-03-12 17:40:011547 # Add do not edit warning
1548 for tests in result.values():
1549 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1550 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1551
1552 return result
1553
1554 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281555 suffix = '.json'
1556 if self.args.new_files:
1557 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011558
1559 for filename, contents in result.items():
1560 jsonstr = self.jsonify(contents)
Garrett Beaty79339e182023-04-10 20:45:471561 file_path = os.path.join(self.args.output_dir, filename + suffix)
1562 self.write_file(file_path, jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281563
Nico Weberd18b8962018-05-16 19:39:381564 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161565 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421566 # NOTE: This reference can cause issues; if a file changes there, the
1567 # presubmit here won't be run by default. A manually maintained list there
1568 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1569 # references to configs outside of this directory are added, please change
1570 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1571 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491572
Garrett Beaty7e866fc2021-06-16 14:12:101573 # Get the generated project.pyl so we can check if we should be enforcing
1574 # that the specs are for builders that actually exist
1575 # If not, return None to indicate that we won't enforce that builders in
1576 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491577 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1578 'project.pyl')
1579 if os.path.exists(project_pyl_path):
1580 settings = ast.literal_eval(self.read_file(project_pyl_path))
1581 if not settings.get('validate_source_side_specs_have_builder', True):
1582 return None
1583
Nico Weberd18b8962018-05-16 19:39:381584 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311585 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161586 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1587 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431588 for c in milo_configs:
1589 for l in self.read_file(c).splitlines():
1590 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311591 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431592 continue
1593 # l looks like
1594 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1595 # Extract win_chromium_dbg_ng part.
1596 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381597 return bot_names
1598
Ben Pastene9a010082019-09-25 20:41:371599 def get_internal_waterfalls(self):
1600 # Similar to get_builders_that_do_not_actually_exist above, but for
1601 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201602 return [
Kramer Ge3bf853a2023-04-13 19:39:471603 'chrome', 'chrome.pgo', 'chrome.gpu.fyi', 'internal.chrome.fyi',
Marco Georgaklis333e8386b2023-09-07 22:46:331604 'internal.chromeos.fyi', 'internal.optimization_guide', 'internal.soda'
Yuke Liaoe6c23dd2021-07-28 16:12:201605 ]
Ben Pastene9a010082019-09-25 20:41:371606
Stephen Martinisf83893722018-09-19 00:02:181607 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201608 self.check_input_files_sorting(verbose)
1609
Kenneth Russelleb60cbd22017-12-05 07:54:281610 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011611 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381612 self.check_composition_type_test_suites('matrix_compound_suites',
1613 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111614 self.resolve_test_id_prefixes()
Stephen Martinis54d64ad2018-09-21 22:16:201615 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381616
1617 # All bots should exist.
1618 bot_names = self.get_valid_bot_names()
Garrett Beaty2a02de3c2020-05-15 13:57:351619 if bot_names is not None:
1620 internal_waterfalls = self.get_internal_waterfalls()
1621 for waterfall in self.waterfalls:
1622 # TODO(crbug.com/991417): Remove the need for this exception.
1623 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011624 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351625 for bot_name in waterfall['machines']:
Garrett Beaty2a02de3c2020-05-15 13:57:351626 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581627 if waterfall['name'] in [
1628 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1629 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351630 # TODO(thakis): Remove this once these bots move to luci.
1631 continue # pragma: no cover
1632 if waterfall['name'] in ['tryserver.webrtc',
1633 'webrtc.chromium.fyi.experimental']:
1634 # These waterfalls have their bot configs in a different repo.
1635 # so we don't know about their bot names.
1636 continue # pragma: no cover
1637 if waterfall['name'] in ['client.devtools-frontend.integration',
1638 'tryserver.devtools-frontend',
1639 'chromium.devtools-frontend']:
1640 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201641 if waterfall['name'] in ['client.openscreen.chromium']:
1642 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351643 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381644
Kenneth Russelleb60cbd22017-12-05 07:54:281645 # All test suites must be referenced.
1646 suites_seen = set()
1647 generator_map = self.get_test_generator_map()
1648 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231649 for bot_name, tester in waterfall['machines'].items():
1650 for suite_type, suite in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281651 if suite_type not in generator_map:
1652 raise self.unknown_test_suite_type(suite_type, bot_name,
1653 waterfall['name'])
1654 if suite not in self.test_suites:
1655 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1656 suites_seen.add(suite)
1657 # Since we didn't resolve the configuration files, this set
1658 # includes both composition test suites and regular ones.
1659 resolved_suites = set()
1660 for suite_name in suites_seen:
1661 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011662 for sub_suite in suite:
1663 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281664 resolved_suites.add(suite_name)
1665 # At this point, every key in test_suites.pyl should be referenced.
1666 missing_suites = set(self.test_suites.keys()) - resolved_suites
1667 if missing_suites:
1668 raise BBGenErr('The following test suites were unreferenced by bots on '
1669 'the waterfalls: ' + str(missing_suites))
1670
1671 # All test suite exceptions must refer to bots on the waterfall.
1672 all_bots = set()
1673 missing_bots = set()
1674 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231675 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281676 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281677 # In order to disambiguate between bots with the same name on
1678 # different waterfalls, support has been added to various
1679 # exceptions for concatenating the waterfall name after the bot
1680 # name.
1681 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231682 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381683 removals = (exception.get('remove_from', []) +
1684 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231685 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381686 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281687 if removal not in all_bots:
1688 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411689
Kenneth Russelleb60cbd22017-12-05 07:54:281690 if missing_bots:
1691 raise BBGenErr('The following nonexistent machines were referenced in '
1692 'the test suite exceptions: ' + str(missing_bots))
1693
Garrett Beatyb061e69d2023-06-27 16:15:351694 for name, mixin in self.mixins.items():
1695 if '$mixin_append' in mixin:
1696 raise BBGenErr(
1697 f'$mixin_append is no longer supported (set in mixin "{name}"),'
1698 ' args and named caches specified as normal will be appended')
1699
Stephen Martinis0382bc12018-09-17 22:29:071700 # All mixins must be referenced
1701 seen_mixins = set()
1702 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011703 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231704 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011705 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071706 for suite in self.test_suites.values():
1707 if isinstance(suite, list):
1708 # Don't care about this, it's a composition, which shouldn't include a
1709 # swarming mixin.
1710 continue
1711
1712 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561713 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011714 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071715
Zhaoyang Li9da047d52021-05-10 21:31:441716 for variant in self.variants:
1717 # Unpack the variant from variants.pyl if it's string based.
1718 if isinstance(variant, str):
1719 variant = self.variants[variant]
1720 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1721
Stephen Martinisb72f6d22018-10-04 23:29:011722 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071723 if missing_mixins:
1724 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1725 ' referenced in a waterfall, machine, or test suite.' % (
1726 str(missing_mixins)))
1727
Jeff Yoonda581c32020-03-06 03:56:051728 # All variant references must be referenced
1729 seen_variants = set()
1730 for suite in self.test_suites.values():
1731 if isinstance(suite, list):
1732 continue
1733
1734 for test in suite.values():
1735 if isinstance(test, dict):
1736 for variant in test.get('variants', []):
1737 if isinstance(variant, str):
1738 seen_variants.add(variant)
1739
1740 missing_variants = set(self.variants.keys()) - seen_variants
1741 if missing_variants:
1742 raise BBGenErr('The following variants were unreferenced: %s. They must '
1743 'be referenced in a matrix test suite under the variants '
1744 'key.' % str(missing_variants))
1745
Stephen Martinis54d64ad2018-09-21 22:16:201746
Garrett Beaty79339e182023-04-10 20:45:471747 def type_assert(self, node, typ, file_path, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201748 """Asserts that the Python AST node |node| is of type |typ|.
1749
1750 If verbose is set, it prints out some helpful context lines, showing where
1751 exactly the error occurred in the file.
1752 """
1753 if not isinstance(node, typ):
1754 if verbose:
Garrett Beaty79339e182023-04-10 20:45:471755 lines = [""] + self.read_file(file_path).splitlines()
Stephen Martinis54d64ad2018-09-21 22:16:201756
1757 context = 2
1758 lines_start = max(node.lineno - context, 0)
1759 # Add one to include the last line
1760 lines_end = min(node.lineno + context, len(lines)) + 1
Garrett Beaty79339e182023-04-10 20:45:471761 lines = itertools.chain(
1762 ['== %s ==\n' % file_path],
1763 ["<snip>\n"],
1764 [
1765 '%d %s' % (lines_start + i, line)
1766 for i, line in enumerate(lines[lines_start:lines_start +
1767 context])
1768 ],
1769 ['-' * 80 + '\n'],
1770 ['%d %s' % (node.lineno, lines[node.lineno])],
1771 [
1772 '-' * (node.col_offset + 3) + '^' + '-' *
1773 (80 - node.col_offset - 4) + '\n'
1774 ],
1775 [
1776 '%d %s' % (node.lineno + 1 + i, line)
1777 for i, line in enumerate(lines[node.lineno + 1:lines_end])
1778 ],
1779 ["<snip>\n"],
Stephen Martinis54d64ad2018-09-21 22:16:201780 )
1781 # Print out a useful message when a type assertion fails.
1782 for l in lines:
1783 self.print_line(l.strip())
1784
1785 node_dumped = ast.dump(node, annotate_fields=False)
1786 # If the node is huge, truncate it so everything fits in a terminal
1787 # window.
1788 if len(node_dumped) > 60: # pragma: no cover
1789 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1790 raise BBGenErr(
Garrett Beaty807011ab2023-04-12 00:52:391791 'Invalid .pyl file \'%s\'. Python AST node %r on line %s expected to'
Garrett Beaty79339e182023-04-10 20:45:471792 ' be %s, is %s' %
1793 (file_path, node_dumped, node.lineno, typ, type(node)))
Stephen Martinis54d64ad2018-09-21 22:16:201794
Garrett Beaty79339e182023-04-10 20:45:471795 def check_ast_list_formatted(self,
1796 keys,
1797 file_path,
1798 verbose,
Stephen Martinis1384ff92020-01-07 19:52:151799 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531800 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201801
Stephen Martinis5bef0fc2020-01-06 22:47:531802 Currently only checks to ensure they're correctly sorted, and that there
1803 are no duplicates.
1804
1805 Args:
1806 keys: An python list of AST nodes.
1807
1808 It's a list of AST nodes instead of a list of strings because
1809 when verbose is set, it tries to print out context of where the
1810 diffs are in the file.
Garrett Beaty79339e182023-04-10 20:45:471811 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531812 verbose: If set, print out diff information about how the keys are
1813 incorrectly formatted.
1814 check_sorting: If true, checks if the list is sorted.
1815 Returns:
1816 If the keys are correctly formatted.
1817 """
1818 if not keys:
1819 return True
1820
1821 assert isinstance(keys[0], ast.Str)
1822
1823 keys_strs = [k.s for k in keys]
1824 # Keys to diff against. Used below.
1825 keys_to_diff_against = None
1826 # If the list is properly formatted.
1827 list_formatted = True
1828
1829 # Duplicates are always bad.
1830 if len(set(keys_strs)) != len(keys_strs):
1831 list_formatted = False
1832 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1833
1834 if check_sorting and sorted(keys_strs) != keys_strs:
1835 list_formatted = False
1836 if list_formatted:
1837 return True
1838
1839 if verbose:
1840 line_num = keys[0].lineno
1841 keys = [k.s for k in keys]
1842 if check_sorting:
1843 # If we have duplicates, sorting this will take care of it anyways.
1844 keys_to_diff_against = sorted(set(keys))
1845 # else, keys_to_diff_against is set above already
1846
1847 self.print_line('=' * 80)
1848 self.print_line('(First line of keys is %s)' % line_num)
Garrett Beaty79339e182023-04-10 20:45:471849 for line in difflib.context_diff(keys,
1850 keys_to_diff_against,
1851 fromfile='current (%r)' % file_path,
1852 tofile='sorted',
1853 lineterm=''):
Stephen Martinis5bef0fc2020-01-06 22:47:531854 self.print_line(line)
1855 self.print_line('=' * 80)
1856
1857 return False
1858
Garrett Beaty79339e182023-04-10 20:45:471859 def check_ast_dict_formatted(self, node, file_path, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531860 """Checks if an ast dictionary's keys are correctly formatted.
1861
1862 Just a simple wrapper around check_ast_list_formatted.
1863 Args:
1864 node: An AST node. Assumed to be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471865 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531866 verbose: If set, print out diff information about how the keys are
1867 incorrectly formatted.
1868 check_sorting: If true, checks if the list is sorted.
1869 Returns:
1870 If the dictionary is correctly formatted.
1871 """
Stephen Martinis54d64ad2018-09-21 22:16:201872 keys = []
1873 # The keys of this dict are ordered as ordered in the file; normal python
1874 # dictionary keys are given an arbitrary order, but since we parsed the
1875 # file itself, the order as given in the file is preserved.
1876 for key in node.keys:
Garrett Beaty79339e182023-04-10 20:45:471877 self.type_assert(key, ast.Str, file_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531878 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201879
Garrett Beaty79339e182023-04-10 20:45:471880 return self.check_ast_list_formatted(keys, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181881
1882 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201883 # TODO(https://crbug.com/886993): Add the ability for this script to
1884 # actually format the files, rather than just complain if they're
1885 # incorrectly formatted.
1886 bad_files = set()
Garrett Beaty79339e182023-04-10 20:45:471887
1888 def parse_file(file_path):
Stephen Martinis5bef0fc2020-01-06 22:47:531889 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201890
Stephen Martinis5bef0fc2020-01-06 22:47:531891 Returns an AST node representing the value in the pyl file."""
Garrett Beaty79339e182023-04-10 20:45:471892 parsed = ast.parse(self.read_file(file_path))
Stephen Martinisf83893722018-09-19 00:02:181893
Stephen Martinisf83893722018-09-19 00:02:181894 # Must be a module.
Garrett Beaty79339e182023-04-10 20:45:471895 self.type_assert(parsed, ast.Module, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181896 module = parsed.body
1897
1898 # Only one expression in the module.
Garrett Beaty79339e182023-04-10 20:45:471899 self.type_assert(module, list, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181900 if len(module) != 1: # pragma: no cover
Garrett Beaty79339e182023-04-10 20:45:471901 raise BBGenErr('Invalid .pyl file %s' % file_path)
Stephen Martinisf83893722018-09-19 00:02:181902 expr = module[0]
Garrett Beaty79339e182023-04-10 20:45:471903 self.type_assert(expr, ast.Expr, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181904
Stephen Martinis5bef0fc2020-01-06 22:47:531905 return expr.value
1906
1907 # Handle this separately
Garrett Beaty79339e182023-04-10 20:45:471908 value = parse_file(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531909 # Value should be a list.
Garrett Beaty79339e182023-04-10 20:45:471910 self.type_assert(value, ast.List, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531911
1912 keys = []
Joshua Hood56c673c2022-03-02 20:29:331913 for elm in value.elts:
Garrett Beaty79339e182023-04-10 20:45:471914 self.type_assert(elm, ast.Dict, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531915 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331916 for key, val in zip(elm.keys, elm.values):
Garrett Beaty79339e182023-04-10 20:45:471917 self.type_assert(key, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531918 if key.s == 'machines':
Garrett Beaty79339e182023-04-10 20:45:471919 if not self.check_ast_dict_formatted(
1920 val, self.args.waterfalls_pyl_path, verbose):
1921 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531922
1923 if key.s == "name":
Garrett Beaty79339e182023-04-10 20:45:471924 self.type_assert(val, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531925 waterfall_name = val
1926 assert waterfall_name
1927 keys.append(waterfall_name)
1928
Garrett Beaty79339e182023-04-10 20:45:471929 if not self.check_ast_list_formatted(keys, self.args.waterfalls_pyl_path,
1930 verbose):
1931 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531932
Garrett Beaty79339e182023-04-10 20:45:471933 for file_path in (
1934 self.args.mixins_pyl_path,
1935 self.args.test_suites_pyl_path,
1936 self.args.test_suite_exceptions_pyl_path,
Stephen Martinis5bef0fc2020-01-06 22:47:531937 ):
Garrett Beaty79339e182023-04-10 20:45:471938 value = parse_file(file_path)
Stephen Martinisf83893722018-09-19 00:02:181939 # Value should be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471940 self.type_assert(value, ast.Dict, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181941
Garrett Beaty79339e182023-04-10 20:45:471942 if not self.check_ast_dict_formatted(value, file_path, verbose):
1943 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531944
Garrett Beaty79339e182023-04-10 20:45:471945 if file_path == self.args.test_suites_pyl_path:
Jeff Yoon8154e582019-12-03 23:30:011946 expected_keys = ['basic_suites',
1947 'compound_suites',
1948 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201949 actual_keys = [node.s for node in value.keys]
1950 assert all(key in expected_keys for key in actual_keys), (
Garrett Beaty79339e182023-04-10 20:45:471951 'Invalid %r file; expected keys %r, got %r' %
1952 (file_path, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:331953 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:201954 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011955 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201956 for suite_group in suite_dicts:
Garrett Beaty79339e182023-04-10 20:45:471957 if not self.check_ast_dict_formatted(suite_group, file_path, verbose):
1958 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181959
Stephen Martinis5bef0fc2020-01-06 22:47:531960 for key, suite in zip(value.keys, value.values):
1961 # The compound suites are checked in
1962 # 'check_composition_type_test_suites()'
1963 if key.s == 'basic_suites':
1964 for group in suite.values:
Garrett Beaty79339e182023-04-10 20:45:471965 if not self.check_ast_dict_formatted(group, file_path, verbose):
1966 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531967 break
Stephen Martinis54d64ad2018-09-21 22:16:201968
Garrett Beaty79339e182023-04-10 20:45:471969 elif file_path == self.args.test_suite_exceptions_pyl_path:
Stephen Martinis5bef0fc2020-01-06 22:47:531970 # Check the values for each test.
1971 for test in value.values:
1972 for kind, node in zip(test.keys, test.values):
1973 if isinstance(node, ast.Dict):
Garrett Beaty79339e182023-04-10 20:45:471974 if not self.check_ast_dict_formatted(node, file_path, verbose):
1975 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531976 elif kind.s == 'remove_from':
1977 # Don't care about sorting; these are usually grouped, since the
1978 # same bug can affect multiple builders. Do want to make sure
1979 # there aren't duplicates.
Garrett Beaty79339e182023-04-10 20:45:471980 if not self.check_ast_list_formatted(
1981 node.elts, file_path, verbose, check_sorting=False):
1982 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:181983
1984 if bad_files:
1985 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201986 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531987 'unsorted, or have duplicates. Re-run this with --verbose to see '
1988 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181989
Kenneth Russelleb60cbd22017-12-05 07:54:281990 def check_output_file_consistency(self, verbose=False):
1991 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011992 # All waterfalls/bucket .json files must have been written
1993 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:281994 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011995 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:161996 outputs = self.generate_outputs()
1997 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:011998 expected = self.jsonify(expected_contents)
Garrett Beaty79339e182023-04-10 20:45:471999 file_path = os.path.join(self.args.output_dir, filename + '.json')
Ben Pastenef21cda32023-03-30 22:00:572000 current = self.read_file(file_path)
Kenneth Russelleb60cbd22017-12-05 07:54:282001 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:012002 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:322003 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:012004 self.print_line('File ' + filename +
2005 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:322006 'contents:')
2007 for line in difflib.unified_diff(
2008 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:502009 current.splitlines(),
2010 fromfile='expected', tofile='current'):
2011 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:012012
2013 if ungenerated_files:
2014 raise BBGenErr(
2015 'The following files have not been properly '
2016 'autogenerated by generate_buildbot_json.py: ' +
2017 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:282018
Dirk Pranke772f55f2021-04-28 04:51:162019 for builder_group, builders in outputs.items():
2020 for builder, step_types in builders.items():
2021 for step_data in step_types.get('gtest_tests', []):
Garrett Beatyffe83c4f2023-09-08 19:07:372022 step_name = step_data['name']
Dirk Pranke772f55f2021-04-28 04:51:162023 self._check_swarming_config(builder_group, builder, step_name,
2024 step_data)
2025 for step_data in step_types.get('isolated_scripts', []):
Garrett Beatyffe83c4f2023-09-08 19:07:372026 step_name = step_data['name']
Dirk Pranke772f55f2021-04-28 04:51:162027 self._check_swarming_config(builder_group, builder, step_name,
2028 step_data)
2029
2030 def _check_swarming_config(self, filename, builder, step_name, step_data):
Ben Pastene338f56b2023-03-31 21:24:452031 # TODO(crbug.com/1203436): Ensure all swarming tests specify cpu, not
Dirk Pranke772f55f2021-04-28 04:51:162032 # just mac tests.
Garrett Beatybb18d532023-06-26 22:16:332033 if 'swarming' in step_data:
Garrett Beatyade673d2023-08-04 22:00:252034 dimensions = step_data['swarming'].get('dimensions')
2035 if not dimensions:
Ben Pastene338f56b2023-03-31 21:24:452036 raise BBGenErr('%s: %s / %s : os must be specified for all '
Dirk Pranke772f55f2021-04-28 04:51:162037 'swarmed tests' % (filename, builder, step_name))
Garrett Beatyade673d2023-08-04 22:00:252038 if not dimensions.get('os'):
2039 raise BBGenErr('%s: %s / %s : os must be specified for all '
2040 'swarmed tests' % (filename, builder, step_name))
2041 if 'Mac' in dimensions.get('os') and not dimensions.get('cpu'):
2042 raise BBGenErr('%s: %s / %s : cpu must be specified for mac '
2043 'swarmed tests' % (filename, builder, step_name))
Dirk Pranke772f55f2021-04-28 04:51:162044
Kenneth Russelleb60cbd22017-12-05 07:54:282045 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502046 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282047 self.check_output_file_consistency(verbose) # pragma: no cover
2048
Karen Qiane24b7ee2019-02-12 23:37:062049 def does_test_match(self, test_info, params_dict):
2050 """Checks to see if the test matches the parameters given.
2051
2052 Compares the provided test_info with the params_dict to see
2053 if the bot matches the parameters given. If so, returns True.
2054 Else, returns false.
2055
2056 Args:
2057 test_info (dict): Information about a specific bot provided
2058 in the format shown in waterfalls.pyl
2059 params_dict (dict): Dictionary of parameters and their values
2060 to look for in the bot
2061 Ex: {
2062 'device_os':'android',
2063 '--flag':True,
2064 'mixins': ['mixin1', 'mixin2'],
2065 'ex_key':'ex_value'
2066 }
2067
2068 """
2069 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2070 'kvm', 'pool', 'integrity'] # dimension parameters
2071 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2072 'can_use_on_swarming_builders']
2073 for param in params_dict:
2074 # if dimension parameter
2075 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2076 if not 'swarming' in test_info:
2077 return False
2078 swarming = test_info['swarming']
2079 if param in SWARMING_PARAMS:
2080 if not param in swarming:
2081 return False
2082 if not str(swarming[param]) == params_dict[param]:
2083 return False
2084 else:
Garrett Beatyade673d2023-08-04 22:00:252085 if not 'dimensions' in swarming:
Karen Qiane24b7ee2019-02-12 23:37:062086 return False
Garrett Beatyade673d2023-08-04 22:00:252087 dimensions = swarming['dimensions']
Karen Qiane24b7ee2019-02-12 23:37:062088 # only looking at the first dimension set
Garrett Beatyade673d2023-08-04 22:00:252089 if not param in dimensions:
Karen Qiane24b7ee2019-02-12 23:37:062090 return False
Garrett Beatyade673d2023-08-04 22:00:252091 if not dimensions[param] == params_dict[param]:
Karen Qiane24b7ee2019-02-12 23:37:062092 return False
2093
2094 # if flag
2095 elif param.startswith('--'):
2096 if not 'args' in test_info:
2097 return False
2098 if not param in test_info['args']:
2099 return False
2100
2101 # not dimension parameter/flag/mixin
2102 else:
2103 if not param in test_info:
2104 return False
2105 if not test_info[param] == params_dict[param]:
2106 return False
2107 return True
2108 def error_msg(self, msg):
2109 """Prints an error message.
2110
2111 In addition to a catered error message, also prints
2112 out where the user can find more help. Then, program exits.
2113 """
2114 self.print_line(msg + (' If you need more information, ' +
2115 'please run with -h or --help to see valid commands.'))
2116 sys.exit(1)
2117
2118 def find_bots_that_run_test(self, test, bots):
2119 matching_bots = []
2120 for bot in bots:
2121 bot_info = bots[bot]
2122 tests = self.flatten_tests_for_bot(bot_info)
2123 for test_info in tests:
Garrett Beatyffe83c4f2023-09-08 19:07:372124 test_name = test_info['name']
Karen Qiane24b7ee2019-02-12 23:37:062125 if not test_name == test:
2126 continue
2127 matching_bots.append(bot)
2128 return matching_bots
2129
2130 def find_tests_with_params(self, tests, params_dict):
2131 matching_tests = []
2132 for test_name in tests:
2133 test_info = tests[test_name]
2134 if not self.does_test_match(test_info, params_dict):
2135 continue
2136 if not test_name in matching_tests:
2137 matching_tests.append(test_name)
2138 return matching_tests
2139
2140 def flatten_waterfalls_for_query(self, waterfalls):
2141 bots = {}
2142 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012143 waterfall_tests = self.generate_output_tests(waterfall)
2144 for bot in waterfall_tests:
2145 bot_info = waterfall_tests[bot]
2146 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062147 return bots
2148
2149 def flatten_tests_for_bot(self, bot_info):
2150 """Returns a list of flattened tests.
2151
2152 Returns a list of tests not grouped by test category
2153 for a specific bot.
2154 """
2155 TEST_CATS = self.get_test_generator_map().keys()
2156 tests = []
2157 for test_cat in TEST_CATS:
2158 if not test_cat in bot_info:
2159 continue
2160 test_cat_tests = bot_info[test_cat]
2161 tests = tests + test_cat_tests
2162 return tests
2163
2164 def flatten_tests_for_query(self, test_suites):
2165 """Returns a flattened dictionary of tests.
2166
2167 Returns a dictionary of tests associate with their
2168 configuration, not grouped by their test suite.
2169 """
2170 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232171 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062172 for test in test_suite:
2173 test_info = test_suite[test]
2174 test_name = test
Karen Qiane24b7ee2019-02-12 23:37:062175 tests[test_name] = test_info
2176 return tests
2177
2178 def parse_query_filter_params(self, params):
2179 """Parses the filter parameters.
2180
2181 Creates a dictionary from the parameters provided
2182 to filter the bot array.
2183 """
2184 params_dict = {}
2185 for p in params:
2186 # flag
2187 if p.startswith("--"):
2188 params_dict[p] = True
2189 else:
2190 pair = p.split(":")
2191 if len(pair) != 2:
2192 self.error_msg('Invalid command.')
2193 # regular parameters
2194 if pair[1].lower() == "true":
2195 params_dict[pair[0]] = True
2196 elif pair[1].lower() == "false":
2197 params_dict[pair[0]] = False
2198 else:
2199 params_dict[pair[0]] = pair[1]
2200 return params_dict
2201
2202 def get_test_suites_dict(self, bots):
2203 """Returns a dictionary of bots and their tests.
2204
2205 Returns a dictionary of bots and a list of their associated tests.
2206 """
2207 test_suite_dict = dict()
2208 for bot in bots:
2209 bot_info = bots[bot]
2210 tests = self.flatten_tests_for_bot(bot_info)
2211 test_suite_dict[bot] = tests
2212 return test_suite_dict
2213
2214 def output_query_result(self, result, json_file=None):
2215 """Outputs the result of the query.
2216
2217 If a json file parameter name is provided, then
2218 the result is output into the json file. If not,
2219 then the result is printed to the console.
2220 """
2221 output = json.dumps(result, indent=2)
2222 if json_file:
2223 self.write_file(json_file, output)
2224 else:
2225 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062226
Joshua Hood56c673c2022-03-02 20:29:332227 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062228 def query(self, args):
2229 """Queries tests or bots.
2230
2231 Depending on the arguments provided, outputs a json of
2232 tests or bots matching the appropriate optional parameters provided.
2233 """
2234 # split up query statement
2235 query = args.query.split('/')
2236 self.load_configuration_files()
2237 self.resolve_configuration_files()
2238
2239 # flatten bots json
2240 tests = self.test_suites
2241 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2242
2243 cmd_class = query[0]
2244
2245 # For queries starting with 'bots'
2246 if cmd_class == "bots":
2247 if len(query) == 1:
2248 return self.output_query_result(bots, args.json)
2249 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332250 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062251 if query[1] == 'tests':
2252 test_suites_dict = self.get_test_suites_dict(bots)
2253 return self.output_query_result(test_suites_dict, args.json)
Joshua Hood56c673c2022-03-02 20:29:332254 self.error_msg("This query should be in the format: bots/tests.")
Karen Qiane24b7ee2019-02-12 23:37:062255
2256 else:
2257 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2258 % str(len(query)-1))
2259
2260 # For queries starting with 'bot'
2261 elif cmd_class == "bot":
2262 if not len(query) == 2 and not len(query) == 3:
2263 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2264 % str(len(query)-1))
2265 bot_id = query[1]
2266 if not bot_id in bots:
2267 self.error_msg("No bot named '" + bot_id + "' found.")
2268 bot_info = bots[bot_id]
2269 if len(query) == 2:
2270 return self.output_query_result(bot_info, args.json)
2271 if not query[2] == 'tests':
2272 self.error_msg("The query should be in the format:" +
2273 "bot/<bot-name>/tests.")
2274
2275 bot_tests = self.flatten_tests_for_bot(bot_info)
2276 return self.output_query_result(bot_tests, args.json)
2277
2278 # For queries starting with 'tests'
2279 elif cmd_class == "tests":
2280 if not len(query) == 1 and not len(query) == 2:
2281 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2282 % str(len(query)-1))
2283 flattened_tests = self.flatten_tests_for_query(tests)
2284 if len(query) == 1:
2285 return self.output_query_result(flattened_tests, args.json)
2286
2287 # create params dict
2288 params = query[1].split('&')
2289 params_dict = self.parse_query_filter_params(params)
2290 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2291 return self.output_query_result(matching_bots)
2292
2293 # For queries starting with 'test'
2294 elif cmd_class == "test":
2295 if not len(query) == 2 and not len(query) == 3:
2296 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2297 % str(len(query)-1))
2298 test_id = query[1]
2299 if len(query) == 2:
2300 flattened_tests = self.flatten_tests_for_query(tests)
2301 for test in flattened_tests:
2302 if test == test_id:
2303 return self.output_query_result(flattened_tests[test], args.json)
2304 self.error_msg("There is no test named %s." % test_id)
2305 if not query[2] == 'bots':
2306 self.error_msg("The query should be in the format: " +
2307 "test/<test-name>/bots")
2308 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2309 return self.output_query_result(bots_for_test)
2310
2311 else:
2312 self.error_msg("Your command did not match any valid commands." +
2313 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Joshua Hood56c673c2022-03-02 20:29:332314 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282315
Garrett Beaty1afaccc2020-06-25 19:58:152316 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282317 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502318 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062319 elif self.args.query:
2320 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282321 else:
Greg Gutermanf60eb052020-03-12 17:40:012322 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282323 return 0
2324
2325if __name__ == "__main__": # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152326 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2327 sys.exit(generator.main())