blob: 438ba19df8a44d394e0929ac423d9e78ae41177f [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
24THIS_DIR = os.path.dirname(os.path.abspath(__file__))
25
26
27class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4928 def __init__(self, message):
29 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2830
31
Kenneth Russell8ceeabf2017-12-11 17:53:2832# This class is only present to accommodate certain machines on
33# chromium.android.fyi which run certain tests as instrumentation
34# tests, but not as gtests. If this discrepancy were fixed then the
35# notion could be removed.
36class TestSuiteTypes(object):
37 GTEST = 'gtest'
38
39
Kenneth Russelleb60cbd22017-12-05 07:54:2840class BaseGenerator(object):
41 def __init__(self, bb_gen):
42 self.bb_gen = bb_gen
43
Kenneth Russell8ceeabf2017-12-11 17:53:2844 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2845 raise NotImplementedError()
46
47 def sort(self, tests):
48 raise NotImplementedError()
49
50
Kenneth Russell8ceeabf2017-12-11 17:53:2851def cmp_tests(a, b):
52 # Prefer to compare based on the "test" key.
53 val = cmp(a['test'], b['test'])
54 if val != 0:
55 return val
56 if 'name' in a and 'name' in b:
57 return cmp(a['name'], b['name']) # pragma: no cover
58 if 'name' not in a and 'name' not in b:
59 return 0 # pragma: no cover
60 # Prefer to put variants of the same test after the first one.
61 if 'name' in a:
62 return 1
63 # 'name' is in b.
64 return -1 # pragma: no cover
65
66
Kenneth Russell8a386d42018-06-02 09:48:0167class GPUTelemetryTestGenerator(BaseGenerator):
Bo Liu555a0f92019-03-29 12:11:5668
69 def __init__(self, bb_gen, is_android_webview=False):
Kenneth Russell8a386d42018-06-02 09:48:0170 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5671 self._is_android_webview = is_android_webview
Kenneth Russell8a386d42018-06-02 09:48:0172
73 def generate(self, waterfall, tester_name, tester_config, input_tests):
74 isolated_scripts = []
75 for test_name, test_config in sorted(input_tests.iteritems()):
76 test = self.bb_gen.generate_gpu_telemetry_test(
Bo Liu555a0f92019-03-29 12:11:5677 waterfall, tester_name, tester_config, test_name, test_config,
78 self._is_android_webview)
Kenneth Russell8a386d42018-06-02 09:48:0179 if test:
80 isolated_scripts.append(test)
81 return isolated_scripts
82
83 def sort(self, tests):
84 return sorted(tests, key=lambda x: x['name'])
85
86
Kenneth Russelleb60cbd22017-12-05 07:54:2887class GTestGenerator(BaseGenerator):
88 def __init__(self, bb_gen):
89 super(GTestGenerator, self).__init__(bb_gen)
90
Kenneth Russell8ceeabf2017-12-11 17:53:2891 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2892 # The relative ordering of some of the tests is important to
93 # minimize differences compared to the handwritten JSON files, since
94 # Python's sorts are stable and there are some tests with the same
95 # key (see gles2_conform_d3d9_test and similar variants). Avoid
96 # losing the order by avoiding coalescing the dictionaries into one.
97 gtests = []
98 for test_name, test_config in sorted(input_tests.iteritems()):
Jeff Yoon67c3e832020-02-08 07:39:3899 # Variants allow more than one definition for a given test, and is defined
100 # in array format from resolve_variants().
101 if not isinstance(test_config, list):
102 test_config = [test_config]
103
104 for config in test_config:
105 test = self.bb_gen.generate_gtest(
106 waterfall, tester_name, tester_config, test_name, config)
107 if test:
108 # generate_gtest may veto the test generation on this tester.
109 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28110 return gtests
111
112 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28113 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28114
115
116class IsolatedScriptTestGenerator(BaseGenerator):
117 def __init__(self, bb_gen):
118 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
119
Kenneth Russell8ceeabf2017-12-11 17:53:28120 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28121 isolated_scripts = []
122 for test_name, test_config in sorted(input_tests.iteritems()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43123 # Variants allow more than one definition for a given test, and is defined
124 # in array format from resolve_variants().
125 if not isinstance(test_config, list):
126 test_config = [test_config]
127
128 for config in test_config:
129 test = self.bb_gen.generate_isolated_script_test(
130 waterfall, tester_name, tester_config, test_name, config)
131 if test:
132 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28133 return isolated_scripts
134
135 def sort(self, tests):
136 return sorted(tests, key=lambda x: x['name'])
137
138
139class ScriptGenerator(BaseGenerator):
140 def __init__(self, bb_gen):
141 super(ScriptGenerator, self).__init__(bb_gen)
142
Kenneth Russell8ceeabf2017-12-11 17:53:28143 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28144 scripts = []
145 for test_name, test_config in sorted(input_tests.iteritems()):
146 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28147 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28148 if test:
149 scripts.append(test)
150 return scripts
151
152 def sort(self, tests):
153 return sorted(tests, key=lambda x: x['name'])
154
155
156class JUnitGenerator(BaseGenerator):
157 def __init__(self, bb_gen):
158 super(JUnitGenerator, self).__init__(bb_gen)
159
Kenneth Russell8ceeabf2017-12-11 17:53:28160 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28161 scripts = []
162 for test_name, test_config in sorted(input_tests.iteritems()):
163 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28164 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28165 if test:
166 scripts.append(test)
167 return scripts
168
169 def sort(self, tests):
170 return sorted(tests, key=lambda x: x['test'])
171
172
173class CTSGenerator(BaseGenerator):
174 def __init__(self, bb_gen):
175 super(CTSGenerator, self).__init__(bb_gen)
176
Kenneth Russell8ceeabf2017-12-11 17:53:28177 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28178 # These only contain one entry and it's the contents of the input tests'
179 # dictionary, verbatim.
180 cts_tests = []
181 cts_tests.append(input_tests)
182 return cts_tests
183
184 def sort(self, tests):
185 return tests
186
187
188class InstrumentationTestGenerator(BaseGenerator):
189 def __init__(self, bb_gen):
190 super(InstrumentationTestGenerator, self).__init__(bb_gen)
191
Kenneth Russell8ceeabf2017-12-11 17:53:28192 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28193 scripts = []
194 for test_name, test_config in sorted(input_tests.iteritems()):
195 test = self.bb_gen.generate_instrumentation_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28196 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28197 if test:
198 scripts.append(test)
199 return scripts
200
201 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28202 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28203
204
Jeff Yoon67c3e832020-02-08 07:39:38205def check_compound_references(other_test_suites=None,
206 sub_suite=None,
207 suite=None,
208 target_test_suites=None,
209 test_type=None,
210 **kwargs):
211 """Ensure comound reference's don't target other compounds"""
212 del kwargs
213 if sub_suite in other_test_suites or sub_suite in target_test_suites:
214 raise BBGenErr('%s may not refer to other composition type test '
215 'suites (error found while processing %s)'
216 % (test_type, suite))
217
218def check_basic_references(basic_suites=None,
219 sub_suite=None,
220 suite=None,
221 **kwargs):
222 """Ensure test has a basic suite reference"""
223 del kwargs
224 if sub_suite not in basic_suites:
225 raise BBGenErr('Unable to find reference to %s while processing %s'
226 % (sub_suite, suite))
227
228def check_conflicting_definitions(basic_suites=None,
229 seen_tests=None,
230 sub_suite=None,
231 suite=None,
232 test_type=None,
233 **kwargs):
234 """Ensure that if a test is reachable via multiple basic suites,
235 all of them have an identical definition of the tests.
236 """
237 del kwargs
238 for test_name in basic_suites[sub_suite]:
239 if (test_name in seen_tests and
240 basic_suites[sub_suite][test_name] !=
241 basic_suites[seen_tests[test_name]][test_name]):
242 raise BBGenErr('Conflicting test definitions for %s from %s '
243 'and %s in %s (error found while processing %s)'
244 % (test_name, seen_tests[test_name], sub_suite,
245 test_type, suite))
246 seen_tests[test_name] = sub_suite
247
248def check_matrix_identifier(sub_suite=None,
249 suite=None,
250 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05251 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38252 **kwargs):
253 """Ensure 'idenfitier' is defined for each variant"""
254 del kwargs
255 sub_suite_config = suite_def[sub_suite]
256 for variant in sub_suite_config.get('variants', []):
Jeff Yoonda581c32020-03-06 03:56:05257 if isinstance(variant, str):
258 if variant not in all_variants:
259 raise BBGenErr('Missing variant definition for %s in variants.pyl'
260 % variant)
261 variant = all_variants[variant]
262
Jeff Yoon67c3e832020-02-08 07:39:38263 if not 'identifier' in variant:
264 raise BBGenErr('Missing required identifier field in matrix '
265 'compound suite %s, %s' % (suite, sub_suite))
266
267
Kenneth Russelleb60cbd22017-12-05 07:54:28268class BBJSONGenerator(object):
269 def __init__(self):
270 self.this_dir = THIS_DIR
271 self.args = None
272 self.waterfalls = None
273 self.test_suites = None
274 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01275 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41276 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05277 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28278
279 def generate_abs_file_path(self, relative_path):
280 return os.path.join(self.this_dir, relative_path) # pragma: no cover
281
Stephen Martinis7eb8b612018-09-21 00:17:50282 def print_line(self, line):
283 # Exists so that tests can mock
284 print line # pragma: no cover
285
Kenneth Russelleb60cbd22017-12-05 07:54:28286 def read_file(self, relative_path):
287 with open(self.generate_abs_file_path(
288 relative_path)) as fp: # pragma: no cover
289 return fp.read() # pragma: no cover
290
291 def write_file(self, relative_path, contents):
292 with open(self.generate_abs_file_path(
293 relative_path), 'wb') as fp: # pragma: no cover
294 fp.write(contents) # pragma: no cover
295
Zhiling Huangbe008172018-03-08 19:13:11296 def pyl_file_path(self, filename):
297 if self.args and self.args.pyl_files_dir:
298 return os.path.join(self.args.pyl_files_dir, filename)
299 return filename
300
Kenneth Russelleb60cbd22017-12-05 07:54:28301 def load_pyl_file(self, filename):
302 try:
Zhiling Huangbe008172018-03-08 19:13:11303 return ast.literal_eval(self.read_file(
304 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28305 except (SyntaxError, ValueError) as e: # pragma: no cover
306 raise BBGenErr('Failed to parse pyl file "%s": %s' %
307 (filename, e)) # pragma: no cover
308
Kenneth Russell8a386d42018-06-02 09:48:01309 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
310 # Currently it is only mandatory for bots which run GPU tests. Change these to
311 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28312 def is_android(self, tester_config):
313 return tester_config.get('os_type') == 'android'
314
Ben Pastenea9e583b2019-01-16 02:57:26315 def is_chromeos(self, tester_config):
316 return tester_config.get('os_type') == 'chromeos'
317
Kenneth Russell8a386d42018-06-02 09:48:01318 def is_linux(self, tester_config):
319 return tester_config.get('os_type') == 'linux'
320
Kai Ninomiya40de9f52019-10-18 21:38:49321 def is_mac(self, tester_config):
322 return tester_config.get('os_type') == 'mac'
323
324 def is_win(self, tester_config):
325 return tester_config.get('os_type') == 'win'
326
327 def is_win64(self, tester_config):
328 return (tester_config.get('os_type') == 'win' and
329 tester_config.get('browser_config') == 'release_x64')
330
Kenneth Russelleb60cbd22017-12-05 07:54:28331 def get_exception_for_test(self, test_name, test_config):
332 # gtests may have both "test" and "name" fields, and usually, if the "name"
333 # field is specified, it means that the same test is being repurposed
334 # multiple times with different command line arguments. To handle this case,
335 # prefer to lookup per the "name" field of the test itself, as opposed to
336 # the "test_name", which is actually the "test" field.
337 if 'name' in test_config:
338 return self.exceptions.get(test_config['name'])
339 else:
340 return self.exceptions.get(test_name)
341
Nico Weberb0b3f5862018-07-13 18:45:15342 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28343 # Currently, the only reason a test should not run on a given tester is that
344 # it's in the exceptions. (Once the GPU waterfall generation script is
345 # incorporated here, the rules will become more complex.)
346 exception = self.get_exception_for_test(test_name, test_config)
347 if not exception:
348 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28349 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28350 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28351 if remove_from:
352 if tester_name in remove_from:
353 return False
354 # TODO(kbr): this code path was added for some tests (including
355 # android_webview_unittests) on one machine (Nougat Phone
356 # Tester) which exists with the same name on two waterfalls,
357 # chromium.android and chromium.fyi; the tests are run on one
358 # but not the other. Once the bots are all uniquely named (a
359 # different ongoing project) this code should be removed.
360 # TODO(kbr): add coverage.
361 return (tester_name + ' ' + waterfall['name']
362 not in remove_from) # pragma: no cover
363 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28364
Nico Weber79dc5f6852018-07-13 19:38:49365 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28366 exception = self.get_exception_for_test(test_name, test)
367 if not exception:
368 return None
Nico Weber79dc5f6852018-07-13 19:38:49369 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28370
Brian Sheedye6ea0ee2019-07-11 02:54:37371 def get_test_replacements(self, test, test_name, tester_name):
372 exception = self.get_exception_for_test(test_name, test)
373 if not exception:
374 return None
375 return exception.get('replacements', {}).get(tester_name)
376
Kenneth Russell8a386d42018-06-02 09:48:01377 def merge_command_line_args(self, arr, prefix, splitter):
378 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01379 idx = 0
380 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01381 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01382 while idx < len(arr):
383 flag = arr[idx]
384 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01385 if flag.startswith(prefix):
386 arg = flag[prefix_len:]
387 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01388 if first_idx < 0:
389 first_idx = idx
390 else:
391 delete_current_entry = True
392 if delete_current_entry:
393 del arr[idx]
394 else:
395 idx += 1
396 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01397 arr[first_idx] = prefix + splitter.join(accumulated_args)
398 return arr
399
400 def maybe_fixup_args_array(self, arr):
401 # The incoming array of strings may be an array of command line
402 # arguments. To make it easier to turn on certain features per-bot or
403 # per-test-suite, look specifically for certain flags and merge them
404 # appropriately.
405 # --enable-features=Feature1 --enable-features=Feature2
406 # are merged to:
407 # --enable-features=Feature1,Feature2
408 # and:
409 # --extra-browser-args=arg1 --extra-browser-args=arg2
410 # are merged to:
411 # --extra-browser-args=arg1 arg2
412 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
413 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01414 return arr
415
Kenneth Russelleb60cbd22017-12-05 07:54:28416 def dictionary_merge(self, a, b, path=None, update=True):
417 """http://stackoverflow.com/questions/7204805/
418 python-dictionaries-of-dictionaries-merge
419 merges b into a
420 """
421 if path is None:
422 path = []
423 for key in b:
424 if key in a:
425 if isinstance(a[key], dict) and isinstance(b[key], dict):
426 self.dictionary_merge(a[key], b[key], path + [str(key)])
427 elif a[key] == b[key]:
428 pass # same leaf value
429 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06430 # Args arrays are lists of strings. Just concatenate them,
431 # and don't sort them, in order to keep some needed
432 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28433 if all(isinstance(x, str)
434 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01435 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28436 else:
437 # TODO(kbr): this only works properly if the two arrays are
438 # the same length, which is currently always the case in the
439 # swarming dimension_sets that we have to merge. It will fail
440 # to merge / override 'args' arrays which are different
441 # length.
442 for idx in xrange(len(b[key])):
443 try:
444 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
445 path + [str(key), str(idx)],
446 update=update)
Jeff Yoon8154e582019-12-03 23:30:01447 except (IndexError, TypeError):
448 raise BBGenErr('Error merging lists by key "%s" from source %s '
449 'into target %s at index %s. Verify target list '
450 'length is equal or greater than source'
451 % (str(key), str(b), str(a), str(idx)))
John Budorick5bc387fe2019-05-09 20:02:53452 elif update:
453 if b[key] is None:
454 del a[key]
455 else:
456 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28457 else:
458 raise BBGenErr('Conflict at %s' % '.'.join(
459 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53460 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28461 a[key] = b[key]
462 return a
463
John Budorickab108712018-09-01 00:12:21464 def initialize_args_for_test(
465 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21466 args = []
467 args.extend(generated_test.get('args', []))
468 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22469
Kenneth Russell8a386d42018-06-02 09:48:01470 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21471 val = generated_test.pop(key, [])
472 if fn(tester_config):
473 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01474
475 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
476 add_conditional_args('linux_args', self.is_linux)
477 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36478 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49479 add_conditional_args('mac_args', self.is_mac)
480 add_conditional_args('win_args', self.is_win)
481 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01482
John Budorickab108712018-09-01 00:12:21483 for key in additional_arg_keys or []:
484 args.extend(generated_test.pop(key, []))
485 args.extend(tester_config.get(key, []))
486
487 if args:
488 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01489
Kenneth Russelleb60cbd22017-12-05 07:54:28490 def initialize_swarming_dictionary_for_test(self, generated_test,
491 tester_config):
492 if 'swarming' not in generated_test:
493 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28494 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
495 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38496 'can_use_on_swarming_builders': tester_config.get('use_swarming',
497 True)
Dirk Pranke81ff51c2017-12-09 19:24:28498 })
Kenneth Russelleb60cbd22017-12-05 07:54:28499 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03500 if ('dimension_sets' not in generated_test['swarming'] and
501 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28502 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
503 tester_config['swarming']['dimension_sets'])
504 self.dictionary_merge(generated_test['swarming'],
505 tester_config['swarming'])
506 # Apply any Android-specific Swarming dimensions after the generic ones.
507 if 'android_swarming' in generated_test:
508 if self.is_android(tester_config): # pragma: no cover
509 self.dictionary_merge(
510 generated_test['swarming'],
511 generated_test['android_swarming']) # pragma: no cover
512 del generated_test['android_swarming'] # pragma: no cover
513
514 def clean_swarming_dictionary(self, swarming_dict):
515 # Clean out redundant entries from a test's "swarming" dictionary.
516 # This is really only needed to retain 100% parity with the
517 # handwritten JSON files, and can be removed once all the files are
518 # autogenerated.
519 if 'shards' in swarming_dict:
520 if swarming_dict['shards'] == 1: # pragma: no cover
521 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24522 if 'hard_timeout' in swarming_dict:
523 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
524 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43525 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28526 # Remove all other keys.
527 for k in swarming_dict.keys(): # pragma: no cover
528 if k != 'can_use_on_swarming_builders': # pragma: no cover
529 del swarming_dict[k] # pragma: no cover
530
Stephen Martinis0382bc12018-09-17 22:29:07531 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
532 waterfall):
533 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01534 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07535 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28536 # See if there are any exceptions that need to be merged into this
537 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49538 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28539 if modifications:
540 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23541 if 'swarming' in test:
542 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28543 # Ensure all Android Swarming tests run only on userdebug builds if another
544 # build type was not specified.
545 if 'swarming' in test and self.is_android(tester_config):
546 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22547 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28548 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37549 self.replace_test_args(test, test_name, tester_name)
Ben Pastenee012aea42019-05-14 22:32:28550
Kenneth Russelleb60cbd22017-12-05 07:54:28551 return test
552
Brian Sheedye6ea0ee2019-07-11 02:54:37553 def replace_test_args(self, test, test_name, tester_name):
554 replacements = self.get_test_replacements(
555 test, test_name, tester_name) or {}
556 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
557 for key, replacement_dict in replacements.iteritems():
558 if key not in valid_replacement_keys:
559 raise BBGenErr(
560 'Given replacement key %s for %s on %s is not in the list of valid '
561 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
562 for replacement_key, replacement_val in replacement_dict.iteritems():
563 found_key = False
564 for i, test_key in enumerate(test.get(key, [])):
565 # Handle both the key/value being replaced being defined as two
566 # separate items or as key=value.
567 if test_key == replacement_key:
568 found_key = True
569 # Handle flags without values.
570 if replacement_val == None:
571 del test[key][i]
572 else:
573 test[key][i+1] = replacement_val
574 break
575 elif test_key.startswith(replacement_key + '='):
576 found_key = True
577 if replacement_val == None:
578 del test[key][i]
579 else:
580 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
581 break
582 if not found_key:
583 raise BBGenErr('Could not find %s in existing list of values for key '
584 '%s in %s on %s' % (replacement_key, key, test_name,
585 tester_name))
586
Shenghua Zhangaba8bad2018-02-07 02:12:09587 def add_common_test_properties(self, test, tester_config):
588 if tester_config.get('use_multi_dimension_trigger_script'):
Kenneth Russell73c3bd8b2018-10-19 22:30:19589 # Assumes update_and_cleanup_test has already been called, so the
590 # builder's mixins have been flattened into the test.
Shenghua Zhangaba8bad2018-02-07 02:12:09591 test['trigger_script'] = {
592 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
593 'args': [
594 '--multiple-trigger-configs',
Kenneth Russell73c3bd8b2018-10-19 22:30:19595 json.dumps(test['swarming']['dimension_sets'] +
Shenghua Zhangaba8bad2018-02-07 02:12:09596 tester_config.get('alternate_swarming_dimensions', [])),
597 '--multiple-dimension-script-verbose',
598 'True'
599 ],
600 }
Ben Pastenea9e583b2019-01-16 02:57:26601 elif self.is_chromeos(tester_config) and tester_config.get('use_swarming',
602 True):
603 # The presence of the "device_type" dimension indicates that the tests
604 # are targetting CrOS hardware and so need the special trigger script.
605 dimension_sets = tester_config['swarming']['dimension_sets']
606 if all('device_type' in ds for ds in dimension_sets):
607 test['trigger_script'] = {
608 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
609 }
Shenghua Zhangaba8bad2018-02-07 02:12:09610
Ben Pastene858f4be2019-01-09 23:52:09611 def add_android_presentation_args(self, tester_config, test_name, result):
612 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38613 bucket = tester_config.get('results_bucket', 'chromium-result-details')
614 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09615 if (result['swarming']['can_use_on_swarming_builders'] and not
616 tester_config.get('skip_merge_script', False)):
617 result['merge'] = {
618 'args': [
619 '--bucket',
John Budorick262ae112019-07-12 19:24:38620 bucket,
Ben Pastene858f4be2019-01-09 23:52:09621 '--test-name',
622 test_name
623 ],
624 'script': '//build/android/pylib/results/presentation/'
625 'test_results_presentation.py',
626 }
627 if not tester_config.get('skip_cipd_packages', False):
Ben Pastenee5949ea82019-01-10 21:45:26628 cipd_packages = result['swarming'].get('cipd_packages', [])
629 cipd_packages.append(
Ben Pastene858f4be2019-01-09 23:52:09630 {
631 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
632 'location': 'bin',
633 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
634 }
Ben Pastenee5949ea82019-01-10 21:45:26635 )
636 result['swarming']['cipd_packages'] = cipd_packages
Ben Pastene858f4be2019-01-09 23:52:09637 if not tester_config.get('skip_output_links', False):
638 result['swarming']['output_links'] = [
639 {
640 'link': [
641 'https://luci-logdog.appspot.com/v/?s',
642 '=android%2Fswarming%2Flogcats%2F',
643 '${TASK_ID}%2F%2B%2Funified_logcats',
644 ],
645 'name': 'shard #${SHARD_INDEX} logcats',
646 },
647 ]
648 if args:
649 result['args'] = args
650
Kenneth Russelleb60cbd22017-12-05 07:54:28651 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
652 test_config):
653 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15654 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28655 return None
656 result = copy.deepcopy(test_config)
657 if 'test' in result:
658 result['name'] = test_name
659 else:
660 result['test'] = test_name
661 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21662
663 self.initialize_args_for_test(
664 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28665 if self.is_android(tester_config) and tester_config.get('use_swarming',
666 True):
Ben Pastene858f4be2019-01-09 23:52:09667 self.add_android_presentation_args(tester_config, test_name, result)
668 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42669
Stephen Martinis0382bc12018-09-17 22:29:07670 result = self.update_and_cleanup_test(
671 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09672 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43673
674 if not result.get('merge'):
675 # TODO(https://crbug.com/958376): Consider adding the ability to not have
676 # this default.
677 result['merge'] = {
678 'script': '//testing/merge_scripts/standard_gtest_merge.py',
679 'args': [],
680 }
Kenneth Russelleb60cbd22017-12-05 07:54:28681 return result
682
683 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
684 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01685 if not self.should_run_on_tester(waterfall, tester_name, test_name,
686 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28687 return None
688 result = copy.deepcopy(test_config)
689 result['isolate_name'] = result.get('isolate_name', test_name)
Jeff Yoonb8bfdbf32020-03-13 19:14:43690 result['name'] = result.get('name', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28691 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01692 self.initialize_args_for_test(result, tester_config)
Ben Pastene858f4be2019-01-09 23:52:09693 if tester_config.get('use_android_presentation', False):
694 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07695 result = self.update_and_cleanup_test(
696 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09697 self.add_common_test_properties(result, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17698
699 if not result.get('merge'):
700 # TODO(https://crbug.com/958376): Consider adding the ability to not have
701 # this default.
702 result['merge'] = {
703 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
704 'args': [],
705 }
Kenneth Russelleb60cbd22017-12-05 07:54:28706 return result
707
708 def generate_script_test(self, waterfall, tester_name, tester_config,
709 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44710 # TODO(https://crbug.com/953072): Remove this check whenever a better
711 # long-term solution is implemented.
712 if (waterfall.get('forbid_script_tests', False) or
713 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
714 raise BBGenErr('Attempted to generate a script test on tester ' +
715 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01716 if not self.should_run_on_tester(waterfall, tester_name, test_name,
717 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28718 return None
719 result = {
720 'name': test_name,
721 'script': test_config['script']
722 }
Stephen Martinis0382bc12018-09-17 22:29:07723 result = self.update_and_cleanup_test(
724 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28725 return result
726
727 def generate_junit_test(self, waterfall, tester_name, tester_config,
728 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01729 if not self.should_run_on_tester(waterfall, tester_name, test_name,
730 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28731 return None
John Budorickdef6acb2019-09-17 22:51:09732 result = copy.deepcopy(test_config)
733 result.update({
John Budorickcadc4952019-09-16 23:51:37734 'name': test_name,
735 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09736 })
737 self.initialize_args_for_test(result, tester_config)
738 result = self.update_and_cleanup_test(
739 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28740 return result
741
742 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
743 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01744 if not self.should_run_on_tester(waterfall, tester_name, test_name,
745 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28746 return None
747 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28748 if 'test' in result and result['test'] != test_name:
749 result['name'] = test_name
750 else:
751 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07752 result = self.update_and_cleanup_test(
753 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28754 return result
755
Stephen Martinis2a0667022018-09-25 22:31:14756 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01757 substitutions = {
758 # Any machine in waterfalls.pyl which desires to run GPU tests
759 # must provide the os_type key.
760 'os_type': tester_config['os_type'],
761 'gpu_vendor_id': '0',
762 'gpu_device_id': '0',
763 }
Stephen Martinis2a0667022018-09-25 22:31:14764 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01765 if 'gpu' in dimension_set:
766 # First remove the driver version, then split into vendor and device.
767 gpu = dimension_set['gpu']
Kenneth Russell384a1732019-03-16 02:36:02768 # Handle certain specialized named GPUs.
769 if gpu.startswith('nvidia-quadro-p400'):
770 gpu = ['10de', '1cb3']
771 elif gpu.startswith('intel-hd-630'):
772 gpu = ['8086', '5912']
Brian Sheedyf9387db7b2019-08-05 19:26:10773 elif gpu.startswith('intel-uhd-630'):
774 gpu = ['8086', '3e92']
Kenneth Russell384a1732019-03-16 02:36:02775 else:
776 gpu = gpu.split('-')[0].split(':')
Kenneth Russell8a386d42018-06-02 09:48:01777 substitutions['gpu_vendor_id'] = gpu[0]
778 substitutions['gpu_device_id'] = gpu[1]
779 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
780
781 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56782 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01783 # These are all just specializations of isolated script tests with
784 # a bunch of boilerplate command line arguments added.
785
786 # The step name must end in 'test' or 'tests' in order for the
787 # results to automatically show up on the flakiness dashboard.
788 # (At least, this was true some time ago.) Continue to use this
789 # naming convention for the time being to minimize changes.
790 step_name = test_config.get('name', test_name)
791 if not (step_name.endswith('test') or step_name.endswith('tests')):
792 step_name = '%s_tests' % step_name
793 result = self.generate_isolated_script_test(
794 waterfall, tester_name, tester_config, step_name, test_config)
795 if not result:
796 return None
Chong Gub75754b32020-03-13 16:39:20797 result['isolate_name'] = test_config.get(
798 'isolate_name', 'telemetry_gpu_integration_test')
Chan Liab7d8dd82020-04-24 23:42:19799
Chan Lia3ad1502020-04-28 05:32:11800 # Populate test_id_prefix.
Chan Liab7d8dd82020-04-24 23:42:19801 gn_entry = (
802 self.gn_isolate_map.get(result['isolate_name']) or
803 self.gn_isolate_map.get('telemetry_gpu_integration_test'))
Chan Lia3ad1502020-04-28 05:32:11804 result['test_id_prefix'] = 'ninja:%s/%s/' % (gn_entry['label'], step_name)
Chan Liab7d8dd82020-04-24 23:42:19805
Kenneth Russell8a386d42018-06-02 09:48:01806 args = result.get('args', [])
807 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14808
809 # These tests upload and download results from cloud storage and therefore
810 # aren't idempotent yet. https://crbug.com/549140.
811 result['swarming']['idempotent'] = False
812
Kenneth Russell44910c32018-12-03 23:35:11813 # The GPU tests act much like integration tests for the entire browser, and
814 # tend to uncover flakiness bugs more readily than other test suites. In
815 # order to surface any flakiness more readily to the developer of the CL
816 # which is introducing it, we disable retries with patch on the commit
817 # queue.
818 result['should_retry_with_patch'] = False
819
Bo Liu555a0f92019-03-29 12:11:56820 browser = ('android-webview-instrumentation'
821 if is_android_webview else tester_config['browser_config'])
Kenneth Russell8a386d42018-06-02 09:48:01822 args = [
Bo Liu555a0f92019-03-29 12:11:56823 test_to_run,
824 '--show-stdout',
825 '--browser=%s' % browser,
826 # --passthrough displays more of the logging in Telemetry when
827 # run via typ, in particular some of the warnings about tests
828 # being expected to fail, but passing.
829 '--passthrough',
830 '-v',
831 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
Kenneth Russell8a386d42018-06-02 09:48:01832 ] + args
833 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14834 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01835 return result
836
Kenneth Russelleb60cbd22017-12-05 07:54:28837 def get_test_generator_map(self):
838 return {
Bo Liu555a0f92019-03-29 12:11:56839 'android_webview_gpu_telemetry_tests':
840 GPUTelemetryTestGenerator(self, is_android_webview=True),
841 'cts_tests':
842 CTSGenerator(self),
843 'gpu_telemetry_tests':
844 GPUTelemetryTestGenerator(self),
845 'gtest_tests':
846 GTestGenerator(self),
847 'instrumentation_tests':
848 InstrumentationTestGenerator(self),
849 'isolated_scripts':
850 IsolatedScriptTestGenerator(self),
851 'junit_tests':
852 JUnitGenerator(self),
853 'scripts':
854 ScriptGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28855 }
856
Kenneth Russell8a386d42018-06-02 09:48:01857 def get_test_type_remapper(self):
858 return {
859 # These are a specialization of isolated_scripts with a bunch of
860 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:56861 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:01862 'gpu_telemetry_tests': 'isolated_scripts',
863 }
864
Jeff Yoon67c3e832020-02-08 07:39:38865 def check_composition_type_test_suites(self, test_type,
866 additional_validators=None):
867 """Pre-pass to catch errors reliabily for compound/matrix suites"""
868 validators = [check_compound_references,
869 check_basic_references,
870 check_conflicting_definitions]
871 if additional_validators:
872 validators += additional_validators
873
874 target_suites = self.test_suites.get(test_type, {})
875 other_test_type = ('compound_suites'
876 if test_type == 'matrix_compound_suites'
877 else 'matrix_compound_suites')
878 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:01879 basic_suites = self.test_suites.get('basic_suites', {})
880
Jeff Yoon67c3e832020-02-08 07:39:38881 for suite, suite_def in target_suites.iteritems():
Jeff Yoon8154e582019-12-03 23:30:01882 if suite in basic_suites:
883 raise BBGenErr('%s names may not duplicate basic test suite names '
884 '(error found while processsing %s)'
885 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:01886
Jeff Yoon67c3e832020-02-08 07:39:38887 seen_tests = {}
888 for sub_suite in suite_def:
889 for validator in validators:
890 validator(
891 basic_suites=basic_suites,
892 other_test_suites=other_suites,
893 seen_tests=seen_tests,
894 sub_suite=sub_suite,
895 suite=suite,
896 suite_def=suite_def,
897 target_test_suites=target_suites,
898 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:05899 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:38900 )
Kenneth Russelleb60cbd22017-12-05 07:54:28901
Stephen Martinis54d64ad2018-09-21 22:16:20902 def flatten_test_suites(self):
903 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:01904 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
905 for category in test_types:
906 for name, value in self.test_suites.get(category, {}).iteritems():
907 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:20908 self.test_suites = new_test_suites
909
Chan Lia3ad1502020-04-28 05:32:11910 def resolve_test_id_prefixes(self):
Nodir Turakulovfce34292019-12-18 17:05:41911 for suite in self.test_suites['basic_suites'].itervalues():
912 for key, test in suite.iteritems():
913 if not isinstance(test, dict):
914 # Some test definitions are just strings, such as CTS.
915 # Skip them.
916 continue
917
918 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
919 # https://source.chromium.org/chromium/chromium/tools/build/+/master:scripts/slave/recipe_modules/chromium_tests/generators.py;l=89;drc=14c062ba0eb418d3c4623dde41a753241b9df06b
920 # TODO(crbug.com/1035124): clean this up.
921 isolate_name = test.get('test') or test.get('isolate_name') or key
922 gn_entry = self.gn_isolate_map.get(isolate_name)
923 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:28924 label = gn_entry['label']
925
926 if label.count(':') != 1:
927 raise BBGenErr(
928 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
929 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
930 (label, isolate_name))
931 if label.split(':')[1] != isolate_name:
932 raise BBGenErr(
933 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
934 ' label "%s" see http://crbug.com/1071091 for details.' %
935 (isolate_name, label))
936
Chan Lia3ad1502020-04-28 05:32:11937 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:41938 else: # pragma: no cover
939 # Some tests do not have an entry gn_isolate_map.pyl, such as
940 # telemetry tests.
941 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
942 pass
943
Kenneth Russelleb60cbd22017-12-05 07:54:28944 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:01945 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:20946
Jeff Yoon8154e582019-12-03 23:30:01947 compound_suites = self.test_suites.get('compound_suites', {})
948 # check_composition_type_test_suites() checks that all basic suites
949 # referenced by compound suites exist.
950 basic_suites = self.test_suites.get('basic_suites')
951
952 for name, value in compound_suites.iteritems():
953 # Resolve this to a dictionary.
954 full_suite = {}
955 for entry in value:
956 suite = basic_suites[entry]
957 full_suite.update(suite)
958 compound_suites[name] = full_suite
959
Jeff Yoon67c3e832020-02-08 07:39:38960 def resolve_variants(self, basic_test_definition, variants):
961 """ Merge variant-defined configurations to each test case definition in a
962 test suite.
963
964 The output maps a unique test name to an array of configurations because
965 there may exist more than one definition for a test name using variants. The
966 test name is referenced while mapping machines to test suites, so unpacking
967 the array is done by the generators.
968
969 Args:
970 basic_test_definition: a {} defined test suite in the format
971 test_name:test_config
972 variants: an [] of {} defining configurations to be applied to each test
973 case in the basic test_definition
974
975 Return:
976 a {} of test_name:[{}], where each {} is a merged configuration
977 """
978
979 # Each test in a basic test suite will have a definition per variant.
980 test_suite = {}
981 for test_name, test_config in basic_test_definition.iteritems():
982 definitions = []
983 for variant in variants:
Jeff Yoonda581c32020-03-06 03:56:05984 # Unpack the variant from variants.pyl if it's string based.
985 if isinstance(variant, str):
986 variant = self.variants[variant]
987
Jeff Yoon67c3e832020-02-08 07:39:38988 # Clone a copy of test_config so that we can have a uniquely updated
989 # version of it per variant
990 cloned_config = copy.deepcopy(test_config)
991 # The variant definition needs to be re-used for each test, so we'll
992 # create a clone and work with it as well.
993 cloned_variant = copy.deepcopy(variant)
994
995 cloned_config['args'] = (cloned_config.get('args', []) +
996 cloned_variant.get('args', []))
997 cloned_config['mixins'] = (cloned_config.get('mixins', []) +
998 cloned_variant.get('mixins', []))
999
1000 basic_swarming_def = cloned_config.get('swarming', {})
1001 variant_swarming_def = cloned_variant.get('swarming', {})
1002 if basic_swarming_def and variant_swarming_def:
1003 if ('dimension_sets' in basic_swarming_def and
1004 'dimension_sets' in variant_swarming_def):
1005 # Retain swarming dimension set merge behavior when both variant and
1006 # the basic test configuration both define it
1007 self.dictionary_merge(basic_swarming_def, variant_swarming_def)
1008 # Remove dimension_sets from the variant definition, so that it does
1009 # not replace what's been done by dictionary_merge in the update
1010 # call below.
1011 del variant_swarming_def['dimension_sets']
1012
1013 # Update the swarming definition with whatever is defined for swarming
1014 # by the variant.
1015 basic_swarming_def.update(variant_swarming_def)
1016 cloned_config['swarming'] = basic_swarming_def
1017
1018 # The identifier is used to make the name of the test unique.
1019 # Generators in the recipe uniquely identify a test by it's name, so we
1020 # don't want to have the same name for each variant.
1021 cloned_config['name'] = '{}_{}'.format(test_name,
1022 cloned_variant['identifier'])
Jeff Yoon67c3e832020-02-08 07:39:381023 definitions.append(cloned_config)
1024 test_suite[test_name] = definitions
1025 return test_suite
1026
Jeff Yoon8154e582019-12-03 23:30:011027 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381028 self.check_composition_type_test_suites('matrix_compound_suites',
1029 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011030
1031 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381032 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011033 # referenced by matrix suites exist.
1034 basic_suites = self.test_suites.get('basic_suites')
1035
Jeff Yoon67c3e832020-02-08 07:39:381036 for test_name, matrix_config in matrix_compound_suites.iteritems():
Jeff Yoon8154e582019-12-03 23:30:011037 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381038
1039 for test_suite, mtx_test_suite_config in matrix_config.iteritems():
1040 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1041
1042 if 'variants' in mtx_test_suite_config:
1043 result = self.resolve_variants(basic_test_def,
1044 mtx_test_suite_config['variants'])
1045 full_suite.update(result)
1046 matrix_compound_suites[test_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281047
1048 def link_waterfalls_to_test_suites(self):
1049 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431050 for tester_name, tester in waterfall['machines'].iteritems():
1051 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281052 if not value in self.test_suites:
1053 # Hard / impossible to cover this in the unit test.
1054 raise self.unknown_test_suite(
1055 value, tester_name, waterfall['name']) # pragma: no cover
1056 tester['test_suites'][suite] = self.test_suites[value]
1057
1058 def load_configuration_files(self):
1059 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
1060 self.test_suites = self.load_pyl_file('test_suites.pyl')
1061 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:011062 self.mixins = self.load_pyl_file('mixins.pyl')
Nodir Turakulovfce34292019-12-18 17:05:411063 self.gn_isolate_map = self.load_pyl_file('gn_isolate_map.pyl')
Jeff Yoonda581c32020-03-06 03:56:051064 self.variants = self.load_pyl_file('variants.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:281065
1066 def resolve_configuration_files(self):
Chan Lia3ad1502020-04-28 05:32:111067 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281068 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011069 self.resolve_matrix_compound_test_suites()
1070 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281071 self.link_waterfalls_to_test_suites()
1072
Nico Weberd18b8962018-05-16 19:39:381073 def unknown_bot(self, bot_name, waterfall_name):
1074 return BBGenErr(
1075 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1076
Kenneth Russelleb60cbd22017-12-05 07:54:281077 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1078 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381079 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281080 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1081
1082 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1083 return BBGenErr(
1084 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1085 ' on waterfall ' + waterfall_name)
1086
Stephen Martinisb72f6d22018-10-04 23:29:011087 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071088 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321089
1090 Checks in the waterfall, builder, and test objects for mixins.
1091 """
1092 def valid_mixin(mixin_name):
1093 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011094 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321095 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381096
Stephen Martinisb6a50492018-09-12 23:59:321097 def must_be_list(mixins, typ, name):
1098 """Asserts that given mixins are a list."""
1099 if not isinstance(mixins, list):
1100 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1101
Brian Sheedy7658c982020-01-08 02:27:581102 test_name = test.get('name')
1103 remove_mixins = set()
1104 if 'remove_mixins' in builder:
1105 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1106 for rm in builder['remove_mixins']:
1107 valid_mixin(rm)
1108 remove_mixins.add(rm)
1109 if 'remove_mixins' in test:
1110 must_be_list(test['remove_mixins'], 'test', test_name)
1111 for rm in test['remove_mixins']:
1112 valid_mixin(rm)
1113 remove_mixins.add(rm)
1114 del test['remove_mixins']
1115
Stephen Martinisb72f6d22018-10-04 23:29:011116 if 'mixins' in waterfall:
1117 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1118 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581119 if mixin in remove_mixins:
1120 continue
Stephen Martinisb6a50492018-09-12 23:59:321121 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011122 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321123
Stephen Martinisb72f6d22018-10-04 23:29:011124 if 'mixins' in builder:
1125 must_be_list(builder['mixins'], 'builder', builder_name)
1126 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581127 if mixin in remove_mixins:
1128 continue
Stephen Martinisb6a50492018-09-12 23:59:321129 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011130 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321131
Stephen Martinisb72f6d22018-10-04 23:29:011132 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071133 return test
1134
Stephen Martinis2a0667022018-09-25 22:31:141135 if not test_name:
1136 test_name = test.get('test')
1137 if not test_name: # pragma: no cover
1138 # Not the best name, but we should say something.
1139 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:011140 must_be_list(test['mixins'], 'test', test_name)
1141 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581142 # We don't bother checking if the given mixin is in remove_mixins here
1143 # since this is already the lowest level, so if a mixin is added here that
1144 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071145 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011146 test = self.apply_mixin(self.mixins[mixin], test)
Jeff Yoon67c3e832020-02-08 07:39:381147 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071148 return test
Stephen Martinisb6a50492018-09-12 23:59:321149
Stephen Martinisb72f6d22018-10-04 23:29:011150 def apply_mixin(self, mixin, test):
1151 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321152
Stephen Martinis0382bc12018-09-17 22:29:071153 Mixins will not override an existing key. This is to ensure exceptions can
1154 override a setting a mixin applies.
1155
Stephen Martinisb72f6d22018-10-04 23:29:011156 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:321157 'dimension_sets', which is how normal test suites specify their dimensions,
1158 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
1159 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011160
Stephen Martinisb6a50492018-09-12 23:59:321161 """
1162 new_test = copy.deepcopy(test)
1163 mixin = copy.deepcopy(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011164 if 'swarming' in mixin:
1165 swarming_mixin = mixin['swarming']
1166 new_test.setdefault('swarming', {})
1167 if 'dimensions' in swarming_mixin:
1168 new_test['swarming'].setdefault('dimension_sets', [{}])
1169 for dimension_set in new_test['swarming']['dimension_sets']:
1170 dimension_set.update(swarming_mixin['dimensions'])
1171 del swarming_mixin['dimensions']
Stephen Martinisb72f6d22018-10-04 23:29:011172 # python dict update doesn't do recursion at all. Just hard code the
1173 # nested update we need (mixin['swarming'] shouldn't clobber
1174 # test['swarming'], but should update it).
1175 new_test['swarming'].update(swarming_mixin)
1176 del mixin['swarming']
1177
Wezc0e835b702018-10-30 00:38:411178 if '$mixin_append' in mixin:
1179 # Values specified under $mixin_append should be appended to existing
1180 # lists, rather than replacing them.
1181 mixin_append = mixin['$mixin_append']
1182 for key in mixin_append:
1183 new_test.setdefault(key, [])
1184 if not isinstance(mixin_append[key], list):
1185 raise BBGenErr(
1186 'Key "' + key + '" in $mixin_append must be a list.')
1187 if not isinstance(new_test[key], list):
1188 raise BBGenErr(
1189 'Cannot apply $mixin_append to non-list "' + key + '".')
1190 new_test[key].extend(mixin_append[key])
1191 if 'args' in mixin_append:
1192 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1193 del mixin['$mixin_append']
1194
Stephen Martinisb72f6d22018-10-04 23:29:011195 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321196 return new_test
1197
Greg Gutermanf60eb052020-03-12 17:40:011198 def generate_output_tests(self, waterfall):
1199 """Generates the tests for a waterfall.
1200
1201 Args:
1202 waterfall: a dictionary parsed from a master pyl file
1203 Returns:
1204 A dictionary mapping builders to test specs
1205 """
1206 return {
1207 name: self.get_tests_for_config(waterfall, name, config)
1208 for name, config
1209 in waterfall['machines'].iteritems()
1210 }
1211
1212 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531213 generator_map = self.get_test_generator_map()
1214 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281215
Greg Gutermanf60eb052020-03-12 17:40:011216 tests = {}
1217 # Copy only well-understood entries in the machine's configuration
1218 # verbatim into the generated JSON.
1219 if 'additional_compile_targets' in config:
1220 tests['additional_compile_targets'] = config[
1221 'additional_compile_targets']
1222 for test_type, input_tests in config.get('test_suites', {}).iteritems():
1223 if test_type not in generator_map:
1224 raise self.unknown_test_suite_type(
1225 test_type, name, waterfall['name']) # pragma: no cover
1226 test_generator = generator_map[test_type]
1227 # Let multiple kinds of generators generate the same kinds
1228 # of tests. For example, gpu_telemetry_tests are a
1229 # specialization of isolated_scripts.
1230 new_tests = test_generator.generate(
1231 waterfall, name, config, input_tests)
1232 remapped_test_type = test_type_remapper.get(test_type, test_type)
1233 tests[remapped_test_type] = test_generator.sort(
1234 tests.get(remapped_test_type, []) + new_tests)
1235
1236 return tests
1237
1238 def jsonify(self, all_tests):
1239 return json.dumps(
1240 all_tests, indent=2, separators=(',', ': '),
1241 sort_keys=True) + '\n'
1242
1243 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281244 self.load_configuration_files()
1245 self.resolve_configuration_files()
1246 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011247 result = collections.defaultdict(dict)
1248
1249 required_fields = ('project', 'bucket', 'name')
1250 for waterfall in self.waterfalls:
1251 for field in required_fields:
1252 # Verify required fields
1253 if field not in waterfall:
1254 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1255
1256 # Handle filter flag, if specified
1257 if filters and waterfall['name'] not in filters:
1258 continue
1259
1260 # Join config files and hardcoded values together
1261 all_tests = self.generate_output_tests(waterfall)
1262 result[waterfall['name']] = all_tests
1263
1264 # Deduce per-bucket mappings
1265 # This will be the standard after masternames are gone
1266 bucket_filename = waterfall['project'] + '.' + waterfall['bucket']
1267 for buildername in waterfall['machines'].keys():
1268 result[bucket_filename][buildername] = all_tests[buildername]
1269
1270 # Add do not edit warning
1271 for tests in result.values():
1272 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1273 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1274
1275 return result
1276
1277 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281278 suffix = '.json'
1279 if self.args.new_files:
1280 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011281
1282 for filename, contents in result.items():
1283 jsonstr = self.jsonify(contents)
1284 self.write_file(self.pyl_file_path(filename + suffix), jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281285
Nico Weberd18b8962018-05-16 19:39:381286 def get_valid_bot_names(self):
John Budorick699282e2019-02-13 01:27:331287 # Extract bot names from infra/config/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421288 # NOTE: This reference can cause issues; if a file changes there, the
1289 # presubmit here won't be run by default. A manually maintained list there
1290 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1291 # references to configs outside of this directory are added, please change
1292 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1293 # never ends up in an invalid state.
Nico Weberd18b8962018-05-16 19:39:381294 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311295 milo_configs = glob.glob(
1296 os.path.join(self.args.infra_config_dir, 'generated', 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431297 for c in milo_configs:
1298 for l in self.read_file(c).splitlines():
1299 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311300 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431301 continue
1302 # l looks like
1303 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1304 # Extract win_chromium_dbg_ng part.
1305 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381306 return bot_names
1307
Ben Pastene9a010082019-09-25 20:41:371308 def get_builders_that_do_not_actually_exist(self):
Kenneth Russell8a386d42018-06-02 09:48:011309 # Some of the bots on the chromium.gpu.fyi waterfall in particular
1310 # are defined only to be mirrored into trybots, and don't actually
1311 # exist on any of the waterfalls or consoles.
1312 return [
Michael Spangeb07eba62019-05-14 22:22:581313 'GPU FYI Fuchsia Builder',
Yuly Novikoveb26b812019-07-26 02:08:191314 'ANGLE GPU Android Release (Nexus 5X)',
Jamie Madillda894ce2019-04-08 17:19:171315 'ANGLE GPU Linux Release (Intel HD 630)',
1316 'ANGLE GPU Linux Release (NVIDIA)',
1317 'ANGLE GPU Mac Release (Intel)',
1318 'ANGLE GPU Mac Retina Release (AMD)',
1319 'ANGLE GPU Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491320 'ANGLE GPU Win10 x64 Release (Intel HD 630)',
1321 'ANGLE GPU Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011322 'Optional Android Release (Nexus 5X)',
1323 'Optional Linux Release (Intel HD 630)',
1324 'Optional Linux Release (NVIDIA)',
1325 'Optional Mac Release (Intel)',
1326 'Optional Mac Retina Release (AMD)',
1327 'Optional Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491328 'Optional Win10 x64 Release (Intel HD 630)',
1329 'Optional Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011330 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:081331 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:291332 'linux-blink-rel-dummy',
1333 'mac10.10-blink-rel-dummy',
1334 'mac10.11-blink-rel-dummy',
1335 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:201336 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291337 'mac10.13-blink-rel-dummy',
John Chenad978322019-12-16 18:07:211338 'mac10.14-blink-rel-dummy',
Ilia Samsonov7efe05e2020-05-07 19:00:461339 'mac10.15-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291340 'win7-blink-rel-dummy',
1341 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:081342 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:331343 'WebKit Linux composite_after_paint Dummy Builder',
Scott Violet744e04662019-08-19 23:51:531344 'WebKit Linux layout_ng_disabled Builder',
Stephen Martinis769b25112018-08-30 18:52:061345 # chromium, due to https://crbug.com/878915
1346 'win-dbg',
1347 'win32-dbg',
Stephen Martinis47d77132019-04-24 23:51:331348 'win-archive-dbg',
1349 'win32-archive-dbg',
Sajjad Mirza2924a012019-12-20 03:46:541350 # TODO(crbug.com/1033753) Delete these when coverage is enabled by default
1351 # on Windows tryjobs.
1352 'GPU Win x64 Builder Code Coverage',
1353 'Win x64 Builder Code Coverage',
1354 'Win10 Tests x64 Code Coverage',
1355 'Win10 x64 Release (NVIDIA) Code Coverage',
Sajjad Mirzafa15665e2020-02-10 23:41:041356 # TODO(crbug.com/1024915) Delete these when coverage is enabled by default
1357 # on Mac OS tryjobs.
1358 'Mac Builder Code Coverage',
1359 'Mac10.13 Tests Code Coverage',
1360 'GPU Mac Builder Code Coverage',
1361 'Mac Release (Intel) Code Coverage',
1362 'Mac Retina Release (AMD) Code Coverage',
Kenneth Russell8a386d42018-06-02 09:48:011363 ]
1364
Ben Pastene9a010082019-09-25 20:41:371365 def get_internal_waterfalls(self):
1366 # Similar to get_builders_that_do_not_actually_exist above, but for
1367 # waterfalls defined in internal configs.
Jeff Yoon8acfdce2020-04-20 22:38:071368 return ['chrome', 'chrome.pgo']
Ben Pastene9a010082019-09-25 20:41:371369
Stephen Martinisf83893722018-09-19 00:02:181370 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201371 self.check_input_files_sorting(verbose)
1372
Kenneth Russelleb60cbd22017-12-05 07:54:281373 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011374 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381375 self.check_composition_type_test_suites('matrix_compound_suites',
1376 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111377 self.resolve_test_id_prefixes()
Stephen Martinis54d64ad2018-09-21 22:16:201378 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381379
1380 # All bots should exist.
1381 bot_names = self.get_valid_bot_names()
Ben Pastene9a010082019-09-25 20:41:371382 internal_waterfalls = self.get_internal_waterfalls()
1383 builders_that_dont_exist = self.get_builders_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:381384 for waterfall in self.waterfalls:
Ben Pastene9a010082019-09-25 20:41:371385 # TODO(crbug.com/991417): Remove the need for this exception.
1386 if waterfall['name'] in internal_waterfalls:
1387 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381388 for bot_name in waterfall['machines']:
Ben Pastene9a010082019-09-25 20:41:371389 if bot_name in builders_that_dont_exist:
Kenneth Russell8a386d42018-06-02 09:48:011390 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381391 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:081392 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:381393 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:521394 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:321395 if waterfall['name'] in ['tryserver.webrtc',
1396 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:381397 # These waterfalls have their bot configs in a different repo.
1398 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:521399 continue # pragma: no cover
Jeff Yoon8154e582019-12-03 23:30:011400 if waterfall['name'] in ['client.devtools-frontend.integration',
Liviu Raud287b1f2020-01-14 07:30:331401 'tryserver.devtools-frontend',
1402 'chromium.devtools-frontend']:
Tamer Tas2c506412019-08-20 07:44:411403 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381404 raise self.unknown_bot(bot_name, waterfall['name'])
1405
Kenneth Russelleb60cbd22017-12-05 07:54:281406 # All test suites must be referenced.
1407 suites_seen = set()
1408 generator_map = self.get_test_generator_map()
1409 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431410 for bot_name, tester in waterfall['machines'].iteritems():
1411 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281412 if suite_type not in generator_map:
1413 raise self.unknown_test_suite_type(suite_type, bot_name,
1414 waterfall['name'])
1415 if suite not in self.test_suites:
1416 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1417 suites_seen.add(suite)
1418 # Since we didn't resolve the configuration files, this set
1419 # includes both composition test suites and regular ones.
1420 resolved_suites = set()
1421 for suite_name in suites_seen:
1422 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011423 for sub_suite in suite:
1424 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281425 resolved_suites.add(suite_name)
1426 # At this point, every key in test_suites.pyl should be referenced.
1427 missing_suites = set(self.test_suites.keys()) - resolved_suites
1428 if missing_suites:
1429 raise BBGenErr('The following test suites were unreferenced by bots on '
1430 'the waterfalls: ' + str(missing_suites))
1431
1432 # All test suite exceptions must refer to bots on the waterfall.
1433 all_bots = set()
1434 missing_bots = set()
1435 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431436 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281437 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281438 # In order to disambiguate between bots with the same name on
1439 # different waterfalls, support has been added to various
1440 # exceptions for concatenating the waterfall name after the bot
1441 # name.
1442 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281443 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381444 removals = (exception.get('remove_from', []) +
1445 exception.get('remove_gtest_from', []) +
1446 exception.get('modifications', {}).keys())
1447 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281448 if removal not in all_bots:
1449 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411450
Ben Pastene9a010082019-09-25 20:41:371451 missing_bots = missing_bots - set(builders_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281452 if missing_bots:
1453 raise BBGenErr('The following nonexistent machines were referenced in '
1454 'the test suite exceptions: ' + str(missing_bots))
1455
Stephen Martinis0382bc12018-09-17 22:29:071456 # All mixins must be referenced
1457 seen_mixins = set()
1458 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011459 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071460 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011461 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071462 for suite in self.test_suites.values():
1463 if isinstance(suite, list):
1464 # Don't care about this, it's a composition, which shouldn't include a
1465 # swarming mixin.
1466 continue
1467
1468 for test in suite.values():
1469 if not isinstance(test, dict):
1470 # Some test suites have top level keys, which currently can't be
1471 # swarming mixin entries. Ignore them
1472 continue
1473
Stephen Martinisb72f6d22018-10-04 23:29:011474 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071475
Stephen Martinisb72f6d22018-10-04 23:29:011476 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071477 if missing_mixins:
1478 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1479 ' referenced in a waterfall, machine, or test suite.' % (
1480 str(missing_mixins)))
1481
Jeff Yoonda581c32020-03-06 03:56:051482 # All variant references must be referenced
1483 seen_variants = set()
1484 for suite in self.test_suites.values():
1485 if isinstance(suite, list):
1486 continue
1487
1488 for test in suite.values():
1489 if isinstance(test, dict):
1490 for variant in test.get('variants', []):
1491 if isinstance(variant, str):
1492 seen_variants.add(variant)
1493
1494 missing_variants = set(self.variants.keys()) - seen_variants
1495 if missing_variants:
1496 raise BBGenErr('The following variants were unreferenced: %s. They must '
1497 'be referenced in a matrix test suite under the variants '
1498 'key.' % str(missing_variants))
1499
Stephen Martinis54d64ad2018-09-21 22:16:201500
1501 def type_assert(self, node, typ, filename, verbose=False):
1502 """Asserts that the Python AST node |node| is of type |typ|.
1503
1504 If verbose is set, it prints out some helpful context lines, showing where
1505 exactly the error occurred in the file.
1506 """
1507 if not isinstance(node, typ):
1508 if verbose:
1509 lines = [""] + self.read_file(filename).splitlines()
1510
1511 context = 2
1512 lines_start = max(node.lineno - context, 0)
1513 # Add one to include the last line
1514 lines_end = min(node.lineno + context, len(lines)) + 1
1515 lines = (
1516 ['== %s ==\n' % filename] +
1517 ["<snip>\n"] +
1518 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1519 lines[lines_start:lines_start + context])] +
1520 ['-' * 80 + '\n'] +
1521 ['%d %s' % (node.lineno, lines[node.lineno])] +
1522 ['-' * (node.col_offset + 3) + '^' + '-' * (
1523 80 - node.col_offset - 4) + '\n'] +
1524 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1525 lines[node.lineno + 1:lines_end])] +
1526 ["<snip>\n"]
1527 )
1528 # Print out a useful message when a type assertion fails.
1529 for l in lines:
1530 self.print_line(l.strip())
1531
1532 node_dumped = ast.dump(node, annotate_fields=False)
1533 # If the node is huge, truncate it so everything fits in a terminal
1534 # window.
1535 if len(node_dumped) > 60: # pragma: no cover
1536 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1537 raise BBGenErr(
1538 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1539 ' be %s, is %s' % (
1540 filename, node_dumped,
1541 node.lineno, typ, type(node)))
1542
Stephen Martinis5bef0fc2020-01-06 22:47:531543 def check_ast_list_formatted(self, keys, filename, verbose,
Stephen Martinis1384ff92020-01-07 19:52:151544 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531545 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201546
Stephen Martinis5bef0fc2020-01-06 22:47:531547 Currently only checks to ensure they're correctly sorted, and that there
1548 are no duplicates.
1549
1550 Args:
1551 keys: An python list of AST nodes.
1552
1553 It's a list of AST nodes instead of a list of strings because
1554 when verbose is set, it tries to print out context of where the
1555 diffs are in the file.
1556 filename: The name of the file this node is from.
1557 verbose: If set, print out diff information about how the keys are
1558 incorrectly formatted.
1559 check_sorting: If true, checks if the list is sorted.
1560 Returns:
1561 If the keys are correctly formatted.
1562 """
1563 if not keys:
1564 return True
1565
1566 assert isinstance(keys[0], ast.Str)
1567
1568 keys_strs = [k.s for k in keys]
1569 # Keys to diff against. Used below.
1570 keys_to_diff_against = None
1571 # If the list is properly formatted.
1572 list_formatted = True
1573
1574 # Duplicates are always bad.
1575 if len(set(keys_strs)) != len(keys_strs):
1576 list_formatted = False
1577 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1578
1579 if check_sorting and sorted(keys_strs) != keys_strs:
1580 list_formatted = False
1581 if list_formatted:
1582 return True
1583
1584 if verbose:
1585 line_num = keys[0].lineno
1586 keys = [k.s for k in keys]
1587 if check_sorting:
1588 # If we have duplicates, sorting this will take care of it anyways.
1589 keys_to_diff_against = sorted(set(keys))
1590 # else, keys_to_diff_against is set above already
1591
1592 self.print_line('=' * 80)
1593 self.print_line('(First line of keys is %s)' % line_num)
1594 for line in difflib.context_diff(
1595 keys, keys_to_diff_against,
1596 fromfile='current (%r)' % filename, tofile='sorted', lineterm=''):
1597 self.print_line(line)
1598 self.print_line('=' * 80)
1599
1600 return False
1601
Stephen Martinis1384ff92020-01-07 19:52:151602 def check_ast_dict_formatted(self, node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531603 """Checks if an ast dictionary's keys are correctly formatted.
1604
1605 Just a simple wrapper around check_ast_list_formatted.
1606 Args:
1607 node: An AST node. Assumed to be a dictionary.
1608 filename: The name of the file this node is from.
1609 verbose: If set, print out diff information about how the keys are
1610 incorrectly formatted.
1611 check_sorting: If true, checks if the list is sorted.
1612 Returns:
1613 If the dictionary is correctly formatted.
1614 """
Stephen Martinis54d64ad2018-09-21 22:16:201615 keys = []
1616 # The keys of this dict are ordered as ordered in the file; normal python
1617 # dictionary keys are given an arbitrary order, but since we parsed the
1618 # file itself, the order as given in the file is preserved.
1619 for key in node.keys:
1620 self.type_assert(key, ast.Str, filename, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531621 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201622
Stephen Martinis1384ff92020-01-07 19:52:151623 return self.check_ast_list_formatted(keys, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181624
1625 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201626 # TODO(https://crbug.com/886993): Add the ability for this script to
1627 # actually format the files, rather than just complain if they're
1628 # incorrectly formatted.
1629 bad_files = set()
Stephen Martinis5bef0fc2020-01-06 22:47:531630 def parse_file(filename):
1631 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201632
Stephen Martinis5bef0fc2020-01-06 22:47:531633 Returns an AST node representing the value in the pyl file."""
Stephen Martinisf83893722018-09-19 00:02:181634 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1635
Stephen Martinisf83893722018-09-19 00:02:181636 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201637 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181638 module = parsed.body
1639
1640 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201641 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181642 if len(module) != 1: # pragma: no cover
1643 raise BBGenErr('Invalid .pyl file %s' % filename)
1644 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201645 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181646
Stephen Martinis5bef0fc2020-01-06 22:47:531647 return expr.value
1648
1649 # Handle this separately
1650 filename = 'waterfalls.pyl'
1651 value = parse_file(filename)
1652 # Value should be a list.
1653 self.type_assert(value, ast.List, filename, verbose)
1654
1655 keys = []
1656 for val in value.elts:
1657 self.type_assert(val, ast.Dict, filename, verbose)
1658 waterfall_name = None
1659 for key, val in zip(val.keys, val.values):
1660 self.type_assert(key, ast.Str, filename, verbose)
1661 if key.s == 'machines':
1662 if not self.check_ast_dict_formatted(val, filename, verbose):
1663 bad_files.add(filename)
1664
1665 if key.s == "name":
1666 self.type_assert(val, ast.Str, filename, verbose)
1667 waterfall_name = val
1668 assert waterfall_name
1669 keys.append(waterfall_name)
1670
Stephen Martinis1384ff92020-01-07 19:52:151671 if not self.check_ast_list_formatted(keys, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531672 bad_files.add(filename)
1673
1674 for filename in (
1675 'mixins.pyl',
1676 'test_suites.pyl',
1677 'test_suite_exceptions.pyl',
1678 ):
1679 value = parse_file(filename)
Stephen Martinisf83893722018-09-19 00:02:181680 # Value should be a dictionary.
Stephen Martinis54d64ad2018-09-21 22:16:201681 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181682
Stephen Martinis5bef0fc2020-01-06 22:47:531683 if not self.check_ast_dict_formatted(
1684 value, filename, verbose):
1685 bad_files.add(filename)
1686
Stephen Martinis54d64ad2018-09-21 22:16:201687 if filename == 'test_suites.pyl':
Jeff Yoon8154e582019-12-03 23:30:011688 expected_keys = ['basic_suites',
1689 'compound_suites',
1690 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201691 actual_keys = [node.s for node in value.keys]
1692 assert all(key in expected_keys for key in actual_keys), (
1693 'Invalid %r file; expected keys %r, got %r' % (
1694 filename, expected_keys, actual_keys))
1695 suite_dicts = [node for node in value.values]
1696 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011697 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201698 for suite_group in suite_dicts:
Stephen Martinis5bef0fc2020-01-06 22:47:531699 if not self.check_ast_dict_formatted(
Stephen Martinis54d64ad2018-09-21 22:16:201700 suite_group, filename, verbose):
1701 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181702
Stephen Martinis5bef0fc2020-01-06 22:47:531703 for key, suite in zip(value.keys, value.values):
1704 # The compound suites are checked in
1705 # 'check_composition_type_test_suites()'
1706 if key.s == 'basic_suites':
1707 for group in suite.values:
Stephen Martinis1384ff92020-01-07 19:52:151708 if not self.check_ast_dict_formatted(group, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531709 bad_files.add(filename)
1710 break
Stephen Martinis54d64ad2018-09-21 22:16:201711
Stephen Martinis5bef0fc2020-01-06 22:47:531712 elif filename == 'test_suite_exceptions.pyl':
1713 # Check the values for each test.
1714 for test in value.values:
1715 for kind, node in zip(test.keys, test.values):
1716 if isinstance(node, ast.Dict):
Stephen Martinis1384ff92020-01-07 19:52:151717 if not self.check_ast_dict_formatted(node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531718 bad_files.add(filename)
1719 elif kind.s == 'remove_from':
1720 # Don't care about sorting; these are usually grouped, since the
1721 # same bug can affect multiple builders. Do want to make sure
1722 # there aren't duplicates.
1723 if not self.check_ast_list_formatted(node.elts, filename, verbose,
1724 check_sorting=False):
1725 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181726
1727 if bad_files:
1728 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201729 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531730 'unsorted, or have duplicates. Re-run this with --verbose to see '
1731 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181732
Kenneth Russelleb60cbd22017-12-05 07:54:281733 def check_output_file_consistency(self, verbose=False):
1734 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011735 # All waterfalls/bucket .json files must have been written
1736 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:281737 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011738 ungenerated_files = set()
1739 for filename, expected_contents in self.generate_outputs().items():
1740 expected = self.jsonify(expected_contents)
1741 file_path = filename + '.json'
Zhiling Huangbe008172018-03-08 19:13:111742 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281743 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:011744 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:321745 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:011746 self.print_line('File ' + filename +
1747 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321748 'contents:')
1749 for line in difflib.unified_diff(
1750 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501751 current.splitlines(),
1752 fromfile='expected', tofile='current'):
1753 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:011754
1755 if ungenerated_files:
1756 raise BBGenErr(
1757 'The following files have not been properly '
1758 'autogenerated by generate_buildbot_json.py: ' +
1759 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:281760
1761 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501762 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281763 self.check_output_file_consistency(verbose) # pragma: no cover
1764
1765 def parse_args(self, argv): # pragma: no cover
Karen Qiane24b7ee2019-02-12 23:37:061766
1767 # RawTextHelpFormatter allows for styling of help statement
1768 parser = argparse.ArgumentParser(formatter_class=
1769 argparse.RawTextHelpFormatter)
1770
1771 group = parser.add_mutually_exclusive_group()
1772 group.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281773 '-c', '--check', action='store_true', help=
1774 'Do consistency checks of configuration and generated files and then '
1775 'exit. Used during presubmit. Causes the tool to not generate any files.')
Karen Qiane24b7ee2019-02-12 23:37:061776 group.add_argument(
1777 '--query', type=str, help=
1778 ("Returns raw JSON information of buildbots and tests.\n" +
1779 "Examples:\n" +
1780 " List all bots (all info):\n" +
1781 " --query bots\n\n" +
1782 " List all bots and only their associated tests:\n" +
1783 " --query bots/tests\n\n" +
1784 " List all information about 'bot1' " +
1785 "(make sure you have quotes):\n" +
1786 " --query bot/'bot1'\n\n" +
1787 " List tests running for 'bot1' (make sure you have quotes):\n" +
1788 " --query bot/'bot1'/tests\n\n" +
1789 " List all tests:\n" +
1790 " --query tests\n\n" +
1791 " List all tests and the bots running them:\n" +
1792 " --query tests/bots\n\n"+
1793 " List all tests that satisfy multiple parameters\n" +
1794 " (separation of parameters by '&' symbol):\n" +
1795 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
1796 " List all tests that run with a specific flag:\n" +
1797 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
1798 " List specific test (make sure you have quotes):\n"
1799 " --query test/'test1'\n\n"
1800 " List all bots running 'test1' " +
1801 "(make sure you have quotes):\n" +
1802 " --query test/'test1'/bots" ))
Kenneth Russelleb60cbd22017-12-05 07:54:281803 parser.add_argument(
1804 '-n', '--new-files', action='store_true', help=
1805 'Write output files as .new.json. Useful during development so old and '
1806 'new files can be looked at side-by-side.')
1807 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501808 '-v', '--verbose', action='store_true', help=
1809 'Increases verbosity. Affects consistency checks.')
1810 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281811 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1812 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111813 parser.add_argument(
1814 '--pyl-files-dir', type=os.path.realpath,
1815 help='Path to the directory containing the input .pyl files.')
Karen Qiane24b7ee2019-02-12 23:37:061816 parser.add_argument(
1817 '--json', help=
1818 ("Outputs results into a json file. Only works with query function.\n" +
1819 "Examples:\n" +
1820 " Outputs file into specified json file: \n" +
1821 " --json <file-name-here.json>"))
Garrett Beatyd5ca75962020-05-07 16:58:311822 parser.add_argument(
1823 '--infra-config-dir',
1824 help='Path to the LUCI services configuration directory',
1825 default=os.path.abspath(
1826 os.path.join(os.path.dirname(__file__),
1827 '..', '..', 'infra', 'config')))
Kenneth Russelleb60cbd22017-12-05 07:54:281828 self.args = parser.parse_args(argv)
Karen Qiane24b7ee2019-02-12 23:37:061829 if self.args.json and not self.args.query:
1830 parser.error("The --json flag can only be used with --query.")
Garrett Beatyd5ca75962020-05-07 16:58:311831 self.args.infra_config_dir = os.path.abspath(self.args.infra_config_dir)
Karen Qiane24b7ee2019-02-12 23:37:061832
1833 def does_test_match(self, test_info, params_dict):
1834 """Checks to see if the test matches the parameters given.
1835
1836 Compares the provided test_info with the params_dict to see
1837 if the bot matches the parameters given. If so, returns True.
1838 Else, returns false.
1839
1840 Args:
1841 test_info (dict): Information about a specific bot provided
1842 in the format shown in waterfalls.pyl
1843 params_dict (dict): Dictionary of parameters and their values
1844 to look for in the bot
1845 Ex: {
1846 'device_os':'android',
1847 '--flag':True,
1848 'mixins': ['mixin1', 'mixin2'],
1849 'ex_key':'ex_value'
1850 }
1851
1852 """
1853 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
1854 'kvm', 'pool', 'integrity'] # dimension parameters
1855 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
1856 'can_use_on_swarming_builders']
1857 for param in params_dict:
1858 # if dimension parameter
1859 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
1860 if not 'swarming' in test_info:
1861 return False
1862 swarming = test_info['swarming']
1863 if param in SWARMING_PARAMS:
1864 if not param in swarming:
1865 return False
1866 if not str(swarming[param]) == params_dict[param]:
1867 return False
1868 else:
1869 if not 'dimension_sets' in swarming:
1870 return False
1871 d_set = swarming['dimension_sets']
1872 # only looking at the first dimension set
1873 if not param in d_set[0]:
1874 return False
1875 if not d_set[0][param] == params_dict[param]:
1876 return False
1877
1878 # if flag
1879 elif param.startswith('--'):
1880 if not 'args' in test_info:
1881 return False
1882 if not param in test_info['args']:
1883 return False
1884
1885 # not dimension parameter/flag/mixin
1886 else:
1887 if not param in test_info:
1888 return False
1889 if not test_info[param] == params_dict[param]:
1890 return False
1891 return True
1892 def error_msg(self, msg):
1893 """Prints an error message.
1894
1895 In addition to a catered error message, also prints
1896 out where the user can find more help. Then, program exits.
1897 """
1898 self.print_line(msg + (' If you need more information, ' +
1899 'please run with -h or --help to see valid commands.'))
1900 sys.exit(1)
1901
1902 def find_bots_that_run_test(self, test, bots):
1903 matching_bots = []
1904 for bot in bots:
1905 bot_info = bots[bot]
1906 tests = self.flatten_tests_for_bot(bot_info)
1907 for test_info in tests:
1908 test_name = ""
1909 if 'name' in test_info:
1910 test_name = test_info['name']
1911 elif 'test' in test_info:
1912 test_name = test_info['test']
1913 if not test_name == test:
1914 continue
1915 matching_bots.append(bot)
1916 return matching_bots
1917
1918 def find_tests_with_params(self, tests, params_dict):
1919 matching_tests = []
1920 for test_name in tests:
1921 test_info = tests[test_name]
1922 if not self.does_test_match(test_info, params_dict):
1923 continue
1924 if not test_name in matching_tests:
1925 matching_tests.append(test_name)
1926 return matching_tests
1927
1928 def flatten_waterfalls_for_query(self, waterfalls):
1929 bots = {}
1930 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:011931 waterfall_tests = self.generate_output_tests(waterfall)
1932 for bot in waterfall_tests:
1933 bot_info = waterfall_tests[bot]
1934 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:061935 return bots
1936
1937 def flatten_tests_for_bot(self, bot_info):
1938 """Returns a list of flattened tests.
1939
1940 Returns a list of tests not grouped by test category
1941 for a specific bot.
1942 """
1943 TEST_CATS = self.get_test_generator_map().keys()
1944 tests = []
1945 for test_cat in TEST_CATS:
1946 if not test_cat in bot_info:
1947 continue
1948 test_cat_tests = bot_info[test_cat]
1949 tests = tests + test_cat_tests
1950 return tests
1951
1952 def flatten_tests_for_query(self, test_suites):
1953 """Returns a flattened dictionary of tests.
1954
1955 Returns a dictionary of tests associate with their
1956 configuration, not grouped by their test suite.
1957 """
1958 tests = {}
1959 for test_suite in test_suites.itervalues():
1960 for test in test_suite:
1961 test_info = test_suite[test]
1962 test_name = test
1963 if 'name' in test_info:
1964 test_name = test_info['name']
1965 tests[test_name] = test_info
1966 return tests
1967
1968 def parse_query_filter_params(self, params):
1969 """Parses the filter parameters.
1970
1971 Creates a dictionary from the parameters provided
1972 to filter the bot array.
1973 """
1974 params_dict = {}
1975 for p in params:
1976 # flag
1977 if p.startswith("--"):
1978 params_dict[p] = True
1979 else:
1980 pair = p.split(":")
1981 if len(pair) != 2:
1982 self.error_msg('Invalid command.')
1983 # regular parameters
1984 if pair[1].lower() == "true":
1985 params_dict[pair[0]] = True
1986 elif pair[1].lower() == "false":
1987 params_dict[pair[0]] = False
1988 else:
1989 params_dict[pair[0]] = pair[1]
1990 return params_dict
1991
1992 def get_test_suites_dict(self, bots):
1993 """Returns a dictionary of bots and their tests.
1994
1995 Returns a dictionary of bots and a list of their associated tests.
1996 """
1997 test_suite_dict = dict()
1998 for bot in bots:
1999 bot_info = bots[bot]
2000 tests = self.flatten_tests_for_bot(bot_info)
2001 test_suite_dict[bot] = tests
2002 return test_suite_dict
2003
2004 def output_query_result(self, result, json_file=None):
2005 """Outputs the result of the query.
2006
2007 If a json file parameter name is provided, then
2008 the result is output into the json file. If not,
2009 then the result is printed to the console.
2010 """
2011 output = json.dumps(result, indent=2)
2012 if json_file:
2013 self.write_file(json_file, output)
2014 else:
2015 self.print_line(output)
2016 return
2017
2018 def query(self, args):
2019 """Queries tests or bots.
2020
2021 Depending on the arguments provided, outputs a json of
2022 tests or bots matching the appropriate optional parameters provided.
2023 """
2024 # split up query statement
2025 query = args.query.split('/')
2026 self.load_configuration_files()
2027 self.resolve_configuration_files()
2028
2029 # flatten bots json
2030 tests = self.test_suites
2031 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2032
2033 cmd_class = query[0]
2034
2035 # For queries starting with 'bots'
2036 if cmd_class == "bots":
2037 if len(query) == 1:
2038 return self.output_query_result(bots, args.json)
2039 # query with specific parameters
2040 elif len(query) == 2:
2041 if query[1] == 'tests':
2042 test_suites_dict = self.get_test_suites_dict(bots)
2043 return self.output_query_result(test_suites_dict, args.json)
2044 else:
2045 self.error_msg("This query should be in the format: bots/tests.")
2046
2047 else:
2048 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2049 % str(len(query)-1))
2050
2051 # For queries starting with 'bot'
2052 elif cmd_class == "bot":
2053 if not len(query) == 2 and not len(query) == 3:
2054 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2055 % str(len(query)-1))
2056 bot_id = query[1]
2057 if not bot_id in bots:
2058 self.error_msg("No bot named '" + bot_id + "' found.")
2059 bot_info = bots[bot_id]
2060 if len(query) == 2:
2061 return self.output_query_result(bot_info, args.json)
2062 if not query[2] == 'tests':
2063 self.error_msg("The query should be in the format:" +
2064 "bot/<bot-name>/tests.")
2065
2066 bot_tests = self.flatten_tests_for_bot(bot_info)
2067 return self.output_query_result(bot_tests, args.json)
2068
2069 # For queries starting with 'tests'
2070 elif cmd_class == "tests":
2071 if not len(query) == 1 and not len(query) == 2:
2072 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2073 % str(len(query)-1))
2074 flattened_tests = self.flatten_tests_for_query(tests)
2075 if len(query) == 1:
2076 return self.output_query_result(flattened_tests, args.json)
2077
2078 # create params dict
2079 params = query[1].split('&')
2080 params_dict = self.parse_query_filter_params(params)
2081 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2082 return self.output_query_result(matching_bots)
2083
2084 # For queries starting with 'test'
2085 elif cmd_class == "test":
2086 if not len(query) == 2 and not len(query) == 3:
2087 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2088 % str(len(query)-1))
2089 test_id = query[1]
2090 if len(query) == 2:
2091 flattened_tests = self.flatten_tests_for_query(tests)
2092 for test in flattened_tests:
2093 if test == test_id:
2094 return self.output_query_result(flattened_tests[test], args.json)
2095 self.error_msg("There is no test named %s." % test_id)
2096 if not query[2] == 'bots':
2097 self.error_msg("The query should be in the format: " +
2098 "test/<test-name>/bots")
2099 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2100 return self.output_query_result(bots_for_test)
2101
2102 else:
2103 self.error_msg("Your command did not match any valid commands." +
2104 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:282105
2106 def main(self, argv): # pragma: no cover
2107 self.parse_args(argv)
2108 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502109 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062110 elif self.args.query:
2111 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282112 else:
Greg Gutermanf60eb052020-03-12 17:40:012113 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282114 return 0
2115
2116if __name__ == "__main__": # pragma: no cover
2117 generator = BBJSONGenerator()
John Budorick699282e2019-02-13 01:27:332118 sys.exit(generator.main(sys.argv[1:]))