blob: b0a555c2d93cf9f77c12dc7374c367e79ef73bd4 [file] [log] [blame]
Jamie Madillcf4f8c72021-05-20 19:24:231#!/usr/bin/env python3
Kenneth Russelleb60cbd22017-12-05 07:54:282# Copyright 2016 The Chromium Authors. All rights reserved.
3# 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
Joshua Hood56c673c2022-03-02 20:29:3320import six
Kenneth Russelleb60cbd22017-12-05 07:54:2821import string
22import sys
23
Brian Sheedya31578e2020-05-18 20:24:3624import buildbot_json_magic_substitutions as magic_substitutions
25
Joshua Hood56c673c2022-03-02 20:29:3326# pylint: disable=super-with-arguments,useless-super-delegation
27
Kenneth Russelleb60cbd22017-12-05 07:54:2828THIS_DIR = os.path.dirname(os.path.abspath(__file__))
29
Brian Sheedyf74819b2021-06-04 01:38:3830BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP = {
31 'android-chromium': '_android_chrome',
32 'android-chromium-monochrome': '_android_monochrome',
33 'android-weblayer': '_android_weblayer',
34 'android-webview': '_android_webview',
35}
36
Kenneth Russelleb60cbd22017-12-05 07:54:2837
38class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4939 def __init__(self, message):
40 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2841
42
Kenneth Russell8ceeabf2017-12-11 17:53:2843# This class is only present to accommodate certain machines on
44# chromium.android.fyi which run certain tests as instrumentation
45# tests, but not as gtests. If this discrepancy were fixed then the
46# notion could be removed.
Joshua Hood56c673c2022-03-02 20:29:3347class TestSuiteTypes(object): # pylint: disable=useless-object-inheritance
Kenneth Russell8ceeabf2017-12-11 17:53:2848 GTEST = 'gtest'
49
50
Joshua Hood56c673c2022-03-02 20:29:3351class BaseGenerator(object): # pylint: disable=useless-object-inheritance
Kenneth Russelleb60cbd22017-12-05 07:54:2852 def __init__(self, bb_gen):
53 self.bb_gen = bb_gen
54
Kenneth Russell8ceeabf2017-12-11 17:53:2855 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2856 raise NotImplementedError()
57
58 def sort(self, tests):
59 raise NotImplementedError()
60
61
Jamie Madillcf4f8c72021-05-20 19:24:2362def custom_cmp(a, b):
63 return int(a > b) - int(a < b)
64
65
Kenneth Russell8ceeabf2017-12-11 17:53:2866def cmp_tests(a, b):
67 # Prefer to compare based on the "test" key.
Jamie Madillcf4f8c72021-05-20 19:24:2368 val = custom_cmp(a['test'], b['test'])
Kenneth Russell8ceeabf2017-12-11 17:53:2869 if val != 0:
70 return val
71 if 'name' in a and 'name' in b:
Jamie Madillcf4f8c72021-05-20 19:24:2372 return custom_cmp(a['name'], b['name']) # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:2873 if 'name' not in a and 'name' not in b:
74 return 0 # pragma: no cover
75 # Prefer to put variants of the same test after the first one.
76 if 'name' in a:
77 return 1
78 # 'name' is in b.
79 return -1 # pragma: no cover
80
81
Kenneth Russell8a386d42018-06-02 09:48:0182class GPUTelemetryTestGenerator(BaseGenerator):
Bo Liu555a0f92019-03-29 12:11:5683 def __init__(self, bb_gen, is_android_webview=False):
Kenneth Russell8a386d42018-06-02 09:48:0184 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5685 self._is_android_webview = is_android_webview
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()):
Kenneth Russell8a386d42018-06-02 09:48:0190 test = self.bb_gen.generate_gpu_telemetry_test(
Bo Liu555a0f92019-03-29 12:11:5691 waterfall, tester_name, tester_config, test_name, test_config,
92 self._is_android_webview)
Kenneth Russell8a386d42018-06-02 09:48:0193 if test:
94 isolated_scripts.append(test)
95 return isolated_scripts
96
97 def sort(self, tests):
98 return sorted(tests, key=lambda x: x['name'])
99
100
Kenneth Russelleb60cbd22017-12-05 07:54:28101class GTestGenerator(BaseGenerator):
102 def __init__(self, bb_gen):
103 super(GTestGenerator, self).__init__(bb_gen)
104
Kenneth Russell8ceeabf2017-12-11 17:53:28105 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28106 # The relative ordering of some of the tests is important to
107 # minimize differences compared to the handwritten JSON files, since
108 # Python's sorts are stable and there are some tests with the same
109 # key (see gles2_conform_d3d9_test and similar variants). Avoid
110 # losing the order by avoiding coalescing the dictionaries into one.
111 gtests = []
Jamie Madillcf4f8c72021-05-20 19:24:23112 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoon67c3e832020-02-08 07:39:38113 # Variants allow more than one definition for a given test, and is defined
114 # in array format from resolve_variants().
115 if not isinstance(test_config, list):
116 test_config = [test_config]
117
118 for config in test_config:
119 test = self.bb_gen.generate_gtest(
120 waterfall, tester_name, tester_config, test_name, config)
121 if test:
122 # generate_gtest may veto the test generation on this tester.
123 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28124 return gtests
125
126 def sort(self, tests):
Jamie Madillcf4f8c72021-05-20 19:24:23127 return sorted(tests, key=functools.cmp_to_key(cmp_tests))
Kenneth Russelleb60cbd22017-12-05 07:54:28128
129
130class IsolatedScriptTestGenerator(BaseGenerator):
131 def __init__(self, bb_gen):
132 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
133
Kenneth Russell8ceeabf2017-12-11 17:53:28134 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28135 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23136 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43137 # Variants allow more than one definition for a given test, and is defined
138 # in array format from resolve_variants().
139 if not isinstance(test_config, list):
140 test_config = [test_config]
141
142 for config in test_config:
143 test = self.bb_gen.generate_isolated_script_test(
144 waterfall, tester_name, tester_config, test_name, config)
145 if test:
146 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28147 return isolated_scripts
148
149 def sort(self, tests):
150 return sorted(tests, key=lambda x: x['name'])
151
152
153class ScriptGenerator(BaseGenerator):
154 def __init__(self, bb_gen):
155 super(ScriptGenerator, self).__init__(bb_gen)
156
Kenneth Russell8ceeabf2017-12-11 17:53:28157 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28158 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23159 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28160 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28161 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28162 if test:
163 scripts.append(test)
164 return scripts
165
166 def sort(self, tests):
167 return sorted(tests, key=lambda x: x['name'])
168
169
170class JUnitGenerator(BaseGenerator):
171 def __init__(self, bb_gen):
172 super(JUnitGenerator, self).__init__(bb_gen)
173
Kenneth Russell8ceeabf2017-12-11 17:53:28174 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28175 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23176 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28177 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28178 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28179 if test:
180 scripts.append(test)
181 return scripts
182
183 def sort(self, tests):
184 return sorted(tests, key=lambda x: x['test'])
185
186
Xinan Lin05fb9c1752020-12-17 00:15:52187class SkylabGenerator(BaseGenerator):
188 def __init__(self, bb_gen):
189 super(SkylabGenerator, self).__init__(bb_gen)
190
191 def generate(self, waterfall, tester_name, tester_config, input_tests):
192 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23193 for test_name, test_config in sorted(input_tests.items()):
Xinan Lin05fb9c1752020-12-17 00:15:52194 for config in test_config:
195 test = self.bb_gen.generate_skylab_test(waterfall, tester_name,
196 tester_config, test_name,
197 config)
198 if test:
199 scripts.append(test)
200 return scripts
201
202 def sort(self, tests):
203 return sorted(tests, key=lambda x: x['test'])
204
205
Jeff Yoon67c3e832020-02-08 07:39:38206def check_compound_references(other_test_suites=None,
207 sub_suite=None,
208 suite=None,
209 target_test_suites=None,
210 test_type=None,
211 **kwargs):
212 """Ensure comound reference's don't target other compounds"""
213 del kwargs
214 if sub_suite in other_test_suites or sub_suite in target_test_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15215 raise BBGenErr('%s may not refer to other composition type test '
216 'suites (error found while processing %s)' %
217 (test_type, suite))
218
Jeff Yoon67c3e832020-02-08 07:39:38219
220def check_basic_references(basic_suites=None,
221 sub_suite=None,
222 suite=None,
223 **kwargs):
224 """Ensure test has a basic suite reference"""
225 del kwargs
226 if sub_suite not in basic_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15227 raise BBGenErr('Unable to find reference to %s while processing %s' %
228 (sub_suite, suite))
229
Jeff Yoon67c3e832020-02-08 07:39:38230
231def check_conflicting_definitions(basic_suites=None,
232 seen_tests=None,
233 sub_suite=None,
234 suite=None,
235 test_type=None,
236 **kwargs):
237 """Ensure that if a test is reachable via multiple basic suites,
238 all of them have an identical definition of the tests.
239 """
240 del kwargs
241 for test_name in basic_suites[sub_suite]:
242 if (test_name in seen_tests and
243 basic_suites[sub_suite][test_name] !=
244 basic_suites[seen_tests[test_name]][test_name]):
245 raise BBGenErr('Conflicting test definitions for %s from %s '
246 'and %s in %s (error found while processing %s)'
247 % (test_name, seen_tests[test_name], sub_suite,
248 test_type, suite))
249 seen_tests[test_name] = sub_suite
250
251def check_matrix_identifier(sub_suite=None,
252 suite=None,
253 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05254 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38255 **kwargs):
256 """Ensure 'idenfitier' is defined for each variant"""
257 del kwargs
258 sub_suite_config = suite_def[sub_suite]
259 for variant in sub_suite_config.get('variants', []):
Jeff Yoonda581c32020-03-06 03:56:05260 if isinstance(variant, str):
261 if variant not in all_variants:
262 raise BBGenErr('Missing variant definition for %s in variants.pyl'
263 % variant)
264 variant = all_variants[variant]
265
Jeff Yoon67c3e832020-02-08 07:39:38266 if not 'identifier' in variant:
267 raise BBGenErr('Missing required identifier field in matrix '
268 'compound suite %s, %s' % (suite, sub_suite))
269
270
Joshua Hood56c673c2022-03-02 20:29:33271class BBJSONGenerator(object): # pylint: disable=useless-object-inheritance
Garrett Beaty1afaccc2020-06-25 19:58:15272 def __init__(self, args):
Kenneth Russelleb60cbd22017-12-05 07:54:28273 self.this_dir = THIS_DIR
Garrett Beaty1afaccc2020-06-25 19:58:15274 self.args = args
Kenneth Russelleb60cbd22017-12-05 07:54:28275 self.waterfalls = None
276 self.test_suites = None
277 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01278 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41279 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05280 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28281
Garrett Beaty1afaccc2020-06-25 19:58:15282 @staticmethod
283 def parse_args(argv):
284
285 # RawTextHelpFormatter allows for styling of help statement
286 parser = argparse.ArgumentParser(
287 formatter_class=argparse.RawTextHelpFormatter)
288
289 group = parser.add_mutually_exclusive_group()
290 group.add_argument(
291 '-c',
292 '--check',
293 action='store_true',
294 help=
295 'Do consistency checks of configuration and generated files and then '
296 'exit. Used during presubmit. '
297 'Causes the tool to not generate any files.')
298 group.add_argument(
299 '--query',
300 type=str,
301 help=(
302 "Returns raw JSON information of buildbots and tests.\n" +
303 "Examples:\n" + " List all bots (all info):\n" +
304 " --query bots\n\n" +
305 " List all bots and only their associated tests:\n" +
306 " --query bots/tests\n\n" +
307 " List all information about 'bot1' " +
308 "(make sure you have quotes):\n" + " --query bot/'bot1'\n\n" +
309 " List tests running for 'bot1' (make sure you have quotes):\n" +
310 " --query bot/'bot1'/tests\n\n" + " List all tests:\n" +
311 " --query tests\n\n" +
312 " List all tests and the bots running them:\n" +
313 " --query tests/bots\n\n" +
314 " List all tests that satisfy multiple parameters\n" +
315 " (separation of parameters by '&' symbol):\n" +
316 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
317 " List all tests that run with a specific flag:\n" +
318 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
319 " List specific test (make sure you have quotes):\n"
320 " --query test/'test1'\n\n"
321 " List all bots running 'test1' " +
322 "(make sure you have quotes):\n" + " --query test/'test1'/bots"))
323 parser.add_argument(
324 '-n',
325 '--new-files',
326 action='store_true',
327 help=
328 'Write output files as .new.json. Useful during development so old and '
329 'new files can be looked at side-by-side.')
330 parser.add_argument('-v',
331 '--verbose',
332 action='store_true',
333 help='Increases verbosity. Affects consistency checks.')
334 parser.add_argument('waterfall_filters',
335 metavar='waterfalls',
336 type=str,
337 nargs='*',
338 help='Optional list of waterfalls to generate.')
339 parser.add_argument(
340 '--pyl-files-dir',
341 type=os.path.realpath,
342 help='Path to the directory containing the input .pyl files.')
343 parser.add_argument(
344 '--json',
345 metavar='JSON_FILE_PATH',
346 help='Outputs results into a json file. Only works with query function.'
347 )
Chong Guee622242020-10-28 18:17:35348 parser.add_argument('--isolate-map-file',
349 metavar='PATH',
350 help='path to additional isolate map files.',
351 default=[],
352 action='append',
353 dest='isolate_map_files')
Garrett Beaty1afaccc2020-06-25 19:58:15354 parser.add_argument(
355 '--infra-config-dir',
356 help='Path to the LUCI services configuration directory',
357 default=os.path.abspath(
358 os.path.join(os.path.dirname(__file__), '..', '..', 'infra',
359 'config')))
360 args = parser.parse_args(argv)
361 if args.json and not args.query:
362 parser.error(
363 "The --json flag can only be used with --query.") # pragma: no cover
364 args.infra_config_dir = os.path.abspath(args.infra_config_dir)
365 return args
366
Kenneth Russelleb60cbd22017-12-05 07:54:28367 def generate_abs_file_path(self, relative_path):
Garrett Beaty1afaccc2020-06-25 19:58:15368 return os.path.join(self.this_dir, relative_path)
Kenneth Russelleb60cbd22017-12-05 07:54:28369
Stephen Martinis7eb8b612018-09-21 00:17:50370 def print_line(self, line):
371 # Exists so that tests can mock
Jamie Madillcf4f8c72021-05-20 19:24:23372 print(line) # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:50373
Kenneth Russelleb60cbd22017-12-05 07:54:28374 def read_file(self, relative_path):
Garrett Beaty1afaccc2020-06-25 19:58:15375 with open(self.generate_abs_file_path(relative_path)) as fp:
376 return fp.read()
Kenneth Russelleb60cbd22017-12-05 07:54:28377
378 def write_file(self, relative_path, contents):
Garrett Beaty1afaccc2020-06-25 19:58:15379 with open(self.generate_abs_file_path(relative_path), 'wb') as fp:
Jamie Madillcf4f8c72021-05-20 19:24:23380 fp.write(contents.encode('utf-8'))
Kenneth Russelleb60cbd22017-12-05 07:54:28381
Zhiling Huangbe008172018-03-08 19:13:11382 def pyl_file_path(self, filename):
383 if self.args and self.args.pyl_files_dir:
384 return os.path.join(self.args.pyl_files_dir, filename)
385 return filename
386
Joshua Hood56c673c2022-03-02 20:29:33387 # pylint: disable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:28388 def load_pyl_file(self, filename):
389 try:
Zhiling Huangbe008172018-03-08 19:13:11390 return ast.literal_eval(self.read_file(
391 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28392 except (SyntaxError, ValueError) as e: # pragma: no cover
Joshua Hood56c673c2022-03-02 20:29:33393 six.raise_from(
394 BBGenErr('Failed to parse pyl file "%s": %s' % (filename, e)),
395 e) # pragma: no cover
396 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:28397
Kenneth Russell8a386d42018-06-02 09:48:01398 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
399 # Currently it is only mandatory for bots which run GPU tests. Change these to
400 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28401 def is_android(self, tester_config):
402 return tester_config.get('os_type') == 'android'
403
Ben Pastenea9e583b2019-01-16 02:57:26404 def is_chromeos(self, tester_config):
405 return tester_config.get('os_type') == 'chromeos'
406
Chong Guc2ca5d02022-01-11 19:52:17407 def is_fuchsia(self, tester_config):
408 return tester_config.get('os_type') == 'fuchsia'
409
Brian Sheedy781c8ca42021-03-08 22:03:21410 def is_lacros(self, tester_config):
411 return tester_config.get('os_type') == 'lacros'
412
Kenneth Russell8a386d42018-06-02 09:48:01413 def is_linux(self, tester_config):
414 return tester_config.get('os_type') == 'linux'
415
Kai Ninomiya40de9f52019-10-18 21:38:49416 def is_mac(self, tester_config):
417 return tester_config.get('os_type') == 'mac'
418
419 def is_win(self, tester_config):
420 return tester_config.get('os_type') == 'win'
421
422 def is_win64(self, tester_config):
423 return (tester_config.get('os_type') == 'win' and
424 tester_config.get('browser_config') == 'release_x64')
425
Kenneth Russelleb60cbd22017-12-05 07:54:28426 def get_exception_for_test(self, test_name, test_config):
427 # gtests may have both "test" and "name" fields, and usually, if the "name"
428 # field is specified, it means that the same test is being repurposed
429 # multiple times with different command line arguments. To handle this case,
430 # prefer to lookup per the "name" field of the test itself, as opposed to
431 # the "test_name", which is actually the "test" field.
432 if 'name' in test_config:
433 return self.exceptions.get(test_config['name'])
Joshua Hood56c673c2022-03-02 20:29:33434 return self.exceptions.get(test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28435
Nico Weberb0b3f5862018-07-13 18:45:15436 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28437 # Currently, the only reason a test should not run on a given tester is that
438 # it's in the exceptions. (Once the GPU waterfall generation script is
439 # incorporated here, the rules will become more complex.)
440 exception = self.get_exception_for_test(test_name, test_config)
441 if not exception:
442 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28443 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28444 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28445 if remove_from:
446 if tester_name in remove_from:
447 return False
448 # TODO(kbr): this code path was added for some tests (including
449 # android_webview_unittests) on one machine (Nougat Phone
450 # Tester) which exists with the same name on two waterfalls,
451 # chromium.android and chromium.fyi; the tests are run on one
452 # but not the other. Once the bots are all uniquely named (a
453 # different ongoing project) this code should be removed.
454 # TODO(kbr): add coverage.
455 return (tester_name + ' ' + waterfall['name']
456 not in remove_from) # pragma: no cover
457 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28458
Nico Weber79dc5f6852018-07-13 19:38:49459 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28460 exception = self.get_exception_for_test(test_name, test)
461 if not exception:
462 return None
Nico Weber79dc5f6852018-07-13 19:38:49463 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28464
Brian Sheedye6ea0ee2019-07-11 02:54:37465 def get_test_replacements(self, test, test_name, tester_name):
466 exception = self.get_exception_for_test(test_name, test)
467 if not exception:
468 return None
469 return exception.get('replacements', {}).get(tester_name)
470
Kenneth Russell8a386d42018-06-02 09:48:01471 def merge_command_line_args(self, arr, prefix, splitter):
472 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01473 idx = 0
474 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01475 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01476 while idx < len(arr):
477 flag = arr[idx]
478 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01479 if flag.startswith(prefix):
480 arg = flag[prefix_len:]
481 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01482 if first_idx < 0:
483 first_idx = idx
484 else:
485 delete_current_entry = True
486 if delete_current_entry:
487 del arr[idx]
488 else:
489 idx += 1
490 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01491 arr[first_idx] = prefix + splitter.join(accumulated_args)
492 return arr
493
494 def maybe_fixup_args_array(self, arr):
495 # The incoming array of strings may be an array of command line
496 # arguments. To make it easier to turn on certain features per-bot or
497 # per-test-suite, look specifically for certain flags and merge them
498 # appropriately.
499 # --enable-features=Feature1 --enable-features=Feature2
500 # are merged to:
501 # --enable-features=Feature1,Feature2
502 # and:
503 # --extra-browser-args=arg1 --extra-browser-args=arg2
504 # are merged to:
505 # --extra-browser-args=arg1 arg2
506 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
507 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Yuly Novikov8c487e72020-10-16 20:00:29508 arr = self.merge_command_line_args(arr, '--test-launcher-filter-file=', ';')
Kenneth Russell650995a2018-05-03 21:17:01509 return arr
510
Brian Sheedy5f173bb2021-11-24 00:45:54511 def substitute_magic_args(self, test_config, tester_name):
Brian Sheedya31578e2020-05-18 20:24:36512 """Substitutes any magic substitution args present in |test_config|.
513
514 Substitutions are done in-place.
515
516 See buildbot_json_magic_substitutions.py for more information on this
517 feature.
518
519 Args:
520 test_config: A dict containing a configuration for a specific test on
521 a specific builder, e.g. the output of update_and_cleanup_test.
Brian Sheedy5f173bb2021-11-24 00:45:54522 tester_name: A string containing the name of the tester that |test_config|
523 came from.
Brian Sheedya31578e2020-05-18 20:24:36524 """
525 substituted_array = []
526 for arg in test_config.get('args', []):
527 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
528 function = arg.replace(
529 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
530 if hasattr(magic_substitutions, function):
531 substituted_array.extend(
Brian Sheedy5f173bb2021-11-24 00:45:54532 getattr(magic_substitutions, function)(test_config, tester_name))
Brian Sheedya31578e2020-05-18 20:24:36533 else:
534 raise BBGenErr(
535 'Magic substitution function %s does not exist' % function)
536 else:
537 substituted_array.append(arg)
538 if substituted_array:
539 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
540
Kenneth Russelleb60cbd22017-12-05 07:54:28541 def dictionary_merge(self, a, b, path=None, update=True):
542 """http://stackoverflow.com/questions/7204805/
543 python-dictionaries-of-dictionaries-merge
544 merges b into a
545 """
546 if path is None:
547 path = []
548 for key in b:
549 if key in a:
550 if isinstance(a[key], dict) and isinstance(b[key], dict):
551 self.dictionary_merge(a[key], b[key], path + [str(key)])
552 elif a[key] == b[key]:
553 pass # same leaf value
554 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06555 # Args arrays are lists of strings. Just concatenate them,
556 # and don't sort them, in order to keep some needed
Weizhong Xia91b53362022-01-05 17:13:35557 # arguments adjacent (like --timeout-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28558 if all(isinstance(x, str)
559 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01560 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28561 else:
562 # TODO(kbr): this only works properly if the two arrays are
563 # the same length, which is currently always the case in the
564 # swarming dimension_sets that we have to merge. It will fail
565 # to merge / override 'args' arrays which are different
566 # length.
Jamie Madillcf4f8c72021-05-20 19:24:23567 for idx in range(len(b[key])):
Kenneth Russell8ceeabf2017-12-11 17:53:28568 try:
569 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
570 path + [str(key), str(idx)],
571 update=update)
Joshua Hood56c673c2022-03-02 20:29:33572 except (IndexError, TypeError) as e:
573 six.raise_from(
574 BBGenErr('Error merging lists by key "%s" from source %s '
575 'into target %s at index %s. Verify target list '
576 'length is equal or greater than source' %
577 (str(key), str(b), str(a), str(idx))), e)
John Budorick5bc387fe2019-05-09 20:02:53578 elif update:
579 if b[key] is None:
580 del a[key]
581 else:
582 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28583 else:
584 raise BBGenErr('Conflict at %s' % '.'.join(
585 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53586 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28587 a[key] = b[key]
588 return a
589
John Budorickab108712018-09-01 00:12:21590 def initialize_args_for_test(
591 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21592 args = []
593 args.extend(generated_test.get('args', []))
594 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22595
Kenneth Russell8a386d42018-06-02 09:48:01596 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21597 val = generated_test.pop(key, [])
598 if fn(tester_config):
599 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01600
601 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
Brian Sheedy781c8ca42021-03-08 22:03:21602 add_conditional_args('lacros_args', self.is_lacros)
Kenneth Russell8a386d42018-06-02 09:48:01603 add_conditional_args('linux_args', self.is_linux)
604 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36605 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49606 add_conditional_args('mac_args', self.is_mac)
607 add_conditional_args('win_args', self.is_win)
608 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01609
John Budorickab108712018-09-01 00:12:21610 for key in additional_arg_keys or []:
611 args.extend(generated_test.pop(key, []))
612 args.extend(tester_config.get(key, []))
613
614 if args:
615 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01616
Kenneth Russelleb60cbd22017-12-05 07:54:28617 def initialize_swarming_dictionary_for_test(self, generated_test,
618 tester_config):
619 if 'swarming' not in generated_test:
620 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28621 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
622 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38623 'can_use_on_swarming_builders': tester_config.get('use_swarming',
624 True)
Dirk Pranke81ff51c2017-12-09 19:24:28625 })
Kenneth Russelleb60cbd22017-12-05 07:54:28626 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03627 if ('dimension_sets' not in generated_test['swarming'] and
628 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28629 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
630 tester_config['swarming']['dimension_sets'])
631 self.dictionary_merge(generated_test['swarming'],
632 tester_config['swarming'])
Brian Sheedybc984e242021-04-21 23:44:51633 # Apply any platform-specific Swarming dimensions after the generic ones.
Kenneth Russelleb60cbd22017-12-05 07:54:28634 if 'android_swarming' in generated_test:
635 if self.is_android(tester_config): # pragma: no cover
636 self.dictionary_merge(
637 generated_test['swarming'],
638 generated_test['android_swarming']) # pragma: no cover
639 del generated_test['android_swarming'] # pragma: no cover
Brian Sheedybc984e242021-04-21 23:44:51640 if 'chromeos_swarming' in generated_test:
641 if self.is_chromeos(tester_config): # pragma: no cover
642 self.dictionary_merge(
643 generated_test['swarming'],
644 generated_test['chromeos_swarming']) # pragma: no cover
645 del generated_test['chromeos_swarming'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28646
647 def clean_swarming_dictionary(self, swarming_dict):
648 # Clean out redundant entries from a test's "swarming" dictionary.
649 # This is really only needed to retain 100% parity with the
650 # handwritten JSON files, and can be removed once all the files are
651 # autogenerated.
652 if 'shards' in swarming_dict:
653 if swarming_dict['shards'] == 1: # pragma: no cover
654 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24655 if 'hard_timeout' in swarming_dict:
656 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
657 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43658 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28659 # Remove all other keys.
Jamie Madillcf4f8c72021-05-20 19:24:23660 for k in list(swarming_dict): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28661 if k != 'can_use_on_swarming_builders': # pragma: no cover
662 del swarming_dict[k] # pragma: no cover
663
Stephen Martinis0382bc12018-09-17 22:29:07664 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
665 waterfall):
666 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01667 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07668 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28669 # See if there are any exceptions that need to be merged into this
670 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49671 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28672 if modifications:
673 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23674 if 'swarming' in test:
675 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28676 # Ensure all Android Swarming tests run only on userdebug builds if another
677 # build type was not specified.
678 if 'swarming' in test and self.is_android(tester_config):
679 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22680 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28681 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37682 self.replace_test_args(test, test_name, tester_name)
Ben Pastenee012aea42019-05-14 22:32:28683
Kenneth Russelleb60cbd22017-12-05 07:54:28684 return test
685
Brian Sheedye6ea0ee2019-07-11 02:54:37686 def replace_test_args(self, test, test_name, tester_name):
687 replacements = self.get_test_replacements(
688 test, test_name, tester_name) or {}
689 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23690 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37691 if key not in valid_replacement_keys:
692 raise BBGenErr(
693 'Given replacement key %s for %s on %s is not in the list of valid '
694 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23695 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37696 found_key = False
697 for i, test_key in enumerate(test.get(key, [])):
698 # Handle both the key/value being replaced being defined as two
699 # separate items or as key=value.
700 if test_key == replacement_key:
701 found_key = True
702 # Handle flags without values.
703 if replacement_val == None:
704 del test[key][i]
705 else:
706 test[key][i+1] = replacement_val
707 break
Joshua Hood56c673c2022-03-02 20:29:33708 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37709 found_key = True
710 if replacement_val == None:
711 del test[key][i]
712 else:
713 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
714 break
715 if not found_key:
716 raise BBGenErr('Could not find %s in existing list of values for key '
717 '%s in %s on %s' % (replacement_key, key, test_name,
718 tester_name))
719
Shenghua Zhangaba8bad2018-02-07 02:12:09720 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05721 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26722 True):
723 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06724 # are targeting CrOS hardware and so need the special trigger script.
725 dimension_sets = test['swarming']['dimension_sets']
Ben Pastenea9e583b2019-01-16 02:57:26726 if all('device_type' in ds for ds in dimension_sets):
727 test['trigger_script'] = {
728 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
729 }
Shenghua Zhangaba8bad2018-02-07 02:12:09730
Yuly Novikov26dd47052021-02-11 00:57:14731 def add_logdog_butler_cipd_package(self, tester_config, result):
732 if not tester_config.get('skip_cipd_packages', False):
733 cipd_packages = result['swarming'].get('cipd_packages', [])
734 already_added = len([
735 package for package in cipd_packages
736 if package.get('cipd_package', "").find('logdog/butler') > 0
737 ]) > 0
738 if not already_added:
739 cipd_packages.append({
740 'cipd_package':
741 'infra/tools/luci/logdog/butler/${platform}',
742 'location':
743 'bin',
744 'revision':
745 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
746 })
747 result['swarming']['cipd_packages'] = cipd_packages
748
Ben Pastene858f4be2019-01-09 23:52:09749 def add_android_presentation_args(self, tester_config, test_name, result):
750 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38751 bucket = tester_config.get('results_bucket', 'chromium-result-details')
752 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09753 if (result['swarming']['can_use_on_swarming_builders'] and not
754 tester_config.get('skip_merge_script', False)):
755 result['merge'] = {
756 'args': [
757 '--bucket',
John Budorick262ae112019-07-12 19:24:38758 bucket,
Ben Pastene858f4be2019-01-09 23:52:09759 '--test-name',
Rakib M. Hasanc9e01c62020-07-27 22:48:12760 result.get('name', test_name)
Ben Pastene858f4be2019-01-09 23:52:09761 ],
762 'script': '//build/android/pylib/results/presentation/'
763 'test_results_presentation.py',
764 }
Ben Pastene858f4be2019-01-09 23:52:09765 if not tester_config.get('skip_output_links', False):
766 result['swarming']['output_links'] = [
767 {
768 'link': [
769 'https://luci-logdog.appspot.com/v/?s',
770 '=android%2Fswarming%2Flogcats%2F',
771 '${TASK_ID}%2F%2B%2Funified_logcats',
772 ],
773 'name': 'shard #${SHARD_INDEX} logcats',
774 },
775 ]
776 if args:
777 result['args'] = args
778
Kenneth Russelleb60cbd22017-12-05 07:54:28779 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
780 test_config):
781 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15782 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28783 return None
784 result = copy.deepcopy(test_config)
785 if 'test' in result:
Rakib M. Hasanc9e01c62020-07-27 22:48:12786 if 'name' not in result:
787 result['name'] = test_name
Kenneth Russelleb60cbd22017-12-05 07:54:28788 else:
789 result['test'] = test_name
790 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21791
792 self.initialize_args_for_test(
793 result, tester_config, additional_arg_keys=['gtest_args'])
Jamie Madilla8be0d72020-10-02 05:24:04794 if self.is_android(tester_config) and tester_config.get(
Yuly Novikov26dd47052021-02-11 00:57:14795 'use_swarming', True):
796 if not test_config.get('use_isolated_scripts_api', False):
797 # TODO(https://crbug.com/1137998) make Android presentation work with
798 # isolated scripts in test_results_presentation.py merge script
799 self.add_android_presentation_args(tester_config, test_name, result)
800 result['args'] = result.get('args', []) + ['--recover-devices']
801 self.add_logdog_butler_cipd_package(tester_config, result)
Benjamin Pastene766d48f52017-12-18 21:47:42802
Stephen Martinis0382bc12018-09-17 22:29:07803 result = self.update_and_cleanup_test(
804 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09805 self.add_common_test_properties(result, tester_config)
Brian Sheedy5f173bb2021-11-24 00:45:54806 self.substitute_magic_args(result, tester_name)
Stephen Martinisbc7b7772019-05-01 22:01:43807
808 if not result.get('merge'):
809 # TODO(https://crbug.com/958376): Consider adding the ability to not have
810 # this default.
Jamie Madilla8be0d72020-10-02 05:24:04811 if test_config.get('use_isolated_scripts_api', False):
812 merge_script = 'standard_isolated_script_merge'
813 else:
814 merge_script = 'standard_gtest_merge'
815
Stephen Martinisbc7b7772019-05-01 22:01:43816 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04817 'script': '//testing/merge_scripts/%s.py' % merge_script,
818 'args': [],
Stephen Martinisbc7b7772019-05-01 22:01:43819 }
Kenneth Russelleb60cbd22017-12-05 07:54:28820 return result
821
822 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
823 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01824 if not self.should_run_on_tester(waterfall, tester_name, test_name,
825 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28826 return None
827 result = copy.deepcopy(test_config)
828 result['isolate_name'] = result.get('isolate_name', test_name)
Jeff Yoonb8bfdbf32020-03-13 19:14:43829 result['name'] = result.get('name', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28830 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01831 self.initialize_args_for_test(result, tester_config)
Yuly Novikov26dd47052021-02-11 00:57:14832 if self.is_android(tester_config) and tester_config.get(
833 'use_swarming', True):
834 if tester_config.get('use_android_presentation', False):
835 # TODO(https://crbug.com/1137998) make Android presentation work with
836 # isolated scripts in test_results_presentation.py merge script
837 self.add_android_presentation_args(tester_config, test_name, result)
838 self.add_logdog_butler_cipd_package(tester_config, result)
Stephen Martinis0382bc12018-09-17 22:29:07839 result = self.update_and_cleanup_test(
840 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09841 self.add_common_test_properties(result, tester_config)
Brian Sheedy5f173bb2021-11-24 00:45:54842 self.substitute_magic_args(result, tester_name)
Stephen Martinisf50047062019-05-06 22:26:17843
844 if not result.get('merge'):
845 # TODO(https://crbug.com/958376): Consider adding the ability to not have
846 # this default.
847 result['merge'] = {
848 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
849 'args': [],
850 }
Kenneth Russelleb60cbd22017-12-05 07:54:28851 return result
852
853 def generate_script_test(self, waterfall, tester_name, tester_config,
854 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44855 # TODO(https://crbug.com/953072): Remove this check whenever a better
856 # long-term solution is implemented.
857 if (waterfall.get('forbid_script_tests', False) or
858 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
859 raise BBGenErr('Attempted to generate a script test on tester ' +
860 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01861 if not self.should_run_on_tester(waterfall, tester_name, test_name,
862 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28863 return None
864 result = {
865 'name': test_name,
866 'script': test_config['script']
867 }
Stephen Martinis0382bc12018-09-17 22:29:07868 result = self.update_and_cleanup_test(
869 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy5f173bb2021-11-24 00:45:54870 self.substitute_magic_args(result, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28871 return result
872
873 def generate_junit_test(self, waterfall, tester_name, tester_config,
874 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01875 if not self.should_run_on_tester(waterfall, tester_name, test_name,
876 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28877 return None
John Budorickdef6acb2019-09-17 22:51:09878 result = copy.deepcopy(test_config)
879 result.update({
John Budorickcadc4952019-09-16 23:51:37880 'name': test_name,
881 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09882 })
883 self.initialize_args_for_test(result, tester_config)
884 result = self.update_and_cleanup_test(
885 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy5f173bb2021-11-24 00:45:54886 self.substitute_magic_args(result, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28887 return result
888
Xinan Lin05fb9c1752020-12-17 00:15:52889 def generate_skylab_test(self, waterfall, tester_name, tester_config,
890 test_name, test_config):
891 if not self.should_run_on_tester(waterfall, tester_name, test_name,
892 test_config):
893 return None
894 result = copy.deepcopy(test_config)
895 result.update({
896 'test': test_name,
897 })
898 self.initialize_args_for_test(result, tester_config)
899 result = self.update_and_cleanup_test(result, test_name, tester_name,
900 tester_config, waterfall)
Brian Sheedy5f173bb2021-11-24 00:45:54901 self.substitute_magic_args(result, tester_name)
Xinan Lin05fb9c1752020-12-17 00:15:52902 return result
903
Stephen Martinis2a0667022018-09-25 22:31:14904 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01905 substitutions = {
906 # Any machine in waterfalls.pyl which desires to run GPU tests
907 # must provide the os_type key.
908 'os_type': tester_config['os_type'],
909 'gpu_vendor_id': '0',
910 'gpu_device_id': '0',
911 }
Stephen Martinis2a0667022018-09-25 22:31:14912 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01913 if 'gpu' in dimension_set:
914 # First remove the driver version, then split into vendor and device.
915 gpu = dimension_set['gpu']
Yuly Novikove4b2fef2020-09-04 05:53:11916 if gpu != 'none':
917 gpu = gpu.split('-')[0].split(':')
918 substitutions['gpu_vendor_id'] = gpu[0]
919 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01920 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
921
922 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56923 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01924 # These are all just specializations of isolated script tests with
925 # a bunch of boilerplate command line arguments added.
926
927 # The step name must end in 'test' or 'tests' in order for the
928 # results to automatically show up on the flakiness dashboard.
929 # (At least, this was true some time ago.) Continue to use this
930 # naming convention for the time being to minimize changes.
931 step_name = test_config.get('name', test_name)
932 if not (step_name.endswith('test') or step_name.endswith('tests')):
933 step_name = '%s_tests' % step_name
934 result = self.generate_isolated_script_test(
935 waterfall, tester_name, tester_config, step_name, test_config)
936 if not result:
937 return None
Chong Gub75754b32020-03-13 16:39:20938 result['isolate_name'] = test_config.get(
Brian Sheedyf74819b2021-06-04 01:38:38939 'isolate_name',
940 self.get_default_isolate_name(tester_config, is_android_webview))
Chan Liab7d8dd82020-04-24 23:42:19941
Chan Lia3ad1502020-04-28 05:32:11942 # Populate test_id_prefix.
Brian Sheedyf74819b2021-06-04 01:38:38943 gn_entry = self.gn_isolate_map[result['isolate_name']]
Chan Li17d969f92020-07-10 00:50:03944 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19945
Kenneth Russell8a386d42018-06-02 09:48:01946 args = result.get('args', [])
947 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14948
Brian Sheedyd8c0c73d2021-07-05 02:11:30949 # TODO(skbug.com/12149): Remove this once Gold-based tests no longer clobber
950 # earlier results on retry attempts.
951 is_gold_based_test = False
952 for a in args:
953 if '--git-revision' in a:
954 is_gold_based_test = True
955 break
956 if is_gold_based_test:
957 for a in args:
958 if '--test-filter' in a or '--isolated-script-test-filter' in a:
959 raise RuntimeError(
960 '--test-filter/--isolated-script-test-filter are currently not '
961 'supported for Gold-based GPU tests. See skbug.com/12100 and '
962 'skbug.com/12149 for more details.')
963
erikchen6da2d9b2018-08-03 23:01:14964 # These tests upload and download results from cloud storage and therefore
965 # aren't idempotent yet. https://crbug.com/549140.
966 result['swarming']['idempotent'] = False
967
Kenneth Russell44910c32018-12-03 23:35:11968 # The GPU tests act much like integration tests for the entire browser, and
969 # tend to uncover flakiness bugs more readily than other test suites. In
970 # order to surface any flakiness more readily to the developer of the CL
971 # which is introducing it, we disable retries with patch on the commit
972 # queue.
973 result['should_retry_with_patch'] = False
974
Bo Liu555a0f92019-03-29 12:11:56975 browser = ('android-webview-instrumentation'
976 if is_android_webview else tester_config['browser_config'])
Brian Sheedy4053a702020-07-28 02:09:52977
978 # Most platforms require --enable-logging=stderr to get useful browser logs.
979 # However, this actively messes with logging on CrOS (because Chrome's
980 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
981 # in order to see JavaScript console messages. See
982 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
983 logging_arg = '--log-level=0' if self.is_chromeos(
984 tester_config) else '--enable-logging=stderr'
985
Kenneth Russell8a386d42018-06-02 09:48:01986 args = [
Bo Liu555a0f92019-03-29 12:11:56987 test_to_run,
988 '--show-stdout',
989 '--browser=%s' % browser,
990 # --passthrough displays more of the logging in Telemetry when
991 # run via typ, in particular some of the warnings about tests
992 # being expected to fail, but passing.
993 '--passthrough',
994 '-v',
Brian Sheedy4053a702020-07-28 02:09:52995 '--extra-browser-args=%s --js-flags=--expose-gc' % logging_arg,
Kenneth Russell8a386d42018-06-02 09:48:01996 ] + args
997 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14998 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01999 return result
1000
Brian Sheedyf74819b2021-06-04 01:38:381001 def get_default_isolate_name(self, tester_config, is_android_webview):
1002 if self.is_android(tester_config):
1003 if is_android_webview:
1004 return 'telemetry_gpu_integration_test_android_webview'
1005 return (
1006 'telemetry_gpu_integration_test' +
1007 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:331008 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:171009 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:331010 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:381011
Kenneth Russelleb60cbd22017-12-05 07:54:281012 def get_test_generator_map(self):
1013 return {
Bo Liu555a0f92019-03-29 12:11:561014 'android_webview_gpu_telemetry_tests':
1015 GPUTelemetryTestGenerator(self, is_android_webview=True),
Bo Liu555a0f92019-03-29 12:11:561016 'gpu_telemetry_tests':
1017 GPUTelemetryTestGenerator(self),
1018 'gtest_tests':
1019 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561020 'isolated_scripts':
1021 IsolatedScriptTestGenerator(self),
1022 'junit_tests':
1023 JUnitGenerator(self),
1024 'scripts':
1025 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:521026 'skylab_tests':
1027 SkylabGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281028 }
1029
Kenneth Russell8a386d42018-06-02 09:48:011030 def get_test_type_remapper(self):
1031 return {
1032 # These are a specialization of isolated_scripts with a bunch of
1033 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:561034 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:011035 'gpu_telemetry_tests': 'isolated_scripts',
1036 }
1037
Jeff Yoon67c3e832020-02-08 07:39:381038 def check_composition_type_test_suites(self, test_type,
1039 additional_validators=None):
1040 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1041 validators = [check_compound_references,
1042 check_basic_references,
1043 check_conflicting_definitions]
1044 if additional_validators:
1045 validators += additional_validators
1046
1047 target_suites = self.test_suites.get(test_type, {})
1048 other_test_type = ('compound_suites'
1049 if test_type == 'matrix_compound_suites'
1050 else 'matrix_compound_suites')
1051 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011052 basic_suites = self.test_suites.get('basic_suites', {})
1053
Jamie Madillcf4f8c72021-05-20 19:24:231054 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011055 if suite in basic_suites:
1056 raise BBGenErr('%s names may not duplicate basic test suite names '
1057 '(error found while processsing %s)'
1058 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011059
Jeff Yoon67c3e832020-02-08 07:39:381060 seen_tests = {}
1061 for sub_suite in suite_def:
1062 for validator in validators:
1063 validator(
1064 basic_suites=basic_suites,
1065 other_test_suites=other_suites,
1066 seen_tests=seen_tests,
1067 sub_suite=sub_suite,
1068 suite=suite,
1069 suite_def=suite_def,
1070 target_test_suites=target_suites,
1071 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051072 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381073 )
Kenneth Russelleb60cbd22017-12-05 07:54:281074
Stephen Martinis54d64ad2018-09-21 22:16:201075 def flatten_test_suites(self):
1076 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011077 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1078 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231079 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011080 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201081 self.test_suites = new_test_suites
1082
Chan Lia3ad1502020-04-28 05:32:111083 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231084 for suite in self.test_suites['basic_suites'].values():
1085 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561086 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411087
1088 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
John Palmera8515fca2021-05-20 03:35:321089 # 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:411090 # TODO(crbug.com/1035124): clean this up.
1091 isolate_name = test.get('test') or test.get('isolate_name') or key
1092 gn_entry = self.gn_isolate_map.get(isolate_name)
1093 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281094 label = gn_entry['label']
1095
1096 if label.count(':') != 1:
1097 raise BBGenErr(
1098 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1099 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1100 (label, isolate_name))
1101 if label.split(':')[1] != isolate_name:
1102 raise BBGenErr(
1103 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1104 ' label "%s" see http://crbug.com/1071091 for details.' %
1105 (isolate_name, label))
1106
Chan Lia3ad1502020-04-28 05:32:111107 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411108 else: # pragma: no cover
1109 # Some tests do not have an entry gn_isolate_map.pyl, such as
1110 # telemetry tests.
1111 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
1112 pass
1113
Kenneth Russelleb60cbd22017-12-05 07:54:281114 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011115 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201116
Jeff Yoon8154e582019-12-03 23:30:011117 compound_suites = self.test_suites.get('compound_suites', {})
1118 # check_composition_type_test_suites() checks that all basic suites
1119 # referenced by compound suites exist.
1120 basic_suites = self.test_suites.get('basic_suites')
1121
Jamie Madillcf4f8c72021-05-20 19:24:231122 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011123 # Resolve this to a dictionary.
1124 full_suite = {}
1125 for entry in value:
1126 suite = basic_suites[entry]
1127 full_suite.update(suite)
1128 compound_suites[name] = full_suite
1129
Jeff Yoon85fb8df2020-08-20 16:47:431130 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381131 """ Merge variant-defined configurations to each test case definition in a
1132 test suite.
1133
1134 The output maps a unique test name to an array of configurations because
1135 there may exist more than one definition for a test name using variants. The
1136 test name is referenced while mapping machines to test suites, so unpacking
1137 the array is done by the generators.
1138
1139 Args:
1140 basic_test_definition: a {} defined test suite in the format
1141 test_name:test_config
1142 variants: an [] of {} defining configurations to be applied to each test
1143 case in the basic test_definition
1144
1145 Return:
1146 a {} of test_name:[{}], where each {} is a merged configuration
1147 """
1148
1149 # Each test in a basic test suite will have a definition per variant.
1150 test_suite = {}
Jamie Madillcf4f8c72021-05-20 19:24:231151 for test_name, test_config in basic_test_definition.items():
Jeff Yoon67c3e832020-02-08 07:39:381152 definitions = []
1153 for variant in variants:
Jeff Yoonda581c32020-03-06 03:56:051154 # Unpack the variant from variants.pyl if it's string based.
1155 if isinstance(variant, str):
1156 variant = self.variants[variant]
1157
Jieting Yangef6b1042021-11-30 21:33:481158 # If 'enabled' is set to False, we will not use this variant;
1159 # otherwise if the variant doesn't include 'enabled' variable or
1160 # 'enabled' is set to True, we will use this variant
1161 if not variant.get('enabled', True):
1162 continue
Jeff Yoon67c3e832020-02-08 07:39:381163 # Clone a copy of test_config so that we can have a uniquely updated
1164 # version of it per variant
1165 cloned_config = copy.deepcopy(test_config)
1166 # The variant definition needs to be re-used for each test, so we'll
1167 # create a clone and work with it as well.
1168 cloned_variant = copy.deepcopy(variant)
1169
1170 cloned_config['args'] = (cloned_config.get('args', []) +
1171 cloned_variant.get('args', []))
1172 cloned_config['mixins'] = (cloned_config.get('mixins', []) +
Jeff Yoon85fb8df2020-08-20 16:47:431173 cloned_variant.get('mixins', []) + mixins)
Jeff Yoon67c3e832020-02-08 07:39:381174
1175 basic_swarming_def = cloned_config.get('swarming', {})
1176 variant_swarming_def = cloned_variant.get('swarming', {})
1177 if basic_swarming_def and variant_swarming_def:
1178 if ('dimension_sets' in basic_swarming_def and
1179 'dimension_sets' in variant_swarming_def):
1180 # Retain swarming dimension set merge behavior when both variant and
1181 # the basic test configuration both define it
1182 self.dictionary_merge(basic_swarming_def, variant_swarming_def)
1183 # Remove dimension_sets from the variant definition, so that it does
1184 # not replace what's been done by dictionary_merge in the update
1185 # call below.
1186 del variant_swarming_def['dimension_sets']
1187
1188 # Update the swarming definition with whatever is defined for swarming
1189 # by the variant.
1190 basic_swarming_def.update(variant_swarming_def)
1191 cloned_config['swarming'] = basic_swarming_def
1192
Xinan Lin05fb9c1752020-12-17 00:15:521193 # Copy all skylab fields defined by the variant.
1194 skylab_config = cloned_variant.get('skylab')
1195 if skylab_config:
1196 for k, v in skylab_config.items():
Jieting Yangef6b1042021-11-30 21:33:481197 # cros_chrome_version is the ash chrome version in the cros img
1198 # in the variant of cros_board. We don't want to include it in
1199 # the final json files; so remove it.
1200 if k == 'cros_chrome_version':
1201 continue
Xinan Lin05fb9c1752020-12-17 00:15:521202 cloned_config[k] = v
1203
Jeff Yoon67c3e832020-02-08 07:39:381204 # The identifier is used to make the name of the test unique.
1205 # Generators in the recipe uniquely identify a test by it's name, so we
1206 # don't want to have the same name for each variant.
1207 cloned_config['name'] = '{}_{}'.format(test_name,
1208 cloned_variant['identifier'])
Jeff Yoon67c3e832020-02-08 07:39:381209 definitions.append(cloned_config)
1210 test_suite[test_name] = definitions
1211 return test_suite
1212
Jeff Yoon8154e582019-12-03 23:30:011213 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381214 self.check_composition_type_test_suites('matrix_compound_suites',
1215 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011216
1217 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381218 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011219 # referenced by matrix suites exist.
1220 basic_suites = self.test_suites.get('basic_suites')
1221
Jamie Madillcf4f8c72021-05-20 19:24:231222 for test_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011223 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381224
Jamie Madillcf4f8c72021-05-20 19:24:231225 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381226 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1227
1228 if 'variants' in mtx_test_suite_config:
Jeff Yoon85fb8df2020-08-20 16:47:431229 mixins = mtx_test_suite_config.get('mixins', [])
Jeff Yoon67c3e832020-02-08 07:39:381230 result = self.resolve_variants(basic_test_def,
Jeff Yoon85fb8df2020-08-20 16:47:431231 mtx_test_suite_config['variants'],
1232 mixins)
Jeff Yoon67c3e832020-02-08 07:39:381233 full_suite.update(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271234 else:
1235 suite = basic_suites[test_suite]
1236 full_suite.update(suite)
Jeff Yoon67c3e832020-02-08 07:39:381237 matrix_compound_suites[test_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281238
1239 def link_waterfalls_to_test_suites(self):
1240 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231241 for tester_name, tester in waterfall['machines'].items():
1242 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281243 if not value in self.test_suites:
1244 # Hard / impossible to cover this in the unit test.
1245 raise self.unknown_test_suite(
1246 value, tester_name, waterfall['name']) # pragma: no cover
1247 tester['test_suites'][suite] = self.test_suites[value]
1248
1249 def load_configuration_files(self):
1250 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
1251 self.test_suites = self.load_pyl_file('test_suites.pyl')
1252 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:011253 self.mixins = self.load_pyl_file('mixins.pyl')
Nodir Turakulovfce34292019-12-18 17:05:411254 self.gn_isolate_map = self.load_pyl_file('gn_isolate_map.pyl')
Chong Guee622242020-10-28 18:17:351255 for isolate_map in self.args.isolate_map_files:
1256 isolate_map = self.load_pyl_file(isolate_map)
1257 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1258 if duplicates:
1259 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1260 ', '.join(duplicates))
1261 self.gn_isolate_map.update(isolate_map)
1262
Jeff Yoonda581c32020-03-06 03:56:051263 self.variants = self.load_pyl_file('variants.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:281264
1265 def resolve_configuration_files(self):
Chan Lia3ad1502020-04-28 05:32:111266 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281267 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011268 self.resolve_matrix_compound_test_suites()
1269 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281270 self.link_waterfalls_to_test_suites()
1271
Nico Weberd18b8962018-05-16 19:39:381272 def unknown_bot(self, bot_name, waterfall_name):
1273 return BBGenErr(
1274 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1275
Kenneth Russelleb60cbd22017-12-05 07:54:281276 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1277 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381278 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281279 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1280
1281 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1282 return BBGenErr(
1283 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1284 ' on waterfall ' + waterfall_name)
1285
Stephen Martinisb72f6d22018-10-04 23:29:011286 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071287 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321288
1289 Checks in the waterfall, builder, and test objects for mixins.
1290 """
1291 def valid_mixin(mixin_name):
1292 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011293 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321294 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381295
Stephen Martinisb6a50492018-09-12 23:59:321296 def must_be_list(mixins, typ, name):
1297 """Asserts that given mixins are a list."""
1298 if not isinstance(mixins, list):
1299 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1300
Brian Sheedy7658c982020-01-08 02:27:581301 test_name = test.get('name')
1302 remove_mixins = set()
1303 if 'remove_mixins' in builder:
1304 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1305 for rm in builder['remove_mixins']:
1306 valid_mixin(rm)
1307 remove_mixins.add(rm)
1308 if 'remove_mixins' in test:
1309 must_be_list(test['remove_mixins'], 'test', test_name)
1310 for rm in test['remove_mixins']:
1311 valid_mixin(rm)
1312 remove_mixins.add(rm)
1313 del test['remove_mixins']
1314
Stephen Martinisb72f6d22018-10-04 23:29:011315 if 'mixins' in waterfall:
1316 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1317 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581318 if mixin in remove_mixins:
1319 continue
Stephen Martinisb6a50492018-09-12 23:59:321320 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531321 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321322
Stephen Martinisb72f6d22018-10-04 23:29:011323 if 'mixins' in builder:
1324 must_be_list(builder['mixins'], 'builder', builder_name)
1325 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581326 if mixin in remove_mixins:
1327 continue
Stephen Martinisb6a50492018-09-12 23:59:321328 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531329 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321330
Stephen Martinisb72f6d22018-10-04 23:29:011331 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071332 return test
1333
Stephen Martinis2a0667022018-09-25 22:31:141334 if not test_name:
1335 test_name = test.get('test')
1336 if not test_name: # pragma: no cover
1337 # Not the best name, but we should say something.
1338 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:011339 must_be_list(test['mixins'], 'test', test_name)
1340 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581341 # We don't bother checking if the given mixin is in remove_mixins here
1342 # since this is already the lowest level, so if a mixin is added here that
1343 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071344 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531345 test = self.apply_mixin(self.mixins[mixin], test, builder)
Jeff Yoon67c3e832020-02-08 07:39:381346 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071347 return test
Stephen Martinisb6a50492018-09-12 23:59:321348
Austin Eng148d9f0f2022-02-08 19:18:531349 def apply_mixin(self, mixin, test, builder):
Stephen Martinisb72f6d22018-10-04 23:29:011350 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321351
Stephen Martinis0382bc12018-09-17 22:29:071352 Mixins will not override an existing key. This is to ensure exceptions can
1353 override a setting a mixin applies.
1354
Stephen Martinisb72f6d22018-10-04 23:29:011355 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:321356 'dimension_sets', which is how normal test suites specify their dimensions,
1357 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
1358 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011359
Stephen Martinisb6a50492018-09-12 23:59:321360 """
1361 new_test = copy.deepcopy(test)
1362 mixin = copy.deepcopy(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011363 if 'swarming' in mixin:
1364 swarming_mixin = mixin['swarming']
1365 new_test.setdefault('swarming', {})
Brian Sheedycae63b22020-06-10 22:52:111366 # Copy over any explicit dimension sets first so that they will be updated
1367 # by any subsequent 'dimensions' entries.
1368 if 'dimension_sets' in swarming_mixin:
1369 existing_dimension_sets = new_test['swarming'].setdefault(
1370 'dimension_sets', [])
1371 # Appending to the existing list could potentially result in different
1372 # behavior depending on the order the mixins were applied, but that's
1373 # already the case for other parts of mixins, so trust that the user
1374 # will verify that the generated output is correct before submitting.
1375 for dimension_set in swarming_mixin['dimension_sets']:
1376 if dimension_set not in existing_dimension_sets:
1377 existing_dimension_sets.append(dimension_set)
1378 del swarming_mixin['dimension_sets']
Stephen Martinisb72f6d22018-10-04 23:29:011379 if 'dimensions' in swarming_mixin:
1380 new_test['swarming'].setdefault('dimension_sets', [{}])
1381 for dimension_set in new_test['swarming']['dimension_sets']:
1382 dimension_set.update(swarming_mixin['dimensions'])
1383 del swarming_mixin['dimensions']
Stephen Martinisb72f6d22018-10-04 23:29:011384 # python dict update doesn't do recursion at all. Just hard code the
1385 # nested update we need (mixin['swarming'] shouldn't clobber
1386 # test['swarming'], but should update it).
1387 new_test['swarming'].update(swarming_mixin)
1388 del mixin['swarming']
1389
Wezc0e835b702018-10-30 00:38:411390 if '$mixin_append' in mixin:
1391 # Values specified under $mixin_append should be appended to existing
1392 # lists, rather than replacing them.
1393 mixin_append = mixin['$mixin_append']
Austin Eng148d9f0f2022-02-08 19:18:531394 del mixin['$mixin_append']
Zhaoyang Li473dd0ae2021-05-10 18:28:281395
1396 # Append swarming named cache and delete swarming key, since it's under
1397 # another layer of dict.
1398 if 'named_caches' in mixin_append.get('swarming', {}):
1399 new_test['swarming'].setdefault('named_caches', [])
1400 new_test['swarming']['named_caches'].extend(
1401 mixin_append['swarming']['named_caches'])
1402 if len(mixin_append['swarming']) > 1:
1403 raise BBGenErr('Only named_caches is supported under swarming key in '
1404 '$mixin_append, but there are: %s' %
1405 sorted(mixin_append['swarming'].keys()))
1406 del mixin_append['swarming']
Wezc0e835b702018-10-30 00:38:411407 for key in mixin_append:
1408 new_test.setdefault(key, [])
1409 if not isinstance(mixin_append[key], list):
1410 raise BBGenErr(
1411 'Key "' + key + '" in $mixin_append must be a list.')
1412 if not isinstance(new_test[key], list):
1413 raise BBGenErr(
1414 'Cannot apply $mixin_append to non-list "' + key + '".')
1415 new_test[key].extend(mixin_append[key])
Austin Eng148d9f0f2022-02-08 19:18:531416
1417 args = new_test.get('args', [])
1418 # Array so we can assign to it in a nested scope.
1419 args_need_fixup = [False]
Wezc0e835b702018-10-30 00:38:411420 if 'args' in mixin_append:
Austin Eng148d9f0f2022-02-08 19:18:531421 args_need_fixup[0] = True
1422
1423 def add_conditional_args(key, fn):
1424 val = new_test.pop(key, [])
1425 if val and fn(builder):
1426 args.extend(val)
1427 args_need_fixup[0] = True
1428
1429 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
1430 add_conditional_args('lacros_args', self.is_lacros)
1431 add_conditional_args('linux_args', self.is_linux)
1432 add_conditional_args('android_args', self.is_android)
1433 add_conditional_args('chromeos_args', self.is_chromeos)
1434 add_conditional_args('mac_args', self.is_mac)
1435 add_conditional_args('win_args', self.is_win)
1436 add_conditional_args('win64_args', self.is_win64)
1437
1438 if args_need_fixup[0]:
1439 new_test['args'] = self.maybe_fixup_args_array(args)
Wezc0e835b702018-10-30 00:38:411440
Stephen Martinisb72f6d22018-10-04 23:29:011441 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321442 return new_test
1443
Greg Gutermanf60eb052020-03-12 17:40:011444 def generate_output_tests(self, waterfall):
1445 """Generates the tests for a waterfall.
1446
1447 Args:
1448 waterfall: a dictionary parsed from a master pyl file
1449 Returns:
1450 A dictionary mapping builders to test specs
1451 """
1452 return {
Jamie Madillcf4f8c72021-05-20 19:24:231453 name: self.get_tests_for_config(waterfall, name, config)
1454 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011455 }
1456
1457 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531458 generator_map = self.get_test_generator_map()
1459 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281460
Greg Gutermanf60eb052020-03-12 17:40:011461 tests = {}
1462 # Copy only well-understood entries in the machine's configuration
1463 # verbatim into the generated JSON.
1464 if 'additional_compile_targets' in config:
1465 tests['additional_compile_targets'] = config[
1466 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231467 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011468 if test_type not in generator_map:
1469 raise self.unknown_test_suite_type(
1470 test_type, name, waterfall['name']) # pragma: no cover
1471 test_generator = generator_map[test_type]
1472 # Let multiple kinds of generators generate the same kinds
1473 # of tests. For example, gpu_telemetry_tests are a
1474 # specialization of isolated_scripts.
1475 new_tests = test_generator.generate(
1476 waterfall, name, config, input_tests)
1477 remapped_test_type = test_type_remapper.get(test_type, test_type)
1478 tests[remapped_test_type] = test_generator.sort(
1479 tests.get(remapped_test_type, []) + new_tests)
1480
1481 return tests
1482
1483 def jsonify(self, all_tests):
1484 return json.dumps(
1485 all_tests, indent=2, separators=(',', ': '),
1486 sort_keys=True) + '\n'
1487
1488 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281489 self.load_configuration_files()
1490 self.resolve_configuration_files()
1491 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011492 result = collections.defaultdict(dict)
1493
Dirk Pranke6269d302020-10-01 00:14:391494 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011495 for waterfall in self.waterfalls:
1496 for field in required_fields:
1497 # Verify required fields
1498 if field not in waterfall:
1499 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1500
1501 # Handle filter flag, if specified
1502 if filters and waterfall['name'] not in filters:
1503 continue
1504
1505 # Join config files and hardcoded values together
1506 all_tests = self.generate_output_tests(waterfall)
1507 result[waterfall['name']] = all_tests
1508
Greg Gutermanf60eb052020-03-12 17:40:011509 # Add do not edit warning
1510 for tests in result.values():
1511 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1512 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1513
1514 return result
1515
1516 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281517 suffix = '.json'
1518 if self.args.new_files:
1519 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011520
1521 for filename, contents in result.items():
1522 jsonstr = self.jsonify(contents)
1523 self.write_file(self.pyl_file_path(filename + suffix), jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281524
Nico Weberd18b8962018-05-16 19:39:381525 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161526 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421527 # NOTE: This reference can cause issues; if a file changes there, the
1528 # presubmit here won't be run by default. A manually maintained list there
1529 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1530 # references to configs outside of this directory are added, please change
1531 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1532 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491533
Garrett Beaty7e866fc2021-06-16 14:12:101534 # Get the generated project.pyl so we can check if we should be enforcing
1535 # that the specs are for builders that actually exist
1536 # If not, return None to indicate that we won't enforce that builders in
1537 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491538 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1539 'project.pyl')
1540 if os.path.exists(project_pyl_path):
1541 settings = ast.literal_eval(self.read_file(project_pyl_path))
1542 if not settings.get('validate_source_side_specs_have_builder', True):
1543 return None
1544
Nico Weberd18b8962018-05-16 19:39:381545 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311546 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161547 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1548 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431549 for c in milo_configs:
1550 for l in self.read_file(c).splitlines():
1551 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311552 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431553 continue
1554 # l looks like
1555 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1556 # Extract win_chromium_dbg_ng part.
1557 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381558 return bot_names
1559
Ben Pastene9a010082019-09-25 20:41:371560 def get_builders_that_do_not_actually_exist(self):
Kenneth Russell8a386d42018-06-02 09:48:011561 # Some of the bots on the chromium.gpu.fyi waterfall in particular
1562 # are defined only to be mirrored into trybots, and don't actually
1563 # exist on any of the waterfalls or consoles.
1564 return [
Yuke Liao8373de52020-08-14 18:30:541565 'GPU FYI Fuchsia Builder',
1566 'ANGLE GPU Android Release (Nexus 5X)',
1567 'ANGLE GPU Linux Release (Intel HD 630)',
1568 'ANGLE GPU Linux Release (NVIDIA)',
Yuke Liao8373de52020-08-14 18:30:541569 'Optional Android Release (Nexus 5X)',
Brian Sheedy9584d812021-05-26 02:07:251570 'Optional Android Release (Pixel 4)',
Yuke Liao8373de52020-08-14 18:30:541571 'Optional Linux Release (Intel HD 630)',
1572 'Optional Linux Release (NVIDIA)',
1573 'Optional Mac Release (Intel)',
1574 'Optional Mac Retina Release (AMD)',
1575 'Optional Mac Retina Release (NVIDIA)',
1576 'Optional Win10 x64 Release (Intel HD 630)',
1577 'Optional Win10 x64 Release (NVIDIA)',
Yuke Liao8373de52020-08-14 18:30:541578 # chromium.fyi
1579 'linux-blink-rel-dummy',
1580 'linux-blink-optional-highdpi-rel-dummy',
1581 'mac10.12-blink-rel-dummy',
1582 'mac10.13-blink-rel-dummy',
1583 'mac10.14-blink-rel-dummy',
1584 'mac10.15-blink-rel-dummy',
Stephanie Kim7fbfd912020-08-21 21:11:001585 'mac11.0-blink-rel-dummy',
Preethi Mohan9c0fa2992021-08-17 17:25:451586 'mac11.0.arm64-blink-rel-dummy',
Yuke Liao8373de52020-08-14 18:30:541587 'win7-blink-rel-dummy',
Preethi Mohan47d03dc2021-06-28 23:08:021588 'win10.20h2-blink-rel-dummy',
Preethi Mohan9f6f13902022-02-14 20:25:091589 'win11-blink-rel-dummy',
Yuke Liao8373de52020-08-14 18:30:541590 'WebKit Linux layout_ng_disabled Builder',
1591 # chromium, due to https://crbug.com/878915
1592 'win-dbg',
1593 'win32-dbg',
1594 'win-archive-dbg',
1595 'win32-archive-dbg',
Stephanie Kim107c1b0e2020-11-18 21:49:411596 # TODO crbug.com/1143924: Remove once experimentation is complete
1597 'Linux Builder Robocrop',
1598 'Linux Tests Robocrop',
Kenneth Russell8a386d42018-06-02 09:48:011599 ]
1600
Ben Pastene9a010082019-09-25 20:41:371601 def get_internal_waterfalls(self):
1602 # Similar to get_builders_that_do_not_actually_exist above, but for
1603 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201604 return [
1605 'chrome', 'chrome.pgo', 'internal.chrome.fyi', 'internal.chromeos.fyi',
1606 'internal.soda'
1607 ]
Ben Pastene9a010082019-09-25 20:41:371608
Stephen Martinisf83893722018-09-19 00:02:181609 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201610 self.check_input_files_sorting(verbose)
1611
Kenneth Russelleb60cbd22017-12-05 07:54:281612 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011613 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381614 self.check_composition_type_test_suites('matrix_compound_suites',
1615 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111616 self.resolve_test_id_prefixes()
Stephen Martinis54d64ad2018-09-21 22:16:201617 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381618
1619 # All bots should exist.
1620 bot_names = self.get_valid_bot_names()
Ben Pastene9a010082019-09-25 20:41:371621 builders_that_dont_exist = self.get_builders_that_do_not_actually_exist()
Garrett Beaty2a02de3c2020-05-15 13:57:351622 if bot_names is not None:
1623 internal_waterfalls = self.get_internal_waterfalls()
1624 for waterfall in self.waterfalls:
1625 # TODO(crbug.com/991417): Remove the need for this exception.
1626 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011627 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351628 for bot_name in waterfall['machines']:
1629 if bot_name in builders_that_dont_exist:
Kenneth Russell78fd8702018-05-17 01:15:521630 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351631 if bot_name not in bot_names:
1632 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
1633 # TODO(thakis): Remove this once these bots move to luci.
1634 continue # pragma: no cover
1635 if waterfall['name'] in ['tryserver.webrtc',
1636 'webrtc.chromium.fyi.experimental']:
1637 # These waterfalls have their bot configs in a different repo.
1638 # so we don't know about their bot names.
1639 continue # pragma: no cover
1640 if waterfall['name'] in ['client.devtools-frontend.integration',
1641 'tryserver.devtools-frontend',
1642 'chromium.devtools-frontend']:
1643 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201644 if waterfall['name'] in ['client.openscreen.chromium']:
1645 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351646 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381647
Kenneth Russelleb60cbd22017-12-05 07:54:281648 # All test suites must be referenced.
1649 suites_seen = set()
1650 generator_map = self.get_test_generator_map()
1651 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231652 for bot_name, tester in waterfall['machines'].items():
1653 for suite_type, suite in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281654 if suite_type not in generator_map:
1655 raise self.unknown_test_suite_type(suite_type, bot_name,
1656 waterfall['name'])
1657 if suite not in self.test_suites:
1658 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1659 suites_seen.add(suite)
1660 # Since we didn't resolve the configuration files, this set
1661 # includes both composition test suites and regular ones.
1662 resolved_suites = set()
1663 for suite_name in suites_seen:
1664 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011665 for sub_suite in suite:
1666 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281667 resolved_suites.add(suite_name)
1668 # At this point, every key in test_suites.pyl should be referenced.
1669 missing_suites = set(self.test_suites.keys()) - resolved_suites
1670 if missing_suites:
1671 raise BBGenErr('The following test suites were unreferenced by bots on '
1672 'the waterfalls: ' + str(missing_suites))
1673
1674 # All test suite exceptions must refer to bots on the waterfall.
1675 all_bots = set()
1676 missing_bots = set()
1677 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231678 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281679 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281680 # In order to disambiguate between bots with the same name on
1681 # different waterfalls, support has been added to various
1682 # exceptions for concatenating the waterfall name after the bot
1683 # name.
1684 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231685 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381686 removals = (exception.get('remove_from', []) +
1687 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231688 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381689 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281690 if removal not in all_bots:
1691 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411692
Ben Pastene9a010082019-09-25 20:41:371693 missing_bots = missing_bots - set(builders_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281694 if missing_bots:
1695 raise BBGenErr('The following nonexistent machines were referenced in '
1696 'the test suite exceptions: ' + str(missing_bots))
1697
Stephen Martinis0382bc12018-09-17 22:29:071698 # All mixins must be referenced
1699 seen_mixins = set()
1700 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011701 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231702 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011703 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071704 for suite in self.test_suites.values():
1705 if isinstance(suite, list):
1706 # Don't care about this, it's a composition, which shouldn't include a
1707 # swarming mixin.
1708 continue
1709
1710 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561711 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011712 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071713
Zhaoyang Li9da047d52021-05-10 21:31:441714 for variant in self.variants:
1715 # Unpack the variant from variants.pyl if it's string based.
1716 if isinstance(variant, str):
1717 variant = self.variants[variant]
1718 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1719
Stephen Martinisb72f6d22018-10-04 23:29:011720 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071721 if missing_mixins:
1722 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1723 ' referenced in a waterfall, machine, or test suite.' % (
1724 str(missing_mixins)))
1725
Jeff Yoonda581c32020-03-06 03:56:051726 # All variant references must be referenced
1727 seen_variants = set()
1728 for suite in self.test_suites.values():
1729 if isinstance(suite, list):
1730 continue
1731
1732 for test in suite.values():
1733 if isinstance(test, dict):
1734 for variant in test.get('variants', []):
1735 if isinstance(variant, str):
1736 seen_variants.add(variant)
1737
1738 missing_variants = set(self.variants.keys()) - seen_variants
1739 if missing_variants:
1740 raise BBGenErr('The following variants were unreferenced: %s. They must '
1741 'be referenced in a matrix test suite under the variants '
1742 'key.' % str(missing_variants))
1743
Stephen Martinis54d64ad2018-09-21 22:16:201744
1745 def type_assert(self, node, typ, filename, verbose=False):
1746 """Asserts that the Python AST node |node| is of type |typ|.
1747
1748 If verbose is set, it prints out some helpful context lines, showing where
1749 exactly the error occurred in the file.
1750 """
1751 if not isinstance(node, typ):
1752 if verbose:
1753 lines = [""] + self.read_file(filename).splitlines()
1754
1755 context = 2
1756 lines_start = max(node.lineno - context, 0)
1757 # Add one to include the last line
1758 lines_end = min(node.lineno + context, len(lines)) + 1
1759 lines = (
1760 ['== %s ==\n' % filename] +
1761 ["<snip>\n"] +
1762 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1763 lines[lines_start:lines_start + context])] +
1764 ['-' * 80 + '\n'] +
1765 ['%d %s' % (node.lineno, lines[node.lineno])] +
1766 ['-' * (node.col_offset + 3) + '^' + '-' * (
1767 80 - node.col_offset - 4) + '\n'] +
1768 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1769 lines[node.lineno + 1:lines_end])] +
1770 ["<snip>\n"]
1771 )
1772 # Print out a useful message when a type assertion fails.
1773 for l in lines:
1774 self.print_line(l.strip())
1775
1776 node_dumped = ast.dump(node, annotate_fields=False)
1777 # If the node is huge, truncate it so everything fits in a terminal
1778 # window.
1779 if len(node_dumped) > 60: # pragma: no cover
1780 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1781 raise BBGenErr(
1782 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1783 ' be %s, is %s' % (
1784 filename, node_dumped,
1785 node.lineno, typ, type(node)))
1786
Stephen Martinis5bef0fc2020-01-06 22:47:531787 def check_ast_list_formatted(self, keys, filename, verbose,
Stephen Martinis1384ff92020-01-07 19:52:151788 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531789 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201790
Stephen Martinis5bef0fc2020-01-06 22:47:531791 Currently only checks to ensure they're correctly sorted, and that there
1792 are no duplicates.
1793
1794 Args:
1795 keys: An python list of AST nodes.
1796
1797 It's a list of AST nodes instead of a list of strings because
1798 when verbose is set, it tries to print out context of where the
1799 diffs are in the file.
1800 filename: The name of the file this node is from.
1801 verbose: If set, print out diff information about how the keys are
1802 incorrectly formatted.
1803 check_sorting: If true, checks if the list is sorted.
1804 Returns:
1805 If the keys are correctly formatted.
1806 """
1807 if not keys:
1808 return True
1809
1810 assert isinstance(keys[0], ast.Str)
1811
1812 keys_strs = [k.s for k in keys]
1813 # Keys to diff against. Used below.
1814 keys_to_diff_against = None
1815 # If the list is properly formatted.
1816 list_formatted = True
1817
1818 # Duplicates are always bad.
1819 if len(set(keys_strs)) != len(keys_strs):
1820 list_formatted = False
1821 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1822
1823 if check_sorting and sorted(keys_strs) != keys_strs:
1824 list_formatted = False
1825 if list_formatted:
1826 return True
1827
1828 if verbose:
1829 line_num = keys[0].lineno
1830 keys = [k.s for k in keys]
1831 if check_sorting:
1832 # If we have duplicates, sorting this will take care of it anyways.
1833 keys_to_diff_against = sorted(set(keys))
1834 # else, keys_to_diff_against is set above already
1835
1836 self.print_line('=' * 80)
1837 self.print_line('(First line of keys is %s)' % line_num)
1838 for line in difflib.context_diff(
1839 keys, keys_to_diff_against,
1840 fromfile='current (%r)' % filename, tofile='sorted', lineterm=''):
1841 self.print_line(line)
1842 self.print_line('=' * 80)
1843
1844 return False
1845
Stephen Martinis1384ff92020-01-07 19:52:151846 def check_ast_dict_formatted(self, node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531847 """Checks if an ast dictionary's keys are correctly formatted.
1848
1849 Just a simple wrapper around check_ast_list_formatted.
1850 Args:
1851 node: An AST node. Assumed to be a dictionary.
1852 filename: The name of the file this node is from.
1853 verbose: If set, print out diff information about how the keys are
1854 incorrectly formatted.
1855 check_sorting: If true, checks if the list is sorted.
1856 Returns:
1857 If the dictionary is correctly formatted.
1858 """
Stephen Martinis54d64ad2018-09-21 22:16:201859 keys = []
1860 # The keys of this dict are ordered as ordered in the file; normal python
1861 # dictionary keys are given an arbitrary order, but since we parsed the
1862 # file itself, the order as given in the file is preserved.
1863 for key in node.keys:
1864 self.type_assert(key, ast.Str, filename, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531865 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201866
Stephen Martinis1384ff92020-01-07 19:52:151867 return self.check_ast_list_formatted(keys, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181868
1869 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201870 # TODO(https://crbug.com/886993): Add the ability for this script to
1871 # actually format the files, rather than just complain if they're
1872 # incorrectly formatted.
1873 bad_files = set()
Stephen Martinis5bef0fc2020-01-06 22:47:531874 def parse_file(filename):
1875 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201876
Stephen Martinis5bef0fc2020-01-06 22:47:531877 Returns an AST node representing the value in the pyl file."""
Stephen Martinisf83893722018-09-19 00:02:181878 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1879
Stephen Martinisf83893722018-09-19 00:02:181880 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201881 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181882 module = parsed.body
1883
1884 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201885 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181886 if len(module) != 1: # pragma: no cover
1887 raise BBGenErr('Invalid .pyl file %s' % filename)
1888 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201889 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181890
Stephen Martinis5bef0fc2020-01-06 22:47:531891 return expr.value
1892
1893 # Handle this separately
1894 filename = 'waterfalls.pyl'
1895 value = parse_file(filename)
1896 # Value should be a list.
1897 self.type_assert(value, ast.List, filename, verbose)
1898
1899 keys = []
Joshua Hood56c673c2022-03-02 20:29:331900 for elm in value.elts:
1901 self.type_assert(elm, ast.Dict, filename, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531902 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331903 for key, val in zip(elm.keys, elm.values):
Stephen Martinis5bef0fc2020-01-06 22:47:531904 self.type_assert(key, ast.Str, filename, verbose)
1905 if key.s == 'machines':
1906 if not self.check_ast_dict_formatted(val, filename, verbose):
1907 bad_files.add(filename)
1908
1909 if key.s == "name":
1910 self.type_assert(val, ast.Str, filename, verbose)
1911 waterfall_name = val
1912 assert waterfall_name
1913 keys.append(waterfall_name)
1914
Stephen Martinis1384ff92020-01-07 19:52:151915 if not self.check_ast_list_formatted(keys, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531916 bad_files.add(filename)
1917
1918 for filename in (
1919 'mixins.pyl',
1920 'test_suites.pyl',
1921 'test_suite_exceptions.pyl',
1922 ):
1923 value = parse_file(filename)
Stephen Martinisf83893722018-09-19 00:02:181924 # Value should be a dictionary.
Stephen Martinis54d64ad2018-09-21 22:16:201925 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181926
Stephen Martinis5bef0fc2020-01-06 22:47:531927 if not self.check_ast_dict_formatted(
1928 value, filename, verbose):
1929 bad_files.add(filename)
1930
Stephen Martinis54d64ad2018-09-21 22:16:201931 if filename == 'test_suites.pyl':
Jeff Yoon8154e582019-12-03 23:30:011932 expected_keys = ['basic_suites',
1933 'compound_suites',
1934 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201935 actual_keys = [node.s for node in value.keys]
1936 assert all(key in expected_keys for key in actual_keys), (
1937 'Invalid %r file; expected keys %r, got %r' % (
1938 filename, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:331939 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:201940 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011941 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201942 for suite_group in suite_dicts:
Stephen Martinis5bef0fc2020-01-06 22:47:531943 if not self.check_ast_dict_formatted(
Stephen Martinis54d64ad2018-09-21 22:16:201944 suite_group, filename, verbose):
1945 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181946
Stephen Martinis5bef0fc2020-01-06 22:47:531947 for key, suite in zip(value.keys, value.values):
1948 # The compound suites are checked in
1949 # 'check_composition_type_test_suites()'
1950 if key.s == 'basic_suites':
1951 for group in suite.values:
Stephen Martinis1384ff92020-01-07 19:52:151952 if not self.check_ast_dict_formatted(group, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531953 bad_files.add(filename)
1954 break
Stephen Martinis54d64ad2018-09-21 22:16:201955
Stephen Martinis5bef0fc2020-01-06 22:47:531956 elif filename == 'test_suite_exceptions.pyl':
1957 # Check the values for each test.
1958 for test in value.values:
1959 for kind, node in zip(test.keys, test.values):
1960 if isinstance(node, ast.Dict):
Stephen Martinis1384ff92020-01-07 19:52:151961 if not self.check_ast_dict_formatted(node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531962 bad_files.add(filename)
1963 elif kind.s == 'remove_from':
1964 # Don't care about sorting; these are usually grouped, since the
1965 # same bug can affect multiple builders. Do want to make sure
1966 # there aren't duplicates.
1967 if not self.check_ast_list_formatted(node.elts, filename, verbose,
1968 check_sorting=False):
1969 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181970
1971 if bad_files:
1972 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201973 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531974 'unsorted, or have duplicates. Re-run this with --verbose to see '
1975 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181976
Kenneth Russelleb60cbd22017-12-05 07:54:281977 def check_output_file_consistency(self, verbose=False):
1978 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011979 # All waterfalls/bucket .json files must have been written
1980 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:281981 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011982 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:161983 outputs = self.generate_outputs()
1984 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:011985 expected = self.jsonify(expected_contents)
1986 file_path = filename + '.json'
Zhiling Huangbe008172018-03-08 19:13:111987 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281988 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:011989 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:321990 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:011991 self.print_line('File ' + filename +
1992 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321993 'contents:')
1994 for line in difflib.unified_diff(
1995 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501996 current.splitlines(),
1997 fromfile='expected', tofile='current'):
1998 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:011999
2000 if ungenerated_files:
2001 raise BBGenErr(
2002 'The following files have not been properly '
2003 'autogenerated by generate_buildbot_json.py: ' +
2004 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:282005
Dirk Pranke772f55f2021-04-28 04:51:162006 for builder_group, builders in outputs.items():
2007 for builder, step_types in builders.items():
2008 for step_data in step_types.get('gtest_tests', []):
2009 step_name = step_data.get('name', step_data['test'])
2010 self._check_swarming_config(builder_group, builder, step_name,
2011 step_data)
2012 for step_data in step_types.get('isolated_scripts', []):
2013 step_name = step_data.get('name', step_data['isolate_name'])
2014 self._check_swarming_config(builder_group, builder, step_name,
2015 step_data)
2016
2017 def _check_swarming_config(self, filename, builder, step_name, step_data):
2018 # TODO(crbug.com/1203436): Ensure all swarming tests specify os and cpu, not
2019 # just mac tests.
2020 if ('mac' in builder.lower()
2021 and step_data['swarming']['can_use_on_swarming_builders']):
2022 dimension_sets = step_data['swarming'].get('dimension_sets')
2023 if not dimension_sets:
2024 raise BBGenErr('%s: %s / %s : os and cpu must be specified for mac '
2025 'swarmed tests' % (filename, builder, step_name))
2026 for s in dimension_sets:
2027 if not s.get('os') or not s.get('cpu'):
2028 raise BBGenErr('%s: %s / %s : os and cpu must be specified for mac '
2029 'swarmed tests' % (filename, builder, step_name))
2030
Kenneth Russelleb60cbd22017-12-05 07:54:282031 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502032 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282033 self.check_output_file_consistency(verbose) # pragma: no cover
2034
Karen Qiane24b7ee2019-02-12 23:37:062035 def does_test_match(self, test_info, params_dict):
2036 """Checks to see if the test matches the parameters given.
2037
2038 Compares the provided test_info with the params_dict to see
2039 if the bot matches the parameters given. If so, returns True.
2040 Else, returns false.
2041
2042 Args:
2043 test_info (dict): Information about a specific bot provided
2044 in the format shown in waterfalls.pyl
2045 params_dict (dict): Dictionary of parameters and their values
2046 to look for in the bot
2047 Ex: {
2048 'device_os':'android',
2049 '--flag':True,
2050 'mixins': ['mixin1', 'mixin2'],
2051 'ex_key':'ex_value'
2052 }
2053
2054 """
2055 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2056 'kvm', 'pool', 'integrity'] # dimension parameters
2057 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2058 'can_use_on_swarming_builders']
2059 for param in params_dict:
2060 # if dimension parameter
2061 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2062 if not 'swarming' in test_info:
2063 return False
2064 swarming = test_info['swarming']
2065 if param in SWARMING_PARAMS:
2066 if not param in swarming:
2067 return False
2068 if not str(swarming[param]) == params_dict[param]:
2069 return False
2070 else:
2071 if not 'dimension_sets' in swarming:
2072 return False
2073 d_set = swarming['dimension_sets']
2074 # only looking at the first dimension set
2075 if not param in d_set[0]:
2076 return False
2077 if not d_set[0][param] == params_dict[param]:
2078 return False
2079
2080 # if flag
2081 elif param.startswith('--'):
2082 if not 'args' in test_info:
2083 return False
2084 if not param in test_info['args']:
2085 return False
2086
2087 # not dimension parameter/flag/mixin
2088 else:
2089 if not param in test_info:
2090 return False
2091 if not test_info[param] == params_dict[param]:
2092 return False
2093 return True
2094 def error_msg(self, msg):
2095 """Prints an error message.
2096
2097 In addition to a catered error message, also prints
2098 out where the user can find more help. Then, program exits.
2099 """
2100 self.print_line(msg + (' If you need more information, ' +
2101 'please run with -h or --help to see valid commands.'))
2102 sys.exit(1)
2103
2104 def find_bots_that_run_test(self, test, bots):
2105 matching_bots = []
2106 for bot in bots:
2107 bot_info = bots[bot]
2108 tests = self.flatten_tests_for_bot(bot_info)
2109 for test_info in tests:
2110 test_name = ""
2111 if 'name' in test_info:
2112 test_name = test_info['name']
2113 elif 'test' in test_info:
2114 test_name = test_info['test']
2115 if not test_name == test:
2116 continue
2117 matching_bots.append(bot)
2118 return matching_bots
2119
2120 def find_tests_with_params(self, tests, params_dict):
2121 matching_tests = []
2122 for test_name in tests:
2123 test_info = tests[test_name]
2124 if not self.does_test_match(test_info, params_dict):
2125 continue
2126 if not test_name in matching_tests:
2127 matching_tests.append(test_name)
2128 return matching_tests
2129
2130 def flatten_waterfalls_for_query(self, waterfalls):
2131 bots = {}
2132 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012133 waterfall_tests = self.generate_output_tests(waterfall)
2134 for bot in waterfall_tests:
2135 bot_info = waterfall_tests[bot]
2136 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062137 return bots
2138
2139 def flatten_tests_for_bot(self, bot_info):
2140 """Returns a list of flattened tests.
2141
2142 Returns a list of tests not grouped by test category
2143 for a specific bot.
2144 """
2145 TEST_CATS = self.get_test_generator_map().keys()
2146 tests = []
2147 for test_cat in TEST_CATS:
2148 if not test_cat in bot_info:
2149 continue
2150 test_cat_tests = bot_info[test_cat]
2151 tests = tests + test_cat_tests
2152 return tests
2153
2154 def flatten_tests_for_query(self, test_suites):
2155 """Returns a flattened dictionary of tests.
2156
2157 Returns a dictionary of tests associate with their
2158 configuration, not grouped by their test suite.
2159 """
2160 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232161 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062162 for test in test_suite:
2163 test_info = test_suite[test]
2164 test_name = test
2165 if 'name' in test_info:
2166 test_name = test_info['name']
2167 tests[test_name] = test_info
2168 return tests
2169
2170 def parse_query_filter_params(self, params):
2171 """Parses the filter parameters.
2172
2173 Creates a dictionary from the parameters provided
2174 to filter the bot array.
2175 """
2176 params_dict = {}
2177 for p in params:
2178 # flag
2179 if p.startswith("--"):
2180 params_dict[p] = True
2181 else:
2182 pair = p.split(":")
2183 if len(pair) != 2:
2184 self.error_msg('Invalid command.')
2185 # regular parameters
2186 if pair[1].lower() == "true":
2187 params_dict[pair[0]] = True
2188 elif pair[1].lower() == "false":
2189 params_dict[pair[0]] = False
2190 else:
2191 params_dict[pair[0]] = pair[1]
2192 return params_dict
2193
2194 def get_test_suites_dict(self, bots):
2195 """Returns a dictionary of bots and their tests.
2196
2197 Returns a dictionary of bots and a list of their associated tests.
2198 """
2199 test_suite_dict = dict()
2200 for bot in bots:
2201 bot_info = bots[bot]
2202 tests = self.flatten_tests_for_bot(bot_info)
2203 test_suite_dict[bot] = tests
2204 return test_suite_dict
2205
2206 def output_query_result(self, result, json_file=None):
2207 """Outputs the result of the query.
2208
2209 If a json file parameter name is provided, then
2210 the result is output into the json file. If not,
2211 then the result is printed to the console.
2212 """
2213 output = json.dumps(result, indent=2)
2214 if json_file:
2215 self.write_file(json_file, output)
2216 else:
2217 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062218
Joshua Hood56c673c2022-03-02 20:29:332219 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062220 def query(self, args):
2221 """Queries tests or bots.
2222
2223 Depending on the arguments provided, outputs a json of
2224 tests or bots matching the appropriate optional parameters provided.
2225 """
2226 # split up query statement
2227 query = args.query.split('/')
2228 self.load_configuration_files()
2229 self.resolve_configuration_files()
2230
2231 # flatten bots json
2232 tests = self.test_suites
2233 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2234
2235 cmd_class = query[0]
2236
2237 # For queries starting with 'bots'
2238 if cmd_class == "bots":
2239 if len(query) == 1:
2240 return self.output_query_result(bots, args.json)
2241 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332242 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062243 if query[1] == 'tests':
2244 test_suites_dict = self.get_test_suites_dict(bots)
2245 return self.output_query_result(test_suites_dict, args.json)
Joshua Hood56c673c2022-03-02 20:29:332246 self.error_msg("This query should be in the format: bots/tests.")
Karen Qiane24b7ee2019-02-12 23:37:062247
2248 else:
2249 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2250 % str(len(query)-1))
2251
2252 # For queries starting with 'bot'
2253 elif cmd_class == "bot":
2254 if not len(query) == 2 and not len(query) == 3:
2255 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2256 % str(len(query)-1))
2257 bot_id = query[1]
2258 if not bot_id in bots:
2259 self.error_msg("No bot named '" + bot_id + "' found.")
2260 bot_info = bots[bot_id]
2261 if len(query) == 2:
2262 return self.output_query_result(bot_info, args.json)
2263 if not query[2] == 'tests':
2264 self.error_msg("The query should be in the format:" +
2265 "bot/<bot-name>/tests.")
2266
2267 bot_tests = self.flatten_tests_for_bot(bot_info)
2268 return self.output_query_result(bot_tests, args.json)
2269
2270 # For queries starting with 'tests'
2271 elif cmd_class == "tests":
2272 if not len(query) == 1 and not len(query) == 2:
2273 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2274 % str(len(query)-1))
2275 flattened_tests = self.flatten_tests_for_query(tests)
2276 if len(query) == 1:
2277 return self.output_query_result(flattened_tests, args.json)
2278
2279 # create params dict
2280 params = query[1].split('&')
2281 params_dict = self.parse_query_filter_params(params)
2282 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2283 return self.output_query_result(matching_bots)
2284
2285 # For queries starting with 'test'
2286 elif cmd_class == "test":
2287 if not len(query) == 2 and not len(query) == 3:
2288 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2289 % str(len(query)-1))
2290 test_id = query[1]
2291 if len(query) == 2:
2292 flattened_tests = self.flatten_tests_for_query(tests)
2293 for test in flattened_tests:
2294 if test == test_id:
2295 return self.output_query_result(flattened_tests[test], args.json)
2296 self.error_msg("There is no test named %s." % test_id)
2297 if not query[2] == 'bots':
2298 self.error_msg("The query should be in the format: " +
2299 "test/<test-name>/bots")
2300 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2301 return self.output_query_result(bots_for_test)
2302
2303 else:
2304 self.error_msg("Your command did not match any valid commands." +
2305 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Joshua Hood56c673c2022-03-02 20:29:332306 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282307
Garrett Beaty1afaccc2020-06-25 19:58:152308 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282309 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502310 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062311 elif self.args.query:
2312 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282313 else:
Greg Gutermanf60eb052020-03-12 17:40:012314 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282315 return 0
2316
2317if __name__ == "__main__": # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152318 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2319 sys.exit(generator.main())