blob: 9e3ee35872fbac17860e92bf732a0786cf9e8378 [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.')
377 parser.add_argument('-v',
378 '--verbose',
379 action='store_true',
380 help='Increases verbosity. Affects consistency checks.')
381 parser.add_argument('waterfall_filters',
382 metavar='waterfalls',
383 type=str,
384 nargs='*',
385 help='Optional list of waterfalls to generate.')
386 parser.add_argument(
387 '--pyl-files-dir',
Garrett Beaty79339e182023-04-10 20:45:47388 type=os.path.abspath,
389 help=('Path to the directory containing the input .pyl files.'
390 ' By default the directory containing this script will be used.'))
Garrett Beaty1afaccc2020-06-25 19:58:15391 parser.add_argument(
Garrett Beaty79339e182023-04-10 20:45:47392 '--output-dir',
393 type=os.path.abspath,
394 help=('Path to the directory to output generated .json files.'
395 'By default, the pyl files directory will be used.'))
Chong Guee622242020-10-28 18:17:35396 parser.add_argument('--isolate-map-file',
397 metavar='PATH',
398 help='path to additional isolate map files.',
Garrett Beaty79339e182023-04-10 20:45:47399 type=os.path.abspath,
Chong Guee622242020-10-28 18:17:35400 default=[],
401 action='append',
402 dest='isolate_map_files')
Garrett Beaty1afaccc2020-06-25 19:58:15403 parser.add_argument(
404 '--infra-config-dir',
405 help='Path to the LUCI services configuration directory',
Garrett Beaty79339e182023-04-10 20:45:47406 type=os.path.abspath,
407 default=os.path.join(os.path.dirname(__file__), '..', '..', 'infra',
408 'config'))
409
Garrett Beaty1afaccc2020-06-25 19:58:15410 args = parser.parse_args(argv)
411 if args.json and not args.query:
412 parser.error(
413 "The --json flag can only be used with --query.") # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:15414
Garrett Beaty79339e182023-04-10 20:45:47415 args.pyl_files_dir = args.pyl_files_dir or THIS_DIR
416 args.output_dir = args.output_dir or args.pyl_files_dir
417
Stephanie Kim572b43c02023-04-13 14:24:13418 def absolute_file_path(filename):
Garrett Beaty79339e182023-04-10 20:45:47419 return os.path.join(args.pyl_files_dir, filename)
420
Stephanie Kim572b43c02023-04-13 14:24:13421 args.waterfalls_pyl_path = absolute_file_path('waterfalls.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13422 args.test_suites_pyl_path = absolute_file_path('test_suites.pyl')
423 args.test_suite_exceptions_pyl_path = absolute_file_path(
Garrett Beaty79339e182023-04-10 20:45:47424 'test_suite_exceptions.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13425 args.gn_isolate_map_pyl_path = absolute_file_path('gn_isolate_map.pyl')
426 args.variants_pyl_path = absolute_file_path('variants.pyl')
427 args.autoshard_exceptions_json_path = absolute_file_path(
428 'autoshard_exceptions.json')
Garrett Beaty79339e182023-04-10 20:45:47429
Garrett Beaty1aa22f202023-06-28 18:29:32430 if args.pyl_files_dir == THIS_DIR:
431 args.mixins_pyl_path = os.path.join(args.infra_config_dir, 'generated',
432 'testing', 'mixins.pyl')
433 else:
Garrett Beaty3f4b6c42023-06-28 21:45:37434 args.mixins_pyl_path = absolute_file_path('mixins.pyl')
Garrett Beaty1aa22f202023-06-28 18:29:32435
Garrett Beaty79339e182023-04-10 20:45:47436 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
Kenneth Russelleb60cbd22017-12-05 07:54:28613 def dictionary_merge(self, a, b, path=None, update=True):
614 """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:
621 if key in a:
622 if isinstance(a[key], dict) and isinstance(b[key], dict):
623 self.dictionary_merge(a[key], b[key], path + [str(key)])
624 elif a[key] == b[key]:
625 pass # same leaf value
626 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06627 # Args arrays are lists of strings. Just concatenate them,
628 # and don't sort them, in order to keep some needed
Weizhong Xia91b53362022-01-05 17:13:35629 # arguments adjacent (like --timeout-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28630 if all(isinstance(x, str)
631 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01632 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28633 else:
634 # TODO(kbr): this only works properly if the two arrays are
635 # the same length, which is currently always the case in the
636 # swarming dimension_sets that we have to merge. It will fail
637 # to merge / override 'args' arrays which are different
638 # length.
Jamie Madillcf4f8c72021-05-20 19:24:23639 for idx in range(len(b[key])):
Kenneth Russell8ceeabf2017-12-11 17:53:28640 try:
641 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
642 path + [str(key), str(idx)],
643 update=update)
Joshua Hood56c673c2022-03-02 20:29:33644 except (IndexError, TypeError) as e:
Josip Sokcevic7110fb382023-06-06 01:05:29645 raise BBGenErr('Error merging lists by key "%s" from source %s '
646 'into target %s at index %s. Verify target list '
647 'length is equal or greater than source' %
648 (str(key), str(b), str(a), str(idx))) from e
John Budorick5bc387fe2019-05-09 20:02:53649 elif update:
650 if b[key] is None:
651 del a[key]
652 else:
653 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28654 else:
655 raise BBGenErr('Conflict at %s' % '.'.join(
656 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53657 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28658 a[key] = b[key]
659 return a
660
John Budorickab108712018-09-01 00:12:21661 def initialize_args_for_test(
662 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21663 args = []
664 args.extend(generated_test.get('args', []))
665 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22666
Kenneth Russell8a386d42018-06-02 09:48:01667 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21668 val = generated_test.pop(key, [])
669 if fn(tester_config):
670 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01671
672 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
Brian Sheedy781c8ca42021-03-08 22:03:21673 add_conditional_args('lacros_args', self.is_lacros)
Kenneth Russell8a386d42018-06-02 09:48:01674 add_conditional_args('linux_args', self.is_linux)
675 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36676 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49677 add_conditional_args('mac_args', self.is_mac)
678 add_conditional_args('win_args', self.is_win)
679 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01680
John Budorickab108712018-09-01 00:12:21681 for key in additional_arg_keys or []:
682 args.extend(generated_test.pop(key, []))
683 args.extend(tester_config.get(key, []))
684
685 if args:
686 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01687
Kenneth Russelleb60cbd22017-12-05 07:54:28688 def initialize_swarming_dictionary_for_test(self, generated_test,
689 tester_config):
690 if 'swarming' not in generated_test:
691 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28692 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
693 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38694 'can_use_on_swarming_builders': tester_config.get('use_swarming',
695 True)
Dirk Pranke81ff51c2017-12-09 19:24:28696 })
Kenneth Russelleb60cbd22017-12-05 07:54:28697 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03698 if ('dimension_sets' not in generated_test['swarming'] and
699 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28700 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
701 tester_config['swarming']['dimension_sets'])
702 self.dictionary_merge(generated_test['swarming'],
703 tester_config['swarming'])
Brian Sheedybc984e242021-04-21 23:44:51704 # Apply any platform-specific Swarming dimensions after the generic ones.
Kenneth Russelleb60cbd22017-12-05 07:54:28705 if 'android_swarming' in generated_test:
706 if self.is_android(tester_config): # pragma: no cover
707 self.dictionary_merge(
708 generated_test['swarming'],
709 generated_test['android_swarming']) # pragma: no cover
710 del generated_test['android_swarming'] # pragma: no cover
Brian Sheedybc984e242021-04-21 23:44:51711 if 'chromeos_swarming' in generated_test:
712 if self.is_chromeos(tester_config): # pragma: no cover
713 self.dictionary_merge(
714 generated_test['swarming'],
715 generated_test['chromeos_swarming']) # pragma: no cover
716 del generated_test['chromeos_swarming'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28717
718 def clean_swarming_dictionary(self, swarming_dict):
719 # Clean out redundant entries from a test's "swarming" dictionary.
720 # This is really only needed to retain 100% parity with the
721 # handwritten JSON files, and can be removed once all the files are
722 # autogenerated.
723 if 'shards' in swarming_dict:
724 if swarming_dict['shards'] == 1: # pragma: no cover
725 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24726 if 'hard_timeout' in swarming_dict:
727 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
728 del swarming_dict['hard_timeout'] # pragma: no cover
Garrett Beatybb18d532023-06-26 22:16:33729 del swarming_dict['can_use_on_swarming_builders']
Kenneth Russelleb60cbd22017-12-05 07:54:28730
Stephen Martinis0382bc12018-09-17 22:29:07731 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
732 waterfall):
733 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01734 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07735 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28736 # See if there are any exceptions that need to be merged into this
737 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49738 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28739 if modifications:
740 test = self.dictionary_merge(test, modifications)
Garrett Beatybfeff8f2023-06-16 18:57:25741 if (swarming_dict := test.get('swarming')) is not None:
Garrett Beatybb18d532023-06-26 22:16:33742 if swarming_dict.get('can_use_on_swarming_builders'):
Garrett Beatybfeff8f2023-06-16 18:57:25743 self.clean_swarming_dictionary(swarming_dict)
744 else:
745 del test['swarming']
Ben Pastenee012aea42019-05-14 22:32:28746 # Ensure all Android Swarming tests run only on userdebug builds if another
747 # build type was not specified.
748 if 'swarming' in test and self.is_android(tester_config):
749 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22750 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28751 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37752 self.replace_test_args(test, test_name, tester_name)
Garrett Beatyafd33e0f2023-06-23 20:47:57753 if 'args' in test and not test['args']:
754 test.pop('args')
Ben Pastenee012aea42019-05-14 22:32:28755
Kenneth Russelleb60cbd22017-12-05 07:54:28756 return test
757
Brian Sheedye6ea0ee2019-07-11 02:54:37758 def replace_test_args(self, test, test_name, tester_name):
759 replacements = self.get_test_replacements(
760 test, test_name, tester_name) or {}
761 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23762 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37763 if key not in valid_replacement_keys:
764 raise BBGenErr(
765 'Given replacement key %s for %s on %s is not in the list of valid '
766 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23767 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37768 found_key = False
769 for i, test_key in enumerate(test.get(key, [])):
770 # Handle both the key/value being replaced being defined as two
771 # separate items or as key=value.
772 if test_key == replacement_key:
773 found_key = True
774 # Handle flags without values.
775 if replacement_val == None:
776 del test[key][i]
777 else:
778 test[key][i+1] = replacement_val
779 break
Joshua Hood56c673c2022-03-02 20:29:33780 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37781 found_key = True
782 if replacement_val == None:
783 del test[key][i]
784 else:
785 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
786 break
787 if not found_key:
788 raise BBGenErr('Could not find %s in existing list of values for key '
789 '%s in %s on %s' % (replacement_key, key, test_name,
790 tester_name))
791
Shenghua Zhangaba8bad2018-02-07 02:12:09792 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05793 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26794 True):
795 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06796 # are targeting CrOS hardware and so need the special trigger script.
797 dimension_sets = test['swarming']['dimension_sets']
Ben Pastenea9e583b2019-01-16 02:57:26798 if all('device_type' in ds for ds in dimension_sets):
799 test['trigger_script'] = {
800 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
801 }
Shenghua Zhangaba8bad2018-02-07 02:12:09802
Ben Pastene858f4be2019-01-09 23:52:09803 def add_android_presentation_args(self, tester_config, test_name, result):
804 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38805 bucket = tester_config.get('results_bucket', 'chromium-result-details')
806 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09807 if (result['swarming']['can_use_on_swarming_builders'] and not
808 tester_config.get('skip_merge_script', False)):
809 result['merge'] = {
810 'args': [
811 '--bucket',
John Budorick262ae112019-07-12 19:24:38812 bucket,
Ben Pastene858f4be2019-01-09 23:52:09813 '--test-name',
Rakib M. Hasanc9e01c602020-07-27 22:48:12814 result.get('name', test_name)
Ben Pastene858f4be2019-01-09 23:52:09815 ],
816 'script': '//build/android/pylib/results/presentation/'
817 'test_results_presentation.py',
818 }
Ben Pastene858f4be2019-01-09 23:52:09819 if not tester_config.get('skip_output_links', False):
820 result['swarming']['output_links'] = [
821 {
822 'link': [
823 'https://luci-logdog.appspot.com/v/?s',
824 '=android%2Fswarming%2Flogcats%2F',
825 '${TASK_ID}%2F%2B%2Funified_logcats',
826 ],
827 'name': 'shard #${SHARD_INDEX} logcats',
828 },
829 ]
830 if args:
831 result['args'] = args
832
Kenneth Russelleb60cbd22017-12-05 07:54:28833 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
834 test_config):
835 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15836 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28837 return None
838 result = copy.deepcopy(test_config)
839 if 'test' in result:
Rakib M. Hasanc9e01c602020-07-27 22:48:12840 if 'name' not in result:
841 result['name'] = test_name
Kenneth Russelleb60cbd22017-12-05 07:54:28842 else:
843 result['test'] = test_name
844 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21845
846 self.initialize_args_for_test(
847 result, tester_config, additional_arg_keys=['gtest_args'])
Jamie Madilla8be0d72020-10-02 05:24:04848 if self.is_android(tester_config) and tester_config.get(
Yuly Novikov26dd47052021-02-11 00:57:14849 'use_swarming', True):
850 if not test_config.get('use_isolated_scripts_api', False):
851 # TODO(https://crbug.com/1137998) make Android presentation work with
852 # isolated scripts in test_results_presentation.py merge script
853 self.add_android_presentation_args(tester_config, test_name, result)
854 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42855
Stephen Martinis0382bc12018-09-17 22:29:07856 result = self.update_and_cleanup_test(
857 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09858 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34859 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43860
Garrett Beatybb18d532023-06-26 22:16:33861 if 'swarming' in result and not result.get('merge'):
Jamie Madilla8be0d72020-10-02 05:24:04862 if test_config.get('use_isolated_scripts_api', False):
863 merge_script = 'standard_isolated_script_merge'
864 else:
865 merge_script = 'standard_gtest_merge'
866
Stephen Martinisbc7b7772019-05-01 22:01:43867 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04868 'script': '//testing/merge_scripts/%s.py' % merge_script,
Stephen Martinisbc7b7772019-05-01 22:01:43869 }
Kenneth Russelleb60cbd22017-12-05 07:54:28870 return result
871
872 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
873 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01874 if not self.should_run_on_tester(waterfall, tester_name, test_name,
875 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28876 return None
877 result = copy.deepcopy(test_config)
878 result['isolate_name'] = result.get('isolate_name', test_name)
Jeff Yoonb8bfdbf32020-03-13 19:14:43879 result['name'] = result.get('name', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28880 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01881 self.initialize_args_for_test(result, tester_config)
Yuly Novikov26dd47052021-02-11 00:57:14882 if self.is_android(tester_config) and tester_config.get(
883 'use_swarming', True):
884 if tester_config.get('use_android_presentation', False):
885 # TODO(https://crbug.com/1137998) make Android presentation work with
886 # isolated scripts in test_results_presentation.py merge script
887 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07888 result = self.update_and_cleanup_test(
889 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09890 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34891 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17892
Garrett Beatybb18d532023-06-26 22:16:33893 if 'swarming' in result and not result.get('merge'):
Stephen Martinisf50047062019-05-06 22:26:17894 # TODO(https://crbug.com/958376): Consider adding the ability to not have
895 # this default.
896 result['merge'] = {
897 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
Stephen Martinisf50047062019-05-06 22:26:17898 }
Kenneth Russelleb60cbd22017-12-05 07:54:28899 return result
900
901 def generate_script_test(self, waterfall, tester_name, tester_config,
902 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44903 # TODO(https://crbug.com/953072): Remove this check whenever a better
904 # long-term solution is implemented.
905 if (waterfall.get('forbid_script_tests', False) or
906 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
907 raise BBGenErr('Attempted to generate a script test on tester ' +
908 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01909 if not self.should_run_on_tester(waterfall, tester_name, test_name,
910 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28911 return None
912 result = {
913 'name': test_name,
914 'script': test_config['script']
915 }
Stephen Martinis0382bc12018-09-17 22:29:07916 result = self.update_and_cleanup_test(
917 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34918 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28919 return result
920
921 def generate_junit_test(self, waterfall, tester_name, tester_config,
922 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01923 if not self.should_run_on_tester(waterfall, tester_name, test_name,
924 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28925 return None
John Budorickdef6acb2019-09-17 22:51:09926 result = copy.deepcopy(test_config)
927 result.update({
John Budorickcadc4952019-09-16 23:51:37928 'name': test_name,
929 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09930 })
931 self.initialize_args_for_test(result, tester_config)
932 result = self.update_and_cleanup_test(
933 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34934 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28935 return result
936
Xinan Lin05fb9c1752020-12-17 00:15:52937 def generate_skylab_test(self, waterfall, tester_name, tester_config,
938 test_name, test_config):
939 if not self.should_run_on_tester(waterfall, tester_name, test_name,
940 test_config):
941 return None
942 result = copy.deepcopy(test_config)
943 result.update({
944 'test': test_name,
945 })
946 self.initialize_args_for_test(result, tester_config)
947 result = self.update_and_cleanup_test(result, test_name, tester_name,
948 tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34949 self.substitute_magic_args(result, tester_name, tester_config)
Xinan Lin05fb9c1752020-12-17 00:15:52950 return result
951
Stephen Martinis2a0667022018-09-25 22:31:14952 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01953 substitutions = {
954 # Any machine in waterfalls.pyl which desires to run GPU tests
955 # must provide the os_type key.
956 'os_type': tester_config['os_type'],
957 'gpu_vendor_id': '0',
958 'gpu_device_id': '0',
959 }
Brian Sheedyb6491ba2022-09-26 20:49:49960 if swarming_config.get('dimension_sets'):
961 dimension_set = swarming_config['dimension_sets'][0]
962 if 'gpu' in dimension_set:
963 # First remove the driver version, then split into vendor and device.
964 gpu = dimension_set['gpu']
965 if gpu != 'none':
966 gpu = gpu.split('-')[0].split(':')
967 substitutions['gpu_vendor_id'] = gpu[0]
968 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01969 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
970
971 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30972 test_name, test_config, is_android_webview,
973 is_cast_streaming):
Kenneth Russell8a386d42018-06-02 09:48:01974 # These are all just specializations of isolated script tests with
975 # a bunch of boilerplate command line arguments added.
976
977 # The step name must end in 'test' or 'tests' in order for the
978 # results to automatically show up on the flakiness dashboard.
979 # (At least, this was true some time ago.) Continue to use this
980 # naming convention for the time being to minimize changes.
981 step_name = test_config.get('name', test_name)
Ben Pastene5f231cf22022-05-05 18:03:07982 variant_id = test_config.get('variant_id')
983 if variant_id:
984 step_name = self.remove_variant_from_test_name(step_name, variant_id)
Kenneth Russell8a386d42018-06-02 09:48:01985 if not (step_name.endswith('test') or step_name.endswith('tests')):
986 step_name = '%s_tests' % step_name
Ben Pastene5f231cf22022-05-05 18:03:07987 if variant_id:
988 step_name = self.add_variant_to_test_name(step_name, variant_id)
Ben Pastene5ff45d82022-05-05 21:54:00989 if 'name' in test_config:
990 test_config['name'] = step_name
Kenneth Russell8a386d42018-06-02 09:48:01991 result = self.generate_isolated_script_test(
992 waterfall, tester_name, tester_config, step_name, test_config)
993 if not result:
994 return None
Chong Gub75754b32020-03-13 16:39:20995 result['isolate_name'] = test_config.get(
Brian Sheedyf74819b2021-06-04 01:38:38996 'isolate_name',
997 self.get_default_isolate_name(tester_config, is_android_webview))
Chan Liab7d8dd82020-04-24 23:42:19998
Chan Lia3ad1502020-04-28 05:32:11999 # Populate test_id_prefix.
Brian Sheedyf74819b2021-06-04 01:38:381000 gn_entry = self.gn_isolate_map[result['isolate_name']]
Chan Li17d969f92020-07-10 00:50:031001 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:191002
Kenneth Russell8a386d42018-06-02 09:48:011003 args = result.get('args', [])
1004 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:141005
erikchen6da2d9b2018-08-03 23:01:141006 # These tests upload and download results from cloud storage and therefore
1007 # aren't idempotent yet. https://crbug.com/549140.
Garrett Beatybfeff8f2023-06-16 18:57:251008 if 'swarming' in result:
1009 result['swarming']['idempotent'] = False
erikchen6da2d9b2018-08-03 23:01:141010
Kenneth Russell44910c32018-12-03 23:35:111011 # The GPU tests act much like integration tests for the entire browser, and
1012 # tend to uncover flakiness bugs more readily than other test suites. In
1013 # order to surface any flakiness more readily to the developer of the CL
1014 # which is introducing it, we disable retries with patch on the commit
1015 # queue.
1016 result['should_retry_with_patch'] = False
1017
Fabrice de Ganscbd655f2022-08-04 20:15:301018 browser = ''
1019 if is_cast_streaming:
1020 browser = 'cast-streaming-shell'
1021 elif is_android_webview:
1022 browser = 'android-webview-instrumentation'
1023 else:
1024 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:521025
Greg Thompsoncec7d8d2023-01-10 19:11:531026 extra_browser_args = []
1027
Brian Sheedy4053a702020-07-28 02:09:521028 # Most platforms require --enable-logging=stderr to get useful browser logs.
1029 # However, this actively messes with logging on CrOS (because Chrome's
1030 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
1031 # in order to see JavaScript console messages. See
1032 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
Greg Thompsoncec7d8d2023-01-10 19:11:531033 if self.is_chromeos(tester_config):
1034 extra_browser_args.append('--log-level=0')
1035 elif not self.is_fuchsia(tester_config) or browser != 'fuchsia-chrome':
1036 # Stderr logging is not needed for Chrome browser on Fuchsia, as ordinary
1037 # logging via syslog is captured.
1038 extra_browser_args.append('--enable-logging=stderr')
1039
1040 # --expose-gc allows the WebGL conformance tests to more reliably
1041 # reproduce GC-related bugs in the V8 bindings.
1042 extra_browser_args.append('--js-flags=--expose-gc')
Brian Sheedy4053a702020-07-28 02:09:521043
Kenneth Russell8a386d42018-06-02 09:48:011044 args = [
Bo Liu555a0f92019-03-29 12:11:561045 test_to_run,
1046 '--show-stdout',
1047 '--browser=%s' % browser,
1048 # --passthrough displays more of the logging in Telemetry when
1049 # run via typ, in particular some of the warnings about tests
1050 # being expected to fail, but passing.
1051 '--passthrough',
1052 '-v',
Brian Sheedy814e0482022-10-03 23:24:121053 '--stable-jobs',
Greg Thompsoncec7d8d2023-01-10 19:11:531054 '--extra-browser-args=%s' % ' '.join(extra_browser_args),
Kenneth Russell8a386d42018-06-02 09:48:011055 ] + args
Garrett Beatybfeff8f2023-06-16 18:57:251056 result['args'] = self.maybe_fixup_args_array(
1057 self.substitute_gpu_args(tester_config, result.get('swarming', {}),
1058 args))
Kenneth Russell8a386d42018-06-02 09:48:011059 return result
1060
Brian Sheedyf74819b2021-06-04 01:38:381061 def get_default_isolate_name(self, tester_config, is_android_webview):
1062 if self.is_android(tester_config):
1063 if is_android_webview:
1064 return 'telemetry_gpu_integration_test_android_webview'
1065 return (
1066 'telemetry_gpu_integration_test' +
1067 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:331068 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:171069 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:331070 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:381071
Kenneth Russelleb60cbd22017-12-05 07:54:281072 def get_test_generator_map(self):
1073 return {
Bo Liu555a0f92019-03-29 12:11:561074 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301075 GPUTelemetryTestGenerator(self, is_android_webview=True),
1076 'cast_streaming_tests':
1077 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:561078 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301079 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561080 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301081 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561082 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301083 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561084 'junit_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301085 JUnitGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561086 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301087 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:521088 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301089 SkylabGenerator(self),
Brian Sheedyb6491ba2022-09-26 20:49:491090 'skylab_gpu_telemetry_tests':
1091 SkylabGPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281092 }
1093
Kenneth Russell8a386d42018-06-02 09:48:011094 def get_test_type_remapper(self):
1095 return {
Fabrice de Gans223272482022-08-08 16:56:571096 # These are a specialization of isolated_scripts with a bunch of
1097 # boilerplate command line arguments added to each one.
1098 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
1099 'cast_streaming_tests': 'isolated_scripts',
1100 'gpu_telemetry_tests': 'isolated_scripts',
Brian Sheedyb6491ba2022-09-26 20:49:491101 # These are the same as existing test types, just configured to run
1102 # in Skylab instead of via normal swarming.
1103 'skylab_gpu_telemetry_tests': 'skylab_tests',
Kenneth Russell8a386d42018-06-02 09:48:011104 }
1105
Jeff Yoon67c3e832020-02-08 07:39:381106 def check_composition_type_test_suites(self, test_type,
1107 additional_validators=None):
1108 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1109 validators = [check_compound_references,
1110 check_basic_references,
1111 check_conflicting_definitions]
1112 if additional_validators:
1113 validators += additional_validators
1114
1115 target_suites = self.test_suites.get(test_type, {})
1116 other_test_type = ('compound_suites'
1117 if test_type == 'matrix_compound_suites'
1118 else 'matrix_compound_suites')
1119 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011120 basic_suites = self.test_suites.get('basic_suites', {})
1121
Jamie Madillcf4f8c72021-05-20 19:24:231122 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011123 if suite in basic_suites:
1124 raise BBGenErr('%s names may not duplicate basic test suite names '
1125 '(error found while processsing %s)'
1126 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011127
Jeff Yoon67c3e832020-02-08 07:39:381128 seen_tests = {}
1129 for sub_suite in suite_def:
1130 for validator in validators:
1131 validator(
1132 basic_suites=basic_suites,
1133 other_test_suites=other_suites,
1134 seen_tests=seen_tests,
1135 sub_suite=sub_suite,
1136 suite=suite,
1137 suite_def=suite_def,
1138 target_test_suites=target_suites,
1139 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051140 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381141 )
Kenneth Russelleb60cbd22017-12-05 07:54:281142
Stephen Martinis54d64ad2018-09-21 22:16:201143 def flatten_test_suites(self):
1144 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011145 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1146 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231147 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011148 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201149 self.test_suites = new_test_suites
1150
Chan Lia3ad1502020-04-28 05:32:111151 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231152 for suite in self.test_suites['basic_suites'].values():
1153 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561154 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411155
1156 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
John Palmera8515fca2021-05-20 03:35:321157 # 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:411158 # TODO(crbug.com/1035124): clean this up.
1159 isolate_name = test.get('test') or test.get('isolate_name') or key
1160 gn_entry = self.gn_isolate_map.get(isolate_name)
1161 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281162 label = gn_entry['label']
1163
1164 if label.count(':') != 1:
1165 raise BBGenErr(
1166 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1167 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1168 (label, isolate_name))
1169 if label.split(':')[1] != isolate_name:
1170 raise BBGenErr(
1171 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1172 ' label "%s" see http://crbug.com/1071091 for details.' %
1173 (isolate_name, label))
1174
Chan Lia3ad1502020-04-28 05:32:111175 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411176 else: # pragma: no cover
1177 # Some tests do not have an entry gn_isolate_map.pyl, such as
1178 # telemetry tests.
1179 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
1180 pass
1181
Kenneth Russelleb60cbd22017-12-05 07:54:281182 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011183 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201184
Jeff Yoon8154e582019-12-03 23:30:011185 compound_suites = self.test_suites.get('compound_suites', {})
1186 # check_composition_type_test_suites() checks that all basic suites
1187 # referenced by compound suites exist.
1188 basic_suites = self.test_suites.get('basic_suites')
1189
Jamie Madillcf4f8c72021-05-20 19:24:231190 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011191 # Resolve this to a dictionary.
1192 full_suite = {}
1193 for entry in value:
1194 suite = basic_suites[entry]
1195 full_suite.update(suite)
1196 compound_suites[name] = full_suite
1197
Jeff Yoon85fb8df2020-08-20 16:47:431198 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381199 """ Merge variant-defined configurations to each test case definition in a
1200 test suite.
1201
1202 The output maps a unique test name to an array of configurations because
1203 there may exist more than one definition for a test name using variants. The
1204 test name is referenced while mapping machines to test suites, so unpacking
1205 the array is done by the generators.
1206
1207 Args:
1208 basic_test_definition: a {} defined test suite in the format
1209 test_name:test_config
1210 variants: an [] of {} defining configurations to be applied to each test
1211 case in the basic test_definition
1212
1213 Return:
1214 a {} of test_name:[{}], where each {} is a merged configuration
1215 """
1216
1217 # Each test in a basic test suite will have a definition per variant.
1218 test_suite = {}
Jamie Madillcf4f8c72021-05-20 19:24:231219 for test_name, test_config in basic_test_definition.items():
Jeff Yoon67c3e832020-02-08 07:39:381220 definitions = []
1221 for variant in variants:
Jeff Yoonda581c32020-03-06 03:56:051222 # Unpack the variant from variants.pyl if it's string based.
1223 if isinstance(variant, str):
1224 variant = self.variants[variant]
1225
Jieting Yangef6b1042021-11-30 21:33:481226 # If 'enabled' is set to False, we will not use this variant;
1227 # otherwise if the variant doesn't include 'enabled' variable or
1228 # 'enabled' is set to True, we will use this variant
1229 if not variant.get('enabled', True):
1230 continue
Jeff Yoon67c3e832020-02-08 07:39:381231 # Clone a copy of test_config so that we can have a uniquely updated
1232 # version of it per variant
1233 cloned_config = copy.deepcopy(test_config)
1234 # The variant definition needs to be re-used for each test, so we'll
1235 # create a clone and work with it as well.
1236 cloned_variant = copy.deepcopy(variant)
1237
1238 cloned_config['args'] = (cloned_config.get('args', []) +
1239 cloned_variant.get('args', []))
1240 cloned_config['mixins'] = (cloned_config.get('mixins', []) +
Jeff Yoon85fb8df2020-08-20 16:47:431241 cloned_variant.get('mixins', []) + mixins)
Jeff Yoon67c3e832020-02-08 07:39:381242
Sven Zhengb51bd0482022-08-26 18:26:251243 description = []
Sven Zhengdcf2ddf2022-08-30 04:24:331244 if cloned_config.get('description'):
1245 description.append(cloned_config.get('description'))
1246 if cloned_variant.get('description'):
1247 description.append(cloned_variant.get('description'))
Sven Zhengb51bd0482022-08-26 18:26:251248 if description:
1249 cloned_config['description'] = '\n'.join(description)
Jeff Yoon67c3e832020-02-08 07:39:381250 basic_swarming_def = cloned_config.get('swarming', {})
1251 variant_swarming_def = cloned_variant.get('swarming', {})
1252 if basic_swarming_def and variant_swarming_def:
1253 if ('dimension_sets' in basic_swarming_def and
1254 'dimension_sets' in variant_swarming_def):
1255 # Retain swarming dimension set merge behavior when both variant and
1256 # the basic test configuration both define it
1257 self.dictionary_merge(basic_swarming_def, variant_swarming_def)
1258 # Remove dimension_sets from the variant definition, so that it does
1259 # not replace what's been done by dictionary_merge in the update
1260 # call below.
1261 del variant_swarming_def['dimension_sets']
1262
1263 # Update the swarming definition with whatever is defined for swarming
1264 # by the variant.
1265 basic_swarming_def.update(variant_swarming_def)
1266 cloned_config['swarming'] = basic_swarming_def
1267
Xinan Lin05fb9c1752020-12-17 00:15:521268 # Copy all skylab fields defined by the variant.
1269 skylab_config = cloned_variant.get('skylab')
1270 if skylab_config:
1271 for k, v in skylab_config.items():
Jieting Yangef6b1042021-11-30 21:33:481272 # cros_chrome_version is the ash chrome version in the cros img
1273 # in the variant of cros_board. We don't want to include it in
1274 # the final json files; so remove it.
1275 if k == 'cros_chrome_version':
1276 continue
Xinan Lin05fb9c1752020-12-17 00:15:521277 cloned_config[k] = v
1278
Jeff Yoon67c3e832020-02-08 07:39:381279 # The identifier is used to make the name of the test unique.
1280 # Generators in the recipe uniquely identify a test by it's name, so we
1281 # don't want to have the same name for each variant.
Ben Pastene5f231cf22022-05-05 18:03:071282 cloned_config['name'] = self.add_variant_to_test_name(
1283 cloned_config.get('name') or test_name,
1284 cloned_variant['identifier'])
1285
1286 # Attach the variant identifier to the test config so downstream
1287 # generators can make modifications based on the original name. This
1288 # is mainly used in generate_gpu_telemetry_test().
1289 cloned_config['variant_id'] = cloned_variant['identifier']
1290
Jeff Yoon67c3e832020-02-08 07:39:381291 definitions.append(cloned_config)
1292 test_suite[test_name] = definitions
1293 return test_suite
1294
Jeff Yoon8154e582019-12-03 23:30:011295 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381296 self.check_composition_type_test_suites('matrix_compound_suites',
1297 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011298
1299 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381300 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011301 # referenced by matrix suites exist.
1302 basic_suites = self.test_suites.get('basic_suites')
1303
Jamie Madillcf4f8c72021-05-20 19:24:231304 for test_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011305 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381306
Jamie Madillcf4f8c72021-05-20 19:24:231307 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381308 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1309
1310 if 'variants' in mtx_test_suite_config:
Jeff Yoon85fb8df2020-08-20 16:47:431311 mixins = mtx_test_suite_config.get('mixins', [])
Jeff Yoon67c3e832020-02-08 07:39:381312 result = self.resolve_variants(basic_test_def,
Jeff Yoon85fb8df2020-08-20 16:47:431313 mtx_test_suite_config['variants'],
1314 mixins)
Jeff Yoon67c3e832020-02-08 07:39:381315 full_suite.update(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271316 else:
1317 suite = basic_suites[test_suite]
1318 full_suite.update(suite)
Jeff Yoon67c3e832020-02-08 07:39:381319 matrix_compound_suites[test_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281320
1321 def link_waterfalls_to_test_suites(self):
1322 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231323 for tester_name, tester in waterfall['machines'].items():
1324 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281325 if not value in self.test_suites:
1326 # Hard / impossible to cover this in the unit test.
1327 raise self.unknown_test_suite(
1328 value, tester_name, waterfall['name']) # pragma: no cover
1329 tester['test_suites'][suite] = self.test_suites[value]
1330
1331 def load_configuration_files(self):
Garrett Beaty79339e182023-04-10 20:45:471332 self.waterfalls = self.load_pyl_file(self.args.waterfalls_pyl_path)
1333 self.test_suites = self.load_pyl_file(self.args.test_suites_pyl_path)
1334 self.exceptions = self.load_pyl_file(
1335 self.args.test_suite_exceptions_pyl_path)
1336 self.mixins = self.load_pyl_file(self.args.mixins_pyl_path)
1337 self.gn_isolate_map = self.load_pyl_file(self.args.gn_isolate_map_pyl_path)
Chong Guee622242020-10-28 18:17:351338 for isolate_map in self.args.isolate_map_files:
1339 isolate_map = self.load_pyl_file(isolate_map)
1340 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1341 if duplicates:
1342 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1343 ', '.join(duplicates))
1344 self.gn_isolate_map.update(isolate_map)
1345
Garrett Beaty79339e182023-04-10 20:45:471346 self.variants = self.load_pyl_file(self.args.variants_pyl_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281347
1348 def resolve_configuration_files(self):
Chan Lia3ad1502020-04-28 05:32:111349 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281350 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011351 self.resolve_matrix_compound_test_suites()
1352 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281353 self.link_waterfalls_to_test_suites()
1354
Nico Weberd18b8962018-05-16 19:39:381355 def unknown_bot(self, bot_name, waterfall_name):
1356 return BBGenErr(
1357 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1358
Kenneth Russelleb60cbd22017-12-05 07:54:281359 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1360 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381361 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281362 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1363
1364 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1365 return BBGenErr(
1366 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1367 ' on waterfall ' + waterfall_name)
1368
Stephen Martinisb72f6d22018-10-04 23:29:011369 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071370 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321371
1372 Checks in the waterfall, builder, and test objects for mixins.
1373 """
1374 def valid_mixin(mixin_name):
1375 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011376 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321377 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381378
Stephen Martinisb6a50492018-09-12 23:59:321379 def must_be_list(mixins, typ, name):
1380 """Asserts that given mixins are a list."""
1381 if not isinstance(mixins, list):
1382 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1383
Brian Sheedy7658c982020-01-08 02:27:581384 test_name = test.get('name')
1385 remove_mixins = set()
1386 if 'remove_mixins' in builder:
1387 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1388 for rm in builder['remove_mixins']:
1389 valid_mixin(rm)
1390 remove_mixins.add(rm)
1391 if 'remove_mixins' in test:
1392 must_be_list(test['remove_mixins'], 'test', test_name)
1393 for rm in test['remove_mixins']:
1394 valid_mixin(rm)
1395 remove_mixins.add(rm)
1396 del test['remove_mixins']
1397
Stephen Martinisb72f6d22018-10-04 23:29:011398 if 'mixins' in waterfall:
1399 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1400 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581401 if mixin in remove_mixins:
1402 continue
Stephen Martinisb6a50492018-09-12 23:59:321403 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531404 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321405
Stephen Martinisb72f6d22018-10-04 23:29:011406 if 'mixins' in builder:
1407 must_be_list(builder['mixins'], 'builder', builder_name)
1408 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581409 if mixin in remove_mixins:
1410 continue
Stephen Martinisb6a50492018-09-12 23:59:321411 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531412 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321413
Stephen Martinisb72f6d22018-10-04 23:29:011414 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071415 return test
1416
Stephen Martinis2a0667022018-09-25 22:31:141417 if not test_name:
1418 test_name = test.get('test')
1419 if not test_name: # pragma: no cover
1420 # Not the best name, but we should say something.
1421 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:011422 must_be_list(test['mixins'], 'test', test_name)
1423 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581424 # We don't bother checking if the given mixin is in remove_mixins here
1425 # since this is already the lowest level, so if a mixin is added here that
1426 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071427 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531428 test = self.apply_mixin(self.mixins[mixin], test, builder)
Jeff Yoon67c3e832020-02-08 07:39:381429 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071430 return test
Stephen Martinisb6a50492018-09-12 23:59:321431
Austin Eng148d9f0f2022-02-08 19:18:531432 def apply_mixin(self, mixin, test, builder):
Stephen Martinisb72f6d22018-10-04 23:29:011433 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321434
Garrett Beaty4c35b142023-06-23 21:01:231435 A mixin is applied by copying all fields from the mixin into the
1436 test with the following exceptions:
1437 * For the various *args keys, the test's existing value (an empty
1438 list if not present) will be extended with the mixin's value.
1439 * The sub-keys of the swarming value will be copied to the test's
1440 swarming value with the following exceptions:
1441 * For the dimension_sets and named_caches sub-keys, the test's
1442 existing value (an empty list if not present) will be extended
1443 with the mixin's value.
1444 * For the dimensions sub-key, after extending the test's
1445 dimension_sets as specified above, each dimension set will be
1446 updated with the value of the dimensions sub-key. If there are
1447 no dimension sets, then one will be added that contains the
1448 specified dimensions.
Stephen Martinisb6a50492018-09-12 23:59:321449 """
Garrett Beaty4c35b142023-06-23 21:01:231450
Stephen Martinisb6a50492018-09-12 23:59:321451 new_test = copy.deepcopy(test)
1452 mixin = copy.deepcopy(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011453 if 'swarming' in mixin:
1454 swarming_mixin = mixin['swarming']
1455 new_test.setdefault('swarming', {})
Brian Sheedycae63b22020-06-10 22:52:111456 # Copy over any explicit dimension sets first so that they will be updated
1457 # by any subsequent 'dimensions' entries.
1458 if 'dimension_sets' in swarming_mixin:
1459 existing_dimension_sets = new_test['swarming'].setdefault(
1460 'dimension_sets', [])
1461 # Appending to the existing list could potentially result in different
1462 # behavior depending on the order the mixins were applied, but that's
1463 # already the case for other parts of mixins, so trust that the user
1464 # will verify that the generated output is correct before submitting.
1465 for dimension_set in swarming_mixin['dimension_sets']:
1466 if dimension_set not in existing_dimension_sets:
1467 existing_dimension_sets.append(dimension_set)
1468 del swarming_mixin['dimension_sets']
Stephen Martinisb72f6d22018-10-04 23:29:011469 if 'dimensions' in swarming_mixin:
1470 new_test['swarming'].setdefault('dimension_sets', [{}])
1471 for dimension_set in new_test['swarming']['dimension_sets']:
1472 dimension_set.update(swarming_mixin['dimensions'])
1473 del swarming_mixin['dimensions']
Garrett Beaty4c35b142023-06-23 21:01:231474 if 'named_caches' in swarming_mixin:
1475 new_test['swarming'].setdefault('named_caches', []).extend(
1476 swarming_mixin['named_caches'])
1477 del swarming_mixin['named_caches']
Stephen Martinisb72f6d22018-10-04 23:29:011478 # python dict update doesn't do recursion at all. Just hard code the
1479 # nested update we need (mixin['swarming'] shouldn't clobber
1480 # test['swarming'], but should update it).
1481 new_test['swarming'].update(swarming_mixin)
1482 del mixin['swarming']
1483
Garrett Beaty4c35b142023-06-23 21:01:231484 # Array so we can assign to it in a nested scope.
1485 args_need_fixup = ['args' in mixin]
1486
1487 for a in (
1488 'args',
1489 'precommit_args',
1490 'non_precommit_args',
1491 'desktop_args',
1492 'lacros_args',
1493 'linux_args',
1494 'android_args',
1495 'chromeos_args',
1496 'mac_args',
1497 'win_args',
1498 'win64_args',
1499 ):
1500 if (value := mixin.pop(a, None)) is None:
1501 continue
1502 if not isinstance(value, list):
1503 raise BBGenErr(f'"{a}" must be a list')
1504 new_test.setdefault(a, []).extend(value)
1505
Garrett Beaty4c35b142023-06-23 21:01:231506 args = new_test.get('args', [])
Austin Eng148d9f0f2022-02-08 19:18:531507
Garrett Beaty4c35b142023-06-23 21:01:231508 def add_conditional_args(key, fn):
1509 val = new_test.pop(key, [])
1510 if val and fn(builder):
1511 args.extend(val)
1512 args_need_fixup[0] = True
Austin Eng148d9f0f2022-02-08 19:18:531513
Garrett Beaty4c35b142023-06-23 21:01:231514 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
1515 add_conditional_args('lacros_args', self.is_lacros)
1516 add_conditional_args('linux_args', self.is_linux)
1517 add_conditional_args('android_args', self.is_android)
1518 add_conditional_args('chromeos_args', self.is_chromeos)
1519 add_conditional_args('mac_args', self.is_mac)
1520 add_conditional_args('win_args', self.is_win)
1521 add_conditional_args('win64_args', self.is_win64)
1522
1523 if args_need_fixup[0]:
1524 new_test['args'] = self.maybe_fixup_args_array(args)
Wezc0e835b702018-10-30 00:38:411525
Stephen Martinisb72f6d22018-10-04 23:29:011526 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321527 return new_test
1528
Greg Gutermanf60eb052020-03-12 17:40:011529 def generate_output_tests(self, waterfall):
1530 """Generates the tests for a waterfall.
1531
1532 Args:
1533 waterfall: a dictionary parsed from a master pyl file
1534 Returns:
1535 A dictionary mapping builders to test specs
1536 """
1537 return {
Jamie Madillcf4f8c72021-05-20 19:24:231538 name: self.get_tests_for_config(waterfall, name, config)
1539 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011540 }
1541
1542 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531543 generator_map = self.get_test_generator_map()
1544 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281545
Greg Gutermanf60eb052020-03-12 17:40:011546 tests = {}
1547 # Copy only well-understood entries in the machine's configuration
1548 # verbatim into the generated JSON.
1549 if 'additional_compile_targets' in config:
1550 tests['additional_compile_targets'] = config[
1551 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231552 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011553 if test_type not in generator_map:
1554 raise self.unknown_test_suite_type(
1555 test_type, name, waterfall['name']) # pragma: no cover
1556 test_generator = generator_map[test_type]
1557 # Let multiple kinds of generators generate the same kinds
1558 # of tests. For example, gpu_telemetry_tests are a
1559 # specialization of isolated_scripts.
1560 new_tests = test_generator.generate(
1561 waterfall, name, config, input_tests)
1562 remapped_test_type = test_type_remapper.get(test_type, test_type)
1563 tests[remapped_test_type] = test_generator.sort(
1564 tests.get(remapped_test_type, []) + new_tests)
1565
1566 return tests
1567
1568 def jsonify(self, all_tests):
1569 return json.dumps(
1570 all_tests, indent=2, separators=(',', ': '),
1571 sort_keys=True) + '\n'
1572
1573 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281574 self.load_configuration_files()
1575 self.resolve_configuration_files()
1576 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011577 result = collections.defaultdict(dict)
1578
Stephanie Kim572b43c02023-04-13 14:24:131579 if os.path.exists(self.args.autoshard_exceptions_json_path):
1580 autoshards = json.loads(
1581 self.read_file(self.args.autoshard_exceptions_json_path))
1582 else:
1583 autoshards = {}
1584
Dirk Pranke6269d302020-10-01 00:14:391585 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011586 for waterfall in self.waterfalls:
1587 for field in required_fields:
1588 # Verify required fields
1589 if field not in waterfall:
1590 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1591
1592 # Handle filter flag, if specified
1593 if filters and waterfall['name'] not in filters:
1594 continue
1595
1596 # Join config files and hardcoded values together
1597 all_tests = self.generate_output_tests(waterfall)
1598 result[waterfall['name']] = all_tests
1599
Stephanie Kim572b43c02023-04-13 14:24:131600 if not autoshards:
1601 continue
1602 for builder, test_spec in all_tests.items():
1603 for target_type, test_list in test_spec.items():
1604 if target_type == 'additional_compile_targets':
1605 continue
1606 for test_dict in test_list:
1607 # Suites that apply variants or other customizations will create
1608 # test_dicts that have "name" value that is different from the
1609 # "test" value. Regular suites without any variations will only have
1610 # "test" and no "name".
1611 # e.g. name = vulkan_swiftshader_content_browsertests, but
1612 # test = content_browsertests and
1613 # test_id_prefix = "ninja://content/test:content_browsertests/"
1614 # Check for "name" first and then fallback to "test"
1615 test_name = test_dict.get('name') or test_dict.get('test')
1616 if not test_name:
1617 continue
1618 shard_info = autoshards.get(waterfall['name'],
1619 {}).get(builder, {}).get(test_name)
1620 if shard_info:
1621 test_dict['swarming'].update(
1622 {'shards': int(shard_info['shards'])})
1623
Greg Gutermanf60eb052020-03-12 17:40:011624 # Add do not edit warning
1625 for tests in result.values():
1626 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1627 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1628
1629 return result
1630
1631 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281632 suffix = '.json'
1633 if self.args.new_files:
1634 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011635
1636 for filename, contents in result.items():
1637 jsonstr = self.jsonify(contents)
Garrett Beaty79339e182023-04-10 20:45:471638 file_path = os.path.join(self.args.output_dir, filename + suffix)
1639 self.write_file(file_path, jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281640
Nico Weberd18b8962018-05-16 19:39:381641 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161642 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421643 # NOTE: This reference can cause issues; if a file changes there, the
1644 # presubmit here won't be run by default. A manually maintained list there
1645 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1646 # references to configs outside of this directory are added, please change
1647 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1648 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491649
Garrett Beaty7e866fc2021-06-16 14:12:101650 # Get the generated project.pyl so we can check if we should be enforcing
1651 # that the specs are for builders that actually exist
1652 # If not, return None to indicate that we won't enforce that builders in
1653 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491654 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1655 'project.pyl')
1656 if os.path.exists(project_pyl_path):
1657 settings = ast.literal_eval(self.read_file(project_pyl_path))
1658 if not settings.get('validate_source_side_specs_have_builder', True):
1659 return None
1660
Nico Weberd18b8962018-05-16 19:39:381661 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311662 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161663 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1664 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431665 for c in milo_configs:
1666 for l in self.read_file(c).splitlines():
1667 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311668 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431669 continue
1670 # l looks like
1671 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1672 # Extract win_chromium_dbg_ng part.
1673 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381674 return bot_names
1675
Ben Pastene9a010082019-09-25 20:41:371676 def get_internal_waterfalls(self):
1677 # Similar to get_builders_that_do_not_actually_exist above, but for
1678 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201679 return [
Kramer Ge3bf853a2023-04-13 19:39:471680 'chrome', 'chrome.pgo', 'chrome.gpu.fyi', 'internal.chrome.fyi',
1681 'internal.chromeos.fyi', 'internal.soda'
Yuke Liaoe6c23dd2021-07-28 16:12:201682 ]
Ben Pastene9a010082019-09-25 20:41:371683
Stephen Martinisf83893722018-09-19 00:02:181684 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201685 self.check_input_files_sorting(verbose)
1686
Kenneth Russelleb60cbd22017-12-05 07:54:281687 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011688 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381689 self.check_composition_type_test_suites('matrix_compound_suites',
1690 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111691 self.resolve_test_id_prefixes()
Stephen Martinis54d64ad2018-09-21 22:16:201692 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381693
1694 # All bots should exist.
1695 bot_names = self.get_valid_bot_names()
Garrett Beaty2a02de3c2020-05-15 13:57:351696 if bot_names is not None:
1697 internal_waterfalls = self.get_internal_waterfalls()
1698 for waterfall in self.waterfalls:
1699 # TODO(crbug.com/991417): Remove the need for this exception.
1700 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011701 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351702 for bot_name in waterfall['machines']:
Garrett Beaty2a02de3c2020-05-15 13:57:351703 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581704 if waterfall['name'] in [
1705 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1706 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351707 # TODO(thakis): Remove this once these bots move to luci.
1708 continue # pragma: no cover
1709 if waterfall['name'] in ['tryserver.webrtc',
1710 'webrtc.chromium.fyi.experimental']:
1711 # These waterfalls have their bot configs in a different repo.
1712 # so we don't know about their bot names.
1713 continue # pragma: no cover
1714 if waterfall['name'] in ['client.devtools-frontend.integration',
1715 'tryserver.devtools-frontend',
1716 'chromium.devtools-frontend']:
1717 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201718 if waterfall['name'] in ['client.openscreen.chromium']:
1719 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351720 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381721
Kenneth Russelleb60cbd22017-12-05 07:54:281722 # All test suites must be referenced.
1723 suites_seen = set()
1724 generator_map = self.get_test_generator_map()
1725 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231726 for bot_name, tester in waterfall['machines'].items():
1727 for suite_type, suite in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281728 if suite_type not in generator_map:
1729 raise self.unknown_test_suite_type(suite_type, bot_name,
1730 waterfall['name'])
1731 if suite not in self.test_suites:
1732 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1733 suites_seen.add(suite)
1734 # Since we didn't resolve the configuration files, this set
1735 # includes both composition test suites and regular ones.
1736 resolved_suites = set()
1737 for suite_name in suites_seen:
1738 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011739 for sub_suite in suite:
1740 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281741 resolved_suites.add(suite_name)
1742 # At this point, every key in test_suites.pyl should be referenced.
1743 missing_suites = set(self.test_suites.keys()) - resolved_suites
1744 if missing_suites:
1745 raise BBGenErr('The following test suites were unreferenced by bots on '
1746 'the waterfalls: ' + str(missing_suites))
1747
1748 # All test suite exceptions must refer to bots on the waterfall.
1749 all_bots = set()
1750 missing_bots = set()
1751 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231752 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281753 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281754 # In order to disambiguate between bots with the same name on
1755 # different waterfalls, support has been added to various
1756 # exceptions for concatenating the waterfall name after the bot
1757 # name.
1758 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231759 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381760 removals = (exception.get('remove_from', []) +
1761 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231762 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381763 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281764 if removal not in all_bots:
1765 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411766
Kenneth Russelleb60cbd22017-12-05 07:54:281767 if missing_bots:
1768 raise BBGenErr('The following nonexistent machines were referenced in '
1769 'the test suite exceptions: ' + str(missing_bots))
1770
Garrett Beatyb061e69d2023-06-27 16:15:351771 for name, mixin in self.mixins.items():
1772 if '$mixin_append' in mixin:
1773 raise BBGenErr(
1774 f'$mixin_append is no longer supported (set in mixin "{name}"),'
1775 ' args and named caches specified as normal will be appended')
1776
Stephen Martinis0382bc12018-09-17 22:29:071777 # All mixins must be referenced
1778 seen_mixins = set()
1779 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011780 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231781 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011782 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071783 for suite in self.test_suites.values():
1784 if isinstance(suite, list):
1785 # Don't care about this, it's a composition, which shouldn't include a
1786 # swarming mixin.
1787 continue
1788
1789 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561790 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011791 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071792
Zhaoyang Li9da047d52021-05-10 21:31:441793 for variant in self.variants:
1794 # Unpack the variant from variants.pyl if it's string based.
1795 if isinstance(variant, str):
1796 variant = self.variants[variant]
1797 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1798
Stephen Martinisb72f6d22018-10-04 23:29:011799 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071800 if missing_mixins:
1801 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1802 ' referenced in a waterfall, machine, or test suite.' % (
1803 str(missing_mixins)))
1804
Jeff Yoonda581c32020-03-06 03:56:051805 # All variant references must be referenced
1806 seen_variants = set()
1807 for suite in self.test_suites.values():
1808 if isinstance(suite, list):
1809 continue
1810
1811 for test in suite.values():
1812 if isinstance(test, dict):
1813 for variant in test.get('variants', []):
1814 if isinstance(variant, str):
1815 seen_variants.add(variant)
1816
1817 missing_variants = set(self.variants.keys()) - seen_variants
1818 if missing_variants:
1819 raise BBGenErr('The following variants were unreferenced: %s. They must '
1820 'be referenced in a matrix test suite under the variants '
1821 'key.' % str(missing_variants))
1822
Stephen Martinis54d64ad2018-09-21 22:16:201823
Garrett Beaty79339e182023-04-10 20:45:471824 def type_assert(self, node, typ, file_path, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201825 """Asserts that the Python AST node |node| is of type |typ|.
1826
1827 If verbose is set, it prints out some helpful context lines, showing where
1828 exactly the error occurred in the file.
1829 """
1830 if not isinstance(node, typ):
1831 if verbose:
Garrett Beaty79339e182023-04-10 20:45:471832 lines = [""] + self.read_file(file_path).splitlines()
Stephen Martinis54d64ad2018-09-21 22:16:201833
1834 context = 2
1835 lines_start = max(node.lineno - context, 0)
1836 # Add one to include the last line
1837 lines_end = min(node.lineno + context, len(lines)) + 1
Garrett Beaty79339e182023-04-10 20:45:471838 lines = itertools.chain(
1839 ['== %s ==\n' % file_path],
1840 ["<snip>\n"],
1841 [
1842 '%d %s' % (lines_start + i, line)
1843 for i, line in enumerate(lines[lines_start:lines_start +
1844 context])
1845 ],
1846 ['-' * 80 + '\n'],
1847 ['%d %s' % (node.lineno, lines[node.lineno])],
1848 [
1849 '-' * (node.col_offset + 3) + '^' + '-' *
1850 (80 - node.col_offset - 4) + '\n'
1851 ],
1852 [
1853 '%d %s' % (node.lineno + 1 + i, line)
1854 for i, line in enumerate(lines[node.lineno + 1:lines_end])
1855 ],
1856 ["<snip>\n"],
Stephen Martinis54d64ad2018-09-21 22:16:201857 )
1858 # Print out a useful message when a type assertion fails.
1859 for l in lines:
1860 self.print_line(l.strip())
1861
1862 node_dumped = ast.dump(node, annotate_fields=False)
1863 # If the node is huge, truncate it so everything fits in a terminal
1864 # window.
1865 if len(node_dumped) > 60: # pragma: no cover
1866 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1867 raise BBGenErr(
Garrett Beaty807011ab2023-04-12 00:52:391868 'Invalid .pyl file \'%s\'. Python AST node %r on line %s expected to'
Garrett Beaty79339e182023-04-10 20:45:471869 ' be %s, is %s' %
1870 (file_path, node_dumped, node.lineno, typ, type(node)))
Stephen Martinis54d64ad2018-09-21 22:16:201871
Garrett Beaty79339e182023-04-10 20:45:471872 def check_ast_list_formatted(self,
1873 keys,
1874 file_path,
1875 verbose,
Stephen Martinis1384ff92020-01-07 19:52:151876 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531877 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201878
Stephen Martinis5bef0fc2020-01-06 22:47:531879 Currently only checks to ensure they're correctly sorted, and that there
1880 are no duplicates.
1881
1882 Args:
1883 keys: An python list of AST nodes.
1884
1885 It's a list of AST nodes instead of a list of strings because
1886 when verbose is set, it tries to print out context of where the
1887 diffs are in the file.
Garrett Beaty79339e182023-04-10 20:45:471888 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531889 verbose: If set, print out diff information about how the keys are
1890 incorrectly formatted.
1891 check_sorting: If true, checks if the list is sorted.
1892 Returns:
1893 If the keys are correctly formatted.
1894 """
1895 if not keys:
1896 return True
1897
1898 assert isinstance(keys[0], ast.Str)
1899
1900 keys_strs = [k.s for k in keys]
1901 # Keys to diff against. Used below.
1902 keys_to_diff_against = None
1903 # If the list is properly formatted.
1904 list_formatted = True
1905
1906 # Duplicates are always bad.
1907 if len(set(keys_strs)) != len(keys_strs):
1908 list_formatted = False
1909 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1910
1911 if check_sorting and sorted(keys_strs) != keys_strs:
1912 list_formatted = False
1913 if list_formatted:
1914 return True
1915
1916 if verbose:
1917 line_num = keys[0].lineno
1918 keys = [k.s for k in keys]
1919 if check_sorting:
1920 # If we have duplicates, sorting this will take care of it anyways.
1921 keys_to_diff_against = sorted(set(keys))
1922 # else, keys_to_diff_against is set above already
1923
1924 self.print_line('=' * 80)
1925 self.print_line('(First line of keys is %s)' % line_num)
Garrett Beaty79339e182023-04-10 20:45:471926 for line in difflib.context_diff(keys,
1927 keys_to_diff_against,
1928 fromfile='current (%r)' % file_path,
1929 tofile='sorted',
1930 lineterm=''):
Stephen Martinis5bef0fc2020-01-06 22:47:531931 self.print_line(line)
1932 self.print_line('=' * 80)
1933
1934 return False
1935
Garrett Beaty79339e182023-04-10 20:45:471936 def check_ast_dict_formatted(self, node, file_path, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531937 """Checks if an ast dictionary's keys are correctly formatted.
1938
1939 Just a simple wrapper around check_ast_list_formatted.
1940 Args:
1941 node: An AST node. Assumed to be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471942 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531943 verbose: If set, print out diff information about how the keys are
1944 incorrectly formatted.
1945 check_sorting: If true, checks if the list is sorted.
1946 Returns:
1947 If the dictionary is correctly formatted.
1948 """
Stephen Martinis54d64ad2018-09-21 22:16:201949 keys = []
1950 # The keys of this dict are ordered as ordered in the file; normal python
1951 # dictionary keys are given an arbitrary order, but since we parsed the
1952 # file itself, the order as given in the file is preserved.
1953 for key in node.keys:
Garrett Beaty79339e182023-04-10 20:45:471954 self.type_assert(key, ast.Str, file_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531955 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201956
Garrett Beaty79339e182023-04-10 20:45:471957 return self.check_ast_list_formatted(keys, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181958
1959 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201960 # TODO(https://crbug.com/886993): Add the ability for this script to
1961 # actually format the files, rather than just complain if they're
1962 # incorrectly formatted.
1963 bad_files = set()
Garrett Beaty79339e182023-04-10 20:45:471964
1965 def parse_file(file_path):
Stephen Martinis5bef0fc2020-01-06 22:47:531966 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201967
Stephen Martinis5bef0fc2020-01-06 22:47:531968 Returns an AST node representing the value in the pyl file."""
Garrett Beaty79339e182023-04-10 20:45:471969 parsed = ast.parse(self.read_file(file_path))
Stephen Martinisf83893722018-09-19 00:02:181970
Stephen Martinisf83893722018-09-19 00:02:181971 # Must be a module.
Garrett Beaty79339e182023-04-10 20:45:471972 self.type_assert(parsed, ast.Module, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181973 module = parsed.body
1974
1975 # Only one expression in the module.
Garrett Beaty79339e182023-04-10 20:45:471976 self.type_assert(module, list, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181977 if len(module) != 1: # pragma: no cover
Garrett Beaty79339e182023-04-10 20:45:471978 raise BBGenErr('Invalid .pyl file %s' % file_path)
Stephen Martinisf83893722018-09-19 00:02:181979 expr = module[0]
Garrett Beaty79339e182023-04-10 20:45:471980 self.type_assert(expr, ast.Expr, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181981
Stephen Martinis5bef0fc2020-01-06 22:47:531982 return expr.value
1983
1984 # Handle this separately
Garrett Beaty79339e182023-04-10 20:45:471985 value = parse_file(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531986 # Value should be a list.
Garrett Beaty79339e182023-04-10 20:45:471987 self.type_assert(value, ast.List, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531988
1989 keys = []
Joshua Hood56c673c2022-03-02 20:29:331990 for elm in value.elts:
Garrett Beaty79339e182023-04-10 20:45:471991 self.type_assert(elm, ast.Dict, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531992 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331993 for key, val in zip(elm.keys, elm.values):
Garrett Beaty79339e182023-04-10 20:45:471994 self.type_assert(key, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531995 if key.s == 'machines':
Garrett Beaty79339e182023-04-10 20:45:471996 if not self.check_ast_dict_formatted(
1997 val, self.args.waterfalls_pyl_path, verbose):
1998 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531999
2000 if key.s == "name":
Garrett Beaty79339e182023-04-10 20:45:472001 self.type_assert(val, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:532002 waterfall_name = val
2003 assert waterfall_name
2004 keys.append(waterfall_name)
2005
Garrett Beaty79339e182023-04-10 20:45:472006 if not self.check_ast_list_formatted(keys, self.args.waterfalls_pyl_path,
2007 verbose):
2008 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:532009
Garrett Beaty79339e182023-04-10 20:45:472010 for file_path in (
2011 self.args.mixins_pyl_path,
2012 self.args.test_suites_pyl_path,
2013 self.args.test_suite_exceptions_pyl_path,
Stephen Martinis5bef0fc2020-01-06 22:47:532014 ):
Garrett Beaty79339e182023-04-10 20:45:472015 value = parse_file(file_path)
Stephen Martinisf83893722018-09-19 00:02:182016 # Value should be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:472017 self.type_assert(value, ast.Dict, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:182018
Garrett Beaty79339e182023-04-10 20:45:472019 if not self.check_ast_dict_formatted(value, file_path, verbose):
2020 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:532021
Garrett Beaty79339e182023-04-10 20:45:472022 if file_path == self.args.test_suites_pyl_path:
Jeff Yoon8154e582019-12-03 23:30:012023 expected_keys = ['basic_suites',
2024 'compound_suites',
2025 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:202026 actual_keys = [node.s for node in value.keys]
2027 assert all(key in expected_keys for key in actual_keys), (
Garrett Beaty79339e182023-04-10 20:45:472028 'Invalid %r file; expected keys %r, got %r' %
2029 (file_path, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:332030 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:202031 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:012032 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:202033 for suite_group in suite_dicts:
Garrett Beaty79339e182023-04-10 20:45:472034 if not self.check_ast_dict_formatted(suite_group, file_path, verbose):
2035 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:182036
Stephen Martinis5bef0fc2020-01-06 22:47:532037 for key, suite in zip(value.keys, value.values):
2038 # The compound suites are checked in
2039 # 'check_composition_type_test_suites()'
2040 if key.s == 'basic_suites':
2041 for group in suite.values:
Garrett Beaty79339e182023-04-10 20:45:472042 if not self.check_ast_dict_formatted(group, file_path, verbose):
2043 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:532044 break
Stephen Martinis54d64ad2018-09-21 22:16:202045
Garrett Beaty79339e182023-04-10 20:45:472046 elif file_path == self.args.test_suite_exceptions_pyl_path:
Stephen Martinis5bef0fc2020-01-06 22:47:532047 # Check the values for each test.
2048 for test in value.values:
2049 for kind, node in zip(test.keys, test.values):
2050 if isinstance(node, ast.Dict):
Garrett Beaty79339e182023-04-10 20:45:472051 if not self.check_ast_dict_formatted(node, file_path, verbose):
2052 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:532053 elif kind.s == 'remove_from':
2054 # Don't care about sorting; these are usually grouped, since the
2055 # same bug can affect multiple builders. Do want to make sure
2056 # there aren't duplicates.
Garrett Beaty79339e182023-04-10 20:45:472057 if not self.check_ast_list_formatted(
2058 node.elts, file_path, verbose, check_sorting=False):
2059 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:182060
2061 if bad_files:
2062 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:202063 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:532064 'unsorted, or have duplicates. Re-run this with --verbose to see '
2065 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:182066
Kenneth Russelleb60cbd22017-12-05 07:54:282067 def check_output_file_consistency(self, verbose=False):
2068 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012069 # All waterfalls/bucket .json files must have been written
2070 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:282071 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012072 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:162073 outputs = self.generate_outputs()
2074 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:012075 expected = self.jsonify(expected_contents)
Garrett Beaty79339e182023-04-10 20:45:472076 file_path = os.path.join(self.args.output_dir, filename + '.json')
Ben Pastenef21cda32023-03-30 22:00:572077 current = self.read_file(file_path)
Kenneth Russelleb60cbd22017-12-05 07:54:282078 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:012079 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:322080 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:012081 self.print_line('File ' + filename +
2082 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:322083 'contents:')
2084 for line in difflib.unified_diff(
2085 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:502086 current.splitlines(),
2087 fromfile='expected', tofile='current'):
2088 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:012089
2090 if ungenerated_files:
2091 raise BBGenErr(
2092 'The following files have not been properly '
2093 'autogenerated by generate_buildbot_json.py: ' +
2094 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:282095
Dirk Pranke772f55f2021-04-28 04:51:162096 for builder_group, builders in outputs.items():
2097 for builder, step_types in builders.items():
2098 for step_data in step_types.get('gtest_tests', []):
2099 step_name = step_data.get('name', step_data['test'])
2100 self._check_swarming_config(builder_group, builder, step_name,
2101 step_data)
2102 for step_data in step_types.get('isolated_scripts', []):
2103 step_name = step_data.get('name', step_data['isolate_name'])
2104 self._check_swarming_config(builder_group, builder, step_name,
2105 step_data)
2106
2107 def _check_swarming_config(self, filename, builder, step_name, step_data):
Ben Pastene338f56b2023-03-31 21:24:452108 # TODO(crbug.com/1203436): Ensure all swarming tests specify cpu, not
Dirk Pranke772f55f2021-04-28 04:51:162109 # just mac tests.
Garrett Beatybb18d532023-06-26 22:16:332110 if 'swarming' in step_data:
Dirk Pranke772f55f2021-04-28 04:51:162111 dimension_sets = step_data['swarming'].get('dimension_sets')
2112 if not dimension_sets:
Ben Pastene338f56b2023-03-31 21:24:452113 raise BBGenErr('%s: %s / %s : os must be specified for all '
Dirk Pranke772f55f2021-04-28 04:51:162114 'swarmed tests' % (filename, builder, step_name))
2115 for s in dimension_sets:
Ben Pastene338f56b2023-03-31 21:24:452116 if not s.get('os'):
2117 raise BBGenErr('%s: %s / %s : os must be specified for all '
2118 'swarmed tests' % (filename, builder, step_name))
2119 if 'Mac' in s.get('os') and not s.get('cpu'):
2120 raise BBGenErr('%s: %s / %s : cpu must be specified for mac '
Dirk Pranke772f55f2021-04-28 04:51:162121 'swarmed tests' % (filename, builder, step_name))
2122
Kenneth Russelleb60cbd22017-12-05 07:54:282123 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502124 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282125 self.check_output_file_consistency(verbose) # pragma: no cover
2126
Karen Qiane24b7ee2019-02-12 23:37:062127 def does_test_match(self, test_info, params_dict):
2128 """Checks to see if the test matches the parameters given.
2129
2130 Compares the provided test_info with the params_dict to see
2131 if the bot matches the parameters given. If so, returns True.
2132 Else, returns false.
2133
2134 Args:
2135 test_info (dict): Information about a specific bot provided
2136 in the format shown in waterfalls.pyl
2137 params_dict (dict): Dictionary of parameters and their values
2138 to look for in the bot
2139 Ex: {
2140 'device_os':'android',
2141 '--flag':True,
2142 'mixins': ['mixin1', 'mixin2'],
2143 'ex_key':'ex_value'
2144 }
2145
2146 """
2147 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2148 'kvm', 'pool', 'integrity'] # dimension parameters
2149 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2150 'can_use_on_swarming_builders']
2151 for param in params_dict:
2152 # if dimension parameter
2153 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2154 if not 'swarming' in test_info:
2155 return False
2156 swarming = test_info['swarming']
2157 if param in SWARMING_PARAMS:
2158 if not param in swarming:
2159 return False
2160 if not str(swarming[param]) == params_dict[param]:
2161 return False
2162 else:
2163 if not 'dimension_sets' in swarming:
2164 return False
2165 d_set = swarming['dimension_sets']
2166 # only looking at the first dimension set
2167 if not param in d_set[0]:
2168 return False
2169 if not d_set[0][param] == params_dict[param]:
2170 return False
2171
2172 # if flag
2173 elif param.startswith('--'):
2174 if not 'args' in test_info:
2175 return False
2176 if not param in test_info['args']:
2177 return False
2178
2179 # not dimension parameter/flag/mixin
2180 else:
2181 if not param in test_info:
2182 return False
2183 if not test_info[param] == params_dict[param]:
2184 return False
2185 return True
2186 def error_msg(self, msg):
2187 """Prints an error message.
2188
2189 In addition to a catered error message, also prints
2190 out where the user can find more help. Then, program exits.
2191 """
2192 self.print_line(msg + (' If you need more information, ' +
2193 'please run with -h or --help to see valid commands.'))
2194 sys.exit(1)
2195
2196 def find_bots_that_run_test(self, test, bots):
2197 matching_bots = []
2198 for bot in bots:
2199 bot_info = bots[bot]
2200 tests = self.flatten_tests_for_bot(bot_info)
2201 for test_info in tests:
2202 test_name = ""
2203 if 'name' in test_info:
2204 test_name = test_info['name']
2205 elif 'test' in test_info:
2206 test_name = test_info['test']
2207 if not test_name == test:
2208 continue
2209 matching_bots.append(bot)
2210 return matching_bots
2211
2212 def find_tests_with_params(self, tests, params_dict):
2213 matching_tests = []
2214 for test_name in tests:
2215 test_info = tests[test_name]
2216 if not self.does_test_match(test_info, params_dict):
2217 continue
2218 if not test_name in matching_tests:
2219 matching_tests.append(test_name)
2220 return matching_tests
2221
2222 def flatten_waterfalls_for_query(self, waterfalls):
2223 bots = {}
2224 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012225 waterfall_tests = self.generate_output_tests(waterfall)
2226 for bot in waterfall_tests:
2227 bot_info = waterfall_tests[bot]
2228 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062229 return bots
2230
2231 def flatten_tests_for_bot(self, bot_info):
2232 """Returns a list of flattened tests.
2233
2234 Returns a list of tests not grouped by test category
2235 for a specific bot.
2236 """
2237 TEST_CATS = self.get_test_generator_map().keys()
2238 tests = []
2239 for test_cat in TEST_CATS:
2240 if not test_cat in bot_info:
2241 continue
2242 test_cat_tests = bot_info[test_cat]
2243 tests = tests + test_cat_tests
2244 return tests
2245
2246 def flatten_tests_for_query(self, test_suites):
2247 """Returns a flattened dictionary of tests.
2248
2249 Returns a dictionary of tests associate with their
2250 configuration, not grouped by their test suite.
2251 """
2252 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232253 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062254 for test in test_suite:
2255 test_info = test_suite[test]
2256 test_name = test
2257 if 'name' in test_info:
2258 test_name = test_info['name']
2259 tests[test_name] = test_info
2260 return tests
2261
2262 def parse_query_filter_params(self, params):
2263 """Parses the filter parameters.
2264
2265 Creates a dictionary from the parameters provided
2266 to filter the bot array.
2267 """
2268 params_dict = {}
2269 for p in params:
2270 # flag
2271 if p.startswith("--"):
2272 params_dict[p] = True
2273 else:
2274 pair = p.split(":")
2275 if len(pair) != 2:
2276 self.error_msg('Invalid command.')
2277 # regular parameters
2278 if pair[1].lower() == "true":
2279 params_dict[pair[0]] = True
2280 elif pair[1].lower() == "false":
2281 params_dict[pair[0]] = False
2282 else:
2283 params_dict[pair[0]] = pair[1]
2284 return params_dict
2285
2286 def get_test_suites_dict(self, bots):
2287 """Returns a dictionary of bots and their tests.
2288
2289 Returns a dictionary of bots and a list of their associated tests.
2290 """
2291 test_suite_dict = dict()
2292 for bot in bots:
2293 bot_info = bots[bot]
2294 tests = self.flatten_tests_for_bot(bot_info)
2295 test_suite_dict[bot] = tests
2296 return test_suite_dict
2297
2298 def output_query_result(self, result, json_file=None):
2299 """Outputs the result of the query.
2300
2301 If a json file parameter name is provided, then
2302 the result is output into the json file. If not,
2303 then the result is printed to the console.
2304 """
2305 output = json.dumps(result, indent=2)
2306 if json_file:
2307 self.write_file(json_file, output)
2308 else:
2309 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062310
Joshua Hood56c673c2022-03-02 20:29:332311 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062312 def query(self, args):
2313 """Queries tests or bots.
2314
2315 Depending on the arguments provided, outputs a json of
2316 tests or bots matching the appropriate optional parameters provided.
2317 """
2318 # split up query statement
2319 query = args.query.split('/')
2320 self.load_configuration_files()
2321 self.resolve_configuration_files()
2322
2323 # flatten bots json
2324 tests = self.test_suites
2325 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2326
2327 cmd_class = query[0]
2328
2329 # For queries starting with 'bots'
2330 if cmd_class == "bots":
2331 if len(query) == 1:
2332 return self.output_query_result(bots, args.json)
2333 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332334 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062335 if query[1] == 'tests':
2336 test_suites_dict = self.get_test_suites_dict(bots)
2337 return self.output_query_result(test_suites_dict, args.json)
Joshua Hood56c673c2022-03-02 20:29:332338 self.error_msg("This query should be in the format: bots/tests.")
Karen Qiane24b7ee2019-02-12 23:37:062339
2340 else:
2341 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2342 % str(len(query)-1))
2343
2344 # For queries starting with 'bot'
2345 elif cmd_class == "bot":
2346 if not len(query) == 2 and not len(query) == 3:
2347 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2348 % str(len(query)-1))
2349 bot_id = query[1]
2350 if not bot_id in bots:
2351 self.error_msg("No bot named '" + bot_id + "' found.")
2352 bot_info = bots[bot_id]
2353 if len(query) == 2:
2354 return self.output_query_result(bot_info, args.json)
2355 if not query[2] == 'tests':
2356 self.error_msg("The query should be in the format:" +
2357 "bot/<bot-name>/tests.")
2358
2359 bot_tests = self.flatten_tests_for_bot(bot_info)
2360 return self.output_query_result(bot_tests, args.json)
2361
2362 # For queries starting with 'tests'
2363 elif cmd_class == "tests":
2364 if not len(query) == 1 and not len(query) == 2:
2365 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2366 % str(len(query)-1))
2367 flattened_tests = self.flatten_tests_for_query(tests)
2368 if len(query) == 1:
2369 return self.output_query_result(flattened_tests, args.json)
2370
2371 # create params dict
2372 params = query[1].split('&')
2373 params_dict = self.parse_query_filter_params(params)
2374 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2375 return self.output_query_result(matching_bots)
2376
2377 # For queries starting with 'test'
2378 elif cmd_class == "test":
2379 if not len(query) == 2 and not len(query) == 3:
2380 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2381 % str(len(query)-1))
2382 test_id = query[1]
2383 if len(query) == 2:
2384 flattened_tests = self.flatten_tests_for_query(tests)
2385 for test in flattened_tests:
2386 if test == test_id:
2387 return self.output_query_result(flattened_tests[test], args.json)
2388 self.error_msg("There is no test named %s." % test_id)
2389 if not query[2] == 'bots':
2390 self.error_msg("The query should be in the format: " +
2391 "test/<test-name>/bots")
2392 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2393 return self.output_query_result(bots_for_test)
2394
2395 else:
2396 self.error_msg("Your command did not match any valid commands." +
2397 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Joshua Hood56c673c2022-03-02 20:29:332398 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282399
Garrett Beaty1afaccc2020-06-25 19:58:152400 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282401 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502402 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062403 elif self.args.query:
2404 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282405 else:
Greg Gutermanf60eb052020-03-12 17:40:012406 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282407 return 0
2408
2409if __name__ == "__main__": # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152410 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2411 sys.exit(generator.main())