blob: 89f306055997beb20a3736f7336bfde42c2edd7e [file] [log] [blame]
Joshua Hood3455e1352022-03-03 23:23:591#!/usr/bin/env vpython3
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):
Fabrice de Ganscbd655f2022-08-04 20:15:3083 def __init__(self, bb_gen, is_android_webview=False, is_cast_streaming=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
Fabrice de Ganscbd655f2022-08-04 20:15:3086 self._is_cast_streaming = is_cast_streaming
Kenneth Russell8a386d42018-06-02 09:48:0187
88 def generate(self, waterfall, tester_name, tester_config, input_tests):
89 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:2390 for test_name, test_config in sorted(input_tests.items()):
Ben Pastene8e7eb2652022-04-29 19:44:3191 # Variants allow more than one definition for a given test, and is defined
92 # in array format from resolve_variants().
93 if not isinstance(test_config, list):
94 test_config = [test_config]
95
96 for config in test_config:
97 test = self.bb_gen.generate_gpu_telemetry_test(waterfall, tester_name,
98 tester_config, test_name,
99 config,
Fabrice de Ganscbd655f2022-08-04 20:15:30100 self._is_android_webview,
101 self._is_cast_streaming)
Ben Pastene8e7eb2652022-04-29 19:44:31102 if test:
103 isolated_scripts.append(test)
104
Kenneth Russell8a386d42018-06-02 09:48:01105 return isolated_scripts
106
107 def sort(self, tests):
108 return sorted(tests, key=lambda x: x['name'])
109
110
Kenneth Russelleb60cbd22017-12-05 07:54:28111class GTestGenerator(BaseGenerator):
112 def __init__(self, bb_gen):
113 super(GTestGenerator, self).__init__(bb_gen)
114
Kenneth Russell8ceeabf2017-12-11 17:53:28115 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28116 # The relative ordering of some of the tests is important to
117 # minimize differences compared to the handwritten JSON files, since
118 # Python's sorts are stable and there are some tests with the same
119 # key (see gles2_conform_d3d9_test and similar variants). Avoid
120 # losing the order by avoiding coalescing the dictionaries into one.
121 gtests = []
Jamie Madillcf4f8c72021-05-20 19:24:23122 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoon67c3e832020-02-08 07:39:38123 # Variants allow more than one definition for a given test, and is defined
124 # in array format from resolve_variants().
125 if not isinstance(test_config, list):
126 test_config = [test_config]
127
128 for config in test_config:
129 test = self.bb_gen.generate_gtest(
130 waterfall, tester_name, tester_config, test_name, config)
131 if test:
132 # generate_gtest may veto the test generation on this tester.
133 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28134 return gtests
135
136 def sort(self, tests):
Jamie Madillcf4f8c72021-05-20 19:24:23137 return sorted(tests, key=functools.cmp_to_key(cmp_tests))
Kenneth Russelleb60cbd22017-12-05 07:54:28138
139
140class IsolatedScriptTestGenerator(BaseGenerator):
141 def __init__(self, bb_gen):
142 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
143
Kenneth Russell8ceeabf2017-12-11 17:53:28144 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28145 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23146 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43147 # Variants allow more than one definition for a given test, and is defined
148 # in array format from resolve_variants().
149 if not isinstance(test_config, list):
150 test_config = [test_config]
151
152 for config in test_config:
153 test = self.bb_gen.generate_isolated_script_test(
154 waterfall, tester_name, tester_config, test_name, config)
155 if test:
156 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28157 return isolated_scripts
158
159 def sort(self, tests):
160 return sorted(tests, key=lambda x: x['name'])
161
162
163class ScriptGenerator(BaseGenerator):
164 def __init__(self, bb_gen):
165 super(ScriptGenerator, self).__init__(bb_gen)
166
Kenneth Russell8ceeabf2017-12-11 17:53:28167 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28168 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23169 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28170 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28171 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28172 if test:
173 scripts.append(test)
174 return scripts
175
176 def sort(self, tests):
177 return sorted(tests, key=lambda x: x['name'])
178
179
180class JUnitGenerator(BaseGenerator):
181 def __init__(self, bb_gen):
182 super(JUnitGenerator, self).__init__(bb_gen)
183
Kenneth Russell8ceeabf2017-12-11 17:53:28184 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28185 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23186 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28187 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28188 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28189 if test:
190 scripts.append(test)
191 return scripts
192
193 def sort(self, tests):
194 return sorted(tests, key=lambda x: x['test'])
195
196
Xinan Lin05fb9c1752020-12-17 00:15:52197class SkylabGenerator(BaseGenerator):
198 def __init__(self, bb_gen):
199 super(SkylabGenerator, self).__init__(bb_gen)
200
201 def generate(self, waterfall, tester_name, tester_config, input_tests):
202 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23203 for test_name, test_config in sorted(input_tests.items()):
Xinan Lin05fb9c1752020-12-17 00:15:52204 for config in test_config:
205 test = self.bb_gen.generate_skylab_test(waterfall, tester_name,
206 tester_config, test_name,
207 config)
208 if test:
209 scripts.append(test)
210 return scripts
211
212 def sort(self, tests):
213 return sorted(tests, key=lambda x: x['test'])
214
215
Jeff Yoon67c3e832020-02-08 07:39:38216def check_compound_references(other_test_suites=None,
217 sub_suite=None,
218 suite=None,
219 target_test_suites=None,
220 test_type=None,
221 **kwargs):
222 """Ensure comound reference's don't target other compounds"""
223 del kwargs
224 if sub_suite in other_test_suites or sub_suite in target_test_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15225 raise BBGenErr('%s may not refer to other composition type test '
226 'suites (error found while processing %s)' %
227 (test_type, suite))
228
Jeff Yoon67c3e832020-02-08 07:39:38229
230def check_basic_references(basic_suites=None,
231 sub_suite=None,
232 suite=None,
233 **kwargs):
234 """Ensure test has a basic suite reference"""
235 del kwargs
236 if sub_suite not in basic_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15237 raise BBGenErr('Unable to find reference to %s while processing %s' %
238 (sub_suite, suite))
239
Jeff Yoon67c3e832020-02-08 07:39:38240
241def check_conflicting_definitions(basic_suites=None,
242 seen_tests=None,
243 sub_suite=None,
244 suite=None,
245 test_type=None,
246 **kwargs):
247 """Ensure that if a test is reachable via multiple basic suites,
248 all of them have an identical definition of the tests.
249 """
250 del kwargs
251 for test_name in basic_suites[sub_suite]:
252 if (test_name in seen_tests and
253 basic_suites[sub_suite][test_name] !=
254 basic_suites[seen_tests[test_name]][test_name]):
255 raise BBGenErr('Conflicting test definitions for %s from %s '
256 'and %s in %s (error found while processing %s)'
257 % (test_name, seen_tests[test_name], sub_suite,
258 test_type, suite))
259 seen_tests[test_name] = sub_suite
260
261def check_matrix_identifier(sub_suite=None,
262 suite=None,
263 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05264 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38265 **kwargs):
266 """Ensure 'idenfitier' is defined for each variant"""
267 del kwargs
268 sub_suite_config = suite_def[sub_suite]
269 for variant in sub_suite_config.get('variants', []):
Jeff Yoonda581c32020-03-06 03:56:05270 if isinstance(variant, str):
271 if variant not in all_variants:
272 raise BBGenErr('Missing variant definition for %s in variants.pyl'
273 % variant)
274 variant = all_variants[variant]
275
Jeff Yoon67c3e832020-02-08 07:39:38276 if not 'identifier' in variant:
277 raise BBGenErr('Missing required identifier field in matrix '
278 'compound suite %s, %s' % (suite, sub_suite))
Sven Zhengef0d0872022-04-04 22:13:29279 if variant['identifier'] == '':
280 raise BBGenErr('Identifier field can not be "" in matrix '
281 'compound suite %s, %s' % (suite, sub_suite))
282 if variant['identifier'].strip() != variant['identifier']:
283 raise BBGenErr('Identifier field can not have leading and trailing '
284 'whitespace in matrix compound suite %s, %s' %
285 (suite, sub_suite))
Jeff Yoon67c3e832020-02-08 07:39:38286
287
Joshua Hood56c673c2022-03-02 20:29:33288class BBJSONGenerator(object): # pylint: disable=useless-object-inheritance
Garrett Beaty1afaccc2020-06-25 19:58:15289 def __init__(self, args):
Kenneth Russelleb60cbd22017-12-05 07:54:28290 self.this_dir = THIS_DIR
Garrett Beaty1afaccc2020-06-25 19:58:15291 self.args = args
Kenneth Russelleb60cbd22017-12-05 07:54:28292 self.waterfalls = None
293 self.test_suites = None
294 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01295 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41296 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05297 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28298
Garrett Beaty1afaccc2020-06-25 19:58:15299 @staticmethod
300 def parse_args(argv):
301
302 # RawTextHelpFormatter allows for styling of help statement
303 parser = argparse.ArgumentParser(
304 formatter_class=argparse.RawTextHelpFormatter)
305
306 group = parser.add_mutually_exclusive_group()
307 group.add_argument(
308 '-c',
309 '--check',
310 action='store_true',
311 help=
312 'Do consistency checks of configuration and generated files and then '
313 'exit. Used during presubmit. '
314 'Causes the tool to not generate any files.')
315 group.add_argument(
316 '--query',
317 type=str,
318 help=(
319 "Returns raw JSON information of buildbots and tests.\n" +
320 "Examples:\n" + " List all bots (all info):\n" +
321 " --query bots\n\n" +
322 " List all bots and only their associated tests:\n" +
323 " --query bots/tests\n\n" +
324 " List all information about 'bot1' " +
325 "(make sure you have quotes):\n" + " --query bot/'bot1'\n\n" +
326 " List tests running for 'bot1' (make sure you have quotes):\n" +
327 " --query bot/'bot1'/tests\n\n" + " List all tests:\n" +
328 " --query tests\n\n" +
329 " List all tests and the bots running them:\n" +
330 " --query tests/bots\n\n" +
331 " List all tests that satisfy multiple parameters\n" +
332 " (separation of parameters by '&' symbol):\n" +
333 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
334 " List all tests that run with a specific flag:\n" +
335 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
336 " List specific test (make sure you have quotes):\n"
337 " --query test/'test1'\n\n"
338 " List all bots running 'test1' " +
339 "(make sure you have quotes):\n" + " --query test/'test1'/bots"))
340 parser.add_argument(
341 '-n',
342 '--new-files',
343 action='store_true',
344 help=
345 'Write output files as .new.json. Useful during development so old and '
346 'new files can be looked at side-by-side.')
347 parser.add_argument('-v',
348 '--verbose',
349 action='store_true',
350 help='Increases verbosity. Affects consistency checks.')
351 parser.add_argument('waterfall_filters',
352 metavar='waterfalls',
353 type=str,
354 nargs='*',
355 help='Optional list of waterfalls to generate.')
356 parser.add_argument(
357 '--pyl-files-dir',
358 type=os.path.realpath,
359 help='Path to the directory containing the input .pyl files.')
360 parser.add_argument(
361 '--json',
362 metavar='JSON_FILE_PATH',
363 help='Outputs results into a json file. Only works with query function.'
364 )
Chong Guee622242020-10-28 18:17:35365 parser.add_argument('--isolate-map-file',
366 metavar='PATH',
367 help='path to additional isolate map files.',
368 default=[],
369 action='append',
370 dest='isolate_map_files')
Garrett Beaty1afaccc2020-06-25 19:58:15371 parser.add_argument(
372 '--infra-config-dir',
373 help='Path to the LUCI services configuration directory',
374 default=os.path.abspath(
375 os.path.join(os.path.dirname(__file__), '..', '..', 'infra',
376 'config')))
377 args = parser.parse_args(argv)
378 if args.json and not args.query:
379 parser.error(
380 "The --json flag can only be used with --query.") # pragma: no cover
381 args.infra_config_dir = os.path.abspath(args.infra_config_dir)
382 return args
383
Kenneth Russelleb60cbd22017-12-05 07:54:28384 def generate_abs_file_path(self, relative_path):
Garrett Beaty1afaccc2020-06-25 19:58:15385 return os.path.join(self.this_dir, relative_path)
Kenneth Russelleb60cbd22017-12-05 07:54:28386
Stephen Martinis7eb8b612018-09-21 00:17:50387 def print_line(self, line):
388 # Exists so that tests can mock
Jamie Madillcf4f8c72021-05-20 19:24:23389 print(line) # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:50390
Kenneth Russelleb60cbd22017-12-05 07:54:28391 def read_file(self, relative_path):
Garrett Beaty1afaccc2020-06-25 19:58:15392 with open(self.generate_abs_file_path(relative_path)) as fp:
393 return fp.read()
Kenneth Russelleb60cbd22017-12-05 07:54:28394
395 def write_file(self, relative_path, contents):
Garrett Beaty1afaccc2020-06-25 19:58:15396 with open(self.generate_abs_file_path(relative_path), 'wb') as fp:
Jamie Madillcf4f8c72021-05-20 19:24:23397 fp.write(contents.encode('utf-8'))
Kenneth Russelleb60cbd22017-12-05 07:54:28398
Zhiling Huangbe008172018-03-08 19:13:11399 def pyl_file_path(self, filename):
400 if self.args and self.args.pyl_files_dir:
401 return os.path.join(self.args.pyl_files_dir, filename)
402 return filename
403
Joshua Hood56c673c2022-03-02 20:29:33404 # pylint: disable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:28405 def load_pyl_file(self, filename):
406 try:
Zhiling Huangbe008172018-03-08 19:13:11407 return ast.literal_eval(self.read_file(
408 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28409 except (SyntaxError, ValueError) as e: # pragma: no cover
Joshua Hood56c673c2022-03-02 20:29:33410 six.raise_from(
411 BBGenErr('Failed to parse pyl file "%s": %s' % (filename, e)),
412 e) # pragma: no cover
413 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:28414
Kenneth Russell8a386d42018-06-02 09:48:01415 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
416 # Currently it is only mandatory for bots which run GPU tests. Change these to
417 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28418 def is_android(self, tester_config):
419 return tester_config.get('os_type') == 'android'
420
Ben Pastenea9e583b2019-01-16 02:57:26421 def is_chromeos(self, tester_config):
422 return tester_config.get('os_type') == 'chromeos'
423
Chong Guc2ca5d02022-01-11 19:52:17424 def is_fuchsia(self, tester_config):
425 return tester_config.get('os_type') == 'fuchsia'
426
Brian Sheedy781c8ca42021-03-08 22:03:21427 def is_lacros(self, tester_config):
428 return tester_config.get('os_type') == 'lacros'
429
Kenneth Russell8a386d42018-06-02 09:48:01430 def is_linux(self, tester_config):
431 return tester_config.get('os_type') == 'linux'
432
Kai Ninomiya40de9f52019-10-18 21:38:49433 def is_mac(self, tester_config):
434 return tester_config.get('os_type') == 'mac'
435
436 def is_win(self, tester_config):
437 return tester_config.get('os_type') == 'win'
438
439 def is_win64(self, tester_config):
440 return (tester_config.get('os_type') == 'win' and
441 tester_config.get('browser_config') == 'release_x64')
442
Ben Pastene5f231cf22022-05-05 18:03:07443 def add_variant_to_test_name(self, test_name, variant_id):
444 return '{} {}'.format(test_name, variant_id)
445
446 def remove_variant_from_test_name(self, test_name, variant_id):
447 return test_name.split(variant_id)[0].strip()
448
Kenneth Russelleb60cbd22017-12-05 07:54:28449 def get_exception_for_test(self, test_name, test_config):
450 # gtests may have both "test" and "name" fields, and usually, if the "name"
451 # field is specified, it means that the same test is being repurposed
452 # multiple times with different command line arguments. To handle this case,
453 # prefer to lookup per the "name" field of the test itself, as opposed to
454 # the "test_name", which is actually the "test" field.
455 if 'name' in test_config:
456 return self.exceptions.get(test_config['name'])
Joshua Hood56c673c2022-03-02 20:29:33457 return self.exceptions.get(test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28458
Nico Weberb0b3f5862018-07-13 18:45:15459 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28460 # Currently, the only reason a test should not run on a given tester is that
461 # it's in the exceptions. (Once the GPU waterfall generation script is
462 # incorporated here, the rules will become more complex.)
463 exception = self.get_exception_for_test(test_name, test_config)
464 if not exception:
465 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28466 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28467 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28468 if remove_from:
469 if tester_name in remove_from:
470 return False
471 # TODO(kbr): this code path was added for some tests (including
472 # android_webview_unittests) on one machine (Nougat Phone
473 # Tester) which exists with the same name on two waterfalls,
474 # chromium.android and chromium.fyi; the tests are run on one
475 # but not the other. Once the bots are all uniquely named (a
476 # different ongoing project) this code should be removed.
477 # TODO(kbr): add coverage.
478 return (tester_name + ' ' + waterfall['name']
479 not in remove_from) # pragma: no cover
480 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28481
Nico Weber79dc5f6852018-07-13 19:38:49482 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28483 exception = self.get_exception_for_test(test_name, test)
484 if not exception:
485 return None
Nico Weber79dc5f6852018-07-13 19:38:49486 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28487
Brian Sheedye6ea0ee2019-07-11 02:54:37488 def get_test_replacements(self, test, test_name, tester_name):
489 exception = self.get_exception_for_test(test_name, test)
490 if not exception:
491 return None
492 return exception.get('replacements', {}).get(tester_name)
493
Kenneth Russell8a386d42018-06-02 09:48:01494 def merge_command_line_args(self, arr, prefix, splitter):
495 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01496 idx = 0
497 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01498 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01499 while idx < len(arr):
500 flag = arr[idx]
501 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01502 if flag.startswith(prefix):
503 arg = flag[prefix_len:]
504 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01505 if first_idx < 0:
506 first_idx = idx
507 else:
508 delete_current_entry = True
509 if delete_current_entry:
510 del arr[idx]
511 else:
512 idx += 1
513 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01514 arr[first_idx] = prefix + splitter.join(accumulated_args)
515 return arr
516
517 def maybe_fixup_args_array(self, arr):
518 # The incoming array of strings may be an array of command line
519 # arguments. To make it easier to turn on certain features per-bot or
520 # per-test-suite, look specifically for certain flags and merge them
521 # appropriately.
522 # --enable-features=Feature1 --enable-features=Feature2
523 # are merged to:
524 # --enable-features=Feature1,Feature2
525 # and:
526 # --extra-browser-args=arg1 --extra-browser-args=arg2
527 # are merged to:
528 # --extra-browser-args=arg1 arg2
529 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
530 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Yuly Novikov8c487e72020-10-16 20:00:29531 arr = self.merge_command_line_args(arr, '--test-launcher-filter-file=', ';')
Kenneth Russell650995a2018-05-03 21:17:01532 return arr
533
Brian Sheedy910cda82022-07-19 11:58:34534 def substitute_magic_args(self, test_config, tester_name, tester_config):
Brian Sheedya31578e2020-05-18 20:24:36535 """Substitutes any magic substitution args present in |test_config|.
536
537 Substitutions are done in-place.
538
539 See buildbot_json_magic_substitutions.py for more information on this
540 feature.
541
542 Args:
543 test_config: A dict containing a configuration for a specific test on
544 a specific builder, e.g. the output of update_and_cleanup_test.
Brian Sheedy5f173bb2021-11-24 00:45:54545 tester_name: A string containing the name of the tester that |test_config|
546 came from.
Brian Sheedy910cda82022-07-19 11:58:34547 tester_config: A dict containing the configuration for the builder that
548 |test_config| is for.
Brian Sheedya31578e2020-05-18 20:24:36549 """
550 substituted_array = []
551 for arg in test_config.get('args', []):
552 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
553 function = arg.replace(
554 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
555 if hasattr(magic_substitutions, function):
556 substituted_array.extend(
Brian Sheedy910cda82022-07-19 11:58:34557 getattr(magic_substitutions, function)(test_config, tester_name,
558 tester_config))
Brian Sheedya31578e2020-05-18 20:24:36559 else:
560 raise BBGenErr(
561 'Magic substitution function %s does not exist' % function)
562 else:
563 substituted_array.append(arg)
564 if substituted_array:
565 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
566
Kenneth Russelleb60cbd22017-12-05 07:54:28567 def dictionary_merge(self, a, b, path=None, update=True):
568 """http://stackoverflow.com/questions/7204805/
569 python-dictionaries-of-dictionaries-merge
570 merges b into a
571 """
572 if path is None:
573 path = []
574 for key in b:
575 if key in a:
576 if isinstance(a[key], dict) and isinstance(b[key], dict):
577 self.dictionary_merge(a[key], b[key], path + [str(key)])
578 elif a[key] == b[key]:
579 pass # same leaf value
580 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06581 # Args arrays are lists of strings. Just concatenate them,
582 # and don't sort them, in order to keep some needed
Weizhong Xia91b53362022-01-05 17:13:35583 # arguments adjacent (like --timeout-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28584 if all(isinstance(x, str)
585 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01586 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28587 else:
588 # TODO(kbr): this only works properly if the two arrays are
589 # the same length, which is currently always the case in the
590 # swarming dimension_sets that we have to merge. It will fail
591 # to merge / override 'args' arrays which are different
592 # length.
Jamie Madillcf4f8c72021-05-20 19:24:23593 for idx in range(len(b[key])):
Kenneth Russell8ceeabf2017-12-11 17:53:28594 try:
595 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
596 path + [str(key), str(idx)],
597 update=update)
Joshua Hood56c673c2022-03-02 20:29:33598 except (IndexError, TypeError) as e:
599 six.raise_from(
600 BBGenErr('Error merging lists by key "%s" from source %s '
601 'into target %s at index %s. Verify target list '
602 'length is equal or greater than source' %
603 (str(key), str(b), str(a), str(idx))), e)
John Budorick5bc387fe2019-05-09 20:02:53604 elif update:
605 if b[key] is None:
606 del a[key]
607 else:
608 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28609 else:
610 raise BBGenErr('Conflict at %s' % '.'.join(
611 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53612 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28613 a[key] = b[key]
614 return a
615
John Budorickab108712018-09-01 00:12:21616 def initialize_args_for_test(
617 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21618 args = []
619 args.extend(generated_test.get('args', []))
620 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22621
Kenneth Russell8a386d42018-06-02 09:48:01622 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21623 val = generated_test.pop(key, [])
624 if fn(tester_config):
625 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01626
627 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
Brian Sheedy781c8ca42021-03-08 22:03:21628 add_conditional_args('lacros_args', self.is_lacros)
Kenneth Russell8a386d42018-06-02 09:48:01629 add_conditional_args('linux_args', self.is_linux)
630 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36631 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49632 add_conditional_args('mac_args', self.is_mac)
633 add_conditional_args('win_args', self.is_win)
634 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01635
John Budorickab108712018-09-01 00:12:21636 for key in additional_arg_keys or []:
637 args.extend(generated_test.pop(key, []))
638 args.extend(tester_config.get(key, []))
639
640 if args:
641 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01642
Kenneth Russelleb60cbd22017-12-05 07:54:28643 def initialize_swarming_dictionary_for_test(self, generated_test,
644 tester_config):
645 if 'swarming' not in generated_test:
646 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28647 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
648 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38649 'can_use_on_swarming_builders': tester_config.get('use_swarming',
650 True)
Dirk Pranke81ff51c2017-12-09 19:24:28651 })
Kenneth Russelleb60cbd22017-12-05 07:54:28652 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03653 if ('dimension_sets' not in generated_test['swarming'] and
654 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28655 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
656 tester_config['swarming']['dimension_sets'])
657 self.dictionary_merge(generated_test['swarming'],
658 tester_config['swarming'])
Brian Sheedybc984e242021-04-21 23:44:51659 # Apply any platform-specific Swarming dimensions after the generic ones.
Kenneth Russelleb60cbd22017-12-05 07:54:28660 if 'android_swarming' in generated_test:
661 if self.is_android(tester_config): # pragma: no cover
662 self.dictionary_merge(
663 generated_test['swarming'],
664 generated_test['android_swarming']) # pragma: no cover
665 del generated_test['android_swarming'] # pragma: no cover
Brian Sheedybc984e242021-04-21 23:44:51666 if 'chromeos_swarming' in generated_test:
667 if self.is_chromeos(tester_config): # pragma: no cover
668 self.dictionary_merge(
669 generated_test['swarming'],
670 generated_test['chromeos_swarming']) # pragma: no cover
671 del generated_test['chromeos_swarming'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28672
673 def clean_swarming_dictionary(self, swarming_dict):
674 # Clean out redundant entries from a test's "swarming" dictionary.
675 # This is really only needed to retain 100% parity with the
676 # handwritten JSON files, and can be removed once all the files are
677 # autogenerated.
678 if 'shards' in swarming_dict:
679 if swarming_dict['shards'] == 1: # pragma: no cover
680 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24681 if 'hard_timeout' in swarming_dict:
682 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
683 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43684 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28685 # Remove all other keys.
Jamie Madillcf4f8c72021-05-20 19:24:23686 for k in list(swarming_dict): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28687 if k != 'can_use_on_swarming_builders': # pragma: no cover
688 del swarming_dict[k] # pragma: no cover
689
Stephen Martinis0382bc12018-09-17 22:29:07690 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
691 waterfall):
692 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01693 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07694 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28695 # See if there are any exceptions that need to be merged into this
696 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49697 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28698 if modifications:
699 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23700 if 'swarming' in test:
701 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28702 # Ensure all Android Swarming tests run only on userdebug builds if another
703 # build type was not specified.
704 if 'swarming' in test and self.is_android(tester_config):
705 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22706 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28707 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37708 self.replace_test_args(test, test_name, tester_name)
Ben Pastenee012aea42019-05-14 22:32:28709
Kenneth Russelleb60cbd22017-12-05 07:54:28710 return test
711
Brian Sheedye6ea0ee2019-07-11 02:54:37712 def replace_test_args(self, test, test_name, tester_name):
713 replacements = self.get_test_replacements(
714 test, test_name, tester_name) or {}
715 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23716 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37717 if key not in valid_replacement_keys:
718 raise BBGenErr(
719 'Given replacement key %s for %s on %s is not in the list of valid '
720 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23721 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37722 found_key = False
723 for i, test_key in enumerate(test.get(key, [])):
724 # Handle both the key/value being replaced being defined as two
725 # separate items or as key=value.
726 if test_key == replacement_key:
727 found_key = True
728 # Handle flags without values.
729 if replacement_val == None:
730 del test[key][i]
731 else:
732 test[key][i+1] = replacement_val
733 break
Joshua Hood56c673c2022-03-02 20:29:33734 if test_key.startswith(replacement_key + '='):
Brian Sheedye6ea0ee2019-07-11 02:54:37735 found_key = True
736 if replacement_val == None:
737 del test[key][i]
738 else:
739 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
740 break
741 if not found_key:
742 raise BBGenErr('Could not find %s in existing list of values for key '
743 '%s in %s on %s' % (replacement_key, key, test_name,
744 tester_name))
745
Shenghua Zhangaba8bad2018-02-07 02:12:09746 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05747 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26748 True):
749 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06750 # are targeting CrOS hardware and so need the special trigger script.
751 dimension_sets = test['swarming']['dimension_sets']
Ben Pastenea9e583b2019-01-16 02:57:26752 if all('device_type' in ds for ds in dimension_sets):
753 test['trigger_script'] = {
754 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
755 }
Shenghua Zhangaba8bad2018-02-07 02:12:09756
Yuly Novikov26dd47052021-02-11 00:57:14757 def add_logdog_butler_cipd_package(self, tester_config, result):
758 if not tester_config.get('skip_cipd_packages', False):
759 cipd_packages = result['swarming'].get('cipd_packages', [])
760 already_added = len([
761 package for package in cipd_packages
762 if package.get('cipd_package', "").find('logdog/butler') > 0
763 ]) > 0
764 if not already_added:
765 cipd_packages.append({
766 'cipd_package':
767 'infra/tools/luci/logdog/butler/${platform}',
768 'location':
769 'bin',
770 'revision':
771 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
772 })
773 result['swarming']['cipd_packages'] = cipd_packages
774
Ben Pastene858f4be2019-01-09 23:52:09775 def add_android_presentation_args(self, tester_config, test_name, result):
776 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38777 bucket = tester_config.get('results_bucket', 'chromium-result-details')
778 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09779 if (result['swarming']['can_use_on_swarming_builders'] and not
780 tester_config.get('skip_merge_script', False)):
781 result['merge'] = {
782 'args': [
783 '--bucket',
John Budorick262ae112019-07-12 19:24:38784 bucket,
Ben Pastene858f4be2019-01-09 23:52:09785 '--test-name',
Rakib M. Hasanc9e01c62020-07-27 22:48:12786 result.get('name', test_name)
Ben Pastene858f4be2019-01-09 23:52:09787 ],
788 'script': '//build/android/pylib/results/presentation/'
789 'test_results_presentation.py',
790 }
Ben Pastene858f4be2019-01-09 23:52:09791 if not tester_config.get('skip_output_links', False):
792 result['swarming']['output_links'] = [
793 {
794 'link': [
795 'https://luci-logdog.appspot.com/v/?s',
796 '=android%2Fswarming%2Flogcats%2F',
797 '${TASK_ID}%2F%2B%2Funified_logcats',
798 ],
799 'name': 'shard #${SHARD_INDEX} logcats',
800 },
801 ]
802 if args:
803 result['args'] = args
804
Kenneth Russelleb60cbd22017-12-05 07:54:28805 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
806 test_config):
807 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15808 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28809 return None
810 result = copy.deepcopy(test_config)
811 if 'test' in result:
Rakib M. Hasanc9e01c62020-07-27 22:48:12812 if 'name' not in result:
813 result['name'] = test_name
Kenneth Russelleb60cbd22017-12-05 07:54:28814 else:
815 result['test'] = test_name
816 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21817
818 self.initialize_args_for_test(
819 result, tester_config, additional_arg_keys=['gtest_args'])
Jamie Madilla8be0d72020-10-02 05:24:04820 if self.is_android(tester_config) and tester_config.get(
Yuly Novikov26dd47052021-02-11 00:57:14821 'use_swarming', True):
822 if not test_config.get('use_isolated_scripts_api', False):
823 # TODO(https://crbug.com/1137998) make Android presentation work with
824 # isolated scripts in test_results_presentation.py merge script
825 self.add_android_presentation_args(tester_config, test_name, result)
826 result['args'] = result.get('args', []) + ['--recover-devices']
827 self.add_logdog_butler_cipd_package(tester_config, result)
Benjamin Pastene766d48f52017-12-18 21:47:42828
Stephen Martinis0382bc12018-09-17 22:29:07829 result = self.update_and_cleanup_test(
830 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09831 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34832 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43833
834 if not result.get('merge'):
835 # TODO(https://crbug.com/958376): Consider adding the ability to not have
836 # this default.
Jamie Madilla8be0d72020-10-02 05:24:04837 if test_config.get('use_isolated_scripts_api', False):
838 merge_script = 'standard_isolated_script_merge'
839 else:
840 merge_script = 'standard_gtest_merge'
841
Stephen Martinisbc7b7772019-05-01 22:01:43842 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04843 'script': '//testing/merge_scripts/%s.py' % merge_script,
844 'args': [],
Stephen Martinisbc7b7772019-05-01 22:01:43845 }
Kenneth Russelleb60cbd22017-12-05 07:54:28846 return result
847
848 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
849 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01850 if not self.should_run_on_tester(waterfall, tester_name, test_name,
851 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28852 return None
853 result = copy.deepcopy(test_config)
854 result['isolate_name'] = result.get('isolate_name', test_name)
Jeff Yoonb8bfdbf32020-03-13 19:14:43855 result['name'] = result.get('name', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28856 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01857 self.initialize_args_for_test(result, tester_config)
Yuly Novikov26dd47052021-02-11 00:57:14858 if self.is_android(tester_config) and tester_config.get(
859 'use_swarming', True):
860 if tester_config.get('use_android_presentation', False):
861 # TODO(https://crbug.com/1137998) make Android presentation work with
862 # isolated scripts in test_results_presentation.py merge script
863 self.add_android_presentation_args(tester_config, test_name, result)
864 self.add_logdog_butler_cipd_package(tester_config, result)
Stephen Martinis0382bc12018-09-17 22:29:07865 result = self.update_and_cleanup_test(
866 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09867 self.add_common_test_properties(result, tester_config)
Brian Sheedy910cda82022-07-19 11:58:34868 self.substitute_magic_args(result, tester_name, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17869
870 if not result.get('merge'):
871 # TODO(https://crbug.com/958376): Consider adding the ability to not have
872 # this default.
873 result['merge'] = {
874 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
875 'args': [],
876 }
Kenneth Russelleb60cbd22017-12-05 07:54:28877 return result
878
879 def generate_script_test(self, waterfall, tester_name, tester_config,
880 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44881 # TODO(https://crbug.com/953072): Remove this check whenever a better
882 # long-term solution is implemented.
883 if (waterfall.get('forbid_script_tests', False) or
884 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
885 raise BBGenErr('Attempted to generate a script test on tester ' +
886 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01887 if not self.should_run_on_tester(waterfall, tester_name, test_name,
888 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28889 return None
890 result = {
891 'name': test_name,
892 'script': test_config['script']
893 }
Stephen Martinis0382bc12018-09-17 22:29:07894 result = self.update_and_cleanup_test(
895 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34896 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28897 return result
898
899 def generate_junit_test(self, waterfall, tester_name, tester_config,
900 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01901 if not self.should_run_on_tester(waterfall, tester_name, test_name,
902 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28903 return None
John Budorickdef6acb2019-09-17 22:51:09904 result = copy.deepcopy(test_config)
905 result.update({
John Budorickcadc4952019-09-16 23:51:37906 'name': test_name,
907 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09908 })
909 self.initialize_args_for_test(result, tester_config)
910 result = self.update_and_cleanup_test(
911 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34912 self.substitute_magic_args(result, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28913 return result
914
Xinan Lin05fb9c1752020-12-17 00:15:52915 def generate_skylab_test(self, waterfall, tester_name, tester_config,
916 test_name, test_config):
917 if not self.should_run_on_tester(waterfall, tester_name, test_name,
918 test_config):
919 return None
920 result = copy.deepcopy(test_config)
921 result.update({
922 'test': test_name,
923 })
924 self.initialize_args_for_test(result, tester_config)
925 result = self.update_and_cleanup_test(result, test_name, tester_name,
926 tester_config, waterfall)
Brian Sheedy910cda82022-07-19 11:58:34927 self.substitute_magic_args(result, tester_name, tester_config)
Xinan Lin05fb9c1752020-12-17 00:15:52928 return result
929
Stephen Martinis2a0667022018-09-25 22:31:14930 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01931 substitutions = {
932 # Any machine in waterfalls.pyl which desires to run GPU tests
933 # must provide the os_type key.
934 'os_type': tester_config['os_type'],
935 'gpu_vendor_id': '0',
936 'gpu_device_id': '0',
937 }
Stephen Martinis2a0667022018-09-25 22:31:14938 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01939 if 'gpu' in dimension_set:
940 # First remove the driver version, then split into vendor and device.
941 gpu = dimension_set['gpu']
Yuly Novikove4b2fef2020-09-04 05:53:11942 if gpu != 'none':
943 gpu = gpu.split('-')[0].split(':')
944 substitutions['gpu_vendor_id'] = gpu[0]
945 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01946 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
947
948 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Fabrice de Ganscbd655f2022-08-04 20:15:30949 test_name, test_config, is_android_webview,
950 is_cast_streaming):
Kenneth Russell8a386d42018-06-02 09:48:01951 # These are all just specializations of isolated script tests with
952 # a bunch of boilerplate command line arguments added.
953
954 # The step name must end in 'test' or 'tests' in order for the
955 # results to automatically show up on the flakiness dashboard.
956 # (At least, this was true some time ago.) Continue to use this
957 # naming convention for the time being to minimize changes.
958 step_name = test_config.get('name', test_name)
Ben Pastene5f231cf22022-05-05 18:03:07959 variant_id = test_config.get('variant_id')
960 if variant_id:
961 step_name = self.remove_variant_from_test_name(step_name, variant_id)
Kenneth Russell8a386d42018-06-02 09:48:01962 if not (step_name.endswith('test') or step_name.endswith('tests')):
963 step_name = '%s_tests' % step_name
Ben Pastene5f231cf22022-05-05 18:03:07964 if variant_id:
965 step_name = self.add_variant_to_test_name(step_name, variant_id)
Ben Pastene5ff45d82022-05-05 21:54:00966 if 'name' in test_config:
967 test_config['name'] = step_name
Kenneth Russell8a386d42018-06-02 09:48:01968 result = self.generate_isolated_script_test(
969 waterfall, tester_name, tester_config, step_name, test_config)
970 if not result:
971 return None
Chong Gub75754b32020-03-13 16:39:20972 result['isolate_name'] = test_config.get(
Brian Sheedyf74819b2021-06-04 01:38:38973 'isolate_name',
974 self.get_default_isolate_name(tester_config, is_android_webview))
Chan Liab7d8dd82020-04-24 23:42:19975
Chan Lia3ad1502020-04-28 05:32:11976 # Populate test_id_prefix.
Brian Sheedyf74819b2021-06-04 01:38:38977 gn_entry = self.gn_isolate_map[result['isolate_name']]
Chan Li17d969f92020-07-10 00:50:03978 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19979
Kenneth Russell8a386d42018-06-02 09:48:01980 args = result.get('args', [])
981 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14982
Brian Sheedyd8c0c73d2021-07-05 02:11:30983 # TODO(skbug.com/12149): Remove this once Gold-based tests no longer clobber
984 # earlier results on retry attempts.
985 is_gold_based_test = False
986 for a in args:
987 if '--git-revision' in a:
988 is_gold_based_test = True
989 break
990 if is_gold_based_test:
991 for a in args:
992 if '--test-filter' in a or '--isolated-script-test-filter' in a:
993 raise RuntimeError(
994 '--test-filter/--isolated-script-test-filter are currently not '
995 'supported for Gold-based GPU tests. See skbug.com/12100 and '
996 'skbug.com/12149 for more details.')
997
erikchen6da2d9b2018-08-03 23:01:14998 # These tests upload and download results from cloud storage and therefore
999 # aren't idempotent yet. https://crbug.com/549140.
1000 result['swarming']['idempotent'] = False
1001
Kenneth Russell44910c32018-12-03 23:35:111002 # The GPU tests act much like integration tests for the entire browser, and
1003 # tend to uncover flakiness bugs more readily than other test suites. In
1004 # order to surface any flakiness more readily to the developer of the CL
1005 # which is introducing it, we disable retries with patch on the commit
1006 # queue.
1007 result['should_retry_with_patch'] = False
1008
Fabrice de Ganscbd655f2022-08-04 20:15:301009 browser = ''
1010 if is_cast_streaming:
1011 browser = 'cast-streaming-shell'
1012 elif is_android_webview:
1013 browser = 'android-webview-instrumentation'
1014 else:
1015 browser = tester_config['browser_config']
Brian Sheedy4053a702020-07-28 02:09:521016
1017 # Most platforms require --enable-logging=stderr to get useful browser logs.
1018 # However, this actively messes with logging on CrOS (because Chrome's
1019 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
1020 # in order to see JavaScript console messages. See
1021 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
1022 logging_arg = '--log-level=0' if self.is_chromeos(
1023 tester_config) else '--enable-logging=stderr'
1024
Kenneth Russell8a386d42018-06-02 09:48:011025 args = [
Bo Liu555a0f92019-03-29 12:11:561026 test_to_run,
1027 '--show-stdout',
1028 '--browser=%s' % browser,
1029 # --passthrough displays more of the logging in Telemetry when
1030 # run via typ, in particular some of the warnings about tests
1031 # being expected to fail, but passing.
1032 '--passthrough',
1033 '-v',
Brian Sheedy4053a702020-07-28 02:09:521034 '--extra-browser-args=%s --js-flags=--expose-gc' % logging_arg,
Kenneth Russell8a386d42018-06-02 09:48:011035 ] + args
1036 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:141037 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:011038 return result
1039
Brian Sheedyf74819b2021-06-04 01:38:381040 def get_default_isolate_name(self, tester_config, is_android_webview):
1041 if self.is_android(tester_config):
1042 if is_android_webview:
1043 return 'telemetry_gpu_integration_test_android_webview'
1044 return (
1045 'telemetry_gpu_integration_test' +
1046 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
Joshua Hood56c673c2022-03-02 20:29:331047 if self.is_fuchsia(tester_config):
Chong Guc2ca5d02022-01-11 19:52:171048 return 'telemetry_gpu_integration_test_fuchsia'
Joshua Hood56c673c2022-03-02 20:29:331049 return 'telemetry_gpu_integration_test'
Brian Sheedyf74819b2021-06-04 01:38:381050
Kenneth Russelleb60cbd22017-12-05 07:54:281051 def get_test_generator_map(self):
1052 return {
Bo Liu555a0f92019-03-29 12:11:561053 'android_webview_gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301054 GPUTelemetryTestGenerator(self, is_android_webview=True),
1055 'cast_streaming_tests':
1056 GPUTelemetryTestGenerator(self, is_cast_streaming=True),
Bo Liu555a0f92019-03-29 12:11:561057 'gpu_telemetry_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301058 GPUTelemetryTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561059 'gtest_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301060 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561061 'isolated_scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301062 IsolatedScriptTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561063 'junit_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301064 JUnitGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561065 'scripts':
Fabrice de Ganscbd655f2022-08-04 20:15:301066 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:521067 'skylab_tests':
Fabrice de Ganscbd655f2022-08-04 20:15:301068 SkylabGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281069 }
1070
Kenneth Russell8a386d42018-06-02 09:48:011071 def get_test_type_remapper(self):
1072 return {
Fabrice de Gans223272482022-08-08 16:56:571073 # These are a specialization of isolated_scripts with a bunch of
1074 # boilerplate command line arguments added to each one.
1075 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
1076 'cast_streaming_tests': 'isolated_scripts',
1077 'gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:011078 }
1079
Jeff Yoon67c3e832020-02-08 07:39:381080 def check_composition_type_test_suites(self, test_type,
1081 additional_validators=None):
1082 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1083 validators = [check_compound_references,
1084 check_basic_references,
1085 check_conflicting_definitions]
1086 if additional_validators:
1087 validators += additional_validators
1088
1089 target_suites = self.test_suites.get(test_type, {})
1090 other_test_type = ('compound_suites'
1091 if test_type == 'matrix_compound_suites'
1092 else 'matrix_compound_suites')
1093 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011094 basic_suites = self.test_suites.get('basic_suites', {})
1095
Jamie Madillcf4f8c72021-05-20 19:24:231096 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011097 if suite in basic_suites:
1098 raise BBGenErr('%s names may not duplicate basic test suite names '
1099 '(error found while processsing %s)'
1100 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011101
Jeff Yoon67c3e832020-02-08 07:39:381102 seen_tests = {}
1103 for sub_suite in suite_def:
1104 for validator in validators:
1105 validator(
1106 basic_suites=basic_suites,
1107 other_test_suites=other_suites,
1108 seen_tests=seen_tests,
1109 sub_suite=sub_suite,
1110 suite=suite,
1111 suite_def=suite_def,
1112 target_test_suites=target_suites,
1113 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051114 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381115 )
Kenneth Russelleb60cbd22017-12-05 07:54:281116
Stephen Martinis54d64ad2018-09-21 22:16:201117 def flatten_test_suites(self):
1118 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011119 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1120 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231121 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011122 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201123 self.test_suites = new_test_suites
1124
Chan Lia3ad1502020-04-28 05:32:111125 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231126 for suite in self.test_suites['basic_suites'].values():
1127 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561128 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411129
1130 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
John Palmera8515fca2021-05-20 03:35:321131 # https://source.chromium.org/chromium/chromium/tools/build/+/main:scripts/slave/recipe_modules/chromium_tests/generators.py;l=89;drc=14c062ba0eb418d3c4623dde41a753241b9df06b
Nodir Turakulovfce34292019-12-18 17:05:411132 # TODO(crbug.com/1035124): clean this up.
1133 isolate_name = test.get('test') or test.get('isolate_name') or key
1134 gn_entry = self.gn_isolate_map.get(isolate_name)
1135 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281136 label = gn_entry['label']
1137
1138 if label.count(':') != 1:
1139 raise BBGenErr(
1140 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1141 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1142 (label, isolate_name))
1143 if label.split(':')[1] != isolate_name:
1144 raise BBGenErr(
1145 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1146 ' label "%s" see http://crbug.com/1071091 for details.' %
1147 (isolate_name, label))
1148
Chan Lia3ad1502020-04-28 05:32:111149 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411150 else: # pragma: no cover
1151 # Some tests do not have an entry gn_isolate_map.pyl, such as
1152 # telemetry tests.
1153 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
1154 pass
1155
Kenneth Russelleb60cbd22017-12-05 07:54:281156 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011157 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201158
Jeff Yoon8154e582019-12-03 23:30:011159 compound_suites = self.test_suites.get('compound_suites', {})
1160 # check_composition_type_test_suites() checks that all basic suites
1161 # referenced by compound suites exist.
1162 basic_suites = self.test_suites.get('basic_suites')
1163
Jamie Madillcf4f8c72021-05-20 19:24:231164 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011165 # Resolve this to a dictionary.
1166 full_suite = {}
1167 for entry in value:
1168 suite = basic_suites[entry]
1169 full_suite.update(suite)
1170 compound_suites[name] = full_suite
1171
Jeff Yoon85fb8df2020-08-20 16:47:431172 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381173 """ Merge variant-defined configurations to each test case definition in a
1174 test suite.
1175
1176 The output maps a unique test name to an array of configurations because
1177 there may exist more than one definition for a test name using variants. The
1178 test name is referenced while mapping machines to test suites, so unpacking
1179 the array is done by the generators.
1180
1181 Args:
1182 basic_test_definition: a {} defined test suite in the format
1183 test_name:test_config
1184 variants: an [] of {} defining configurations to be applied to each test
1185 case in the basic test_definition
1186
1187 Return:
1188 a {} of test_name:[{}], where each {} is a merged configuration
1189 """
1190
1191 # Each test in a basic test suite will have a definition per variant.
1192 test_suite = {}
Jamie Madillcf4f8c72021-05-20 19:24:231193 for test_name, test_config in basic_test_definition.items():
Jeff Yoon67c3e832020-02-08 07:39:381194 definitions = []
1195 for variant in variants:
Jeff Yoonda581c32020-03-06 03:56:051196 # Unpack the variant from variants.pyl if it's string based.
1197 if isinstance(variant, str):
1198 variant = self.variants[variant]
1199
Jieting Yangef6b1042021-11-30 21:33:481200 # If 'enabled' is set to False, we will not use this variant;
1201 # otherwise if the variant doesn't include 'enabled' variable or
1202 # 'enabled' is set to True, we will use this variant
1203 if not variant.get('enabled', True):
1204 continue
Jeff Yoon67c3e832020-02-08 07:39:381205 # Clone a copy of test_config so that we can have a uniquely updated
1206 # version of it per variant
1207 cloned_config = copy.deepcopy(test_config)
1208 # The variant definition needs to be re-used for each test, so we'll
1209 # create a clone and work with it as well.
1210 cloned_variant = copy.deepcopy(variant)
1211
1212 cloned_config['args'] = (cloned_config.get('args', []) +
1213 cloned_variant.get('args', []))
1214 cloned_config['mixins'] = (cloned_config.get('mixins', []) +
Jeff Yoon85fb8df2020-08-20 16:47:431215 cloned_variant.get('mixins', []) + mixins)
Jeff Yoon67c3e832020-02-08 07:39:381216
Sven Zhengb51bd0482022-08-26 18:26:251217 description = []
1218 if piece := cloned_config.get('description'):
1219 description.append(piece)
1220 if piece := cloned_variant.get('description'):
1221 description.append(piece)
1222 if description:
1223 cloned_config['description'] = '\n'.join(description)
Jeff Yoon67c3e832020-02-08 07:39:381224 basic_swarming_def = cloned_config.get('swarming', {})
1225 variant_swarming_def = cloned_variant.get('swarming', {})
1226 if basic_swarming_def and variant_swarming_def:
1227 if ('dimension_sets' in basic_swarming_def and
1228 'dimension_sets' in variant_swarming_def):
1229 # Retain swarming dimension set merge behavior when both variant and
1230 # the basic test configuration both define it
1231 self.dictionary_merge(basic_swarming_def, variant_swarming_def)
1232 # Remove dimension_sets from the variant definition, so that it does
1233 # not replace what's been done by dictionary_merge in the update
1234 # call below.
1235 del variant_swarming_def['dimension_sets']
1236
1237 # Update the swarming definition with whatever is defined for swarming
1238 # by the variant.
1239 basic_swarming_def.update(variant_swarming_def)
1240 cloned_config['swarming'] = basic_swarming_def
1241
Xinan Lin05fb9c1752020-12-17 00:15:521242 # Copy all skylab fields defined by the variant.
1243 skylab_config = cloned_variant.get('skylab')
1244 if skylab_config:
1245 for k, v in skylab_config.items():
Jieting Yangef6b1042021-11-30 21:33:481246 # cros_chrome_version is the ash chrome version in the cros img
1247 # in the variant of cros_board. We don't want to include it in
1248 # the final json files; so remove it.
1249 if k == 'cros_chrome_version':
1250 continue
Xinan Lin05fb9c1752020-12-17 00:15:521251 cloned_config[k] = v
1252
Jeff Yoon67c3e832020-02-08 07:39:381253 # The identifier is used to make the name of the test unique.
1254 # Generators in the recipe uniquely identify a test by it's name, so we
1255 # don't want to have the same name for each variant.
Ben Pastene5f231cf22022-05-05 18:03:071256 cloned_config['name'] = self.add_variant_to_test_name(
1257 cloned_config.get('name') or test_name,
1258 cloned_variant['identifier'])
1259
1260 # Attach the variant identifier to the test config so downstream
1261 # generators can make modifications based on the original name. This
1262 # is mainly used in generate_gpu_telemetry_test().
1263 cloned_config['variant_id'] = cloned_variant['identifier']
1264
Jeff Yoon67c3e832020-02-08 07:39:381265 definitions.append(cloned_config)
1266 test_suite[test_name] = definitions
1267 return test_suite
1268
Jeff Yoon8154e582019-12-03 23:30:011269 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381270 self.check_composition_type_test_suites('matrix_compound_suites',
1271 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011272
1273 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381274 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011275 # referenced by matrix suites exist.
1276 basic_suites = self.test_suites.get('basic_suites')
1277
Jamie Madillcf4f8c72021-05-20 19:24:231278 for test_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011279 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381280
Jamie Madillcf4f8c72021-05-20 19:24:231281 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381282 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1283
1284 if 'variants' in mtx_test_suite_config:
Jeff Yoon85fb8df2020-08-20 16:47:431285 mixins = mtx_test_suite_config.get('mixins', [])
Jeff Yoon67c3e832020-02-08 07:39:381286 result = self.resolve_variants(basic_test_def,
Jeff Yoon85fb8df2020-08-20 16:47:431287 mtx_test_suite_config['variants'],
1288 mixins)
Jeff Yoon67c3e832020-02-08 07:39:381289 full_suite.update(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271290 else:
1291 suite = basic_suites[test_suite]
1292 full_suite.update(suite)
Jeff Yoon67c3e832020-02-08 07:39:381293 matrix_compound_suites[test_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281294
1295 def link_waterfalls_to_test_suites(self):
1296 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231297 for tester_name, tester in waterfall['machines'].items():
1298 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281299 if not value in self.test_suites:
1300 # Hard / impossible to cover this in the unit test.
1301 raise self.unknown_test_suite(
1302 value, tester_name, waterfall['name']) # pragma: no cover
1303 tester['test_suites'][suite] = self.test_suites[value]
1304
1305 def load_configuration_files(self):
1306 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
1307 self.test_suites = self.load_pyl_file('test_suites.pyl')
1308 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:011309 self.mixins = self.load_pyl_file('mixins.pyl')
Nodir Turakulovfce34292019-12-18 17:05:411310 self.gn_isolate_map = self.load_pyl_file('gn_isolate_map.pyl')
Chong Guee622242020-10-28 18:17:351311 for isolate_map in self.args.isolate_map_files:
1312 isolate_map = self.load_pyl_file(isolate_map)
1313 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1314 if duplicates:
1315 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1316 ', '.join(duplicates))
1317 self.gn_isolate_map.update(isolate_map)
1318
Jeff Yoonda581c32020-03-06 03:56:051319 self.variants = self.load_pyl_file('variants.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:281320
1321 def resolve_configuration_files(self):
Chan Lia3ad1502020-04-28 05:32:111322 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281323 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011324 self.resolve_matrix_compound_test_suites()
1325 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281326 self.link_waterfalls_to_test_suites()
1327
Nico Weberd18b8962018-05-16 19:39:381328 def unknown_bot(self, bot_name, waterfall_name):
1329 return BBGenErr(
1330 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1331
Kenneth Russelleb60cbd22017-12-05 07:54:281332 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1333 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381334 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281335 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1336
1337 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1338 return BBGenErr(
1339 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1340 ' on waterfall ' + waterfall_name)
1341
Stephen Martinisb72f6d22018-10-04 23:29:011342 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071343 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321344
1345 Checks in the waterfall, builder, and test objects for mixins.
1346 """
1347 def valid_mixin(mixin_name):
1348 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011349 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321350 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381351
Stephen Martinisb6a50492018-09-12 23:59:321352 def must_be_list(mixins, typ, name):
1353 """Asserts that given mixins are a list."""
1354 if not isinstance(mixins, list):
1355 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1356
Brian Sheedy7658c982020-01-08 02:27:581357 test_name = test.get('name')
1358 remove_mixins = set()
1359 if 'remove_mixins' in builder:
1360 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1361 for rm in builder['remove_mixins']:
1362 valid_mixin(rm)
1363 remove_mixins.add(rm)
1364 if 'remove_mixins' in test:
1365 must_be_list(test['remove_mixins'], 'test', test_name)
1366 for rm in test['remove_mixins']:
1367 valid_mixin(rm)
1368 remove_mixins.add(rm)
1369 del test['remove_mixins']
1370
Stephen Martinisb72f6d22018-10-04 23:29:011371 if 'mixins' in waterfall:
1372 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1373 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581374 if mixin in remove_mixins:
1375 continue
Stephen Martinisb6a50492018-09-12 23:59:321376 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531377 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321378
Stephen Martinisb72f6d22018-10-04 23:29:011379 if 'mixins' in builder:
1380 must_be_list(builder['mixins'], 'builder', builder_name)
1381 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581382 if mixin in remove_mixins:
1383 continue
Stephen Martinisb6a50492018-09-12 23:59:321384 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531385 test = self.apply_mixin(self.mixins[mixin], test, builder)
Stephen Martinisb6a50492018-09-12 23:59:321386
Stephen Martinisb72f6d22018-10-04 23:29:011387 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071388 return test
1389
Stephen Martinis2a0667022018-09-25 22:31:141390 if not test_name:
1391 test_name = test.get('test')
1392 if not test_name: # pragma: no cover
1393 # Not the best name, but we should say something.
1394 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:011395 must_be_list(test['mixins'], 'test', test_name)
1396 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581397 # We don't bother checking if the given mixin is in remove_mixins here
1398 # since this is already the lowest level, so if a mixin is added here that
1399 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071400 valid_mixin(mixin)
Austin Eng148d9f0f2022-02-08 19:18:531401 test = self.apply_mixin(self.mixins[mixin], test, builder)
Jeff Yoon67c3e832020-02-08 07:39:381402 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071403 return test
Stephen Martinisb6a50492018-09-12 23:59:321404
Austin Eng148d9f0f2022-02-08 19:18:531405 def apply_mixin(self, mixin, test, builder):
Stephen Martinisb72f6d22018-10-04 23:29:011406 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321407
Stephen Martinis0382bc12018-09-17 22:29:071408 Mixins will not override an existing key. This is to ensure exceptions can
1409 override a setting a mixin applies.
1410
Stephen Martinisb72f6d22018-10-04 23:29:011411 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:321412 'dimension_sets', which is how normal test suites specify their dimensions,
1413 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
1414 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011415
Stephen Martinisb6a50492018-09-12 23:59:321416 """
1417 new_test = copy.deepcopy(test)
1418 mixin = copy.deepcopy(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011419 if 'swarming' in mixin:
1420 swarming_mixin = mixin['swarming']
1421 new_test.setdefault('swarming', {})
Brian Sheedycae63b22020-06-10 22:52:111422 # Copy over any explicit dimension sets first so that they will be updated
1423 # by any subsequent 'dimensions' entries.
1424 if 'dimension_sets' in swarming_mixin:
1425 existing_dimension_sets = new_test['swarming'].setdefault(
1426 'dimension_sets', [])
1427 # Appending to the existing list could potentially result in different
1428 # behavior depending on the order the mixins were applied, but that's
1429 # already the case for other parts of mixins, so trust that the user
1430 # will verify that the generated output is correct before submitting.
1431 for dimension_set in swarming_mixin['dimension_sets']:
1432 if dimension_set not in existing_dimension_sets:
1433 existing_dimension_sets.append(dimension_set)
1434 del swarming_mixin['dimension_sets']
Stephen Martinisb72f6d22018-10-04 23:29:011435 if 'dimensions' in swarming_mixin:
1436 new_test['swarming'].setdefault('dimension_sets', [{}])
1437 for dimension_set in new_test['swarming']['dimension_sets']:
1438 dimension_set.update(swarming_mixin['dimensions'])
1439 del swarming_mixin['dimensions']
Stephen Martinisb72f6d22018-10-04 23:29:011440 # python dict update doesn't do recursion at all. Just hard code the
1441 # nested update we need (mixin['swarming'] shouldn't clobber
1442 # test['swarming'], but should update it).
1443 new_test['swarming'].update(swarming_mixin)
1444 del mixin['swarming']
1445
Wezc0e835b702018-10-30 00:38:411446 if '$mixin_append' in mixin:
1447 # Values specified under $mixin_append should be appended to existing
1448 # lists, rather than replacing them.
1449 mixin_append = mixin['$mixin_append']
Austin Eng148d9f0f2022-02-08 19:18:531450 del mixin['$mixin_append']
Zhaoyang Li473dd0ae2021-05-10 18:28:281451
1452 # Append swarming named cache and delete swarming key, since it's under
1453 # another layer of dict.
1454 if 'named_caches' in mixin_append.get('swarming', {}):
1455 new_test['swarming'].setdefault('named_caches', [])
1456 new_test['swarming']['named_caches'].extend(
1457 mixin_append['swarming']['named_caches'])
1458 if len(mixin_append['swarming']) > 1:
1459 raise BBGenErr('Only named_caches is supported under swarming key in '
1460 '$mixin_append, but there are: %s' %
1461 sorted(mixin_append['swarming'].keys()))
1462 del mixin_append['swarming']
Wezc0e835b702018-10-30 00:38:411463 for key in mixin_append:
1464 new_test.setdefault(key, [])
1465 if not isinstance(mixin_append[key], list):
1466 raise BBGenErr(
1467 'Key "' + key + '" in $mixin_append must be a list.')
1468 if not isinstance(new_test[key], list):
1469 raise BBGenErr(
1470 'Cannot apply $mixin_append to non-list "' + key + '".')
1471 new_test[key].extend(mixin_append[key])
Austin Eng148d9f0f2022-02-08 19:18:531472
1473 args = new_test.get('args', [])
1474 # Array so we can assign to it in a nested scope.
1475 args_need_fixup = [False]
Wezc0e835b702018-10-30 00:38:411476 if 'args' in mixin_append:
Austin Eng148d9f0f2022-02-08 19:18:531477 args_need_fixup[0] = True
1478
1479 def add_conditional_args(key, fn):
1480 val = new_test.pop(key, [])
1481 if val and fn(builder):
1482 args.extend(val)
1483 args_need_fixup[0] = True
1484
1485 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
1486 add_conditional_args('lacros_args', self.is_lacros)
1487 add_conditional_args('linux_args', self.is_linux)
1488 add_conditional_args('android_args', self.is_android)
1489 add_conditional_args('chromeos_args', self.is_chromeos)
1490 add_conditional_args('mac_args', self.is_mac)
1491 add_conditional_args('win_args', self.is_win)
1492 add_conditional_args('win64_args', self.is_win64)
1493
1494 if args_need_fixup[0]:
1495 new_test['args'] = self.maybe_fixup_args_array(args)
Wezc0e835b702018-10-30 00:38:411496
Stephen Martinisb72f6d22018-10-04 23:29:011497 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321498 return new_test
1499
Greg Gutermanf60eb052020-03-12 17:40:011500 def generate_output_tests(self, waterfall):
1501 """Generates the tests for a waterfall.
1502
1503 Args:
1504 waterfall: a dictionary parsed from a master pyl file
1505 Returns:
1506 A dictionary mapping builders to test specs
1507 """
1508 return {
Jamie Madillcf4f8c72021-05-20 19:24:231509 name: self.get_tests_for_config(waterfall, name, config)
1510 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011511 }
1512
1513 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531514 generator_map = self.get_test_generator_map()
1515 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281516
Greg Gutermanf60eb052020-03-12 17:40:011517 tests = {}
1518 # Copy only well-understood entries in the machine's configuration
1519 # verbatim into the generated JSON.
1520 if 'additional_compile_targets' in config:
1521 tests['additional_compile_targets'] = config[
1522 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231523 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011524 if test_type not in generator_map:
1525 raise self.unknown_test_suite_type(
1526 test_type, name, waterfall['name']) # pragma: no cover
1527 test_generator = generator_map[test_type]
1528 # Let multiple kinds of generators generate the same kinds
1529 # of tests. For example, gpu_telemetry_tests are a
1530 # specialization of isolated_scripts.
1531 new_tests = test_generator.generate(
1532 waterfall, name, config, input_tests)
1533 remapped_test_type = test_type_remapper.get(test_type, test_type)
1534 tests[remapped_test_type] = test_generator.sort(
1535 tests.get(remapped_test_type, []) + new_tests)
1536
1537 return tests
1538
1539 def jsonify(self, all_tests):
1540 return json.dumps(
1541 all_tests, indent=2, separators=(',', ': '),
1542 sort_keys=True) + '\n'
1543
1544 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281545 self.load_configuration_files()
1546 self.resolve_configuration_files()
1547 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011548 result = collections.defaultdict(dict)
1549
Dirk Pranke6269d302020-10-01 00:14:391550 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011551 for waterfall in self.waterfalls:
1552 for field in required_fields:
1553 # Verify required fields
1554 if field not in waterfall:
1555 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1556
1557 # Handle filter flag, if specified
1558 if filters and waterfall['name'] not in filters:
1559 continue
1560
1561 # Join config files and hardcoded values together
1562 all_tests = self.generate_output_tests(waterfall)
1563 result[waterfall['name']] = all_tests
1564
Greg Gutermanf60eb052020-03-12 17:40:011565 # Add do not edit warning
1566 for tests in result.values():
1567 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1568 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1569
1570 return result
1571
1572 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281573 suffix = '.json'
1574 if self.args.new_files:
1575 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011576
1577 for filename, contents in result.items():
1578 jsonstr = self.jsonify(contents)
1579 self.write_file(self.pyl_file_path(filename + suffix), jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281580
Nico Weberd18b8962018-05-16 19:39:381581 def get_valid_bot_names(self):
Garrett Beatyff6e98d2021-09-02 17:00:161582 # Extract bot names from infra/config/generated/luci/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421583 # NOTE: This reference can cause issues; if a file changes there, the
1584 # presubmit here won't be run by default. A manually maintained list there
1585 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1586 # references to configs outside of this directory are added, please change
1587 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1588 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491589
Garrett Beaty7e866fc2021-06-16 14:12:101590 # Get the generated project.pyl so we can check if we should be enforcing
1591 # that the specs are for builders that actually exist
1592 # If not, return None to indicate that we won't enforce that builders in
1593 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491594 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1595 'project.pyl')
1596 if os.path.exists(project_pyl_path):
1597 settings = ast.literal_eval(self.read_file(project_pyl_path))
1598 if not settings.get('validate_source_side_specs_have_builder', True):
1599 return None
1600
Nico Weberd18b8962018-05-16 19:39:381601 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311602 milo_configs = glob.glob(
Garrett Beatyff6e98d2021-09-02 17:00:161603 os.path.join(self.args.infra_config_dir, 'generated', 'luci',
1604 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431605 for c in milo_configs:
1606 for l in self.read_file(c).splitlines():
1607 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311608 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431609 continue
1610 # l looks like
1611 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1612 # Extract win_chromium_dbg_ng part.
1613 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381614 return bot_names
1615
Ben Pastene9a010082019-09-25 20:41:371616 def get_builders_that_do_not_actually_exist(self):
Ben Pastene564fa572022-05-18 18:26:051617 # Some of the bots on the chromium.fyi waterfall in particular
Kenneth Russell8a386d42018-06-02 09:48:011618 # are defined only to be mirrored into trybots, and don't actually
1619 # exist on any of the waterfalls or consoles.
1620 return [
Yuke Liao8373de52020-08-14 18:30:541621 # chromium.fyi
Yuke Liao8373de52020-08-14 18:30:541622 'linux-blink-optional-highdpi-rel-dummy',
Yuke Liao8373de52020-08-14 18:30:541623 'mac10.13-blink-rel-dummy',
1624 'mac10.14-blink-rel-dummy',
1625 'mac10.15-blink-rel-dummy',
Stephanie Kim7fbfd912020-08-21 21:11:001626 'mac11.0-blink-rel-dummy',
Preethi Mohan9c0fa2992021-08-17 17:25:451627 'mac11.0.arm64-blink-rel-dummy',
Kenneth Russell8a386d42018-06-02 09:48:011628 ]
1629
Ben Pastene9a010082019-09-25 20:41:371630 def get_internal_waterfalls(self):
1631 # Similar to get_builders_that_do_not_actually_exist above, but for
1632 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201633 return [
1634 'chrome', 'chrome.pgo', 'internal.chrome.fyi', 'internal.chromeos.fyi',
1635 'internal.soda'
1636 ]
Ben Pastene9a010082019-09-25 20:41:371637
Stephen Martinisf83893722018-09-19 00:02:181638 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201639 self.check_input_files_sorting(verbose)
1640
Kenneth Russelleb60cbd22017-12-05 07:54:281641 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011642 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381643 self.check_composition_type_test_suites('matrix_compound_suites',
1644 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111645 self.resolve_test_id_prefixes()
Stephen Martinis54d64ad2018-09-21 22:16:201646 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381647
1648 # All bots should exist.
1649 bot_names = self.get_valid_bot_names()
Ben Pastene9a010082019-09-25 20:41:371650 builders_that_dont_exist = self.get_builders_that_do_not_actually_exist()
Garrett Beaty2a02de3c2020-05-15 13:57:351651 if bot_names is not None:
1652 internal_waterfalls = self.get_internal_waterfalls()
1653 for waterfall in self.waterfalls:
1654 # TODO(crbug.com/991417): Remove the need for this exception.
1655 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011656 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351657 for bot_name in waterfall['machines']:
1658 if bot_name in builders_that_dont_exist:
Kenneth Russell78fd8702018-05-17 01:15:521659 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351660 if bot_name not in bot_names:
Garrett Beatyb9895922022-04-18 23:34:581661 if waterfall['name'] in [
1662 'client.v8.chromium', 'client.v8.fyi', 'tryserver.v8'
1663 ]:
Garrett Beaty2a02de3c2020-05-15 13:57:351664 # TODO(thakis): Remove this once these bots move to luci.
1665 continue # pragma: no cover
1666 if waterfall['name'] in ['tryserver.webrtc',
1667 'webrtc.chromium.fyi.experimental']:
1668 # These waterfalls have their bot configs in a different repo.
1669 # so we don't know about their bot names.
1670 continue # pragma: no cover
1671 if waterfall['name'] in ['client.devtools-frontend.integration',
1672 'tryserver.devtools-frontend',
1673 'chromium.devtools-frontend']:
1674 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201675 if waterfall['name'] in ['client.openscreen.chromium']:
1676 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351677 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381678
Kenneth Russelleb60cbd22017-12-05 07:54:281679 # All test suites must be referenced.
1680 suites_seen = set()
1681 generator_map = self.get_test_generator_map()
1682 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231683 for bot_name, tester in waterfall['machines'].items():
1684 for suite_type, suite in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281685 if suite_type not in generator_map:
1686 raise self.unknown_test_suite_type(suite_type, bot_name,
1687 waterfall['name'])
1688 if suite not in self.test_suites:
1689 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1690 suites_seen.add(suite)
1691 # Since we didn't resolve the configuration files, this set
1692 # includes both composition test suites and regular ones.
1693 resolved_suites = set()
1694 for suite_name in suites_seen:
1695 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011696 for sub_suite in suite:
1697 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281698 resolved_suites.add(suite_name)
1699 # At this point, every key in test_suites.pyl should be referenced.
1700 missing_suites = set(self.test_suites.keys()) - resolved_suites
1701 if missing_suites:
1702 raise BBGenErr('The following test suites were unreferenced by bots on '
1703 'the waterfalls: ' + str(missing_suites))
1704
1705 # All test suite exceptions must refer to bots on the waterfall.
1706 all_bots = set()
1707 missing_bots = set()
1708 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231709 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281710 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281711 # In order to disambiguate between bots with the same name on
1712 # different waterfalls, support has been added to various
1713 # exceptions for concatenating the waterfall name after the bot
1714 # name.
1715 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231716 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381717 removals = (exception.get('remove_from', []) +
1718 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231719 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381720 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281721 if removal not in all_bots:
1722 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411723
Ben Pastene9a010082019-09-25 20:41:371724 missing_bots = missing_bots - set(builders_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281725 if missing_bots:
1726 raise BBGenErr('The following nonexistent machines were referenced in '
1727 'the test suite exceptions: ' + str(missing_bots))
1728
Stephen Martinis0382bc12018-09-17 22:29:071729 # All mixins must be referenced
1730 seen_mixins = set()
1731 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011732 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231733 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011734 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071735 for suite in self.test_suites.values():
1736 if isinstance(suite, list):
1737 # Don't care about this, it's a composition, which shouldn't include a
1738 # swarming mixin.
1739 continue
1740
1741 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561742 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011743 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071744
Zhaoyang Li9da047d52021-05-10 21:31:441745 for variant in self.variants:
1746 # Unpack the variant from variants.pyl if it's string based.
1747 if isinstance(variant, str):
1748 variant = self.variants[variant]
1749 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1750
Stephen Martinisb72f6d22018-10-04 23:29:011751 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071752 if missing_mixins:
1753 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1754 ' referenced in a waterfall, machine, or test suite.' % (
1755 str(missing_mixins)))
1756
Jeff Yoonda581c32020-03-06 03:56:051757 # All variant references must be referenced
1758 seen_variants = set()
1759 for suite in self.test_suites.values():
1760 if isinstance(suite, list):
1761 continue
1762
1763 for test in suite.values():
1764 if isinstance(test, dict):
1765 for variant in test.get('variants', []):
1766 if isinstance(variant, str):
1767 seen_variants.add(variant)
1768
1769 missing_variants = set(self.variants.keys()) - seen_variants
1770 if missing_variants:
1771 raise BBGenErr('The following variants were unreferenced: %s. They must '
1772 'be referenced in a matrix test suite under the variants '
1773 'key.' % str(missing_variants))
1774
Stephen Martinis54d64ad2018-09-21 22:16:201775
1776 def type_assert(self, node, typ, filename, verbose=False):
1777 """Asserts that the Python AST node |node| is of type |typ|.
1778
1779 If verbose is set, it prints out some helpful context lines, showing where
1780 exactly the error occurred in the file.
1781 """
1782 if not isinstance(node, typ):
1783 if verbose:
1784 lines = [""] + self.read_file(filename).splitlines()
1785
1786 context = 2
1787 lines_start = max(node.lineno - context, 0)
1788 # Add one to include the last line
1789 lines_end = min(node.lineno + context, len(lines)) + 1
1790 lines = (
1791 ['== %s ==\n' % filename] +
1792 ["<snip>\n"] +
1793 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1794 lines[lines_start:lines_start + context])] +
1795 ['-' * 80 + '\n'] +
1796 ['%d %s' % (node.lineno, lines[node.lineno])] +
1797 ['-' * (node.col_offset + 3) + '^' + '-' * (
1798 80 - node.col_offset - 4) + '\n'] +
1799 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1800 lines[node.lineno + 1:lines_end])] +
1801 ["<snip>\n"]
1802 )
1803 # Print out a useful message when a type assertion fails.
1804 for l in lines:
1805 self.print_line(l.strip())
1806
1807 node_dumped = ast.dump(node, annotate_fields=False)
1808 # If the node is huge, truncate it so everything fits in a terminal
1809 # window.
1810 if len(node_dumped) > 60: # pragma: no cover
1811 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1812 raise BBGenErr(
1813 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1814 ' be %s, is %s' % (
1815 filename, node_dumped,
1816 node.lineno, typ, type(node)))
1817
Stephen Martinis5bef0fc2020-01-06 22:47:531818 def check_ast_list_formatted(self, keys, filename, verbose,
Stephen Martinis1384ff92020-01-07 19:52:151819 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531820 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201821
Stephen Martinis5bef0fc2020-01-06 22:47:531822 Currently only checks to ensure they're correctly sorted, and that there
1823 are no duplicates.
1824
1825 Args:
1826 keys: An python list of AST nodes.
1827
1828 It's a list of AST nodes instead of a list of strings because
1829 when verbose is set, it tries to print out context of where the
1830 diffs are in the file.
1831 filename: The name of the file this node is from.
1832 verbose: If set, print out diff information about how the keys are
1833 incorrectly formatted.
1834 check_sorting: If true, checks if the list is sorted.
1835 Returns:
1836 If the keys are correctly formatted.
1837 """
1838 if not keys:
1839 return True
1840
1841 assert isinstance(keys[0], ast.Str)
1842
1843 keys_strs = [k.s for k in keys]
1844 # Keys to diff against. Used below.
1845 keys_to_diff_against = None
1846 # If the list is properly formatted.
1847 list_formatted = True
1848
1849 # Duplicates are always bad.
1850 if len(set(keys_strs)) != len(keys_strs):
1851 list_formatted = False
1852 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1853
1854 if check_sorting and sorted(keys_strs) != keys_strs:
1855 list_formatted = False
1856 if list_formatted:
1857 return True
1858
1859 if verbose:
1860 line_num = keys[0].lineno
1861 keys = [k.s for k in keys]
1862 if check_sorting:
1863 # If we have duplicates, sorting this will take care of it anyways.
1864 keys_to_diff_against = sorted(set(keys))
1865 # else, keys_to_diff_against is set above already
1866
1867 self.print_line('=' * 80)
1868 self.print_line('(First line of keys is %s)' % line_num)
1869 for line in difflib.context_diff(
1870 keys, keys_to_diff_against,
1871 fromfile='current (%r)' % filename, tofile='sorted', lineterm=''):
1872 self.print_line(line)
1873 self.print_line('=' * 80)
1874
1875 return False
1876
Stephen Martinis1384ff92020-01-07 19:52:151877 def check_ast_dict_formatted(self, node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531878 """Checks if an ast dictionary's keys are correctly formatted.
1879
1880 Just a simple wrapper around check_ast_list_formatted.
1881 Args:
1882 node: An AST node. Assumed to be a dictionary.
1883 filename: The name of the file this node is from.
1884 verbose: If set, print out diff information about how the keys are
1885 incorrectly formatted.
1886 check_sorting: If true, checks if the list is sorted.
1887 Returns:
1888 If the dictionary is correctly formatted.
1889 """
Stephen Martinis54d64ad2018-09-21 22:16:201890 keys = []
1891 # The keys of this dict are ordered as ordered in the file; normal python
1892 # dictionary keys are given an arbitrary order, but since we parsed the
1893 # file itself, the order as given in the file is preserved.
1894 for key in node.keys:
1895 self.type_assert(key, ast.Str, filename, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531896 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201897
Stephen Martinis1384ff92020-01-07 19:52:151898 return self.check_ast_list_formatted(keys, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181899
1900 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201901 # TODO(https://crbug.com/886993): Add the ability for this script to
1902 # actually format the files, rather than just complain if they're
1903 # incorrectly formatted.
1904 bad_files = set()
Stephen Martinis5bef0fc2020-01-06 22:47:531905 def parse_file(filename):
1906 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201907
Stephen Martinis5bef0fc2020-01-06 22:47:531908 Returns an AST node representing the value in the pyl file."""
Stephen Martinisf83893722018-09-19 00:02:181909 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1910
Stephen Martinisf83893722018-09-19 00:02:181911 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201912 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181913 module = parsed.body
1914
1915 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201916 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181917 if len(module) != 1: # pragma: no cover
1918 raise BBGenErr('Invalid .pyl file %s' % filename)
1919 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201920 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181921
Stephen Martinis5bef0fc2020-01-06 22:47:531922 return expr.value
1923
1924 # Handle this separately
1925 filename = 'waterfalls.pyl'
1926 value = parse_file(filename)
1927 # Value should be a list.
1928 self.type_assert(value, ast.List, filename, verbose)
1929
1930 keys = []
Joshua Hood56c673c2022-03-02 20:29:331931 for elm in value.elts:
1932 self.type_assert(elm, ast.Dict, filename, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531933 waterfall_name = None
Joshua Hood56c673c2022-03-02 20:29:331934 for key, val in zip(elm.keys, elm.values):
Stephen Martinis5bef0fc2020-01-06 22:47:531935 self.type_assert(key, ast.Str, filename, verbose)
1936 if key.s == 'machines':
1937 if not self.check_ast_dict_formatted(val, filename, verbose):
1938 bad_files.add(filename)
1939
1940 if key.s == "name":
1941 self.type_assert(val, ast.Str, filename, verbose)
1942 waterfall_name = val
1943 assert waterfall_name
1944 keys.append(waterfall_name)
1945
Stephen Martinis1384ff92020-01-07 19:52:151946 if not self.check_ast_list_formatted(keys, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531947 bad_files.add(filename)
1948
1949 for filename in (
1950 'mixins.pyl',
1951 'test_suites.pyl',
1952 'test_suite_exceptions.pyl',
1953 ):
1954 value = parse_file(filename)
Stephen Martinisf83893722018-09-19 00:02:181955 # Value should be a dictionary.
Stephen Martinis54d64ad2018-09-21 22:16:201956 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181957
Stephen Martinis5bef0fc2020-01-06 22:47:531958 if not self.check_ast_dict_formatted(
1959 value, filename, verbose):
1960 bad_files.add(filename)
1961
Stephen Martinis54d64ad2018-09-21 22:16:201962 if filename == 'test_suites.pyl':
Jeff Yoon8154e582019-12-03 23:30:011963 expected_keys = ['basic_suites',
1964 'compound_suites',
1965 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201966 actual_keys = [node.s for node in value.keys]
1967 assert all(key in expected_keys for key in actual_keys), (
1968 'Invalid %r file; expected keys %r, got %r' % (
1969 filename, expected_keys, actual_keys))
Joshua Hood56c673c2022-03-02 20:29:331970 suite_dicts = list(value.values)
Stephen Martinis54d64ad2018-09-21 22:16:201971 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011972 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201973 for suite_group in suite_dicts:
Stephen Martinis5bef0fc2020-01-06 22:47:531974 if not self.check_ast_dict_formatted(
Stephen Martinis54d64ad2018-09-21 22:16:201975 suite_group, filename, verbose):
1976 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181977
Stephen Martinis5bef0fc2020-01-06 22:47:531978 for key, suite in zip(value.keys, value.values):
1979 # The compound suites are checked in
1980 # 'check_composition_type_test_suites()'
1981 if key.s == 'basic_suites':
1982 for group in suite.values:
Stephen Martinis1384ff92020-01-07 19:52:151983 if not self.check_ast_dict_formatted(group, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531984 bad_files.add(filename)
1985 break
Stephen Martinis54d64ad2018-09-21 22:16:201986
Stephen Martinis5bef0fc2020-01-06 22:47:531987 elif filename == 'test_suite_exceptions.pyl':
1988 # Check the values for each test.
1989 for test in value.values:
1990 for kind, node in zip(test.keys, test.values):
1991 if isinstance(node, ast.Dict):
Stephen Martinis1384ff92020-01-07 19:52:151992 if not self.check_ast_dict_formatted(node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531993 bad_files.add(filename)
1994 elif kind.s == 'remove_from':
1995 # Don't care about sorting; these are usually grouped, since the
1996 # same bug can affect multiple builders. Do want to make sure
1997 # there aren't duplicates.
1998 if not self.check_ast_list_formatted(node.elts, filename, verbose,
1999 check_sorting=False):
2000 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:182001
2002 if bad_files:
2003 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:202004 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:532005 'unsorted, or have duplicates. Re-run this with --verbose to see '
2006 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:182007
Kenneth Russelleb60cbd22017-12-05 07:54:282008 def check_output_file_consistency(self, verbose=False):
2009 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012010 # All waterfalls/bucket .json files must have been written
2011 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:282012 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:012013 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:162014 outputs = self.generate_outputs()
2015 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:012016 expected = self.jsonify(expected_contents)
2017 file_path = filename + '.json'
Zhiling Huangbe008172018-03-08 19:13:112018 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:282019 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:012020 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:322021 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:012022 self.print_line('File ' + filename +
2023 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:322024 'contents:')
2025 for line in difflib.unified_diff(
2026 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:502027 current.splitlines(),
2028 fromfile='expected', tofile='current'):
2029 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:012030
2031 if ungenerated_files:
2032 raise BBGenErr(
2033 'The following files have not been properly '
2034 'autogenerated by generate_buildbot_json.py: ' +
2035 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:282036
Dirk Pranke772f55f2021-04-28 04:51:162037 for builder_group, builders in outputs.items():
2038 for builder, step_types in builders.items():
2039 for step_data in step_types.get('gtest_tests', []):
2040 step_name = step_data.get('name', step_data['test'])
2041 self._check_swarming_config(builder_group, builder, step_name,
2042 step_data)
2043 for step_data in step_types.get('isolated_scripts', []):
2044 step_name = step_data.get('name', step_data['isolate_name'])
2045 self._check_swarming_config(builder_group, builder, step_name,
2046 step_data)
2047
2048 def _check_swarming_config(self, filename, builder, step_name, step_data):
2049 # TODO(crbug.com/1203436): Ensure all swarming tests specify os and cpu, not
2050 # just mac tests.
2051 if ('mac' in builder.lower()
2052 and step_data['swarming']['can_use_on_swarming_builders']):
2053 dimension_sets = step_data['swarming'].get('dimension_sets')
2054 if not dimension_sets:
2055 raise BBGenErr('%s: %s / %s : os and cpu must be specified for mac '
2056 'swarmed tests' % (filename, builder, step_name))
2057 for s in dimension_sets:
2058 if not s.get('os') or not s.get('cpu'):
2059 raise BBGenErr('%s: %s / %s : os and cpu must be specified for mac '
2060 'swarmed tests' % (filename, builder, step_name))
2061
Kenneth Russelleb60cbd22017-12-05 07:54:282062 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:502063 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282064 self.check_output_file_consistency(verbose) # pragma: no cover
2065
Karen Qiane24b7ee2019-02-12 23:37:062066 def does_test_match(self, test_info, params_dict):
2067 """Checks to see if the test matches the parameters given.
2068
2069 Compares the provided test_info with the params_dict to see
2070 if the bot matches the parameters given. If so, returns True.
2071 Else, returns false.
2072
2073 Args:
2074 test_info (dict): Information about a specific bot provided
2075 in the format shown in waterfalls.pyl
2076 params_dict (dict): Dictionary of parameters and their values
2077 to look for in the bot
2078 Ex: {
2079 'device_os':'android',
2080 '--flag':True,
2081 'mixins': ['mixin1', 'mixin2'],
2082 'ex_key':'ex_value'
2083 }
2084
2085 """
2086 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2087 'kvm', 'pool', 'integrity'] # dimension parameters
2088 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2089 'can_use_on_swarming_builders']
2090 for param in params_dict:
2091 # if dimension parameter
2092 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2093 if not 'swarming' in test_info:
2094 return False
2095 swarming = test_info['swarming']
2096 if param in SWARMING_PARAMS:
2097 if not param in swarming:
2098 return False
2099 if not str(swarming[param]) == params_dict[param]:
2100 return False
2101 else:
2102 if not 'dimension_sets' in swarming:
2103 return False
2104 d_set = swarming['dimension_sets']
2105 # only looking at the first dimension set
2106 if not param in d_set[0]:
2107 return False
2108 if not d_set[0][param] == params_dict[param]:
2109 return False
2110
2111 # if flag
2112 elif param.startswith('--'):
2113 if not 'args' in test_info:
2114 return False
2115 if not param in test_info['args']:
2116 return False
2117
2118 # not dimension parameter/flag/mixin
2119 else:
2120 if not param in test_info:
2121 return False
2122 if not test_info[param] == params_dict[param]:
2123 return False
2124 return True
2125 def error_msg(self, msg):
2126 """Prints an error message.
2127
2128 In addition to a catered error message, also prints
2129 out where the user can find more help. Then, program exits.
2130 """
2131 self.print_line(msg + (' If you need more information, ' +
2132 'please run with -h or --help to see valid commands.'))
2133 sys.exit(1)
2134
2135 def find_bots_that_run_test(self, test, bots):
2136 matching_bots = []
2137 for bot in bots:
2138 bot_info = bots[bot]
2139 tests = self.flatten_tests_for_bot(bot_info)
2140 for test_info in tests:
2141 test_name = ""
2142 if 'name' in test_info:
2143 test_name = test_info['name']
2144 elif 'test' in test_info:
2145 test_name = test_info['test']
2146 if not test_name == test:
2147 continue
2148 matching_bots.append(bot)
2149 return matching_bots
2150
2151 def find_tests_with_params(self, tests, params_dict):
2152 matching_tests = []
2153 for test_name in tests:
2154 test_info = tests[test_name]
2155 if not self.does_test_match(test_info, params_dict):
2156 continue
2157 if not test_name in matching_tests:
2158 matching_tests.append(test_name)
2159 return matching_tests
2160
2161 def flatten_waterfalls_for_query(self, waterfalls):
2162 bots = {}
2163 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012164 waterfall_tests = self.generate_output_tests(waterfall)
2165 for bot in waterfall_tests:
2166 bot_info = waterfall_tests[bot]
2167 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062168 return bots
2169
2170 def flatten_tests_for_bot(self, bot_info):
2171 """Returns a list of flattened tests.
2172
2173 Returns a list of tests not grouped by test category
2174 for a specific bot.
2175 """
2176 TEST_CATS = self.get_test_generator_map().keys()
2177 tests = []
2178 for test_cat in TEST_CATS:
2179 if not test_cat in bot_info:
2180 continue
2181 test_cat_tests = bot_info[test_cat]
2182 tests = tests + test_cat_tests
2183 return tests
2184
2185 def flatten_tests_for_query(self, test_suites):
2186 """Returns a flattened dictionary of tests.
2187
2188 Returns a dictionary of tests associate with their
2189 configuration, not grouped by their test suite.
2190 """
2191 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232192 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062193 for test in test_suite:
2194 test_info = test_suite[test]
2195 test_name = test
2196 if 'name' in test_info:
2197 test_name = test_info['name']
2198 tests[test_name] = test_info
2199 return tests
2200
2201 def parse_query_filter_params(self, params):
2202 """Parses the filter parameters.
2203
2204 Creates a dictionary from the parameters provided
2205 to filter the bot array.
2206 """
2207 params_dict = {}
2208 for p in params:
2209 # flag
2210 if p.startswith("--"):
2211 params_dict[p] = True
2212 else:
2213 pair = p.split(":")
2214 if len(pair) != 2:
2215 self.error_msg('Invalid command.')
2216 # regular parameters
2217 if pair[1].lower() == "true":
2218 params_dict[pair[0]] = True
2219 elif pair[1].lower() == "false":
2220 params_dict[pair[0]] = False
2221 else:
2222 params_dict[pair[0]] = pair[1]
2223 return params_dict
2224
2225 def get_test_suites_dict(self, bots):
2226 """Returns a dictionary of bots and their tests.
2227
2228 Returns a dictionary of bots and a list of their associated tests.
2229 """
2230 test_suite_dict = dict()
2231 for bot in bots:
2232 bot_info = bots[bot]
2233 tests = self.flatten_tests_for_bot(bot_info)
2234 test_suite_dict[bot] = tests
2235 return test_suite_dict
2236
2237 def output_query_result(self, result, json_file=None):
2238 """Outputs the result of the query.
2239
2240 If a json file parameter name is provided, then
2241 the result is output into the json file. If not,
2242 then the result is printed to the console.
2243 """
2244 output = json.dumps(result, indent=2)
2245 if json_file:
2246 self.write_file(json_file, output)
2247 else:
2248 self.print_line(output)
Karen Qiane24b7ee2019-02-12 23:37:062249
Joshua Hood56c673c2022-03-02 20:29:332250 # pylint: disable=inconsistent-return-statements
Karen Qiane24b7ee2019-02-12 23:37:062251 def query(self, args):
2252 """Queries tests or bots.
2253
2254 Depending on the arguments provided, outputs a json of
2255 tests or bots matching the appropriate optional parameters provided.
2256 """
2257 # split up query statement
2258 query = args.query.split('/')
2259 self.load_configuration_files()
2260 self.resolve_configuration_files()
2261
2262 # flatten bots json
2263 tests = self.test_suites
2264 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2265
2266 cmd_class = query[0]
2267
2268 # For queries starting with 'bots'
2269 if cmd_class == "bots":
2270 if len(query) == 1:
2271 return self.output_query_result(bots, args.json)
2272 # query with specific parameters
Joshua Hood56c673c2022-03-02 20:29:332273 if len(query) == 2:
Karen Qiane24b7ee2019-02-12 23:37:062274 if query[1] == 'tests':
2275 test_suites_dict = self.get_test_suites_dict(bots)
2276 return self.output_query_result(test_suites_dict, args.json)
Joshua Hood56c673c2022-03-02 20:29:332277 self.error_msg("This query should be in the format: bots/tests.")
Karen Qiane24b7ee2019-02-12 23:37:062278
2279 else:
2280 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2281 % str(len(query)-1))
2282
2283 # For queries starting with 'bot'
2284 elif cmd_class == "bot":
2285 if not len(query) == 2 and not len(query) == 3:
2286 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2287 % str(len(query)-1))
2288 bot_id = query[1]
2289 if not bot_id in bots:
2290 self.error_msg("No bot named '" + bot_id + "' found.")
2291 bot_info = bots[bot_id]
2292 if len(query) == 2:
2293 return self.output_query_result(bot_info, args.json)
2294 if not query[2] == 'tests':
2295 self.error_msg("The query should be in the format:" +
2296 "bot/<bot-name>/tests.")
2297
2298 bot_tests = self.flatten_tests_for_bot(bot_info)
2299 return self.output_query_result(bot_tests, args.json)
2300
2301 # For queries starting with 'tests'
2302 elif cmd_class == "tests":
2303 if not len(query) == 1 and not len(query) == 2:
2304 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2305 % str(len(query)-1))
2306 flattened_tests = self.flatten_tests_for_query(tests)
2307 if len(query) == 1:
2308 return self.output_query_result(flattened_tests, args.json)
2309
2310 # create params dict
2311 params = query[1].split('&')
2312 params_dict = self.parse_query_filter_params(params)
2313 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2314 return self.output_query_result(matching_bots)
2315
2316 # For queries starting with 'test'
2317 elif cmd_class == "test":
2318 if not len(query) == 2 and not len(query) == 3:
2319 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2320 % str(len(query)-1))
2321 test_id = query[1]
2322 if len(query) == 2:
2323 flattened_tests = self.flatten_tests_for_query(tests)
2324 for test in flattened_tests:
2325 if test == test_id:
2326 return self.output_query_result(flattened_tests[test], args.json)
2327 self.error_msg("There is no test named %s." % test_id)
2328 if not query[2] == 'bots':
2329 self.error_msg("The query should be in the format: " +
2330 "test/<test-name>/bots")
2331 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2332 return self.output_query_result(bots_for_test)
2333
2334 else:
2335 self.error_msg("Your command did not match any valid commands." +
2336 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Joshua Hood56c673c2022-03-02 20:29:332337 # pylint: enable=inconsistent-return-statements
Kenneth Russelleb60cbd22017-12-05 07:54:282338
Garrett Beaty1afaccc2020-06-25 19:58:152339 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282340 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502341 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062342 elif self.args.query:
2343 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282344 else:
Greg Gutermanf60eb052020-03-12 17:40:012345 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282346 return 0
2347
2348if __name__ == "__main__": # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152349 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2350 sys.exit(generator.main())