blob: 7c31d02045e3dd0aa4f801d9a8769c96a07336bb [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')
Garrett Beaty96802d02023-07-07 14:18:05422 args.mixins_pyl_path = absolute_file_path('mixins.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13423 args.test_suites_pyl_path = absolute_file_path('test_suites.pyl')
424 args.test_suite_exceptions_pyl_path = absolute_file_path(
Garrett Beaty79339e182023-04-10 20:45:47425 'test_suite_exceptions.pyl')
Stephanie Kim572b43c02023-04-13 14:24:13426 args.gn_isolate_map_pyl_path = absolute_file_path('gn_isolate_map.pyl')
427 args.variants_pyl_path = absolute_file_path('variants.pyl')
428 args.autoshard_exceptions_json_path = absolute_file_path(
429 'autoshard_exceptions.json')
Garrett Beaty79339e182023-04-10 20:45:47430
431 return args
Kenneth Russelleb60cbd22017-12-05 07:54:28432
Stephen Martinis7eb8b612018-09-21 00:17:50433 def print_line(self, line):
434 # Exists so that tests can mock
Jamie Madillcf4f8c72021-05-20 19:24:23435 print(line) # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:50436
Kenneth Russelleb60cbd22017-12-05 07:54:28437 def read_file(self, relative_path):
Garrett Beaty79339e182023-04-10 20:45:47438 with open(relative_path) as fp:
Garrett Beaty1afaccc2020-06-25 19:58:15439 return fp.read()
Kenneth Russelleb60cbd22017-12-05 07:54:28440
Garrett Beaty79339e182023-04-10 20:45:47441 def write_file(self, file_path, contents):
442 with open(file_path, 'w') as fp:
443 fp.write(contents)
Zhiling Huangbe008172018-03-08 19:13:11444
Joshua Hood56c673c2022-03-02 20:29:33445 # pylint: disable=inconsistent-return-statements
Garrett Beaty79339e182023-04-10 20:45:47446 def load_pyl_file(self, pyl_file_path):
Kenneth Russelleb60cbd22017-12-05 07:54:28447 try:
Garrett Beaty79339e182023-04-10 20:45:47448 return ast.literal_eval(self.read_file(pyl_file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:28449 except (SyntaxError, ValueError) as e: # pragma: no cover
Josip Sokcevic7110fb382023-06-06 01:05:29450 raise BBGenErr('Failed to parse pyl file "%s": %s' %
451 (pyl_file_path, e)) from e
Joshua Hood56c673c2022-03-02 20:29:33452 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:28453
Kenneth Russell8a386d42018-06-02 09:48:01454 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
455 # Currently it is only mandatory for bots which run GPU tests. Change these to
456 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28457 def is_android(self, tester_config):
458 return tester_config.get('os_type') == 'android'
459
Ben Pastenea9e583b2019-01-16 02:57:26460 def is_chromeos(self, tester_config):
461 return tester_config.get('os_type') == 'chromeos'
462
Chong Guc2ca5d02022-01-11 19:52:17463 def is_fuchsia(self, tester_config):
464 return tester_config.get('os_type') == 'fuchsia'
465
Brian Sheedy781c8ca42021-03-08 22:03:21466 def is_lacros(self, tester_config):
467 return tester_config.get('os_type') == 'lacros'
468
Kenneth Russell8a386d42018-06-02 09:48:01469 def is_linux(self, tester_config):
470 return tester_config.get('os_type') == 'linux'
471
Kai Ninomiya40de9f52019-10-18 21:38:49472 def is_mac(self, tester_config):
473 return tester_config.get('os_type') == 'mac'
474
475 def is_win(self, tester_config):
476 return tester_config.get('os_type') == 'win'
477
478 def is_win64(self, tester_config):
479 return (tester_config.get('os_type') == 'win' and
480 tester_config.get('browser_config') == 'release_x64')
481
Ben Pastene5f231cf22022-05-05 18:03:07482 def add_variant_to_test_name(self, test_name, variant_id):
483 return '{} {}'.format(test_name, variant_id)
484
485 def remove_variant_from_test_name(self, test_name, variant_id):
486 return test_name.split(variant_id)[0].strip()
487
Kenneth Russelleb60cbd22017-12-05 07:54:28488 def get_exception_for_test(self, test_name, test_config):
489 # gtests may have both "test" and "name" fields, and usually, if the "name"
490 # field is specified, it means that the same test is being repurposed
491 # multiple times with different command line arguments. To handle this case,
492 # prefer to lookup per the "name" field of the test itself, as opposed to
493 # the "test_name", which is actually the "test" field.
494 if 'name' in test_config:
495 return self.exceptions.get(test_config['name'])
Joshua Hood56c673c2022-03-02 20:29:33496 return self.exceptions.get(test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28497
Nico Weberb0b3f5862018-07-13 18:45:15498 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28499 # Currently, the only reason a test should not run on a given tester is that
500 # it's in the exceptions. (Once the GPU waterfall generation script is
501 # incorporated here, the rules will become more complex.)
502 exception = self.get_exception_for_test(test_name, test_config)
503 if not exception:
504 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28505 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28506 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28507 if remove_from:
508 if tester_name in remove_from:
509 return False
510 # TODO(kbr): this code path was added for some tests (including
511 # android_webview_unittests) on one machine (Nougat Phone
512 # Tester) which exists with the same name on two waterfalls,
513 # chromium.android and chromium.fyi; the tests are run on one
514 # but not the other. Once the bots are all uniquely named (a
515 # different ongoing project) this code should be removed.
516 # TODO(kbr): add coverage.
517 return (tester_name + ' ' + waterfall['name']
518 not in remove_from) # pragma: no cover
519 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28520
Nico Weber79dc5f6852018-07-13 19:38:49521 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28522 exception = self.get_exception_for_test(test_name, test)
523 if not exception:
524 return None
Nico Weber79dc5f6852018-07-13 19:38:49525 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28526
Brian Sheedye6ea0ee2019-07-11 02:54:37527 def get_test_replacements(self, test, test_name, tester_name):
528 exception = self.get_exception_for_test(test_name, test)
529 if not exception:
530 return None
531 return exception.get('replacements', {}).get(tester_name)
532
Kenneth Russell8a386d42018-06-02 09:48:01533 def merge_command_line_args(self, arr, prefix, splitter):
534 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01535 idx = 0
536 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01537 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01538 while idx < len(arr):
539 flag = arr[idx]
540 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01541 if flag.startswith(prefix):
542 arg = flag[prefix_len:]
543 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01544 if first_idx < 0:
545 first_idx = idx
546 else:
547 delete_current_entry = True
548 if delete_current_entry:
549 del arr[idx]
550 else:
551 idx += 1
552 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01553 arr[first_idx] = prefix + splitter.join(accumulated_args)
554 return arr
555
556 def maybe_fixup_args_array(self, arr):
557 # The incoming array of strings may be an array of command line
558 # arguments. To make it easier to turn on certain features per-bot or
559 # per-test-suite, look specifically for certain flags and merge them
560 # appropriately.
561 # --enable-features=Feature1 --enable-features=Feature2
562 # are merged to:
563 # --enable-features=Feature1,Feature2
564 # and:
565 # --extra-browser-args=arg1 --extra-browser-args=arg2
566 # are merged to:
567 # --extra-browser-args=arg1 arg2
568 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
569 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Yuly Novikov8c487e72020-10-16 20:00:29570 arr = self.merge_command_line_args(arr, '--test-launcher-filter-file=', ';')
Cameron Higgins971f0b92023-01-03 18:05:09571 arr = self.merge_command_line_args(arr, '--extra-app-args=', ',')
Kenneth Russell650995a2018-05-03 21:17:01572 return arr
573
Brian Sheedy910cda82022-07-19 11:58:34574 def substitute_magic_args(self, test_config, tester_name, tester_config):
Brian Sheedya31578e2020-05-18 20:24:36575 """Substitutes any magic substitution args present in |test_config|.
576
577 Substitutions are done in-place.
578
579 See buildbot_json_magic_substitutions.py for more information on this
580 feature.
581
582 Args:
583 test_config: A dict containing a configuration for a specific test on
584 a specific builder, e.g. the output of update_and_cleanup_test.
Brian Sheedy5f173bb2021-11-24 00:45:54585 tester_name: A string containing the name of the tester that |test_config|
586 came from.
Brian Sheedy910cda82022-07-19 11:58:34587 tester_config: A dict containing the configuration for the builder that
588 |test_config| is for.
Brian Sheedya31578e2020-05-18 20:24:36589 """
590 substituted_array = []
Brian Sheedyba13cf522022-09-13 21:00:09591 original_args = test_config.get('args', [])
592 for arg in original_args:
Brian Sheedya31578e2020-05-18 20:24:36593 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
594 function = arg.replace(
595 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
596 if hasattr(magic_substitutions, function):
597 substituted_array.extend(
Brian Sheedy910cda82022-07-19 11:58:34598 getattr(magic_substitutions, function)(test_config, tester_name,
599 tester_config))
Brian Sheedya31578e2020-05-18 20:24:36600 else:
601 raise BBGenErr(
602 'Magic substitution function %s does not exist' % function)
603 else:
604 substituted_array.append(arg)
Brian Sheedyba13cf522022-09-13 21:00:09605 if substituted_array != original_args:
Brian Sheedya31578e2020-05-18 20:24:36606 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
607
Garrett Beaty8d6708c2023-07-20 17:20:41608 def dictionary_merge(self, a, b, path=None):
Kenneth Russelleb60cbd22017-12-05 07:54:28609 """http://stackoverflow.com/questions/7204805/
610 python-dictionaries-of-dictionaries-merge
611 merges b into a
612 """
613 if path is None:
614 path = []
615 for key in b:
Garrett Beaty8d6708c2023-07-20 17:20:41616 if key not in a:
617 if b[key] is not None:
618 a[key] = b[key]
619 continue
620
621 if isinstance(a[key], dict) and isinstance(b[key], dict):
622 self.dictionary_merge(a[key], b[key], path + [str(key)])
623 elif a[key] == b[key]:
624 pass # same leaf value
625 elif isinstance(a[key], list) and isinstance(b[key], list):
626 # Args arrays are lists of strings. Just concatenate them,
627 # and don't sort them, in order to keep some needed
628 # arguments adjacent (like --timeout-ms [arg], etc.)
629 if all(isinstance(x, str) for x in itertools.chain(a[key], b[key])):
630 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russelleb60cbd22017-12-05 07:54:28631 else:
Garrett Beaty8d6708c2023-07-20 17:20:41632 # TODO(kbr): this only works properly if the two arrays are
633 # the same length, which is currently always the case in the
634 # swarming dimension_sets that we have to merge. It will fail
635 # to merge / override 'args' arrays which are different
636 # length.
637 for idx in range(len(b[key])):
638 try:
639 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
640 path +
641 [str(key), str(idx)])
642 except (IndexError, TypeError) as e: # pragma: no cover
643 raise BBGenErr('Error merging lists by key "%s" from source %s '
644 'into target %s at index %s. Verify target list '
645 'length is equal or greater than source' %
646 (str(key), str(b), str(a), str(idx))) from e
647 elif b[key] is None:
648 del a[key]
649 else:
Kenneth Russelleb60cbd22017-12-05 07:54:28650 a[key] = b[key]
Garrett Beaty8d6708c2023-07-20 17:20:41651
Kenneth Russelleb60cbd22017-12-05 07:54:28652 return a
653
John Budorickab108712018-09-01 00:12:21654 def initialize_args_for_test(
655 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21656 args = []
657 args.extend(generated_test.get('args', []))
658 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22659
Kenneth Russell8a386d42018-06-02 09:48:01660 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21661 val = generated_test.pop(key, [])
662 if fn(tester_config):
663 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01664
665 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
Brian Sheedy781c8ca42021-03-08 22:03:21666 add_conditional_args('lacros_args', self.is_lacros)
Kenneth Russell8a386d42018-06-02 09:48:01667 add_conditional_args('linux_args', self.is_linux)
668 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36669 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49670 add_conditional_args('mac_args', self.is_mac)
671 add_conditional_args('win_args', self.is_win)
672 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01673
John Budorickab108712018-09-01 00:12:21674 for key in additional_arg_keys or []:
675 args.extend(generated_test.pop(key, []))
676 args.extend(tester_config.get(key, []))
677
678 if args:
679 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01680
Kenneth Russelleb60cbd22017-12-05 07:54:28681 def initialize_swarming_dictionary_for_test(self, generated_test,
682 tester_config):
683 if 'swarming' not in generated_test:
684 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28685 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
686 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38687 'can_use_on_swarming_builders': tester_config.get('use_swarming',
688 True)
Dirk Pranke81ff51c2017-12-09 19:24:28689 })
Kenneth Russelleb60cbd22017-12-05 07:54:28690 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03691 if ('dimension_sets' not in generated_test['swarming'] and
692 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28693 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
694 tester_config['swarming']['dimension_sets'])
695 self.dictionary_merge(generated_test['swarming'],
696 tester_config['swarming'])
Brian Sheedybc984e242021-04-21 23:44:51697 # Apply any platform-specific Swarming dimensions after the generic ones.
Kenneth Russelleb60cbd22017-12-05 07:54:28698 if 'android_swarming' in generated_test:
699 if self.is_android(tester_config): # pragma: no cover
700 self.dictionary_merge(
701 generated_test['swarming'],
702 generated_test['android_swarming']) # pragma: no cover
703 del generated_test['android_swarming'] # pragma: no cover
Brian Sheedybc984e242021-04-21 23:44:51704 if 'chromeos_swarming' in generated_test:
705 if self.is_chromeos(tester_config): # pragma: no cover
706 self.dictionary_merge(
707 generated_test['swarming'],
708 generated_test['chromeos_swarming']) # pragma: no cover
709 del generated_test['chromeos_swarming'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28710
711 def clean_swarming_dictionary(self, swarming_dict):
712 # Clean out redundant entries from a test's "swarming" dictionary.
713 # This is really only needed to retain 100% parity with the
714 # handwritten JSON files, and can be removed once all the files are
715 # autogenerated.
716 if 'shards' in swarming_dict:
717 if swarming_dict['shards'] == 1: # pragma: no cover
718 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24719 if 'hard_timeout' in swarming_dict:
720 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
721 del swarming_dict['hard_timeout'] # pragma: no cover
Garrett Beatybb18d532023-06-26 22:16:33722 del swarming_dict['can_use_on_swarming_builders']
Kenneth Russelleb60cbd22017-12-05 07:54:28723
Stephen Martinis0382bc12018-09-17 22:29:07724 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
725 waterfall):
726 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01727 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07728 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28729 # See if there are any exceptions that need to be merged into this
730 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49731 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28732 if modifications:
733 test = self.dictionary_merge(test, modifications)
Garrett Beatybfeff8f2023-06-16 18:57:25734 if (swarming_dict := test.get('swarming')) is not None:
Garrett Beatybb18d532023-06-26 22:16:33735 if swarming_dict.get('can_use_on_swarming_builders'):
Garrett Beatybfeff8f2023-06-16 18:57:25736 self.clean_swarming_dictionary(swarming_dict)
737 else:
738 del test['swarming']
Ben Pastenee012aea42019-05-14 22:32:28739 # Ensure all Android Swarming tests run only on userdebug builds if another
740 # build type was not specified.
741 if 'swarming' in test and self.is_android(tester_config):
742 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22743 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28744 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37745 self.replace_test_args(test, test_name, tester_name)
Garrett Beatyafd33e0f2023-06-23 20:47:57746 if 'args' in test and not test['args']:
747 test.pop('args')
Ben Pastenee012aea42019-05-14 22:32:28748
Kenneth Russelleb60cbd22017-12-05 07:54:28749 return test
750
Brian Sheedye6ea0ee2019-07-11 02:54:37751 def replace_test_args(self, test, test_name, tester_name):
752 replacements = self.get_test_replacements(
753 test, test_name, tester_name) or {}
754 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23755 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37756 if key not in valid_replacement_keys:
757 raise BBGenErr(
758 'Given replacement key %s for %s on %s is not in the list of valid '
759 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23760 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37761 found_key = False
762 for i, test_key in enumerate(test.get(key, [])):
763 # Handle both the key/value being replaced being defined as two
764 # separate items or as key=value.
765 if test_key == replacement_key:
766 found_key = True
767 # Handle flags without values.
768 if replacement_val == None:
769 del test[key][i]
770 else:
771 test[key][i+1] = replacement_val
772 break
Joshua Hood56c673c2022-03-02 20:29:33773 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37774 found_key = True
775 if replacement_val == None:
776 del test[key][i]
777 else:
778 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
779 break
780 if not found_key:
781 raise BBGenErr('Could not find %s in existing list of values for key '
782 '%s in %s on %s' % (replacement_key, key, test_name,
783 tester_name))
784
Shenghua Zhangaba8bad2018-02-07 02:12:09785 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05786 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26787 True):
788 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06789 # are targeting CrOS hardware and so need the special trigger script.
790 dimension_sets = test['swarming']['dimension_sets']
Ben Pastenea9e583b2019-01-16 02:57:26791 if all('device_type' in ds for ds in dimension_sets):
792 test['trigger_script'] = {
793 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
794 }
Shenghua Zhangaba8bad2018-02-07 02:12:09795
Ben Pastene858f4be2019-01-09 23:52:09796 def add_android_presentation_args(self, tester_config, test_name, result):
797 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38798 bucket = tester_config.get('results_bucket', 'chromium-result-details')
799 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09800 if (result['swarming']['can_use_on_swarming_builders'] and not
801 tester_config.get('skip_merge_script', False)):
802 result['merge'] = {
803 'args': [
804 '--bucket',
John Budorick262ae112019-07-12 19:24:38805 bucket,
Ben Pastene858f4be2019-01-09 23:52:09806 '--test-name',
Rakib M. Hasanc9e01c62020-07-27 22:48:12807 result.get('name', test_name)
Ben Pastene858f4be2019-01-09 23:52:09808 ],
809 'script': '//build/android/pylib/results/presentation/'
810 'test_results_presentation.py',
811 }
Ben Pastene858f4be2019-01-09 23:52:09812 if not tester_config.get('skip_output_links', False):
813 result['swarming']['output_links'] = [
814 {
815 'link': [
816 'https://luci-logdog.appspot.com/v/?s',
817 '=android%2Fswarming%2Flogcats%2F',
818 '${TASK_ID}%2F%2B%2Funified_logcats',
819 ],
820 'name': 'shard #${SHARD_INDEX} logcats',
821 },
822 ]
823 if args:
824 result['args'] = args
825
Kenneth Russelleb60cbd22017-12-05 07:54:28826 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
827 test_config):
828 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15829 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28830 return None
831 result = copy.deepcopy(test_config)
832 if 'test' in result:
Rakib M. Hasanc9e01c62020-07-27 22:48:12833 if 'name' not in result:
834 result['name'] = test_name
Kenneth Russelleb60cbd22017-12-05 07:54:28835 else:
836 result['test'] = test_name
837 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21838
839 self.initialize_args_for_test(
840 result, tester_config, additional_arg_keys=['gtest_args'])
Jamie Madilla8be0d72020-10-02 05:24:04841 if self.is_android(tester_config) and tester_config.get(
Yuly Novikov26dd47052021-02-11 00:57:14842 'use_swarming', True):
843 if not test_config.get('use_isolated_scripts_api', False):
844 # TODO(https://crbug.com/1137998) make Android presentation work with
845 # isolated scripts in test_results_presentation.py merge script
846 self.add_android_presentation_args(tester_config, test_name, result)
847 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42848
Stephen Martinis0382bc12018-09-17 22:29:07849 result = self.update_and_cleanup_test(
850 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09851 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34852 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43853
Garrett Beatybb18d532023-06-26 22:16:33854 if 'swarming' in result and not result.get('merge'):
Jamie Madilla8be0d72020-10-02 05:24:04855 if test_config.get('use_isolated_scripts_api', False):
856 merge_script = 'standard_isolated_script_merge'
857 else:
858 merge_script = 'standard_gtest_merge'
859
Stephen Martinisbc7b7772019-05-01 22:01:43860 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04861 'script': '//testing/merge_scripts/%s.py' % merge_script,
Stephen Martinisbc7b7772019-05-01 22:01:43862 }
Kenneth Russelleb60cbd22017-12-05 07:54:28863 return result
864
865 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
866 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01867 if not self.should_run_on_tester(waterfall, tester_name, test_name,
868 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28869 return None
870 result = copy.deepcopy(test_config)
871 result['isolate_name'] = result.get('isolate_name', test_name)
Jeff Yoonb8bfdbf32020-03-13 19:14:43872 result['name'] = result.get('name', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28873 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01874 self.initialize_args_for_test(result, tester_config)
Yuly Novikov26dd47052021-02-11 00:57:14875 if self.is_android(tester_config) and tester_config.get(
876 'use_swarming', True):
877 if tester_config.get('use_android_presentation', False):
878 # TODO(https://crbug.com/1137998) make Android presentation work with
879 # isolated scripts in test_results_presentation.py merge script
880 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07881 result = self.update_and_cleanup_test(
882 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09883 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34884 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17885
Garrett Beatybb18d532023-06-26 22:16:33886 if 'swarming' in result and not result.get('merge'):
Stephen Martinisf50047062019-05-06 22:26:17887 # TODO(https://crbug.com/958376): Consider adding the ability to not have
888 # this default.
889 result['merge'] = {
890 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
Stephen Martinisf50047062019-05-06 22:26:17891 }
Kenneth Russelleb60cbd22017-12-05 07:54:28892 return result
893
894 def generate_script_test(self, waterfall, tester_name, tester_config,
895 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44896 # TODO(https://crbug.com/953072): Remove this check whenever a better
897 # long-term solution is implemented.
898 if (waterfall.get('forbid_script_tests', False) or
899 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
900 raise BBGenErr('Attempted to generate a script test on tester ' +
901 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01902 if not self.should_run_on_tester(waterfall, tester_name, test_name,
903 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28904 return None
905 result = {
906 'name': test_name,
907 'script': test_config['script']
908 }
Stephen Martinis0382bc12018-09-17 22:29:07909 result = self.update_and_cleanup_test(
910 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34911 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28912 return result
913
914 def generate_junit_test(self, waterfall, tester_name, tester_config,
915 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01916 if not self.should_run_on_tester(waterfall, tester_name, test_name,
917 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28918 return None
John Budorickdef6acb2019-09-17 22:51:09919 result = copy.deepcopy(test_config)
920 result.update({
John Budorickcadc4952019-09-16 23:51:37921 'name': test_name,
922 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09923 })
924 self.initialize_args_for_test(result, tester_config)
925 result = self.update_and_cleanup_test(
926 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34927 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28928 return result
929
Xinan Lin05fb9c1752020-12-17 00:15:52930 def generate_skylab_test(self, waterfall, tester_name, tester_config,
931 test_name, test_config):
932 if not self.should_run_on_tester(waterfall, tester_name, test_name,
933 test_config):
934 return None
935 result = copy.deepcopy(test_config)
936 result.update({
937 'test': test_name,
938 })
939 self.initialize_args_for_test(result, tester_config)
940 result = self.update_and_cleanup_test(result, test_name, tester_name,
941 tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34942 self.substitute_magic_args(result, tester_name, tester_config)
Xinan Lin05fb9c1752020-12-17 00:15:52943 return result
944
Stephen Martinis2a0667022018-09-25 22:31:14945 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01946 substitutions = {
947 # Any machine in waterfalls.pyl which desires to run GPU tests
948 # must provide the os_type key.
949 'os_type': tester_config['os_type'],
950 'gpu_vendor_id': '0',
951 'gpu_device_id': '0',
952 }
Brian Sheedyb6491ba2022-09-26 20:49:49953 if swarming_config.get('dimension_sets'):
954 dimension_set = swarming_config['dimension_sets'][0]
955 if 'gpu' in dimension_set:
956 # First remove the driver version, then split into vendor and device.
957 gpu = dimension_set['gpu']
958 if gpu != 'none':
959 gpu = gpu.split('-')[0].split(':')
960 substitutions['gpu_vendor_id'] = gpu[0]
961 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01962 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
963
964 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30965 test_name, test_config, is_android_webview,
966 is_cast_streaming):
Kenneth Russell8a386d42018-06-02 09:48:01967 # These are all just specializations of isolated script tests with
968 # a bunch of boilerplate command line arguments added.
969
970 # The step name must end in 'test' or 'tests' in order for the
971 # results to automatically show up on the flakiness dashboard.
972 # (At least, this was true some time ago.) Continue to use this
973 # naming convention for the time being to minimize changes.
974 step_name = test_config.get('name', test_name)
Ben Pastene5f231cf22022-05-05 18:03:07975 variant_id = test_config.get('variant_id')
976 if variant_id:
977 step_name = self.remove_variant_from_test_name(step_name, variant_id)
Kenneth Russell8a386d42018-06-02 09:48:01978 if not (step_name.endswith('test') or step_name.endswith('tests')):
979 step_name = '%s_tests' % step_name
Ben Pastene5f231cf22022-05-05 18:03:07980 if variant_id:
981 step_name = self.add_variant_to_test_name(step_name, variant_id)
Ben Pastene5ff45d82022-05-05 21:54:00982 if 'name' in test_config:
983 test_config['name'] = step_name
Kenneth Russell8a386d42018-06-02 09:48:01984 result = self.generate_isolated_script_test(
985 waterfall, tester_name, tester_config, step_name, test_config)
986 if not result:
987 return None
Chong Gub75754b32020-03-13 16:39:20988 result['isolate_name'] = test_config.get(
Brian Sheedyf74819b2021-06-04 01:38:38989 'isolate_name',
990 self.get_default_isolate_name(tester_config, is_android_webview))
Chan Liab7d8dd82020-04-24 23:42:19991
Chan Lia3ad1502020-04-28 05:32:11992 # Populate test_id_prefix.
Brian Sheedyf74819b2021-06-04 01:38:38993 gn_entry = self.gn_isolate_map[result['isolate_name']]
Chan Li17d969f92020-07-10 00:50:03994 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19995
Kenneth Russell8a386d42018-06-02 09:48:01996 args = result.get('args', [])
997 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14998
erikchen6da2d9b2018-08-03 23:01:14999 # These tests upload and download results from cloud storage and therefore
1000 # aren't idempotent yet. https://crbug.com/549140.
Garrett Beatybfeff8f2023-06-16 18:57:251001 if 'swarming' in result:
1002 result['swarming']['idempotent'] = False
erikchen6da2d9b2018-08-03 23:01:141003
Kenneth Russell44910c32018-12-03 23:35:111004 # The GPU tests act much like integration tests for the entire browser, and
1005 # tend to uncover flakiness bugs more readily than other test suites. In
1006 # order to surface any flakiness more readily to the developer of the CL
1007 # which is introducing it, we disable retries with patch on the commit
1008 # queue.
1009 result['should_retry_with_patch'] = False
1010
Fabrice de Ganscbd655f2022-08-04 20:15:301011 browser = ''
1012 if is_cast_streaming:
1013 browser = 'cast-streaming-shell'
1014 elif is_android_webview:
1015 browser = 'android-webview-instrumentation'
1016 else:
1017 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:521018
Greg Thompsoncec7d8d2023-01-10 19:11:531019 extra_browser_args = []
1020
Brian Sheedy4053a702020-07-28 02:09:521021 # Most platforms require --enable-logging=stderr to get useful browser logs.
1022 # However, this actively messes with logging on CrOS (because Chrome's
1023 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
1024 # in order to see JavaScript console messages. See
1025 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
Greg Thompsoncec7d8d2023-01-10 19:11:531026 if self.is_chromeos(tester_config):
1027 extra_browser_args.append('--log-level=0')
1028 elif not self.is_fuchsia(tester_config) or browser != 'fuchsia-chrome':
1029 # Stderr logging is not needed for Chrome browser on Fuchsia, as ordinary
1030 # logging via syslog is captured.
1031 extra_browser_args.append('--enable-logging=stderr')
1032
1033 # --expose-gc allows the WebGL conformance tests to more reliably
1034 # reproduce GC-related bugs in the V8 bindings.
1035 extra_browser_args.append('--js-flags=--expose-gc')
Brian Sheedy4053a702020-07-28 02:09:521036
Kenneth Russell8a386d42018-06-02 09:48:011037 args = [
Bo Liu555a0f92019-03-29 12:11:561038 test_to_run,
1039 '--show-stdout',
1040 '--browser=%s' % browser,
1041 # --passthrough displays more of the logging in Telemetry when
1042 # run via typ, in particular some of the warnings about tests
1043 # being expected to fail, but passing.
1044 '--passthrough',
1045 '-v',
Brian Sheedy814e0482022-10-03 23:24:121046 '--stable-jobs',
Greg Thompsoncec7d8d2023-01-10 19:11:531047 '--extra-browser-args=%s' % ' '.join(extra_browser_args),
Kenneth Russell8a386d42018-06-02 09:48:011048 ] + args
Garrett Beatybfeff8f2023-06-16 18:57:251049 result['args'] = self.maybe_fixup_args_array(
1050 self.substitute_gpu_args(tester_config, result.get('swarming', {}),
1051 args))
Kenneth Russell8a386d42018-06-02 09:48:011052 return result
1053
Brian Sheedyf74819b2021-06-04 01:38:381054 def get_default_isolate_name(self, tester_config, is_android_webview):
1055 if self.is_android(tester_config):
1056 if is_android_webview:
1057 return 'telemetry_gpu_integration_test_android_webview'
1058 return (
1059 'telemetry_gpu_integration_test' +
1060 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:331061 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:171062 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:331063 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:381064
Kenneth Russelleb60cbd22017-12-05 07:54:281065 def get_test_generator_map(self):
1066 return {
Bo Liu555a0f92019-03-29 12:11:561067 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301068 GPUTelemetryTestGenerator(self, is_android_webview=True),
1069 'cast_streaming_tests':
1070 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:561071 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301072 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561073 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301074 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561075 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301076 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561077 'junit_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301078 JUnitGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561079 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301080 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:521081 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301082 SkylabGenerator(self),
Brian Sheedyb6491ba2022-09-26 20:49:491083 'skylab_gpu_telemetry_tests':
1084 SkylabGPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281085 }
1086
Kenneth Russell8a386d42018-06-02 09:48:011087 def get_test_type_remapper(self):
1088 return {
Fabrice de Gans223272482022-08-08 16:56:571089 # These are a specialization of isolated_scripts with a bunch of
1090 # boilerplate command line arguments added to each one.
1091 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
1092 'cast_streaming_tests': 'isolated_scripts',
1093 'gpu_telemetry_tests': 'isolated_scripts',
Brian Sheedyb6491ba2022-09-26 20:49:491094 # These are the same as existing test types, just configured to run
1095 # in Skylab instead of via normal swarming.
1096 'skylab_gpu_telemetry_tests': 'skylab_tests',
Kenneth Russell8a386d42018-06-02 09:48:011097 }
1098
Jeff Yoon67c3e832020-02-08 07:39:381099 def check_composition_type_test_suites(self, test_type,
1100 additional_validators=None):
1101 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1102 validators = [check_compound_references,
1103 check_basic_references,
1104 check_conflicting_definitions]
1105 if additional_validators:
1106 validators += additional_validators
1107
1108 target_suites = self.test_suites.get(test_type, {})
1109 other_test_type = ('compound_suites'
1110 if test_type == 'matrix_compound_suites'
1111 else 'matrix_compound_suites')
1112 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011113 basic_suites = self.test_suites.get('basic_suites', {})
1114
Jamie Madillcf4f8c72021-05-20 19:24:231115 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011116 if suite in basic_suites:
1117 raise BBGenErr('%s names may not duplicate basic test suite names '
1118 '(error found while processsing %s)'
1119 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011120
Jeff Yoon67c3e832020-02-08 07:39:381121 seen_tests = {}
1122 for sub_suite in suite_def:
1123 for validator in validators:
1124 validator(
1125 basic_suites=basic_suites,
1126 other_test_suites=other_suites,
1127 seen_tests=seen_tests,
1128 sub_suite=sub_suite,
1129 suite=suite,
1130 suite_def=suite_def,
1131 target_test_suites=target_suites,
1132 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051133 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381134 )
Kenneth Russelleb60cbd22017-12-05 07:54:281135
Stephen Martinis54d64ad2018-09-21 22:16:201136 def flatten_test_suites(self):
1137 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011138 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1139 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231140 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011141 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201142 self.test_suites = new_test_suites
1143
Chan Lia3ad1502020-04-28 05:32:111144 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231145 for suite in self.test_suites['basic_suites'].values():
1146 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561147 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411148
1149 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
John Palmera8515fca2021-05-20 03:35:321150 # 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:411151 # TODO(crbug.com/1035124): clean this up.
1152 isolate_name = test.get('test') or test.get('isolate_name') or key
1153 gn_entry = self.gn_isolate_map.get(isolate_name)
1154 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281155 label = gn_entry['label']
1156
1157 if label.count(':') != 1:
1158 raise BBGenErr(
1159 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1160 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1161 (label, isolate_name))
1162 if label.split(':')[1] != isolate_name:
1163 raise BBGenErr(
1164 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1165 ' label "%s" see http://crbug.com/1071091 for details.' %
1166 (isolate_name, label))
1167
Chan Lia3ad1502020-04-28 05:32:111168 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411169 else: # pragma: no cover
1170 # Some tests do not have an entry gn_isolate_map.pyl, such as
1171 # telemetry tests.
1172 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
1173 pass
1174
Kenneth Russelleb60cbd22017-12-05 07:54:281175 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011176 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201177
Jeff Yoon8154e582019-12-03 23:30:011178 compound_suites = self.test_suites.get('compound_suites', {})
1179 # check_composition_type_test_suites() checks that all basic suites
1180 # referenced by compound suites exist.
1181 basic_suites = self.test_suites.get('basic_suites')
1182
Jamie Madillcf4f8c72021-05-20 19:24:231183 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011184 # Resolve this to a dictionary.
1185 full_suite = {}
1186 for entry in value:
1187 suite = basic_suites[entry]
1188 full_suite.update(suite)
1189 compound_suites[name] = full_suite
1190
Jeff Yoon85fb8df2020-08-20 16:47:431191 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381192 """ Merge variant-defined configurations to each test case definition in a
1193 test suite.
1194
1195 The output maps a unique test name to an array of configurations because
1196 there may exist more than one definition for a test name using variants. The
1197 test name is referenced while mapping machines to test suites, so unpacking
1198 the array is done by the generators.
1199
1200 Args:
1201 basic_test_definition: a {} defined test suite in the format
1202 test_name:test_config
1203 variants: an [] of {} defining configurations to be applied to each test
1204 case in the basic test_definition
1205
1206 Return:
1207 a {} of test_name:[{}], where each {} is a merged configuration
1208 """
1209
1210 # Each test in a basic test suite will have a definition per variant.
1211 test_suite = {}
Garrett Beaty8d6708c2023-07-20 17:20:411212 for variant in variants:
1213 # Unpack the variant from variants.pyl if it's string based.
1214 if isinstance(variant, str):
1215 variant = self.variants[variant]
Jeff Yoonda581c32020-03-06 03:56:051216
Garrett Beaty8d6708c2023-07-20 17:20:411217 # If 'enabled' is set to False, we will not use this variant; otherwise if
1218 # the variant doesn't include 'enabled' variable or 'enabled' is set to
1219 # True, we will use this variant
1220 if not variant.get('enabled', True):
1221 continue
Jeff Yoon67c3e832020-02-08 07:39:381222
Garrett Beaty8d6708c2023-07-20 17:20:411223 # Make a shallow copy of the variant to remove variant-specific fields,
1224 # leaving just mixin fields
1225 variant = copy.copy(variant)
1226 variant.pop('enabled', None)
1227 identifier = variant.pop('identifier')
1228 variant_mixins = variant.pop('mixins', [])
1229 variant_skylab = variant.pop('skylab', {})
Jeff Yoon67c3e832020-02-08 07:39:381230
Garrett Beaty8d6708c2023-07-20 17:20:411231 for test_name, test_config in basic_test_definition.items():
1232 new_test = self.apply_mixin(variant, test_config)
Jeff Yoon67c3e832020-02-08 07:39:381233
Garrett Beaty8d6708c2023-07-20 17:20:411234 new_test['mixins'] = (test_config.get('mixins', []) + variant_mixins +
1235 mixins)
Xinan Lin05fb9c1752020-12-17 00:15:521236
Jeff Yoon67c3e832020-02-08 07:39:381237 # The identifier is used to make the name of the test unique.
1238 # Generators in the recipe uniquely identify a test by it's name, so we
1239 # don't want to have the same name for each variant.
Garrett Beaty8d6708c2023-07-20 17:20:411240 new_test['name'] = self.add_variant_to_test_name(
1241 new_test.get('name', test_name), identifier)
Ben Pastene5f231cf22022-05-05 18:03:071242
1243 # Attach the variant identifier to the test config so downstream
1244 # generators can make modifications based on the original name. This
1245 # is mainly used in generate_gpu_telemetry_test().
Garrett Beaty8d6708c2023-07-20 17:20:411246 new_test['variant_id'] = identifier
Ben Pastene5f231cf22022-05-05 18:03:071247
Garrett Beaty8d6708c2023-07-20 17:20:411248 # cros_chrome_version is the ash chrome version in the cros img in the
1249 # variant of cros_board. We don't want to include it in the final json
1250 # files; so remove it.
1251 for k, v in variant_skylab.items():
1252 if k != 'cros_chrome_version':
1253 new_test[k] = v
1254
1255 test_suite.setdefault(test_name, []).append(new_test)
1256
Jeff Yoon67c3e832020-02-08 07:39:381257 return test_suite
1258
Jeff Yoon8154e582019-12-03 23:30:011259 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381260 self.check_composition_type_test_suites('matrix_compound_suites',
1261 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011262
1263 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381264 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011265 # referenced by matrix suites exist.
1266 basic_suites = self.test_suites.get('basic_suites')
1267
Jamie Madillcf4f8c72021-05-20 19:24:231268 for test_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011269 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381270
Jamie Madillcf4f8c72021-05-20 19:24:231271 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381272 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1273
1274 if 'variants' in mtx_test_suite_config:
Jeff Yoon85fb8df2020-08-20 16:47:431275 mixins = mtx_test_suite_config.get('mixins', [])
Jeff Yoon67c3e832020-02-08 07:39:381276 result = self.resolve_variants(basic_test_def,
Jeff Yoon85fb8df2020-08-20 16:47:431277 mtx_test_suite_config['variants'],
1278 mixins)
Jeff Yoon67c3e832020-02-08 07:39:381279 full_suite.update(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271280 else:
1281 suite = basic_suites[test_suite]
1282 full_suite.update(suite)
Jeff Yoon67c3e832020-02-08 07:39:381283 matrix_compound_suites[test_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281284
1285 def link_waterfalls_to_test_suites(self):
1286 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231287 for tester_name, tester in waterfall['machines'].items():
1288 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281289 if not value in self.test_suites:
1290 # Hard / impossible to cover this in the unit test.
1291 raise self.unknown_test_suite(
1292 value, tester_name, waterfall['name']) # pragma: no cover
1293 tester['test_suites'][suite] = self.test_suites[value]
1294
1295 def load_configuration_files(self):
Garrett Beaty79339e182023-04-10 20:45:471296 self.waterfalls = self.load_pyl_file(self.args.waterfalls_pyl_path)
1297 self.test_suites = self.load_pyl_file(self.args.test_suites_pyl_path)
1298 self.exceptions = self.load_pyl_file(
1299 self.args.test_suite_exceptions_pyl_path)
1300 self.mixins = self.load_pyl_file(self.args.mixins_pyl_path)
1301 self.gn_isolate_map = self.load_pyl_file(self.args.gn_isolate_map_pyl_path)
Chong Guee622242020-10-28 18:17:351302 for isolate_map in self.args.isolate_map_files:
1303 isolate_map = self.load_pyl_file(isolate_map)
1304 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1305 if duplicates:
1306 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1307 ', '.join(duplicates))
1308 self.gn_isolate_map.update(isolate_map)
1309
Garrett Beaty79339e182023-04-10 20:45:471310 self.variants = self.load_pyl_file(self.args.variants_pyl_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281311
1312 def resolve_configuration_files(self):
Chan Lia3ad1502020-04-28 05:32:111313 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281314 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011315 self.resolve_matrix_compound_test_suites()
1316 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281317 self.link_waterfalls_to_test_suites()
1318
Nico Weberd18b8962018-05-16 19:39:381319 def unknown_bot(self, bot_name, waterfall_name):
1320 return BBGenErr(
1321 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1322
Kenneth Russelleb60cbd22017-12-05 07:54:281323 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1324 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381325 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281326 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1327
1328 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1329 return BBGenErr(
1330 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1331 ' on waterfall ' + waterfall_name)
1332
Stephen Martinisb72f6d22018-10-04 23:29:011333 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071334 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321335
1336 Checks in the waterfall, builder, and test objects for mixins.
1337 """
1338 def valid_mixin(mixin_name):
1339 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011340 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321341 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381342
Stephen Martinisb6a50492018-09-12 23:59:321343 def must_be_list(mixins, typ, name):
1344 """Asserts that given mixins are a list."""
1345 if not isinstance(mixins, list):
1346 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1347
Brian Sheedy7658c982020-01-08 02:27:581348 test_name = test.get('name')
1349 remove_mixins = set()
1350 if 'remove_mixins' in builder:
1351 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1352 for rm in builder['remove_mixins']:
1353 valid_mixin(rm)
1354 remove_mixins.add(rm)
1355 if 'remove_mixins' in test:
1356 must_be_list(test['remove_mixins'], 'test', test_name)
1357 for rm in test['remove_mixins']:
1358 valid_mixin(rm)
1359 remove_mixins.add(rm)
1360 del test['remove_mixins']
1361
Stephen Martinisb72f6d22018-10-04 23:29:011362 if 'mixins' in waterfall:
1363 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1364 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581365 if mixin in remove_mixins:
1366 continue
Stephen Martinisb6a50492018-09-12 23:59:321367 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531368 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321369
Stephen Martinisb72f6d22018-10-04 23:29:011370 if 'mixins' in builder:
1371 must_be_list(builder['mixins'], 'builder', builder_name)
1372 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581373 if mixin in remove_mixins:
1374 continue
Stephen Martinisb6a50492018-09-12 23:59:321375 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531376 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321377
Stephen Martinisb72f6d22018-10-04 23:29:011378 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071379 return test
1380
Stephen Martinis2a0667022018-09-25 22:31:141381 if not test_name:
1382 test_name = test.get('test')
1383 if not test_name: # pragma: no cover
1384 # Not the best name, but we should say something.
1385 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:011386 must_be_list(test['mixins'], 'test', test_name)
1387 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581388 # We don't bother checking if the given mixin is in remove_mixins here
1389 # since this is already the lowest level, so if a mixin is added here that
1390 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071391 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531392 test = self.apply_mixin(self.mixins[mixin], test, builder)
Jeff Yoon67c3e832020-02-08 07:39:381393 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071394 return test
Stephen Martinisb6a50492018-09-12 23:59:321395
Garrett Beaty8d6708c2023-07-20 17:20:411396 def apply_mixin(self, mixin, test, builder=None):
Stephen Martinisb72f6d22018-10-04 23:29:011397 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321398
Garrett Beaty4c35b142023-06-23 21:01:231399 A mixin is applied by copying all fields from the mixin into the
1400 test with the following exceptions:
1401 * For the various *args keys, the test's existing value (an empty
1402 list if not present) will be extended with the mixin's value.
1403 * The sub-keys of the swarming value will be copied to the test's
1404 swarming value with the following exceptions:
1405 * For the dimension_sets and named_caches sub-keys, the test's
1406 existing value (an empty list if not present) will be extended
1407 with the mixin's value.
1408 * For the dimensions sub-key, after extending the test's
1409 dimension_sets as specified above, each dimension set will be
1410 updated with the value of the dimensions sub-key. If there are
1411 no dimension sets, then one will be added that contains the
1412 specified dimensions.
Stephen Martinisb6a50492018-09-12 23:59:321413 """
Garrett Beaty4c35b142023-06-23 21:01:231414
Stephen Martinisb6a50492018-09-12 23:59:321415 new_test = copy.deepcopy(test)
1416 mixin = copy.deepcopy(mixin)
Garrett Beaty8d6708c2023-07-20 17:20:411417
1418 if 'description' in mixin:
1419 description = []
1420 if 'description' in new_test:
1421 description.append(new_test['description'])
1422 description.append(mixin.pop('description'))
1423 new_test['description'] = '\n'.join(description)
1424
Stephen Martinisb72f6d22018-10-04 23:29:011425 if 'swarming' in mixin:
1426 swarming_mixin = mixin['swarming']
1427 new_test.setdefault('swarming', {})
Brian Sheedycae63b22020-06-10 22:52:111428 # Copy over any explicit dimension sets first so that they will be updated
1429 # by any subsequent 'dimensions' entries.
1430 if 'dimension_sets' in swarming_mixin:
1431 existing_dimension_sets = new_test['swarming'].setdefault(
1432 'dimension_sets', [])
1433 # Appending to the existing list could potentially result in different
1434 # behavior depending on the order the mixins were applied, but that's
1435 # already the case for other parts of mixins, so trust that the user
1436 # will verify that the generated output is correct before submitting.
1437 for dimension_set in swarming_mixin['dimension_sets']:
1438 if dimension_set not in existing_dimension_sets:
1439 existing_dimension_sets.append(dimension_set)
1440 del swarming_mixin['dimension_sets']
Stephen Martinisb72f6d22018-10-04 23:29:011441 if 'dimensions' in swarming_mixin:
1442 new_test['swarming'].setdefault('dimension_sets', [{}])
1443 for dimension_set in new_test['swarming']['dimension_sets']:
1444 dimension_set.update(swarming_mixin['dimensions'])
1445 del swarming_mixin['dimensions']
Garrett Beaty4c35b142023-06-23 21:01:231446 if 'named_caches' in swarming_mixin:
1447 new_test['swarming'].setdefault('named_caches', []).extend(
1448 swarming_mixin['named_caches'])
1449 del swarming_mixin['named_caches']
Stephen Martinisb72f6d22018-10-04 23:29:011450 # python dict update doesn't do recursion at all. Just hard code the
1451 # nested update we need (mixin['swarming'] shouldn't clobber
1452 # test['swarming'], but should update it).
1453 new_test['swarming'].update(swarming_mixin)
1454 del mixin['swarming']
1455
Garrett Beaty4c35b142023-06-23 21:01:231456 # Array so we can assign to it in a nested scope.
1457 args_need_fixup = ['args' in mixin]
1458
1459 for a in (
1460 'args',
1461 'precommit_args',
1462 'non_precommit_args',
1463 'desktop_args',
1464 'lacros_args',
1465 'linux_args',
1466 'android_args',
1467 'chromeos_args',
1468 'mac_args',
1469 'win_args',
1470 'win64_args',
1471 ):
1472 if (value := mixin.pop(a, None)) is None:
1473 continue
1474 if not isinstance(value, list):
1475 raise BBGenErr(f'"{a}" must be a list')
1476 new_test.setdefault(a, []).extend(value)
1477
Garrett Beaty4c35b142023-06-23 21:01:231478 args = new_test.get('args', [])
Austin Eng148d9f0f2022-02-08 19:18:531479
Garrett Beaty4c35b142023-06-23 21:01:231480 def add_conditional_args(key, fn):
Garrett Beaty8d6708c2023-07-20 17:20:411481 if builder is None:
1482 return
Garrett Beaty4c35b142023-06-23 21:01:231483 val = new_test.pop(key, [])
1484 if val and fn(builder):
1485 args.extend(val)
1486 args_need_fixup[0] = True
Austin Eng148d9f0f2022-02-08 19:18:531487
Garrett Beaty4c35b142023-06-23 21:01:231488 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
1489 add_conditional_args('lacros_args', self.is_lacros)
1490 add_conditional_args('linux_args', self.is_linux)
1491 add_conditional_args('android_args', self.is_android)
1492 add_conditional_args('chromeos_args', self.is_chromeos)
1493 add_conditional_args('mac_args', self.is_mac)
1494 add_conditional_args('win_args', self.is_win)
1495 add_conditional_args('win64_args', self.is_win64)
1496
1497 if args_need_fixup[0]:
1498 new_test['args'] = self.maybe_fixup_args_array(args)
Wezc0e835b702018-10-30 00:38:411499
Stephen Martinisb72f6d22018-10-04 23:29:011500 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321501 return new_test
1502
Greg Gutermanf60eb052020-03-12 17:40:011503 def generate_output_tests(self, waterfall):
1504 """Generates the tests for a waterfall.
1505
1506 Args:
1507 waterfall: a dictionary parsed from a master pyl file
1508 Returns:
1509 A dictionary mapping builders to test specs
1510 """
1511 return {
Jamie Madillcf4f8c72021-05-20 19:24:231512 name: self.get_tests_for_config(waterfall, name, config)
1513 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011514 }
1515
1516 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531517 generator_map = self.get_test_generator_map()
1518 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281519
Greg Gutermanf60eb052020-03-12 17:40:011520 tests = {}
1521 # Copy only well-understood entries in the machine's configuration
1522 # verbatim into the generated JSON.
1523 if 'additional_compile_targets' in config:
1524 tests['additional_compile_targets'] = config[
1525 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231526 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011527 if test_type not in generator_map:
1528 raise self.unknown_test_suite_type(
1529 test_type, name, waterfall['name']) # pragma: no cover
1530 test_generator = generator_map[test_type]
1531 # Let multiple kinds of generators generate the same kinds
1532 # of tests. For example, gpu_telemetry_tests are a
1533 # specialization of isolated_scripts.
1534 new_tests = test_generator.generate(
1535 waterfall, name, config, input_tests)
1536 remapped_test_type = test_type_remapper.get(test_type, test_type)
1537 tests[remapped_test_type] = test_generator.sort(
1538 tests.get(remapped_test_type, []) + new_tests)
1539
1540 return tests
1541
1542 def jsonify(self, all_tests):
1543 return json.dumps(
1544 all_tests, indent=2, separators=(',', ': '),
1545 sort_keys=True) + '\n'
1546
1547 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281548 self.load_configuration_files()
1549 self.resolve_configuration_files()
1550 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011551 result = collections.defaultdict(dict)
1552
Stephanie Kim572b43c02023-04-13 14:24:131553 if os.path.exists(self.args.autoshard_exceptions_json_path):
1554 autoshards = json.loads(
1555 self.read_file(self.args.autoshard_exceptions_json_path))
1556 else:
1557 autoshards = {}
1558
Dirk Pranke6269d302020-10-01 00:14:391559 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011560 for waterfall in self.waterfalls:
1561 for field in required_fields:
1562 # Verify required fields
1563 if field not in waterfall:
1564 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1565
1566 # Handle filter flag, if specified
1567 if filters and waterfall['name'] not in filters:
1568 continue
1569
1570 # Join config files and hardcoded values together
1571 all_tests = self.generate_output_tests(waterfall)
1572 result[waterfall['name']] = all_tests
1573
Stephanie Kim572b43c02023-04-13 14:24:131574 if not autoshards:
1575 continue
1576 for builder, test_spec in all_tests.items():
1577 for target_type, test_list in test_spec.items():
1578 if target_type == 'additional_compile_targets':
1579 continue
1580 for test_dict in test_list:
1581 # Suites that apply variants or other customizations will create
1582 # test_dicts that have "name" value that is different from the
1583 # "test" value. Regular suites without any variations will only have
1584 # "test" and no "name".
1585 # e.g. name = vulkan_swiftshader_content_browsertests, but
1586 # test = content_browsertests and
1587 # test_id_prefix = "ninja://content/test:content_browsertests/"
1588 # Check for "name" first and then fallback to "test"
1589 test_name = test_dict.get('name') or test_dict.get('test')
1590 if not test_name:
1591 continue
1592 shard_info = autoshards.get(waterfall['name'],
1593 {}).get(builder, {}).get(test_name)
1594 if shard_info:
1595 test_dict['swarming'].update(
1596 {'shards': int(shard_info['shards'])})
1597
Greg Gutermanf60eb052020-03-12 17:40:011598 # Add do not edit warning
1599 for tests in result.values():
1600 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1601 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1602
1603 return result
1604
1605 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281606 suffix = '.json'
1607 if self.args.new_files:
1608 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011609
1610 for filename, contents in result.items():
1611 jsonstr = self.jsonify(contents)
Garrett Beaty79339e182023-04-10 20:45:471612 file_path = os.path.join(self.args.output_dir, filename + suffix)
1613 self.write_file(file_path, jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281614
Nico Weberd18b8962018-05-16 19:39:381615 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161616 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421617 # NOTE: This reference can cause issues; if a file changes there, the
1618 # presubmit here won't be run by default. A manually maintained list there
1619 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1620 # references to configs outside of this directory are added, please change
1621 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1622 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491623
Garrett Beaty7e866fc2021-06-16 14:12:101624 # Get the generated project.pyl so we can check if we should be enforcing
1625 # that the specs are for builders that actually exist
1626 # If not, return None to indicate that we won't enforce that builders in
1627 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491628 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1629 'project.pyl')
1630 if os.path.exists(project_pyl_path):
1631 settings = ast.literal_eval(self.read_file(project_pyl_path))
1632 if not settings.get('validate_source_side_specs_have_builder', True):
1633 return None
1634
Nico Weberd18b8962018-05-16 19:39:381635 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311636 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161637 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1638 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431639 for c in milo_configs:
1640 for l in self.read_file(c).splitlines():
1641 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311642 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431643 continue
1644 # l looks like
1645 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1646 # Extract win_chromium_dbg_ng part.
1647 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381648 return bot_names
1649
Ben Pastene9a010082019-09-25 20:41:371650 def get_internal_waterfalls(self):
1651 # Similar to get_builders_that_do_not_actually_exist above, but for
1652 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201653 return [
Kramer Ge3bf853a2023-04-13 19:39:471654 'chrome', 'chrome.pgo', 'chrome.gpu.fyi', 'internal.chrome.fyi',
1655 'internal.chromeos.fyi', 'internal.soda'
Yuke Liaoe6c23dd2021-07-28 16:12:201656 ]
Ben Pastene9a010082019-09-25 20:41:371657
Stephen Martinisf83893722018-09-19 00:02:181658 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201659 self.check_input_files_sorting(verbose)
1660
Kenneth Russelleb60cbd22017-12-05 07:54:281661 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011662 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381663 self.check_composition_type_test_suites('matrix_compound_suites',
1664 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111665 self.resolve_test_id_prefixes()
Stephen Martinis54d64ad2018-09-21 22:16:201666 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381667
1668 # All bots should exist.
1669 bot_names = self.get_valid_bot_names()
Garrett Beaty2a02de3c2020-05-15 13:57:351670 if bot_names is not None:
1671 internal_waterfalls = self.get_internal_waterfalls()
1672 for waterfall in self.waterfalls:
1673 # TODO(crbug.com/991417): Remove the need for this exception.
1674 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011675 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351676 for bot_name in waterfall['machines']:
Garrett Beaty2a02de3c2020-05-15 13:57:351677 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581678 if waterfall['name'] in [
1679 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1680 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351681 # TODO(thakis): Remove this once these bots move to luci.
1682 continue # pragma: no cover
1683 if waterfall['name'] in ['tryserver.webrtc',
1684 'webrtc.chromium.fyi.experimental']:
1685 # These waterfalls have their bot configs in a different repo.
1686 # so we don't know about their bot names.
1687 continue # pragma: no cover
1688 if waterfall['name'] in ['client.devtools-frontend.integration',
1689 'tryserver.devtools-frontend',
1690 'chromium.devtools-frontend']:
1691 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201692 if waterfall['name'] in ['client.openscreen.chromium']:
1693 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351694 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381695
Kenneth Russelleb60cbd22017-12-05 07:54:281696 # All test suites must be referenced.
1697 suites_seen = set()
1698 generator_map = self.get_test_generator_map()
1699 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231700 for bot_name, tester in waterfall['machines'].items():
1701 for suite_type, suite in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281702 if suite_type not in generator_map:
1703 raise self.unknown_test_suite_type(suite_type, bot_name,
1704 waterfall['name'])
1705 if suite not in self.test_suites:
1706 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1707 suites_seen.add(suite)
1708 # Since we didn't resolve the configuration files, this set
1709 # includes both composition test suites and regular ones.
1710 resolved_suites = set()
1711 for suite_name in suites_seen:
1712 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011713 for sub_suite in suite:
1714 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281715 resolved_suites.add(suite_name)
1716 # At this point, every key in test_suites.pyl should be referenced.
1717 missing_suites = set(self.test_suites.keys()) - resolved_suites
1718 if missing_suites:
1719 raise BBGenErr('The following test suites were unreferenced by bots on '
1720 'the waterfalls: ' + str(missing_suites))
1721
1722 # All test suite exceptions must refer to bots on the waterfall.
1723 all_bots = set()
1724 missing_bots = set()
1725 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231726 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281727 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281728 # In order to disambiguate between bots with the same name on
1729 # different waterfalls, support has been added to various
1730 # exceptions for concatenating the waterfall name after the bot
1731 # name.
1732 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231733 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381734 removals = (exception.get('remove_from', []) +
1735 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231736 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381737 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281738 if removal not in all_bots:
1739 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411740
Kenneth Russelleb60cbd22017-12-05 07:54:281741 if missing_bots:
1742 raise BBGenErr('The following nonexistent machines were referenced in '
1743 'the test suite exceptions: ' + str(missing_bots))
1744
Garrett Beatyb061e69d2023-06-27 16:15:351745 for name, mixin in self.mixins.items():
1746 if '$mixin_append' in mixin:
1747 raise BBGenErr(
1748 f'$mixin_append is no longer supported (set in mixin "{name}"),'
1749 ' args and named caches specified as normal will be appended')
1750
Stephen Martinis0382bc12018-09-17 22:29:071751 # All mixins must be referenced
1752 seen_mixins = set()
1753 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011754 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231755 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011756 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071757 for suite in self.test_suites.values():
1758 if isinstance(suite, list):
1759 # Don't care about this, it's a composition, which shouldn't include a
1760 # swarming mixin.
1761 continue
1762
1763 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561764 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011765 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071766
Zhaoyang Li9da047d52021-05-10 21:31:441767 for variant in self.variants:
1768 # Unpack the variant from variants.pyl if it's string based.
1769 if isinstance(variant, str):
1770 variant = self.variants[variant]
1771 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1772
Stephen Martinisb72f6d22018-10-04 23:29:011773 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071774 if missing_mixins:
1775 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1776 ' referenced in a waterfall, machine, or test suite.' % (
1777 str(missing_mixins)))
1778
Jeff Yoonda581c32020-03-06 03:56:051779 # All variant references must be referenced
1780 seen_variants = set()
1781 for suite in self.test_suites.values():
1782 if isinstance(suite, list):
1783 continue
1784
1785 for test in suite.values():
1786 if isinstance(test, dict):
1787 for variant in test.get('variants', []):
1788 if isinstance(variant, str):
1789 seen_variants.add(variant)
1790
1791 missing_variants = set(self.variants.keys()) - seen_variants
1792 if missing_variants:
1793 raise BBGenErr('The following variants were unreferenced: %s. They must '
1794 'be referenced in a matrix test suite under the variants '
1795 'key.' % str(missing_variants))
1796
Stephen Martinis54d64ad2018-09-21 22:16:201797
Garrett Beaty79339e182023-04-10 20:45:471798 def type_assert(self, node, typ, file_path, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201799 """Asserts that the Python AST node |node| is of type |typ|.
1800
1801 If verbose is set, it prints out some helpful context lines, showing where
1802 exactly the error occurred in the file.
1803 """
1804 if not isinstance(node, typ):
1805 if verbose:
Garrett Beaty79339e182023-04-10 20:45:471806 lines = [""] + self.read_file(file_path).splitlines()
Stephen Martinis54d64ad2018-09-21 22:16:201807
1808 context = 2
1809 lines_start = max(node.lineno - context, 0)
1810 # Add one to include the last line
1811 lines_end = min(node.lineno + context, len(lines)) + 1
Garrett Beaty79339e182023-04-10 20:45:471812 lines = itertools.chain(
1813 ['== %s ==\n' % file_path],
1814 ["<snip>\n"],
1815 [
1816 '%d %s' % (lines_start + i, line)
1817 for i, line in enumerate(lines[lines_start:lines_start +
1818 context])
1819 ],
1820 ['-' * 80 + '\n'],
1821 ['%d %s' % (node.lineno, lines[node.lineno])],
1822 [
1823 '-' * (node.col_offset + 3) + '^' + '-' *
1824 (80 - node.col_offset - 4) + '\n'
1825 ],
1826 [
1827 '%d %s' % (node.lineno + 1 + i, line)
1828 for i, line in enumerate(lines[node.lineno + 1:lines_end])
1829 ],
1830 ["<snip>\n"],
Stephen Martinis54d64ad2018-09-21 22:16:201831 )
1832 # Print out a useful message when a type assertion fails.
1833 for l in lines:
1834 self.print_line(l.strip())
1835
1836 node_dumped = ast.dump(node, annotate_fields=False)
1837 # If the node is huge, truncate it so everything fits in a terminal
1838 # window.
1839 if len(node_dumped) > 60: # pragma: no cover
1840 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1841 raise BBGenErr(
Garrett Beaty807011ab2023-04-12 00:52:391842 'Invalid .pyl file \'%s\'. Python AST node %r on line %s expected to'
Garrett Beaty79339e182023-04-10 20:45:471843 ' be %s, is %s' %
1844 (file_path, node_dumped, node.lineno, typ, type(node)))
Stephen Martinis54d64ad2018-09-21 22:16:201845
Garrett Beaty79339e182023-04-10 20:45:471846 def check_ast_list_formatted(self,
1847 keys,
1848 file_path,
1849 verbose,
Stephen Martinis1384ff92020-01-07 19:52:151850 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531851 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201852
Stephen Martinis5bef0fc2020-01-06 22:47:531853 Currently only checks to ensure they're correctly sorted, and that there
1854 are no duplicates.
1855
1856 Args:
1857 keys: An python list of AST nodes.
1858
1859 It's a list of AST nodes instead of a list of strings because
1860 when verbose is set, it tries to print out context of where the
1861 diffs are in the file.
Garrett Beaty79339e182023-04-10 20:45:471862 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531863 verbose: If set, print out diff information about how the keys are
1864 incorrectly formatted.
1865 check_sorting: If true, checks if the list is sorted.
1866 Returns:
1867 If the keys are correctly formatted.
1868 """
1869 if not keys:
1870 return True
1871
1872 assert isinstance(keys[0], ast.Str)
1873
1874 keys_strs = [k.s for k in keys]
1875 # Keys to diff against. Used below.
1876 keys_to_diff_against = None
1877 # If the list is properly formatted.
1878 list_formatted = True
1879
1880 # Duplicates are always bad.
1881 if len(set(keys_strs)) != len(keys_strs):
1882 list_formatted = False
1883 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1884
1885 if check_sorting and sorted(keys_strs) != keys_strs:
1886 list_formatted = False
1887 if list_formatted:
1888 return True
1889
1890 if verbose:
1891 line_num = keys[0].lineno
1892 keys = [k.s for k in keys]
1893 if check_sorting:
1894 # If we have duplicates, sorting this will take care of it anyways.
1895 keys_to_diff_against = sorted(set(keys))
1896 # else, keys_to_diff_against is set above already
1897
1898 self.print_line('=' * 80)
1899 self.print_line('(First line of keys is %s)' % line_num)
Garrett Beaty79339e182023-04-10 20:45:471900 for line in difflib.context_diff(keys,
1901 keys_to_diff_against,
1902 fromfile='current (%r)' % file_path,
1903 tofile='sorted',
1904 lineterm=''):
Stephen Martinis5bef0fc2020-01-06 22:47:531905 self.print_line(line)
1906 self.print_line('=' * 80)
1907
1908 return False
1909
Garrett Beaty79339e182023-04-10 20:45:471910 def check_ast_dict_formatted(self, node, file_path, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531911 """Checks if an ast dictionary's keys are correctly formatted.
1912
1913 Just a simple wrapper around check_ast_list_formatted.
1914 Args:
1915 node: An AST node. Assumed to be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471916 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531917 verbose: If set, print out diff information about how the keys are
1918 incorrectly formatted.
1919 check_sorting: If true, checks if the list is sorted.
1920 Returns:
1921 If the dictionary is correctly formatted.
1922 """
Stephen Martinis54d64ad2018-09-21 22:16:201923 keys = []
1924 # The keys of this dict are ordered as ordered in the file; normal python
1925 # dictionary keys are given an arbitrary order, but since we parsed the
1926 # file itself, the order as given in the file is preserved.
1927 for key in node.keys:
Garrett Beaty79339e182023-04-10 20:45:471928 self.type_assert(key, ast.Str, file_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531929 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201930
Garrett Beaty79339e182023-04-10 20:45:471931 return self.check_ast_list_formatted(keys, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181932
1933 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201934 # TODO(https://crbug.com/886993): Add the ability for this script to
1935 # actually format the files, rather than just complain if they're
1936 # incorrectly formatted.
1937 bad_files = set()
Garrett Beaty79339e182023-04-10 20:45:471938
1939 def parse_file(file_path):
Stephen Martinis5bef0fc2020-01-06 22:47:531940 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201941
Stephen Martinis5bef0fc2020-01-06 22:47:531942 Returns an AST node representing the value in the pyl file."""
Garrett Beaty79339e182023-04-10 20:45:471943 parsed = ast.parse(self.read_file(file_path))
Stephen Martinisf83893722018-09-19 00:02:181944
Stephen Martinisf83893722018-09-19 00:02:181945 # Must be a module.
Garrett Beaty79339e182023-04-10 20:45:471946 self.type_assert(parsed, ast.Module, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181947 module = parsed.body
1948
1949 # Only one expression in the module.
Garrett Beaty79339e182023-04-10 20:45:471950 self.type_assert(module, list, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181951 if len(module) != 1: # pragma: no cover
Garrett Beaty79339e182023-04-10 20:45:471952 raise BBGenErr('Invalid .pyl file %s' % file_path)
Stephen Martinisf83893722018-09-19 00:02:181953 expr = module[0]
Garrett Beaty79339e182023-04-10 20:45:471954 self.type_assert(expr, ast.Expr, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181955
Stephen Martinis5bef0fc2020-01-06 22:47:531956 return expr.value
1957
1958 # Handle this separately
Garrett Beaty79339e182023-04-10 20:45:471959 value = parse_file(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531960 # Value should be a list.
Garrett Beaty79339e182023-04-10 20:45:471961 self.type_assert(value, ast.List, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531962
1963 keys = []
Joshua Hood56c673c2022-03-02 20:29:331964 for elm in value.elts:
Garrett Beaty79339e182023-04-10 20:45:471965 self.type_assert(elm, ast.Dict, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531966 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331967 for key, val in zip(elm.keys, elm.values):
Garrett Beaty79339e182023-04-10 20:45:471968 self.type_assert(key, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531969 if key.s == 'machines':
Garrett Beaty79339e182023-04-10 20:45:471970 if not self.check_ast_dict_formatted(
1971 val, self.args.waterfalls_pyl_path, verbose):
1972 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531973
1974 if key.s == "name":
Garrett Beaty79339e182023-04-10 20:45:471975 self.type_assert(val, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531976 waterfall_name = val
1977 assert waterfall_name
1978 keys.append(waterfall_name)
1979
Garrett Beaty79339e182023-04-10 20:45:471980 if not self.check_ast_list_formatted(keys, self.args.waterfalls_pyl_path,
1981 verbose):
1982 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531983
Garrett Beaty79339e182023-04-10 20:45:471984 for file_path in (
1985 self.args.mixins_pyl_path,
1986 self.args.test_suites_pyl_path,
1987 self.args.test_suite_exceptions_pyl_path,
Stephen Martinis5bef0fc2020-01-06 22:47:531988 ):
Garrett Beaty79339e182023-04-10 20:45:471989 value = parse_file(file_path)
Stephen Martinisf83893722018-09-19 00:02:181990 # Value should be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471991 self.type_assert(value, ast.Dict, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181992
Garrett Beaty79339e182023-04-10 20:45:471993 if not self.check_ast_dict_formatted(value, file_path, verbose):
1994 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:531995
Garrett Beaty79339e182023-04-10 20:45:471996 if file_path == self.args.test_suites_pyl_path:
Jeff Yoon8154e582019-12-03 23:30:011997 expected_keys = ['basic_suites',
1998 'compound_suites',
1999 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:202000 actual_keys = [node.s for node in value.keys]
2001 assert all(key in expected_keys for key in actual_keys), (
Garrett Beaty79339e182023-04-10 20:45:472002 'Invalid %r file; expected keys %r, got %r' %
2003 (file_path, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:332004 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:202005 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:012006 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:202007 for suite_group in suite_dicts:
Garrett Beaty79339e182023-04-10 20:45:472008 if not self.check_ast_dict_formatted(suite_group, file_path, verbose):
2009 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:182010
Stephen Martinis5bef0fc2020-01-06 22:47:532011 for key, suite in zip(value.keys, value.values):
2012 # The compound suites are checked in
2013 # 'check_composition_type_test_suites()'
2014 if key.s == 'basic_suites':
2015 for group in suite.values:
Garrett Beaty79339e182023-04-10 20:45:472016 if not self.check_ast_dict_formatted(group, file_path, verbose):
2017 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:532018 break
Stephen Martinis54d64ad2018-09-21 22:16:202019
Garrett Beaty79339e182023-04-10 20:45:472020 elif file_path == self.args.test_suite_exceptions_pyl_path:
Stephen Martinis5bef0fc2020-01-06 22:47:532021 # Check the values for each test.
2022 for test in value.values:
2023 for kind, node in zip(test.keys, test.values):
2024 if isinstance(node, ast.Dict):
Garrett Beaty79339e182023-04-10 20:45:472025 if not self.check_ast_dict_formatted(node, file_path, verbose):
2026 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:532027 elif kind.s == 'remove_from':
2028 # Don't care about sorting; these are usually grouped, since the
2029 # same bug can affect multiple builders. Do want to make sure
2030 # there aren't duplicates.
Garrett Beaty79339e182023-04-10 20:45:472031 if not self.check_ast_list_formatted(
2032 node.elts, file_path, verbose, check_sorting=False):
2033 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:182034
2035 if bad_files:
2036 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:202037 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:532038 'unsorted, or have duplicates. Re-run this with --verbose to see '
2039 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:182040
Kenneth Russelleb60cbd22017-12-05 07:54:282041 def check_output_file_consistency(self, verbose=False):
2042 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012043 # All waterfalls/bucket .json files must have been written
2044 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:282045 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012046 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:162047 outputs = self.generate_outputs()
2048 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:012049 expected = self.jsonify(expected_contents)
Garrett Beaty79339e182023-04-10 20:45:472050 file_path = os.path.join(self.args.output_dir, filename + '.json')
Ben Pastenef21cda32023-03-30 22:00:572051 current = self.read_file(file_path)
Kenneth Russelleb60cbd22017-12-05 07:54:282052 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:012053 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:322054 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:012055 self.print_line('File ' + filename +
2056 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:322057 'contents:')
2058 for line in difflib.unified_diff(
2059 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:502060 current.splitlines(),
2061 fromfile='expected', tofile='current'):
2062 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:012063
2064 if ungenerated_files:
2065 raise BBGenErr(
2066 'The following files have not been properly '
2067 'autogenerated by generate_buildbot_json.py: ' +
2068 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:282069
Dirk Pranke772f55f2021-04-28 04:51:162070 for builder_group, builders in outputs.items():
2071 for builder, step_types in builders.items():
2072 for step_data in step_types.get('gtest_tests', []):
2073 step_name = step_data.get('name', step_data['test'])
2074 self._check_swarming_config(builder_group, builder, step_name,
2075 step_data)
2076 for step_data in step_types.get('isolated_scripts', []):
2077 step_name = step_data.get('name', step_data['isolate_name'])
2078 self._check_swarming_config(builder_group, builder, step_name,
2079 step_data)
2080
2081 def _check_swarming_config(self, filename, builder, step_name, step_data):
Ben Pastene338f56b2023-03-31 21:24:452082 # TODO(crbug.com/1203436): Ensure all swarming tests specify cpu, not
Dirk Pranke772f55f2021-04-28 04:51:162083 # just mac tests.
Garrett Beatybb18d532023-06-26 22:16:332084 if 'swarming' in step_data:
Dirk Pranke772f55f2021-04-28 04:51:162085 dimension_sets = step_data['swarming'].get('dimension_sets')
2086 if not dimension_sets:
Ben Pastene338f56b2023-03-31 21:24:452087 raise BBGenErr('%s: %s / %s : os must be specified for all '
Dirk Pranke772f55f2021-04-28 04:51:162088 'swarmed tests' % (filename, builder, step_name))
2089 for s in dimension_sets:
Ben Pastene338f56b2023-03-31 21:24:452090 if not s.get('os'):
2091 raise BBGenErr('%s: %s / %s : os must be specified for all '
2092 'swarmed tests' % (filename, builder, step_name))
2093 if 'Mac' in s.get('os') and not s.get('cpu'):
2094 raise BBGenErr('%s: %s / %s : cpu must be specified for mac '
Dirk Pranke772f55f2021-04-28 04:51:162095 'swarmed tests' % (filename, builder, step_name))
2096
Kenneth Russelleb60cbd22017-12-05 07:54:282097 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502098 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282099 self.check_output_file_consistency(verbose) # pragma: no cover
2100
Karen Qiane24b7ee2019-02-12 23:37:062101 def does_test_match(self, test_info, params_dict):
2102 """Checks to see if the test matches the parameters given.
2103
2104 Compares the provided test_info with the params_dict to see
2105 if the bot matches the parameters given. If so, returns True.
2106 Else, returns false.
2107
2108 Args:
2109 test_info (dict): Information about a specific bot provided
2110 in the format shown in waterfalls.pyl
2111 params_dict (dict): Dictionary of parameters and their values
2112 to look for in the bot
2113 Ex: {
2114 'device_os':'android',
2115 '--flag':True,
2116 'mixins': ['mixin1', 'mixin2'],
2117 'ex_key':'ex_value'
2118 }
2119
2120 """
2121 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2122 'kvm', 'pool', 'integrity'] # dimension parameters
2123 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2124 'can_use_on_swarming_builders']
2125 for param in params_dict:
2126 # if dimension parameter
2127 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2128 if not 'swarming' in test_info:
2129 return False
2130 swarming = test_info['swarming']
2131 if param in SWARMING_PARAMS:
2132 if not param in swarming:
2133 return False
2134 if not str(swarming[param]) == params_dict[param]:
2135 return False
2136 else:
2137 if not 'dimension_sets' in swarming:
2138 return False
2139 d_set = swarming['dimension_sets']
2140 # only looking at the first dimension set
2141 if not param in d_set[0]:
2142 return False
2143 if not d_set[0][param] == params_dict[param]:
2144 return False
2145
2146 # if flag
2147 elif param.startswith('--'):
2148 if not 'args' in test_info:
2149 return False
2150 if not param in test_info['args']:
2151 return False
2152
2153 # not dimension parameter/flag/mixin
2154 else:
2155 if not param in test_info:
2156 return False
2157 if not test_info[param] == params_dict[param]:
2158 return False
2159 return True
2160 def error_msg(self, msg):
2161 """Prints an error message.
2162
2163 In addition to a catered error message, also prints
2164 out where the user can find more help. Then, program exits.
2165 """
2166 self.print_line(msg + (' If you need more information, ' +
2167 'please run with -h or --help to see valid commands.'))
2168 sys.exit(1)
2169
2170 def find_bots_that_run_test(self, test, bots):
2171 matching_bots = []
2172 for bot in bots:
2173 bot_info = bots[bot]
2174 tests = self.flatten_tests_for_bot(bot_info)
2175 for test_info in tests:
2176 test_name = ""
2177 if 'name' in test_info:
2178 test_name = test_info['name']
2179 elif 'test' in test_info:
2180 test_name = test_info['test']
2181 if not test_name == test:
2182 continue
2183 matching_bots.append(bot)
2184 return matching_bots
2185
2186 def find_tests_with_params(self, tests, params_dict):
2187 matching_tests = []
2188 for test_name in tests:
2189 test_info = tests[test_name]
2190 if not self.does_test_match(test_info, params_dict):
2191 continue
2192 if not test_name in matching_tests:
2193 matching_tests.append(test_name)
2194 return matching_tests
2195
2196 def flatten_waterfalls_for_query(self, waterfalls):
2197 bots = {}
2198 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012199 waterfall_tests = self.generate_output_tests(waterfall)
2200 for bot in waterfall_tests:
2201 bot_info = waterfall_tests[bot]
2202 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062203 return bots
2204
2205 def flatten_tests_for_bot(self, bot_info):
2206 """Returns a list of flattened tests.
2207
2208 Returns a list of tests not grouped by test category
2209 for a specific bot.
2210 """
2211 TEST_CATS = self.get_test_generator_map().keys()
2212 tests = []
2213 for test_cat in TEST_CATS:
2214 if not test_cat in bot_info:
2215 continue
2216 test_cat_tests = bot_info[test_cat]
2217 tests = tests + test_cat_tests
2218 return tests
2219
2220 def flatten_tests_for_query(self, test_suites):
2221 """Returns a flattened dictionary of tests.
2222
2223 Returns a dictionary of tests associate with their
2224 configuration, not grouped by their test suite.
2225 """
2226 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232227 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062228 for test in test_suite:
2229 test_info = test_suite[test]
2230 test_name = test
2231 if 'name' in test_info:
2232 test_name = test_info['name']
2233 tests[test_name] = test_info
2234 return tests
2235
2236 def parse_query_filter_params(self, params):
2237 """Parses the filter parameters.
2238
2239 Creates a dictionary from the parameters provided
2240 to filter the bot array.
2241 """
2242 params_dict = {}
2243 for p in params:
2244 # flag
2245 if p.startswith("--"):
2246 params_dict[p] = True
2247 else:
2248 pair = p.split(":")
2249 if len(pair) != 2:
2250 self.error_msg('Invalid command.')
2251 # regular parameters
2252 if pair[1].lower() == "true":
2253 params_dict[pair[0]] = True
2254 elif pair[1].lower() == "false":
2255 params_dict[pair[0]] = False
2256 else:
2257 params_dict[pair[0]] = pair[1]
2258 return params_dict
2259
2260 def get_test_suites_dict(self, bots):
2261 """Returns a dictionary of bots and their tests.
2262
2263 Returns a dictionary of bots and a list of their associated tests.
2264 """
2265 test_suite_dict = dict()
2266 for bot in bots:
2267 bot_info = bots[bot]
2268 tests = self.flatten_tests_for_bot(bot_info)
2269 test_suite_dict[bot] = tests
2270 return test_suite_dict
2271
2272 def output_query_result(self, result, json_file=None):
2273 """Outputs the result of the query.
2274
2275 If a json file parameter name is provided, then
2276 the result is output into the json file. If not,
2277 then the result is printed to the console.
2278 """
2279 output = json.dumps(result, indent=2)
2280 if json_file:
2281 self.write_file(json_file, output)
2282 else:
2283 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062284
Joshua Hood56c673c2022-03-02 20:29:332285 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062286 def query(self, args):
2287 """Queries tests or bots.
2288
2289 Depending on the arguments provided, outputs a json of
2290 tests or bots matching the appropriate optional parameters provided.
2291 """
2292 # split up query statement
2293 query = args.query.split('/')
2294 self.load_configuration_files()
2295 self.resolve_configuration_files()
2296
2297 # flatten bots json
2298 tests = self.test_suites
2299 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2300
2301 cmd_class = query[0]
2302
2303 # For queries starting with 'bots'
2304 if cmd_class == "bots":
2305 if len(query) == 1:
2306 return self.output_query_result(bots, args.json)
2307 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332308 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062309 if query[1] == 'tests':
2310 test_suites_dict = self.get_test_suites_dict(bots)
2311 return self.output_query_result(test_suites_dict, args.json)
Joshua Hood56c673c2022-03-02 20:29:332312 self.error_msg("This query should be in the format: bots/tests.")
Karen Qiane24b7ee2019-02-12 23:37:062313
2314 else:
2315 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2316 % str(len(query)-1))
2317
2318 # For queries starting with 'bot'
2319 elif cmd_class == "bot":
2320 if not len(query) == 2 and not len(query) == 3:
2321 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2322 % str(len(query)-1))
2323 bot_id = query[1]
2324 if not bot_id in bots:
2325 self.error_msg("No bot named '" + bot_id + "' found.")
2326 bot_info = bots[bot_id]
2327 if len(query) == 2:
2328 return self.output_query_result(bot_info, args.json)
2329 if not query[2] == 'tests':
2330 self.error_msg("The query should be in the format:" +
2331 "bot/<bot-name>/tests.")
2332
2333 bot_tests = self.flatten_tests_for_bot(bot_info)
2334 return self.output_query_result(bot_tests, args.json)
2335
2336 # For queries starting with 'tests'
2337 elif cmd_class == "tests":
2338 if not len(query) == 1 and not len(query) == 2:
2339 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2340 % str(len(query)-1))
2341 flattened_tests = self.flatten_tests_for_query(tests)
2342 if len(query) == 1:
2343 return self.output_query_result(flattened_tests, args.json)
2344
2345 # create params dict
2346 params = query[1].split('&')
2347 params_dict = self.parse_query_filter_params(params)
2348 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2349 return self.output_query_result(matching_bots)
2350
2351 # For queries starting with 'test'
2352 elif cmd_class == "test":
2353 if not len(query) == 2 and not len(query) == 3:
2354 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2355 % str(len(query)-1))
2356 test_id = query[1]
2357 if len(query) == 2:
2358 flattened_tests = self.flatten_tests_for_query(tests)
2359 for test in flattened_tests:
2360 if test == test_id:
2361 return self.output_query_result(flattened_tests[test], args.json)
2362 self.error_msg("There is no test named %s." % test_id)
2363 if not query[2] == 'bots':
2364 self.error_msg("The query should be in the format: " +
2365 "test/<test-name>/bots")
2366 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2367 return self.output_query_result(bots_for_test)
2368
2369 else:
2370 self.error_msg("Your command did not match any valid commands." +
2371 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Joshua Hood56c673c2022-03-02 20:29:332372 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282373
Garrett Beaty1afaccc2020-06-25 19:58:152374 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282375 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502376 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062377 elif self.args.query:
2378 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282379 else:
Greg Gutermanf60eb052020-03-12 17:40:012380 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282381 return 0
2382
2383if __name__ == "__main__": # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152384 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2385 sys.exit(generator.main())