blob: f3c439ce924a18f9998333fc984355f89b3f77ef [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')
422 args.mixins_pyl_path = absolute_file_path('mixins.pyl')
423 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
Kenneth Russelleb60cbd22017-12-05 07:54:28608 def dictionary_merge(self, a, b, path=None, update=True):
609 """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:
616 if key in a:
617 if isinstance(a[key], dict) and isinstance(b[key], dict):
618 self.dictionary_merge(a[key], b[key], path + [str(key)])
619 elif a[key] == b[key]:
620 pass # same leaf value
621 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06622 # Args arrays are lists of strings. Just concatenate them,
623 # and don't sort them, in order to keep some needed
Weizhong Xia91b53362022-01-05 17:13:35624 # arguments adjacent (like --timeout-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28625 if all(isinstance(x, str)
626 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01627 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28628 else:
629 # TODO(kbr): this only works properly if the two arrays are
630 # the same length, which is currently always the case in the
631 # swarming dimension_sets that we have to merge. It will fail
632 # to merge / override 'args' arrays which are different
633 # length.
Jamie Madillcf4f8c72021-05-20 19:24:23634 for idx in range(len(b[key])):
Kenneth Russell8ceeabf2017-12-11 17:53:28635 try:
636 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
637 path + [str(key), str(idx)],
638 update=update)
Joshua Hood56c673c2022-03-02 20:29:33639 except (IndexError, TypeError) as e:
Josip Sokcevic7110fb382023-06-06 01:05:29640 raise BBGenErr('Error merging lists by key "%s" from source %s '
641 'into target %s at index %s. Verify target list '
642 'length is equal or greater than source' %
643 (str(key), str(b), str(a), str(idx))) from e
John Budorick5bc387fe2019-05-09 20:02:53644 elif update:
645 if b[key] is None:
646 del a[key]
647 else:
648 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28649 else:
650 raise BBGenErr('Conflict at %s' % '.'.join(
651 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53652 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28653 a[key] = b[key]
654 return a
655
John Budorickab108712018-09-01 00:12:21656 def initialize_args_for_test(
657 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21658 args = []
659 args.extend(generated_test.get('args', []))
660 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22661
Kenneth Russell8a386d42018-06-02 09:48:01662 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21663 val = generated_test.pop(key, [])
664 if fn(tester_config):
665 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01666
667 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
Brian Sheedy781c8ca42021-03-08 22:03:21668 add_conditional_args('lacros_args', self.is_lacros)
Kenneth Russell8a386d42018-06-02 09:48:01669 add_conditional_args('linux_args', self.is_linux)
670 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36671 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49672 add_conditional_args('mac_args', self.is_mac)
673 add_conditional_args('win_args', self.is_win)
674 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01675
John Budorickab108712018-09-01 00:12:21676 for key in additional_arg_keys or []:
677 args.extend(generated_test.pop(key, []))
678 args.extend(tester_config.get(key, []))
679
680 if args:
681 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01682
Kenneth Russelleb60cbd22017-12-05 07:54:28683 def initialize_swarming_dictionary_for_test(self, generated_test,
684 tester_config):
685 if 'swarming' not in generated_test:
686 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28687 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
688 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38689 'can_use_on_swarming_builders': tester_config.get('use_swarming',
690 True)
Dirk Pranke81ff51c2017-12-09 19:24:28691 })
Kenneth Russelleb60cbd22017-12-05 07:54:28692 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03693 if ('dimension_sets' not in generated_test['swarming'] and
694 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28695 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
696 tester_config['swarming']['dimension_sets'])
697 self.dictionary_merge(generated_test['swarming'],
698 tester_config['swarming'])
Brian Sheedybc984e242021-04-21 23:44:51699 # Apply any platform-specific Swarming dimensions after the generic ones.
Kenneth Russelleb60cbd22017-12-05 07:54:28700 if 'android_swarming' in generated_test:
701 if self.is_android(tester_config): # pragma: no cover
702 self.dictionary_merge(
703 generated_test['swarming'],
704 generated_test['android_swarming']) # pragma: no cover
705 del generated_test['android_swarming'] # pragma: no cover
Brian Sheedybc984e242021-04-21 23:44:51706 if 'chromeos_swarming' in generated_test:
707 if self.is_chromeos(tester_config): # pragma: no cover
708 self.dictionary_merge(
709 generated_test['swarming'],
710 generated_test['chromeos_swarming']) # pragma: no cover
711 del generated_test['chromeos_swarming'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28712
713 def clean_swarming_dictionary(self, swarming_dict):
714 # Clean out redundant entries from a test's "swarming" dictionary.
715 # This is really only needed to retain 100% parity with the
716 # handwritten JSON files, and can be removed once all the files are
717 # autogenerated.
718 if 'shards' in swarming_dict:
719 if swarming_dict['shards'] == 1: # pragma: no cover
720 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24721 if 'hard_timeout' in swarming_dict:
722 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
723 del swarming_dict['hard_timeout'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28724
Stephen Martinis0382bc12018-09-17 22:29:07725 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
726 waterfall):
727 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01728 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07729 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28730 # See if there are any exceptions that need to be merged into this
731 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49732 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28733 if modifications:
734 test = self.dictionary_merge(test, modifications)
Garrett Beatybfeff8f2023-06-16 18:57:25735 if (swarming_dict := test.get('swarming')) is not None:
736 if swarming_dict.get('can_use_on_swarming_builders', False):
737 self.clean_swarming_dictionary(swarming_dict)
738 else:
739 del test['swarming']
Ben Pastenee012aea42019-05-14 22:32:28740 # Ensure all Android Swarming tests run only on userdebug builds if another
741 # build type was not specified.
742 if 'swarming' in test and self.is_android(tester_config):
743 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22744 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28745 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37746 self.replace_test_args(test, test_name, tester_name)
Garrett Beatyafd33e0f2023-06-23 20:47:57747 if 'args' in test and not test['args']:
748 test.pop('args')
Ben Pastenee012aea42019-05-14 22:32:28749
Kenneth Russelleb60cbd22017-12-05 07:54:28750 return test
751
Brian Sheedye6ea0ee2019-07-11 02:54:37752 def replace_test_args(self, test, test_name, tester_name):
753 replacements = self.get_test_replacements(
754 test, test_name, tester_name) or {}
755 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23756 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37757 if key not in valid_replacement_keys:
758 raise BBGenErr(
759 'Given replacement key %s for %s on %s is not in the list of valid '
760 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23761 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37762 found_key = False
763 for i, test_key in enumerate(test.get(key, [])):
764 # Handle both the key/value being replaced being defined as two
765 # separate items or as key=value.
766 if test_key == replacement_key:
767 found_key = True
768 # Handle flags without values.
769 if replacement_val == None:
770 del test[key][i]
771 else:
772 test[key][i+1] = replacement_val
773 break
Joshua Hood56c673c2022-03-02 20:29:33774 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37775 found_key = True
776 if replacement_val == None:
777 del test[key][i]
778 else:
779 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
780 break
781 if not found_key:
782 raise BBGenErr('Could not find %s in existing list of values for key '
783 '%s in %s on %s' % (replacement_key, key, test_name,
784 tester_name))
785
Shenghua Zhangaba8bad2018-02-07 02:12:09786 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05787 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26788 True):
789 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06790 # are targeting CrOS hardware and so need the special trigger script.
791 dimension_sets = test['swarming']['dimension_sets']
Ben Pastenea9e583b2019-01-16 02:57:26792 if all('device_type' in ds for ds in dimension_sets):
793 test['trigger_script'] = {
794 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
795 }
Shenghua Zhangaba8bad2018-02-07 02:12:09796
Ben Pastene858f4be2019-01-09 23:52:09797 def add_android_presentation_args(self, tester_config, test_name, result):
798 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38799 bucket = tester_config.get('results_bucket', 'chromium-result-details')
800 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09801 if (result['swarming']['can_use_on_swarming_builders'] and not
802 tester_config.get('skip_merge_script', False)):
803 result['merge'] = {
804 'args': [
805 '--bucket',
John Budorick262ae112019-07-12 19:24:38806 bucket,
Ben Pastene858f4be2019-01-09 23:52:09807 '--test-name',
Rakib M. Hasanc9e01c62020-07-27 22:48:12808 result.get('name', test_name)
Ben Pastene858f4be2019-01-09 23:52:09809 ],
810 'script': '//build/android/pylib/results/presentation/'
811 'test_results_presentation.py',
812 }
Ben Pastene858f4be2019-01-09 23:52:09813 if not tester_config.get('skip_output_links', False):
814 result['swarming']['output_links'] = [
815 {
816 'link': [
817 'https://luci-logdog.appspot.com/v/?s',
818 '=android%2Fswarming%2Flogcats%2F',
819 '${TASK_ID}%2F%2B%2Funified_logcats',
820 ],
821 'name': 'shard #${SHARD_INDEX} logcats',
822 },
823 ]
824 if args:
825 result['args'] = args
826
Kenneth Russelleb60cbd22017-12-05 07:54:28827 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
828 test_config):
829 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15830 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28831 return None
832 result = copy.deepcopy(test_config)
833 if 'test' in result:
Rakib M. Hasanc9e01c62020-07-27 22:48:12834 if 'name' not in result:
835 result['name'] = test_name
Kenneth Russelleb60cbd22017-12-05 07:54:28836 else:
837 result['test'] = test_name
838 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21839
840 self.initialize_args_for_test(
841 result, tester_config, additional_arg_keys=['gtest_args'])
Jamie Madilla8be0d72020-10-02 05:24:04842 if self.is_android(tester_config) and tester_config.get(
Yuly Novikov26dd47052021-02-11 00:57:14843 'use_swarming', True):
844 if not test_config.get('use_isolated_scripts_api', False):
845 # TODO(https://crbug.com/1137998) make Android presentation work with
846 # isolated scripts in test_results_presentation.py merge script
847 self.add_android_presentation_args(tester_config, test_name, result)
848 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42849
Stephen Martinis0382bc12018-09-17 22:29:07850 result = self.update_and_cleanup_test(
851 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09852 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34853 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43854
Garrett Beatybfeff8f2023-06-16 18:57:25855 if (result.get('swarming', {}).get('can_use_on_swarming_builders')
856 and not result.get('merge')):
Jamie Madilla8be0d72020-10-02 05:24:04857 if test_config.get('use_isolated_scripts_api', False):
858 merge_script = 'standard_isolated_script_merge'
859 else:
860 merge_script = 'standard_gtest_merge'
861
Stephen Martinisbc7b7772019-05-01 22:01:43862 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04863 'script': '//testing/merge_scripts/%s.py' % merge_script,
Stephen Martinisbc7b7772019-05-01 22:01:43864 }
Kenneth Russelleb60cbd22017-12-05 07:54:28865 return result
866
867 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
868 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01869 if not self.should_run_on_tester(waterfall, tester_name, test_name,
870 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28871 return None
872 result = copy.deepcopy(test_config)
873 result['isolate_name'] = result.get('isolate_name', test_name)
Jeff Yoonb8bfdbf32020-03-13 19:14:43874 result['name'] = result.get('name', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28875 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01876 self.initialize_args_for_test(result, tester_config)
Yuly Novikov26dd47052021-02-11 00:57:14877 if self.is_android(tester_config) and tester_config.get(
878 'use_swarming', True):
879 if tester_config.get('use_android_presentation', False):
880 # TODO(https://crbug.com/1137998) make Android presentation work with
881 # isolated scripts in test_results_presentation.py merge script
882 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07883 result = self.update_and_cleanup_test(
884 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09885 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34886 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17887
Garrett Beatybfeff8f2023-06-16 18:57:25888 if (result.get('swarming', {}).get('can_use_on_swarming_builders')
889 and not result.get('merge')):
Stephen Martinisf50047062019-05-06 22:26:17890 # TODO(https://crbug.com/958376): Consider adding the ability to not have
891 # this default.
892 result['merge'] = {
893 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
Stephen Martinisf50047062019-05-06 22:26:17894 }
Kenneth Russelleb60cbd22017-12-05 07:54:28895 return result
896
897 def generate_script_test(self, waterfall, tester_name, tester_config,
898 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44899 # TODO(https://crbug.com/953072): Remove this check whenever a better
900 # long-term solution is implemented.
901 if (waterfall.get('forbid_script_tests', False) or
902 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
903 raise BBGenErr('Attempted to generate a script test on tester ' +
904 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01905 if not self.should_run_on_tester(waterfall, tester_name, test_name,
906 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28907 return None
908 result = {
909 'name': test_name,
910 'script': test_config['script']
911 }
Stephen Martinis0382bc12018-09-17 22:29:07912 result = self.update_and_cleanup_test(
913 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34914 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28915 return result
916
917 def generate_junit_test(self, waterfall, tester_name, tester_config,
918 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01919 if not self.should_run_on_tester(waterfall, tester_name, test_name,
920 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28921 return None
John Budorickdef6acb2019-09-17 22:51:09922 result = copy.deepcopy(test_config)
923 result.update({
John Budorickcadc4952019-09-16 23:51:37924 'name': test_name,
925 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09926 })
927 self.initialize_args_for_test(result, tester_config)
928 result = self.update_and_cleanup_test(
929 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34930 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28931 return result
932
Xinan Lin05fb9c1752020-12-17 00:15:52933 def generate_skylab_test(self, waterfall, tester_name, tester_config,
934 test_name, test_config):
935 if not self.should_run_on_tester(waterfall, tester_name, test_name,
936 test_config):
937 return None
938 result = copy.deepcopy(test_config)
939 result.update({
940 'test': test_name,
941 })
942 self.initialize_args_for_test(result, tester_config)
943 result = self.update_and_cleanup_test(result, test_name, tester_name,
944 tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34945 self.substitute_magic_args(result, tester_name, tester_config)
Xinan Lin05fb9c1752020-12-17 00:15:52946 return result
947
Stephen Martinis2a0667022018-09-25 22:31:14948 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01949 substitutions = {
950 # Any machine in waterfalls.pyl which desires to run GPU tests
951 # must provide the os_type key.
952 'os_type': tester_config['os_type'],
953 'gpu_vendor_id': '0',
954 'gpu_device_id': '0',
955 }
Brian Sheedyb6491ba2022-09-26 20:49:49956 if swarming_config.get('dimension_sets'):
957 dimension_set = swarming_config['dimension_sets'][0]
958 if 'gpu' in dimension_set:
959 # First remove the driver version, then split into vendor and device.
960 gpu = dimension_set['gpu']
961 if gpu != 'none':
962 gpu = gpu.split('-')[0].split(':')
963 substitutions['gpu_vendor_id'] = gpu[0]
964 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01965 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
966
967 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30968 test_name, test_config, is_android_webview,
969 is_cast_streaming):
Kenneth Russell8a386d42018-06-02 09:48:01970 # These are all just specializations of isolated script tests with
971 # a bunch of boilerplate command line arguments added.
972
973 # The step name must end in 'test' or 'tests' in order for the
974 # results to automatically show up on the flakiness dashboard.
975 # (At least, this was true some time ago.) Continue to use this
976 # naming convention for the time being to minimize changes.
977 step_name = test_config.get('name', test_name)
Ben Pastene5f231cf22022-05-05 18:03:07978 variant_id = test_config.get('variant_id')
979 if variant_id:
980 step_name = self.remove_variant_from_test_name(step_name, variant_id)
Kenneth Russell8a386d42018-06-02 09:48:01981 if not (step_name.endswith('test') or step_name.endswith('tests')):
982 step_name = '%s_tests' % step_name
Ben Pastene5f231cf22022-05-05 18:03:07983 if variant_id:
984 step_name = self.add_variant_to_test_name(step_name, variant_id)
Ben Pastene5ff45d82022-05-05 21:54:00985 if 'name' in test_config:
986 test_config['name'] = step_name
Kenneth Russell8a386d42018-06-02 09:48:01987 result = self.generate_isolated_script_test(
988 waterfall, tester_name, tester_config, step_name, test_config)
989 if not result:
990 return None
Chong Gub75754b32020-03-13 16:39:20991 result['isolate_name'] = test_config.get(
Brian Sheedyf74819b2021-06-04 01:38:38992 'isolate_name',
993 self.get_default_isolate_name(tester_config, is_android_webview))
Chan Liab7d8dd82020-04-24 23:42:19994
Chan Lia3ad1502020-04-28 05:32:11995 # Populate test_id_prefix.
Brian Sheedyf74819b2021-06-04 01:38:38996 gn_entry = self.gn_isolate_map[result['isolate_name']]
Chan Li17d969f92020-07-10 00:50:03997 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19998
Kenneth Russell8a386d42018-06-02 09:48:01999 args = result.get('args', [])
1000 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:141001
erikchen6da2d9b2018-08-03 23:01:141002 # These tests upload and download results from cloud storage and therefore
1003 # aren't idempotent yet. https://crbug.com/549140.
Garrett Beatybfeff8f2023-06-16 18:57:251004 if 'swarming' in result:
1005 result['swarming']['idempotent'] = False
erikchen6da2d9b2018-08-03 23:01:141006
Kenneth Russell44910c32018-12-03 23:35:111007 # The GPU tests act much like integration tests for the entire browser, and
1008 # tend to uncover flakiness bugs more readily than other test suites. In
1009 # order to surface any flakiness more readily to the developer of the CL
1010 # which is introducing it, we disable retries with patch on the commit
1011 # queue.
1012 result['should_retry_with_patch'] = False
1013
Fabrice de Ganscbd655f2022-08-04 20:15:301014 browser = ''
1015 if is_cast_streaming:
1016 browser = 'cast-streaming-shell'
1017 elif is_android_webview:
1018 browser = 'android-webview-instrumentation'
1019 else:
1020 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:521021
Greg Thompsoncec7d8d2023-01-10 19:11:531022 extra_browser_args = []
1023
Brian Sheedy4053a702020-07-28 02:09:521024 # Most platforms require --enable-logging=stderr to get useful browser logs.
1025 # However, this actively messes with logging on CrOS (because Chrome's
1026 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
1027 # in order to see JavaScript console messages. See
1028 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
Greg Thompsoncec7d8d2023-01-10 19:11:531029 if self.is_chromeos(tester_config):
1030 extra_browser_args.append('--log-level=0')
1031 elif not self.is_fuchsia(tester_config) or browser != 'fuchsia-chrome':
1032 # Stderr logging is not needed for Chrome browser on Fuchsia, as ordinary
1033 # logging via syslog is captured.
1034 extra_browser_args.append('--enable-logging=stderr')
1035
1036 # --expose-gc allows the WebGL conformance tests to more reliably
1037 # reproduce GC-related bugs in the V8 bindings.
1038 extra_browser_args.append('--js-flags=--expose-gc')
Brian Sheedy4053a702020-07-28 02:09:521039
Kenneth Russell8a386d42018-06-02 09:48:011040 args = [
Bo Liu555a0f92019-03-29 12:11:561041 test_to_run,
1042 '--show-stdout',
1043 '--browser=%s' % browser,
1044 # --passthrough displays more of the logging in Telemetry when
1045 # run via typ, in particular some of the warnings about tests
1046 # being expected to fail, but passing.
1047 '--passthrough',
1048 '-v',
Brian Sheedy814e0482022-10-03 23:24:121049 '--stable-jobs',
Greg Thompsoncec7d8d2023-01-10 19:11:531050 '--extra-browser-args=%s' % ' '.join(extra_browser_args),
Kenneth Russell8a386d42018-06-02 09:48:011051 ] + args
Garrett Beatybfeff8f2023-06-16 18:57:251052 result['args'] = self.maybe_fixup_args_array(
1053 self.substitute_gpu_args(tester_config, result.get('swarming', {}),
1054 args))
Kenneth Russell8a386d42018-06-02 09:48:011055 return result
1056
Brian Sheedyf74819b2021-06-04 01:38:381057 def get_default_isolate_name(self, tester_config, is_android_webview):
1058 if self.is_android(tester_config):
1059 if is_android_webview:
1060 return 'telemetry_gpu_integration_test_android_webview'
1061 return (
1062 'telemetry_gpu_integration_test' +
1063 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:331064 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:171065 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:331066 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:381067
Kenneth Russelleb60cbd22017-12-05 07:54:281068 def get_test_generator_map(self):
1069 return {
Bo Liu555a0f92019-03-29 12:11:561070 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301071 GPUTelemetryTestGenerator(self, is_android_webview=True),
1072 'cast_streaming_tests':
1073 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:561074 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301075 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561076 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301077 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561078 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301079 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561080 'junit_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301081 JUnitGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561082 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301083 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:521084 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301085 SkylabGenerator(self),
Brian Sheedyb6491ba2022-09-26 20:49:491086 'skylab_gpu_telemetry_tests':
1087 SkylabGPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281088 }
1089
Kenneth Russell8a386d42018-06-02 09:48:011090 def get_test_type_remapper(self):
1091 return {
Fabrice de Gans223272482022-08-08 16:56:571092 # These are a specialization of isolated_scripts with a bunch of
1093 # boilerplate command line arguments added to each one.
1094 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
1095 'cast_streaming_tests': 'isolated_scripts',
1096 'gpu_telemetry_tests': 'isolated_scripts',
Brian Sheedyb6491ba2022-09-26 20:49:491097 # These are the same as existing test types, just configured to run
1098 # in Skylab instead of via normal swarming.
1099 'skylab_gpu_telemetry_tests': 'skylab_tests',
Kenneth Russell8a386d42018-06-02 09:48:011100 }
1101
Jeff Yoon67c3e832020-02-08 07:39:381102 def check_composition_type_test_suites(self, test_type,
1103 additional_validators=None):
1104 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1105 validators = [check_compound_references,
1106 check_basic_references,
1107 check_conflicting_definitions]
1108 if additional_validators:
1109 validators += additional_validators
1110
1111 target_suites = self.test_suites.get(test_type, {})
1112 other_test_type = ('compound_suites'
1113 if test_type == 'matrix_compound_suites'
1114 else 'matrix_compound_suites')
1115 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011116 basic_suites = self.test_suites.get('basic_suites', {})
1117
Jamie Madillcf4f8c72021-05-20 19:24:231118 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011119 if suite in basic_suites:
1120 raise BBGenErr('%s names may not duplicate basic test suite names '
1121 '(error found while processsing %s)'
1122 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011123
Jeff Yoon67c3e832020-02-08 07:39:381124 seen_tests = {}
1125 for sub_suite in suite_def:
1126 for validator in validators:
1127 validator(
1128 basic_suites=basic_suites,
1129 other_test_suites=other_suites,
1130 seen_tests=seen_tests,
1131 sub_suite=sub_suite,
1132 suite=suite,
1133 suite_def=suite_def,
1134 target_test_suites=target_suites,
1135 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051136 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381137 )
Kenneth Russelleb60cbd22017-12-05 07:54:281138
Stephen Martinis54d64ad2018-09-21 22:16:201139 def flatten_test_suites(self):
1140 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011141 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1142 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231143 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011144 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201145 self.test_suites = new_test_suites
1146
Chan Lia3ad1502020-04-28 05:32:111147 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231148 for suite in self.test_suites['basic_suites'].values():
1149 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561150 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411151
1152 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
John Palmera8515fca2021-05-20 03:35:321153 # 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:411154 # TODO(crbug.com/1035124): clean this up.
1155 isolate_name = test.get('test') or test.get('isolate_name') or key
1156 gn_entry = self.gn_isolate_map.get(isolate_name)
1157 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281158 label = gn_entry['label']
1159
1160 if label.count(':') != 1:
1161 raise BBGenErr(
1162 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1163 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1164 (label, isolate_name))
1165 if label.split(':')[1] != isolate_name:
1166 raise BBGenErr(
1167 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1168 ' label "%s" see http://crbug.com/1071091 for details.' %
1169 (isolate_name, label))
1170
Chan Lia3ad1502020-04-28 05:32:111171 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411172 else: # pragma: no cover
1173 # Some tests do not have an entry gn_isolate_map.pyl, such as
1174 # telemetry tests.
1175 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
1176 pass
1177
Kenneth Russelleb60cbd22017-12-05 07:54:281178 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011179 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201180
Jeff Yoon8154e582019-12-03 23:30:011181 compound_suites = self.test_suites.get('compound_suites', {})
1182 # check_composition_type_test_suites() checks that all basic suites
1183 # referenced by compound suites exist.
1184 basic_suites = self.test_suites.get('basic_suites')
1185
Jamie Madillcf4f8c72021-05-20 19:24:231186 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011187 # Resolve this to a dictionary.
1188 full_suite = {}
1189 for entry in value:
1190 suite = basic_suites[entry]
1191 full_suite.update(suite)
1192 compound_suites[name] = full_suite
1193
Jeff Yoon85fb8df2020-08-20 16:47:431194 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381195 """ Merge variant-defined configurations to each test case definition in a
1196 test suite.
1197
1198 The output maps a unique test name to an array of configurations because
1199 there may exist more than one definition for a test name using variants. The
1200 test name is referenced while mapping machines to test suites, so unpacking
1201 the array is done by the generators.
1202
1203 Args:
1204 basic_test_definition: a {} defined test suite in the format
1205 test_name:test_config
1206 variants: an [] of {} defining configurations to be applied to each test
1207 case in the basic test_definition
1208
1209 Return:
1210 a {} of test_name:[{}], where each {} is a merged configuration
1211 """
1212
1213 # Each test in a basic test suite will have a definition per variant.
1214 test_suite = {}
Jamie Madillcf4f8c72021-05-20 19:24:231215 for test_name, test_config in basic_test_definition.items():
Jeff Yoon67c3e832020-02-08 07:39:381216 definitions = []
1217 for variant in variants:
Jeff Yoonda581c32020-03-06 03:56:051218 # Unpack the variant from variants.pyl if it's string based.
1219 if isinstance(variant, str):
1220 variant = self.variants[variant]
1221
Jieting Yangef6b1042021-11-30 21:33:481222 # If 'enabled' is set to False, we will not use this variant;
1223 # otherwise if the variant doesn't include 'enabled' variable or
1224 # 'enabled' is set to True, we will use this variant
1225 if not variant.get('enabled', True):
1226 continue
Jeff Yoon67c3e832020-02-08 07:39:381227 # Clone a copy of test_config so that we can have a uniquely updated
1228 # version of it per variant
1229 cloned_config = copy.deepcopy(test_config)
1230 # The variant definition needs to be re-used for each test, so we'll
1231 # create a clone and work with it as well.
1232 cloned_variant = copy.deepcopy(variant)
1233
1234 cloned_config['args'] = (cloned_config.get('args', []) +
1235 cloned_variant.get('args', []))
1236 cloned_config['mixins'] = (cloned_config.get('mixins', []) +
Jeff Yoon85fb8df2020-08-20 16:47:431237 cloned_variant.get('mixins', []) + mixins)
Jeff Yoon67c3e832020-02-08 07:39:381238
Sven Zhengb51bd0482022-08-26 18:26:251239 description = []
Sven Zhengdcf2ddf2022-08-30 04:24:331240 if cloned_config.get('description'):
1241 description.append(cloned_config.get('description'))
1242 if cloned_variant.get('description'):
1243 description.append(cloned_variant.get('description'))
Sven Zhengb51bd0482022-08-26 18:26:251244 if description:
1245 cloned_config['description'] = '\n'.join(description)
Jeff Yoon67c3e832020-02-08 07:39:381246 basic_swarming_def = cloned_config.get('swarming', {})
1247 variant_swarming_def = cloned_variant.get('swarming', {})
1248 if basic_swarming_def and variant_swarming_def:
1249 if ('dimension_sets' in basic_swarming_def and
1250 'dimension_sets' in variant_swarming_def):
1251 # Retain swarming dimension set merge behavior when both variant and
1252 # the basic test configuration both define it
1253 self.dictionary_merge(basic_swarming_def, variant_swarming_def)
1254 # Remove dimension_sets from the variant definition, so that it does
1255 # not replace what's been done by dictionary_merge in the update
1256 # call below.
1257 del variant_swarming_def['dimension_sets']
1258
1259 # Update the swarming definition with whatever is defined for swarming
1260 # by the variant.
1261 basic_swarming_def.update(variant_swarming_def)
1262 cloned_config['swarming'] = basic_swarming_def
1263
Xinan Lin05fb9c1752020-12-17 00:15:521264 # Copy all skylab fields defined by the variant.
1265 skylab_config = cloned_variant.get('skylab')
1266 if skylab_config:
1267 for k, v in skylab_config.items():
Jieting Yangef6b1042021-11-30 21:33:481268 # cros_chrome_version is the ash chrome version in the cros img
1269 # in the variant of cros_board. We don't want to include it in
1270 # the final json files; so remove it.
1271 if k == 'cros_chrome_version':
1272 continue
Xinan Lin05fb9c1752020-12-17 00:15:521273 cloned_config[k] = v
1274
Jeff Yoon67c3e832020-02-08 07:39:381275 # The identifier is used to make the name of the test unique.
1276 # Generators in the recipe uniquely identify a test by it's name, so we
1277 # don't want to have the same name for each variant.
Ben Pastene5f231cf22022-05-05 18:03:071278 cloned_config['name'] = self.add_variant_to_test_name(
1279 cloned_config.get('name') or test_name,
1280 cloned_variant['identifier'])
1281
1282 # Attach the variant identifier to the test config so downstream
1283 # generators can make modifications based on the original name. This
1284 # is mainly used in generate_gpu_telemetry_test().
1285 cloned_config['variant_id'] = cloned_variant['identifier']
1286
Jeff Yoon67c3e832020-02-08 07:39:381287 definitions.append(cloned_config)
1288 test_suite[test_name] = definitions
1289 return test_suite
1290
Jeff Yoon8154e582019-12-03 23:30:011291 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381292 self.check_composition_type_test_suites('matrix_compound_suites',
1293 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011294
1295 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381296 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011297 # referenced by matrix suites exist.
1298 basic_suites = self.test_suites.get('basic_suites')
1299
Jamie Madillcf4f8c72021-05-20 19:24:231300 for test_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011301 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381302
Jamie Madillcf4f8c72021-05-20 19:24:231303 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381304 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1305
1306 if 'variants' in mtx_test_suite_config:
Jeff Yoon85fb8df2020-08-20 16:47:431307 mixins = mtx_test_suite_config.get('mixins', [])
Jeff Yoon67c3e832020-02-08 07:39:381308 result = self.resolve_variants(basic_test_def,
Jeff Yoon85fb8df2020-08-20 16:47:431309 mtx_test_suite_config['variants'],
1310 mixins)
Jeff Yoon67c3e832020-02-08 07:39:381311 full_suite.update(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271312 else:
1313 suite = basic_suites[test_suite]
1314 full_suite.update(suite)
Jeff Yoon67c3e832020-02-08 07:39:381315 matrix_compound_suites[test_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281316
1317 def link_waterfalls_to_test_suites(self):
1318 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231319 for tester_name, tester in waterfall['machines'].items():
1320 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281321 if not value in self.test_suites:
1322 # Hard / impossible to cover this in the unit test.
1323 raise self.unknown_test_suite(
1324 value, tester_name, waterfall['name']) # pragma: no cover
1325 tester['test_suites'][suite] = self.test_suites[value]
1326
1327 def load_configuration_files(self):
Garrett Beaty79339e182023-04-10 20:45:471328 self.waterfalls = self.load_pyl_file(self.args.waterfalls_pyl_path)
1329 self.test_suites = self.load_pyl_file(self.args.test_suites_pyl_path)
1330 self.exceptions = self.load_pyl_file(
1331 self.args.test_suite_exceptions_pyl_path)
1332 self.mixins = self.load_pyl_file(self.args.mixins_pyl_path)
1333 self.gn_isolate_map = self.load_pyl_file(self.args.gn_isolate_map_pyl_path)
Chong Guee622242020-10-28 18:17:351334 for isolate_map in self.args.isolate_map_files:
1335 isolate_map = self.load_pyl_file(isolate_map)
1336 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1337 if duplicates:
1338 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1339 ', '.join(duplicates))
1340 self.gn_isolate_map.update(isolate_map)
1341
Garrett Beaty79339e182023-04-10 20:45:471342 self.variants = self.load_pyl_file(self.args.variants_pyl_path)
Kenneth Russelleb60cbd22017-12-05 07:54:281343
1344 def resolve_configuration_files(self):
Chan Lia3ad1502020-04-28 05:32:111345 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281346 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011347 self.resolve_matrix_compound_test_suites()
1348 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281349 self.link_waterfalls_to_test_suites()
1350
Nico Weberd18b8962018-05-16 19:39:381351 def unknown_bot(self, bot_name, waterfall_name):
1352 return BBGenErr(
1353 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1354
Kenneth Russelleb60cbd22017-12-05 07:54:281355 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1356 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381357 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281358 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1359
1360 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1361 return BBGenErr(
1362 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1363 ' on waterfall ' + waterfall_name)
1364
Stephen Martinisb72f6d22018-10-04 23:29:011365 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071366 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321367
1368 Checks in the waterfall, builder, and test objects for mixins.
1369 """
1370 def valid_mixin(mixin_name):
1371 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011372 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321373 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381374
Stephen Martinisb6a50492018-09-12 23:59:321375 def must_be_list(mixins, typ, name):
1376 """Asserts that given mixins are a list."""
1377 if not isinstance(mixins, list):
1378 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1379
Brian Sheedy7658c982020-01-08 02:27:581380 test_name = test.get('name')
1381 remove_mixins = set()
1382 if 'remove_mixins' in builder:
1383 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1384 for rm in builder['remove_mixins']:
1385 valid_mixin(rm)
1386 remove_mixins.add(rm)
1387 if 'remove_mixins' in test:
1388 must_be_list(test['remove_mixins'], 'test', test_name)
1389 for rm in test['remove_mixins']:
1390 valid_mixin(rm)
1391 remove_mixins.add(rm)
1392 del test['remove_mixins']
1393
Stephen Martinisb72f6d22018-10-04 23:29:011394 if 'mixins' in waterfall:
1395 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1396 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581397 if mixin in remove_mixins:
1398 continue
Stephen Martinisb6a50492018-09-12 23:59:321399 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531400 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321401
Stephen Martinisb72f6d22018-10-04 23:29:011402 if 'mixins' in builder:
1403 must_be_list(builder['mixins'], 'builder', builder_name)
1404 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581405 if mixin in remove_mixins:
1406 continue
Stephen Martinisb6a50492018-09-12 23:59:321407 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531408 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321409
Stephen Martinisb72f6d22018-10-04 23:29:011410 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071411 return test
1412
Stephen Martinis2a0667022018-09-25 22:31:141413 if not test_name:
1414 test_name = test.get('test')
1415 if not test_name: # pragma: no cover
1416 # Not the best name, but we should say something.
1417 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:011418 must_be_list(test['mixins'], 'test', test_name)
1419 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581420 # We don't bother checking if the given mixin is in remove_mixins here
1421 # since this is already the lowest level, so if a mixin is added here that
1422 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071423 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531424 test = self.apply_mixin(self.mixins[mixin], test, builder)
Jeff Yoon67c3e832020-02-08 07:39:381425 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071426 return test
Stephen Martinisb6a50492018-09-12 23:59:321427
Austin Eng148d9f0f2022-02-08 19:18:531428 def apply_mixin(self, mixin, test, builder):
Stephen Martinisb72f6d22018-10-04 23:29:011429 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321430
Garrett Beaty4c35b142023-06-23 21:01:231431 A mixin is applied by copying all fields from the mixin into the
1432 test with the following exceptions:
1433 * For the various *args keys, the test's existing value (an empty
1434 list if not present) will be extended with the mixin's value.
1435 * The sub-keys of the swarming value will be copied to the test's
1436 swarming value with the following exceptions:
1437 * For the dimension_sets and named_caches sub-keys, the test's
1438 existing value (an empty list if not present) will be extended
1439 with the mixin's value.
1440 * For the dimensions sub-key, after extending the test's
1441 dimension_sets as specified above, each dimension set will be
1442 updated with the value of the dimensions sub-key. If there are
1443 no dimension sets, then one will be added that contains the
1444 specified dimensions.
Stephen Martinisb6a50492018-09-12 23:59:321445 """
Garrett Beaty4c35b142023-06-23 21:01:231446
Stephen Martinisb6a50492018-09-12 23:59:321447 new_test = copy.deepcopy(test)
1448 mixin = copy.deepcopy(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011449 if 'swarming' in mixin:
1450 swarming_mixin = mixin['swarming']
1451 new_test.setdefault('swarming', {})
Brian Sheedycae63b22020-06-10 22:52:111452 # Copy over any explicit dimension sets first so that they will be updated
1453 # by any subsequent 'dimensions' entries.
1454 if 'dimension_sets' in swarming_mixin:
1455 existing_dimension_sets = new_test['swarming'].setdefault(
1456 'dimension_sets', [])
1457 # Appending to the existing list could potentially result in different
1458 # behavior depending on the order the mixins were applied, but that's
1459 # already the case for other parts of mixins, so trust that the user
1460 # will verify that the generated output is correct before submitting.
1461 for dimension_set in swarming_mixin['dimension_sets']:
1462 if dimension_set not in existing_dimension_sets:
1463 existing_dimension_sets.append(dimension_set)
1464 del swarming_mixin['dimension_sets']
Stephen Martinisb72f6d22018-10-04 23:29:011465 if 'dimensions' in swarming_mixin:
1466 new_test['swarming'].setdefault('dimension_sets', [{}])
1467 for dimension_set in new_test['swarming']['dimension_sets']:
1468 dimension_set.update(swarming_mixin['dimensions'])
1469 del swarming_mixin['dimensions']
Garrett Beaty4c35b142023-06-23 21:01:231470 if 'named_caches' in swarming_mixin:
1471 new_test['swarming'].setdefault('named_caches', []).extend(
1472 swarming_mixin['named_caches'])
1473 del swarming_mixin['named_caches']
Stephen Martinisb72f6d22018-10-04 23:29:011474 # python dict update doesn't do recursion at all. Just hard code the
1475 # nested update we need (mixin['swarming'] shouldn't clobber
1476 # test['swarming'], but should update it).
1477 new_test['swarming'].update(swarming_mixin)
1478 del mixin['swarming']
1479
Garrett Beaty4c35b142023-06-23 21:01:231480 # Array so we can assign to it in a nested scope.
1481 args_need_fixup = ['args' in mixin]
1482
1483 for a in (
1484 'args',
1485 'precommit_args',
1486 'non_precommit_args',
1487 'desktop_args',
1488 'lacros_args',
1489 'linux_args',
1490 'android_args',
1491 'chromeos_args',
1492 'mac_args',
1493 'win_args',
1494 'win64_args',
1495 ):
1496 if (value := mixin.pop(a, None)) is None:
1497 continue
1498 if not isinstance(value, list):
1499 raise BBGenErr(f'"{a}" must be a list')
1500 new_test.setdefault(a, []).extend(value)
1501
1502 # TODO(gbeaty) Remove this once all mixins have removed '$mixin_append'
Wezc0e835b702018-10-30 00:38:411503 if '$mixin_append' in mixin:
1504 # Values specified under $mixin_append should be appended to existing
1505 # lists, rather than replacing them.
1506 mixin_append = mixin['$mixin_append']
Austin Eng148d9f0f2022-02-08 19:18:531507 del mixin['$mixin_append']
Zhaoyang Li473dd0ae2021-05-10 18:28:281508
1509 # Append swarming named cache and delete swarming key, since it's under
1510 # another layer of dict.
1511 if 'named_caches' in mixin_append.get('swarming', {}):
1512 new_test['swarming'].setdefault('named_caches', [])
1513 new_test['swarming']['named_caches'].extend(
1514 mixin_append['swarming']['named_caches'])
1515 if len(mixin_append['swarming']) > 1:
1516 raise BBGenErr('Only named_caches is supported under swarming key in '
1517 '$mixin_append, but there are: %s' %
1518 sorted(mixin_append['swarming'].keys()))
1519 del mixin_append['swarming']
Wezc0e835b702018-10-30 00:38:411520 for key in mixin_append:
1521 new_test.setdefault(key, [])
1522 if not isinstance(mixin_append[key], list):
1523 raise BBGenErr(
1524 'Key "' + key + '" in $mixin_append must be a list.')
1525 if not isinstance(new_test[key], list):
1526 raise BBGenErr(
1527 'Cannot apply $mixin_append to non-list "' + key + '".')
1528 new_test[key].extend(mixin_append[key])
Austin Eng148d9f0f2022-02-08 19:18:531529
Wezc0e835b702018-10-30 00:38:411530 if 'args' in mixin_append:
Austin Eng148d9f0f2022-02-08 19:18:531531 args_need_fixup[0] = True
1532
Garrett Beaty4c35b142023-06-23 21:01:231533 args = new_test.get('args', [])
Austin Eng148d9f0f2022-02-08 19:18:531534
Garrett Beaty4c35b142023-06-23 21:01:231535 def add_conditional_args(key, fn):
1536 val = new_test.pop(key, [])
1537 if val and fn(builder):
1538 args.extend(val)
1539 args_need_fixup[0] = True
Austin Eng148d9f0f2022-02-08 19:18:531540
Garrett Beaty4c35b142023-06-23 21:01:231541 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
1542 add_conditional_args('lacros_args', self.is_lacros)
1543 add_conditional_args('linux_args', self.is_linux)
1544 add_conditional_args('android_args', self.is_android)
1545 add_conditional_args('chromeos_args', self.is_chromeos)
1546 add_conditional_args('mac_args', self.is_mac)
1547 add_conditional_args('win_args', self.is_win)
1548 add_conditional_args('win64_args', self.is_win64)
1549
1550 if args_need_fixup[0]:
1551 new_test['args'] = self.maybe_fixup_args_array(args)
Wezc0e835b702018-10-30 00:38:411552
Stephen Martinisb72f6d22018-10-04 23:29:011553 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321554 return new_test
1555
Greg Gutermanf60eb052020-03-12 17:40:011556 def generate_output_tests(self, waterfall):
1557 """Generates the tests for a waterfall.
1558
1559 Args:
1560 waterfall: a dictionary parsed from a master pyl file
1561 Returns:
1562 A dictionary mapping builders to test specs
1563 """
1564 return {
Jamie Madillcf4f8c72021-05-20 19:24:231565 name: self.get_tests_for_config(waterfall, name, config)
1566 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011567 }
1568
1569 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531570 generator_map = self.get_test_generator_map()
1571 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281572
Greg Gutermanf60eb052020-03-12 17:40:011573 tests = {}
1574 # Copy only well-understood entries in the machine's configuration
1575 # verbatim into the generated JSON.
1576 if 'additional_compile_targets' in config:
1577 tests['additional_compile_targets'] = config[
1578 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231579 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011580 if test_type not in generator_map:
1581 raise self.unknown_test_suite_type(
1582 test_type, name, waterfall['name']) # pragma: no cover
1583 test_generator = generator_map[test_type]
1584 # Let multiple kinds of generators generate the same kinds
1585 # of tests. For example, gpu_telemetry_tests are a
1586 # specialization of isolated_scripts.
1587 new_tests = test_generator.generate(
1588 waterfall, name, config, input_tests)
1589 remapped_test_type = test_type_remapper.get(test_type, test_type)
1590 tests[remapped_test_type] = test_generator.sort(
1591 tests.get(remapped_test_type, []) + new_tests)
1592
1593 return tests
1594
1595 def jsonify(self, all_tests):
1596 return json.dumps(
1597 all_tests, indent=2, separators=(',', ': '),
1598 sort_keys=True) + '\n'
1599
1600 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281601 self.load_configuration_files()
1602 self.resolve_configuration_files()
1603 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011604 result = collections.defaultdict(dict)
1605
Stephanie Kim572b43c02023-04-13 14:24:131606 if os.path.exists(self.args.autoshard_exceptions_json_path):
1607 autoshards = json.loads(
1608 self.read_file(self.args.autoshard_exceptions_json_path))
1609 else:
1610 autoshards = {}
1611
Dirk Pranke6269d302020-10-01 00:14:391612 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011613 for waterfall in self.waterfalls:
1614 for field in required_fields:
1615 # Verify required fields
1616 if field not in waterfall:
1617 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1618
1619 # Handle filter flag, if specified
1620 if filters and waterfall['name'] not in filters:
1621 continue
1622
1623 # Join config files and hardcoded values together
1624 all_tests = self.generate_output_tests(waterfall)
1625 result[waterfall['name']] = all_tests
1626
Stephanie Kim572b43c02023-04-13 14:24:131627 if not autoshards:
1628 continue
1629 for builder, test_spec in all_tests.items():
1630 for target_type, test_list in test_spec.items():
1631 if target_type == 'additional_compile_targets':
1632 continue
1633 for test_dict in test_list:
1634 # Suites that apply variants or other customizations will create
1635 # test_dicts that have "name" value that is different from the
1636 # "test" value. Regular suites without any variations will only have
1637 # "test" and no "name".
1638 # e.g. name = vulkan_swiftshader_content_browsertests, but
1639 # test = content_browsertests and
1640 # test_id_prefix = "ninja://content/test:content_browsertests/"
1641 # Check for "name" first and then fallback to "test"
1642 test_name = test_dict.get('name') or test_dict.get('test')
1643 if not test_name:
1644 continue
1645 shard_info = autoshards.get(waterfall['name'],
1646 {}).get(builder, {}).get(test_name)
1647 if shard_info:
1648 test_dict['swarming'].update(
1649 {'shards': int(shard_info['shards'])})
1650
Greg Gutermanf60eb052020-03-12 17:40:011651 # Add do not edit warning
1652 for tests in result.values():
1653 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1654 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1655
1656 return result
1657
1658 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281659 suffix = '.json'
1660 if self.args.new_files:
1661 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011662
1663 for filename, contents in result.items():
1664 jsonstr = self.jsonify(contents)
Garrett Beaty79339e182023-04-10 20:45:471665 file_path = os.path.join(self.args.output_dir, filename + suffix)
1666 self.write_file(file_path, jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281667
Nico Weberd18b8962018-05-16 19:39:381668 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161669 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421670 # NOTE: This reference can cause issues; if a file changes there, the
1671 # presubmit here won't be run by default. A manually maintained list there
1672 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1673 # references to configs outside of this directory are added, please change
1674 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1675 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491676
Garrett Beaty7e866fc2021-06-16 14:12:101677 # Get the generated project.pyl so we can check if we should be enforcing
1678 # that the specs are for builders that actually exist
1679 # If not, return None to indicate that we won't enforce that builders in
1680 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491681 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1682 'project.pyl')
1683 if os.path.exists(project_pyl_path):
1684 settings = ast.literal_eval(self.read_file(project_pyl_path))
1685 if not settings.get('validate_source_side_specs_have_builder', True):
1686 return None
1687
Nico Weberd18b8962018-05-16 19:39:381688 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311689 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161690 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1691 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431692 for c in milo_configs:
1693 for l in self.read_file(c).splitlines():
1694 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311695 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431696 continue
1697 # l looks like
1698 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1699 # Extract win_chromium_dbg_ng part.
1700 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381701 return bot_names
1702
Ben Pastene9a010082019-09-25 20:41:371703 def get_internal_waterfalls(self):
1704 # Similar to get_builders_that_do_not_actually_exist above, but for
1705 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201706 return [
Kramer Ge3bf853a2023-04-13 19:39:471707 'chrome', 'chrome.pgo', 'chrome.gpu.fyi', 'internal.chrome.fyi',
1708 'internal.chromeos.fyi', 'internal.soda'
Yuke Liaoe6c23dd2021-07-28 16:12:201709 ]
Ben Pastene9a010082019-09-25 20:41:371710
Stephen Martinisf83893722018-09-19 00:02:181711 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201712 self.check_input_files_sorting(verbose)
1713
Kenneth Russelleb60cbd22017-12-05 07:54:281714 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011715 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381716 self.check_composition_type_test_suites('matrix_compound_suites',
1717 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111718 self.resolve_test_id_prefixes()
Stephen Martinis54d64ad2018-09-21 22:16:201719 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381720
1721 # All bots should exist.
1722 bot_names = self.get_valid_bot_names()
Garrett Beaty2a02de3c2020-05-15 13:57:351723 if bot_names is not None:
1724 internal_waterfalls = self.get_internal_waterfalls()
1725 for waterfall in self.waterfalls:
1726 # TODO(crbug.com/991417): Remove the need for this exception.
1727 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011728 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351729 for bot_name in waterfall['machines']:
Garrett Beaty2a02de3c2020-05-15 13:57:351730 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581731 if waterfall['name'] in [
1732 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1733 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351734 # TODO(thakis): Remove this once these bots move to luci.
1735 continue # pragma: no cover
1736 if waterfall['name'] in ['tryserver.webrtc',
1737 'webrtc.chromium.fyi.experimental']:
1738 # These waterfalls have their bot configs in a different repo.
1739 # so we don't know about their bot names.
1740 continue # pragma: no cover
1741 if waterfall['name'] in ['client.devtools-frontend.integration',
1742 'tryserver.devtools-frontend',
1743 'chromium.devtools-frontend']:
1744 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201745 if waterfall['name'] in ['client.openscreen.chromium']:
1746 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351747 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381748
Kenneth Russelleb60cbd22017-12-05 07:54:281749 # All test suites must be referenced.
1750 suites_seen = set()
1751 generator_map = self.get_test_generator_map()
1752 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231753 for bot_name, tester in waterfall['machines'].items():
1754 for suite_type, suite in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281755 if suite_type not in generator_map:
1756 raise self.unknown_test_suite_type(suite_type, bot_name,
1757 waterfall['name'])
1758 if suite not in self.test_suites:
1759 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1760 suites_seen.add(suite)
1761 # Since we didn't resolve the configuration files, this set
1762 # includes both composition test suites and regular ones.
1763 resolved_suites = set()
1764 for suite_name in suites_seen:
1765 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011766 for sub_suite in suite:
1767 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281768 resolved_suites.add(suite_name)
1769 # At this point, every key in test_suites.pyl should be referenced.
1770 missing_suites = set(self.test_suites.keys()) - resolved_suites
1771 if missing_suites:
1772 raise BBGenErr('The following test suites were unreferenced by bots on '
1773 'the waterfalls: ' + str(missing_suites))
1774
1775 # All test suite exceptions must refer to bots on the waterfall.
1776 all_bots = set()
1777 missing_bots = set()
1778 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231779 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281780 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281781 # In order to disambiguate between bots with the same name on
1782 # different waterfalls, support has been added to various
1783 # exceptions for concatenating the waterfall name after the bot
1784 # name.
1785 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231786 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381787 removals = (exception.get('remove_from', []) +
1788 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231789 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381790 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281791 if removal not in all_bots:
1792 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411793
Kenneth Russelleb60cbd22017-12-05 07:54:281794 if missing_bots:
1795 raise BBGenErr('The following nonexistent machines were referenced in '
1796 'the test suite exceptions: ' + str(missing_bots))
1797
Stephen Martinis0382bc12018-09-17 22:29:071798 # All mixins must be referenced
1799 seen_mixins = set()
1800 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011801 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231802 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011803 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071804 for suite in self.test_suites.values():
1805 if isinstance(suite, list):
1806 # Don't care about this, it's a composition, which shouldn't include a
1807 # swarming mixin.
1808 continue
1809
1810 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561811 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011812 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071813
Zhaoyang Li9da047d52021-05-10 21:31:441814 for variant in self.variants:
1815 # Unpack the variant from variants.pyl if it's string based.
1816 if isinstance(variant, str):
1817 variant = self.variants[variant]
1818 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1819
Stephen Martinisb72f6d22018-10-04 23:29:011820 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071821 if missing_mixins:
1822 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1823 ' referenced in a waterfall, machine, or test suite.' % (
1824 str(missing_mixins)))
1825
Jeff Yoonda581c32020-03-06 03:56:051826 # All variant references must be referenced
1827 seen_variants = set()
1828 for suite in self.test_suites.values():
1829 if isinstance(suite, list):
1830 continue
1831
1832 for test in suite.values():
1833 if isinstance(test, dict):
1834 for variant in test.get('variants', []):
1835 if isinstance(variant, str):
1836 seen_variants.add(variant)
1837
1838 missing_variants = set(self.variants.keys()) - seen_variants
1839 if missing_variants:
1840 raise BBGenErr('The following variants were unreferenced: %s. They must '
1841 'be referenced in a matrix test suite under the variants '
1842 'key.' % str(missing_variants))
1843
Stephen Martinis54d64ad2018-09-21 22:16:201844
Garrett Beaty79339e182023-04-10 20:45:471845 def type_assert(self, node, typ, file_path, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201846 """Asserts that the Python AST node |node| is of type |typ|.
1847
1848 If verbose is set, it prints out some helpful context lines, showing where
1849 exactly the error occurred in the file.
1850 """
1851 if not isinstance(node, typ):
1852 if verbose:
Garrett Beaty79339e182023-04-10 20:45:471853 lines = [""] + self.read_file(file_path).splitlines()
Stephen Martinis54d64ad2018-09-21 22:16:201854
1855 context = 2
1856 lines_start = max(node.lineno - context, 0)
1857 # Add one to include the last line
1858 lines_end = min(node.lineno + context, len(lines)) + 1
Garrett Beaty79339e182023-04-10 20:45:471859 lines = itertools.chain(
1860 ['== %s ==\n' % file_path],
1861 ["<snip>\n"],
1862 [
1863 '%d %s' % (lines_start + i, line)
1864 for i, line in enumerate(lines[lines_start:lines_start +
1865 context])
1866 ],
1867 ['-' * 80 + '\n'],
1868 ['%d %s' % (node.lineno, lines[node.lineno])],
1869 [
1870 '-' * (node.col_offset + 3) + '^' + '-' *
1871 (80 - node.col_offset - 4) + '\n'
1872 ],
1873 [
1874 '%d %s' % (node.lineno + 1 + i, line)
1875 for i, line in enumerate(lines[node.lineno + 1:lines_end])
1876 ],
1877 ["<snip>\n"],
Stephen Martinis54d64ad2018-09-21 22:16:201878 )
1879 # Print out a useful message when a type assertion fails.
1880 for l in lines:
1881 self.print_line(l.strip())
1882
1883 node_dumped = ast.dump(node, annotate_fields=False)
1884 # If the node is huge, truncate it so everything fits in a terminal
1885 # window.
1886 if len(node_dumped) > 60: # pragma: no cover
1887 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1888 raise BBGenErr(
Garrett Beaty807011ab2023-04-12 00:52:391889 'Invalid .pyl file \'%s\'. Python AST node %r on line %s expected to'
Garrett Beaty79339e182023-04-10 20:45:471890 ' be %s, is %s' %
1891 (file_path, node_dumped, node.lineno, typ, type(node)))
Stephen Martinis54d64ad2018-09-21 22:16:201892
Garrett Beaty79339e182023-04-10 20:45:471893 def check_ast_list_formatted(self,
1894 keys,
1895 file_path,
1896 verbose,
Stephen Martinis1384ff92020-01-07 19:52:151897 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531898 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201899
Stephen Martinis5bef0fc2020-01-06 22:47:531900 Currently only checks to ensure they're correctly sorted, and that there
1901 are no duplicates.
1902
1903 Args:
1904 keys: An python list of AST nodes.
1905
1906 It's a list of AST nodes instead of a list of strings because
1907 when verbose is set, it tries to print out context of where the
1908 diffs are in the file.
Garrett Beaty79339e182023-04-10 20:45:471909 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531910 verbose: If set, print out diff information about how the keys are
1911 incorrectly formatted.
1912 check_sorting: If true, checks if the list is sorted.
1913 Returns:
1914 If the keys are correctly formatted.
1915 """
1916 if not keys:
1917 return True
1918
1919 assert isinstance(keys[0], ast.Str)
1920
1921 keys_strs = [k.s for k in keys]
1922 # Keys to diff against. Used below.
1923 keys_to_diff_against = None
1924 # If the list is properly formatted.
1925 list_formatted = True
1926
1927 # Duplicates are always bad.
1928 if len(set(keys_strs)) != len(keys_strs):
1929 list_formatted = False
1930 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1931
1932 if check_sorting and sorted(keys_strs) != keys_strs:
1933 list_formatted = False
1934 if list_formatted:
1935 return True
1936
1937 if verbose:
1938 line_num = keys[0].lineno
1939 keys = [k.s for k in keys]
1940 if check_sorting:
1941 # If we have duplicates, sorting this will take care of it anyways.
1942 keys_to_diff_against = sorted(set(keys))
1943 # else, keys_to_diff_against is set above already
1944
1945 self.print_line('=' * 80)
1946 self.print_line('(First line of keys is %s)' % line_num)
Garrett Beaty79339e182023-04-10 20:45:471947 for line in difflib.context_diff(keys,
1948 keys_to_diff_against,
1949 fromfile='current (%r)' % file_path,
1950 tofile='sorted',
1951 lineterm=''):
Stephen Martinis5bef0fc2020-01-06 22:47:531952 self.print_line(line)
1953 self.print_line('=' * 80)
1954
1955 return False
1956
Garrett Beaty79339e182023-04-10 20:45:471957 def check_ast_dict_formatted(self, node, file_path, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531958 """Checks if an ast dictionary's keys are correctly formatted.
1959
1960 Just a simple wrapper around check_ast_list_formatted.
1961 Args:
1962 node: An AST node. Assumed to be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:471963 file_path: The path to the file this node is from.
Stephen Martinis5bef0fc2020-01-06 22:47:531964 verbose: If set, print out diff information about how the keys are
1965 incorrectly formatted.
1966 check_sorting: If true, checks if the list is sorted.
1967 Returns:
1968 If the dictionary is correctly formatted.
1969 """
Stephen Martinis54d64ad2018-09-21 22:16:201970 keys = []
1971 # The keys of this dict are ordered as ordered in the file; normal python
1972 # dictionary keys are given an arbitrary order, but since we parsed the
1973 # file itself, the order as given in the file is preserved.
1974 for key in node.keys:
Garrett Beaty79339e182023-04-10 20:45:471975 self.type_assert(key, ast.Str, file_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531976 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201977
Garrett Beaty79339e182023-04-10 20:45:471978 return self.check_ast_list_formatted(keys, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181979
1980 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201981 # TODO(https://crbug.com/886993): Add the ability for this script to
1982 # actually format the files, rather than just complain if they're
1983 # incorrectly formatted.
1984 bad_files = set()
Garrett Beaty79339e182023-04-10 20:45:471985
1986 def parse_file(file_path):
Stephen Martinis5bef0fc2020-01-06 22:47:531987 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201988
Stephen Martinis5bef0fc2020-01-06 22:47:531989 Returns an AST node representing the value in the pyl file."""
Garrett Beaty79339e182023-04-10 20:45:471990 parsed = ast.parse(self.read_file(file_path))
Stephen Martinisf83893722018-09-19 00:02:181991
Stephen Martinisf83893722018-09-19 00:02:181992 # Must be a module.
Garrett Beaty79339e182023-04-10 20:45:471993 self.type_assert(parsed, ast.Module, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181994 module = parsed.body
1995
1996 # Only one expression in the module.
Garrett Beaty79339e182023-04-10 20:45:471997 self.type_assert(module, list, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:181998 if len(module) != 1: # pragma: no cover
Garrett Beaty79339e182023-04-10 20:45:471999 raise BBGenErr('Invalid .pyl file %s' % file_path)
Stephen Martinisf83893722018-09-19 00:02:182000 expr = module[0]
Garrett Beaty79339e182023-04-10 20:45:472001 self.type_assert(expr, ast.Expr, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:182002
Stephen Martinis5bef0fc2020-01-06 22:47:532003 return expr.value
2004
2005 # Handle this separately
Garrett Beaty79339e182023-04-10 20:45:472006 value = parse_file(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:532007 # Value should be a list.
Garrett Beaty79339e182023-04-10 20:45:472008 self.type_assert(value, ast.List, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:532009
2010 keys = []
Joshua Hood56c673c2022-03-02 20:29:332011 for elm in value.elts:
Garrett Beaty79339e182023-04-10 20:45:472012 self.type_assert(elm, ast.Dict, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:532013 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:332014 for key, val in zip(elm.keys, elm.values):
Garrett Beaty79339e182023-04-10 20:45:472015 self.type_assert(key, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:532016 if key.s == 'machines':
Garrett Beaty79339e182023-04-10 20:45:472017 if not self.check_ast_dict_formatted(
2018 val, self.args.waterfalls_pyl_path, verbose):
2019 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:532020
2021 if key.s == "name":
Garrett Beaty79339e182023-04-10 20:45:472022 self.type_assert(val, ast.Str, self.args.waterfalls_pyl_path, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:532023 waterfall_name = val
2024 assert waterfall_name
2025 keys.append(waterfall_name)
2026
Garrett Beaty79339e182023-04-10 20:45:472027 if not self.check_ast_list_formatted(keys, self.args.waterfalls_pyl_path,
2028 verbose):
2029 bad_files.add(self.args.waterfalls_pyl_path)
Stephen Martinis5bef0fc2020-01-06 22:47:532030
Garrett Beaty79339e182023-04-10 20:45:472031 for file_path in (
2032 self.args.mixins_pyl_path,
2033 self.args.test_suites_pyl_path,
2034 self.args.test_suite_exceptions_pyl_path,
Stephen Martinis5bef0fc2020-01-06 22:47:532035 ):
Garrett Beaty79339e182023-04-10 20:45:472036 value = parse_file(file_path)
Stephen Martinisf83893722018-09-19 00:02:182037 # Value should be a dictionary.
Garrett Beaty79339e182023-04-10 20:45:472038 self.type_assert(value, ast.Dict, file_path, verbose)
Stephen Martinisf83893722018-09-19 00:02:182039
Garrett Beaty79339e182023-04-10 20:45:472040 if not self.check_ast_dict_formatted(value, file_path, verbose):
2041 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:532042
Garrett Beaty79339e182023-04-10 20:45:472043 if file_path == self.args.test_suites_pyl_path:
Jeff Yoon8154e582019-12-03 23:30:012044 expected_keys = ['basic_suites',
2045 'compound_suites',
2046 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:202047 actual_keys = [node.s for node in value.keys]
2048 assert all(key in expected_keys for key in actual_keys), (
Garrett Beaty79339e182023-04-10 20:45:472049 'Invalid %r file; expected keys %r, got %r' %
2050 (file_path, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:332051 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:202052 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:012053 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:202054 for suite_group in suite_dicts:
Garrett Beaty79339e182023-04-10 20:45:472055 if not self.check_ast_dict_formatted(suite_group, file_path, verbose):
2056 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:182057
Stephen Martinis5bef0fc2020-01-06 22:47:532058 for key, suite in zip(value.keys, value.values):
2059 # The compound suites are checked in
2060 # 'check_composition_type_test_suites()'
2061 if key.s == 'basic_suites':
2062 for group in suite.values:
Garrett Beaty79339e182023-04-10 20:45:472063 if not self.check_ast_dict_formatted(group, file_path, verbose):
2064 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:532065 break
Stephen Martinis54d64ad2018-09-21 22:16:202066
Garrett Beaty79339e182023-04-10 20:45:472067 elif file_path == self.args.test_suite_exceptions_pyl_path:
Stephen Martinis5bef0fc2020-01-06 22:47:532068 # Check the values for each test.
2069 for test in value.values:
2070 for kind, node in zip(test.keys, test.values):
2071 if isinstance(node, ast.Dict):
Garrett Beaty79339e182023-04-10 20:45:472072 if not self.check_ast_dict_formatted(node, file_path, verbose):
2073 bad_files.add(file_path)
Stephen Martinis5bef0fc2020-01-06 22:47:532074 elif kind.s == 'remove_from':
2075 # Don't care about sorting; these are usually grouped, since the
2076 # same bug can affect multiple builders. Do want to make sure
2077 # there aren't duplicates.
Garrett Beaty79339e182023-04-10 20:45:472078 if not self.check_ast_list_formatted(
2079 node.elts, file_path, verbose, check_sorting=False):
2080 bad_files.add(file_path)
Stephen Martinisf83893722018-09-19 00:02:182081
2082 if bad_files:
2083 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:202084 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:532085 'unsorted, or have duplicates. Re-run this with --verbose to see '
2086 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:182087
Kenneth Russelleb60cbd22017-12-05 07:54:282088 def check_output_file_consistency(self, verbose=False):
2089 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012090 # All waterfalls/bucket .json files must have been written
2091 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:282092 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012093 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:162094 outputs = self.generate_outputs()
2095 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:012096 expected = self.jsonify(expected_contents)
Garrett Beaty79339e182023-04-10 20:45:472097 file_path = os.path.join(self.args.output_dir, filename + '.json')
Ben Pastenef21cda32023-03-30 22:00:572098 current = self.read_file(file_path)
Kenneth Russelleb60cbd22017-12-05 07:54:282099 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:012100 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:322101 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:012102 self.print_line('File ' + filename +
2103 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:322104 'contents:')
2105 for line in difflib.unified_diff(
2106 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:502107 current.splitlines(),
2108 fromfile='expected', tofile='current'):
2109 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:012110
2111 if ungenerated_files:
2112 raise BBGenErr(
2113 'The following files have not been properly '
2114 'autogenerated by generate_buildbot_json.py: ' +
2115 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:282116
Dirk Pranke772f55f2021-04-28 04:51:162117 for builder_group, builders in outputs.items():
2118 for builder, step_types in builders.items():
2119 for step_data in step_types.get('gtest_tests', []):
2120 step_name = step_data.get('name', step_data['test'])
2121 self._check_swarming_config(builder_group, builder, step_name,
2122 step_data)
2123 for step_data in step_types.get('isolated_scripts', []):
2124 step_name = step_data.get('name', step_data['isolate_name'])
2125 self._check_swarming_config(builder_group, builder, step_name,
2126 step_data)
2127
2128 def _check_swarming_config(self, filename, builder, step_name, step_data):
Ben Pastene338f56b2023-03-31 21:24:452129 # TODO(crbug.com/1203436): Ensure all swarming tests specify cpu, not
Dirk Pranke772f55f2021-04-28 04:51:162130 # just mac tests.
Garrett Beatybfeff8f2023-06-16 18:57:252131 if step_data.get('swarming', {}).get('can_use_on_swarming_builders'):
Dirk Pranke772f55f2021-04-28 04:51:162132 dimension_sets = step_data['swarming'].get('dimension_sets')
2133 if not dimension_sets:
Ben Pastene338f56b2023-03-31 21:24:452134 raise BBGenErr('%s: %s / %s : os must be specified for all '
Dirk Pranke772f55f2021-04-28 04:51:162135 'swarmed tests' % (filename, builder, step_name))
2136 for s in dimension_sets:
Ben Pastene338f56b2023-03-31 21:24:452137 if not s.get('os'):
2138 raise BBGenErr('%s: %s / %s : os must be specified for all '
2139 'swarmed tests' % (filename, builder, step_name))
2140 if 'Mac' in s.get('os') and not s.get('cpu'):
2141 raise BBGenErr('%s: %s / %s : cpu must be specified for mac '
Dirk Pranke772f55f2021-04-28 04:51:162142 'swarmed tests' % (filename, builder, step_name))
2143
Kenneth Russelleb60cbd22017-12-05 07:54:282144 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502145 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282146 self.check_output_file_consistency(verbose) # pragma: no cover
2147
Karen Qiane24b7ee2019-02-12 23:37:062148 def does_test_match(self, test_info, params_dict):
2149 """Checks to see if the test matches the parameters given.
2150
2151 Compares the provided test_info with the params_dict to see
2152 if the bot matches the parameters given. If so, returns True.
2153 Else, returns false.
2154
2155 Args:
2156 test_info (dict): Information about a specific bot provided
2157 in the format shown in waterfalls.pyl
2158 params_dict (dict): Dictionary of parameters and their values
2159 to look for in the bot
2160 Ex: {
2161 'device_os':'android',
2162 '--flag':True,
2163 'mixins': ['mixin1', 'mixin2'],
2164 'ex_key':'ex_value'
2165 }
2166
2167 """
2168 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2169 'kvm', 'pool', 'integrity'] # dimension parameters
2170 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2171 'can_use_on_swarming_builders']
2172 for param in params_dict:
2173 # if dimension parameter
2174 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2175 if not 'swarming' in test_info:
2176 return False
2177 swarming = test_info['swarming']
2178 if param in SWARMING_PARAMS:
2179 if not param in swarming:
2180 return False
2181 if not str(swarming[param]) == params_dict[param]:
2182 return False
2183 else:
2184 if not 'dimension_sets' in swarming:
2185 return False
2186 d_set = swarming['dimension_sets']
2187 # only looking at the first dimension set
2188 if not param in d_set[0]:
2189 return False
2190 if not d_set[0][param] == params_dict[param]:
2191 return False
2192
2193 # if flag
2194 elif param.startswith('--'):
2195 if not 'args' in test_info:
2196 return False
2197 if not param in test_info['args']:
2198 return False
2199
2200 # not dimension parameter/flag/mixin
2201 else:
2202 if not param in test_info:
2203 return False
2204 if not test_info[param] == params_dict[param]:
2205 return False
2206 return True
2207 def error_msg(self, msg):
2208 """Prints an error message.
2209
2210 In addition to a catered error message, also prints
2211 out where the user can find more help. Then, program exits.
2212 """
2213 self.print_line(msg + (' If you need more information, ' +
2214 'please run with -h or --help to see valid commands.'))
2215 sys.exit(1)
2216
2217 def find_bots_that_run_test(self, test, bots):
2218 matching_bots = []
2219 for bot in bots:
2220 bot_info = bots[bot]
2221 tests = self.flatten_tests_for_bot(bot_info)
2222 for test_info in tests:
2223 test_name = ""
2224 if 'name' in test_info:
2225 test_name = test_info['name']
2226 elif 'test' in test_info:
2227 test_name = test_info['test']
2228 if not test_name == test:
2229 continue
2230 matching_bots.append(bot)
2231 return matching_bots
2232
2233 def find_tests_with_params(self, tests, params_dict):
2234 matching_tests = []
2235 for test_name in tests:
2236 test_info = tests[test_name]
2237 if not self.does_test_match(test_info, params_dict):
2238 continue
2239 if not test_name in matching_tests:
2240 matching_tests.append(test_name)
2241 return matching_tests
2242
2243 def flatten_waterfalls_for_query(self, waterfalls):
2244 bots = {}
2245 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012246 waterfall_tests = self.generate_output_tests(waterfall)
2247 for bot in waterfall_tests:
2248 bot_info = waterfall_tests[bot]
2249 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062250 return bots
2251
2252 def flatten_tests_for_bot(self, bot_info):
2253 """Returns a list of flattened tests.
2254
2255 Returns a list of tests not grouped by test category
2256 for a specific bot.
2257 """
2258 TEST_CATS = self.get_test_generator_map().keys()
2259 tests = []
2260 for test_cat in TEST_CATS:
2261 if not test_cat in bot_info:
2262 continue
2263 test_cat_tests = bot_info[test_cat]
2264 tests = tests + test_cat_tests
2265 return tests
2266
2267 def flatten_tests_for_query(self, test_suites):
2268 """Returns a flattened dictionary of tests.
2269
2270 Returns a dictionary of tests associate with their
2271 configuration, not grouped by their test suite.
2272 """
2273 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232274 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062275 for test in test_suite:
2276 test_info = test_suite[test]
2277 test_name = test
2278 if 'name' in test_info:
2279 test_name = test_info['name']
2280 tests[test_name] = test_info
2281 return tests
2282
2283 def parse_query_filter_params(self, params):
2284 """Parses the filter parameters.
2285
2286 Creates a dictionary from the parameters provided
2287 to filter the bot array.
2288 """
2289 params_dict = {}
2290 for p in params:
2291 # flag
2292 if p.startswith("--"):
2293 params_dict[p] = True
2294 else:
2295 pair = p.split(":")
2296 if len(pair) != 2:
2297 self.error_msg('Invalid command.')
2298 # regular parameters
2299 if pair[1].lower() == "true":
2300 params_dict[pair[0]] = True
2301 elif pair[1].lower() == "false":
2302 params_dict[pair[0]] = False
2303 else:
2304 params_dict[pair[0]] = pair[1]
2305 return params_dict
2306
2307 def get_test_suites_dict(self, bots):
2308 """Returns a dictionary of bots and their tests.
2309
2310 Returns a dictionary of bots and a list of their associated tests.
2311 """
2312 test_suite_dict = dict()
2313 for bot in bots:
2314 bot_info = bots[bot]
2315 tests = self.flatten_tests_for_bot(bot_info)
2316 test_suite_dict[bot] = tests
2317 return test_suite_dict
2318
2319 def output_query_result(self, result, json_file=None):
2320 """Outputs the result of the query.
2321
2322 If a json file parameter name is provided, then
2323 the result is output into the json file. If not,
2324 then the result is printed to the console.
2325 """
2326 output = json.dumps(result, indent=2)
2327 if json_file:
2328 self.write_file(json_file, output)
2329 else:
2330 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062331
Joshua Hood56c673c2022-03-02 20:29:332332 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062333 def query(self, args):
2334 """Queries tests or bots.
2335
2336 Depending on the arguments provided, outputs a json of
2337 tests or bots matching the appropriate optional parameters provided.
2338 """
2339 # split up query statement
2340 query = args.query.split('/')
2341 self.load_configuration_files()
2342 self.resolve_configuration_files()
2343
2344 # flatten bots json
2345 tests = self.test_suites
2346 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2347
2348 cmd_class = query[0]
2349
2350 # For queries starting with 'bots'
2351 if cmd_class == "bots":
2352 if len(query) == 1:
2353 return self.output_query_result(bots, args.json)
2354 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332355 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062356 if query[1] == 'tests':
2357 test_suites_dict = self.get_test_suites_dict(bots)
2358 return self.output_query_result(test_suites_dict, args.json)
Joshua Hood56c673c2022-03-02 20:29:332359 self.error_msg("This query should be in the format: bots/tests.")
Karen Qiane24b7ee2019-02-12 23:37:062360
2361 else:
2362 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2363 % str(len(query)-1))
2364
2365 # For queries starting with 'bot'
2366 elif cmd_class == "bot":
2367 if not len(query) == 2 and not len(query) == 3:
2368 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2369 % str(len(query)-1))
2370 bot_id = query[1]
2371 if not bot_id in bots:
2372 self.error_msg("No bot named '" + bot_id + "' found.")
2373 bot_info = bots[bot_id]
2374 if len(query) == 2:
2375 return self.output_query_result(bot_info, args.json)
2376 if not query[2] == 'tests':
2377 self.error_msg("The query should be in the format:" +
2378 "bot/<bot-name>/tests.")
2379
2380 bot_tests = self.flatten_tests_for_bot(bot_info)
2381 return self.output_query_result(bot_tests, args.json)
2382
2383 # For queries starting with 'tests'
2384 elif cmd_class == "tests":
2385 if not len(query) == 1 and not len(query) == 2:
2386 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2387 % str(len(query)-1))
2388 flattened_tests = self.flatten_tests_for_query(tests)
2389 if len(query) == 1:
2390 return self.output_query_result(flattened_tests, args.json)
2391
2392 # create params dict
2393 params = query[1].split('&')
2394 params_dict = self.parse_query_filter_params(params)
2395 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2396 return self.output_query_result(matching_bots)
2397
2398 # For queries starting with 'test'
2399 elif cmd_class == "test":
2400 if not len(query) == 2 and not len(query) == 3:
2401 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2402 % str(len(query)-1))
2403 test_id = query[1]
2404 if len(query) == 2:
2405 flattened_tests = self.flatten_tests_for_query(tests)
2406 for test in flattened_tests:
2407 if test == test_id:
2408 return self.output_query_result(flattened_tests[test], args.json)
2409 self.error_msg("There is no test named %s." % test_id)
2410 if not query[2] == 'bots':
2411 self.error_msg("The query should be in the format: " +
2412 "test/<test-name>/bots")
2413 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2414 return self.output_query_result(bots_for_test)
2415
2416 else:
2417 self.error_msg("Your command did not match any valid commands." +
2418 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Joshua Hood56c673c2022-03-02 20:29:332419 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282420
Garrett Beaty1afaccc2020-06-25 19:58:152421 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282422 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502423 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062424 elif self.args.query:
2425 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282426 else:
Greg Gutermanf60eb052020-03-12 17:40:012427 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282428 return 0
2429
2430if __name__ == "__main__": # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152431 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2432 sys.exit(generator.main())