blob: fb02ef8740f32c4624b69aa031b2a34bb6a5bfa7 [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
Brian Sheedy9493da892020-05-13 22:58:06604 # are targeting CrOS hardware and so need the special trigger script.
605 dimension_sets = test['swarming']['dimension_sets']
Ben Pastenea9e583b2019-01-16 02:57:26606 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.
Garrett Beaty2a02de3c2020-05-15 13:57:351294 project_star = glob.glob(
1295 os.path.join(self.args.infra_config_dir, 'project.star'))
1296 if project_star:
1297 is_master_pattern = re.compile('is_master\s*=\s*(True|False)')
1298 for l in self.read_file(project_star[0]).splitlines():
1299 match = is_master_pattern.search(l)
1300 if match:
1301 if match.group(1) == 'False':
1302 return None
1303 break
Nico Weberd18b8962018-05-16 19:39:381304 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311305 milo_configs = glob.glob(
1306 os.path.join(self.args.infra_config_dir, 'generated', 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431307 for c in milo_configs:
1308 for l in self.read_file(c).splitlines():
1309 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311310 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431311 continue
1312 # l looks like
1313 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1314 # Extract win_chromium_dbg_ng part.
1315 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381316 return bot_names
1317
Ben Pastene9a010082019-09-25 20:41:371318 def get_builders_that_do_not_actually_exist(self):
Kenneth Russell8a386d42018-06-02 09:48:011319 # Some of the bots on the chromium.gpu.fyi waterfall in particular
1320 # are defined only to be mirrored into trybots, and don't actually
1321 # exist on any of the waterfalls or consoles.
1322 return [
Michael Spangeb07eba62019-05-14 22:22:581323 'GPU FYI Fuchsia Builder',
Yuly Novikoveb26b812019-07-26 02:08:191324 'ANGLE GPU Android Release (Nexus 5X)',
Jamie Madillda894ce2019-04-08 17:19:171325 'ANGLE GPU Linux Release (Intel HD 630)',
1326 'ANGLE GPU Linux Release (NVIDIA)',
1327 'ANGLE GPU Mac Release (Intel)',
1328 'ANGLE GPU Mac Retina Release (AMD)',
1329 'ANGLE GPU Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491330 'ANGLE GPU Win10 x64 Release (Intel HD 630)',
1331 'ANGLE GPU Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011332 'Optional Android Release (Nexus 5X)',
1333 'Optional Linux Release (Intel HD 630)',
1334 'Optional Linux Release (NVIDIA)',
1335 'Optional Mac Release (Intel)',
1336 'Optional Mac Retina Release (AMD)',
1337 'Optional Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491338 'Optional Win10 x64 Release (Intel HD 630)',
1339 'Optional Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011340 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:081341 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:291342 'linux-blink-rel-dummy',
1343 'mac10.10-blink-rel-dummy',
1344 'mac10.11-blink-rel-dummy',
1345 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d2018-07-17 21:39:201346 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291347 'mac10.13-blink-rel-dummy',
John Chenad978322019-12-16 18:07:211348 'mac10.14-blink-rel-dummy',
Ilia Samsonov7efe05e2020-05-07 19:00:461349 'mac10.15-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291350 'win7-blink-rel-dummy',
1351 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:081352 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:331353 'WebKit Linux composite_after_paint Dummy Builder',
Scott Violet744e04662019-08-19 23:51:531354 'WebKit Linux layout_ng_disabled Builder',
Stephen Martinis769b25112018-08-30 18:52:061355 # chromium, due to https://crbug.com/878915
1356 'win-dbg',
1357 'win32-dbg',
Stephen Martinis47d77132019-04-24 23:51:331358 'win-archive-dbg',
1359 'win32-archive-dbg',
Sajjad Mirza2924a012019-12-20 03:46:541360 # TODO(crbug.com/1033753) Delete these when coverage is enabled by default
1361 # on Windows tryjobs.
1362 'GPU Win x64 Builder Code Coverage',
1363 'Win x64 Builder Code Coverage',
1364 'Win10 Tests x64 Code Coverage',
1365 'Win10 x64 Release (NVIDIA) Code Coverage',
Sajjad Mirzafa15665e2020-02-10 23:41:041366 # TODO(crbug.com/1024915) Delete these when coverage is enabled by default
1367 # on Mac OS tryjobs.
1368 'Mac Builder Code Coverage',
1369 'Mac10.13 Tests Code Coverage',
1370 'GPU Mac Builder Code Coverage',
1371 'Mac Release (Intel) Code Coverage',
1372 'Mac Retina Release (AMD) Code Coverage',
Kenneth Russell8a386d42018-06-02 09:48:011373 ]
1374
Ben Pastene9a010082019-09-25 20:41:371375 def get_internal_waterfalls(self):
1376 # Similar to get_builders_that_do_not_actually_exist above, but for
1377 # waterfalls defined in internal configs.
Jeff Yoon8acfdce2020-04-20 22:38:071378 return ['chrome', 'chrome.pgo']
Ben Pastene9a010082019-09-25 20:41:371379
Stephen Martinisf83893722018-09-19 00:02:181380 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201381 self.check_input_files_sorting(verbose)
1382
Kenneth Russelleb60cbd22017-12-05 07:54:281383 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011384 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381385 self.check_composition_type_test_suites('matrix_compound_suites',
1386 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111387 self.resolve_test_id_prefixes()
Stephen Martinis54d64ad2018-09-21 22:16:201388 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381389
1390 # All bots should exist.
1391 bot_names = self.get_valid_bot_names()
Ben Pastene9a010082019-09-25 20:41:371392 builders_that_dont_exist = self.get_builders_that_do_not_actually_exist()
Garrett Beaty2a02de3c2020-05-15 13:57:351393 if bot_names is not None:
1394 internal_waterfalls = self.get_internal_waterfalls()
1395 for waterfall in self.waterfalls:
1396 # TODO(crbug.com/991417): Remove the need for this exception.
1397 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011398 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351399 for bot_name in waterfall['machines']:
1400 if bot_name in builders_that_dont_exist:
Kenneth Russell78fd8702018-05-17 01:15:521401 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351402 if bot_name not in bot_names:
1403 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
1404 # TODO(thakis): Remove this once these bots move to luci.
1405 continue # pragma: no cover
1406 if waterfall['name'] in ['tryserver.webrtc',
1407 'webrtc.chromium.fyi.experimental']:
1408 # These waterfalls have their bot configs in a different repo.
1409 # so we don't know about their bot names.
1410 continue # pragma: no cover
1411 if waterfall['name'] in ['client.devtools-frontend.integration',
1412 'tryserver.devtools-frontend',
1413 'chromium.devtools-frontend']:
1414 continue # pragma: no cover
1415 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381416
Kenneth Russelleb60cbd22017-12-05 07:54:281417 # All test suites must be referenced.
1418 suites_seen = set()
1419 generator_map = self.get_test_generator_map()
1420 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431421 for bot_name, tester in waterfall['machines'].iteritems():
1422 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281423 if suite_type not in generator_map:
1424 raise self.unknown_test_suite_type(suite_type, bot_name,
1425 waterfall['name'])
1426 if suite not in self.test_suites:
1427 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1428 suites_seen.add(suite)
1429 # Since we didn't resolve the configuration files, this set
1430 # includes both composition test suites and regular ones.
1431 resolved_suites = set()
1432 for suite_name in suites_seen:
1433 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011434 for sub_suite in suite:
1435 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281436 resolved_suites.add(suite_name)
1437 # At this point, every key in test_suites.pyl should be referenced.
1438 missing_suites = set(self.test_suites.keys()) - resolved_suites
1439 if missing_suites:
1440 raise BBGenErr('The following test suites were unreferenced by bots on '
1441 'the waterfalls: ' + str(missing_suites))
1442
1443 # All test suite exceptions must refer to bots on the waterfall.
1444 all_bots = set()
1445 missing_bots = set()
1446 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431447 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281448 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281449 # In order to disambiguate between bots with the same name on
1450 # different waterfalls, support has been added to various
1451 # exceptions for concatenating the waterfall name after the bot
1452 # name.
1453 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281454 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381455 removals = (exception.get('remove_from', []) +
1456 exception.get('remove_gtest_from', []) +
1457 exception.get('modifications', {}).keys())
1458 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281459 if removal not in all_bots:
1460 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411461
Ben Pastene9a010082019-09-25 20:41:371462 missing_bots = missing_bots - set(builders_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281463 if missing_bots:
1464 raise BBGenErr('The following nonexistent machines were referenced in '
1465 'the test suite exceptions: ' + str(missing_bots))
1466
Stephen Martinis0382bc12018-09-17 22:29:071467 # All mixins must be referenced
1468 seen_mixins = set()
1469 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011470 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071471 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011472 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071473 for suite in self.test_suites.values():
1474 if isinstance(suite, list):
1475 # Don't care about this, it's a composition, which shouldn't include a
1476 # swarming mixin.
1477 continue
1478
1479 for test in suite.values():
1480 if not isinstance(test, dict):
1481 # Some test suites have top level keys, which currently can't be
1482 # swarming mixin entries. Ignore them
1483 continue
1484
Stephen Martinisb72f6d22018-10-04 23:29:011485 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071486
Stephen Martinisb72f6d22018-10-04 23:29:011487 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071488 if missing_mixins:
1489 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1490 ' referenced in a waterfall, machine, or test suite.' % (
1491 str(missing_mixins)))
1492
Jeff Yoonda581c32020-03-06 03:56:051493 # All variant references must be referenced
1494 seen_variants = set()
1495 for suite in self.test_suites.values():
1496 if isinstance(suite, list):
1497 continue
1498
1499 for test in suite.values():
1500 if isinstance(test, dict):
1501 for variant in test.get('variants', []):
1502 if isinstance(variant, str):
1503 seen_variants.add(variant)
1504
1505 missing_variants = set(self.variants.keys()) - seen_variants
1506 if missing_variants:
1507 raise BBGenErr('The following variants were unreferenced: %s. They must '
1508 'be referenced in a matrix test suite under the variants '
1509 'key.' % str(missing_variants))
1510
Stephen Martinis54d64ad2018-09-21 22:16:201511
1512 def type_assert(self, node, typ, filename, verbose=False):
1513 """Asserts that the Python AST node |node| is of type |typ|.
1514
1515 If verbose is set, it prints out some helpful context lines, showing where
1516 exactly the error occurred in the file.
1517 """
1518 if not isinstance(node, typ):
1519 if verbose:
1520 lines = [""] + self.read_file(filename).splitlines()
1521
1522 context = 2
1523 lines_start = max(node.lineno - context, 0)
1524 # Add one to include the last line
1525 lines_end = min(node.lineno + context, len(lines)) + 1
1526 lines = (
1527 ['== %s ==\n' % filename] +
1528 ["<snip>\n"] +
1529 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1530 lines[lines_start:lines_start + context])] +
1531 ['-' * 80 + '\n'] +
1532 ['%d %s' % (node.lineno, lines[node.lineno])] +
1533 ['-' * (node.col_offset + 3) + '^' + '-' * (
1534 80 - node.col_offset - 4) + '\n'] +
1535 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1536 lines[node.lineno + 1:lines_end])] +
1537 ["<snip>\n"]
1538 )
1539 # Print out a useful message when a type assertion fails.
1540 for l in lines:
1541 self.print_line(l.strip())
1542
1543 node_dumped = ast.dump(node, annotate_fields=False)
1544 # If the node is huge, truncate it so everything fits in a terminal
1545 # window.
1546 if len(node_dumped) > 60: # pragma: no cover
1547 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1548 raise BBGenErr(
1549 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1550 ' be %s, is %s' % (
1551 filename, node_dumped,
1552 node.lineno, typ, type(node)))
1553
Stephen Martinis5bef0fc2020-01-06 22:47:531554 def check_ast_list_formatted(self, keys, filename, verbose,
Stephen Martinis1384ff92020-01-07 19:52:151555 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531556 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201557
Stephen Martinis5bef0fc2020-01-06 22:47:531558 Currently only checks to ensure they're correctly sorted, and that there
1559 are no duplicates.
1560
1561 Args:
1562 keys: An python list of AST nodes.
1563
1564 It's a list of AST nodes instead of a list of strings because
1565 when verbose is set, it tries to print out context of where the
1566 diffs are in the file.
1567 filename: The name of the file this node is from.
1568 verbose: If set, print out diff information about how the keys are
1569 incorrectly formatted.
1570 check_sorting: If true, checks if the list is sorted.
1571 Returns:
1572 If the keys are correctly formatted.
1573 """
1574 if not keys:
1575 return True
1576
1577 assert isinstance(keys[0], ast.Str)
1578
1579 keys_strs = [k.s for k in keys]
1580 # Keys to diff against. Used below.
1581 keys_to_diff_against = None
1582 # If the list is properly formatted.
1583 list_formatted = True
1584
1585 # Duplicates are always bad.
1586 if len(set(keys_strs)) != len(keys_strs):
1587 list_formatted = False
1588 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1589
1590 if check_sorting and sorted(keys_strs) != keys_strs:
1591 list_formatted = False
1592 if list_formatted:
1593 return True
1594
1595 if verbose:
1596 line_num = keys[0].lineno
1597 keys = [k.s for k in keys]
1598 if check_sorting:
1599 # If we have duplicates, sorting this will take care of it anyways.
1600 keys_to_diff_against = sorted(set(keys))
1601 # else, keys_to_diff_against is set above already
1602
1603 self.print_line('=' * 80)
1604 self.print_line('(First line of keys is %s)' % line_num)
1605 for line in difflib.context_diff(
1606 keys, keys_to_diff_against,
1607 fromfile='current (%r)' % filename, tofile='sorted', lineterm=''):
1608 self.print_line(line)
1609 self.print_line('=' * 80)
1610
1611 return False
1612
Stephen Martinis1384ff92020-01-07 19:52:151613 def check_ast_dict_formatted(self, node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531614 """Checks if an ast dictionary's keys are correctly formatted.
1615
1616 Just a simple wrapper around check_ast_list_formatted.
1617 Args:
1618 node: An AST node. Assumed to be a dictionary.
1619 filename: The name of the file this node is from.
1620 verbose: If set, print out diff information about how the keys are
1621 incorrectly formatted.
1622 check_sorting: If true, checks if the list is sorted.
1623 Returns:
1624 If the dictionary is correctly formatted.
1625 """
Stephen Martinis54d64ad2018-09-21 22:16:201626 keys = []
1627 # The keys of this dict are ordered as ordered in the file; normal python
1628 # dictionary keys are given an arbitrary order, but since we parsed the
1629 # file itself, the order as given in the file is preserved.
1630 for key in node.keys:
1631 self.type_assert(key, ast.Str, filename, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531632 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201633
Stephen Martinis1384ff92020-01-07 19:52:151634 return self.check_ast_list_formatted(keys, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181635
1636 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201637 # TODO(https://crbug.com/886993): Add the ability for this script to
1638 # actually format the files, rather than just complain if they're
1639 # incorrectly formatted.
1640 bad_files = set()
Stephen Martinis5bef0fc2020-01-06 22:47:531641 def parse_file(filename):
1642 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201643
Stephen Martinis5bef0fc2020-01-06 22:47:531644 Returns an AST node representing the value in the pyl file."""
Stephen Martinisf83893722018-09-19 00:02:181645 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1646
Stephen Martinisf83893722018-09-19 00:02:181647 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201648 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181649 module = parsed.body
1650
1651 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201652 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181653 if len(module) != 1: # pragma: no cover
1654 raise BBGenErr('Invalid .pyl file %s' % filename)
1655 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201656 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181657
Stephen Martinis5bef0fc2020-01-06 22:47:531658 return expr.value
1659
1660 # Handle this separately
1661 filename = 'waterfalls.pyl'
1662 value = parse_file(filename)
1663 # Value should be a list.
1664 self.type_assert(value, ast.List, filename, verbose)
1665
1666 keys = []
1667 for val in value.elts:
1668 self.type_assert(val, ast.Dict, filename, verbose)
1669 waterfall_name = None
1670 for key, val in zip(val.keys, val.values):
1671 self.type_assert(key, ast.Str, filename, verbose)
1672 if key.s == 'machines':
1673 if not self.check_ast_dict_formatted(val, filename, verbose):
1674 bad_files.add(filename)
1675
1676 if key.s == "name":
1677 self.type_assert(val, ast.Str, filename, verbose)
1678 waterfall_name = val
1679 assert waterfall_name
1680 keys.append(waterfall_name)
1681
Stephen Martinis1384ff92020-01-07 19:52:151682 if not self.check_ast_list_formatted(keys, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531683 bad_files.add(filename)
1684
1685 for filename in (
1686 'mixins.pyl',
1687 'test_suites.pyl',
1688 'test_suite_exceptions.pyl',
1689 ):
1690 value = parse_file(filename)
Stephen Martinisf83893722018-09-19 00:02:181691 # Value should be a dictionary.
Stephen Martinis54d64ad2018-09-21 22:16:201692 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181693
Stephen Martinis5bef0fc2020-01-06 22:47:531694 if not self.check_ast_dict_formatted(
1695 value, filename, verbose):
1696 bad_files.add(filename)
1697
Stephen Martinis54d64ad2018-09-21 22:16:201698 if filename == 'test_suites.pyl':
Jeff Yoon8154e582019-12-03 23:30:011699 expected_keys = ['basic_suites',
1700 'compound_suites',
1701 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201702 actual_keys = [node.s for node in value.keys]
1703 assert all(key in expected_keys for key in actual_keys), (
1704 'Invalid %r file; expected keys %r, got %r' % (
1705 filename, expected_keys, actual_keys))
1706 suite_dicts = [node for node in value.values]
1707 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011708 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201709 for suite_group in suite_dicts:
Stephen Martinis5bef0fc2020-01-06 22:47:531710 if not self.check_ast_dict_formatted(
Stephen Martinis54d64ad2018-09-21 22:16:201711 suite_group, filename, verbose):
1712 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181713
Stephen Martinis5bef0fc2020-01-06 22:47:531714 for key, suite in zip(value.keys, value.values):
1715 # The compound suites are checked in
1716 # 'check_composition_type_test_suites()'
1717 if key.s == 'basic_suites':
1718 for group in suite.values:
Stephen Martinis1384ff92020-01-07 19:52:151719 if not self.check_ast_dict_formatted(group, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531720 bad_files.add(filename)
1721 break
Stephen Martinis54d64ad2018-09-21 22:16:201722
Stephen Martinis5bef0fc2020-01-06 22:47:531723 elif filename == 'test_suite_exceptions.pyl':
1724 # Check the values for each test.
1725 for test in value.values:
1726 for kind, node in zip(test.keys, test.values):
1727 if isinstance(node, ast.Dict):
Stephen Martinis1384ff92020-01-07 19:52:151728 if not self.check_ast_dict_formatted(node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531729 bad_files.add(filename)
1730 elif kind.s == 'remove_from':
1731 # Don't care about sorting; these are usually grouped, since the
1732 # same bug can affect multiple builders. Do want to make sure
1733 # there aren't duplicates.
1734 if not self.check_ast_list_formatted(node.elts, filename, verbose,
1735 check_sorting=False):
1736 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181737
1738 if bad_files:
1739 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201740 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531741 'unsorted, or have duplicates. Re-run this with --verbose to see '
1742 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181743
Kenneth Russelleb60cbd22017-12-05 07:54:281744 def check_output_file_consistency(self, verbose=False):
1745 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011746 # All waterfalls/bucket .json files must have been written
1747 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:281748 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011749 ungenerated_files = set()
1750 for filename, expected_contents in self.generate_outputs().items():
1751 expected = self.jsonify(expected_contents)
1752 file_path = filename + '.json'
Zhiling Huangbe008172018-03-08 19:13:111753 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281754 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:011755 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:321756 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:011757 self.print_line('File ' + filename +
1758 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321759 'contents:')
1760 for line in difflib.unified_diff(
1761 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501762 current.splitlines(),
1763 fromfile='expected', tofile='current'):
1764 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:011765
1766 if ungenerated_files:
1767 raise BBGenErr(
1768 'The following files have not been properly '
1769 'autogenerated by generate_buildbot_json.py: ' +
1770 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:281771
1772 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501773 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281774 self.check_output_file_consistency(verbose) # pragma: no cover
1775
1776 def parse_args(self, argv): # pragma: no cover
Karen Qiane24b7ee2019-02-12 23:37:061777
1778 # RawTextHelpFormatter allows for styling of help statement
1779 parser = argparse.ArgumentParser(formatter_class=
1780 argparse.RawTextHelpFormatter)
1781
1782 group = parser.add_mutually_exclusive_group()
1783 group.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281784 '-c', '--check', action='store_true', help=
1785 'Do consistency checks of configuration and generated files and then '
1786 'exit. Used during presubmit. Causes the tool to not generate any files.')
Karen Qiane24b7ee2019-02-12 23:37:061787 group.add_argument(
1788 '--query', type=str, help=
1789 ("Returns raw JSON information of buildbots and tests.\n" +
1790 "Examples:\n" +
1791 " List all bots (all info):\n" +
1792 " --query bots\n\n" +
1793 " List all bots and only their associated tests:\n" +
1794 " --query bots/tests\n\n" +
1795 " List all information about 'bot1' " +
1796 "(make sure you have quotes):\n" +
1797 " --query bot/'bot1'\n\n" +
1798 " List tests running for 'bot1' (make sure you have quotes):\n" +
1799 " --query bot/'bot1'/tests\n\n" +
1800 " List all tests:\n" +
1801 " --query tests\n\n" +
1802 " List all tests and the bots running them:\n" +
1803 " --query tests/bots\n\n"+
1804 " List all tests that satisfy multiple parameters\n" +
1805 " (separation of parameters by '&' symbol):\n" +
1806 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
1807 " List all tests that run with a specific flag:\n" +
1808 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
1809 " List specific test (make sure you have quotes):\n"
1810 " --query test/'test1'\n\n"
1811 " List all bots running 'test1' " +
1812 "(make sure you have quotes):\n" +
1813 " --query test/'test1'/bots" ))
Kenneth Russelleb60cbd22017-12-05 07:54:281814 parser.add_argument(
1815 '-n', '--new-files', action='store_true', help=
1816 'Write output files as .new.json. Useful during development so old and '
1817 'new files can be looked at side-by-side.')
1818 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501819 '-v', '--verbose', action='store_true', help=
1820 'Increases verbosity. Affects consistency checks.')
1821 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281822 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1823 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111824 parser.add_argument(
1825 '--pyl-files-dir', type=os.path.realpath,
1826 help='Path to the directory containing the input .pyl files.')
Karen Qiane24b7ee2019-02-12 23:37:061827 parser.add_argument(
1828 '--json', help=
1829 ("Outputs results into a json file. Only works with query function.\n" +
1830 "Examples:\n" +
1831 " Outputs file into specified json file: \n" +
1832 " --json <file-name-here.json>"))
Garrett Beatyd5ca75962020-05-07 16:58:311833 parser.add_argument(
1834 '--infra-config-dir',
1835 help='Path to the LUCI services configuration directory',
1836 default=os.path.abspath(
1837 os.path.join(os.path.dirname(__file__),
1838 '..', '..', 'infra', 'config')))
Kenneth Russelleb60cbd22017-12-05 07:54:281839 self.args = parser.parse_args(argv)
Karen Qiane24b7ee2019-02-12 23:37:061840 if self.args.json and not self.args.query:
1841 parser.error("The --json flag can only be used with --query.")
Garrett Beatyd5ca75962020-05-07 16:58:311842 self.args.infra_config_dir = os.path.abspath(self.args.infra_config_dir)
Karen Qiane24b7ee2019-02-12 23:37:061843
1844 def does_test_match(self, test_info, params_dict):
1845 """Checks to see if the test matches the parameters given.
1846
1847 Compares the provided test_info with the params_dict to see
1848 if the bot matches the parameters given. If so, returns True.
1849 Else, returns false.
1850
1851 Args:
1852 test_info (dict): Information about a specific bot provided
1853 in the format shown in waterfalls.pyl
1854 params_dict (dict): Dictionary of parameters and their values
1855 to look for in the bot
1856 Ex: {
1857 'device_os':'android',
1858 '--flag':True,
1859 'mixins': ['mixin1', 'mixin2'],
1860 'ex_key':'ex_value'
1861 }
1862
1863 """
1864 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
1865 'kvm', 'pool', 'integrity'] # dimension parameters
1866 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
1867 'can_use_on_swarming_builders']
1868 for param in params_dict:
1869 # if dimension parameter
1870 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
1871 if not 'swarming' in test_info:
1872 return False
1873 swarming = test_info['swarming']
1874 if param in SWARMING_PARAMS:
1875 if not param in swarming:
1876 return False
1877 if not str(swarming[param]) == params_dict[param]:
1878 return False
1879 else:
1880 if not 'dimension_sets' in swarming:
1881 return False
1882 d_set = swarming['dimension_sets']
1883 # only looking at the first dimension set
1884 if not param in d_set[0]:
1885 return False
1886 if not d_set[0][param] == params_dict[param]:
1887 return False
1888
1889 # if flag
1890 elif param.startswith('--'):
1891 if not 'args' in test_info:
1892 return False
1893 if not param in test_info['args']:
1894 return False
1895
1896 # not dimension parameter/flag/mixin
1897 else:
1898 if not param in test_info:
1899 return False
1900 if not test_info[param] == params_dict[param]:
1901 return False
1902 return True
1903 def error_msg(self, msg):
1904 """Prints an error message.
1905
1906 In addition to a catered error message, also prints
1907 out where the user can find more help. Then, program exits.
1908 """
1909 self.print_line(msg + (' If you need more information, ' +
1910 'please run with -h or --help to see valid commands.'))
1911 sys.exit(1)
1912
1913 def find_bots_that_run_test(self, test, bots):
1914 matching_bots = []
1915 for bot in bots:
1916 bot_info = bots[bot]
1917 tests = self.flatten_tests_for_bot(bot_info)
1918 for test_info in tests:
1919 test_name = ""
1920 if 'name' in test_info:
1921 test_name = test_info['name']
1922 elif 'test' in test_info:
1923 test_name = test_info['test']
1924 if not test_name == test:
1925 continue
1926 matching_bots.append(bot)
1927 return matching_bots
1928
1929 def find_tests_with_params(self, tests, params_dict):
1930 matching_tests = []
1931 for test_name in tests:
1932 test_info = tests[test_name]
1933 if not self.does_test_match(test_info, params_dict):
1934 continue
1935 if not test_name in matching_tests:
1936 matching_tests.append(test_name)
1937 return matching_tests
1938
1939 def flatten_waterfalls_for_query(self, waterfalls):
1940 bots = {}
1941 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:011942 waterfall_tests = self.generate_output_tests(waterfall)
1943 for bot in waterfall_tests:
1944 bot_info = waterfall_tests[bot]
1945 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:061946 return bots
1947
1948 def flatten_tests_for_bot(self, bot_info):
1949 """Returns a list of flattened tests.
1950
1951 Returns a list of tests not grouped by test category
1952 for a specific bot.
1953 """
1954 TEST_CATS = self.get_test_generator_map().keys()
1955 tests = []
1956 for test_cat in TEST_CATS:
1957 if not test_cat in bot_info:
1958 continue
1959 test_cat_tests = bot_info[test_cat]
1960 tests = tests + test_cat_tests
1961 return tests
1962
1963 def flatten_tests_for_query(self, test_suites):
1964 """Returns a flattened dictionary of tests.
1965
1966 Returns a dictionary of tests associate with their
1967 configuration, not grouped by their test suite.
1968 """
1969 tests = {}
1970 for test_suite in test_suites.itervalues():
1971 for test in test_suite:
1972 test_info = test_suite[test]
1973 test_name = test
1974 if 'name' in test_info:
1975 test_name = test_info['name']
1976 tests[test_name] = test_info
1977 return tests
1978
1979 def parse_query_filter_params(self, params):
1980 """Parses the filter parameters.
1981
1982 Creates a dictionary from the parameters provided
1983 to filter the bot array.
1984 """
1985 params_dict = {}
1986 for p in params:
1987 # flag
1988 if p.startswith("--"):
1989 params_dict[p] = True
1990 else:
1991 pair = p.split(":")
1992 if len(pair) != 2:
1993 self.error_msg('Invalid command.')
1994 # regular parameters
1995 if pair[1].lower() == "true":
1996 params_dict[pair[0]] = True
1997 elif pair[1].lower() == "false":
1998 params_dict[pair[0]] = False
1999 else:
2000 params_dict[pair[0]] = pair[1]
2001 return params_dict
2002
2003 def get_test_suites_dict(self, bots):
2004 """Returns a dictionary of bots and their tests.
2005
2006 Returns a dictionary of bots and a list of their associated tests.
2007 """
2008 test_suite_dict = dict()
2009 for bot in bots:
2010 bot_info = bots[bot]
2011 tests = self.flatten_tests_for_bot(bot_info)
2012 test_suite_dict[bot] = tests
2013 return test_suite_dict
2014
2015 def output_query_result(self, result, json_file=None):
2016 """Outputs the result of the query.
2017
2018 If a json file parameter name is provided, then
2019 the result is output into the json file. If not,
2020 then the result is printed to the console.
2021 """
2022 output = json.dumps(result, indent=2)
2023 if json_file:
2024 self.write_file(json_file, output)
2025 else:
2026 self.print_line(output)
2027 return
2028
2029 def query(self, args):
2030 """Queries tests or bots.
2031
2032 Depending on the arguments provided, outputs a json of
2033 tests or bots matching the appropriate optional parameters provided.
2034 """
2035 # split up query statement
2036 query = args.query.split('/')
2037 self.load_configuration_files()
2038 self.resolve_configuration_files()
2039
2040 # flatten bots json
2041 tests = self.test_suites
2042 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2043
2044 cmd_class = query[0]
2045
2046 # For queries starting with 'bots'
2047 if cmd_class == "bots":
2048 if len(query) == 1:
2049 return self.output_query_result(bots, args.json)
2050 # query with specific parameters
2051 elif len(query) == 2:
2052 if query[1] == 'tests':
2053 test_suites_dict = self.get_test_suites_dict(bots)
2054 return self.output_query_result(test_suites_dict, args.json)
2055 else:
2056 self.error_msg("This query should be in the format: bots/tests.")
2057
2058 else:
2059 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2060 % str(len(query)-1))
2061
2062 # For queries starting with 'bot'
2063 elif cmd_class == "bot":
2064 if not len(query) == 2 and not len(query) == 3:
2065 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2066 % str(len(query)-1))
2067 bot_id = query[1]
2068 if not bot_id in bots:
2069 self.error_msg("No bot named '" + bot_id + "' found.")
2070 bot_info = bots[bot_id]
2071 if len(query) == 2:
2072 return self.output_query_result(bot_info, args.json)
2073 if not query[2] == 'tests':
2074 self.error_msg("The query should be in the format:" +
2075 "bot/<bot-name>/tests.")
2076
2077 bot_tests = self.flatten_tests_for_bot(bot_info)
2078 return self.output_query_result(bot_tests, args.json)
2079
2080 # For queries starting with 'tests'
2081 elif cmd_class == "tests":
2082 if not len(query) == 1 and not len(query) == 2:
2083 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2084 % str(len(query)-1))
2085 flattened_tests = self.flatten_tests_for_query(tests)
2086 if len(query) == 1:
2087 return self.output_query_result(flattened_tests, args.json)
2088
2089 # create params dict
2090 params = query[1].split('&')
2091 params_dict = self.parse_query_filter_params(params)
2092 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2093 return self.output_query_result(matching_bots)
2094
2095 # For queries starting with 'test'
2096 elif cmd_class == "test":
2097 if not len(query) == 2 and not len(query) == 3:
2098 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2099 % str(len(query)-1))
2100 test_id = query[1]
2101 if len(query) == 2:
2102 flattened_tests = self.flatten_tests_for_query(tests)
2103 for test in flattened_tests:
2104 if test == test_id:
2105 return self.output_query_result(flattened_tests[test], args.json)
2106 self.error_msg("There is no test named %s." % test_id)
2107 if not query[2] == 'bots':
2108 self.error_msg("The query should be in the format: " +
2109 "test/<test-name>/bots")
2110 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2111 return self.output_query_result(bots_for_test)
2112
2113 else:
2114 self.error_msg("Your command did not match any valid commands." +
2115 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:282116
2117 def main(self, argv): # pragma: no cover
2118 self.parse_args(argv)
2119 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502120 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062121 elif self.args.query:
2122 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282123 else:
Greg Gutermanf60eb052020-03-12 17:40:012124 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282125 return 0
2126
2127if __name__ == "__main__": # pragma: no cover
2128 generator = BBJSONGenerator()
John Budorick699282e2019-02-13 01:27:332129 sys.exit(generator.main(sys.argv[1:]))