blob: 371cde0e2cdcd5ab0612378c9772b9951bed805f [file] [log] [blame]
Kenneth Russelleb60cbd22017-12-05 07:54:281#!/usr/bin/env python
2# 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
Garrett Beatyd5ca75962020-05-07 16:58:3115import glob
Kenneth Russell8ceeabf2017-12-11 17:53:2816import itertools
Kenneth Russelleb60cbd22017-12-05 07:54:2817import json
18import os
Greg Gutermanf60eb052020-03-12 17:40:0119import re
Kenneth Russelleb60cbd22017-12-05 07:54:2820import string
21import sys
John Budorick826d5ed2017-12-28 19:27:3222import traceback
Kenneth Russelleb60cbd22017-12-05 07:54:2823
Brian Sheedya31578e2020-05-18 20:24:3624import buildbot_json_magic_substitutions as magic_substitutions
25
Kenneth Russelleb60cbd22017-12-05 07:54:2826THIS_DIR = os.path.dirname(os.path.abspath(__file__))
27
28
29class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4930 def __init__(self, message):
31 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2832
33
Kenneth Russell8ceeabf2017-12-11 17:53:2834# This class is only present to accommodate certain machines on
35# chromium.android.fyi which run certain tests as instrumentation
36# tests, but not as gtests. If this discrepancy were fixed then the
37# notion could be removed.
38class TestSuiteTypes(object):
39 GTEST = 'gtest'
40
41
Kenneth Russelleb60cbd22017-12-05 07:54:2842class BaseGenerator(object):
43 def __init__(self, bb_gen):
44 self.bb_gen = bb_gen
45
Kenneth Russell8ceeabf2017-12-11 17:53:2846 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2847 raise NotImplementedError()
48
49 def sort(self, tests):
50 raise NotImplementedError()
51
52
Kenneth Russell8ceeabf2017-12-11 17:53:2853def cmp_tests(a, b):
54 # Prefer to compare based on the "test" key.
55 val = cmp(a['test'], b['test'])
56 if val != 0:
57 return val
58 if 'name' in a and 'name' in b:
59 return cmp(a['name'], b['name']) # pragma: no cover
60 if 'name' not in a and 'name' not in b:
61 return 0 # pragma: no cover
62 # Prefer to put variants of the same test after the first one.
63 if 'name' in a:
64 return 1
65 # 'name' is in b.
66 return -1 # pragma: no cover
67
68
Kenneth Russell8a386d42018-06-02 09:48:0169class GPUTelemetryTestGenerator(BaseGenerator):
Bo Liu555a0f92019-03-29 12:11:5670
71 def __init__(self, bb_gen, is_android_webview=False):
Kenneth Russell8a386d42018-06-02 09:48:0172 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5673 self._is_android_webview = is_android_webview
Kenneth Russell8a386d42018-06-02 09:48:0174
75 def generate(self, waterfall, tester_name, tester_config, input_tests):
76 isolated_scripts = []
77 for test_name, test_config in sorted(input_tests.iteritems()):
78 test = self.bb_gen.generate_gpu_telemetry_test(
Bo Liu555a0f92019-03-29 12:11:5679 waterfall, tester_name, tester_config, test_name, test_config,
80 self._is_android_webview)
Kenneth Russell8a386d42018-06-02 09:48:0181 if test:
82 isolated_scripts.append(test)
83 return isolated_scripts
84
85 def sort(self, tests):
86 return sorted(tests, key=lambda x: x['name'])
87
88
Kenneth Russelleb60cbd22017-12-05 07:54:2889class GTestGenerator(BaseGenerator):
90 def __init__(self, bb_gen):
91 super(GTestGenerator, self).__init__(bb_gen)
92
Kenneth Russell8ceeabf2017-12-11 17:53:2893 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2894 # The relative ordering of some of the tests is important to
95 # minimize differences compared to the handwritten JSON files, since
96 # Python's sorts are stable and there are some tests with the same
97 # key (see gles2_conform_d3d9_test and similar variants). Avoid
98 # losing the order by avoiding coalescing the dictionaries into one.
99 gtests = []
100 for test_name, test_config in sorted(input_tests.iteritems()):
Jeff Yoon67c3e832020-02-08 07:39:38101 # Variants allow more than one definition for a given test, and is defined
102 # in array format from resolve_variants().
103 if not isinstance(test_config, list):
104 test_config = [test_config]
105
106 for config in test_config:
107 test = self.bb_gen.generate_gtest(
108 waterfall, tester_name, tester_config, test_name, config)
109 if test:
110 # generate_gtest may veto the test generation on this tester.
111 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28112 return gtests
113
114 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28115 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28116
117
118class IsolatedScriptTestGenerator(BaseGenerator):
119 def __init__(self, bb_gen):
120 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
121
Kenneth Russell8ceeabf2017-12-11 17:53:28122 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28123 isolated_scripts = []
124 for test_name, test_config in sorted(input_tests.iteritems()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43125 # Variants allow more than one definition for a given test, and is defined
126 # in array format from resolve_variants().
127 if not isinstance(test_config, list):
128 test_config = [test_config]
129
130 for config in test_config:
131 test = self.bb_gen.generate_isolated_script_test(
132 waterfall, tester_name, tester_config, test_name, config)
133 if test:
134 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28135 return isolated_scripts
136
137 def sort(self, tests):
138 return sorted(tests, key=lambda x: x['name'])
139
140
141class ScriptGenerator(BaseGenerator):
142 def __init__(self, bb_gen):
143 super(ScriptGenerator, self).__init__(bb_gen)
144
Kenneth Russell8ceeabf2017-12-11 17:53:28145 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28146 scripts = []
147 for test_name, test_config in sorted(input_tests.iteritems()):
148 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28149 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28150 if test:
151 scripts.append(test)
152 return scripts
153
154 def sort(self, tests):
155 return sorted(tests, key=lambda x: x['name'])
156
157
158class JUnitGenerator(BaseGenerator):
159 def __init__(self, bb_gen):
160 super(JUnitGenerator, self).__init__(bb_gen)
161
Kenneth Russell8ceeabf2017-12-11 17:53:28162 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28163 scripts = []
164 for test_name, test_config in sorted(input_tests.iteritems()):
165 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28166 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28167 if test:
168 scripts.append(test)
169 return scripts
170
171 def sort(self, tests):
172 return sorted(tests, key=lambda x: x['test'])
173
174
Jeff Yoon67c3e832020-02-08 07:39:38175def check_compound_references(other_test_suites=None,
176 sub_suite=None,
177 suite=None,
178 target_test_suites=None,
179 test_type=None,
180 **kwargs):
181 """Ensure comound reference's don't target other compounds"""
182 del kwargs
183 if sub_suite in other_test_suites or sub_suite in target_test_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15184 raise BBGenErr('%s may not refer to other composition type test '
185 'suites (error found while processing %s)' %
186 (test_type, suite))
187
Jeff Yoon67c3e832020-02-08 07:39:38188
189def check_basic_references(basic_suites=None,
190 sub_suite=None,
191 suite=None,
192 **kwargs):
193 """Ensure test has a basic suite reference"""
194 del kwargs
195 if sub_suite not in basic_suites:
Garrett Beaty1afaccc2020-06-25 19:58:15196 raise BBGenErr('Unable to find reference to %s while processing %s' %
197 (sub_suite, suite))
198
Jeff Yoon67c3e832020-02-08 07:39:38199
200def check_conflicting_definitions(basic_suites=None,
201 seen_tests=None,
202 sub_suite=None,
203 suite=None,
204 test_type=None,
205 **kwargs):
206 """Ensure that if a test is reachable via multiple basic suites,
207 all of them have an identical definition of the tests.
208 """
209 del kwargs
210 for test_name in basic_suites[sub_suite]:
211 if (test_name in seen_tests and
212 basic_suites[sub_suite][test_name] !=
213 basic_suites[seen_tests[test_name]][test_name]):
214 raise BBGenErr('Conflicting test definitions for %s from %s '
215 'and %s in %s (error found while processing %s)'
216 % (test_name, seen_tests[test_name], sub_suite,
217 test_type, suite))
218 seen_tests[test_name] = sub_suite
219
220def check_matrix_identifier(sub_suite=None,
221 suite=None,
222 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05223 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38224 **kwargs):
225 """Ensure 'idenfitier' is defined for each variant"""
226 del kwargs
227 sub_suite_config = suite_def[sub_suite]
228 for variant in sub_suite_config.get('variants', []):
Jeff Yoonda581c32020-03-06 03:56:05229 if isinstance(variant, str):
230 if variant not in all_variants:
231 raise BBGenErr('Missing variant definition for %s in variants.pyl'
232 % variant)
233 variant = all_variants[variant]
234
Jeff Yoon67c3e832020-02-08 07:39:38235 if not 'identifier' in variant:
236 raise BBGenErr('Missing required identifier field in matrix '
237 'compound suite %s, %s' % (suite, sub_suite))
238
239
Kenneth Russelleb60cbd22017-12-05 07:54:28240class BBJSONGenerator(object):
Garrett Beaty1afaccc2020-06-25 19:58:15241 def __init__(self, args):
Kenneth Russelleb60cbd22017-12-05 07:54:28242 self.this_dir = THIS_DIR
Garrett Beaty1afaccc2020-06-25 19:58:15243 self.args = args
Kenneth Russelleb60cbd22017-12-05 07:54:28244 self.waterfalls = None
245 self.test_suites = None
246 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01247 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41248 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05249 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28250
Garrett Beaty1afaccc2020-06-25 19:58:15251 @staticmethod
252 def parse_args(argv):
253
254 # RawTextHelpFormatter allows for styling of help statement
255 parser = argparse.ArgumentParser(
256 formatter_class=argparse.RawTextHelpFormatter)
257
258 group = parser.add_mutually_exclusive_group()
259 group.add_argument(
260 '-c',
261 '--check',
262 action='store_true',
263 help=
264 'Do consistency checks of configuration and generated files and then '
265 'exit. Used during presubmit. '
266 'Causes the tool to not generate any files.')
267 group.add_argument(
268 '--query',
269 type=str,
270 help=(
271 "Returns raw JSON information of buildbots and tests.\n" +
272 "Examples:\n" + " List all bots (all info):\n" +
273 " --query bots\n\n" +
274 " List all bots and only their associated tests:\n" +
275 " --query bots/tests\n\n" +
276 " List all information about 'bot1' " +
277 "(make sure you have quotes):\n" + " --query bot/'bot1'\n\n" +
278 " List tests running for 'bot1' (make sure you have quotes):\n" +
279 " --query bot/'bot1'/tests\n\n" + " List all tests:\n" +
280 " --query tests\n\n" +
281 " List all tests and the bots running them:\n" +
282 " --query tests/bots\n\n" +
283 " List all tests that satisfy multiple parameters\n" +
284 " (separation of parameters by '&' symbol):\n" +
285 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
286 " List all tests that run with a specific flag:\n" +
287 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
288 " List specific test (make sure you have quotes):\n"
289 " --query test/'test1'\n\n"
290 " List all bots running 'test1' " +
291 "(make sure you have quotes):\n" + " --query test/'test1'/bots"))
292 parser.add_argument(
293 '-n',
294 '--new-files',
295 action='store_true',
296 help=
297 'Write output files as .new.json. Useful during development so old and '
298 'new files can be looked at side-by-side.')
299 parser.add_argument('-v',
300 '--verbose',
301 action='store_true',
302 help='Increases verbosity. Affects consistency checks.')
303 parser.add_argument('waterfall_filters',
304 metavar='waterfalls',
305 type=str,
306 nargs='*',
307 help='Optional list of waterfalls to generate.')
308 parser.add_argument(
309 '--pyl-files-dir',
310 type=os.path.realpath,
311 help='Path to the directory containing the input .pyl files.')
312 parser.add_argument(
313 '--json',
314 metavar='JSON_FILE_PATH',
315 help='Outputs results into a json file. Only works with query function.'
316 )
317 parser.add_argument(
318 '--infra-config-dir',
319 help='Path to the LUCI services configuration directory',
320 default=os.path.abspath(
321 os.path.join(os.path.dirname(__file__), '..', '..', 'infra',
322 'config')))
323 args = parser.parse_args(argv)
324 if args.json and not args.query:
325 parser.error(
326 "The --json flag can only be used with --query.") # pragma: no cover
327 args.infra_config_dir = os.path.abspath(args.infra_config_dir)
328 return args
329
Kenneth Russelleb60cbd22017-12-05 07:54:28330 def generate_abs_file_path(self, relative_path):
Garrett Beaty1afaccc2020-06-25 19:58:15331 return os.path.join(self.this_dir, relative_path)
Kenneth Russelleb60cbd22017-12-05 07:54:28332
Stephen Martinis7eb8b612018-09-21 00:17:50333 def print_line(self, line):
334 # Exists so that tests can mock
335 print line # pragma: no cover
336
Kenneth Russelleb60cbd22017-12-05 07:54:28337 def read_file(self, relative_path):
Garrett Beaty1afaccc2020-06-25 19:58:15338 with open(self.generate_abs_file_path(relative_path)) as fp:
339 return fp.read()
Kenneth Russelleb60cbd22017-12-05 07:54:28340
341 def write_file(self, relative_path, contents):
Garrett Beaty1afaccc2020-06-25 19:58:15342 with open(self.generate_abs_file_path(relative_path), 'wb') as fp:
343 fp.write(contents)
Kenneth Russelleb60cbd22017-12-05 07:54:28344
Zhiling Huangbe008172018-03-08 19:13:11345 def pyl_file_path(self, filename):
346 if self.args and self.args.pyl_files_dir:
347 return os.path.join(self.args.pyl_files_dir, filename)
348 return filename
349
Kenneth Russelleb60cbd22017-12-05 07:54:28350 def load_pyl_file(self, filename):
351 try:
Zhiling Huangbe008172018-03-08 19:13:11352 return ast.literal_eval(self.read_file(
353 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28354 except (SyntaxError, ValueError) as e: # pragma: no cover
355 raise BBGenErr('Failed to parse pyl file "%s": %s' %
356 (filename, e)) # pragma: no cover
357
Kenneth Russell8a386d42018-06-02 09:48:01358 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
359 # Currently it is only mandatory for bots which run GPU tests. Change these to
360 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28361 def is_android(self, tester_config):
362 return tester_config.get('os_type') == 'android'
363
Ben Pastenea9e583b2019-01-16 02:57:26364 def is_chromeos(self, tester_config):
365 return tester_config.get('os_type') == 'chromeos'
366
Kenneth Russell8a386d42018-06-02 09:48:01367 def is_linux(self, tester_config):
368 return tester_config.get('os_type') == 'linux'
369
Kai Ninomiya40de9f52019-10-18 21:38:49370 def is_mac(self, tester_config):
371 return tester_config.get('os_type') == 'mac'
372
373 def is_win(self, tester_config):
374 return tester_config.get('os_type') == 'win'
375
376 def is_win64(self, tester_config):
377 return (tester_config.get('os_type') == 'win' and
378 tester_config.get('browser_config') == 'release_x64')
379
Kenneth Russelleb60cbd22017-12-05 07:54:28380 def get_exception_for_test(self, test_name, test_config):
381 # gtests may have both "test" and "name" fields, and usually, if the "name"
382 # field is specified, it means that the same test is being repurposed
383 # multiple times with different command line arguments. To handle this case,
384 # prefer to lookup per the "name" field of the test itself, as opposed to
385 # the "test_name", which is actually the "test" field.
386 if 'name' in test_config:
387 return self.exceptions.get(test_config['name'])
388 else:
389 return self.exceptions.get(test_name)
390
Nico Weberb0b3f5862018-07-13 18:45:15391 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28392 # Currently, the only reason a test should not run on a given tester is that
393 # it's in the exceptions. (Once the GPU waterfall generation script is
394 # incorporated here, the rules will become more complex.)
395 exception = self.get_exception_for_test(test_name, test_config)
396 if not exception:
397 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28398 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28399 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28400 if remove_from:
401 if tester_name in remove_from:
402 return False
403 # TODO(kbr): this code path was added for some tests (including
404 # android_webview_unittests) on one machine (Nougat Phone
405 # Tester) which exists with the same name on two waterfalls,
406 # chromium.android and chromium.fyi; the tests are run on one
407 # but not the other. Once the bots are all uniquely named (a
408 # different ongoing project) this code should be removed.
409 # TODO(kbr): add coverage.
410 return (tester_name + ' ' + waterfall['name']
411 not in remove_from) # pragma: no cover
412 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28413
Nico Weber79dc5f6852018-07-13 19:38:49414 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28415 exception = self.get_exception_for_test(test_name, test)
416 if not exception:
417 return None
Nico Weber79dc5f6852018-07-13 19:38:49418 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28419
Brian Sheedye6ea0ee2019-07-11 02:54:37420 def get_test_replacements(self, test, test_name, tester_name):
421 exception = self.get_exception_for_test(test_name, test)
422 if not exception:
423 return None
424 return exception.get('replacements', {}).get(tester_name)
425
Kenneth Russell8a386d42018-06-02 09:48:01426 def merge_command_line_args(self, arr, prefix, splitter):
427 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01428 idx = 0
429 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01430 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01431 while idx < len(arr):
432 flag = arr[idx]
433 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01434 if flag.startswith(prefix):
435 arg = flag[prefix_len:]
436 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01437 if first_idx < 0:
438 first_idx = idx
439 else:
440 delete_current_entry = True
441 if delete_current_entry:
442 del arr[idx]
443 else:
444 idx += 1
445 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01446 arr[first_idx] = prefix + splitter.join(accumulated_args)
447 return arr
448
449 def maybe_fixup_args_array(self, arr):
450 # The incoming array of strings may be an array of command line
451 # arguments. To make it easier to turn on certain features per-bot or
452 # per-test-suite, look specifically for certain flags and merge them
453 # appropriately.
454 # --enable-features=Feature1 --enable-features=Feature2
455 # are merged to:
456 # --enable-features=Feature1,Feature2
457 # and:
458 # --extra-browser-args=arg1 --extra-browser-args=arg2
459 # are merged to:
460 # --extra-browser-args=arg1 arg2
461 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
462 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01463 return arr
464
Brian Sheedya31578e2020-05-18 20:24:36465 def substitute_magic_args(self, test_config):
466 """Substitutes any magic substitution args present in |test_config|.
467
468 Substitutions are done in-place.
469
470 See buildbot_json_magic_substitutions.py for more information on this
471 feature.
472
473 Args:
474 test_config: A dict containing a configuration for a specific test on
475 a specific builder, e.g. the output of update_and_cleanup_test.
476 """
477 substituted_array = []
478 for arg in test_config.get('args', []):
479 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
480 function = arg.replace(
481 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
482 if hasattr(magic_substitutions, function):
483 substituted_array.extend(
484 getattr(magic_substitutions, function)(test_config))
485 else:
486 raise BBGenErr(
487 'Magic substitution function %s does not exist' % function)
488 else:
489 substituted_array.append(arg)
490 if substituted_array:
491 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
492
Kenneth Russelleb60cbd22017-12-05 07:54:28493 def dictionary_merge(self, a, b, path=None, update=True):
494 """http://stackoverflow.com/questions/7204805/
495 python-dictionaries-of-dictionaries-merge
496 merges b into a
497 """
498 if path is None:
499 path = []
500 for key in b:
501 if key in a:
502 if isinstance(a[key], dict) and isinstance(b[key], dict):
503 self.dictionary_merge(a[key], b[key], path + [str(key)])
504 elif a[key] == b[key]:
505 pass # same leaf value
506 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06507 # Args arrays are lists of strings. Just concatenate them,
508 # and don't sort them, in order to keep some needed
509 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28510 if all(isinstance(x, str)
511 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01512 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28513 else:
514 # TODO(kbr): this only works properly if the two arrays are
515 # the same length, which is currently always the case in the
516 # swarming dimension_sets that we have to merge. It will fail
517 # to merge / override 'args' arrays which are different
518 # length.
519 for idx in xrange(len(b[key])):
520 try:
521 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
522 path + [str(key), str(idx)],
523 update=update)
Jeff Yoon8154e582019-12-03 23:30:01524 except (IndexError, TypeError):
525 raise BBGenErr('Error merging lists by key "%s" from source %s '
526 'into target %s at index %s. Verify target list '
527 'length is equal or greater than source'
528 % (str(key), str(b), str(a), str(idx)))
John Budorick5bc387fe2019-05-09 20:02:53529 elif update:
530 if b[key] is None:
531 del a[key]
532 else:
533 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28534 else:
535 raise BBGenErr('Conflict at %s' % '.'.join(
536 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53537 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28538 a[key] = b[key]
539 return a
540
John Budorickab108712018-09-01 00:12:21541 def initialize_args_for_test(
542 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21543 args = []
544 args.extend(generated_test.get('args', []))
545 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22546
Kenneth Russell8a386d42018-06-02 09:48:01547 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21548 val = generated_test.pop(key, [])
549 if fn(tester_config):
550 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01551
552 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
553 add_conditional_args('linux_args', self.is_linux)
554 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36555 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49556 add_conditional_args('mac_args', self.is_mac)
557 add_conditional_args('win_args', self.is_win)
558 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01559
John Budorickab108712018-09-01 00:12:21560 for key in additional_arg_keys or []:
561 args.extend(generated_test.pop(key, []))
562 args.extend(tester_config.get(key, []))
563
564 if args:
565 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01566
Kenneth Russelleb60cbd22017-12-05 07:54:28567 def initialize_swarming_dictionary_for_test(self, generated_test,
568 tester_config):
569 if 'swarming' not in generated_test:
570 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28571 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
572 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38573 'can_use_on_swarming_builders': tester_config.get('use_swarming',
574 True)
Dirk Pranke81ff51c2017-12-09 19:24:28575 })
Kenneth Russelleb60cbd22017-12-05 07:54:28576 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03577 if ('dimension_sets' not in generated_test['swarming'] and
578 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28579 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
580 tester_config['swarming']['dimension_sets'])
581 self.dictionary_merge(generated_test['swarming'],
582 tester_config['swarming'])
583 # Apply any Android-specific Swarming dimensions after the generic ones.
584 if 'android_swarming' in generated_test:
585 if self.is_android(tester_config): # pragma: no cover
586 self.dictionary_merge(
587 generated_test['swarming'],
588 generated_test['android_swarming']) # pragma: no cover
589 del generated_test['android_swarming'] # pragma: no cover
590
591 def clean_swarming_dictionary(self, swarming_dict):
592 # Clean out redundant entries from a test's "swarming" dictionary.
593 # This is really only needed to retain 100% parity with the
594 # handwritten JSON files, and can be removed once all the files are
595 # autogenerated.
596 if 'shards' in swarming_dict:
597 if swarming_dict['shards'] == 1: # pragma: no cover
598 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24599 if 'hard_timeout' in swarming_dict:
600 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
601 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43602 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28603 # Remove all other keys.
604 for k in swarming_dict.keys(): # pragma: no cover
605 if k != 'can_use_on_swarming_builders': # pragma: no cover
606 del swarming_dict[k] # pragma: no cover
607
Stephen Martinis0382bc12018-09-17 22:29:07608 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
609 waterfall):
610 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01611 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07612 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28613 # See if there are any exceptions that need to be merged into this
614 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49615 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28616 if modifications:
617 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23618 if 'swarming' in test:
619 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28620 # Ensure all Android Swarming tests run only on userdebug builds if another
621 # build type was not specified.
622 if 'swarming' in test and self.is_android(tester_config):
623 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22624 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28625 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37626 self.replace_test_args(test, test_name, tester_name)
Ben Pastenee012aea42019-05-14 22:32:28627
Kenneth Russelleb60cbd22017-12-05 07:54:28628 return test
629
Brian Sheedye6ea0ee2019-07-11 02:54:37630 def replace_test_args(self, test, test_name, tester_name):
631 replacements = self.get_test_replacements(
632 test, test_name, tester_name) or {}
633 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
634 for key, replacement_dict in replacements.iteritems():
635 if key not in valid_replacement_keys:
636 raise BBGenErr(
637 'Given replacement key %s for %s on %s is not in the list of valid '
638 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
639 for replacement_key, replacement_val in replacement_dict.iteritems():
640 found_key = False
641 for i, test_key in enumerate(test.get(key, [])):
642 # Handle both the key/value being replaced being defined as two
643 # separate items or as key=value.
644 if test_key == replacement_key:
645 found_key = True
646 # Handle flags without values.
647 if replacement_val == None:
648 del test[key][i]
649 else:
650 test[key][i+1] = replacement_val
651 break
652 elif test_key.startswith(replacement_key + '='):
653 found_key = True
654 if replacement_val == None:
655 del test[key][i]
656 else:
657 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
658 break
659 if not found_key:
660 raise BBGenErr('Could not find %s in existing list of values for key '
661 '%s in %s on %s' % (replacement_key, key, test_name,
662 tester_name))
663
Shenghua Zhangaba8bad2018-02-07 02:12:09664 def add_common_test_properties(self, test, tester_config):
Brian Sheedy5ea8f6c62020-05-21 03:05:05665 if self.is_chromeos(tester_config) and tester_config.get('use_swarming',
Ben Pastenea9e583b2019-01-16 02:57:26666 True):
667 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06668 # are targeting CrOS hardware and so need the special trigger script.
669 dimension_sets = test['swarming']['dimension_sets']
Ben Pastenea9e583b2019-01-16 02:57:26670 if all('device_type' in ds for ds in dimension_sets):
671 test['trigger_script'] = {
672 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
673 }
Shenghua Zhangaba8bad2018-02-07 02:12:09674
Ben Pastene858f4be2019-01-09 23:52:09675 def add_android_presentation_args(self, tester_config, test_name, result):
676 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38677 bucket = tester_config.get('results_bucket', 'chromium-result-details')
678 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09679 if (result['swarming']['can_use_on_swarming_builders'] and not
680 tester_config.get('skip_merge_script', False)):
681 result['merge'] = {
682 'args': [
683 '--bucket',
John Budorick262ae112019-07-12 19:24:38684 bucket,
Ben Pastene858f4be2019-01-09 23:52:09685 '--test-name',
Rakib M. Hasanc9e01c602020-07-27 22:48:12686 result.get('name', test_name)
Ben Pastene858f4be2019-01-09 23:52:09687 ],
688 'script': '//build/android/pylib/results/presentation/'
689 'test_results_presentation.py',
690 }
691 if not tester_config.get('skip_cipd_packages', False):
Ben Pastenee5949ea82019-01-10 21:45:26692 cipd_packages = result['swarming'].get('cipd_packages', [])
693 cipd_packages.append(
Ben Pastene858f4be2019-01-09 23:52:09694 {
695 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
696 'location': 'bin',
697 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
698 }
Ben Pastenee5949ea82019-01-10 21:45:26699 )
700 result['swarming']['cipd_packages'] = cipd_packages
Ben Pastene858f4be2019-01-09 23:52:09701 if not tester_config.get('skip_output_links', False):
702 result['swarming']['output_links'] = [
703 {
704 'link': [
705 'https://luci-logdog.appspot.com/v/?s',
706 '=android%2Fswarming%2Flogcats%2F',
707 '${TASK_ID}%2F%2B%2Funified_logcats',
708 ],
709 'name': 'shard #${SHARD_INDEX} logcats',
710 },
711 ]
712 if args:
713 result['args'] = args
714
Kenneth Russelleb60cbd22017-12-05 07:54:28715 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
716 test_config):
717 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15718 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28719 return None
720 result = copy.deepcopy(test_config)
721 if 'test' in result:
Rakib M. Hasanc9e01c602020-07-27 22:48:12722 if 'name' not in result:
723 result['name'] = test_name
Kenneth Russelleb60cbd22017-12-05 07:54:28724 else:
725 result['test'] = test_name
726 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21727
728 self.initialize_args_for_test(
729 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28730 if self.is_android(tester_config) and tester_config.get('use_swarming',
731 True):
Ben Pastene858f4be2019-01-09 23:52:09732 self.add_android_presentation_args(tester_config, test_name, result)
733 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42734
Stephen Martinis0382bc12018-09-17 22:29:07735 result = self.update_and_cleanup_test(
736 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09737 self.add_common_test_properties(result, tester_config)
Brian Sheedya31578e2020-05-18 20:24:36738 self.substitute_magic_args(result)
Stephen Martinisbc7b7772019-05-01 22:01:43739
740 if not result.get('merge'):
741 # TODO(https://crbug.com/958376): Consider adding the ability to not have
742 # this default.
743 result['merge'] = {
744 'script': '//testing/merge_scripts/standard_gtest_merge.py',
745 'args': [],
746 }
Kenneth Russelleb60cbd22017-12-05 07:54:28747 return result
748
749 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
750 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01751 if not self.should_run_on_tester(waterfall, tester_name, test_name,
752 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28753 return None
754 result = copy.deepcopy(test_config)
755 result['isolate_name'] = result.get('isolate_name', test_name)
Jeff Yoonb8bfdbf32020-03-13 19:14:43756 result['name'] = result.get('name', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28757 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01758 self.initialize_args_for_test(result, tester_config)
Ben Pastene858f4be2019-01-09 23:52:09759 if tester_config.get('use_android_presentation', False):
760 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07761 result = self.update_and_cleanup_test(
762 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09763 self.add_common_test_properties(result, tester_config)
Brian Sheedya31578e2020-05-18 20:24:36764 self.substitute_magic_args(result)
Stephen Martinisf50047062019-05-06 22:26:17765
766 if not result.get('merge'):
767 # TODO(https://crbug.com/958376): Consider adding the ability to not have
768 # this default.
769 result['merge'] = {
770 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
771 'args': [],
772 }
Kenneth Russelleb60cbd22017-12-05 07:54:28773 return result
774
775 def generate_script_test(self, waterfall, tester_name, tester_config,
776 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44777 # TODO(https://crbug.com/953072): Remove this check whenever a better
778 # long-term solution is implemented.
779 if (waterfall.get('forbid_script_tests', False) or
780 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
781 raise BBGenErr('Attempted to generate a script test on tester ' +
782 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01783 if not self.should_run_on_tester(waterfall, tester_name, test_name,
784 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28785 return None
786 result = {
787 'name': test_name,
788 'script': test_config['script']
789 }
Stephen Martinis0382bc12018-09-17 22:29:07790 result = self.update_and_cleanup_test(
791 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedya31578e2020-05-18 20:24:36792 self.substitute_magic_args(result)
Kenneth Russelleb60cbd22017-12-05 07:54:28793 return result
794
795 def generate_junit_test(self, waterfall, tester_name, tester_config,
796 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01797 if not self.should_run_on_tester(waterfall, tester_name, test_name,
798 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28799 return None
John Budorickdef6acb2019-09-17 22:51:09800 result = copy.deepcopy(test_config)
801 result.update({
John Budorickcadc4952019-09-16 23:51:37802 'name': test_name,
803 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09804 })
805 self.initialize_args_for_test(result, tester_config)
806 result = self.update_and_cleanup_test(
807 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedya31578e2020-05-18 20:24:36808 self.substitute_magic_args(result)
Kenneth Russelleb60cbd22017-12-05 07:54:28809 return result
810
Stephen Martinis2a0667022018-09-25 22:31:14811 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01812 substitutions = {
813 # Any machine in waterfalls.pyl which desires to run GPU tests
814 # must provide the os_type key.
815 'os_type': tester_config['os_type'],
816 'gpu_vendor_id': '0',
817 'gpu_device_id': '0',
818 }
Stephen Martinis2a0667022018-09-25 22:31:14819 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01820 if 'gpu' in dimension_set:
821 # First remove the driver version, then split into vendor and device.
822 gpu = dimension_set['gpu']
Yuly Novikove4b2fef2020-09-04 05:53:11823 if gpu != 'none':
824 gpu = gpu.split('-')[0].split(':')
825 substitutions['gpu_vendor_id'] = gpu[0]
826 substitutions['gpu_device_id'] = gpu[1]
Kenneth Russell8a386d42018-06-02 09:48:01827 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
828
829 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56830 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01831 # These are all just specializations of isolated script tests with
832 # a bunch of boilerplate command line arguments added.
833
834 # The step name must end in 'test' or 'tests' in order for the
835 # results to automatically show up on the flakiness dashboard.
836 # (At least, this was true some time ago.) Continue to use this
837 # naming convention for the time being to minimize changes.
838 step_name = test_config.get('name', test_name)
839 if not (step_name.endswith('test') or step_name.endswith('tests')):
840 step_name = '%s_tests' % step_name
841 result = self.generate_isolated_script_test(
842 waterfall, tester_name, tester_config, step_name, test_config)
843 if not result:
844 return None
Chong Gub75754b32020-03-13 16:39:20845 result['isolate_name'] = test_config.get(
846 'isolate_name', 'telemetry_gpu_integration_test')
Chan Liab7d8dd82020-04-24 23:42:19847
Chan Lia3ad1502020-04-28 05:32:11848 # Populate test_id_prefix.
Chan Liab7d8dd82020-04-24 23:42:19849 gn_entry = (
850 self.gn_isolate_map.get(result['isolate_name']) or
851 self.gn_isolate_map.get('telemetry_gpu_integration_test'))
Chan Li17d969f92020-07-10 00:50:03852 result['test_id_prefix'] = 'ninja:%s/' % gn_entry['label']
Chan Liab7d8dd82020-04-24 23:42:19853
Kenneth Russell8a386d42018-06-02 09:48:01854 args = result.get('args', [])
855 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14856
857 # These tests upload and download results from cloud storage and therefore
858 # aren't idempotent yet. https://crbug.com/549140.
859 result['swarming']['idempotent'] = False
860
Kenneth Russell44910c32018-12-03 23:35:11861 # The GPU tests act much like integration tests for the entire browser, and
862 # tend to uncover flakiness bugs more readily than other test suites. In
863 # order to surface any flakiness more readily to the developer of the CL
864 # which is introducing it, we disable retries with patch on the commit
865 # queue.
866 result['should_retry_with_patch'] = False
867
Bo Liu555a0f92019-03-29 12:11:56868 browser = ('android-webview-instrumentation'
869 if is_android_webview else tester_config['browser_config'])
Brian Sheedy4053a702020-07-28 02:09:52870
871 # Most platforms require --enable-logging=stderr to get useful browser logs.
872 # However, this actively messes with logging on CrOS (because Chrome's
873 # stderr goes nowhere on CrOS) AND --log-level=0 is required for some reason
874 # in order to see JavaScript console messages. See
875 # https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/chrome_os_logging.md
876 logging_arg = '--log-level=0' if self.is_chromeos(
877 tester_config) else '--enable-logging=stderr'
878
Kenneth Russell8a386d42018-06-02 09:48:01879 args = [
Bo Liu555a0f92019-03-29 12:11:56880 test_to_run,
881 '--show-stdout',
882 '--browser=%s' % browser,
883 # --passthrough displays more of the logging in Telemetry when
884 # run via typ, in particular some of the warnings about tests
885 # being expected to fail, but passing.
886 '--passthrough',
887 '-v',
Brian Sheedy4053a702020-07-28 02:09:52888 '--extra-browser-args=%s --js-flags=--expose-gc' % logging_arg,
Kenneth Russell8a386d42018-06-02 09:48:01889 ] + args
890 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14891 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01892 return result
893
Kenneth Russelleb60cbd22017-12-05 07:54:28894 def get_test_generator_map(self):
895 return {
Bo Liu555a0f92019-03-29 12:11:56896 'android_webview_gpu_telemetry_tests':
897 GPUTelemetryTestGenerator(self, is_android_webview=True),
Bo Liu555a0f92019-03-29 12:11:56898 'gpu_telemetry_tests':
899 GPUTelemetryTestGenerator(self),
900 'gtest_tests':
901 GTestGenerator(self),
Bo Liu555a0f92019-03-29 12:11:56902 'isolated_scripts':
903 IsolatedScriptTestGenerator(self),
904 'junit_tests':
905 JUnitGenerator(self),
906 'scripts':
907 ScriptGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28908 }
909
Kenneth Russell8a386d42018-06-02 09:48:01910 def get_test_type_remapper(self):
911 return {
912 # These are a specialization of isolated_scripts with a bunch of
913 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:56914 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:01915 'gpu_telemetry_tests': 'isolated_scripts',
916 }
917
Jeff Yoon67c3e832020-02-08 07:39:38918 def check_composition_type_test_suites(self, test_type,
919 additional_validators=None):
920 """Pre-pass to catch errors reliabily for compound/matrix suites"""
921 validators = [check_compound_references,
922 check_basic_references,
923 check_conflicting_definitions]
924 if additional_validators:
925 validators += additional_validators
926
927 target_suites = self.test_suites.get(test_type, {})
928 other_test_type = ('compound_suites'
929 if test_type == 'matrix_compound_suites'
930 else 'matrix_compound_suites')
931 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:01932 basic_suites = self.test_suites.get('basic_suites', {})
933
Jeff Yoon67c3e832020-02-08 07:39:38934 for suite, suite_def in target_suites.iteritems():
Jeff Yoon8154e582019-12-03 23:30:01935 if suite in basic_suites:
936 raise BBGenErr('%s names may not duplicate basic test suite names '
937 '(error found while processsing %s)'
938 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:01939
Jeff Yoon67c3e832020-02-08 07:39:38940 seen_tests = {}
941 for sub_suite in suite_def:
942 for validator in validators:
943 validator(
944 basic_suites=basic_suites,
945 other_test_suites=other_suites,
946 seen_tests=seen_tests,
947 sub_suite=sub_suite,
948 suite=suite,
949 suite_def=suite_def,
950 target_test_suites=target_suites,
951 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:05952 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:38953 )
Kenneth Russelleb60cbd22017-12-05 07:54:28954
Stephen Martinis54d64ad2018-09-21 22:16:20955 def flatten_test_suites(self):
956 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:01957 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
958 for category in test_types:
959 for name, value in self.test_suites.get(category, {}).iteritems():
960 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:20961 self.test_suites = new_test_suites
962
Chan Lia3ad1502020-04-28 05:32:11963 def resolve_test_id_prefixes(self):
Nodir Turakulovfce34292019-12-18 17:05:41964 for suite in self.test_suites['basic_suites'].itervalues():
965 for key, test in suite.iteritems():
Dirk Pranke0e879b22020-07-16 23:53:56966 assert isinstance(test, dict)
Nodir Turakulovfce34292019-12-18 17:05:41967
968 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
969 # https://source.chromium.org/chromium/chromium/tools/build/+/master:scripts/slave/recipe_modules/chromium_tests/generators.py;l=89;drc=14c062ba0eb418d3c4623dde41a753241b9df06b
970 # TODO(crbug.com/1035124): clean this up.
971 isolate_name = test.get('test') or test.get('isolate_name') or key
972 gn_entry = self.gn_isolate_map.get(isolate_name)
973 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:28974 label = gn_entry['label']
975
976 if label.count(':') != 1:
977 raise BBGenErr(
978 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
979 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
980 (label, isolate_name))
981 if label.split(':')[1] != isolate_name:
982 raise BBGenErr(
983 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
984 ' label "%s" see http://crbug.com/1071091 for details.' %
985 (isolate_name, label))
986
Chan Lia3ad1502020-04-28 05:32:11987 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:41988 else: # pragma: no cover
989 # Some tests do not have an entry gn_isolate_map.pyl, such as
990 # telemetry tests.
991 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
992 pass
993
Kenneth Russelleb60cbd22017-12-05 07:54:28994 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:01995 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:20996
Jeff Yoon8154e582019-12-03 23:30:01997 compound_suites = self.test_suites.get('compound_suites', {})
998 # check_composition_type_test_suites() checks that all basic suites
999 # referenced by compound suites exist.
1000 basic_suites = self.test_suites.get('basic_suites')
1001
1002 for name, value in compound_suites.iteritems():
1003 # Resolve this to a dictionary.
1004 full_suite = {}
1005 for entry in value:
1006 suite = basic_suites[entry]
1007 full_suite.update(suite)
1008 compound_suites[name] = full_suite
1009
Jeff Yoon85fb8df2020-08-20 16:47:431010 def resolve_variants(self, basic_test_definition, variants, mixins):
Jeff Yoon67c3e832020-02-08 07:39:381011 """ Merge variant-defined configurations to each test case definition in a
1012 test suite.
1013
1014 The output maps a unique test name to an array of configurations because
1015 there may exist more than one definition for a test name using variants. The
1016 test name is referenced while mapping machines to test suites, so unpacking
1017 the array is done by the generators.
1018
1019 Args:
1020 basic_test_definition: a {} defined test suite in the format
1021 test_name:test_config
1022 variants: an [] of {} defining configurations to be applied to each test
1023 case in the basic test_definition
1024
1025 Return:
1026 a {} of test_name:[{}], where each {} is a merged configuration
1027 """
1028
1029 # Each test in a basic test suite will have a definition per variant.
1030 test_suite = {}
1031 for test_name, test_config in basic_test_definition.iteritems():
1032 definitions = []
1033 for variant in variants:
Jeff Yoonda581c32020-03-06 03:56:051034 # Unpack the variant from variants.pyl if it's string based.
1035 if isinstance(variant, str):
1036 variant = self.variants[variant]
1037
Jeff Yoon67c3e832020-02-08 07:39:381038 # Clone a copy of test_config so that we can have a uniquely updated
1039 # version of it per variant
1040 cloned_config = copy.deepcopy(test_config)
1041 # The variant definition needs to be re-used for each test, so we'll
1042 # create a clone and work with it as well.
1043 cloned_variant = copy.deepcopy(variant)
1044
1045 cloned_config['args'] = (cloned_config.get('args', []) +
1046 cloned_variant.get('args', []))
1047 cloned_config['mixins'] = (cloned_config.get('mixins', []) +
Jeff Yoon85fb8df2020-08-20 16:47:431048 cloned_variant.get('mixins', []) + mixins)
Jeff Yoon67c3e832020-02-08 07:39:381049
1050 basic_swarming_def = cloned_config.get('swarming', {})
1051 variant_swarming_def = cloned_variant.get('swarming', {})
1052 if basic_swarming_def and variant_swarming_def:
1053 if ('dimension_sets' in basic_swarming_def and
1054 'dimension_sets' in variant_swarming_def):
1055 # Retain swarming dimension set merge behavior when both variant and
1056 # the basic test configuration both define it
1057 self.dictionary_merge(basic_swarming_def, variant_swarming_def)
1058 # Remove dimension_sets from the variant definition, so that it does
1059 # not replace what's been done by dictionary_merge in the update
1060 # call below.
1061 del variant_swarming_def['dimension_sets']
1062
1063 # Update the swarming definition with whatever is defined for swarming
1064 # by the variant.
1065 basic_swarming_def.update(variant_swarming_def)
1066 cloned_config['swarming'] = basic_swarming_def
1067
1068 # The identifier is used to make the name of the test unique.
1069 # Generators in the recipe uniquely identify a test by it's name, so we
1070 # don't want to have the same name for each variant.
1071 cloned_config['name'] = '{}_{}'.format(test_name,
1072 cloned_variant['identifier'])
Jeff Yoon67c3e832020-02-08 07:39:381073 definitions.append(cloned_config)
1074 test_suite[test_name] = definitions
1075 return test_suite
1076
Jeff Yoon8154e582019-12-03 23:30:011077 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381078 self.check_composition_type_test_suites('matrix_compound_suites',
1079 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011080
1081 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381082 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011083 # referenced by matrix suites exist.
1084 basic_suites = self.test_suites.get('basic_suites')
1085
Jeff Yoon67c3e832020-02-08 07:39:381086 for test_name, matrix_config in matrix_compound_suites.iteritems():
Jeff Yoon8154e582019-12-03 23:30:011087 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381088
1089 for test_suite, mtx_test_suite_config in matrix_config.iteritems():
1090 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1091
1092 if 'variants' in mtx_test_suite_config:
Jeff Yoon85fb8df2020-08-20 16:47:431093 mixins = mtx_test_suite_config.get('mixins', [])
Jeff Yoon67c3e832020-02-08 07:39:381094 result = self.resolve_variants(basic_test_def,
Jeff Yoon85fb8df2020-08-20 16:47:431095 mtx_test_suite_config['variants'],
1096 mixins)
Jeff Yoon67c3e832020-02-08 07:39:381097 full_suite.update(result)
1098 matrix_compound_suites[test_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281099
1100 def link_waterfalls_to_test_suites(self):
1101 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431102 for tester_name, tester in waterfall['machines'].iteritems():
1103 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281104 if not value in self.test_suites:
1105 # Hard / impossible to cover this in the unit test.
1106 raise self.unknown_test_suite(
1107 value, tester_name, waterfall['name']) # pragma: no cover
1108 tester['test_suites'][suite] = self.test_suites[value]
1109
1110 def load_configuration_files(self):
1111 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
1112 self.test_suites = self.load_pyl_file('test_suites.pyl')
1113 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:011114 self.mixins = self.load_pyl_file('mixins.pyl')
Nodir Turakulovfce34292019-12-18 17:05:411115 self.gn_isolate_map = self.load_pyl_file('gn_isolate_map.pyl')
Jeff Yoonda581c32020-03-06 03:56:051116 self.variants = self.load_pyl_file('variants.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:281117
1118 def resolve_configuration_files(self):
Chan Lia3ad1502020-04-28 05:32:111119 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281120 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011121 self.resolve_matrix_compound_test_suites()
1122 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281123 self.link_waterfalls_to_test_suites()
1124
Nico Weberd18b8962018-05-16 19:39:381125 def unknown_bot(self, bot_name, waterfall_name):
1126 return BBGenErr(
1127 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1128
Kenneth Russelleb60cbd22017-12-05 07:54:281129 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1130 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381131 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281132 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1133
1134 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1135 return BBGenErr(
1136 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1137 ' on waterfall ' + waterfall_name)
1138
Stephen Martinisb72f6d22018-10-04 23:29:011139 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071140 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321141
1142 Checks in the waterfall, builder, and test objects for mixins.
1143 """
1144 def valid_mixin(mixin_name):
1145 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011146 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321147 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381148
Stephen Martinisb6a50492018-09-12 23:59:321149 def must_be_list(mixins, typ, name):
1150 """Asserts that given mixins are a list."""
1151 if not isinstance(mixins, list):
1152 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1153
Brian Sheedy7658c982020-01-08 02:27:581154 test_name = test.get('name')
1155 remove_mixins = set()
1156 if 'remove_mixins' in builder:
1157 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1158 for rm in builder['remove_mixins']:
1159 valid_mixin(rm)
1160 remove_mixins.add(rm)
1161 if 'remove_mixins' in test:
1162 must_be_list(test['remove_mixins'], 'test', test_name)
1163 for rm in test['remove_mixins']:
1164 valid_mixin(rm)
1165 remove_mixins.add(rm)
1166 del test['remove_mixins']
1167
Stephen Martinisb72f6d22018-10-04 23:29:011168 if 'mixins' in waterfall:
1169 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1170 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581171 if mixin in remove_mixins:
1172 continue
Stephen Martinisb6a50492018-09-12 23:59:321173 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011174 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321175
Stephen Martinisb72f6d22018-10-04 23:29:011176 if 'mixins' in builder:
1177 must_be_list(builder['mixins'], 'builder', builder_name)
1178 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581179 if mixin in remove_mixins:
1180 continue
Stephen Martinisb6a50492018-09-12 23:59:321181 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011182 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321183
Stephen Martinisb72f6d22018-10-04 23:29:011184 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071185 return test
1186
Stephen Martinis2a0667022018-09-25 22:31:141187 if not test_name:
1188 test_name = test.get('test')
1189 if not test_name: # pragma: no cover
1190 # Not the best name, but we should say something.
1191 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:011192 must_be_list(test['mixins'], 'test', test_name)
1193 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581194 # We don't bother checking if the given mixin is in remove_mixins here
1195 # since this is already the lowest level, so if a mixin is added here that
1196 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071197 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011198 test = self.apply_mixin(self.mixins[mixin], test)
Jeff Yoon67c3e832020-02-08 07:39:381199 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071200 return test
Stephen Martinisb6a50492018-09-12 23:59:321201
Stephen Martinisb72f6d22018-10-04 23:29:011202 def apply_mixin(self, mixin, test):
1203 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321204
Stephen Martinis0382bc12018-09-17 22:29:071205 Mixins will not override an existing key. This is to ensure exceptions can
1206 override a setting a mixin applies.
1207
Stephen Martinisb72f6d22018-10-04 23:29:011208 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:321209 'dimension_sets', which is how normal test suites specify their dimensions,
1210 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
1211 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011212
Stephen Martinisb6a50492018-09-12 23:59:321213 """
1214 new_test = copy.deepcopy(test)
1215 mixin = copy.deepcopy(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011216 if 'swarming' in mixin:
1217 swarming_mixin = mixin['swarming']
1218 new_test.setdefault('swarming', {})
Brian Sheedycae63b22020-06-10 22:52:111219 # Copy over any explicit dimension sets first so that they will be updated
1220 # by any subsequent 'dimensions' entries.
1221 if 'dimension_sets' in swarming_mixin:
1222 existing_dimension_sets = new_test['swarming'].setdefault(
1223 'dimension_sets', [])
1224 # Appending to the existing list could potentially result in different
1225 # behavior depending on the order the mixins were applied, but that's
1226 # already the case for other parts of mixins, so trust that the user
1227 # will verify that the generated output is correct before submitting.
1228 for dimension_set in swarming_mixin['dimension_sets']:
1229 if dimension_set not in existing_dimension_sets:
1230 existing_dimension_sets.append(dimension_set)
1231 del swarming_mixin['dimension_sets']
Stephen Martinisb72f6d22018-10-04 23:29:011232 if 'dimensions' in swarming_mixin:
1233 new_test['swarming'].setdefault('dimension_sets', [{}])
1234 for dimension_set in new_test['swarming']['dimension_sets']:
1235 dimension_set.update(swarming_mixin['dimensions'])
1236 del swarming_mixin['dimensions']
Stephen Martinisb72f6d22018-10-04 23:29:011237 # python dict update doesn't do recursion at all. Just hard code the
1238 # nested update we need (mixin['swarming'] shouldn't clobber
1239 # test['swarming'], but should update it).
1240 new_test['swarming'].update(swarming_mixin)
1241 del mixin['swarming']
1242
Wezc0e835b702018-10-30 00:38:411243 if '$mixin_append' in mixin:
1244 # Values specified under $mixin_append should be appended to existing
1245 # lists, rather than replacing them.
1246 mixin_append = mixin['$mixin_append']
1247 for key in mixin_append:
1248 new_test.setdefault(key, [])
1249 if not isinstance(mixin_append[key], list):
1250 raise BBGenErr(
1251 'Key "' + key + '" in $mixin_append must be a list.')
1252 if not isinstance(new_test[key], list):
1253 raise BBGenErr(
1254 'Cannot apply $mixin_append to non-list "' + key + '".')
1255 new_test[key].extend(mixin_append[key])
1256 if 'args' in mixin_append:
1257 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1258 del mixin['$mixin_append']
1259
Stephen Martinisb72f6d22018-10-04 23:29:011260 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321261 return new_test
1262
Greg Gutermanf60eb052020-03-12 17:40:011263 def generate_output_tests(self, waterfall):
1264 """Generates the tests for a waterfall.
1265
1266 Args:
1267 waterfall: a dictionary parsed from a master pyl file
1268 Returns:
1269 A dictionary mapping builders to test specs
1270 """
1271 return {
1272 name: self.get_tests_for_config(waterfall, name, config)
1273 for name, config
1274 in waterfall['machines'].iteritems()
1275 }
1276
1277 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531278 generator_map = self.get_test_generator_map()
1279 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281280
Greg Gutermanf60eb052020-03-12 17:40:011281 tests = {}
1282 # Copy only well-understood entries in the machine's configuration
1283 # verbatim into the generated JSON.
1284 if 'additional_compile_targets' in config:
1285 tests['additional_compile_targets'] = config[
1286 'additional_compile_targets']
1287 for test_type, input_tests in config.get('test_suites', {}).iteritems():
1288 if test_type not in generator_map:
1289 raise self.unknown_test_suite_type(
1290 test_type, name, waterfall['name']) # pragma: no cover
1291 test_generator = generator_map[test_type]
1292 # Let multiple kinds of generators generate the same kinds
1293 # of tests. For example, gpu_telemetry_tests are a
1294 # specialization of isolated_scripts.
1295 new_tests = test_generator.generate(
1296 waterfall, name, config, input_tests)
1297 remapped_test_type = test_type_remapper.get(test_type, test_type)
1298 tests[remapped_test_type] = test_generator.sort(
1299 tests.get(remapped_test_type, []) + new_tests)
1300
1301 return tests
1302
1303 def jsonify(self, all_tests):
1304 return json.dumps(
1305 all_tests, indent=2, separators=(',', ': '),
1306 sort_keys=True) + '\n'
1307
1308 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281309 self.load_configuration_files()
1310 self.resolve_configuration_files()
1311 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011312 result = collections.defaultdict(dict)
1313
1314 required_fields = ('project', 'bucket', 'name')
1315 for waterfall in self.waterfalls:
1316 for field in required_fields:
1317 # Verify required fields
1318 if field not in waterfall:
1319 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1320
1321 # Handle filter flag, if specified
1322 if filters and waterfall['name'] not in filters:
1323 continue
1324
1325 # Join config files and hardcoded values together
1326 all_tests = self.generate_output_tests(waterfall)
1327 result[waterfall['name']] = all_tests
1328
1329 # Deduce per-bucket mappings
1330 # This will be the standard after masternames are gone
1331 bucket_filename = waterfall['project'] + '.' + waterfall['bucket']
1332 for buildername in waterfall['machines'].keys():
1333 result[bucket_filename][buildername] = all_tests[buildername]
1334
1335 # Add do not edit warning
1336 for tests in result.values():
1337 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1338 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1339
1340 return result
1341
1342 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281343 suffix = '.json'
1344 if self.args.new_files:
1345 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011346
1347 for filename, contents in result.items():
1348 jsonstr = self.jsonify(contents)
1349 self.write_file(self.pyl_file_path(filename + suffix), jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281350
Nico Weberd18b8962018-05-16 19:39:381351 def get_valid_bot_names(self):
John Budorick699282e2019-02-13 01:27:331352 # Extract bot names from infra/config/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421353 # NOTE: This reference can cause issues; if a file changes there, the
1354 # presubmit here won't be run by default. A manually maintained list there
1355 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1356 # references to configs outside of this directory are added, please change
1357 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1358 # never ends up in an invalid state.
Garrett Beaty4f3e9212020-06-25 20:21:491359
1360 # Get the generated project.pyl so we can check if we should be enforcing
1361 # that the specs are for builders that actually exist
1362 # If not, return None to indicate that we won't enforce that builders in
1363 # waterfalls.pyl are defined in LUCI
1364 project_pyl_path = os.path.join(self.args.infra_config_dir, 'generated',
1365 'project.pyl')
1366 if os.path.exists(project_pyl_path):
1367 settings = ast.literal_eval(self.read_file(project_pyl_path))
1368 if not settings.get('validate_source_side_specs_have_builder', True):
1369 return None
1370
Nico Weberd18b8962018-05-16 19:39:381371 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311372 milo_configs = glob.glob(
1373 os.path.join(self.args.infra_config_dir, 'generated', 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431374 for c in milo_configs:
1375 for l in self.read_file(c).splitlines():
1376 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311377 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431378 continue
1379 # l looks like
1380 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1381 # Extract win_chromium_dbg_ng part.
1382 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381383 return bot_names
1384
Ben Pastene9a010082019-09-25 20:41:371385 def get_builders_that_do_not_actually_exist(self):
Kenneth Russell8a386d42018-06-02 09:48:011386 # Some of the bots on the chromium.gpu.fyi waterfall in particular
1387 # are defined only to be mirrored into trybots, and don't actually
1388 # exist on any of the waterfalls or consoles.
1389 return [
Yuke Liao8373de52020-08-14 18:30:541390 'GPU FYI Fuchsia Builder',
1391 'ANGLE GPU Android Release (Nexus 5X)',
1392 'ANGLE GPU Linux Release (Intel HD 630)',
1393 'ANGLE GPU Linux Release (NVIDIA)',
1394 'ANGLE GPU Mac Release (Intel)',
1395 'ANGLE GPU Mac Retina Release (AMD)',
1396 'ANGLE GPU Mac Retina Release (NVIDIA)',
1397 'ANGLE GPU Win10 x64 Release (Intel HD 630)',
1398 'ANGLE GPU Win10 x64 Release (NVIDIA)',
1399 'Optional Android Release (Nexus 5X)',
1400 'Optional Linux Release (Intel HD 630)',
1401 'Optional Linux Release (NVIDIA)',
1402 'Optional Mac Release (Intel)',
1403 'Optional Mac Retina Release (AMD)',
1404 'Optional Mac Retina Release (NVIDIA)',
1405 'Optional Win10 x64 Release (Intel HD 630)',
1406 'Optional Win10 x64 Release (NVIDIA)',
1407 'Win7 ANGLE Tryserver (AMD)',
1408 # chromium.chromiumos
1409 'linux-lacros-rel',
1410 # chromium.fyi
1411 'linux-blink-rel-dummy',
1412 'linux-blink-optional-highdpi-rel-dummy',
1413 'mac10.12-blink-rel-dummy',
1414 'mac10.13-blink-rel-dummy',
1415 'mac10.14-blink-rel-dummy',
1416 'mac10.15-blink-rel-dummy',
Stephanie Kim7fbfd912020-08-21 21:11:001417 'mac11.0-blink-rel-dummy',
Yuke Liao8373de52020-08-14 18:30:541418 'win7-blink-rel-dummy',
1419 'win10-blink-rel-dummy',
1420 'WebKit Linux composite_after_paint Dummy Builder',
1421 'WebKit Linux layout_ng_disabled Builder',
1422 # chromium, due to https://crbug.com/878915
1423 'win-dbg',
1424 'win32-dbg',
1425 'win-archive-dbg',
1426 'win32-archive-dbg',
1427 # TODO(crbug.com/1033753) Delete these when coverage is enabled by
1428 # default on Windows tryjobs.
1429 'GPU Win x64 Builder Code Coverage',
1430 'Win x64 Builder Code Coverage',
1431 'Win10 Tests x64 Code Coverage',
1432 'Win10 x64 Release (NVIDIA) Code Coverage',
1433 # TODO(crbug.com/1024915) Delete these when coverage is enabled by
1434 # default on Mac OS tryjobs.
1435 'Mac Builder Code Coverage',
1436 'Mac10.13 Tests Code Coverage',
1437 'GPU Mac Builder Code Coverage',
1438 'Mac Release (Intel) Code Coverage',
1439 'Mac Retina Release (AMD) Code Coverage',
Kenneth Russell8a386d42018-06-02 09:48:011440 ]
1441
Ben Pastene9a010082019-09-25 20:41:371442 def get_internal_waterfalls(self):
1443 # Similar to get_builders_that_do_not_actually_exist above, but for
1444 # waterfalls defined in internal configs.
Ben Pastenec7f5c472020-09-18 19:35:471445 return ['chrome', 'chrome.pgo', 'internal.chromeos.fyi', 'internal.soda']
Ben Pastene9a010082019-09-25 20:41:371446
Stephen Martinisf83893722018-09-19 00:02:181447 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201448 self.check_input_files_sorting(verbose)
1449
Kenneth Russelleb60cbd22017-12-05 07:54:281450 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011451 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381452 self.check_composition_type_test_suites('matrix_compound_suites',
1453 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111454 self.resolve_test_id_prefixes()
Stephen Martinis54d64ad2018-09-21 22:16:201455 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381456
1457 # All bots should exist.
1458 bot_names = self.get_valid_bot_names()
Ben Pastene9a010082019-09-25 20:41:371459 builders_that_dont_exist = self.get_builders_that_do_not_actually_exist()
Garrett Beaty2a02de3c2020-05-15 13:57:351460 if bot_names is not None:
1461 internal_waterfalls = self.get_internal_waterfalls()
1462 for waterfall in self.waterfalls:
1463 # TODO(crbug.com/991417): Remove the need for this exception.
1464 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011465 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351466 for bot_name in waterfall['machines']:
1467 if bot_name in builders_that_dont_exist:
Kenneth Russell78fd8702018-05-17 01:15:521468 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351469 if bot_name not in bot_names:
1470 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
1471 # TODO(thakis): Remove this once these bots move to luci.
1472 continue # pragma: no cover
1473 if waterfall['name'] in ['tryserver.webrtc',
1474 'webrtc.chromium.fyi.experimental']:
1475 # These waterfalls have their bot configs in a different repo.
1476 # so we don't know about their bot names.
1477 continue # pragma: no cover
1478 if waterfall['name'] in ['client.devtools-frontend.integration',
1479 'tryserver.devtools-frontend',
1480 'chromium.devtools-frontend']:
1481 continue # pragma: no cover
Garrett Beaty48d261a2020-09-17 22:11:201482 if waterfall['name'] in ['client.openscreen.chromium']:
1483 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351484 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381485
Kenneth Russelleb60cbd22017-12-05 07:54:281486 # All test suites must be referenced.
1487 suites_seen = set()
1488 generator_map = self.get_test_generator_map()
1489 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431490 for bot_name, tester in waterfall['machines'].iteritems():
1491 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281492 if suite_type not in generator_map:
1493 raise self.unknown_test_suite_type(suite_type, bot_name,
1494 waterfall['name'])
1495 if suite not in self.test_suites:
1496 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1497 suites_seen.add(suite)
1498 # Since we didn't resolve the configuration files, this set
1499 # includes both composition test suites and regular ones.
1500 resolved_suites = set()
1501 for suite_name in suites_seen:
1502 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011503 for sub_suite in suite:
1504 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281505 resolved_suites.add(suite_name)
1506 # At this point, every key in test_suites.pyl should be referenced.
1507 missing_suites = set(self.test_suites.keys()) - resolved_suites
1508 if missing_suites:
1509 raise BBGenErr('The following test suites were unreferenced by bots on '
1510 'the waterfalls: ' + str(missing_suites))
1511
1512 # All test suite exceptions must refer to bots on the waterfall.
1513 all_bots = set()
1514 missing_bots = set()
1515 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431516 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281517 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281518 # In order to disambiguate between bots with the same name on
1519 # different waterfalls, support has been added to various
1520 # exceptions for concatenating the waterfall name after the bot
1521 # name.
1522 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281523 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381524 removals = (exception.get('remove_from', []) +
1525 exception.get('remove_gtest_from', []) +
1526 exception.get('modifications', {}).keys())
1527 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281528 if removal not in all_bots:
1529 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411530
Ben Pastene9a010082019-09-25 20:41:371531 missing_bots = missing_bots - set(builders_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281532 if missing_bots:
1533 raise BBGenErr('The following nonexistent machines were referenced in '
1534 'the test suite exceptions: ' + str(missing_bots))
1535
Stephen Martinis0382bc12018-09-17 22:29:071536 # All mixins must be referenced
1537 seen_mixins = set()
1538 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011539 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071540 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011541 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071542 for suite in self.test_suites.values():
1543 if isinstance(suite, list):
1544 # Don't care about this, it's a composition, which shouldn't include a
1545 # swarming mixin.
1546 continue
1547
1548 for test in suite.values():
Dirk Pranke0e879b22020-07-16 23:53:561549 assert isinstance(test, dict)
Stephen Martinisb72f6d22018-10-04 23:29:011550 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071551
Stephen Martinisb72f6d22018-10-04 23:29:011552 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071553 if missing_mixins:
1554 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1555 ' referenced in a waterfall, machine, or test suite.' % (
1556 str(missing_mixins)))
1557
Jeff Yoonda581c32020-03-06 03:56:051558 # All variant references must be referenced
1559 seen_variants = set()
1560 for suite in self.test_suites.values():
1561 if isinstance(suite, list):
1562 continue
1563
1564 for test in suite.values():
1565 if isinstance(test, dict):
1566 for variant in test.get('variants', []):
1567 if isinstance(variant, str):
1568 seen_variants.add(variant)
1569
1570 missing_variants = set(self.variants.keys()) - seen_variants
1571 if missing_variants:
1572 raise BBGenErr('The following variants were unreferenced: %s. They must '
1573 'be referenced in a matrix test suite under the variants '
1574 'key.' % str(missing_variants))
1575
Stephen Martinis54d64ad2018-09-21 22:16:201576
1577 def type_assert(self, node, typ, filename, verbose=False):
1578 """Asserts that the Python AST node |node| is of type |typ|.
1579
1580 If verbose is set, it prints out some helpful context lines, showing where
1581 exactly the error occurred in the file.
1582 """
1583 if not isinstance(node, typ):
1584 if verbose:
1585 lines = [""] + self.read_file(filename).splitlines()
1586
1587 context = 2
1588 lines_start = max(node.lineno - context, 0)
1589 # Add one to include the last line
1590 lines_end = min(node.lineno + context, len(lines)) + 1
1591 lines = (
1592 ['== %s ==\n' % filename] +
1593 ["<snip>\n"] +
1594 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1595 lines[lines_start:lines_start + context])] +
1596 ['-' * 80 + '\n'] +
1597 ['%d %s' % (node.lineno, lines[node.lineno])] +
1598 ['-' * (node.col_offset + 3) + '^' + '-' * (
1599 80 - node.col_offset - 4) + '\n'] +
1600 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1601 lines[node.lineno + 1:lines_end])] +
1602 ["<snip>\n"]
1603 )
1604 # Print out a useful message when a type assertion fails.
1605 for l in lines:
1606 self.print_line(l.strip())
1607
1608 node_dumped = ast.dump(node, annotate_fields=False)
1609 # If the node is huge, truncate it so everything fits in a terminal
1610 # window.
1611 if len(node_dumped) > 60: # pragma: no cover
1612 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1613 raise BBGenErr(
1614 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1615 ' be %s, is %s' % (
1616 filename, node_dumped,
1617 node.lineno, typ, type(node)))
1618
Stephen Martinis5bef0fc2020-01-06 22:47:531619 def check_ast_list_formatted(self, keys, filename, verbose,
Stephen Martinis1384ff92020-01-07 19:52:151620 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531621 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201622
Stephen Martinis5bef0fc2020-01-06 22:47:531623 Currently only checks to ensure they're correctly sorted, and that there
1624 are no duplicates.
1625
1626 Args:
1627 keys: An python list of AST nodes.
1628
1629 It's a list of AST nodes instead of a list of strings because
1630 when verbose is set, it tries to print out context of where the
1631 diffs are in the file.
1632 filename: The name of the file this node is from.
1633 verbose: If set, print out diff information about how the keys are
1634 incorrectly formatted.
1635 check_sorting: If true, checks if the list is sorted.
1636 Returns:
1637 If the keys are correctly formatted.
1638 """
1639 if not keys:
1640 return True
1641
1642 assert isinstance(keys[0], ast.Str)
1643
1644 keys_strs = [k.s for k in keys]
1645 # Keys to diff against. Used below.
1646 keys_to_diff_against = None
1647 # If the list is properly formatted.
1648 list_formatted = True
1649
1650 # Duplicates are always bad.
1651 if len(set(keys_strs)) != len(keys_strs):
1652 list_formatted = False
1653 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1654
1655 if check_sorting and sorted(keys_strs) != keys_strs:
1656 list_formatted = False
1657 if list_formatted:
1658 return True
1659
1660 if verbose:
1661 line_num = keys[0].lineno
1662 keys = [k.s for k in keys]
1663 if check_sorting:
1664 # If we have duplicates, sorting this will take care of it anyways.
1665 keys_to_diff_against = sorted(set(keys))
1666 # else, keys_to_diff_against is set above already
1667
1668 self.print_line('=' * 80)
1669 self.print_line('(First line of keys is %s)' % line_num)
1670 for line in difflib.context_diff(
1671 keys, keys_to_diff_against,
1672 fromfile='current (%r)' % filename, tofile='sorted', lineterm=''):
1673 self.print_line(line)
1674 self.print_line('=' * 80)
1675
1676 return False
1677
Stephen Martinis1384ff92020-01-07 19:52:151678 def check_ast_dict_formatted(self, node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531679 """Checks if an ast dictionary's keys are correctly formatted.
1680
1681 Just a simple wrapper around check_ast_list_formatted.
1682 Args:
1683 node: An AST node. Assumed to be a dictionary.
1684 filename: The name of the file this node is from.
1685 verbose: If set, print out diff information about how the keys are
1686 incorrectly formatted.
1687 check_sorting: If true, checks if the list is sorted.
1688 Returns:
1689 If the dictionary is correctly formatted.
1690 """
Stephen Martinis54d64ad2018-09-21 22:16:201691 keys = []
1692 # The keys of this dict are ordered as ordered in the file; normal python
1693 # dictionary keys are given an arbitrary order, but since we parsed the
1694 # file itself, the order as given in the file is preserved.
1695 for key in node.keys:
1696 self.type_assert(key, ast.Str, filename, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531697 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201698
Stephen Martinis1384ff92020-01-07 19:52:151699 return self.check_ast_list_formatted(keys, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181700
1701 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201702 # TODO(https://crbug.com/886993): Add the ability for this script to
1703 # actually format the files, rather than just complain if they're
1704 # incorrectly formatted.
1705 bad_files = set()
Stephen Martinis5bef0fc2020-01-06 22:47:531706 def parse_file(filename):
1707 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201708
Stephen Martinis5bef0fc2020-01-06 22:47:531709 Returns an AST node representing the value in the pyl file."""
Stephen Martinisf83893722018-09-19 00:02:181710 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1711
Stephen Martinisf83893722018-09-19 00:02:181712 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201713 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181714 module = parsed.body
1715
1716 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201717 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181718 if len(module) != 1: # pragma: no cover
1719 raise BBGenErr('Invalid .pyl file %s' % filename)
1720 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201721 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181722
Stephen Martinis5bef0fc2020-01-06 22:47:531723 return expr.value
1724
1725 # Handle this separately
1726 filename = 'waterfalls.pyl'
1727 value = parse_file(filename)
1728 # Value should be a list.
1729 self.type_assert(value, ast.List, filename, verbose)
1730
1731 keys = []
1732 for val in value.elts:
1733 self.type_assert(val, ast.Dict, filename, verbose)
1734 waterfall_name = None
1735 for key, val in zip(val.keys, val.values):
1736 self.type_assert(key, ast.Str, filename, verbose)
1737 if key.s == 'machines':
1738 if not self.check_ast_dict_formatted(val, filename, verbose):
1739 bad_files.add(filename)
1740
1741 if key.s == "name":
1742 self.type_assert(val, ast.Str, filename, verbose)
1743 waterfall_name = val
1744 assert waterfall_name
1745 keys.append(waterfall_name)
1746
Stephen Martinis1384ff92020-01-07 19:52:151747 if not self.check_ast_list_formatted(keys, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531748 bad_files.add(filename)
1749
1750 for filename in (
1751 'mixins.pyl',
1752 'test_suites.pyl',
1753 'test_suite_exceptions.pyl',
1754 ):
1755 value = parse_file(filename)
Stephen Martinisf83893722018-09-19 00:02:181756 # Value should be a dictionary.
Stephen Martinis54d64ad2018-09-21 22:16:201757 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181758
Stephen Martinis5bef0fc2020-01-06 22:47:531759 if not self.check_ast_dict_formatted(
1760 value, filename, verbose):
1761 bad_files.add(filename)
1762
Stephen Martinis54d64ad2018-09-21 22:16:201763 if filename == 'test_suites.pyl':
Jeff Yoon8154e582019-12-03 23:30:011764 expected_keys = ['basic_suites',
1765 'compound_suites',
1766 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201767 actual_keys = [node.s for node in value.keys]
1768 assert all(key in expected_keys for key in actual_keys), (
1769 'Invalid %r file; expected keys %r, got %r' % (
1770 filename, expected_keys, actual_keys))
1771 suite_dicts = [node for node in value.values]
1772 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011773 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201774 for suite_group in suite_dicts:
Stephen Martinis5bef0fc2020-01-06 22:47:531775 if not self.check_ast_dict_formatted(
Stephen Martinis54d64ad2018-09-21 22:16:201776 suite_group, filename, verbose):
1777 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181778
Stephen Martinis5bef0fc2020-01-06 22:47:531779 for key, suite in zip(value.keys, value.values):
1780 # The compound suites are checked in
1781 # 'check_composition_type_test_suites()'
1782 if key.s == 'basic_suites':
1783 for group in suite.values:
Stephen Martinis1384ff92020-01-07 19:52:151784 if not self.check_ast_dict_formatted(group, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531785 bad_files.add(filename)
1786 break
Stephen Martinis54d64ad2018-09-21 22:16:201787
Stephen Martinis5bef0fc2020-01-06 22:47:531788 elif filename == 'test_suite_exceptions.pyl':
1789 # Check the values for each test.
1790 for test in value.values:
1791 for kind, node in zip(test.keys, test.values):
1792 if isinstance(node, ast.Dict):
Stephen Martinis1384ff92020-01-07 19:52:151793 if not self.check_ast_dict_formatted(node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531794 bad_files.add(filename)
1795 elif kind.s == 'remove_from':
1796 # Don't care about sorting; these are usually grouped, since the
1797 # same bug can affect multiple builders. Do want to make sure
1798 # there aren't duplicates.
1799 if not self.check_ast_list_formatted(node.elts, filename, verbose,
1800 check_sorting=False):
1801 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181802
1803 if bad_files:
1804 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201805 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531806 'unsorted, or have duplicates. Re-run this with --verbose to see '
1807 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181808
Kenneth Russelleb60cbd22017-12-05 07:54:281809 def check_output_file_consistency(self, verbose=False):
1810 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011811 # All waterfalls/bucket .json files must have been written
1812 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:281813 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011814 ungenerated_files = set()
1815 for filename, expected_contents in self.generate_outputs().items():
1816 expected = self.jsonify(expected_contents)
1817 file_path = filename + '.json'
Zhiling Huangbe008172018-03-08 19:13:111818 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281819 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:011820 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:321821 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:011822 self.print_line('File ' + filename +
1823 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321824 'contents:')
1825 for line in difflib.unified_diff(
1826 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501827 current.splitlines(),
1828 fromfile='expected', tofile='current'):
1829 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:011830
1831 if ungenerated_files:
1832 raise BBGenErr(
1833 'The following files have not been properly '
1834 'autogenerated by generate_buildbot_json.py: ' +
1835 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:281836
1837 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501838 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281839 self.check_output_file_consistency(verbose) # pragma: no cover
1840
Karen Qiane24b7ee2019-02-12 23:37:061841 def does_test_match(self, test_info, params_dict):
1842 """Checks to see if the test matches the parameters given.
1843
1844 Compares the provided test_info with the params_dict to see
1845 if the bot matches the parameters given. If so, returns True.
1846 Else, returns false.
1847
1848 Args:
1849 test_info (dict): Information about a specific bot provided
1850 in the format shown in waterfalls.pyl
1851 params_dict (dict): Dictionary of parameters and their values
1852 to look for in the bot
1853 Ex: {
1854 'device_os':'android',
1855 '--flag':True,
1856 'mixins': ['mixin1', 'mixin2'],
1857 'ex_key':'ex_value'
1858 }
1859
1860 """
1861 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
1862 'kvm', 'pool', 'integrity'] # dimension parameters
1863 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
1864 'can_use_on_swarming_builders']
1865 for param in params_dict:
1866 # if dimension parameter
1867 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
1868 if not 'swarming' in test_info:
1869 return False
1870 swarming = test_info['swarming']
1871 if param in SWARMING_PARAMS:
1872 if not param in swarming:
1873 return False
1874 if not str(swarming[param]) == params_dict[param]:
1875 return False
1876 else:
1877 if not 'dimension_sets' in swarming:
1878 return False
1879 d_set = swarming['dimension_sets']
1880 # only looking at the first dimension set
1881 if not param in d_set[0]:
1882 return False
1883 if not d_set[0][param] == params_dict[param]:
1884 return False
1885
1886 # if flag
1887 elif param.startswith('--'):
1888 if not 'args' in test_info:
1889 return False
1890 if not param in test_info['args']:
1891 return False
1892
1893 # not dimension parameter/flag/mixin
1894 else:
1895 if not param in test_info:
1896 return False
1897 if not test_info[param] == params_dict[param]:
1898 return False
1899 return True
1900 def error_msg(self, msg):
1901 """Prints an error message.
1902
1903 In addition to a catered error message, also prints
1904 out where the user can find more help. Then, program exits.
1905 """
1906 self.print_line(msg + (' If you need more information, ' +
1907 'please run with -h or --help to see valid commands.'))
1908 sys.exit(1)
1909
1910 def find_bots_that_run_test(self, test, bots):
1911 matching_bots = []
1912 for bot in bots:
1913 bot_info = bots[bot]
1914 tests = self.flatten_tests_for_bot(bot_info)
1915 for test_info in tests:
1916 test_name = ""
1917 if 'name' in test_info:
1918 test_name = test_info['name']
1919 elif 'test' in test_info:
1920 test_name = test_info['test']
1921 if not test_name == test:
1922 continue
1923 matching_bots.append(bot)
1924 return matching_bots
1925
1926 def find_tests_with_params(self, tests, params_dict):
1927 matching_tests = []
1928 for test_name in tests:
1929 test_info = tests[test_name]
1930 if not self.does_test_match(test_info, params_dict):
1931 continue
1932 if not test_name in matching_tests:
1933 matching_tests.append(test_name)
1934 return matching_tests
1935
1936 def flatten_waterfalls_for_query(self, waterfalls):
1937 bots = {}
1938 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:011939 waterfall_tests = self.generate_output_tests(waterfall)
1940 for bot in waterfall_tests:
1941 bot_info = waterfall_tests[bot]
1942 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:061943 return bots
1944
1945 def flatten_tests_for_bot(self, bot_info):
1946 """Returns a list of flattened tests.
1947
1948 Returns a list of tests not grouped by test category
1949 for a specific bot.
1950 """
1951 TEST_CATS = self.get_test_generator_map().keys()
1952 tests = []
1953 for test_cat in TEST_CATS:
1954 if not test_cat in bot_info:
1955 continue
1956 test_cat_tests = bot_info[test_cat]
1957 tests = tests + test_cat_tests
1958 return tests
1959
1960 def flatten_tests_for_query(self, test_suites):
1961 """Returns a flattened dictionary of tests.
1962
1963 Returns a dictionary of tests associate with their
1964 configuration, not grouped by their test suite.
1965 """
1966 tests = {}
1967 for test_suite in test_suites.itervalues():
1968 for test in test_suite:
1969 test_info = test_suite[test]
1970 test_name = test
1971 if 'name' in test_info:
1972 test_name = test_info['name']
1973 tests[test_name] = test_info
1974 return tests
1975
1976 def parse_query_filter_params(self, params):
1977 """Parses the filter parameters.
1978
1979 Creates a dictionary from the parameters provided
1980 to filter the bot array.
1981 """
1982 params_dict = {}
1983 for p in params:
1984 # flag
1985 if p.startswith("--"):
1986 params_dict[p] = True
1987 else:
1988 pair = p.split(":")
1989 if len(pair) != 2:
1990 self.error_msg('Invalid command.')
1991 # regular parameters
1992 if pair[1].lower() == "true":
1993 params_dict[pair[0]] = True
1994 elif pair[1].lower() == "false":
1995 params_dict[pair[0]] = False
1996 else:
1997 params_dict[pair[0]] = pair[1]
1998 return params_dict
1999
2000 def get_test_suites_dict(self, bots):
2001 """Returns a dictionary of bots and their tests.
2002
2003 Returns a dictionary of bots and a list of their associated tests.
2004 """
2005 test_suite_dict = dict()
2006 for bot in bots:
2007 bot_info = bots[bot]
2008 tests = self.flatten_tests_for_bot(bot_info)
2009 test_suite_dict[bot] = tests
2010 return test_suite_dict
2011
2012 def output_query_result(self, result, json_file=None):
2013 """Outputs the result of the query.
2014
2015 If a json file parameter name is provided, then
2016 the result is output into the json file. If not,
2017 then the result is printed to the console.
2018 """
2019 output = json.dumps(result, indent=2)
2020 if json_file:
2021 self.write_file(json_file, output)
2022 else:
2023 self.print_line(output)
2024 return
2025
2026 def query(self, args):
2027 """Queries tests or bots.
2028
2029 Depending on the arguments provided, outputs a json of
2030 tests or bots matching the appropriate optional parameters provided.
2031 """
2032 # split up query statement
2033 query = args.query.split('/')
2034 self.load_configuration_files()
2035 self.resolve_configuration_files()
2036
2037 # flatten bots json
2038 tests = self.test_suites
2039 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2040
2041 cmd_class = query[0]
2042
2043 # For queries starting with 'bots'
2044 if cmd_class == "bots":
2045 if len(query) == 1:
2046 return self.output_query_result(bots, args.json)
2047 # query with specific parameters
2048 elif len(query) == 2:
2049 if query[1] == 'tests':
2050 test_suites_dict = self.get_test_suites_dict(bots)
2051 return self.output_query_result(test_suites_dict, args.json)
2052 else:
2053 self.error_msg("This query should be in the format: bots/tests.")
2054
2055 else:
2056 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2057 % str(len(query)-1))
2058
2059 # For queries starting with 'bot'
2060 elif cmd_class == "bot":
2061 if not len(query) == 2 and not len(query) == 3:
2062 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2063 % str(len(query)-1))
2064 bot_id = query[1]
2065 if not bot_id in bots:
2066 self.error_msg("No bot named '" + bot_id + "' found.")
2067 bot_info = bots[bot_id]
2068 if len(query) == 2:
2069 return self.output_query_result(bot_info, args.json)
2070 if not query[2] == 'tests':
2071 self.error_msg("The query should be in the format:" +
2072 "bot/<bot-name>/tests.")
2073
2074 bot_tests = self.flatten_tests_for_bot(bot_info)
2075 return self.output_query_result(bot_tests, args.json)
2076
2077 # For queries starting with 'tests'
2078 elif cmd_class == "tests":
2079 if not len(query) == 1 and not len(query) == 2:
2080 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2081 % str(len(query)-1))
2082 flattened_tests = self.flatten_tests_for_query(tests)
2083 if len(query) == 1:
2084 return self.output_query_result(flattened_tests, args.json)
2085
2086 # create params dict
2087 params = query[1].split('&')
2088 params_dict = self.parse_query_filter_params(params)
2089 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2090 return self.output_query_result(matching_bots)
2091
2092 # For queries starting with 'test'
2093 elif cmd_class == "test":
2094 if not len(query) == 2 and not len(query) == 3:
2095 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2096 % str(len(query)-1))
2097 test_id = query[1]
2098 if len(query) == 2:
2099 flattened_tests = self.flatten_tests_for_query(tests)
2100 for test in flattened_tests:
2101 if test == test_id:
2102 return self.output_query_result(flattened_tests[test], args.json)
2103 self.error_msg("There is no test named %s." % test_id)
2104 if not query[2] == 'bots':
2105 self.error_msg("The query should be in the format: " +
2106 "test/<test-name>/bots")
2107 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2108 return self.output_query_result(bots_for_test)
2109
2110 else:
2111 self.error_msg("Your command did not match any valid commands." +
2112 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:282113
Garrett Beaty1afaccc2020-06-25 19:58:152114 def main(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:282115 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502116 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062117 elif self.args.query:
2118 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282119 else:
Greg Gutermanf60eb052020-03-12 17:40:012120 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282121 return 0
2122
2123if __name__ == "__main__": # pragma: no cover
Garrett Beaty1afaccc2020-06-25 19:58:152124 generator = BBJSONGenerator(BBJSONGenerator.parse_args(sys.argv[1:]))
2125 sys.exit(generator.main())