blob: 4b168e5f3fb783e3b79c0703fbea5eef4aca3cb6 [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
Jamie Madillcf4f8c72021-05-20 19:24:2315import functools
Garrett Beatyd5ca75962020-05-07 16:58:3116import glob
Kenneth Russell8ceeabf2017-12-11 17:53:2817import itertools
Kenneth Russelleb60cbd22017-12-05 07:54:2818import json
19import os
Kenneth Russelleb60cbd22017-12-05 07:54:2820import string
21import sys
22
Brian Sheedya31578e2020-05-18 20:24:3623import buildbot_json_magic_substitutions as magic_substitutions
24
Joshua Hood56c673c2022-03-02 20:29:3325# pylint: disable=super-with-arguments,useless-super-delegation
26
Kenneth Russelleb60cbd22017-12-05 07:54:2827THIS_DIR = os.path.dirname(os.path.abspath(__file__))
28
Brian Sheedyf74819b2021-06-04 01:38:3829BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP = {
30 'android-chromium': '_android_chrome',
31 'android-chromium-monochrome': '_android_monochrome',
32 'android-weblayer': '_android_weblayer',
33 'android-webview': '_android_webview',
34}
35
Kenneth Russelleb60cbd22017-12-05 07:54:2836
37class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4938 def __init__(self, message):
39 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2840
41
Kenneth Russell8ceeabf2017-12-11 17:53:2842# This class is only present to accommodate certain machines on
43# chromium.android.fyi which run certain tests as instrumentation
44# tests, but not as gtests. If this discrepancy were fixed then the
45# notion could be removed.
Joshua Hood56c673c2022-03-02 20:29:3346class TestSuiteTypes(object): # pylint: disable=useless-object-inheritance
Kenneth Russell8ceeabf2017-12-11 17:53:2847 GTEST = 'gtest'
48
49
Joshua Hood56c673c2022-03-02 20:29:3350class BaseGenerator(object): # pylint: disable=useless-object-inheritance
Kenneth Russelleb60cbd22017-12-05 07:54:2851 def __init__(self, bb_gen):
52 self.bb_gen = bb_gen
53
Kenneth Russell8ceeabf2017-12-11 17:53:2854 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2855 raise NotImplementedError()
56
57 def sort(self, tests):
58 raise NotImplementedError()
59
60
Jamie Madillcf4f8c72021-05-20 19:24:2361def custom_cmp(a, b):
62 return int(a > b) - int(a < b)
63
64
Kenneth Russell8ceeabf2017-12-11 17:53:2865def cmp_tests(a, b):
66 # Prefer to compare based on the "test" key.
Jamie Madillcf4f8c72021-05-20 19:24:2367 val = custom_cmp(a['test'], b['test'])
Kenneth Russell8ceeabf2017-12-11 17:53:2868 if val != 0:
69 return val
70 if 'name' in a and 'name' in b:
Jamie Madillcf4f8c72021-05-20 19:24:2371 return custom_cmp(a['name'], b['name']) # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:2872 if 'name' not in a and 'name' not in b:
73 return 0 # pragma: no cover
74 # Prefer to put variants of the same test after the first one.
75 if 'name' in a:
76 return 1
77 # 'name' is in b.
78 return -1 # pragma: no cover
79
80
Kenneth Russell8a386d42018-06-02 09:48:0181class GPUTelemetryTestGenerator(BaseGenerator):
Fabrice de Ganscbd655f2022-08-04 20:15:3082 def __init__(self, bb_gen, is_android_webview=False, is_cast_streaming=False):
Kenneth Russell8a386d42018-06-02 09:48:0183 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5684 self._is_android_webview = is_android_webview
Fabrice de Ganscbd655f2022-08-04 20:15:3085 self._is_cast_streaming = is_cast_streaming
Kenneth Russell8a386d42018-06-02 09:48:0186
87 def generate(self, waterfall, tester_name, tester_config, input_tests):
88 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:2389 for test_name, test_config in sorted(input_tests.items()):
Ben Pastene8e7eb2652022-04-29 19:44:3190 # Variants allow more than one definition for a given test, and is defined
91 # in array format from resolve_variants().
92 if not isinstance(test_config, list):
93 test_config = [test_config]
94
95 for config in test_config:
96 test = self.bb_gen.generate_gpu_telemetry_test(waterfall, tester_name,
97 tester_config, test_name,
98 config,
Fabrice de Ganscbd655f2022-08-04 20:15:3099 self._is_android_webview,
100 self._is_cast_streaming)
Ben Pastene8e7eb2652022-04-29 19:44:31101 if test:
102 isolated_scripts.append(test)
103
Kenneth Russell8a386d42018-06-02 09:48:01104 return isolated_scripts
105
106 def sort(self, tests):
107 return sorted(tests, key=lambda x: x['name'])
108
109
Brian Sheedyb6491ba2022-09-26 20:49:49110class SkylabGPUTelemetryTestGenerator(GPUTelemetryTestGenerator):
111 def generate(self, *args, **kwargs):
112 # This should be identical to a regular GPU Telemetry test, but with any
113 # swarming arguments removed.
114 isolated_scripts = super(SkylabGPUTelemetryTestGenerator,
115 self).generate(*args, **kwargs)
116 for test in isolated_scripts:
Brian Sheedyc860f022022-09-30 23:32:17117 if 'isolate_name' in test:
118 test['test'] = test['isolate_name']
119 del test['isolate_name']
Xinan Lind9b1d2e72022-11-14 20:57:02120 # chromium_GPU is the Autotest wrapper created for browser GPU tests
121 # run in Skylab.
Xinan Lin1f28a0d2023-03-13 17:39:41122 test['autotest_name'] = 'chromium_Graphics'
Xinan Lind9b1d2e72022-11-14 20:57:02123 # As of 22Q4, Skylab tests are running on a CrOS flavored Autotest
124 # framework and it does not support the sub-args like
125 # extra-browser-args. So we have to pop it out and create a new
126 # key for it. See crrev.com/c/3965359 for details.
127 for idx, arg in enumerate(test.get('args', [])):
128 if '--extra-browser-args' in arg:
129 test['args'].pop(idx)
130 test['extra_browser_args'] = arg.replace('--extra-browser-args=', '')
131 break
Brian Sheedyb6491ba2022-09-26 20:49:49132 return isolated_scripts
133
134
Kenneth Russelleb60cbd22017-12-05 07:54:28135class GTestGenerator(BaseGenerator):
136 def __init__(self, bb_gen):
137 super(GTestGenerator, self).__init__(bb_gen)
138
Kenneth Russell8ceeabf2017-12-11 17:53:28139 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28140 # The relative ordering of some of the tests is important to
141 # minimize differences compared to the handwritten JSON files, since
142 # Python's sorts are stable and there are some tests with the same
143 # key (see gles2_conform_d3d9_test and similar variants). Avoid
144 # losing the order by avoiding coalescing the dictionaries into one.
145 gtests = []
Jamie Madillcf4f8c72021-05-20 19:24:23146 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoon67c3e832020-02-08 07:39:38147 # Variants allow more than one definition for a given test, and is defined
148 # in array format from resolve_variants().
149 if not isinstance(test_config, list):
150 test_config = [test_config]
151
152 for config in test_config:
153 test = self.bb_gen.generate_gtest(
154 waterfall, tester_name, tester_config, test_name, config)
155 if test:
156 # generate_gtest may veto the test generation on this tester.
157 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28158 return gtests
159
160 def sort(self, tests):
Jamie Madillcf4f8c72021-05-20 19:24:23161 return sorted(tests, key=functools.cmp_to_key(cmp_tests))
Kenneth Russelleb60cbd22017-12-05 07:54:28162
163
164class IsolatedScriptTestGenerator(BaseGenerator):
165 def __init__(self, bb_gen):
166 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
167
Kenneth Russell8ceeabf2017-12-11 17:53:28168 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28169 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23170 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43171 # Variants allow more than one definition for a given test, and is defined
172 # in array format from resolve_variants().
173 if not isinstance(test_config, list):
174 test_config = [test_config]
175
176 for config in test_config:
177 test = self.bb_gen.generate_isolated_script_test(
178 waterfall, tester_name, tester_config, test_name, config)
179 if test:
180 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28181 return isolated_scripts
182
183 def sort(self, tests):
184 return sorted(tests, key=lambda x: x['name'])
185
186
187class ScriptGenerator(BaseGenerator):
188 def __init__(self, bb_gen):
189 super(ScriptGenerator, self).__init__(bb_gen)
190
Kenneth Russell8ceeabf2017-12-11 17:53:28191 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28192 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23193 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28194 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28195 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28196 if test:
197 scripts.append(test)
198 return scripts
199
200 def sort(self, tests):
201 return sorted(tests, key=lambda x: x['name'])
202
203
204class JUnitGenerator(BaseGenerator):
205 def __init__(self, bb_gen):
206 super(JUnitGenerator, self).__init__(bb_gen)
207
Kenneth Russell8ceeabf2017-12-11 17:53:28208 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28209 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23210 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28211 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28212 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28213 if test:
214 scripts.append(test)
215 return scripts
216
217 def sort(self, tests):
218 return sorted(tests, key=lambda x: x['test'])
219
220
Xinan Lin05fb9c1752020-12-17 00:15:52221class SkylabGenerator(BaseGenerator):
222 def __init__(self, bb_gen):
223 super(SkylabGenerator, self).__init__(bb_gen)
224
225 def generate(self, waterfall, tester_name, tester_config, input_tests):
226 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23227 for test_name, test_config in sorted(input_tests.items()):
Xinan Lin05fb9c1752020-12-17 00:15:52228 for config in test_config:
229 test = self.bb_gen.generate_skylab_test(waterfall, tester_name,
230 tester_config, test_name,
231 config)
232 if test:
233 scripts.append(test)
234 return scripts
235
236 def sort(self, tests):
237 return sorted(tests, key=lambda x: x['test'])
238
239
Jeff Yoon67c3e832020-02-08 07:39:38240def check_compound_references(other_test_suites=None,
241 sub_suite=None,
242 suite=None,
243 target_test_suites=None,
244 test_type=None,
245 **kwargs):
246 """Ensure comound reference's don't target other compounds"""
247 del kwargs
248 if sub_suite in other_test_suites or sub_suite in target_test_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15249 raise BBGenErr('%s may not refer to other composition type test '
250 'suites (error found while processing %s)' %
251 (test_type, suite))
252
Jeff Yoon67c3e832020-02-08 07:39:38253
254def check_basic_references(basic_suites=None,
255 sub_suite=None,
256 suite=None,
257 **kwargs):
258 """Ensure test has a basic suite reference"""
259 del kwargs
260 if sub_suite not in basic_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15261 raise BBGenErr('Unable to find reference to %s while processing %s' %
262 (sub_suite, suite))
263
Jeff Yoon67c3e832020-02-08 07:39:38264
265def check_conflicting_definitions(basic_suites=None,
266 seen_tests=None,
267 sub_suite=None,
268 suite=None,
269 test_type=None,
270 **kwargs):
271 """Ensure that if a test is reachable via multiple basic suites,
272 all of them have an identical definition of the tests.
273 """
274 del kwargs
275 for test_name in basic_suites[sub_suite]:
276 if (test_name in seen_tests and
277 basic_suites[sub_suite][test_name] !=
278 basic_suites[seen_tests[test_name]][test_name]):
279 raise BBGenErr('Conflicting test definitions for %s from %s '
280 'and %s in %s (error found while processing %s)'
281 % (test_name, seen_tests[test_name], sub_suite,
282 test_type, suite))
283 seen_tests[test_name] = sub_suite
284
285def check_matrix_identifier(sub_suite=None,
286 suite=None,
287 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05288 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38289 **kwargs):
290 """Ensure 'idenfitier' is defined for each variant"""
291 del kwargs
292 sub_suite_config = suite_def[sub_suite]
293 for variant in sub_suite_config.get('variants', []):
Jeff Yoonda581c32020-03-06 03:56:05294 if isinstance(variant, str):
295 if variant not in all_variants:
296 raise BBGenErr('Missing variant definition for %s in variants.pyl'
297 % variant)
298 variant = all_variants[variant]
299
Jeff Yoon67c3e832020-02-08 07:39:38300 if not 'identifier' in variant:
301 raise BBGenErr('Missing required identifier field in matrix '
302 'compound suite %s, %s' % (suite, sub_suite))
Sven Zhengef0d0872022-04-04 22:13:29303 if variant['identifier'] == '':
304 raise BBGenErr('Identifier field can not be "" in matrix '
305 'compound suite %s, %s' % (suite, sub_suite))
306 if variant['identifier'].strip() != variant['identifier']:
307 raise BBGenErr('Identifier field can not have leading and trailing '
308 'whitespace in matrix compound suite %s, %s' %
309 (suite, sub_suite))
Jeff Yoon67c3e832020-02-08 07:39:38310
311
Joshua Hood56c673c2022-03-02 20:29:33312class BBJSONGenerator(object): # pylint: disable=useless-object-inheritance
Garrett Beaty1afaccc2020-06-25 19:58:15313 def __init__(self, args):
Kenneth Russelleb60cbd22017-12-05 07:54:28314 self.this_dir = THIS_DIR
Garrett Beaty1afaccc2020-06-25 19:58:15315 self.args = args
Kenneth Russelleb60cbd22017-12-05 07:54:28316 self.waterfalls = None
317 self.test_suites = None
318 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01319 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41320 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05321 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28322
Garrett Beaty1afaccc2020-06-25 19:58:15323 @staticmethod
324 def parse_args(argv):
325
326 # RawTextHelpFormatter allows for styling of help statement
327 parser = argparse.ArgumentParser(
328 formatter_class=argparse.RawTextHelpFormatter)
329
330 group = parser.add_mutually_exclusive_group()
331 group.add_argument(
332 '-c',
333 '--check',
334 action='store_true',
335 help=
336 'Do consistency checks of configuration and generated files and then '
337 'exit. Used during presubmit. '
338 'Causes the tool to not generate any files.')
339 group.add_argument(
340 '--query',
341 type=str,
342 help=(
343 "Returns raw JSON information of buildbots and tests.\n" +
344 "Examples:\n" + " List all bots (all info):\n" +
345 " --query bots\n\n" +
346 " List all bots and only their associated tests:\n" +
347 " --query bots/tests\n\n" +
348 " List all information about 'bot1' " +
349 "(make sure you have quotes):\n" + " --query bot/'bot1'\n\n" +
350 " List tests running for 'bot1' (make sure you have quotes):\n" +
351 " --query bot/'bot1'/tests\n\n" + " List all tests:\n" +
352 " --query tests\n\n" +
353 " List all tests and the bots running them:\n" +
354 " --query tests/bots\n\n" +
355 " List all tests that satisfy multiple parameters\n" +
356 " (separation of parameters by '&' symbol):\n" +
357 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
358 " List all tests that run with a specific flag:\n" +
359 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
360 " List specific test (make sure you have quotes):\n"
361 " --query test/'test1'\n\n"
362 " List all bots running 'test1' " +
363 "(make sure you have quotes):\n" + " --query test/'test1'/bots"))
364 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47365 '--json',
366 metavar='JSON_FILE_PATH',
367 type=os.path.abspath,
368 help='Outputs results into a json file. Only works with query function.'
369 )
370 parser.add_argument(
Garrett Beaty1afaccc2020-06-25 19:58:15371 '-n',
372 '--new-files',
373 action='store_true',
374 help=
375 'Write output files as .new.json. Useful during development so old and '
376 'new files can be looked at side-by-side.')
Garrett Beatyade673d2023-08-04 22:00:25377 parser.add_argument('--dimension-sets-handling',
378 choices=['disable'],
379 default='disable',
380 help=('This flag no longer has any effect:'
381 ' dimension_sets fields are not allowed'))
Garrett Beaty1afaccc2020-06-25 19:58:15382 parser.add_argument('-v',
383 '--verbose',
384 action='store_true',
385 help='Increases verbosity. Affects consistency checks.')
386 parser.add_argument('waterfall_filters',
387 metavar='waterfalls',
388 type=str,
389 nargs='*',
390 help='Optional list of waterfalls to generate.')
391 parser.add_argument(
392 '--pyl-files-dir',
Garrett Beaty79339e182023-04-10 20:45:47393 type=os.path.abspath,
394 help=('Path to the directory containing the input .pyl files.'
395 ' By default the directory containing this script will be used.'))
Garrett Beaty1afaccc2020-06-25 19:58:15396 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47397 '--output-dir',
398 type=os.path.abspath,
399 help=('Path to the directory to output generated .json files.'
400 'By default, the pyl files directory will be used.'))
Chong Guee622242020-10-28 18:17:35401 parser.add_argument('--isolate-map-file',
402 metavar='PATH',
403 help='path to additional isolate map files.',
Garrett Beaty79339e182023-04-10 20:45:47404 type=os.path.abspath,
Chong Guee622242020-10-28 18:17:35405 default=[],
406 action='append',
407 dest='isolate_map_files')
Garrett Beaty1afaccc2020-06-25 19:58:15408 parser.add_argument(
409 '--infra-config-dir',
410 help='Path to the LUCI services configuration directory',
Garrett Beaty79339e182023-04-10 20:45:47411 type=os.path.abspath,
412 default=os.path.join(os.path.dirname(__file__), '..', '..', 'infra',
413 'config'))
414
Garrett Beaty1afaccc2020-06-25 19:58:15415 args = parser.parse_args(argv)
416 if args.json and not args.query:
417 parser.error(
418 "The --json flag can only be used with --query.") # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:15419
Garrett Beaty79339e182023-04-10 20:45:47420 args.pyl_files_dir = args.pyl_files_dir or THIS_DIR
421 args.output_dir = args.output_dir or args.pyl_files_dir
422
Stephanie Kim572b43c02023-04-13 14:24:13423 def absolute_file_path(filename):
Garrett Beaty79339e182023-04-10 20:45:47424 return os.path.join(args.pyl_files_dir, filename)
425
Stephanie Kim572b43c02023-04-13 14:24:13426 args.waterfalls_pyl_path = absolute_file_path('waterfalls.pyl')
Garrett Beaty96802d02023-07-07 14:18:05427 args.mixins_pyl_path = absolute_file_path('mixins.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13428 args.test_suites_pyl_path = absolute_file_path('test_suites.pyl')
429 args.test_suite_exceptions_pyl_path = absolute_file_path(
Garrett Beaty79339e182023-04-10 20:45:47430 'test_suite_exceptions.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13431 args.gn_isolate_map_pyl_path = absolute_file_path('gn_isolate_map.pyl')
432 args.variants_pyl_path = absolute_file_path('variants.pyl')
433 args.autoshard_exceptions_json_path = absolute_file_path(
434 'autoshard_exceptions.json')
Garrett Beaty79339e182023-04-10 20:45:47435
436 return args
Kenneth Russelleb60cbd22017-12-05 07:54:28437
Stephen Martinis7eb8b612018-09-21 00:17:50438 def print_line(self, line):
439 # Exists so that tests can mock
Jamie Madillcf4f8c72021-05-20 19:24:23440 print(line) # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:50441
Kenneth Russelleb60cbd22017-12-05 07:54:28442 def read_file(self, relative_path):
Garrett Beaty79339e182023-04-10 20:45:47443 with open(relative_path) as fp:
Garrett Beaty1afaccc2020-06-25 19:58:15444 return fp.read()
Kenneth Russelleb60cbd22017-12-05 07:54:28445
Garrett Beaty79339e182023-04-10 20:45:47446 def write_file(self, file_path, contents):
447 with open(file_path, 'w') as fp:
448 fp.write(contents)
Zhiling Huangbe008172018-03-08 19:13:11449
Joshua Hood56c673c2022-03-02 20:29:33450 # pylint: disable=inconsistent-return-statements
Garrett Beaty79339e182023-04-10 20:45:47451 def load_pyl_file(self, pyl_file_path):
Kenneth Russelleb60cbd22017-12-05 07:54:28452 try:
Garrett Beaty79339e182023-04-10 20:45:47453 return ast.literal_eval(self.read_file(pyl_file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:28454 except (SyntaxError, ValueError) as e: # pragma: no cover
Josip Sokcevic7110fb382023-06-06 01:05:29455 raise BBGenErr('Failed to parse pyl file "%s": %s' %
456 (pyl_file_path, e)) from e
Joshua Hood56c673c2022-03-02 20:29:33457 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:28458
Kenneth Russell8a386d42018-06-02 09:48:01459 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
460 # Currently it is only mandatory for bots which run GPU tests. Change these to
461 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28462 def is_android(self, tester_config):
463 return tester_config.get('os_type') == 'android'
464
Ben Pastenea9e583b2019-01-16 02:57:26465 def is_chromeos(self, tester_config):
466 return tester_config.get('os_type') == 'chromeos'
467
Chong Guc2ca5d02022-01-11 19:52:17468 def is_fuchsia(self, tester_config):
469 return tester_config.get('os_type') == 'fuchsia'
470
Brian Sheedy781c8ca42021-03-08 22:03:21471 def is_lacros(self, tester_config):
472 return tester_config.get('os_type') == 'lacros'
473
Kenneth Russell8a386d42018-06-02 09:48:01474 def is_linux(self, tester_config):
475 return tester_config.get('os_type') == 'linux'
476
Kai Ninomiya40de9f52019-10-18 21:38:49477 def is_mac(self, tester_config):
478 return tester_config.get('os_type') == 'mac'
479
480 def is_win(self, tester_config):
481 return tester_config.get('os_type') == 'win'
482
483 def is_win64(self, tester_config):
484 return (tester_config.get('os_type') == 'win' and
485 tester_config.get('browser_config') == 'release_x64')
486
Ben Pastene5f231cf22022-05-05 18:03:07487 def add_variant_to_test_name(self, test_name, variant_id):
488 return '{} {}'.format(test_name, variant_id)
489
490 def remove_variant_from_test_name(self, test_name, variant_id):
491 return test_name.split(variant_id)[0].strip()
492
Kenneth Russelleb60cbd22017-12-05 07:54:28493 def get_exception_for_test(self, test_name, test_config):
494 # gtests may have both "test" and "name" fields, and usually, if the "name"
495 # field is specified, it means that the same test is being repurposed
496 # multiple times with different command line arguments. To handle this case,
497 # prefer to lookup per the "name" field of the test itself, as opposed to
498 # the "test_name", which is actually the "test" field.
499 if 'name' in test_config:
500 return self.exceptions.get(test_config['name'])
Joshua Hood56c673c2022-03-02 20:29:33501 return self.exceptions.get(test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28502
Nico Weberb0b3f5862018-07-13 18:45:15503 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28504 # Currently, the only reason a test should not run on a given tester is that
505 # it's in the exceptions. (Once the GPU waterfall generation script is
506 # incorporated here, the rules will become more complex.)
507 exception = self.get_exception_for_test(test_name, test_config)
508 if not exception:
509 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28510 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28511 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28512 if remove_from:
513 if tester_name in remove_from:
514 return False
515 # TODO(kbr): this code path was added for some tests (including
516 # android_webview_unittests) on one machine (Nougat Phone
517 # Tester) which exists with the same name on two waterfalls,
518 # chromium.android and chromium.fyi; the tests are run on one
519 # but not the other. Once the bots are all uniquely named (a
520 # different ongoing project) this code should be removed.
521 # TODO(kbr): add coverage.
522 return (tester_name + ' ' + waterfall['name']
523 not in remove_from) # pragma: no cover
524 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28525
Nico Weber79dc5f6852018-07-13 19:38:49526 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28527 exception = self.get_exception_for_test(test_name, test)
528 if not exception:
529 return None
Nico Weber79dc5f6852018-07-13 19:38:49530 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28531
Brian Sheedye6ea0ee2019-07-11 02:54:37532 def get_test_replacements(self, test, test_name, tester_name):
533 exception = self.get_exception_for_test(test_name, test)
534 if not exception:
535 return None
536 return exception.get('replacements', {}).get(tester_name)
537
Kenneth Russell8a386d42018-06-02 09:48:01538 def merge_command_line_args(self, arr, prefix, splitter):
539 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01540 idx = 0
541 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01542 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01543 while idx < len(arr):
544 flag = arr[idx]
545 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01546 if flag.startswith(prefix):
547 arg = flag[prefix_len:]
548 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01549 if first_idx < 0:
550 first_idx = idx
551 else:
552 delete_current_entry = True
553 if delete_current_entry:
554 del arr[idx]
555 else:
556 idx += 1
557 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01558 arr[first_idx] = prefix + splitter.join(accumulated_args)
559 return arr
560
561 def maybe_fixup_args_array(self, arr):
562 # The incoming array of strings may be an array of command line
563 # arguments. To make it easier to turn on certain features per-bot or
564 # per-test-suite, look specifically for certain flags and merge them
565 # appropriately.
566 # --enable-features=Feature1 --enable-features=Feature2
567 # are merged to:
568 # --enable-features=Feature1,Feature2
569 # and:
570 # --extra-browser-args=arg1 --extra-browser-args=arg2
571 # are merged to:
572 # --extra-browser-args=arg1 arg2
573 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
574 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Yuly Novikov8c487e72020-10-16 20:00:29575 arr = self.merge_command_line_args(arr, '--test-launcher-filter-file=', ';')
Cameron Higgins971f0b92023-01-03 18:05:09576 arr = self.merge_command_line_args(arr, '--extra-app-args=', ',')
Kenneth Russell650995a2018-05-03 21:17:01577 return arr
578
Brian Sheedy910cda82022-07-19 11:58:34579 def substitute_magic_args(self, test_config, tester_name, tester_config):
Brian Sheedya31578e2020-05-18 20:24:36580 """Substitutes any magic substitution args present in |test_config|.
581
582 Substitutions are done in-place.
583
584 See buildbot_json_magic_substitutions.py for more information on this
585 feature.
586
587 Args:
588 test_config: A dict containing a configuration for a specific test on
589 a specific builder, e.g. the output of update_and_cleanup_test.
Brian Sheedy5f173bb2021-11-24 00:45:54590 tester_name: A string containing the name of the tester that |test_config|
591 came from.
Brian Sheedy910cda82022-07-19 11:58:34592 tester_config: A dict containing the configuration for the builder that
593 |test_config| is for.
Brian Sheedya31578e2020-05-18 20:24:36594 """
595 substituted_array = []
Brian Sheedyba13cf522022-09-13 21:00:09596 original_args = test_config.get('args', [])
597 for arg in original_args:
Brian Sheedya31578e2020-05-18 20:24:36598 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
599 function = arg.replace(
600 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
601 if hasattr(magic_substitutions, function):
602 substituted_array.extend(
Brian Sheedy910cda82022-07-19 11:58:34603 getattr(magic_substitutions, function)(test_config, tester_name,
604 tester_config))
Brian Sheedya31578e2020-05-18 20:24:36605 else:
606 raise BBGenErr(
607 'Magic substitution function %s does not exist' % function)
608 else:
609 substituted_array.append(arg)
Brian Sheedyba13cf522022-09-13 21:00:09610 if substituted_array != original_args:
Brian Sheedya31578e2020-05-18 20:24:36611 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
612
Garrett Beaty8d6708c2023-07-20 17:20:41613 def dictionary_merge(self, a, b, path=None):
Kenneth Russelleb60cbd22017-12-05 07:54:28614 """http://stackoverflow.com/questions/7204805/
615 python-dictionaries-of-dictionaries-merge
616 merges b into a
617 """
618 if path is None:
619 path = []
620 for key in b:
Garrett Beaty8d6708c2023-07-20 17:20:41621 if key not in a:
622 if b[key] is not None:
623 a[key] = b[key]
624 continue
625
626 if isinstance(a[key], dict) and isinstance(b[key], dict):
627 self.dictionary_merge(a[key], b[key], path + [str(key)])
628 elif a[key] == b[key]:
629 pass # same leaf value
630 elif isinstance(a[key], list) and isinstance(b[key], list):
Garrett Beatyade673d2023-08-04 22:00:25631 a[key] = a[key] + b[key]
632 if key.endswith('args'):
633 a[key] = self.maybe_fixup_args_array(a[key])
Garrett Beaty8d6708c2023-07-20 17:20:41634 elif b[key] is None:
635 del a[key]
636 else:
Kenneth Russelleb60cbd22017-12-05 07:54:28637 a[key] = b[key]
Garrett Beaty8d6708c2023-07-20 17:20:41638
Kenneth Russelleb60cbd22017-12-05 07:54:28639 return a
640
John Budorickab108712018-09-01 00:12:21641 def initialize_args_for_test(
642 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21643 args = []
644 args.extend(generated_test.get('args', []))
645 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22646
Kenneth Russell8a386d42018-06-02 09:48:01647 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21648 val = generated_test.pop(key, [])
649 if fn(tester_config):
650 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01651
652 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
Brian Sheedy781c8ca42021-03-08 22:03:21653 add_conditional_args('lacros_args', self.is_lacros)
Kenneth Russell8a386d42018-06-02 09:48:01654 add_conditional_args('linux_args', self.is_linux)
655 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36656 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49657 add_conditional_args('mac_args', self.is_mac)
658 add_conditional_args('win_args', self.is_win)
659 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01660
John Budorickab108712018-09-01 00:12:21661 for key in additional_arg_keys or []:
662 args.extend(generated_test.pop(key, []))
663 args.extend(tester_config.get(key, []))
664
665 if args:
666 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01667
Kenneth Russelleb60cbd22017-12-05 07:54:28668 def initialize_swarming_dictionary_for_test(self, generated_test,
669 tester_config):
670 if 'swarming' not in generated_test:
671 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28672 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
673 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38674 'can_use_on_swarming_builders': tester_config.get('use_swarming',
675 True)
Dirk Pranke81ff51c2017-12-09 19:24:28676 })
Kenneth Russelleb60cbd22017-12-05 07:54:28677 if 'swarming' in tester_config:
Kenneth Russelleb60cbd22017-12-05 07:54:28678 self.dictionary_merge(generated_test['swarming'],
679 tester_config['swarming'])
Brian Sheedybc984e242021-04-21 23:44:51680 # Apply any platform-specific Swarming dimensions after the generic ones.
Kenneth Russelleb60cbd22017-12-05 07:54:28681 if 'android_swarming' in generated_test:
682 if self.is_android(tester_config): # pragma: no cover
683 self.dictionary_merge(
684 generated_test['swarming'],
685 generated_test['android_swarming']) # pragma: no cover
686 del generated_test['android_swarming'] # pragma: no cover
Brian Sheedybc984e242021-04-21 23:44:51687 if 'chromeos_swarming' in generated_test:
688 if self.is_chromeos(tester_config): # pragma: no cover
689 self.dictionary_merge(
690 generated_test['swarming'],
691 generated_test['chromeos_swarming']) # pragma: no cover
692 del generated_test['chromeos_swarming'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28693
694 def clean_swarming_dictionary(self, swarming_dict):
695 # Clean out redundant entries from a test's "swarming" dictionary.
696 # This is really only needed to retain 100% parity with the
697 # handwritten JSON files, and can be removed once all the files are
698 # autogenerated.
699 if 'shards' in swarming_dict:
700 if swarming_dict['shards'] == 1: # pragma: no cover
701 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24702 if 'hard_timeout' in swarming_dict:
703 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
704 del swarming_dict['hard_timeout'] # pragma: no cover
Garrett Beatybb18d532023-06-26 22:16:33705 del swarming_dict['can_use_on_swarming_builders']
Kenneth Russelleb60cbd22017-12-05 07:54:28706
Stephen Martinis0382bc12018-09-17 22:29:07707 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
708 waterfall):
709 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01710 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07711 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28712 # See if there are any exceptions that need to be merged into this
713 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49714 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28715 if modifications:
716 test = self.dictionary_merge(test, modifications)
Garrett Beatybfeff8f2023-06-16 18:57:25717 if (swarming_dict := test.get('swarming')) is not None:
Garrett Beatybb18d532023-06-26 22:16:33718 if swarming_dict.get('can_use_on_swarming_builders'):
Garrett Beatybfeff8f2023-06-16 18:57:25719 self.clean_swarming_dictionary(swarming_dict)
720 else:
721 del test['swarming']
Ben Pastenee012aea42019-05-14 22:32:28722 # Ensure all Android Swarming tests run only on userdebug builds if another
723 # build type was not specified.
724 if 'swarming' in test and self.is_android(tester_config):
Garrett Beatyade673d2023-08-04 22:00:25725 dimensions = test.get('swarming', {}).get('dimensions', {})
726 if (dimensions.get('os') == 'Android'
727 and not dimensions.get('device_os_type')):
728 dimensions['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37729 self.replace_test_args(test, test_name, tester_name)
Garrett Beatyafd33e0f2023-06-23 20:47:57730 if 'args' in test and not test['args']:
731 test.pop('args')
Ben Pastenee012aea42019-05-14 22:32:28732
Kenneth Russelleb60cbd22017-12-05 07:54:28733 return test
734
Brian Sheedye6ea0ee2019-07-11 02:54:37735 def replace_test_args(self, test, test_name, tester_name):
736 replacements = self.get_test_replacements(
737 test, test_name, tester_name) or {}
738 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23739 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37740 if key not in valid_replacement_keys:
741 raise BBGenErr(
742 'Given replacement key %s for %s on %s is not in the list of valid '
743 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23744 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37745 found_key = False
746 for i, test_key in enumerate(test.get(key, [])):
747 # Handle both the key/value being replaced being defined as two
748 # separate items or as key=value.
749 if test_key == replacement_key:
750 found_key = True
751 # Handle flags without values.
752 if replacement_val == None:
753 del test[key][i]
754 else:
755 test[key][i+1] = replacement_val
756 break
Joshua Hood56c673c2022-03-02 20:29:33757 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37758 found_key = True
759 if replacement_val == None:
760 del test[key][i]
761 else:
762 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
763 break
764 if not found_key:
765 raise BBGenErr('Could not find %s in existing list of values for key '
766 '%s in %s on %s' % (replacement_key, key, test_name,
767 tester_name))
768
Shenghua Zhangaba8bad2018-02-07 02:12:09769 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05770 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26771 True):
772 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06773 # are targeting CrOS hardware and so need the special trigger script.
Garrett Beatyade673d2023-08-04 22:00:25774 if 'device_type' in test.get('swarming', {}).get('dimensions', {}):
Ben Pastenea9e583b2019-01-16 02:57:26775 test['trigger_script'] = {
776 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
777 }
Shenghua Zhangaba8bad2018-02-07 02:12:09778
Ben Pastene858f4be2019-01-09 23:52:09779 def add_android_presentation_args(self, tester_config, test_name, result):
780 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38781 bucket = tester_config.get('results_bucket', 'chromium-result-details')
782 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09783 if (result['swarming']['can_use_on_swarming_builders'] and not
784 tester_config.get('skip_merge_script', False)):
785 result['merge'] = {
786 'args': [
787 '--bucket',
John Budorick262ae112019-07-12 19:24:38788 bucket,
Ben Pastene858f4be2019-01-09 23:52:09789 '--test-name',
Rakib M. Hasanc9e01c62020-07-27 22:48:12790 result.get('name', test_name)
Ben Pastene858f4be2019-01-09 23:52:09791 ],
792 'script': '//build/android/pylib/results/presentation/'
793 'test_results_presentation.py',
794 }
Ben Pastene858f4be2019-01-09 23:52:09795 if not tester_config.get('skip_output_links', False):
796 result['swarming']['output_links'] = [
797 {
798 'link': [
799 'https://luci-logdog.appspot.com/v/?s',
800 '=android%2Fswarming%2Flogcats%2F',
801 '${TASK_ID}%2F%2B%2Funified_logcats',
802 ],
803 'name': 'shard #${SHARD_INDEX} logcats',
804 },
805 ]
806 if args:
807 result['args'] = args
808
Kenneth Russelleb60cbd22017-12-05 07:54:28809 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
810 test_config):
811 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15812 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28813 return None
814 result = copy.deepcopy(test_config)
815 if 'test' in result:
Rakib M. Hasanc9e01c62020-07-27 22:48:12816 if 'name' not in result:
817 result['name'] = test_name
Kenneth Russelleb60cbd22017-12-05 07:54:28818 else:
819 result['test'] = test_name
820 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21821
822 self.initialize_args_for_test(
823 result, tester_config, additional_arg_keys=['gtest_args'])
Jamie Madilla8be0d72020-10-02 05:24:04824 if self.is_android(tester_config) and tester_config.get(
Yuly Novikov26dd47052021-02-11 00:57:14825 'use_swarming', True):
826 if not test_config.get('use_isolated_scripts_api', False):
827 # TODO(https://crbug.com/1137998) make Android presentation work with
828 # isolated scripts in test_results_presentation.py merge script
829 self.add_android_presentation_args(tester_config, test_name, result)
830 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42831
Stephen Martinis0382bc12018-09-17 22:29:07832 result = self.update_and_cleanup_test(
833 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09834 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34835 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43836
Garrett Beatybb18d532023-06-26 22:16:33837 if 'swarming' in result and not result.get('merge'):
Jamie Madilla8be0d72020-10-02 05:24:04838 if test_config.get('use_isolated_scripts_api', False):
839 merge_script = 'standard_isolated_script_merge'
840 else:
841 merge_script = 'standard_gtest_merge'
842
Stephen Martinisbc7b7772019-05-01 22:01:43843 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04844 'script': '//testing/merge_scripts/%s.py' % merge_script,
Stephen Martinisbc7b7772019-05-01 22:01:43845 }
Kenneth Russelleb60cbd22017-12-05 07:54:28846 return result
847
848 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
849 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01850 if not self.should_run_on_tester(waterfall, tester_name, test_name,
851 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28852 return None
853 result = copy.deepcopy(test_config)
854 result['isolate_name'] = result.get('isolate_name', test_name)
Jeff Yoonb8bfdbf32020-03-13 19:14:43855 result['name'] = result.get('name', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28856 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01857 self.initialize_args_for_test(result, tester_config)
Yuly Novikov26dd47052021-02-11 00:57:14858 if self.is_android(tester_config) and tester_config.get(
859 'use_swarming', True):
860 if tester_config.get('use_android_presentation', False):
861 # TODO(https://crbug.com/1137998) make Android presentation work with
862 # isolated scripts in test_results_presentation.py merge script
863 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07864 result = self.update_and_cleanup_test(
865 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09866 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34867 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17868
Garrett Beatybb18d532023-06-26 22:16:33869 if 'swarming' in result and not result.get('merge'):
Stephen Martinisf50047062019-05-06 22:26:17870 # TODO(https://crbug.com/958376): Consider adding the ability to not have
871 # this default.
872 result['merge'] = {
873 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
Stephen Martinisf50047062019-05-06 22:26:17874 }
Kenneth Russelleb60cbd22017-12-05 07:54:28875 return result
876
877 def generate_script_test(self, waterfall, tester_name, tester_config,
878 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44879 # TODO(https://crbug.com/953072): Remove this check whenever a better
880 # long-term solution is implemented.
881 if (waterfall.get('forbid_script_tests', False) or
882 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
883 raise BBGenErr('Attempted to generate a script test on tester ' +
884 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01885 if not self.should_run_on_tester(waterfall, tester_name, test_name,
886 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28887 return None
888 result = {
889 'name': test_name,
890 'script': test_config['script']
891 }
Stephen Martinis0382bc12018-09-17 22:29:07892 result = self.update_and_cleanup_test(
893 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34894 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28895 return result
896
897 def generate_junit_test(self, waterfall, tester_name, tester_config,
898 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01899 if not self.should_run_on_tester(waterfall, tester_name, test_name,
900 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28901 return None
John Budorickdef6acb2019-09-17 22:51:09902 result = copy.deepcopy(test_config)
903 result.update({
John Budorickcadc4952019-09-16 23:51:37904 'name': test_name,
905 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09906 })
907 self.initialize_args_for_test(result, tester_config)
908 result = self.update_and_cleanup_test(
909 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34910 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28911 return result
912
Xinan Lin05fb9c1752020-12-17 00:15:52913 def generate_skylab_test(self, waterfall, tester_name, tester_config,
914 test_name, test_config):
915 if not self.should_run_on_tester(waterfall, tester_name, test_name,
916 test_config):
917 return None
918 result = copy.deepcopy(test_config)
919 result.update({
920 'test': test_name,
921 })
922 self.initialize_args_for_test(result, tester_config)
923 result = self.update_and_cleanup_test(result, test_name, tester_name,
924 tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34925 self.substitute_magic_args(result, tester_name, tester_config)
Xinan Lin05fb9c1752020-12-17 00:15:52926 return result
927
Garrett Beaty65d44222023-08-01 17:22:11928 def substitute_gpu_args(self, tester_config, test, args):
Kenneth Russell8a386d42018-06-02 09:48:01929 substitutions = {
930 # Any machine in waterfalls.pyl which desires to run GPU tests
931 # must provide the os_type key.
932 'os_type': tester_config['os_type'],
933 'gpu_vendor_id': '0',
934 'gpu_device_id': '0',
935 }
Garrett Beatyade673d2023-08-04 22:00:25936 dimensions = test.get('swarming', {}).get('dimensions', {})
937 if 'gpu' in dimensions:
938 # First remove the driver version, then split into vendor and device.
939 gpu = dimensions['gpu']
940 if gpu != 'none':
941 gpu = gpu.split('-')[0].split(':')
942 substitutions['gpu_vendor_id'] = gpu[0]
943 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01944 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
945
946 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30947 test_name, test_config, is_android_webview,
948 is_cast_streaming):
Kenneth Russell8a386d42018-06-02 09:48:01949 # These are all just specializations of isolated script tests with
950 # a bunch of boilerplate command line arguments added.
951
952 # The step name must end in 'test' or 'tests' in order for the
953 # results to automatically show up on the flakiness dashboard.
954 # (At least, this was true some time ago.) Continue to use this
955 # naming convention for the time being to minimize changes.
956 step_name = test_config.get('name', test_name)
Ben Pastene5f231cf22022-05-05 18:03:07957 variant_id = test_config.get('variant_id')
958 if variant_id:
959 step_name = self.remove_variant_from_test_name(step_name, variant_id)
Kenneth Russell8a386d42018-06-02 09:48:01960 if not (step_name.endswith('test') or step_name.endswith('tests')):
961 step_name = '%s_tests' % step_name
Ben Pastene5f231cf22022-05-05 18:03:07962 if variant_id:
963 step_name = self.add_variant_to_test_name(step_name, variant_id)
Ben Pastene5ff45d82022-05-05 21:54:00964 if 'name' in test_config:
965 test_config['name'] = step_name
Kenneth Russell8a386d42018-06-02 09:48:01966 result = self.generate_isolated_script_test(
967 waterfall, tester_name, tester_config, step_name, test_config)
968 if not result:
969 return None
Chong Gub75754b32020-03-13 16:39:20970 result['isolate_name'] = test_config.get(
Brian Sheedyf74819b2021-06-04 01:38:38971 'isolate_name',
972 self.get_default_isolate_name(tester_config, is_android_webview))
Chan Liab7d8dd82020-04-24 23:42:19973
Chan Lia3ad1502020-04-28 05:32:11974 # Populate test_id_prefix.
Brian Sheedyf74819b2021-06-04 01:38:38975 gn_entry = self.gn_isolate_map[result['isolate_name']]
Chan Li17d969f92020-07-10 00:50:03976 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19977
Kenneth Russell8a386d42018-06-02 09:48:01978 args = result.get('args', [])
979 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14980
erikchen6da2d9b2018-08-03 23:01:14981 # These tests upload and download results from cloud storage and therefore
982 # aren't idempotent yet. https://crbug.com/549140.
Garrett Beatybfeff8f2023-06-16 18:57:25983 if 'swarming' in result:
984 result['swarming']['idempotent'] = False
erikchen6da2d9b2018-08-03 23:01:14985
Kenneth Russell44910c32018-12-03 23:35:11986 # The GPU tests act much like integration tests for the entire browser, and
987 # tend to uncover flakiness bugs more readily than other test suites. In
988 # order to surface any flakiness more readily to the developer of the CL
989 # which is introducing it, we disable retries with patch on the commit
990 # queue.
991 result['should_retry_with_patch'] = False
992
Fabrice de Ganscbd655f2022-08-04 20:15:30993 browser = ''
994 if is_cast_streaming:
995 browser = 'cast-streaming-shell'
996 elif is_android_webview:
997 browser = 'android-webview-instrumentation'
998 else:
999 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:521000
Greg Thompsoncec7d8d2023-01-10 19:11:531001 extra_browser_args = []
1002
Brian Sheedy4053a702020-07-28 02:09:521003 # Most platforms require --enable-logging=stderr to get useful browser logs.
1004 # However, this actively messes with logging on CrOS (because Chrome's
1005 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
1006 # in order to see JavaScript console messages. See
1007 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
Greg Thompsoncec7d8d2023-01-10 19:11:531008 if self.is_chromeos(tester_config):
1009 extra_browser_args.append('--log-level=0')
1010 elif not self.is_fuchsia(tester_config) or browser != 'fuchsia-chrome':
1011 # Stderr logging is not needed for Chrome browser on Fuchsia, as ordinary
1012 # logging via syslog is captured.
1013 extra_browser_args.append('--enable-logging=stderr')
1014
1015 # --expose-gc allows the WebGL conformance tests to more reliably
1016 # reproduce GC-related bugs in the V8 bindings.
1017 extra_browser_args.append('--js-flags=--expose-gc')
Brian Sheedy4053a702020-07-28 02:09:521018
Kenneth Russell8a386d42018-06-02 09:48:011019 args = [
Bo Liu555a0f92019-03-29 12:11:561020 test_to_run,
1021 '--show-stdout',
1022 '--browser=%s' % browser,
1023 # --passthrough displays more of the logging in Telemetry when
1024 # run via typ, in particular some of the warnings about tests
1025 # being expected to fail, but passing.
1026 '--passthrough',
1027 '-v',
Brian Sheedy814e0482022-10-03 23:24:121028 '--stable-jobs',
Greg Thompsoncec7d8d2023-01-10 19:11:531029 '--extra-browser-args=%s' % ' '.join(extra_browser_args),
Kenneth Russell8a386d42018-06-02 09:48:011030 ] + args
Garrett Beatybfeff8f2023-06-16 18:57:251031 result['args'] = self.maybe_fixup_args_array(
Garrett Beaty65d44222023-08-01 17:22:111032 self.substitute_gpu_args(tester_config, result, args))
Kenneth Russell8a386d42018-06-02 09:48:011033 return result
1034
Brian Sheedyf74819b2021-06-04 01:38:381035 def get_default_isolate_name(self, tester_config, is_android_webview):
1036 if self.is_android(tester_config):
1037 if is_android_webview:
1038 return 'telemetry_gpu_integration_test_android_webview'
1039 return (
1040 'telemetry_gpu_integration_test' +
1041 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:331042 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:171043 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:331044 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:381045
Kenneth Russelleb60cbd22017-12-05 07:54:281046 def get_test_generator_map(self):
1047 return {
Bo Liu555a0f92019-03-29 12:11:561048 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301049 GPUTelemetryTestGenerator(self, is_android_webview=True),
1050 'cast_streaming_tests':
1051 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:561052 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301053 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561054 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301055 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561056 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301057 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561058 'junit_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301059 JUnitGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561060 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301061 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:521062 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301063 SkylabGenerator(self),
Brian Sheedyb6491ba2022-09-26 20:49:491064 'skylab_gpu_telemetry_tests':
1065 SkylabGPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281066 }
1067
Kenneth Russell8a386d42018-06-02 09:48:011068 def get_test_type_remapper(self):
1069 return {
Fabrice de Gans223272482022-08-08 16:56:571070 # These are a specialization of isolated_scripts with a bunch of
1071 # boilerplate command line arguments added to each one.
1072 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
1073 'cast_streaming_tests': 'isolated_scripts',
1074 'gpu_telemetry_tests': 'isolated_scripts',
Brian Sheedyb6491ba2022-09-26 20:49:491075 # These are the same as existing test types, just configured to run
1076 # in Skylab instead of via normal swarming.
1077 'skylab_gpu_telemetry_tests': 'skylab_tests',
Kenneth Russell8a386d42018-06-02 09:48:011078 }
1079
Jeff Yoon67c3e832020-02-08 07:39:381080 def check_composition_type_test_suites(self, test_type,
1081 additional_validators=None):
1082 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1083 validators = [check_compound_references,
1084 check_basic_references,
1085 check_conflicting_definitions]
1086 if additional_validators:
1087 validators += additional_validators
1088
1089 target_suites = self.test_suites.get(test_type, {})
1090 other_test_type = ('compound_suites'
1091 if test_type == 'matrix_compound_suites'
1092 else 'matrix_compound_suites')
1093 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011094 basic_suites = self.test_suites.get('basic_suites', {})
1095
Jamie Madillcf4f8c72021-05-20 19:24:231096 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011097 if suite in basic_suites:
1098 raise BBGenErr('%s names may not duplicate basic test suite names '
1099 '(error found while processsing %s)'
1100 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011101
Jeff Yoon67c3e832020-02-08 07:39:381102 seen_tests = {}
1103 for sub_suite in suite_def:
1104 for validator in validators:
1105 validator(
1106 basic_suites=basic_suites,
1107 other_test_suites=other_suites,
1108 seen_tests=seen_tests,
1109 sub_suite=sub_suite,
1110 suite=suite,
1111 suite_def=suite_def,
1112 target_test_suites=target_suites,
1113 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051114 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381115 )
Kenneth Russelleb60cbd22017-12-05 07:54:281116
Stephen Martinis54d64ad2018-09-21 22:16:201117 def flatten_test_suites(self):
1118 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011119 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1120 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231121 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011122 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201123 self.test_suites = new_test_suites
1124
Chan Lia3ad1502020-04-28 05:32:111125 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231126 for suite in self.test_suites['basic_suites'].values():
1127 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561128 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411129
1130 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
John Palmera8515fca2021-05-20 03:35:321131 # 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:411132 # TODO(crbug.com/1035124): clean this up.
1133 isolate_name = test.get('test') or test.get('isolate_name') or key
1134 gn_entry = self.gn_isolate_map.get(isolate_name)
1135 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281136 label = gn_entry['label']
1137
1138 if label.count(':') != 1:
1139 raise BBGenErr(
1140 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1141 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1142 (label, isolate_name))
1143 if label.split(':')[1] != isolate_name:
1144 raise BBGenErr(
1145 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1146 ' label "%s" see http://crbug.com/1071091 for details.' %
1147 (isolate_name, label))
1148
Chan Lia3ad1502020-04-28 05:32:111149 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411150 else: # pragma: no cover
1151 # Some tests do not have an entry gn_isolate_map.pyl, such as
1152 # telemetry tests.
1153 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
1154 pass
1155
Kenneth Russelleb60cbd22017-12-05 07:54:281156 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011157 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201158
Jeff Yoon8154e582019-12-03 23:30:011159 compound_suites = self.test_suites.get('compound_suites', {})
1160 # check_composition_type_test_suites() checks that all basic suites
1161 # referenced by compound suites exist.
1162 basic_suites = self.test_suites.get('basic_suites')
1163
Jamie Madillcf4f8c72021-05-20 19:24:231164 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011165 # Resolve this to a dictionary.
1166 full_suite = {}
1167 for entry in value:
1168 suite = basic_suites[entry]
1169 full_suite.update(suite)
1170 compound_suites[name] = full_suite
1171
Jeff Yoon85fb8df2020-08-20 16:47:431172 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381173 """ Merge variant-defined configurations to each test case definition in a
1174 test suite.
1175
1176 The output maps a unique test name to an array of configurations because
1177 there may exist more than one definition for a test name using variants. The
1178 test name is referenced while mapping machines to test suites, so unpacking
1179 the array is done by the generators.
1180
1181 Args:
1182 basic_test_definition: a {} defined test suite in the format
1183 test_name:test_config
1184 variants: an [] of {} defining configurations to be applied to each test
1185 case in the basic test_definition
1186
1187 Return:
1188 a {} of test_name:[{}], where each {} is a merged configuration
1189 """
1190
1191 # Each test in a basic test suite will have a definition per variant.
1192 test_suite = {}
Garrett Beaty8d6708c2023-07-20 17:20:411193 for variant in variants:
1194 # Unpack the variant from variants.pyl if it's string based.
1195 if isinstance(variant, str):
1196 variant = self.variants[variant]
Jeff Yoonda581c32020-03-06 03:56:051197
Garrett Beaty8d6708c2023-07-20 17:20:411198 # If 'enabled' is set to False, we will not use this variant; otherwise if
1199 # the variant doesn't include 'enabled' variable or 'enabled' is set to
1200 # True, we will use this variant
1201 if not variant.get('enabled', True):
1202 continue
Jeff Yoon67c3e832020-02-08 07:39:381203
Garrett Beaty8d6708c2023-07-20 17:20:411204 # Make a shallow copy of the variant to remove variant-specific fields,
1205 # leaving just mixin fields
1206 variant = copy.copy(variant)
1207 variant.pop('enabled', None)
1208 identifier = variant.pop('identifier')
1209 variant_mixins = variant.pop('mixins', [])
1210 variant_skylab = variant.pop('skylab', {})
Jeff Yoon67c3e832020-02-08 07:39:381211
Garrett Beaty8d6708c2023-07-20 17:20:411212 for test_name, test_config in basic_test_definition.items():
1213 new_test = self.apply_mixin(variant, test_config)
Jeff Yoon67c3e832020-02-08 07:39:381214
Garrett Beaty8d6708c2023-07-20 17:20:411215 new_test['mixins'] = (test_config.get('mixins', []) + variant_mixins +
1216 mixins)
Xinan Lin05fb9c1752020-12-17 00:15:521217
Jeff Yoon67c3e832020-02-08 07:39:381218 # The identifier is used to make the name of the test unique.
1219 # Generators in the recipe uniquely identify a test by it's name, so we
1220 # don't want to have the same name for each variant.
Garrett Beaty8d6708c2023-07-20 17:20:411221 new_test['name'] = self.add_variant_to_test_name(
1222 new_test.get('name', test_name), identifier)
Ben Pastene5f231cf22022-05-05 18:03:071223
1224 # Attach the variant identifier to the test config so downstream
1225 # generators can make modifications based on the original name. This
1226 # is mainly used in generate_gpu_telemetry_test().
Garrett Beaty8d6708c2023-07-20 17:20:411227 new_test['variant_id'] = identifier
Ben Pastene5f231cf22022-05-05 18:03:071228
Garrett Beaty8d6708c2023-07-20 17:20:411229 # cros_chrome_version is the ash chrome version in the cros img in the
1230 # variant of cros_board. We don't want to include it in the final json
1231 # files; so remove it.
1232 for k, v in variant_skylab.items():
1233 if k != 'cros_chrome_version':
1234 new_test[k] = v
1235
1236 test_suite.setdefault(test_name, []).append(new_test)
1237
Jeff Yoon67c3e832020-02-08 07:39:381238 return test_suite
1239
Jeff Yoon8154e582019-12-03 23:30:011240 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381241 self.check_composition_type_test_suites('matrix_compound_suites',
1242 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011243
1244 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381245 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011246 # referenced by matrix suites exist.
1247 basic_suites = self.test_suites.get('basic_suites')
1248
Jamie Madillcf4f8c72021-05-20 19:24:231249 for test_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011250 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381251
Jamie Madillcf4f8c72021-05-20 19:24:231252 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381253 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1254
1255 if 'variants' in mtx_test_suite_config:
Jeff Yoon85fb8df2020-08-20 16:47:431256 mixins = mtx_test_suite_config.get('mixins', [])
Jeff Yoon67c3e832020-02-08 07:39:381257 result = self.resolve_variants(basic_test_def,
Jeff Yoon85fb8df2020-08-20 16:47:431258 mtx_test_suite_config['variants'],
1259 mixins)
Jeff Yoon67c3e832020-02-08 07:39:381260 full_suite.update(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271261 else:
1262 suite = basic_suites[test_suite]
1263 full_suite.update(suite)
Jeff Yoon67c3e832020-02-08 07:39:381264 matrix_compound_suites[test_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281265
1266 def link_waterfalls_to_test_suites(self):
1267 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231268 for tester_name, tester in waterfall['machines'].items():
1269 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281270 if not value in self.test_suites:
1271 # Hard / impossible to cover this in the unit test.
1272 raise self.unknown_test_suite(
1273 value, tester_name, waterfall['name']) # pragma: no cover
1274 tester['test_suites'][suite] = self.test_suites[value]
1275
1276 def load_configuration_files(self):
Garrett Beaty79339e182023-04-10 20:45:471277 self.waterfalls = self.load_pyl_file(self.args.waterfalls_pyl_path)
1278 self.test_suites = self.load_pyl_file(self.args.test_suites_pyl_path)
1279 self.exceptions = self.load_pyl_file(
1280 self.args.test_suite_exceptions_pyl_path)
1281 self.mixins = self.load_pyl_file(self.args.mixins_pyl_path)
1282 self.gn_isolate_map = self.load_pyl_file(self.args.gn_isolate_map_pyl_path)
Chong Guee622242020-10-28 18:17:351283 for isolate_map in self.args.isolate_map_files:
1284 isolate_map = self.load_pyl_file(isolate_map)
1285 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1286 if duplicates:
1287 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1288 ', '.join(duplicates))
1289 self.gn_isolate_map.update(isolate_map)
1290
Garrett Beaty79339e182023-04-10 20:45:471291 self.variants = self.load_pyl_file(self.args.variants_pyl_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281292
1293 def resolve_configuration_files(self):
Garrett Beaty65d44222023-08-01 17:22:111294 self.resolve_dimension_sets()
Chan Lia3ad1502020-04-28 05:32:111295 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281296 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011297 self.resolve_matrix_compound_test_suites()
1298 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281299 self.link_waterfalls_to_test_suites()
1300
Garrett Beaty65d44222023-08-01 17:22:111301 def resolve_dimension_sets(self):
Garrett Beaty65d44222023-08-01 17:22:111302
1303 def definitions():
1304 for suite_name, suite in self.test_suites.get('basic_suites', {}).items():
1305 for test_name, test in suite.items():
1306 yield test, f'test {test_name} in basic suite {suite_name}'
1307
1308 for mixin_name, mixin in self.mixins.items():
1309 yield mixin, f'mixin {mixin_name}'
1310
1311 for waterfall in self.waterfalls:
1312 for builder_name, builder in waterfall.get('machines', {}).items():
1313 yield (
1314 builder,
1315 f'builder {builder_name} in waterfall {waterfall["name"]}',
1316 )
1317
1318 for test_name, exceptions in self.exceptions.items():
1319 modifications = exceptions.get('modifications', {})
1320 for builder_name, mods in modifications.items():
1321 yield (
1322 mods,
1323 f'exception for test {test_name} on builder {builder_name}',
1324 )
1325
1326 for definition, location in definitions():
1327 for swarming_attr in (
1328 'swarming',
1329 'android_swarming',
1330 'chromeos_swarming',
1331 ):
1332 if (swarming :=
1333 definition.get(swarming_attr)) and 'dimension_sets' in swarming:
Garrett Beatyade673d2023-08-04 22:00:251334 raise BBGenErr(
1335 f'dimension_sets is no longer supported (set in {location}),'
1336 ' instead, use set dimensions to a single dict')
Garrett Beaty65d44222023-08-01 17:22:111337
Nico Weberd18b8962018-05-16 19:39:381338 def unknown_bot(self, bot_name, waterfall_name):
1339 return BBGenErr(
1340 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1341
Kenneth Russelleb60cbd22017-12-05 07:54:281342 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1343 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381344 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281345 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1346
1347 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1348 return BBGenErr(
1349 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1350 ' on waterfall ' + waterfall_name)
1351
Stephen Martinisb72f6d22018-10-04 23:29:011352 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071353 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321354
1355 Checks in the waterfall, builder, and test objects for mixins.
1356 """
1357 def valid_mixin(mixin_name):
1358 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011359 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321360 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381361
Stephen Martinisb6a50492018-09-12 23:59:321362 def must_be_list(mixins, typ, name):
1363 """Asserts that given mixins are a list."""
1364 if not isinstance(mixins, list):
1365 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1366
Brian Sheedy7658c982020-01-08 02:27:581367 test_name = test.get('name')
1368 remove_mixins = set()
1369 if 'remove_mixins' in builder:
1370 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1371 for rm in builder['remove_mixins']:
1372 valid_mixin(rm)
1373 remove_mixins.add(rm)
1374 if 'remove_mixins' in test:
1375 must_be_list(test['remove_mixins'], 'test', test_name)
1376 for rm in test['remove_mixins']:
1377 valid_mixin(rm)
1378 remove_mixins.add(rm)
1379 del test['remove_mixins']
1380
Stephen Martinisb72f6d22018-10-04 23:29:011381 if 'mixins' in waterfall:
1382 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1383 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581384 if mixin in remove_mixins:
1385 continue
Stephen Martinisb6a50492018-09-12 23:59:321386 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531387 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321388
Stephen Martinisb72f6d22018-10-04 23:29:011389 if 'mixins' in builder:
1390 must_be_list(builder['mixins'], 'builder', builder_name)
1391 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581392 if mixin in remove_mixins:
1393 continue
Stephen Martinisb6a50492018-09-12 23:59:321394 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531395 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321396
Stephen Martinisb72f6d22018-10-04 23:29:011397 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071398 return test
1399
Stephen Martinis2a0667022018-09-25 22:31:141400 if not test_name:
1401 test_name = test.get('test')
1402 if not test_name: # pragma: no cover
1403 # Not the best name, but we should say something.
1404 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:011405 must_be_list(test['mixins'], 'test', test_name)
1406 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581407 # We don't bother checking if the given mixin is in remove_mixins here
1408 # since this is already the lowest level, so if a mixin is added here that
1409 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071410 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531411 test = self.apply_mixin(self.mixins[mixin], test, builder)
Jeff Yoon67c3e832020-02-08 07:39:381412 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071413 return test
Stephen Martinisb6a50492018-09-12 23:59:321414
Garrett Beaty8d6708c2023-07-20 17:20:411415 def apply_mixin(self, mixin, test, builder=None):
Stephen Martinisb72f6d22018-10-04 23:29:011416 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321417
Garrett Beaty4c35b142023-06-23 21:01:231418 A mixin is applied by copying all fields from the mixin into the
1419 test with the following exceptions:
1420 * For the various *args keys, the test's existing value (an empty
1421 list if not present) will be extended with the mixin's value.
1422 * The sub-keys of the swarming value will be copied to the test's
1423 swarming value with the following exceptions:
Garrett Beatyade673d2023-08-04 22:00:251424 * For the named_caches sub-keys, the test's existing value (an
1425 empty list if not present) will be extended with the mixin's
1426 value.
1427 * For the dimensions sub-key, the tests's existing value (an empty
1428 dict if not present) will be updated with the mixin's value.
Stephen Martinisb6a50492018-09-12 23:59:321429 """
Garrett Beaty4c35b142023-06-23 21:01:231430
Stephen Martinisb6a50492018-09-12 23:59:321431 new_test = copy.deepcopy(test)
1432 mixin = copy.deepcopy(mixin)
Garrett Beaty8d6708c2023-07-20 17:20:411433
1434 if 'description' in mixin:
1435 description = []
1436 if 'description' in new_test:
1437 description.append(new_test['description'])
1438 description.append(mixin.pop('description'))
1439 new_test['description'] = '\n'.join(description)
1440
Stephen Martinisb72f6d22018-10-04 23:29:011441 if 'swarming' in mixin:
1442 swarming_mixin = mixin['swarming']
1443 new_test.setdefault('swarming', {})
Stephen Martinisb72f6d22018-10-04 23:29:011444 if 'dimensions' in swarming_mixin:
Garrett Beatyade673d2023-08-04 22:00:251445 new_test['swarming'].setdefault('dimensions', {}).update(
1446 swarming_mixin.pop('dimensions'))
Garrett Beaty4c35b142023-06-23 21:01:231447 if 'named_caches' in swarming_mixin:
1448 new_test['swarming'].setdefault('named_caches', []).extend(
1449 swarming_mixin['named_caches'])
1450 del swarming_mixin['named_caches']
Stephen Martinisb72f6d22018-10-04 23:29:011451 # python dict update doesn't do recursion at all. Just hard code the
1452 # nested update we need (mixin['swarming'] shouldn't clobber
1453 # test['swarming'], but should update it).
1454 new_test['swarming'].update(swarming_mixin)
1455 del mixin['swarming']
1456
Garrett Beaty4c35b142023-06-23 21:01:231457 # Array so we can assign to it in a nested scope.
1458 args_need_fixup = ['args' in mixin]
1459
1460 for a in (
1461 'args',
1462 'precommit_args',
1463 'non_precommit_args',
1464 'desktop_args',
1465 'lacros_args',
1466 'linux_args',
1467 'android_args',
1468 'chromeos_args',
1469 'mac_args',
1470 'win_args',
1471 'win64_args',
1472 ):
1473 if (value := mixin.pop(a, None)) is None:
1474 continue
1475 if not isinstance(value, list):
1476 raise BBGenErr(f'"{a}" must be a list')
1477 new_test.setdefault(a, []).extend(value)
1478
Garrett Beaty4c35b142023-06-23 21:01:231479 args = new_test.get('args', [])
Austin Eng148d9f0f2022-02-08 19:18:531480
Garrett Beaty4c35b142023-06-23 21:01:231481 def add_conditional_args(key, fn):
Garrett Beaty8d6708c2023-07-20 17:20:411482 if builder is None:
1483 return
Garrett Beaty4c35b142023-06-23 21:01:231484 val = new_test.pop(key, [])
1485 if val and fn(builder):
1486 args.extend(val)
1487 args_need_fixup[0] = True
Austin Eng148d9f0f2022-02-08 19:18:531488
Garrett Beaty4c35b142023-06-23 21:01:231489 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
1490 add_conditional_args('lacros_args', self.is_lacros)
1491 add_conditional_args('linux_args', self.is_linux)
1492 add_conditional_args('android_args', self.is_android)
1493 add_conditional_args('chromeos_args', self.is_chromeos)
1494 add_conditional_args('mac_args', self.is_mac)
1495 add_conditional_args('win_args', self.is_win)
1496 add_conditional_args('win64_args', self.is_win64)
1497
1498 if args_need_fixup[0]:
1499 new_test['args'] = self.maybe_fixup_args_array(args)
Wezc0e835b702018-10-30 00:38:411500
Stephen Martinisb72f6d22018-10-04 23:29:011501 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321502 return new_test
1503
Greg Gutermanf60eb052020-03-12 17:40:011504 def generate_output_tests(self, waterfall):
1505 """Generates the tests for a waterfall.
1506
1507 Args:
1508 waterfall: a dictionary parsed from a master pyl file
1509 Returns:
1510 A dictionary mapping builders to test specs
1511 """
1512 return {
Jamie Madillcf4f8c72021-05-20 19:24:231513 name: self.get_tests_for_config(waterfall, name, config)
1514 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011515 }
1516
1517 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531518 generator_map = self.get_test_generator_map()
1519 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281520
Greg Gutermanf60eb052020-03-12 17:40:011521 tests = {}
1522 # Copy only well-understood entries in the machine's configuration
1523 # verbatim into the generated JSON.
1524 if 'additional_compile_targets' in config:
1525 tests['additional_compile_targets'] = config[
1526 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231527 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011528 if test_type not in generator_map:
1529 raise self.unknown_test_suite_type(
1530 test_type, name, waterfall['name']) # pragma: no cover
1531 test_generator = generator_map[test_type]
1532 # Let multiple kinds of generators generate the same kinds
1533 # of tests. For example, gpu_telemetry_tests are a
1534 # specialization of isolated_scripts.
1535 new_tests = test_generator.generate(
1536 waterfall, name, config, input_tests)
1537 remapped_test_type = test_type_remapper.get(test_type, test_type)
1538 tests[remapped_test_type] = test_generator.sort(
1539 tests.get(remapped_test_type, []) + new_tests)
1540
1541 return tests
1542
1543 def jsonify(self, all_tests):
1544 return json.dumps(
1545 all_tests, indent=2, separators=(',', ': '),
1546 sort_keys=True) + '\n'
1547
1548 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281549 self.load_configuration_files()
1550 self.resolve_configuration_files()
1551 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011552 result = collections.defaultdict(dict)
1553
Stephanie Kim572b43c02023-04-13 14:24:131554 if os.path.exists(self.args.autoshard_exceptions_json_path):
1555 autoshards = json.loads(
1556 self.read_file(self.args.autoshard_exceptions_json_path))
1557 else:
1558 autoshards = {}
1559
Dirk Pranke6269d302020-10-01 00:14:391560 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011561 for waterfall in self.waterfalls:
1562 for field in required_fields:
1563 # Verify required fields
1564 if field not in waterfall:
1565 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1566
1567 # Handle filter flag, if specified
1568 if filters and waterfall['name'] not in filters:
1569 continue
1570
1571 # Join config files and hardcoded values together
1572 all_tests = self.generate_output_tests(waterfall)
1573 result[waterfall['name']] = all_tests
1574
Stephanie Kim572b43c02023-04-13 14:24:131575 if not autoshards:
1576 continue
1577 for builder, test_spec in all_tests.items():
1578 for target_type, test_list in test_spec.items():
1579 if target_type == 'additional_compile_targets':
1580 continue
1581 for test_dict in test_list:
1582 # Suites that apply variants or other customizations will create
1583 # test_dicts that have "name" value that is different from the
1584 # "test" value. Regular suites without any variations will only have
1585 # "test" and no "name".
1586 # e.g. name = vulkan_swiftshader_content_browsertests, but
1587 # test = content_browsertests and
1588 # test_id_prefix = "ninja://content/test:content_browsertests/"
1589 # Check for "name" first and then fallback to "test"
1590 test_name = test_dict.get('name') or test_dict.get('test')
1591 if not test_name:
1592 continue
1593 shard_info = autoshards.get(waterfall['name'],
1594 {}).get(builder, {}).get(test_name)
1595 if shard_info:
1596 test_dict['swarming'].update(
1597 {'shards': int(shard_info['shards'])})
1598
Greg Gutermanf60eb052020-03-12 17:40:011599 # Add do not edit warning
1600 for tests in result.values():
1601 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1602 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1603
1604 return result
1605
1606 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281607 suffix = '.json'
1608 if self.args.new_files:
1609 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011610
1611 for filename, contents in result.items():
1612 jsonstr = self.jsonify(contents)
Garrett Beaty79339e182023-04-10 20:45:471613 file_path = os.path.join(self.args.output_dir, filename + suffix)
1614 self.write_file(file_path, jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281615
Nico Weberd18b8962018-05-16 19:39:381616 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161617 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421618 # NOTE: This reference can cause issues; if a file changes there, the
1619 # presubmit here won't be run by default. A manually maintained list there
1620 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1621 # references to configs outside of this directory are added, please change
1622 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1623 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491624
Garrett Beaty7e866fc2021-06-16 14:12:101625 # Get the generated project.pyl so we can check if we should be enforcing
1626 # that the specs are for builders that actually exist
1627 # If not, return None to indicate that we won't enforce that builders in
1628 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491629 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1630 'project.pyl')
1631 if os.path.exists(project_pyl_path):
1632 settings = ast.literal_eval(self.read_file(project_pyl_path))
1633 if not settings.get('validate_source_side_specs_have_builder', True):
1634 return None
1635
Nico Weberd18b8962018-05-16 19:39:381636 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311637 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161638 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1639 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431640 for c in milo_configs:
1641 for l in self.read_file(c).splitlines():
1642 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311643 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431644 continue
1645 # l looks like
1646 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1647 # Extract win_chromium_dbg_ng part.
1648 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381649 return bot_names
1650
Ben Pastene9a010082019-09-25 20:41:371651 def get_internal_waterfalls(self):
1652 # Similar to get_builders_that_do_not_actually_exist above, but for
1653 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201654 return [
Kramer Ge3bf853a2023-04-13 19:39:471655 'chrome', 'chrome.pgo', 'chrome.gpu.fyi', 'internal.chrome.fyi',
1656 'internal.chromeos.fyi', 'internal.soda'
Yuke Liaoe6c23dd2021-07-28 16:12:201657 ]
Ben Pastene9a010082019-09-25 20:41:371658
Stephen Martinisf83893722018-09-19 00:02:181659 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201660 self.check_input_files_sorting(verbose)
1661
Kenneth Russelleb60cbd22017-12-05 07:54:281662 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011663 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381664 self.check_composition_type_test_suites('matrix_compound_suites',
1665 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111666 self.resolve_test_id_prefixes()
Stephen Martinis54d64ad2018-09-21 22:16:201667 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381668
1669 # All bots should exist.
1670 bot_names = self.get_valid_bot_names()
Garrett Beaty2a02de3c2020-05-15 13:57:351671 if bot_names is not None:
1672 internal_waterfalls = self.get_internal_waterfalls()
1673 for waterfall in self.waterfalls:
1674 # TODO(crbug.com/991417): Remove the need for this exception.
1675 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011676 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351677 for bot_name in waterfall['machines']:
Garrett Beaty2a02de3c2020-05-15 13:57:351678 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581679 if waterfall['name'] in [
1680 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1681 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351682 # TODO(thakis): Remove this once these bots move to luci.
1683 continue # pragma: no cover
1684 if waterfall['name'] in ['tryserver.webrtc',
1685 'webrtc.chromium.fyi.experimental']:
1686 # These waterfalls have their bot configs in a different repo.
1687 # so we don't know about their bot names.
1688 continue # pragma: no cover
1689 if waterfall['name'] in ['client.devtools-frontend.integration',
1690 'tryserver.devtools-frontend',
1691 'chromium.devtools-frontend']:
1692 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201693 if waterfall['name'] in ['client.openscreen.chromium']:
1694 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351695 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381696
Kenneth Russelleb60cbd22017-12-05 07:54:281697 # All test suites must be referenced.
1698 suites_seen = set()
1699 generator_map = self.get_test_generator_map()
1700 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231701 for bot_name, tester in waterfall['machines'].items():
1702 for suite_type, suite in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281703 if suite_type not in generator_map:
1704 raise self.unknown_test_suite_type(suite_type, bot_name,
1705 waterfall['name'])
1706 if suite not in self.test_suites:
1707 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1708 suites_seen.add(suite)
1709 # Since we didn't resolve the configuration files, this set
1710 # includes both composition test suites and regular ones.
1711 resolved_suites = set()
1712 for suite_name in suites_seen:
1713 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011714 for sub_suite in suite:
1715 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281716 resolved_suites.add(suite_name)
1717 # At this point, every key in test_suites.pyl should be referenced.
1718 missing_suites = set(self.test_suites.keys()) - resolved_suites
1719 if missing_suites:
1720 raise BBGenErr('The following test suites were unreferenced by bots on '
1721 'the waterfalls: ' + str(missing_suites))
1722
1723 # All test suite exceptions must refer to bots on the waterfall.
1724 all_bots = set()
1725 missing_bots = set()
1726 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231727 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281728 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281729 # In order to disambiguate between bots with the same name on
1730 # different waterfalls, support has been added to various
1731 # exceptions for concatenating the waterfall name after the bot
1732 # name.
1733 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231734 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381735 removals = (exception.get('remove_from', []) +
1736 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231737 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381738 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281739 if removal not in all_bots:
1740 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411741
Kenneth Russelleb60cbd22017-12-05 07:54:281742 if missing_bots:
1743 raise BBGenErr('The following nonexistent machines were referenced in '
1744 'the test suite exceptions: ' + str(missing_bots))
1745
Garrett Beatyb061e69d2023-06-27 16:15:351746 for name, mixin in self.mixins.items():
1747 if '$mixin_append' in mixin:
1748 raise BBGenErr(
1749 f'$mixin_append is no longer supported (set in mixin "{name}"),'
1750 ' args and named caches specified as normal will be appended')
1751
Stephen Martinis0382bc12018-09-17 22:29:071752 # All mixins must be referenced
1753 seen_mixins = set()
1754 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011755 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231756 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011757 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071758 for suite in self.test_suites.values():
1759 if isinstance(suite, list):
1760 # Don't care about this, it's a composition, which shouldn't include a
1761 # swarming mixin.
1762 continue
1763
1764 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561765 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011766 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071767
Zhaoyang Li9da047d52021-05-10 21:31:441768 for variant in self.variants:
1769 # Unpack the variant from variants.pyl if it's string based.
1770 if isinstance(variant, str):
1771 variant = self.variants[variant]
1772 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1773
Stephen Martinisb72f6d22018-10-04 23:29:011774 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071775 if missing_mixins:
1776 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1777 ' referenced in a waterfall, machine, or test suite.' % (
1778 str(missing_mixins)))
1779
Jeff Yoonda581c32020-03-06 03:56:051780 # All variant references must be referenced
1781 seen_variants = set()
1782 for suite in self.test_suites.values():
1783 if isinstance(suite, list):
1784 continue
1785
1786 for test in suite.values():
1787 if isinstance(test, dict):
1788 for variant in test.get('variants', []):
1789 if isinstance(variant, str):
1790 seen_variants.add(variant)
1791
1792 missing_variants = set(self.variants.keys()) - seen_variants
1793 if missing_variants:
1794 raise BBGenErr('The following variants were unreferenced: %s. They must '
1795 'be referenced in a matrix test suite under the variants '
1796 'key.' % str(missing_variants))
1797
Stephen Martinis54d64ad2018-09-21 22:16:201798
Garrett Beaty79339e182023-04-10 20:45:471799 def type_assert(self, node, typ, file_path, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201800 """Asserts that the Python AST node |node| is of type |typ|.
1801
1802 If verbose is set, it prints out some helpful context lines, showing where
1803 exactly the error occurred in the file.
1804 """
1805 if not isinstance(node, typ):
1806 if verbose:
Garrett Beaty79339e182023-04-10 20:45:471807 lines = [""] + self.read_file(file_path).splitlines()
Stephen Martinis54d64ad2018-09-21 22:16:201808
1809 context = 2
1810 lines_start = max(node.lineno - context, 0)
1811 # Add one to include the last line
1812 lines_end = min(node.lineno + context, len(lines)) + 1
Garrett Beaty79339e182023-04-10 20:45:471813 lines = itertools.chain(
1814 ['== %s ==\n' % file_path],
1815 ["<snip>\n"],
1816 [
1817 '%d %s' % (lines_start + i, line)
1818 for i, line in enumerate(lines[lines_start:lines_start +
1819 context])
1820 ],
1821 ['-' * 80 + '\n'],
1822 ['%d %s' % (node.lineno, lines[node.lineno])],
1823 [
1824 '-' * (node.col_offset + 3) + '^' + '-' *
1825 (80 - node.col_offset - 4) + '\n'
1826 ],
1827 [
1828 '%d %s' % (node.lineno + 1 + i, line)
1829 for i, line in enumerate(lines[node.lineno + 1:lines_end])
1830 ],
1831 ["<snip>\n"],
Stephen Martinis54d64ad2018-09-21 22:16:201832 )
1833 # Print out a useful message when a type assertion fails.
1834 for l in lines:
1835 self.print_line(l.strip())
1836
1837 node_dumped = ast.dump(node, annotate_fields=False)
1838 # If the node is huge, truncate it so everything fits in a terminal
1839 # window.
1840 if len(node_dumped) > 60: # pragma: no cover
1841 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1842 raise BBGenErr(
Garrett Beaty807011ab2023-04-12 00:52:391843 'Invalid .pyl file \'%s\'. Python AST node %r on line %s expected to'
Garrett Beaty79339e182023-04-10 20:45:471844 ' be %s, is %s' %
1845 (file_path, node_dumped, node.lineno, typ, type(node)))
Stephen Martinis54d64ad2018-09-21 22:16:201846
Garrett Beaty79339e182023-04-10 20:45:471847 def check_ast_list_formatted(self,
1848 keys,
1849 file_path,
1850 verbose,
Stephen Martinis1384ff92020-01-07 19:52:151851 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531852 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201853
Stephen Martinis5bef0fc2020-01-06 22:47:531854 Currently only checks to ensure they're correctly sorted, and that there
1855 are no duplicates.
1856
1857 Args:
1858 keys: An python list of AST nodes.
1859
1860 It's a list of AST nodes instead of a list of strings because
1861 when verbose is set, it tries to print out context of where the
1862 diffs are in the file.
Garrett Beaty79339e182023-04-10 20:45:471863 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531864 verbose: If set, print out diff information about how the keys are
1865 incorrectly formatted.
1866 check_sorting: If true, checks if the list is sorted.
1867 Returns:
1868 If the keys are correctly formatted.
1869 """
1870 if not keys:
1871 return True
1872
1873 assert isinstance(keys[0], ast.Str)
1874
1875 keys_strs = [k.s for k in keys]
1876 # Keys to diff against. Used below.
1877 keys_to_diff_against = None
1878 # If the list is properly formatted.
1879 list_formatted = True
1880
1881 # Duplicates are always bad.
1882 if len(set(keys_strs)) != len(keys_strs):
1883 list_formatted = False
1884 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1885
1886 if check_sorting and sorted(keys_strs) != keys_strs:
1887 list_formatted = False
1888 if list_formatted:
1889 return True
1890
1891 if verbose:
1892 line_num = keys[0].lineno
1893 keys = [k.s for k in keys]
1894 if check_sorting:
1895 # If we have duplicates, sorting this will take care of it anyways.
1896 keys_to_diff_against = sorted(set(keys))
1897 # else, keys_to_diff_against is set above already
1898
1899 self.print_line('=' * 80)
1900 self.print_line('(First line of keys is %s)' % line_num)
Garrett Beaty79339e182023-04-10 20:45:471901 for line in difflib.context_diff(keys,
1902 keys_to_diff_against,
1903 fromfile='current (%r)' % file_path,
1904 tofile='sorted',
1905 lineterm=''):
Stephen Martinis5bef0fc2020-01-06 22:47:531906 self.print_line(line)
1907 self.print_line('=' * 80)
1908
1909 return False
1910
Garrett Beaty79339e182023-04-10 20:45:471911 def check_ast_dict_formatted(self, node, file_path, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531912 """Checks if an ast dictionary's keys are correctly formatted.
1913
1914 Just a simple wrapper around check_ast_list_formatted.
1915 Args:
1916 node: An AST node. Assumed to be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471917 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531918 verbose: If set, print out diff information about how the keys are
1919 incorrectly formatted.
1920 check_sorting: If true, checks if the list is sorted.
1921 Returns:
1922 If the dictionary is correctly formatted.
1923 """
Stephen Martinis54d64ad2018-09-21 22:16:201924 keys = []
1925 # The keys of this dict are ordered as ordered in the file; normal python
1926 # dictionary keys are given an arbitrary order, but since we parsed the
1927 # file itself, the order as given in the file is preserved.
1928 for key in node.keys:
Garrett Beaty79339e182023-04-10 20:45:471929 self.type_assert(key, ast.Str, file_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531930 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201931
Garrett Beaty79339e182023-04-10 20:45:471932 return self.check_ast_list_formatted(keys, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181933
1934 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201935 # TODO(https://crbug.com/886993): Add the ability for this script to
1936 # actually format the files, rather than just complain if they're
1937 # incorrectly formatted.
1938 bad_files = set()
Garrett Beaty79339e182023-04-10 20:45:471939
1940 def parse_file(file_path):
Stephen Martinis5bef0fc2020-01-06 22:47:531941 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201942
Stephen Martinis5bef0fc2020-01-06 22:47:531943 Returns an AST node representing the value in the pyl file."""
Garrett Beaty79339e182023-04-10 20:45:471944 parsed = ast.parse(self.read_file(file_path))
Stephen Martinisf83893722018-09-19 00:02:181945
Stephen Martinisf83893722018-09-19 00:02:181946 # Must be a module.
Garrett Beaty79339e182023-04-10 20:45:471947 self.type_assert(parsed, ast.Module, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181948 module = parsed.body
1949
1950 # Only one expression in the module.
Garrett Beaty79339e182023-04-10 20:45:471951 self.type_assert(module, list, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181952 if len(module) != 1: # pragma: no cover
Garrett Beaty79339e182023-04-10 20:45:471953 raise BBGenErr('Invalid .pyl file %s' % file_path)
Stephen Martinisf83893722018-09-19 00:02:181954 expr = module[0]
Garrett Beaty79339e182023-04-10 20:45:471955 self.type_assert(expr, ast.Expr, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181956
Stephen Martinis5bef0fc2020-01-06 22:47:531957 return expr.value
1958
1959 # Handle this separately
Garrett Beaty79339e182023-04-10 20:45:471960 value = parse_file(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531961 # Value should be a list.
Garrett Beaty79339e182023-04-10 20:45:471962 self.type_assert(value, ast.List, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531963
1964 keys = []
Joshua Hood56c673c2022-03-02 20:29:331965 for elm in value.elts:
Garrett Beaty79339e182023-04-10 20:45:471966 self.type_assert(elm, ast.Dict, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531967 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331968 for key, val in zip(elm.keys, elm.values):
Garrett Beaty79339e182023-04-10 20:45:471969 self.type_assert(key, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531970 if key.s == 'machines':
Garrett Beaty79339e182023-04-10 20:45:471971 if not self.check_ast_dict_formatted(
1972 val, self.args.waterfalls_pyl_path, verbose):
1973 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531974
1975 if key.s == "name":
Garrett Beaty79339e182023-04-10 20:45:471976 self.type_assert(val, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531977 waterfall_name = val
1978 assert waterfall_name
1979 keys.append(waterfall_name)
1980
Garrett Beaty79339e182023-04-10 20:45:471981 if not self.check_ast_list_formatted(keys, self.args.waterfalls_pyl_path,
1982 verbose):
1983 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531984
Garrett Beaty79339e182023-04-10 20:45:471985 for file_path in (
1986 self.args.mixins_pyl_path,
1987 self.args.test_suites_pyl_path,
1988 self.args.test_suite_exceptions_pyl_path,
Stephen Martinis5bef0fc2020-01-06 22:47:531989 ):
Garrett Beaty79339e182023-04-10 20:45:471990 value = parse_file(file_path)
Stephen Martinisf83893722018-09-19 00:02:181991 # Value should be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471992 self.type_assert(value, ast.Dict, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181993
Garrett Beaty79339e182023-04-10 20:45:471994 if not self.check_ast_dict_formatted(value, file_path, verbose):
1995 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531996
Garrett Beaty79339e182023-04-10 20:45:471997 if file_path == self.args.test_suites_pyl_path:
Jeff Yoon8154e582019-12-03 23:30:011998 expected_keys = ['basic_suites',
1999 'compound_suites',
2000 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:202001 actual_keys = [node.s for node in value.keys]
2002 assert all(key in expected_keys for key in actual_keys), (
Garrett Beaty79339e182023-04-10 20:45:472003 'Invalid %r file; expected keys %r, got %r' %
2004 (file_path, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:332005 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:202006 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:012007 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:202008 for suite_group in suite_dicts:
Garrett Beaty79339e182023-04-10 20:45:472009 if not self.check_ast_dict_formatted(suite_group, file_path, verbose):
2010 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:182011
Stephen Martinis5bef0fc2020-01-06 22:47:532012 for key, suite in zip(value.keys, value.values):
2013 # The compound suites are checked in
2014 # 'check_composition_type_test_suites()'
2015 if key.s == 'basic_suites':
2016 for group in suite.values:
Garrett Beaty79339e182023-04-10 20:45:472017 if not self.check_ast_dict_formatted(group, file_path, verbose):
2018 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:532019 break
Stephen Martinis54d64ad2018-09-21 22:16:202020
Garrett Beaty79339e182023-04-10 20:45:472021 elif file_path == self.args.test_suite_exceptions_pyl_path:
Stephen Martinis5bef0fc2020-01-06 22:47:532022 # Check the values for each test.
2023 for test in value.values:
2024 for kind, node in zip(test.keys, test.values):
2025 if isinstance(node, ast.Dict):
Garrett Beaty79339e182023-04-10 20:45:472026 if not self.check_ast_dict_formatted(node, file_path, verbose):
2027 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:532028 elif kind.s == 'remove_from':
2029 # Don't care about sorting; these are usually grouped, since the
2030 # same bug can affect multiple builders. Do want to make sure
2031 # there aren't duplicates.
Garrett Beaty79339e182023-04-10 20:45:472032 if not self.check_ast_list_formatted(
2033 node.elts, file_path, verbose, check_sorting=False):
2034 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:182035
2036 if bad_files:
2037 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:202038 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:532039 'unsorted, or have duplicates. Re-run this with --verbose to see '
2040 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:182041
Kenneth Russelleb60cbd22017-12-05 07:54:282042 def check_output_file_consistency(self, verbose=False):
2043 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012044 # All waterfalls/bucket .json files must have been written
2045 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:282046 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012047 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:162048 outputs = self.generate_outputs()
2049 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:012050 expected = self.jsonify(expected_contents)
Garrett Beaty79339e182023-04-10 20:45:472051 file_path = os.path.join(self.args.output_dir, filename + '.json')
Ben Pastenef21cda32023-03-30 22:00:572052 current = self.read_file(file_path)
Kenneth Russelleb60cbd22017-12-05 07:54:282053 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:012054 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:322055 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:012056 self.print_line('File ' + filename +
2057 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:322058 'contents:')
2059 for line in difflib.unified_diff(
2060 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:502061 current.splitlines(),
2062 fromfile='expected', tofile='current'):
2063 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:012064
2065 if ungenerated_files:
2066 raise BBGenErr(
2067 'The following files have not been properly '
2068 'autogenerated by generate_buildbot_json.py: ' +
2069 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:282070
Dirk Pranke772f55f2021-04-28 04:51:162071 for builder_group, builders in outputs.items():
2072 for builder, step_types in builders.items():
2073 for step_data in step_types.get('gtest_tests', []):
2074 step_name = step_data.get('name', step_data['test'])
2075 self._check_swarming_config(builder_group, builder, step_name,
2076 step_data)
2077 for step_data in step_types.get('isolated_scripts', []):
2078 step_name = step_data.get('name', step_data['isolate_name'])
2079 self._check_swarming_config(builder_group, builder, step_name,
2080 step_data)
2081
2082 def _check_swarming_config(self, filename, builder, step_name, step_data):
Ben Pastene338f56b2023-03-31 21:24:452083 # TODO(crbug.com/1203436): Ensure all swarming tests specify cpu, not
Dirk Pranke772f55f2021-04-28 04:51:162084 # just mac tests.
Garrett Beatybb18d532023-06-26 22:16:332085 if 'swarming' in step_data:
Garrett Beatyade673d2023-08-04 22:00:252086 dimensions = step_data['swarming'].get('dimensions')
2087 if not dimensions:
Ben Pastene338f56b2023-03-31 21:24:452088 raise BBGenErr('%s: %s / %s : os must be specified for all '
Dirk Pranke772f55f2021-04-28 04:51:162089 'swarmed tests' % (filename, builder, step_name))
Garrett Beatyade673d2023-08-04 22:00:252090 if not dimensions.get('os'):
2091 raise BBGenErr('%s: %s / %s : os must be specified for all '
2092 'swarmed tests' % (filename, builder, step_name))
2093 if 'Mac' in dimensions.get('os') and not dimensions.get('cpu'):
2094 raise BBGenErr('%s: %s / %s : cpu must be specified for mac '
2095 'swarmed tests' % (filename, builder, step_name))
Dirk Pranke772f55f2021-04-28 04:51:162096
Kenneth Russelleb60cbd22017-12-05 07:54:282097 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502098 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282099 self.check_output_file_consistency(verbose) # pragma: no cover
2100
Karen Qiane24b7ee2019-02-12 23:37:062101 def does_test_match(self, test_info, params_dict):
2102 """Checks to see if the test matches the parameters given.
2103
2104 Compares the provided test_info with the params_dict to see
2105 if the bot matches the parameters given. If so, returns True.
2106 Else, returns false.
2107
2108 Args:
2109 test_info (dict): Information about a specific bot provided
2110 in the format shown in waterfalls.pyl
2111 params_dict (dict): Dictionary of parameters and their values
2112 to look for in the bot
2113 Ex: {
2114 'device_os':'android',
2115 '--flag':True,
2116 'mixins': ['mixin1', 'mixin2'],
2117 'ex_key':'ex_value'
2118 }
2119
2120 """
2121 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2122 'kvm', 'pool', 'integrity'] # dimension parameters
2123 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2124 'can_use_on_swarming_builders']
2125 for param in params_dict:
2126 # if dimension parameter
2127 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2128 if not 'swarming' in test_info:
2129 return False
2130 swarming = test_info['swarming']
2131 if param in SWARMING_PARAMS:
2132 if not param in swarming:
2133 return False
2134 if not str(swarming[param]) == params_dict[param]:
2135 return False
2136 else:
Garrett Beatyade673d2023-08-04 22:00:252137 if not 'dimensions' in swarming:
Karen Qiane24b7ee2019-02-12 23:37:062138 return False
Garrett Beatyade673d2023-08-04 22:00:252139 dimensions = swarming['dimensions']
Karen Qiane24b7ee2019-02-12 23:37:062140 # only looking at the first dimension set
Garrett Beatyade673d2023-08-04 22:00:252141 if not param in dimensions:
Karen Qiane24b7ee2019-02-12 23:37:062142 return False
Garrett Beatyade673d2023-08-04 22:00:252143 if not dimensions[param] == params_dict[param]:
Karen Qiane24b7ee2019-02-12 23:37:062144 return False
2145
2146 # if flag
2147 elif param.startswith('--'):
2148 if not 'args' in test_info:
2149 return False
2150 if not param in test_info['args']:
2151 return False
2152
2153 # not dimension parameter/flag/mixin
2154 else:
2155 if not param in test_info:
2156 return False
2157 if not test_info[param] == params_dict[param]:
2158 return False
2159 return True
2160 def error_msg(self, msg):
2161 """Prints an error message.
2162
2163 In addition to a catered error message, also prints
2164 out where the user can find more help. Then, program exits.
2165 """
2166 self.print_line(msg + (' If you need more information, ' +
2167 'please run with -h or --help to see valid commands.'))
2168 sys.exit(1)
2169
2170 def find_bots_that_run_test(self, test, bots):
2171 matching_bots = []
2172 for bot in bots:
2173 bot_info = bots[bot]
2174 tests = self.flatten_tests_for_bot(bot_info)
2175 for test_info in tests:
2176 test_name = ""
2177 if 'name' in test_info:
2178 test_name = test_info['name']
2179 elif 'test' in test_info:
2180 test_name = test_info['test']
2181 if not test_name == test:
2182 continue
2183 matching_bots.append(bot)
2184 return matching_bots
2185
2186 def find_tests_with_params(self, tests, params_dict):
2187 matching_tests = []
2188 for test_name in tests:
2189 test_info = tests[test_name]
2190 if not self.does_test_match(test_info, params_dict):
2191 continue
2192 if not test_name in matching_tests:
2193 matching_tests.append(test_name)
2194 return matching_tests
2195
2196 def flatten_waterfalls_for_query(self, waterfalls):
2197 bots = {}
2198 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012199 waterfall_tests = self.generate_output_tests(waterfall)
2200 for bot in waterfall_tests:
2201 bot_info = waterfall_tests[bot]
2202 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062203 return bots
2204
2205 def flatten_tests_for_bot(self, bot_info):
2206 """Returns a list of flattened tests.
2207
2208 Returns a list of tests not grouped by test category
2209 for a specific bot.
2210 """
2211 TEST_CATS = self.get_test_generator_map().keys()
2212 tests = []
2213 for test_cat in TEST_CATS:
2214 if not test_cat in bot_info:
2215 continue
2216 test_cat_tests = bot_info[test_cat]
2217 tests = tests + test_cat_tests
2218 return tests
2219
2220 def flatten_tests_for_query(self, test_suites):
2221 """Returns a flattened dictionary of tests.
2222
2223 Returns a dictionary of tests associate with their
2224 configuration, not grouped by their test suite.
2225 """
2226 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232227 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062228 for test in test_suite:
2229 test_info = test_suite[test]
2230 test_name = test
2231 if 'name' in test_info:
2232 test_name = test_info['name']
2233 tests[test_name] = test_info
2234 return tests
2235
2236 def parse_query_filter_params(self, params):
2237 """Parses the filter parameters.
2238
2239 Creates a dictionary from the parameters provided
2240 to filter the bot array.
2241 """
2242 params_dict = {}
2243 for p in params:
2244 # flag
2245 if p.startswith("--"):
2246 params_dict[p] = True
2247 else:
2248 pair = p.split(":")
2249 if len(pair) != 2:
2250 self.error_msg('Invalid command.')
2251 # regular parameters
2252 if pair[1].lower() == "true":
2253 params_dict[pair[0]] = True
2254 elif pair[1].lower() == "false":
2255 params_dict[pair[0]] = False
2256 else:
2257 params_dict[pair[0]] = pair[1]
2258 return params_dict
2259
2260 def get_test_suites_dict(self, bots):
2261 """Returns a dictionary of bots and their tests.
2262
2263 Returns a dictionary of bots and a list of their associated tests.
2264 """
2265 test_suite_dict = dict()
2266 for bot in bots:
2267 bot_info = bots[bot]
2268 tests = self.flatten_tests_for_bot(bot_info)
2269 test_suite_dict[bot] = tests
2270 return test_suite_dict
2271
2272 def output_query_result(self, result, json_file=None):
2273 """Outputs the result of the query.
2274
2275 If a json file parameter name is provided, then
2276 the result is output into the json file. If not,
2277 then the result is printed to the console.
2278 """
2279 output = json.dumps(result, indent=2)
2280 if json_file:
2281 self.write_file(json_file, output)
2282 else:
2283 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062284
Joshua Hood56c673c2022-03-02 20:29:332285 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062286 def query(self, args):
2287 """Queries tests or bots.
2288
2289 Depending on the arguments provided, outputs a json of
2290 tests or bots matching the appropriate optional parameters provided.
2291 """
2292 # split up query statement
2293 query = args.query.split('/')
2294 self.load_configuration_files()
2295 self.resolve_configuration_files()
2296
2297 # flatten bots json
2298 tests = self.test_suites
2299 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2300
2301 cmd_class = query[0]
2302
2303 # For queries starting with 'bots'
2304 if cmd_class == "bots":
2305 if len(query) == 1:
2306 return self.output_query_result(bots, args.json)
2307 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332308 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062309 if query[1] == 'tests':
2310 test_suites_dict = self.get_test_suites_dict(bots)
2311 return self.output_query_result(test_suites_dict, args.json)
Joshua Hood56c673c2022-03-02 20:29:332312 self.error_msg("This query should be in the format: bots/tests.")
Karen Qiane24b7ee2019-02-12 23:37:062313
2314 else:
2315 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2316 % str(len(query)-1))
2317
2318 # For queries starting with 'bot'
2319 elif cmd_class == "bot":
2320 if not len(query) == 2 and not len(query) == 3:
2321 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2322 % str(len(query)-1))
2323 bot_id = query[1]
2324 if not bot_id in bots:
2325 self.error_msg("No bot named '" + bot_id + "' found.")
2326 bot_info = bots[bot_id]
2327 if len(query) == 2:
2328 return self.output_query_result(bot_info, args.json)
2329 if not query[2] == 'tests':
2330 self.error_msg("The query should be in the format:" +
2331 "bot/<bot-name>/tests.")
2332
2333 bot_tests = self.flatten_tests_for_bot(bot_info)
2334 return self.output_query_result(bot_tests, args.json)
2335
2336 # For queries starting with 'tests'
2337 elif cmd_class == "tests":
2338 if not len(query) == 1 and not len(query) == 2:
2339 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2340 % str(len(query)-1))
2341 flattened_tests = self.flatten_tests_for_query(tests)
2342 if len(query) == 1:
2343 return self.output_query_result(flattened_tests, args.json)
2344
2345 # create params dict
2346 params = query[1].split('&')
2347 params_dict = self.parse_query_filter_params(params)
2348 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2349 return self.output_query_result(matching_bots)
2350
2351 # For queries starting with 'test'
2352 elif cmd_class == "test":
2353 if not len(query) == 2 and not len(query) == 3:
2354 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2355 % str(len(query)-1))
2356 test_id = query[1]
2357 if len(query) == 2:
2358 flattened_tests = self.flatten_tests_for_query(tests)
2359 for test in flattened_tests:
2360 if test == test_id:
2361 return self.output_query_result(flattened_tests[test], args.json)
2362 self.error_msg("There is no test named %s." % test_id)
2363 if not query[2] == 'bots':
2364 self.error_msg("The query should be in the format: " +
2365 "test/<test-name>/bots")
2366 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2367 return self.output_query_result(bots_for_test)
2368
2369 else:
2370 self.error_msg("Your command did not match any valid commands." +
2371 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Joshua Hood56c673c2022-03-02 20:29:332372 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282373
Garrett Beaty1afaccc2020-06-25 19:58:152374 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282375 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502376 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062377 elif self.args.query:
2378 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282379 else:
Greg Gutermanf60eb052020-03-12 17:40:012380 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282381 return 0
2382
2383if __name__ == "__main__": # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152384 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2385 sys.exit(generator.main())