blob: c6a4067ff5dda226d795729b948f836be6295b5b [file] [log] [blame]
Jamie Madillcf4f8c72021-05-20 19:24:231#!/usr/bin/env python3
Kenneth Russelleb60cbd22017-12-05 07:54:282# Copyright 2016 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Script to generate the majority of the JSON files in the src/testing/buildbot
7directory. Maintaining these files by hand is too unwieldy.
8"""
9
10import argparse
11import ast
12import collections
13import copy
John Budorick826d5ed2017-12-28 19:27:3214import difflib
Jamie Madillcf4f8c72021-05-20 19:24:2315import functools
Garrett Beatyd5ca75962020-05-07 16:58:3116import glob
Kenneth Russell8ceeabf2017-12-11 17:53:2817import itertools
Kenneth Russelleb60cbd22017-12-05 07:54:2818import json
19import os
Greg Gutermanf60eb052020-03-12 17:40:0120import re
Kenneth Russelleb60cbd22017-12-05 07:54:2821import string
22import sys
John Budorick826d5ed2017-12-28 19:27:3223import traceback
Kenneth Russelleb60cbd22017-12-05 07:54:2824
Brian Sheedya31578e2020-05-18 20:24:3625import buildbot_json_magic_substitutions as magic_substitutions
26
Kenneth Russelleb60cbd22017-12-05 07:54:2827THIS_DIR = os.path.dirname(os.path.abspath(__file__))
28
Brian Sheedyf74819b2021-06-04 01:38:3829BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP = {
30 'android-chromium': '_android_chrome',
31 'android-chromium-monochrome': '_android_monochrome',
32 'android-weblayer': '_android_weblayer',
33 'android-webview': '_android_webview',
34}
35
Kenneth Russelleb60cbd22017-12-05 07:54:2836
37class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4938 def __init__(self, message):
39 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2840
41
Kenneth Russell8ceeabf2017-12-11 17:53:2842# This class is only present to accommodate certain machines on
43# chromium.android.fyi which run certain tests as instrumentation
44# tests, but not as gtests. If this discrepancy were fixed then the
45# notion could be removed.
46class TestSuiteTypes(object):
47 GTEST = 'gtest'
48
49
Kenneth Russelleb60cbd22017-12-05 07:54:2850class BaseGenerator(object):
51 def __init__(self, bb_gen):
52 self.bb_gen = bb_gen
53
Kenneth Russell8ceeabf2017-12-11 17:53:2854 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2855 raise NotImplementedError()
56
57 def sort(self, tests):
58 raise NotImplementedError()
59
60
Jamie Madillcf4f8c72021-05-20 19:24:2361def custom_cmp(a, b):
62 return int(a > b) - int(a < b)
63
64
Kenneth Russell8ceeabf2017-12-11 17:53:2865def cmp_tests(a, b):
66 # Prefer to compare based on the "test" key.
Jamie Madillcf4f8c72021-05-20 19:24:2367 val = custom_cmp(a['test'], b['test'])
Kenneth Russell8ceeabf2017-12-11 17:53:2868 if val != 0:
69 return val
70 if 'name' in a and 'name' in b:
Jamie Madillcf4f8c72021-05-20 19:24:2371 return custom_cmp(a['name'], b['name']) # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:2872 if 'name' not in a and 'name' not in b:
73 return 0 # pragma: no cover
74 # Prefer to put variants of the same test after the first one.
75 if 'name' in a:
76 return 1
77 # 'name' is in b.
78 return -1 # pragma: no cover
79
80
Kenneth Russell8a386d42018-06-02 09:48:0181class GPUTelemetryTestGenerator(BaseGenerator):
Bo Liu555a0f92019-03-29 12:11:5682
83 def __init__(self, bb_gen, is_android_webview=False):
Kenneth Russell8a386d42018-06-02 09:48:0184 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5685 self._is_android_webview = is_android_webview
Kenneth Russell8a386d42018-06-02 09:48:0186
87 def generate(self, waterfall, tester_name, tester_config, input_tests):
88 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:2389 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russell8a386d42018-06-02 09:48:0190 test = self.bb_gen.generate_gpu_telemetry_test(
Bo Liu555a0f92019-03-29 12:11:5691 waterfall, tester_name, tester_config, test_name, test_config,
92 self._is_android_webview)
Kenneth Russell8a386d42018-06-02 09:48:0193 if test:
94 isolated_scripts.append(test)
95 return isolated_scripts
96
97 def sort(self, tests):
98 return sorted(tests, key=lambda x: x['name'])
99
100
Kenneth Russelleb60cbd22017-12-05 07:54:28101class GTestGenerator(BaseGenerator):
102 def __init__(self, bb_gen):
103 super(GTestGenerator, self).__init__(bb_gen)
104
Kenneth Russell8ceeabf2017-12-11 17:53:28105 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28106 # The relative ordering of some of the tests is important to
107 # minimize differences compared to the handwritten JSON files, since
108 # Python's sorts are stable and there are some tests with the same
109 # key (see gles2_conform_d3d9_test and similar variants). Avoid
110 # losing the order by avoiding coalescing the dictionaries into one.
111 gtests = []
Jamie Madillcf4f8c72021-05-20 19:24:23112 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoon67c3e832020-02-08 07:39:38113 # Variants allow more than one definition for a given test, and is defined
114 # in array format from resolve_variants().
115 if not isinstance(test_config, list):
116 test_config = [test_config]
117
118 for config in test_config:
119 test = self.bb_gen.generate_gtest(
120 waterfall, tester_name, tester_config, test_name, config)
121 if test:
122 # generate_gtest may veto the test generation on this tester.
123 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28124 return gtests
125
126 def sort(self, tests):
Jamie Madillcf4f8c72021-05-20 19:24:23127 return sorted(tests, key=functools.cmp_to_key(cmp_tests))
Kenneth Russelleb60cbd22017-12-05 07:54:28128
129
130class IsolatedScriptTestGenerator(BaseGenerator):
131 def __init__(self, bb_gen):
132 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
133
Kenneth Russell8ceeabf2017-12-11 17:53:28134 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28135 isolated_scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23136 for test_name, test_config in sorted(input_tests.items()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43137 # Variants allow more than one definition for a given test, and is defined
138 # in array format from resolve_variants().
139 if not isinstance(test_config, list):
140 test_config = [test_config]
141
142 for config in test_config:
143 test = self.bb_gen.generate_isolated_script_test(
144 waterfall, tester_name, tester_config, test_name, config)
145 if test:
146 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28147 return isolated_scripts
148
149 def sort(self, tests):
150 return sorted(tests, key=lambda x: x['name'])
151
152
153class ScriptGenerator(BaseGenerator):
154 def __init__(self, bb_gen):
155 super(ScriptGenerator, self).__init__(bb_gen)
156
Kenneth Russell8ceeabf2017-12-11 17:53:28157 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28158 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23159 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28160 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28161 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28162 if test:
163 scripts.append(test)
164 return scripts
165
166 def sort(self, tests):
167 return sorted(tests, key=lambda x: x['name'])
168
169
170class JUnitGenerator(BaseGenerator):
171 def __init__(self, bb_gen):
172 super(JUnitGenerator, self).__init__(bb_gen)
173
Kenneth Russell8ceeabf2017-12-11 17:53:28174 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28175 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23176 for test_name, test_config in sorted(input_tests.items()):
Kenneth Russelleb60cbd22017-12-05 07:54:28177 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28178 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28179 if test:
180 scripts.append(test)
181 return scripts
182
183 def sort(self, tests):
184 return sorted(tests, key=lambda x: x['test'])
185
186
Xinan Lin05fb9c1752020-12-17 00:15:52187class SkylabGenerator(BaseGenerator):
188 def __init__(self, bb_gen):
189 super(SkylabGenerator, self).__init__(bb_gen)
190
191 def generate(self, waterfall, tester_name, tester_config, input_tests):
192 scripts = []
Jamie Madillcf4f8c72021-05-20 19:24:23193 for test_name, test_config in sorted(input_tests.items()):
Xinan Lin05fb9c1752020-12-17 00:15:52194 for config in test_config:
195 test = self.bb_gen.generate_skylab_test(waterfall, tester_name,
196 tester_config, test_name,
197 config)
198 if test:
199 scripts.append(test)
200 return scripts
201
202 def sort(self, tests):
203 return sorted(tests, key=lambda x: x['test'])
204
205
Jeff Yoon67c3e832020-02-08 07:39:38206def check_compound_references(other_test_suites=None,
207 sub_suite=None,
208 suite=None,
209 target_test_suites=None,
210 test_type=None,
211 **kwargs):
212 """Ensure comound reference's don't target other compounds"""
213 del kwargs
214 if sub_suite in other_test_suites or sub_suite in target_test_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15215 raise BBGenErr('%s may not refer to other composition type test '
216 'suites (error found while processing %s)' %
217 (test_type, suite))
218
Jeff Yoon67c3e832020-02-08 07:39:38219
220def check_basic_references(basic_suites=None,
221 sub_suite=None,
222 suite=None,
223 **kwargs):
224 """Ensure test has a basic suite reference"""
225 del kwargs
226 if sub_suite not in basic_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15227 raise BBGenErr('Unable to find reference to %s while processing %s' %
228 (sub_suite, suite))
229
Jeff Yoon67c3e832020-02-08 07:39:38230
231def check_conflicting_definitions(basic_suites=None,
232 seen_tests=None,
233 sub_suite=None,
234 suite=None,
235 test_type=None,
236 **kwargs):
237 """Ensure that if a test is reachable via multiple basic suites,
238 all of them have an identical definition of the tests.
239 """
240 del kwargs
241 for test_name in basic_suites[sub_suite]:
242 if (test_name in seen_tests and
243 basic_suites[sub_suite][test_name] !=
244 basic_suites[seen_tests[test_name]][test_name]):
245 raise BBGenErr('Conflicting test definitions for %s from %s '
246 'and %s in %s (error found while processing %s)'
247 % (test_name, seen_tests[test_name], sub_suite,
248 test_type, suite))
249 seen_tests[test_name] = sub_suite
250
251def check_matrix_identifier(sub_suite=None,
252 suite=None,
253 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05254 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38255 **kwargs):
256 """Ensure 'idenfitier' is defined for each variant"""
257 del kwargs
258 sub_suite_config = suite_def[sub_suite]
259 for variant in sub_suite_config.get('variants', []):
Jeff Yoonda581c32020-03-06 03:56:05260 if isinstance(variant, str):
261 if variant not in all_variants:
262 raise BBGenErr('Missing variant definition for %s in variants.pyl'
263 % variant)
264 variant = all_variants[variant]
265
Jeff Yoon67c3e832020-02-08 07:39:38266 if not 'identifier' in variant:
267 raise BBGenErr('Missing required identifier field in matrix '
268 'compound suite %s, %s' % (suite, sub_suite))
269
270
Kenneth Russelleb60cbd22017-12-05 07:54:28271class BBJSONGenerator(object):
Garrett Beaty1afaccc2020-06-25 19:58:15272 def __init__(self, args):
Kenneth Russelleb60cbd22017-12-05 07:54:28273 self.this_dir = THIS_DIR
Garrett Beaty1afaccc2020-06-25 19:58:15274 self.args = args
Kenneth Russelleb60cbd22017-12-05 07:54:28275 self.waterfalls = None
276 self.test_suites = None
277 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01278 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41279 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05280 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28281
Garrett Beaty1afaccc2020-06-25 19:58:15282 @staticmethod
283 def parse_args(argv):
284
285 # RawTextHelpFormatter allows for styling of help statement
286 parser = argparse.ArgumentParser(
287 formatter_class=argparse.RawTextHelpFormatter)
288
289 group = parser.add_mutually_exclusive_group()
290 group.add_argument(
291 '-c',
292 '--check',
293 action='store_true',
294 help=
295 'Do consistency checks of configuration and generated files and then '
296 'exit. Used during presubmit. '
297 'Causes the tool to not generate any files.')
298 group.add_argument(
299 '--query',
300 type=str,
301 help=(
302 "Returns raw JSON information of buildbots and tests.\n" +
303 "Examples:\n" + " List all bots (all info):\n" +
304 " --query bots\n\n" +
305 " List all bots and only their associated tests:\n" +
306 " --query bots/tests\n\n" +
307 " List all information about 'bot1' " +
308 "(make sure you have quotes):\n" + " --query bot/'bot1'\n\n" +
309 " List tests running for 'bot1' (make sure you have quotes):\n" +
310 " --query bot/'bot1'/tests\n\n" + " List all tests:\n" +
311 " --query tests\n\n" +
312 " List all tests and the bots running them:\n" +
313 " --query tests/bots\n\n" +
314 " List all tests that satisfy multiple parameters\n" +
315 " (separation of parameters by '&' symbol):\n" +
316 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
317 " List all tests that run with a specific flag:\n" +
318 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
319 " List specific test (make sure you have quotes):\n"
320 " --query test/'test1'\n\n"
321 " List all bots running 'test1' " +
322 "(make sure you have quotes):\n" + " --query test/'test1'/bots"))
323 parser.add_argument(
324 '-n',
325 '--new-files',
326 action='store_true',
327 help=
328 'Write output files as .new.json. Useful during development so old and '
329 'new files can be looked at side-by-side.')
330 parser.add_argument('-v',
331 '--verbose',
332 action='store_true',
333 help='Increases verbosity. Affects consistency checks.')
334 parser.add_argument('waterfall_filters',
335 metavar='waterfalls',
336 type=str,
337 nargs='*',
338 help='Optional list of waterfalls to generate.')
339 parser.add_argument(
340 '--pyl-files-dir',
341 type=os.path.realpath,
342 help='Path to the directory containing the input .pyl files.')
343 parser.add_argument(
344 '--json',
345 metavar='JSON_FILE_PATH',
346 help='Outputs results into a json file. Only works with query function.'
347 )
Chong Guee622242020-10-28 18:17:35348 parser.add_argument('--isolate-map-file',
349 metavar='PATH',
350 help='path to additional isolate map files.',
351 default=[],
352 action='append',
353 dest='isolate_map_files')
Garrett Beaty1afaccc2020-06-25 19:58:15354 parser.add_argument(
355 '--infra-config-dir',
356 help='Path to the LUCI services configuration directory',
357 default=os.path.abspath(
358 os.path.join(os.path.dirname(__file__), '..', '..', 'infra',
359 'config')))
360 args = parser.parse_args(argv)
361 if args.json and not args.query:
362 parser.error(
363 "The --json flag can only be used with --query.") # pragma: no cover
364 args.infra_config_dir = os.path.abspath(args.infra_config_dir)
365 return args
366
Kenneth Russelleb60cbd22017-12-05 07:54:28367 def generate_abs_file_path(self, relative_path):
Garrett Beaty1afaccc2020-06-25 19:58:15368 return os.path.join(self.this_dir, relative_path)
Kenneth Russelleb60cbd22017-12-05 07:54:28369
Stephen Martinis7eb8b612018-09-21 00:17:50370 def print_line(self, line):
371 # Exists so that tests can mock
Jamie Madillcf4f8c72021-05-20 19:24:23372 print(line) # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:50373
Kenneth Russelleb60cbd22017-12-05 07:54:28374 def read_file(self, relative_path):
Garrett Beaty1afaccc2020-06-25 19:58:15375 with open(self.generate_abs_file_path(relative_path)) as fp:
376 return fp.read()
Kenneth Russelleb60cbd22017-12-05 07:54:28377
378 def write_file(self, relative_path, contents):
Garrett Beaty1afaccc2020-06-25 19:58:15379 with open(self.generate_abs_file_path(relative_path), 'wb') as fp:
Jamie Madillcf4f8c72021-05-20 19:24:23380 fp.write(contents.encode('utf-8'))
Kenneth Russelleb60cbd22017-12-05 07:54:28381
Zhiling Huangbe008172018-03-08 19:13:11382 def pyl_file_path(self, filename):
383 if self.args and self.args.pyl_files_dir:
384 return os.path.join(self.args.pyl_files_dir, filename)
385 return filename
386
Kenneth Russelleb60cbd22017-12-05 07:54:28387 def load_pyl_file(self, filename):
388 try:
Zhiling Huangbe008172018-03-08 19:13:11389 return ast.literal_eval(self.read_file(
390 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28391 except (SyntaxError, ValueError) as e: # pragma: no cover
392 raise BBGenErr('Failed to parse pyl file "%s": %s' %
393 (filename, e)) # pragma: no cover
394
Kenneth Russell8a386d42018-06-02 09:48:01395 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
396 # Currently it is only mandatory for bots which run GPU tests. Change these to
397 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28398 def is_android(self, tester_config):
399 return tester_config.get('os_type') == 'android'
400
Ben Pastenea9e583b2019-01-16 02:57:26401 def is_chromeos(self, tester_config):
402 return tester_config.get('os_type') == 'chromeos'
403
Brian Sheedy781c8ca42021-03-08 22:03:21404 def is_lacros(self, tester_config):
405 return tester_config.get('os_type') == 'lacros'
406
Kenneth Russell8a386d42018-06-02 09:48:01407 def is_linux(self, tester_config):
408 return tester_config.get('os_type') == 'linux'
409
Kai Ninomiya40de9f52019-10-18 21:38:49410 def is_mac(self, tester_config):
411 return tester_config.get('os_type') == 'mac'
412
413 def is_win(self, tester_config):
414 return tester_config.get('os_type') == 'win'
415
416 def is_win64(self, tester_config):
417 return (tester_config.get('os_type') == 'win' and
418 tester_config.get('browser_config') == 'release_x64')
419
Kenneth Russelleb60cbd22017-12-05 07:54:28420 def get_exception_for_test(self, test_name, test_config):
421 # gtests may have both "test" and "name" fields, and usually, if the "name"
422 # field is specified, it means that the same test is being repurposed
423 # multiple times with different command line arguments. To handle this case,
424 # prefer to lookup per the "name" field of the test itself, as opposed to
425 # the "test_name", which is actually the "test" field.
426 if 'name' in test_config:
427 return self.exceptions.get(test_config['name'])
428 else:
429 return self.exceptions.get(test_name)
430
Nico Weberb0b3f5862018-07-13 18:45:15431 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28432 # Currently, the only reason a test should not run on a given tester is that
433 # it's in the exceptions. (Once the GPU waterfall generation script is
434 # incorporated here, the rules will become more complex.)
435 exception = self.get_exception_for_test(test_name, test_config)
436 if not exception:
437 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28438 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28439 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28440 if remove_from:
441 if tester_name in remove_from:
442 return False
443 # TODO(kbr): this code path was added for some tests (including
444 # android_webview_unittests) on one machine (Nougat Phone
445 # Tester) which exists with the same name on two waterfalls,
446 # chromium.android and chromium.fyi; the tests are run on one
447 # but not the other. Once the bots are all uniquely named (a
448 # different ongoing project) this code should be removed.
449 # TODO(kbr): add coverage.
450 return (tester_name + ' ' + waterfall['name']
451 not in remove_from) # pragma: no cover
452 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28453
Nico Weber79dc5f6852018-07-13 19:38:49454 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28455 exception = self.get_exception_for_test(test_name, test)
456 if not exception:
457 return None
Nico Weber79dc5f6852018-07-13 19:38:49458 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28459
Brian Sheedye6ea0ee2019-07-11 02:54:37460 def get_test_replacements(self, test, test_name, tester_name):
461 exception = self.get_exception_for_test(test_name, test)
462 if not exception:
463 return None
464 return exception.get('replacements', {}).get(tester_name)
465
Kenneth Russell8a386d42018-06-02 09:48:01466 def merge_command_line_args(self, arr, prefix, splitter):
467 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01468 idx = 0
469 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01470 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01471 while idx < len(arr):
472 flag = arr[idx]
473 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01474 if flag.startswith(prefix):
475 arg = flag[prefix_len:]
476 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01477 if first_idx < 0:
478 first_idx = idx
479 else:
480 delete_current_entry = True
481 if delete_current_entry:
482 del arr[idx]
483 else:
484 idx += 1
485 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01486 arr[first_idx] = prefix + splitter.join(accumulated_args)
487 return arr
488
489 def maybe_fixup_args_array(self, arr):
490 # The incoming array of strings may be an array of command line
491 # arguments. To make it easier to turn on certain features per-bot or
492 # per-test-suite, look specifically for certain flags and merge them
493 # appropriately.
494 # --enable-features=Feature1 --enable-features=Feature2
495 # are merged to:
496 # --enable-features=Feature1,Feature2
497 # and:
498 # --extra-browser-args=arg1 --extra-browser-args=arg2
499 # are merged to:
500 # --extra-browser-args=arg1 arg2
501 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
502 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Yuly Novikov8c487e72020-10-16 20:00:29503 arr = self.merge_command_line_args(arr, '--test-launcher-filter-file=', ';')
Kenneth Russell650995a2018-05-03 21:17:01504 return arr
505
Brian Sheedya31578e2020-05-18 20:24:36506 def substitute_magic_args(self, test_config):
507 """Substitutes any magic substitution args present in |test_config|.
508
509 Substitutions are done in-place.
510
511 See buildbot_json_magic_substitutions.py for more information on this
512 feature.
513
514 Args:
515 test_config: A dict containing a configuration for a specific test on
516 a specific builder, e.g. the output of update_and_cleanup_test.
517 """
518 substituted_array = []
519 for arg in test_config.get('args', []):
520 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
521 function = arg.replace(
522 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
523 if hasattr(magic_substitutions, function):
524 substituted_array.extend(
525 getattr(magic_substitutions, function)(test_config))
526 else:
527 raise BBGenErr(
528 'Magic substitution function %s does not exist' % function)
529 else:
530 substituted_array.append(arg)
531 if substituted_array:
532 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
533
Kenneth Russelleb60cbd22017-12-05 07:54:28534 def dictionary_merge(self, a, b, path=None, update=True):
535 """http://stackoverflow.com/questions/7204805/
536 python-dictionaries-of-dictionaries-merge
537 merges b into a
538 """
539 if path is None:
540 path = []
541 for key in b:
542 if key in a:
543 if isinstance(a[key], dict) and isinstance(b[key], dict):
544 self.dictionary_merge(a[key], b[key], path + [str(key)])
545 elif a[key] == b[key]:
546 pass # same leaf value
547 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06548 # Args arrays are lists of strings. Just concatenate them,
549 # and don't sort them, in order to keep some needed
550 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28551 if all(isinstance(x, str)
552 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01553 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28554 else:
555 # TODO(kbr): this only works properly if the two arrays are
556 # the same length, which is currently always the case in the
557 # swarming dimension_sets that we have to merge. It will fail
558 # to merge / override 'args' arrays which are different
559 # length.
Jamie Madillcf4f8c72021-05-20 19:24:23560 for idx in range(len(b[key])):
Kenneth Russell8ceeabf2017-12-11 17:53:28561 try:
562 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
563 path + [str(key), str(idx)],
564 update=update)
Jeff Yoon8154e582019-12-03 23:30:01565 except (IndexError, TypeError):
566 raise BBGenErr('Error merging lists by key "%s" from source %s '
567 'into target %s at index %s. Verify target list '
568 'length is equal or greater than source'
569 % (str(key), str(b), str(a), str(idx)))
John Budorick5bc387fe2019-05-09 20:02:53570 elif update:
571 if b[key] is None:
572 del a[key]
573 else:
574 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28575 else:
576 raise BBGenErr('Conflict at %s' % '.'.join(
577 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53578 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28579 a[key] = b[key]
580 return a
581
John Budorickab108712018-09-01 00:12:21582 def initialize_args_for_test(
583 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21584 args = []
585 args.extend(generated_test.get('args', []))
586 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22587
Kenneth Russell8a386d42018-06-02 09:48:01588 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21589 val = generated_test.pop(key, [])
590 if fn(tester_config):
591 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01592
593 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
Brian Sheedy781c8ca42021-03-08 22:03:21594 add_conditional_args('lacros_args', self.is_lacros)
Kenneth Russell8a386d42018-06-02 09:48:01595 add_conditional_args('linux_args', self.is_linux)
596 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36597 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49598 add_conditional_args('mac_args', self.is_mac)
599 add_conditional_args('win_args', self.is_win)
600 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01601
John Budorickab108712018-09-01 00:12:21602 for key in additional_arg_keys or []:
603 args.extend(generated_test.pop(key, []))
604 args.extend(tester_config.get(key, []))
605
606 if args:
607 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01608
Kenneth Russelleb60cbd22017-12-05 07:54:28609 def initialize_swarming_dictionary_for_test(self, generated_test,
610 tester_config):
611 if 'swarming' not in generated_test:
612 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28613 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
614 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38615 'can_use_on_swarming_builders': tester_config.get('use_swarming',
616 True)
Dirk Pranke81ff51c2017-12-09 19:24:28617 })
Kenneth Russelleb60cbd22017-12-05 07:54:28618 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03619 if ('dimension_sets' not in generated_test['swarming'] and
620 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28621 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
622 tester_config['swarming']['dimension_sets'])
623 self.dictionary_merge(generated_test['swarming'],
624 tester_config['swarming'])
Brian Sheedybc984e242021-04-21 23:44:51625 # Apply any platform-specific Swarming dimensions after the generic ones.
Kenneth Russelleb60cbd22017-12-05 07:54:28626 if 'android_swarming' in generated_test:
627 if self.is_android(tester_config): # pragma: no cover
628 self.dictionary_merge(
629 generated_test['swarming'],
630 generated_test['android_swarming']) # pragma: no cover
631 del generated_test['android_swarming'] # pragma: no cover
Brian Sheedybc984e242021-04-21 23:44:51632 if 'chromeos_swarming' in generated_test:
633 if self.is_chromeos(tester_config): # pragma: no cover
634 self.dictionary_merge(
635 generated_test['swarming'],
636 generated_test['chromeos_swarming']) # pragma: no cover
637 del generated_test['chromeos_swarming'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28638
639 def clean_swarming_dictionary(self, swarming_dict):
640 # Clean out redundant entries from a test's "swarming" dictionary.
641 # This is really only needed to retain 100% parity with the
642 # handwritten JSON files, and can be removed once all the files are
643 # autogenerated.
644 if 'shards' in swarming_dict:
645 if swarming_dict['shards'] == 1: # pragma: no cover
646 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24647 if 'hard_timeout' in swarming_dict:
648 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
649 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43650 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28651 # Remove all other keys.
Jamie Madillcf4f8c72021-05-20 19:24:23652 for k in list(swarming_dict): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28653 if k != 'can_use_on_swarming_builders': # pragma: no cover
654 del swarming_dict[k] # pragma: no cover
655
Stephen Martinis0382bc12018-09-17 22:29:07656 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
657 waterfall):
658 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01659 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07660 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28661 # See if there are any exceptions that need to be merged into this
662 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49663 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28664 if modifications:
665 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23666 if 'swarming' in test:
667 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28668 # Ensure all Android Swarming tests run only on userdebug builds if another
669 # build type was not specified.
670 if 'swarming' in test and self.is_android(tester_config):
671 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22672 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28673 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37674 self.replace_test_args(test, test_name, tester_name)
Ben Pastenee012aea42019-05-14 22:32:28675
Kenneth Russelleb60cbd22017-12-05 07:54:28676 return test
677
Brian Sheedye6ea0ee2019-07-11 02:54:37678 def replace_test_args(self, test, test_name, tester_name):
679 replacements = self.get_test_replacements(
680 test, test_name, tester_name) or {}
681 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
Jamie Madillcf4f8c72021-05-20 19:24:23682 for key, replacement_dict in replacements.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37683 if key not in valid_replacement_keys:
684 raise BBGenErr(
685 'Given replacement key %s for %s on %s is not in the list of valid '
686 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
Jamie Madillcf4f8c72021-05-20 19:24:23687 for replacement_key, replacement_val in replacement_dict.items():
Brian Sheedye6ea0ee2019-07-11 02:54:37688 found_key = False
689 for i, test_key in enumerate(test.get(key, [])):
690 # Handle both the key/value being replaced being defined as two
691 # separate items or as key=value.
692 if test_key == replacement_key:
693 found_key = True
694 # Handle flags without values.
695 if replacement_val == None:
696 del test[key][i]
697 else:
698 test[key][i+1] = replacement_val
699 break
700 elif test_key.startswith(replacement_key + '='):
701 found_key = True
702 if replacement_val == None:
703 del test[key][i]
704 else:
705 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
706 break
707 if not found_key:
708 raise BBGenErr('Could not find %s in existing list of values for key '
709 '%s in %s on %s' % (replacement_key, key, test_name,
710 tester_name))
711
Shenghua Zhangaba8bad2018-02-07 02:12:09712 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05713 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26714 True):
715 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06716 # are targeting CrOS hardware and so need the special trigger script.
717 dimension_sets = test['swarming']['dimension_sets']
Ben Pastenea9e583b2019-01-16 02:57:26718 if all('device_type' in ds for ds in dimension_sets):
719 test['trigger_script'] = {
720 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
721 }
Shenghua Zhangaba8bad2018-02-07 02:12:09722
Yuly Novikov26dd47052021-02-11 00:57:14723 def add_logdog_butler_cipd_package(self, tester_config, result):
724 if not tester_config.get('skip_cipd_packages', False):
725 cipd_packages = result['swarming'].get('cipd_packages', [])
726 already_added = len([
727 package for package in cipd_packages
728 if package.get('cipd_package', "").find('logdog/butler') > 0
729 ]) > 0
730 if not already_added:
731 cipd_packages.append({
732 'cipd_package':
733 'infra/tools/luci/logdog/butler/${platform}',
734 'location':
735 'bin',
736 'revision':
737 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
738 })
739 result['swarming']['cipd_packages'] = cipd_packages
740
Ben Pastene858f4be2019-01-09 23:52:09741 def add_android_presentation_args(self, tester_config, test_name, result):
742 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38743 bucket = tester_config.get('results_bucket', 'chromium-result-details')
744 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09745 if (result['swarming']['can_use_on_swarming_builders'] and not
746 tester_config.get('skip_merge_script', False)):
747 result['merge'] = {
748 'args': [
749 '--bucket',
John Budorick262ae112019-07-12 19:24:38750 bucket,
Ben Pastene858f4be2019-01-09 23:52:09751 '--test-name',
Rakib M. Hasanc9e01c62020-07-27 22:48:12752 result.get('name', test_name)
Ben Pastene858f4be2019-01-09 23:52:09753 ],
754 'script': '//build/android/pylib/results/presentation/'
755 'test_results_presentation.py',
756 }
Ben Pastene858f4be2019-01-09 23:52:09757 if not tester_config.get('skip_output_links', False):
758 result['swarming']['output_links'] = [
759 {
760 'link': [
761 'https://luci-logdog.appspot.com/v/?s',
762 '=android%2Fswarming%2Flogcats%2F',
763 '${TASK_ID}%2F%2B%2Funified_logcats',
764 ],
765 'name': 'shard #${SHARD_INDEX} logcats',
766 },
767 ]
768 if args:
769 result['args'] = args
770
Kenneth Russelleb60cbd22017-12-05 07:54:28771 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
772 test_config):
773 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15774 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28775 return None
776 result = copy.deepcopy(test_config)
777 if 'test' in result:
Rakib M. Hasanc9e01c62020-07-27 22:48:12778 if 'name' not in result:
779 result['name'] = test_name
Kenneth Russelleb60cbd22017-12-05 07:54:28780 else:
781 result['test'] = test_name
782 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21783
784 self.initialize_args_for_test(
785 result, tester_config, additional_arg_keys=['gtest_args'])
Jamie Madilla8be0d72020-10-02 05:24:04786 if self.is_android(tester_config) and tester_config.get(
Yuly Novikov26dd47052021-02-11 00:57:14787 'use_swarming', True):
788 if not test_config.get('use_isolated_scripts_api', False):
789 # TODO(https://crbug.com/1137998) make Android presentation work with
790 # isolated scripts in test_results_presentation.py merge script
791 self.add_android_presentation_args(tester_config, test_name, result)
792 result['args'] = result.get('args', []) + ['--recover-devices']
793 self.add_logdog_butler_cipd_package(tester_config, result)
Benjamin Pastene766d48f52017-12-18 21:47:42794
Stephen Martinis0382bc12018-09-17 22:29:07795 result = self.update_and_cleanup_test(
796 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09797 self.add_common_test_properties(result, tester_config)
Brian Sheedya31578e2020-05-18 20:24:36798 self.substitute_magic_args(result)
Stephen Martinisbc7b7772019-05-01 22:01:43799
800 if not result.get('merge'):
801 # TODO(https://crbug.com/958376): Consider adding the ability to not have
802 # this default.
Jamie Madilla8be0d72020-10-02 05:24:04803 if test_config.get('use_isolated_scripts_api', False):
804 merge_script = 'standard_isolated_script_merge'
805 else:
806 merge_script = 'standard_gtest_merge'
807
Stephen Martinisbc7b7772019-05-01 22:01:43808 result['merge'] = {
Jamie Madilla8be0d72020-10-02 05:24:04809 'script': '//testing/merge_scripts/%s.py' % merge_script,
810 'args': [],
Stephen Martinisbc7b7772019-05-01 22:01:43811 }
Kenneth Russelleb60cbd22017-12-05 07:54:28812 return result
813
814 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
815 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01816 if not self.should_run_on_tester(waterfall, tester_name, test_name,
817 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28818 return None
819 result = copy.deepcopy(test_config)
820 result['isolate_name'] = result.get('isolate_name', test_name)
Jeff Yoonb8bfdbf32020-03-13 19:14:43821 result['name'] = result.get('name', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28822 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01823 self.initialize_args_for_test(result, tester_config)
Yuly Novikov26dd47052021-02-11 00:57:14824 if self.is_android(tester_config) and tester_config.get(
825 'use_swarming', True):
826 if tester_config.get('use_android_presentation', False):
827 # TODO(https://crbug.com/1137998) make Android presentation work with
828 # isolated scripts in test_results_presentation.py merge script
829 self.add_android_presentation_args(tester_config, test_name, result)
830 self.add_logdog_butler_cipd_package(tester_config, result)
Stephen Martinis0382bc12018-09-17 22:29:07831 result = self.update_and_cleanup_test(
832 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09833 self.add_common_test_properties(result, tester_config)
Brian Sheedya31578e2020-05-18 20:24:36834 self.substitute_magic_args(result)
Stephen Martinisf50047062019-05-06 22:26:17835
836 if not result.get('merge'):
837 # TODO(https://crbug.com/958376): Consider adding the ability to not have
838 # this default.
839 result['merge'] = {
840 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
841 'args': [],
842 }
Kenneth Russelleb60cbd22017-12-05 07:54:28843 return result
844
845 def generate_script_test(self, waterfall, tester_name, tester_config,
846 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44847 # TODO(https://crbug.com/953072): Remove this check whenever a better
848 # long-term solution is implemented.
849 if (waterfall.get('forbid_script_tests', False) or
850 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
851 raise BBGenErr('Attempted to generate a script test on tester ' +
852 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01853 if not self.should_run_on_tester(waterfall, tester_name, test_name,
854 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28855 return None
856 result = {
857 'name': test_name,
858 'script': test_config['script']
859 }
Stephen Martinis0382bc12018-09-17 22:29:07860 result = self.update_and_cleanup_test(
861 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedya31578e2020-05-18 20:24:36862 self.substitute_magic_args(result)
Kenneth Russelleb60cbd22017-12-05 07:54:28863 return result
864
865 def generate_junit_test(self, waterfall, tester_name, tester_config,
866 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01867 if not self.should_run_on_tester(waterfall, tester_name, test_name,
868 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28869 return None
John Budorickdef6acb2019-09-17 22:51:09870 result = copy.deepcopy(test_config)
871 result.update({
John Budorickcadc4952019-09-16 23:51:37872 'name': test_name,
873 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09874 })
875 self.initialize_args_for_test(result, tester_config)
876 result = self.update_and_cleanup_test(
877 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedya31578e2020-05-18 20:24:36878 self.substitute_magic_args(result)
Kenneth Russelleb60cbd22017-12-05 07:54:28879 return result
880
Xinan Lin05fb9c1752020-12-17 00:15:52881 def generate_skylab_test(self, waterfall, tester_name, tester_config,
882 test_name, test_config):
883 if not self.should_run_on_tester(waterfall, tester_name, test_name,
884 test_config):
885 return None
886 result = copy.deepcopy(test_config)
887 result.update({
888 'test': test_name,
889 })
890 self.initialize_args_for_test(result, tester_config)
891 result = self.update_and_cleanup_test(result, test_name, tester_name,
892 tester_config, waterfall)
893 self.substitute_magic_args(result)
894 return result
895
Stephen Martinis2a0667022018-09-25 22:31:14896 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01897 substitutions = {
898 # Any machine in waterfalls.pyl which desires to run GPU tests
899 # must provide the os_type key.
900 'os_type': tester_config['os_type'],
901 'gpu_vendor_id': '0',
902 'gpu_device_id': '0',
903 }
Stephen Martinis2a0667022018-09-25 22:31:14904 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01905 if 'gpu' in dimension_set:
906 # First remove the driver version, then split into vendor and device.
907 gpu = dimension_set['gpu']
Yuly Novikove4b2fef2020-09-04 05:53:11908 if gpu != 'none':
909 gpu = gpu.split('-')[0].split(':')
910 substitutions['gpu_vendor_id'] = gpu[0]
911 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01912 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
913
914 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56915 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01916 # These are all just specializations of isolated script tests with
917 # a bunch of boilerplate command line arguments added.
918
919 # The step name must end in 'test' or 'tests' in order for the
920 # results to automatically show up on the flakiness dashboard.
921 # (At least, this was true some time ago.) Continue to use this
922 # naming convention for the time being to minimize changes.
923 step_name = test_config.get('name', test_name)
924 if not (step_name.endswith('test') or step_name.endswith('tests')):
925 step_name = '%s_tests' % step_name
926 result = self.generate_isolated_script_test(
927 waterfall, tester_name, tester_config, step_name, test_config)
928 if not result:
929 return None
Chong Gub75754b32020-03-13 16:39:20930 result['isolate_name'] = test_config.get(
Brian Sheedyf74819b2021-06-04 01:38:38931 'isolate_name',
932 self.get_default_isolate_name(tester_config, is_android_webview))
Chan Liab7d8dd82020-04-24 23:42:19933
Chan Lia3ad1502020-04-28 05:32:11934 # Populate test_id_prefix.
Brian Sheedyf74819b2021-06-04 01:38:38935 gn_entry = self.gn_isolate_map[result['isolate_name']]
Chan Li17d969f92020-07-10 00:50:03936 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19937
Kenneth Russell8a386d42018-06-02 09:48:01938 args = result.get('args', [])
939 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14940
Brian Sheedyd8c0c73d2021-07-05 02:11:30941 # TODO(skbug.com/12149): Remove this once Gold-based tests no longer clobber
942 # earlier results on retry attempts.
943 is_gold_based_test = False
944 for a in args:
945 if '--git-revision' in a:
946 is_gold_based_test = True
947 break
948 if is_gold_based_test:
949 for a in args:
950 if '--test-filter' in a or '--isolated-script-test-filter' in a:
951 raise RuntimeError(
952 '--test-filter/--isolated-script-test-filter are currently not '
953 'supported for Gold-based GPU tests. See skbug.com/12100 and '
954 'skbug.com/12149 for more details.')
955
erikchen6da2d9b2018-08-03 23:01:14956 # These tests upload and download results from cloud storage and therefore
957 # aren't idempotent yet. https://crbug.com/549140.
958 result['swarming']['idempotent'] = False
959
Kenneth Russell44910c32018-12-03 23:35:11960 # The GPU tests act much like integration tests for the entire browser, and
961 # tend to uncover flakiness bugs more readily than other test suites. In
962 # order to surface any flakiness more readily to the developer of the CL
963 # which is introducing it, we disable retries with patch on the commit
964 # queue.
965 result['should_retry_with_patch'] = False
966
Bo Liu555a0f92019-03-29 12:11:56967 browser = ('android-webview-instrumentation'
968 if is_android_webview else tester_config['browser_config'])
Brian Sheedy4053a702020-07-28 02:09:52969
970 # Most platforms require --enable-logging=stderr to get useful browser logs.
971 # However, this actively messes with logging on CrOS (because Chrome's
972 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
973 # in order to see JavaScript console messages. See
974 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
975 logging_arg = '--log-level=0' if self.is_chromeos(
976 tester_config) else '--enable-logging=stderr'
977
Kenneth Russell8a386d42018-06-02 09:48:01978 args = [
Bo Liu555a0f92019-03-29 12:11:56979 test_to_run,
980 '--show-stdout',
981 '--browser=%s' % browser,
982 # --passthrough displays more of the logging in Telemetry when
983 # run via typ, in particular some of the warnings about tests
984 # being expected to fail, but passing.
985 '--passthrough',
986 '-v',
Brian Sheedy4053a702020-07-28 02:09:52987 '--extra-browser-args=%s --js-flags=--expose-gc' % logging_arg,
Kenneth Russell8a386d42018-06-02 09:48:01988 ] + args
989 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14990 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01991 return result
992
Brian Sheedyf74819b2021-06-04 01:38:38993 def get_default_isolate_name(self, tester_config, is_android_webview):
994 if self.is_android(tester_config):
995 if is_android_webview:
996 return 'telemetry_gpu_integration_test_android_webview'
997 return (
998 'telemetry_gpu_integration_test' +
999 BROWSER_CONFIG_TO_TARGET_SUFFIX_MAP[tester_config['browser_config']])
1000 else:
1001 return 'telemetry_gpu_integration_test'
1002
Kenneth Russelleb60cbd22017-12-05 07:54:281003 def get_test_generator_map(self):
1004 return {
Bo Liu555a0f92019-03-29 12:11:561005 'android_webview_gpu_telemetry_tests':
1006 GPUTelemetryTestGenerator(self, is_android_webview=True),
Bo Liu555a0f92019-03-29 12:11:561007 'gpu_telemetry_tests':
1008 GPUTelemetryTestGenerator(self),
1009 'gtest_tests':
1010 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:561011 'isolated_scripts':
1012 IsolatedScriptTestGenerator(self),
1013 'junit_tests':
1014 JUnitGenerator(self),
1015 'scripts':
1016 ScriptGenerator(self),
Xinan Lin05fb9c1752020-12-17 00:15:521017 'skylab_tests':
1018 SkylabGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:281019 }
1020
Kenneth Russell8a386d42018-06-02 09:48:011021 def get_test_type_remapper(self):
1022 return {
1023 # These are a specialization of isolated_scripts with a bunch of
1024 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:561025 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:011026 'gpu_telemetry_tests': 'isolated_scripts',
1027 }
1028
Jeff Yoon67c3e832020-02-08 07:39:381029 def check_composition_type_test_suites(self, test_type,
1030 additional_validators=None):
1031 """Pre-pass to catch errors reliabily for compound/matrix suites"""
1032 validators = [check_compound_references,
1033 check_basic_references,
1034 check_conflicting_definitions]
1035 if additional_validators:
1036 validators += additional_validators
1037
1038 target_suites = self.test_suites.get(test_type, {})
1039 other_test_type = ('compound_suites'
1040 if test_type == 'matrix_compound_suites'
1041 else 'matrix_compound_suites')
1042 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:011043 basic_suites = self.test_suites.get('basic_suites', {})
1044
Jamie Madillcf4f8c72021-05-20 19:24:231045 for suite, suite_def in target_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011046 if suite in basic_suites:
1047 raise BBGenErr('%s names may not duplicate basic test suite names '
1048 '(error found while processsing %s)'
1049 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:011050
Jeff Yoon67c3e832020-02-08 07:39:381051 seen_tests = {}
1052 for sub_suite in suite_def:
1053 for validator in validators:
1054 validator(
1055 basic_suites=basic_suites,
1056 other_test_suites=other_suites,
1057 seen_tests=seen_tests,
1058 sub_suite=sub_suite,
1059 suite=suite,
1060 suite_def=suite_def,
1061 target_test_suites=target_suites,
1062 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:051063 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:381064 )
Kenneth Russelleb60cbd22017-12-05 07:54:281065
Stephen Martinis54d64ad2018-09-21 22:16:201066 def flatten_test_suites(self):
1067 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:011068 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
1069 for category in test_types:
Jamie Madillcf4f8c72021-05-20 19:24:231070 for name, value in self.test_suites.get(category, {}).items():
Jeff Yoon8154e582019-12-03 23:30:011071 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:201072 self.test_suites = new_test_suites
1073
Chan Lia3ad1502020-04-28 05:32:111074 def resolve_test_id_prefixes(self):
Jamie Madillcf4f8c72021-05-20 19:24:231075 for suite in self.test_suites['basic_suites'].values():
1076 for key, test in suite.items():
Dirk Pranke0e879b22020-07-16 23:53:561077 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:411078
1079 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
John Palmera8515fca2021-05-20 03:35:321080 # 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:411081 # TODO(crbug.com/1035124): clean this up.
1082 isolate_name = test.get('test') or test.get('isolate_name') or key
1083 gn_entry = self.gn_isolate_map.get(isolate_name)
1084 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:281085 label = gn_entry['label']
1086
1087 if label.count(':') != 1:
1088 raise BBGenErr(
1089 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
1090 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
1091 (label, isolate_name))
1092 if label.split(':')[1] != isolate_name:
1093 raise BBGenErr(
1094 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
1095 ' label "%s" see http://crbug.com/1071091 for details.' %
1096 (isolate_name, label))
1097
Chan Lia3ad1502020-04-28 05:32:111098 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:411099 else: # pragma: no cover
1100 # Some tests do not have an entry gn_isolate_map.pyl, such as
1101 # telemetry tests.
1102 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
1103 pass
1104
Kenneth Russelleb60cbd22017-12-05 07:54:281105 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:011106 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:201107
Jeff Yoon8154e582019-12-03 23:30:011108 compound_suites = self.test_suites.get('compound_suites', {})
1109 # check_composition_type_test_suites() checks that all basic suites
1110 # referenced by compound suites exist.
1111 basic_suites = self.test_suites.get('basic_suites')
1112
Jamie Madillcf4f8c72021-05-20 19:24:231113 for name, value in compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011114 # Resolve this to a dictionary.
1115 full_suite = {}
1116 for entry in value:
1117 suite = basic_suites[entry]
1118 full_suite.update(suite)
1119 compound_suites[name] = full_suite
1120
Jeff Yoon85fb8df2020-08-20 16:47:431121 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381122 """ Merge variant-defined configurations to each test case definition in a
1123 test suite.
1124
1125 The output maps a unique test name to an array of configurations because
1126 there may exist more than one definition for a test name using variants. The
1127 test name is referenced while mapping machines to test suites, so unpacking
1128 the array is done by the generators.
1129
1130 Args:
1131 basic_test_definition: a {} defined test suite in the format
1132 test_name:test_config
1133 variants: an [] of {} defining configurations to be applied to each test
1134 case in the basic test_definition
1135
1136 Return:
1137 a {} of test_name:[{}], where each {} is a merged configuration
1138 """
1139
1140 # Each test in a basic test suite will have a definition per variant.
1141 test_suite = {}
Jamie Madillcf4f8c72021-05-20 19:24:231142 for test_name, test_config in basic_test_definition.items():
Jeff Yoon67c3e832020-02-08 07:39:381143 definitions = []
1144 for variant in variants:
Jeff Yoonda581c32020-03-06 03:56:051145 # Unpack the variant from variants.pyl if it's string based.
1146 if isinstance(variant, str):
1147 variant = self.variants[variant]
1148
Jeff Yoon67c3e832020-02-08 07:39:381149 # Clone a copy of test_config so that we can have a uniquely updated
1150 # version of it per variant
1151 cloned_config = copy.deepcopy(test_config)
1152 # The variant definition needs to be re-used for each test, so we'll
1153 # create a clone and work with it as well.
1154 cloned_variant = copy.deepcopy(variant)
1155
1156 cloned_config['args'] = (cloned_config.get('args', []) +
1157 cloned_variant.get('args', []))
1158 cloned_config['mixins'] = (cloned_config.get('mixins', []) +
Jeff Yoon85fb8df2020-08-20 16:47:431159 cloned_variant.get('mixins', []) + mixins)
Jeff Yoon67c3e832020-02-08 07:39:381160
1161 basic_swarming_def = cloned_config.get('swarming', {})
1162 variant_swarming_def = cloned_variant.get('swarming', {})
1163 if basic_swarming_def and variant_swarming_def:
1164 if ('dimension_sets' in basic_swarming_def and
1165 'dimension_sets' in variant_swarming_def):
1166 # Retain swarming dimension set merge behavior when both variant and
1167 # the basic test configuration both define it
1168 self.dictionary_merge(basic_swarming_def, variant_swarming_def)
1169 # Remove dimension_sets from the variant definition, so that it does
1170 # not replace what's been done by dictionary_merge in the update
1171 # call below.
1172 del variant_swarming_def['dimension_sets']
1173
1174 # Update the swarming definition with whatever is defined for swarming
1175 # by the variant.
1176 basic_swarming_def.update(variant_swarming_def)
1177 cloned_config['swarming'] = basic_swarming_def
1178
Xinan Lin05fb9c1752020-12-17 00:15:521179 # Copy all skylab fields defined by the variant.
1180 skylab_config = cloned_variant.get('skylab')
1181 if skylab_config:
1182 for k, v in skylab_config.items():
1183 cloned_config[k] = v
1184
Jeff Yoon67c3e832020-02-08 07:39:381185 # The identifier is used to make the name of the test unique.
1186 # Generators in the recipe uniquely identify a test by it's name, so we
1187 # don't want to have the same name for each variant.
1188 cloned_config['name'] = '{}_{}'.format(test_name,
1189 cloned_variant['identifier'])
Jeff Yoon67c3e832020-02-08 07:39:381190 definitions.append(cloned_config)
1191 test_suite[test_name] = definitions
1192 return test_suite
1193
Jeff Yoon8154e582019-12-03 23:30:011194 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381195 self.check_composition_type_test_suites('matrix_compound_suites',
1196 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011197
1198 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381199 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011200 # referenced by matrix suites exist.
1201 basic_suites = self.test_suites.get('basic_suites')
1202
Jamie Madillcf4f8c72021-05-20 19:24:231203 for test_name, matrix_config in matrix_compound_suites.items():
Jeff Yoon8154e582019-12-03 23:30:011204 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381205
Jamie Madillcf4f8c72021-05-20 19:24:231206 for test_suite, mtx_test_suite_config in matrix_config.items():
Jeff Yoon67c3e832020-02-08 07:39:381207 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1208
1209 if 'variants' in mtx_test_suite_config:
Jeff Yoon85fb8df2020-08-20 16:47:431210 mixins = mtx_test_suite_config.get('mixins', [])
Jeff Yoon67c3e832020-02-08 07:39:381211 result = self.resolve_variants(basic_test_def,
Jeff Yoon85fb8df2020-08-20 16:47:431212 mtx_test_suite_config['variants'],
1213 mixins)
Jeff Yoon67c3e832020-02-08 07:39:381214 full_suite.update(result)
Sven Zheng2fe6dd6f2021-08-06 21:12:271215 else:
1216 suite = basic_suites[test_suite]
1217 full_suite.update(suite)
Jeff Yoon67c3e832020-02-08 07:39:381218 matrix_compound_suites[test_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281219
1220 def link_waterfalls_to_test_suites(self):
1221 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231222 for tester_name, tester in waterfall['machines'].items():
1223 for suite, value in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281224 if not value in self.test_suites:
1225 # Hard / impossible to cover this in the unit test.
1226 raise self.unknown_test_suite(
1227 value, tester_name, waterfall['name']) # pragma: no cover
1228 tester['test_suites'][suite] = self.test_suites[value]
1229
1230 def load_configuration_files(self):
1231 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
1232 self.test_suites = self.load_pyl_file('test_suites.pyl')
1233 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:011234 self.mixins = self.load_pyl_file('mixins.pyl')
Nodir Turakulovfce34292019-12-18 17:05:411235 self.gn_isolate_map = self.load_pyl_file('gn_isolate_map.pyl')
Chong Guee622242020-10-28 18:17:351236 for isolate_map in self.args.isolate_map_files:
1237 isolate_map = self.load_pyl_file(isolate_map)
1238 duplicates = set(isolate_map).intersection(self.gn_isolate_map)
1239 if duplicates:
1240 raise BBGenErr('Duplicate targets in isolate map files: %s.' %
1241 ', '.join(duplicates))
1242 self.gn_isolate_map.update(isolate_map)
1243
Jeff Yoonda581c32020-03-06 03:56:051244 self.variants = self.load_pyl_file('variants.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:281245
1246 def resolve_configuration_files(self):
Chan Lia3ad1502020-04-28 05:32:111247 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281248 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011249 self.resolve_matrix_compound_test_suites()
1250 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281251 self.link_waterfalls_to_test_suites()
1252
Nico Weberd18b8962018-05-16 19:39:381253 def unknown_bot(self, bot_name, waterfall_name):
1254 return BBGenErr(
1255 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1256
Kenneth Russelleb60cbd22017-12-05 07:54:281257 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1258 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381259 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281260 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1261
1262 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1263 return BBGenErr(
1264 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1265 ' on waterfall ' + waterfall_name)
1266
Stephen Martinisb72f6d22018-10-04 23:29:011267 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071268 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321269
1270 Checks in the waterfall, builder, and test objects for mixins.
1271 """
1272 def valid_mixin(mixin_name):
1273 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011274 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321275 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381276
Stephen Martinisb6a50492018-09-12 23:59:321277 def must_be_list(mixins, typ, name):
1278 """Asserts that given mixins are a list."""
1279 if not isinstance(mixins, list):
1280 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1281
Brian Sheedy7658c982020-01-08 02:27:581282 test_name = test.get('name')
1283 remove_mixins = set()
1284 if 'remove_mixins' in builder:
1285 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1286 for rm in builder['remove_mixins']:
1287 valid_mixin(rm)
1288 remove_mixins.add(rm)
1289 if 'remove_mixins' in test:
1290 must_be_list(test['remove_mixins'], 'test', test_name)
1291 for rm in test['remove_mixins']:
1292 valid_mixin(rm)
1293 remove_mixins.add(rm)
1294 del test['remove_mixins']
1295
Stephen Martinisb72f6d22018-10-04 23:29:011296 if 'mixins' in waterfall:
1297 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1298 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581299 if mixin in remove_mixins:
1300 continue
Stephen Martinisb6a50492018-09-12 23:59:321301 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011302 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321303
Stephen Martinisb72f6d22018-10-04 23:29:011304 if 'mixins' in builder:
1305 must_be_list(builder['mixins'], 'builder', builder_name)
1306 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581307 if mixin in remove_mixins:
1308 continue
Stephen Martinisb6a50492018-09-12 23:59:321309 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011310 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321311
Stephen Martinisb72f6d22018-10-04 23:29:011312 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071313 return test
1314
Stephen Martinis2a0667022018-09-25 22:31:141315 if not test_name:
1316 test_name = test.get('test')
1317 if not test_name: # pragma: no cover
1318 # Not the best name, but we should say something.
1319 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:011320 must_be_list(test['mixins'], 'test', test_name)
1321 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581322 # We don't bother checking if the given mixin is in remove_mixins here
1323 # since this is already the lowest level, so if a mixin is added here that
1324 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071325 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011326 test = self.apply_mixin(self.mixins[mixin], test)
Jeff Yoon67c3e832020-02-08 07:39:381327 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071328 return test
Stephen Martinisb6a50492018-09-12 23:59:321329
Stephen Martinisb72f6d22018-10-04 23:29:011330 def apply_mixin(self, mixin, test):
1331 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321332
Stephen Martinis0382bc12018-09-17 22:29:071333 Mixins will not override an existing key. This is to ensure exceptions can
1334 override a setting a mixin applies.
1335
Stephen Martinisb72f6d22018-10-04 23:29:011336 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:321337 'dimension_sets', which is how normal test suites specify their dimensions,
1338 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
1339 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011340
Stephen Martinisb6a50492018-09-12 23:59:321341 """
1342 new_test = copy.deepcopy(test)
1343 mixin = copy.deepcopy(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011344 if 'swarming' in mixin:
1345 swarming_mixin = mixin['swarming']
1346 new_test.setdefault('swarming', {})
Brian Sheedycae63b22020-06-10 22:52:111347 # Copy over any explicit dimension sets first so that they will be updated
1348 # by any subsequent 'dimensions' entries.
1349 if 'dimension_sets' in swarming_mixin:
1350 existing_dimension_sets = new_test['swarming'].setdefault(
1351 'dimension_sets', [])
1352 # Appending to the existing list could potentially result in different
1353 # behavior depending on the order the mixins were applied, but that's
1354 # already the case for other parts of mixins, so trust that the user
1355 # will verify that the generated output is correct before submitting.
1356 for dimension_set in swarming_mixin['dimension_sets']:
1357 if dimension_set not in existing_dimension_sets:
1358 existing_dimension_sets.append(dimension_set)
1359 del swarming_mixin['dimension_sets']
Stephen Martinisb72f6d22018-10-04 23:29:011360 if 'dimensions' in swarming_mixin:
1361 new_test['swarming'].setdefault('dimension_sets', [{}])
1362 for dimension_set in new_test['swarming']['dimension_sets']:
1363 dimension_set.update(swarming_mixin['dimensions'])
1364 del swarming_mixin['dimensions']
Stephen Martinisb72f6d22018-10-04 23:29:011365 # python dict update doesn't do recursion at all. Just hard code the
1366 # nested update we need (mixin['swarming'] shouldn't clobber
1367 # test['swarming'], but should update it).
1368 new_test['swarming'].update(swarming_mixin)
1369 del mixin['swarming']
1370
Wezc0e835b702018-10-30 00:38:411371 if '$mixin_append' in mixin:
1372 # Values specified under $mixin_append should be appended to existing
1373 # lists, rather than replacing them.
1374 mixin_append = mixin['$mixin_append']
Zhaoyang Li473dd0ae2021-05-10 18:28:281375
1376 # Append swarming named cache and delete swarming key, since it's under
1377 # another layer of dict.
1378 if 'named_caches' in mixin_append.get('swarming', {}):
1379 new_test['swarming'].setdefault('named_caches', [])
1380 new_test['swarming']['named_caches'].extend(
1381 mixin_append['swarming']['named_caches'])
1382 if len(mixin_append['swarming']) > 1:
1383 raise BBGenErr('Only named_caches is supported under swarming key in '
1384 '$mixin_append, but there are: %s' %
1385 sorted(mixin_append['swarming'].keys()))
1386 del mixin_append['swarming']
Wezc0e835b702018-10-30 00:38:411387 for key in mixin_append:
1388 new_test.setdefault(key, [])
1389 if not isinstance(mixin_append[key], list):
1390 raise BBGenErr(
1391 'Key "' + key + '" in $mixin_append must be a list.')
1392 if not isinstance(new_test[key], list):
1393 raise BBGenErr(
1394 'Cannot apply $mixin_append to non-list "' + key + '".')
1395 new_test[key].extend(mixin_append[key])
1396 if 'args' in mixin_append:
1397 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1398 del mixin['$mixin_append']
1399
Stephen Martinisb72f6d22018-10-04 23:29:011400 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321401 return new_test
1402
Greg Gutermanf60eb052020-03-12 17:40:011403 def generate_output_tests(self, waterfall):
1404 """Generates the tests for a waterfall.
1405
1406 Args:
1407 waterfall: a dictionary parsed from a master pyl file
1408 Returns:
1409 A dictionary mapping builders to test specs
1410 """
1411 return {
Jamie Madillcf4f8c72021-05-20 19:24:231412 name: self.get_tests_for_config(waterfall, name, config)
1413 for name, config in waterfall['machines'].items()
Greg Gutermanf60eb052020-03-12 17:40:011414 }
1415
1416 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531417 generator_map = self.get_test_generator_map()
1418 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281419
Greg Gutermanf60eb052020-03-12 17:40:011420 tests = {}
1421 # Copy only well-understood entries in the machine's configuration
1422 # verbatim into the generated JSON.
1423 if 'additional_compile_targets' in config:
1424 tests['additional_compile_targets'] = config[
1425 'additional_compile_targets']
Jamie Madillcf4f8c72021-05-20 19:24:231426 for test_type, input_tests in config.get('test_suites', {}).items():
Greg Gutermanf60eb052020-03-12 17:40:011427 if test_type not in generator_map:
1428 raise self.unknown_test_suite_type(
1429 test_type, name, waterfall['name']) # pragma: no cover
1430 test_generator = generator_map[test_type]
1431 # Let multiple kinds of generators generate the same kinds
1432 # of tests. For example, gpu_telemetry_tests are a
1433 # specialization of isolated_scripts.
1434 new_tests = test_generator.generate(
1435 waterfall, name, config, input_tests)
1436 remapped_test_type = test_type_remapper.get(test_type, test_type)
1437 tests[remapped_test_type] = test_generator.sort(
1438 tests.get(remapped_test_type, []) + new_tests)
1439
1440 return tests
1441
1442 def jsonify(self, all_tests):
1443 return json.dumps(
1444 all_tests, indent=2, separators=(',', ': '),
1445 sort_keys=True) + '\n'
1446
1447 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281448 self.load_configuration_files()
1449 self.resolve_configuration_files()
1450 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011451 result = collections.defaultdict(dict)
1452
Dirk Pranke6269d302020-10-01 00:14:391453 required_fields = ('name',)
Greg Gutermanf60eb052020-03-12 17:40:011454 for waterfall in self.waterfalls:
1455 for field in required_fields:
1456 # Verify required fields
1457 if field not in waterfall:
1458 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1459
1460 # Handle filter flag, if specified
1461 if filters and waterfall['name'] not in filters:
1462 continue
1463
1464 # Join config files and hardcoded values together
1465 all_tests = self.generate_output_tests(waterfall)
1466 result[waterfall['name']] = all_tests
1467
Greg Gutermanf60eb052020-03-12 17:40:011468 # Add do not edit warning
1469 for tests in result.values():
1470 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1471 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1472
1473 return result
1474
1475 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281476 suffix = '.json'
1477 if self.args.new_files:
1478 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011479
1480 for filename, contents in result.items():
1481 jsonstr = self.jsonify(contents)
1482 self.write_file(self.pyl_file_path(filename + suffix), jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281483
Nico Weberd18b8962018-05-16 19:39:381484 def get_valid_bot_names(self):
Ben Joyce2f48b6942020-11-10 21:22:371485 # Extract bot names from infra/config/generated/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421486 # NOTE: This reference can cause issues; if a file changes there, the
1487 # presubmit here won't be run by default. A manually maintained list there
1488 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1489 # references to configs outside of this directory are added, please change
1490 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1491 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491492
Garrett Beaty7e866fc2021-06-16 14:12:101493 # Get the generated project.pyl so we can check if we should be enforcing
1494 # that the specs are for builders that actually exist
1495 # If not, return None to indicate that we won't enforce that builders in
1496 # waterfalls.pyl are defined in LUCI
Garrett Beaty4f3e9212020-06-25 20:21:491497 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1498 'project.pyl')
1499 if os.path.exists(project_pyl_path):
1500 settings = ast.literal_eval(self.read_file(project_pyl_path))
1501 if not settings.get('validate_source_side_specs_have_builder', True):
1502 return None
1503
Nico Weberd18b8962018-05-16 19:39:381504 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311505 milo_configs = glob.glob(
1506 os.path.join(self.args.infra_config_dir, 'generated', 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431507 for c in milo_configs:
1508 for l in self.read_file(c).splitlines():
1509 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311510 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431511 continue
1512 # l looks like
1513 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1514 # Extract win_chromium_dbg_ng part.
1515 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381516 return bot_names
1517
Ben Pastene9a010082019-09-25 20:41:371518 def get_builders_that_do_not_actually_exist(self):
Kenneth Russell8a386d42018-06-02 09:48:011519 # Some of the bots on the chromium.gpu.fyi waterfall in particular
1520 # are defined only to be mirrored into trybots, and don't actually
1521 # exist on any of the waterfalls or consoles.
1522 return [
Yuke Liao8373de52020-08-14 18:30:541523 'GPU FYI Fuchsia Builder',
1524 'ANGLE GPU Android Release (Nexus 5X)',
1525 'ANGLE GPU Linux Release (Intel HD 630)',
1526 'ANGLE GPU Linux Release (NVIDIA)',
Yuke Liao8373de52020-08-14 18:30:541527 'Optional Android Release (Nexus 5X)',
Brian Sheedy9584d812021-05-26 02:07:251528 'Optional Android Release (Pixel 4)',
Yuke Liao8373de52020-08-14 18:30:541529 'Optional Linux Release (Intel HD 630)',
1530 'Optional Linux Release (NVIDIA)',
1531 'Optional Mac Release (Intel)',
1532 'Optional Mac Retina Release (AMD)',
1533 'Optional Mac Retina Release (NVIDIA)',
1534 'Optional Win10 x64 Release (Intel HD 630)',
1535 'Optional Win10 x64 Release (NVIDIA)',
Yuke Liao8373de52020-08-14 18:30:541536 # chromium.fyi
1537 'linux-blink-rel-dummy',
1538 'linux-blink-optional-highdpi-rel-dummy',
1539 'mac10.12-blink-rel-dummy',
1540 'mac10.13-blink-rel-dummy',
1541 'mac10.14-blink-rel-dummy',
1542 'mac10.15-blink-rel-dummy',
Stephanie Kim7fbfd912020-08-21 21:11:001543 'mac11.0-blink-rel-dummy',
Preethi Mohan9c0fa2992021-08-17 17:25:451544 'mac11.0.arm64-blink-rel-dummy',
Yuke Liao8373de52020-08-14 18:30:541545 'win7-blink-rel-dummy',
1546 'win10-blink-rel-dummy',
Preethi Mohan47d03dc2021-06-28 23:08:021547 'win10.20h2-blink-rel-dummy',
Yuke Liao8373de52020-08-14 18:30:541548 'WebKit Linux composite_after_paint Dummy Builder',
1549 'WebKit Linux layout_ng_disabled Builder',
1550 # chromium, due to https://crbug.com/878915
1551 'win-dbg',
1552 'win32-dbg',
1553 'win-archive-dbg',
1554 'win32-archive-dbg',
Stephanie Kim107c1b0e2020-11-18 21:49:411555 # TODO crbug.com/1143924: Remove once experimentation is complete
1556 'Linux Builder Robocrop',
1557 'Linux Tests Robocrop',
Kenneth Russell8a386d42018-06-02 09:48:011558 ]
1559
Ben Pastene9a010082019-09-25 20:41:371560 def get_internal_waterfalls(self):
1561 # Similar to get_builders_that_do_not_actually_exist above, but for
1562 # waterfalls defined in internal configs.
Yuke Liaoe6c23dd2021-07-28 16:12:201563 return [
1564 'chrome', 'chrome.pgo', 'internal.chrome.fyi', 'internal.chromeos.fyi',
1565 'internal.soda'
1566 ]
Ben Pastene9a010082019-09-25 20:41:371567
Stephen Martinisf83893722018-09-19 00:02:181568 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201569 self.check_input_files_sorting(verbose)
1570
Kenneth Russelleb60cbd22017-12-05 07:54:281571 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011572 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381573 self.check_composition_type_test_suites('matrix_compound_suites',
1574 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111575 self.resolve_test_id_prefixes()
Stephen Martinis54d64ad2018-09-21 22:16:201576 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381577
1578 # All bots should exist.
1579 bot_names = self.get_valid_bot_names()
Ben Pastene9a010082019-09-25 20:41:371580 builders_that_dont_exist = self.get_builders_that_do_not_actually_exist()
Garrett Beaty2a02de3c2020-05-15 13:57:351581 if bot_names is not None:
1582 internal_waterfalls = self.get_internal_waterfalls()
1583 for waterfall in self.waterfalls:
1584 # TODO(crbug.com/991417): Remove the need for this exception.
1585 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011586 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351587 for bot_name in waterfall['machines']:
1588 if bot_name in builders_that_dont_exist:
Kenneth Russell78fd8702018-05-17 01:15:521589 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351590 if bot_name not in bot_names:
1591 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
1592 # TODO(thakis): Remove this once these bots move to luci.
1593 continue # pragma: no cover
1594 if waterfall['name'] in ['tryserver.webrtc',
1595 'webrtc.chromium.fyi.experimental']:
1596 # These waterfalls have their bot configs in a different repo.
1597 # so we don't know about their bot names.
1598 continue # pragma: no cover
1599 if waterfall['name'] in ['client.devtools-frontend.integration',
1600 'tryserver.devtools-frontend',
1601 'chromium.devtools-frontend']:
1602 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201603 if waterfall['name'] in ['client.openscreen.chromium']:
1604 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351605 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381606
Kenneth Russelleb60cbd22017-12-05 07:54:281607 # All test suites must be referenced.
1608 suites_seen = set()
1609 generator_map = self.get_test_generator_map()
1610 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231611 for bot_name, tester in waterfall['machines'].items():
1612 for suite_type, suite in tester.get('test_suites', {}).items():
Kenneth Russelleb60cbd22017-12-05 07:54:281613 if suite_type not in generator_map:
1614 raise self.unknown_test_suite_type(suite_type, bot_name,
1615 waterfall['name'])
1616 if suite not in self.test_suites:
1617 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1618 suites_seen.add(suite)
1619 # Since we didn't resolve the configuration files, this set
1620 # includes both composition test suites and regular ones.
1621 resolved_suites = set()
1622 for suite_name in suites_seen:
1623 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011624 for sub_suite in suite:
1625 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281626 resolved_suites.add(suite_name)
1627 # At this point, every key in test_suites.pyl should be referenced.
1628 missing_suites = set(self.test_suites.keys()) - resolved_suites
1629 if missing_suites:
1630 raise BBGenErr('The following test suites were unreferenced by bots on '
1631 'the waterfalls: ' + str(missing_suites))
1632
1633 # All test suite exceptions must refer to bots on the waterfall.
1634 all_bots = set()
1635 missing_bots = set()
1636 for waterfall in self.waterfalls:
Jamie Madillcf4f8c72021-05-20 19:24:231637 for bot_name, tester in waterfall['machines'].items():
Kenneth Russelleb60cbd22017-12-05 07:54:281638 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281639 # In order to disambiguate between bots with the same name on
1640 # different waterfalls, support has been added to various
1641 # exceptions for concatenating the waterfall name after the bot
1642 # name.
1643 all_bots.add(bot_name + ' ' + waterfall['name'])
Jamie Madillcf4f8c72021-05-20 19:24:231644 for exception in self.exceptions.values():
Nico Weberd18b8962018-05-16 19:39:381645 removals = (exception.get('remove_from', []) +
1646 exception.get('remove_gtest_from', []) +
Jamie Madillcf4f8c72021-05-20 19:24:231647 list(exception.get('modifications', {}).keys()))
Nico Weberd18b8962018-05-16 19:39:381648 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281649 if removal not in all_bots:
1650 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411651
Ben Pastene9a010082019-09-25 20:41:371652 missing_bots = missing_bots - set(builders_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281653 if missing_bots:
1654 raise BBGenErr('The following nonexistent machines were referenced in '
1655 'the test suite exceptions: ' + str(missing_bots))
1656
Stephen Martinis0382bc12018-09-17 22:29:071657 # All mixins must be referenced
1658 seen_mixins = set()
1659 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011660 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Jamie Madillcf4f8c72021-05-20 19:24:231661 for bot_name, tester in waterfall['machines'].items():
Stephen Martinisb72f6d22018-10-04 23:29:011662 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071663 for suite in self.test_suites.values():
1664 if isinstance(suite, list):
1665 # Don't care about this, it's a composition, which shouldn't include a
1666 # swarming mixin.
1667 continue
1668
1669 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561670 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011671 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071672
Zhaoyang Li9da047d52021-05-10 21:31:441673 for variant in self.variants:
1674 # Unpack the variant from variants.pyl if it's string based.
1675 if isinstance(variant, str):
1676 variant = self.variants[variant]
1677 seen_mixins = seen_mixins.union(variant.get('mixins', set()))
1678
Stephen Martinisb72f6d22018-10-04 23:29:011679 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071680 if missing_mixins:
1681 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1682 ' referenced in a waterfall, machine, or test suite.' % (
1683 str(missing_mixins)))
1684
Jeff Yoonda581c32020-03-06 03:56:051685 # All variant references must be referenced
1686 seen_variants = set()
1687 for suite in self.test_suites.values():
1688 if isinstance(suite, list):
1689 continue
1690
1691 for test in suite.values():
1692 if isinstance(test, dict):
1693 for variant in test.get('variants', []):
1694 if isinstance(variant, str):
1695 seen_variants.add(variant)
1696
1697 missing_variants = set(self.variants.keys()) - seen_variants
1698 if missing_variants:
1699 raise BBGenErr('The following variants were unreferenced: %s. They must '
1700 'be referenced in a matrix test suite under the variants '
1701 'key.' % str(missing_variants))
1702
Stephen Martinis54d64ad2018-09-21 22:16:201703
1704 def type_assert(self, node, typ, filename, verbose=False):
1705 """Asserts that the Python AST node |node| is of type |typ|.
1706
1707 If verbose is set, it prints out some helpful context lines, showing where
1708 exactly the error occurred in the file.
1709 """
1710 if not isinstance(node, typ):
1711 if verbose:
1712 lines = [""] + self.read_file(filename).splitlines()
1713
1714 context = 2
1715 lines_start = max(node.lineno - context, 0)
1716 # Add one to include the last line
1717 lines_end = min(node.lineno + context, len(lines)) + 1
1718 lines = (
1719 ['== %s ==\n' % filename] +
1720 ["<snip>\n"] +
1721 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1722 lines[lines_start:lines_start + context])] +
1723 ['-' * 80 + '\n'] +
1724 ['%d %s' % (node.lineno, lines[node.lineno])] +
1725 ['-' * (node.col_offset + 3) + '^' + '-' * (
1726 80 - node.col_offset - 4) + '\n'] +
1727 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1728 lines[node.lineno + 1:lines_end])] +
1729 ["<snip>\n"]
1730 )
1731 # Print out a useful message when a type assertion fails.
1732 for l in lines:
1733 self.print_line(l.strip())
1734
1735 node_dumped = ast.dump(node, annotate_fields=False)
1736 # If the node is huge, truncate it so everything fits in a terminal
1737 # window.
1738 if len(node_dumped) > 60: # pragma: no cover
1739 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1740 raise BBGenErr(
1741 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1742 ' be %s, is %s' % (
1743 filename, node_dumped,
1744 node.lineno, typ, type(node)))
1745
Stephen Martinis5bef0fc2020-01-06 22:47:531746 def check_ast_list_formatted(self, keys, filename, verbose,
Stephen Martinis1384ff92020-01-07 19:52:151747 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531748 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201749
Stephen Martinis5bef0fc2020-01-06 22:47:531750 Currently only checks to ensure they're correctly sorted, and that there
1751 are no duplicates.
1752
1753 Args:
1754 keys: An python list of AST nodes.
1755
1756 It's a list of AST nodes instead of a list of strings because
1757 when verbose is set, it tries to print out context of where the
1758 diffs are in the file.
1759 filename: The name of the file this node is from.
1760 verbose: If set, print out diff information about how the keys are
1761 incorrectly formatted.
1762 check_sorting: If true, checks if the list is sorted.
1763 Returns:
1764 If the keys are correctly formatted.
1765 """
1766 if not keys:
1767 return True
1768
1769 assert isinstance(keys[0], ast.Str)
1770
1771 keys_strs = [k.s for k in keys]
1772 # Keys to diff against. Used below.
1773 keys_to_diff_against = None
1774 # If the list is properly formatted.
1775 list_formatted = True
1776
1777 # Duplicates are always bad.
1778 if len(set(keys_strs)) != len(keys_strs):
1779 list_formatted = False
1780 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1781
1782 if check_sorting and sorted(keys_strs) != keys_strs:
1783 list_formatted = False
1784 if list_formatted:
1785 return True
1786
1787 if verbose:
1788 line_num = keys[0].lineno
1789 keys = [k.s for k in keys]
1790 if check_sorting:
1791 # If we have duplicates, sorting this will take care of it anyways.
1792 keys_to_diff_against = sorted(set(keys))
1793 # else, keys_to_diff_against is set above already
1794
1795 self.print_line('=' * 80)
1796 self.print_line('(First line of keys is %s)' % line_num)
1797 for line in difflib.context_diff(
1798 keys, keys_to_diff_against,
1799 fromfile='current (%r)' % filename, tofile='sorted', lineterm=''):
1800 self.print_line(line)
1801 self.print_line('=' * 80)
1802
1803 return False
1804
Stephen Martinis1384ff92020-01-07 19:52:151805 def check_ast_dict_formatted(self, node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531806 """Checks if an ast dictionary's keys are correctly formatted.
1807
1808 Just a simple wrapper around check_ast_list_formatted.
1809 Args:
1810 node: An AST node. Assumed to be a dictionary.
1811 filename: The name of the file this node is from.
1812 verbose: If set, print out diff information about how the keys are
1813 incorrectly formatted.
1814 check_sorting: If true, checks if the list is sorted.
1815 Returns:
1816 If the dictionary is correctly formatted.
1817 """
Stephen Martinis54d64ad2018-09-21 22:16:201818 keys = []
1819 # The keys of this dict are ordered as ordered in the file; normal python
1820 # dictionary keys are given an arbitrary order, but since we parsed the
1821 # file itself, the order as given in the file is preserved.
1822 for key in node.keys:
1823 self.type_assert(key, ast.Str, filename, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531824 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201825
Stephen Martinis1384ff92020-01-07 19:52:151826 return self.check_ast_list_formatted(keys, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181827
1828 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201829 # TODO(https://crbug.com/886993): Add the ability for this script to
1830 # actually format the files, rather than just complain if they're
1831 # incorrectly formatted.
1832 bad_files = set()
Stephen Martinis5bef0fc2020-01-06 22:47:531833 def parse_file(filename):
1834 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201835
Stephen Martinis5bef0fc2020-01-06 22:47:531836 Returns an AST node representing the value in the pyl file."""
Stephen Martinisf83893722018-09-19 00:02:181837 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1838
Stephen Martinisf83893722018-09-19 00:02:181839 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201840 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181841 module = parsed.body
1842
1843 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201844 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181845 if len(module) != 1: # pragma: no cover
1846 raise BBGenErr('Invalid .pyl file %s' % filename)
1847 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201848 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181849
Stephen Martinis5bef0fc2020-01-06 22:47:531850 return expr.value
1851
1852 # Handle this separately
1853 filename = 'waterfalls.pyl'
1854 value = parse_file(filename)
1855 # Value should be a list.
1856 self.type_assert(value, ast.List, filename, verbose)
1857
1858 keys = []
1859 for val in value.elts:
1860 self.type_assert(val, ast.Dict, filename, verbose)
1861 waterfall_name = None
1862 for key, val in zip(val.keys, val.values):
1863 self.type_assert(key, ast.Str, filename, verbose)
1864 if key.s == 'machines':
1865 if not self.check_ast_dict_formatted(val, filename, verbose):
1866 bad_files.add(filename)
1867
1868 if key.s == "name":
1869 self.type_assert(val, ast.Str, filename, verbose)
1870 waterfall_name = val
1871 assert waterfall_name
1872 keys.append(waterfall_name)
1873
Stephen Martinis1384ff92020-01-07 19:52:151874 if not self.check_ast_list_formatted(keys, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531875 bad_files.add(filename)
1876
1877 for filename in (
1878 'mixins.pyl',
1879 'test_suites.pyl',
1880 'test_suite_exceptions.pyl',
1881 ):
1882 value = parse_file(filename)
Stephen Martinisf83893722018-09-19 00:02:181883 # Value should be a dictionary.
Stephen Martinis54d64ad2018-09-21 22:16:201884 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181885
Stephen Martinis5bef0fc2020-01-06 22:47:531886 if not self.check_ast_dict_formatted(
1887 value, filename, verbose):
1888 bad_files.add(filename)
1889
Stephen Martinis54d64ad2018-09-21 22:16:201890 if filename == 'test_suites.pyl':
Jeff Yoon8154e582019-12-03 23:30:011891 expected_keys = ['basic_suites',
1892 'compound_suites',
1893 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201894 actual_keys = [node.s for node in value.keys]
1895 assert all(key in expected_keys for key in actual_keys), (
1896 'Invalid %r file; expected keys %r, got %r' % (
1897 filename, expected_keys, actual_keys))
1898 suite_dicts = [node for node in value.values]
1899 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011900 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201901 for suite_group in suite_dicts:
Stephen Martinis5bef0fc2020-01-06 22:47:531902 if not self.check_ast_dict_formatted(
Stephen Martinis54d64ad2018-09-21 22:16:201903 suite_group, filename, verbose):
1904 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181905
Stephen Martinis5bef0fc2020-01-06 22:47:531906 for key, suite in zip(value.keys, value.values):
1907 # The compound suites are checked in
1908 # 'check_composition_type_test_suites()'
1909 if key.s == 'basic_suites':
1910 for group in suite.values:
Stephen Martinis1384ff92020-01-07 19:52:151911 if not self.check_ast_dict_formatted(group, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531912 bad_files.add(filename)
1913 break
Stephen Martinis54d64ad2018-09-21 22:16:201914
Stephen Martinis5bef0fc2020-01-06 22:47:531915 elif filename == 'test_suite_exceptions.pyl':
1916 # Check the values for each test.
1917 for test in value.values:
1918 for kind, node in zip(test.keys, test.values):
1919 if isinstance(node, ast.Dict):
Stephen Martinis1384ff92020-01-07 19:52:151920 if not self.check_ast_dict_formatted(node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531921 bad_files.add(filename)
1922 elif kind.s == 'remove_from':
1923 # Don't care about sorting; these are usually grouped, since the
1924 # same bug can affect multiple builders. Do want to make sure
1925 # there aren't duplicates.
1926 if not self.check_ast_list_formatted(node.elts, filename, verbose,
1927 check_sorting=False):
1928 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181929
1930 if bad_files:
1931 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201932 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531933 'unsorted, or have duplicates. Re-run this with --verbose to see '
1934 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181935
Kenneth Russelleb60cbd22017-12-05 07:54:281936 def check_output_file_consistency(self, verbose=False):
1937 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011938 # All waterfalls/bucket .json files must have been written
1939 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:281940 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011941 ungenerated_files = set()
Dirk Pranke772f55f2021-04-28 04:51:161942 outputs = self.generate_outputs()
1943 for filename, expected_contents in outputs.items():
Greg Gutermanf60eb052020-03-12 17:40:011944 expected = self.jsonify(expected_contents)
1945 file_path = filename + '.json'
Zhiling Huangbe008172018-03-08 19:13:111946 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281947 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:011948 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:321949 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:011950 self.print_line('File ' + filename +
1951 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321952 'contents:')
1953 for line in difflib.unified_diff(
1954 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501955 current.splitlines(),
1956 fromfile='expected', tofile='current'):
1957 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:011958
1959 if ungenerated_files:
1960 raise BBGenErr(
1961 'The following files have not been properly '
1962 'autogenerated by generate_buildbot_json.py: ' +
1963 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:281964
Dirk Pranke772f55f2021-04-28 04:51:161965 for builder_group, builders in outputs.items():
1966 for builder, step_types in builders.items():
1967 for step_data in step_types.get('gtest_tests', []):
1968 step_name = step_data.get('name', step_data['test'])
1969 self._check_swarming_config(builder_group, builder, step_name,
1970 step_data)
1971 for step_data in step_types.get('isolated_scripts', []):
1972 step_name = step_data.get('name', step_data['isolate_name'])
1973 self._check_swarming_config(builder_group, builder, step_name,
1974 step_data)
1975
1976 def _check_swarming_config(self, filename, builder, step_name, step_data):
1977 # TODO(crbug.com/1203436): Ensure all swarming tests specify os and cpu, not
1978 # just mac tests.
1979 if ('mac' in builder.lower()
1980 and step_data['swarming']['can_use_on_swarming_builders']):
1981 dimension_sets = step_data['swarming'].get('dimension_sets')
1982 if not dimension_sets:
1983 raise BBGenErr('%s: %s / %s : os and cpu must be specified for mac '
1984 'swarmed tests' % (filename, builder, step_name))
1985 for s in dimension_sets:
1986 if not s.get('os') or not s.get('cpu'):
1987 raise BBGenErr('%s: %s / %s : os and cpu must be specified for mac '
1988 'swarmed tests' % (filename, builder, step_name))
1989
Kenneth Russelleb60cbd22017-12-05 07:54:281990 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501991 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281992 self.check_output_file_consistency(verbose) # pragma: no cover
1993
Karen Qiane24b7ee2019-02-12 23:37:061994 def does_test_match(self, test_info, params_dict):
1995 """Checks to see if the test matches the parameters given.
1996
1997 Compares the provided test_info with the params_dict to see
1998 if the bot matches the parameters given. If so, returns True.
1999 Else, returns false.
2000
2001 Args:
2002 test_info (dict): Information about a specific bot provided
2003 in the format shown in waterfalls.pyl
2004 params_dict (dict): Dictionary of parameters and their values
2005 to look for in the bot
2006 Ex: {
2007 'device_os':'android',
2008 '--flag':True,
2009 'mixins': ['mixin1', 'mixin2'],
2010 'ex_key':'ex_value'
2011 }
2012
2013 """
2014 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
2015 'kvm', 'pool', 'integrity'] # dimension parameters
2016 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
2017 'can_use_on_swarming_builders']
2018 for param in params_dict:
2019 # if dimension parameter
2020 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
2021 if not 'swarming' in test_info:
2022 return False
2023 swarming = test_info['swarming']
2024 if param in SWARMING_PARAMS:
2025 if not param in swarming:
2026 return False
2027 if not str(swarming[param]) == params_dict[param]:
2028 return False
2029 else:
2030 if not 'dimension_sets' in swarming:
2031 return False
2032 d_set = swarming['dimension_sets']
2033 # only looking at the first dimension set
2034 if not param in d_set[0]:
2035 return False
2036 if not d_set[0][param] == params_dict[param]:
2037 return False
2038
2039 # if flag
2040 elif param.startswith('--'):
2041 if not 'args' in test_info:
2042 return False
2043 if not param in test_info['args']:
2044 return False
2045
2046 # not dimension parameter/flag/mixin
2047 else:
2048 if not param in test_info:
2049 return False
2050 if not test_info[param] == params_dict[param]:
2051 return False
2052 return True
2053 def error_msg(self, msg):
2054 """Prints an error message.
2055
2056 In addition to a catered error message, also prints
2057 out where the user can find more help. Then, program exits.
2058 """
2059 self.print_line(msg + (' If you need more information, ' +
2060 'please run with -h or --help to see valid commands.'))
2061 sys.exit(1)
2062
2063 def find_bots_that_run_test(self, test, bots):
2064 matching_bots = []
2065 for bot in bots:
2066 bot_info = bots[bot]
2067 tests = self.flatten_tests_for_bot(bot_info)
2068 for test_info in tests:
2069 test_name = ""
2070 if 'name' in test_info:
2071 test_name = test_info['name']
2072 elif 'test' in test_info:
2073 test_name = test_info['test']
2074 if not test_name == test:
2075 continue
2076 matching_bots.append(bot)
2077 return matching_bots
2078
2079 def find_tests_with_params(self, tests, params_dict):
2080 matching_tests = []
2081 for test_name in tests:
2082 test_info = tests[test_name]
2083 if not self.does_test_match(test_info, params_dict):
2084 continue
2085 if not test_name in matching_tests:
2086 matching_tests.append(test_name)
2087 return matching_tests
2088
2089 def flatten_waterfalls_for_query(self, waterfalls):
2090 bots = {}
2091 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:012092 waterfall_tests = self.generate_output_tests(waterfall)
2093 for bot in waterfall_tests:
2094 bot_info = waterfall_tests[bot]
2095 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:062096 return bots
2097
2098 def flatten_tests_for_bot(self, bot_info):
2099 """Returns a list of flattened tests.
2100
2101 Returns a list of tests not grouped by test category
2102 for a specific bot.
2103 """
2104 TEST_CATS = self.get_test_generator_map().keys()
2105 tests = []
2106 for test_cat in TEST_CATS:
2107 if not test_cat in bot_info:
2108 continue
2109 test_cat_tests = bot_info[test_cat]
2110 tests = tests + test_cat_tests
2111 return tests
2112
2113 def flatten_tests_for_query(self, test_suites):
2114 """Returns a flattened dictionary of tests.
2115
2116 Returns a dictionary of tests associate with their
2117 configuration, not grouped by their test suite.
2118 """
2119 tests = {}
Jamie Madillcf4f8c72021-05-20 19:24:232120 for test_suite in test_suites.values():
Karen Qiane24b7ee2019-02-12 23:37:062121 for test in test_suite:
2122 test_info = test_suite[test]
2123 test_name = test
2124 if 'name' in test_info:
2125 test_name = test_info['name']
2126 tests[test_name] = test_info
2127 return tests
2128
2129 def parse_query_filter_params(self, params):
2130 """Parses the filter parameters.
2131
2132 Creates a dictionary from the parameters provided
2133 to filter the bot array.
2134 """
2135 params_dict = {}
2136 for p in params:
2137 # flag
2138 if p.startswith("--"):
2139 params_dict[p] = True
2140 else:
2141 pair = p.split(":")
2142 if len(pair) != 2:
2143 self.error_msg('Invalid command.')
2144 # regular parameters
2145 if pair[1].lower() == "true":
2146 params_dict[pair[0]] = True
2147 elif pair[1].lower() == "false":
2148 params_dict[pair[0]] = False
2149 else:
2150 params_dict[pair[0]] = pair[1]
2151 return params_dict
2152
2153 def get_test_suites_dict(self, bots):
2154 """Returns a dictionary of bots and their tests.
2155
2156 Returns a dictionary of bots and a list of their associated tests.
2157 """
2158 test_suite_dict = dict()
2159 for bot in bots:
2160 bot_info = bots[bot]
2161 tests = self.flatten_tests_for_bot(bot_info)
2162 test_suite_dict[bot] = tests
2163 return test_suite_dict
2164
2165 def output_query_result(self, result, json_file=None):
2166 """Outputs the result of the query.
2167
2168 If a json file parameter name is provided, then
2169 the result is output into the json file. If not,
2170 then the result is printed to the console.
2171 """
2172 output = json.dumps(result, indent=2)
2173 if json_file:
2174 self.write_file(json_file, output)
2175 else:
2176 self.print_line(output)
2177 return
2178
2179 def query(self, args):
2180 """Queries tests or bots.
2181
2182 Depending on the arguments provided, outputs a json of
2183 tests or bots matching the appropriate optional parameters provided.
2184 """
2185 # split up query statement
2186 query = args.query.split('/')
2187 self.load_configuration_files()
2188 self.resolve_configuration_files()
2189
2190 # flatten bots json
2191 tests = self.test_suites
2192 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2193
2194 cmd_class = query[0]
2195
2196 # For queries starting with 'bots'
2197 if cmd_class == "bots":
2198 if len(query) == 1:
2199 return self.output_query_result(bots, args.json)
2200 # query with specific parameters
2201 elif len(query) == 2:
2202 if query[1] == 'tests':
2203 test_suites_dict = self.get_test_suites_dict(bots)
2204 return self.output_query_result(test_suites_dict, args.json)
2205 else:
2206 self.error_msg("This query should be in the format: bots/tests.")
2207
2208 else:
2209 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2210 % str(len(query)-1))
2211
2212 # For queries starting with 'bot'
2213 elif cmd_class == "bot":
2214 if not len(query) == 2 and not len(query) == 3:
2215 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2216 % str(len(query)-1))
2217 bot_id = query[1]
2218 if not bot_id in bots:
2219 self.error_msg("No bot named '" + bot_id + "' found.")
2220 bot_info = bots[bot_id]
2221 if len(query) == 2:
2222 return self.output_query_result(bot_info, args.json)
2223 if not query[2] == 'tests':
2224 self.error_msg("The query should be in the format:" +
2225 "bot/<bot-name>/tests.")
2226
2227 bot_tests = self.flatten_tests_for_bot(bot_info)
2228 return self.output_query_result(bot_tests, args.json)
2229
2230 # For queries starting with 'tests'
2231 elif cmd_class == "tests":
2232 if not len(query) == 1 and not len(query) == 2:
2233 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2234 % str(len(query)-1))
2235 flattened_tests = self.flatten_tests_for_query(tests)
2236 if len(query) == 1:
2237 return self.output_query_result(flattened_tests, args.json)
2238
2239 # create params dict
2240 params = query[1].split('&')
2241 params_dict = self.parse_query_filter_params(params)
2242 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2243 return self.output_query_result(matching_bots)
2244
2245 # For queries starting with 'test'
2246 elif cmd_class == "test":
2247 if not len(query) == 2 and not len(query) == 3:
2248 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2249 % str(len(query)-1))
2250 test_id = query[1]
2251 if len(query) == 2:
2252 flattened_tests = self.flatten_tests_for_query(tests)
2253 for test in flattened_tests:
2254 if test == test_id:
2255 return self.output_query_result(flattened_tests[test], args.json)
2256 self.error_msg("There is no test named %s." % test_id)
2257 if not query[2] == 'bots':
2258 self.error_msg("The query should be in the format: " +
2259 "test/<test-name>/bots")
2260 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2261 return self.output_query_result(bots_for_test)
2262
2263 else:
2264 self.error_msg("Your command did not match any valid commands." +
2265 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:282266
Garrett Beaty1afaccc2020-06-25 19:58:152267 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282268 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502269 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062270 elif self.args.query:
2271 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282272 else:
Greg Gutermanf60eb052020-03-12 17:40:012273 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282274 return 0
2275
2276if __name__ == "__main__": # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152277 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2278 sys.exit(generator.main())