blob: a39a0848f9211f4ff634e1dd381cd0863f919b23 [file] [log] [blame]
Kenneth Russelleb60cbd22017-12-05 07:54:281#!/usr/bin/env python
2# Copyright 2016 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Script to generate the majority of the JSON files in the src/testing/buildbot
7directory. Maintaining these files by hand is too unwieldy.
8"""
9
10import argparse
11import ast
12import collections
13import copy
John Budorick826d5ed2017-12-28 19:27:3214import difflib
Garrett Beatyd5ca75962020-05-07 16:58:3115import glob
Kenneth Russell8ceeabf2017-12-11 17:53:2816import itertools
Kenneth Russelleb60cbd22017-12-05 07:54:2817import json
18import os
Greg Gutermanf60eb052020-03-12 17:40:0119import re
Kenneth Russelleb60cbd22017-12-05 07:54:2820import string
21import sys
John Budorick826d5ed2017-12-28 19:27:3222import traceback
Kenneth Russelleb60cbd22017-12-05 07:54:2823
Brian Sheedya31578e2020-05-18 20:24:3624import buildbot_json_magic_substitutions as magic_substitutions
25
Kenneth Russelleb60cbd22017-12-05 07:54:2826THIS_DIR = os.path.dirname(os.path.abspath(__file__))
27
28
29class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4930 def __init__(self, message):
31 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2832
33
Kenneth Russell8ceeabf2017-12-11 17:53:2834# This class is only present to accommodate certain machines on
35# chromium.android.fyi which run certain tests as instrumentation
36# tests, but not as gtests. If this discrepancy were fixed then the
37# notion could be removed.
38class TestSuiteTypes(object):
39 GTEST = 'gtest'
40
41
Kenneth Russelleb60cbd22017-12-05 07:54:2842class BaseGenerator(object):
43 def __init__(self, bb_gen):
44 self.bb_gen = bb_gen
45
Kenneth Russell8ceeabf2017-12-11 17:53:2846 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2847 raise NotImplementedError()
48
49 def sort(self, tests):
50 raise NotImplementedError()
51
52
Kenneth Russell8ceeabf2017-12-11 17:53:2853def cmp_tests(a, b):
54 # Prefer to compare based on the "test" key.
55 val = cmp(a['test'], b['test'])
56 if val != 0:
57 return val
58 if 'name' in a and 'name' in b:
59 return cmp(a['name'], b['name']) # pragma: no cover
60 if 'name' not in a and 'name' not in b:
61 return 0 # pragma: no cover
62 # Prefer to put variants of the same test after the first one.
63 if 'name' in a:
64 return 1
65 # 'name' is in b.
66 return -1 # pragma: no cover
67
68
Kenneth Russell8a386d42018-06-02 09:48:0169class GPUTelemetryTestGenerator(BaseGenerator):
Bo Liu555a0f92019-03-29 12:11:5670
71 def __init__(self, bb_gen, is_android_webview=False):
Kenneth Russell8a386d42018-06-02 09:48:0172 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5673 self._is_android_webview = is_android_webview
Kenneth Russell8a386d42018-06-02 09:48:0174
75 def generate(self, waterfall, tester_name, tester_config, input_tests):
76 isolated_scripts = []
77 for test_name, test_config in sorted(input_tests.iteritems()):
78 test = self.bb_gen.generate_gpu_telemetry_test(
Bo Liu555a0f92019-03-29 12:11:5679 waterfall, tester_name, tester_config, test_name, test_config,
80 self._is_android_webview)
Kenneth Russell8a386d42018-06-02 09:48:0181 if test:
82 isolated_scripts.append(test)
83 return isolated_scripts
84
85 def sort(self, tests):
86 return sorted(tests, key=lambda x: x['name'])
87
88
Kenneth Russelleb60cbd22017-12-05 07:54:2889class GTestGenerator(BaseGenerator):
90 def __init__(self, bb_gen):
91 super(GTestGenerator, self).__init__(bb_gen)
92
Kenneth Russell8ceeabf2017-12-11 17:53:2893 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2894 # The relative ordering of some of the tests is important to
95 # minimize differences compared to the handwritten JSON files, since
96 # Python's sorts are stable and there are some tests with the same
97 # key (see gles2_conform_d3d9_test and similar variants). Avoid
98 # losing the order by avoiding coalescing the dictionaries into one.
99 gtests = []
100 for test_name, test_config in sorted(input_tests.iteritems()):
Jeff Yoon67c3e832020-02-08 07:39:38101 # Variants allow more than one definition for a given test, and is defined
102 # in array format from resolve_variants().
103 if not isinstance(test_config, list):
104 test_config = [test_config]
105
106 for config in test_config:
107 test = self.bb_gen.generate_gtest(
108 waterfall, tester_name, tester_config, test_name, config)
109 if test:
110 # generate_gtest may veto the test generation on this tester.
111 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28112 return gtests
113
114 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28115 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28116
117
118class IsolatedScriptTestGenerator(BaseGenerator):
119 def __init__(self, bb_gen):
120 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
121
Kenneth Russell8ceeabf2017-12-11 17:53:28122 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28123 isolated_scripts = []
124 for test_name, test_config in sorted(input_tests.iteritems()):
Jeff Yoonb8bfdbf32020-03-13 19:14:43125 # Variants allow more than one definition for a given test, and is defined
126 # in array format from resolve_variants().
127 if not isinstance(test_config, list):
128 test_config = [test_config]
129
130 for config in test_config:
131 test = self.bb_gen.generate_isolated_script_test(
132 waterfall, tester_name, tester_config, test_name, config)
133 if test:
134 isolated_scripts.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28135 return isolated_scripts
136
137 def sort(self, tests):
138 return sorted(tests, key=lambda x: x['name'])
139
140
141class ScriptGenerator(BaseGenerator):
142 def __init__(self, bb_gen):
143 super(ScriptGenerator, self).__init__(bb_gen)
144
Kenneth Russell8ceeabf2017-12-11 17:53:28145 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28146 scripts = []
147 for test_name, test_config in sorted(input_tests.iteritems()):
148 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28149 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28150 if test:
151 scripts.append(test)
152 return scripts
153
154 def sort(self, tests):
155 return sorted(tests, key=lambda x: x['name'])
156
157
158class JUnitGenerator(BaseGenerator):
159 def __init__(self, bb_gen):
160 super(JUnitGenerator, self).__init__(bb_gen)
161
Kenneth Russell8ceeabf2017-12-11 17:53:28162 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28163 scripts = []
164 for test_name, test_config in sorted(input_tests.iteritems()):
165 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28166 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28167 if test:
168 scripts.append(test)
169 return scripts
170
171 def sort(self, tests):
172 return sorted(tests, key=lambda x: x['test'])
173
174
175class CTSGenerator(BaseGenerator):
176 def __init__(self, bb_gen):
177 super(CTSGenerator, self).__init__(bb_gen)
178
Kenneth Russell8ceeabf2017-12-11 17:53:28179 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28180 # These only contain one entry and it's the contents of the input tests'
181 # dictionary, verbatim.
182 cts_tests = []
183 cts_tests.append(input_tests)
184 return cts_tests
185
186 def sort(self, tests):
187 return tests
188
189
190class InstrumentationTestGenerator(BaseGenerator):
191 def __init__(self, bb_gen):
192 super(InstrumentationTestGenerator, self).__init__(bb_gen)
193
Kenneth Russell8ceeabf2017-12-11 17:53:28194 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28195 scripts = []
196 for test_name, test_config in sorted(input_tests.iteritems()):
197 test = self.bb_gen.generate_instrumentation_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28198 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28199 if test:
200 scripts.append(test)
201 return scripts
202
203 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28204 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28205
206
Jeff Yoon67c3e832020-02-08 07:39:38207def check_compound_references(other_test_suites=None,
208 sub_suite=None,
209 suite=None,
210 target_test_suites=None,
211 test_type=None,
212 **kwargs):
213 """Ensure comound reference's don't target other compounds"""
214 del kwargs
215 if sub_suite in other_test_suites or sub_suite in target_test_suites:
216 raise BBGenErr('%s may not refer to other composition type test '
217 'suites (error found while processing %s)'
218 % (test_type, suite))
219
220def check_basic_references(basic_suites=None,
221 sub_suite=None,
222 suite=None,
223 **kwargs):
224 """Ensure test has a basic suite reference"""
225 del kwargs
226 if sub_suite not in basic_suites:
227 raise BBGenErr('Unable to find reference to %s while processing %s'
228 % (sub_suite, suite))
229
230def check_conflicting_definitions(basic_suites=None,
231 seen_tests=None,
232 sub_suite=None,
233 suite=None,
234 test_type=None,
235 **kwargs):
236 """Ensure that if a test is reachable via multiple basic suites,
237 all of them have an identical definition of the tests.
238 """
239 del kwargs
240 for test_name in basic_suites[sub_suite]:
241 if (test_name in seen_tests and
242 basic_suites[sub_suite][test_name] !=
243 basic_suites[seen_tests[test_name]][test_name]):
244 raise BBGenErr('Conflicting test definitions for %s from %s '
245 'and %s in %s (error found while processing %s)'
246 % (test_name, seen_tests[test_name], sub_suite,
247 test_type, suite))
248 seen_tests[test_name] = sub_suite
249
250def check_matrix_identifier(sub_suite=None,
251 suite=None,
252 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05253 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38254 **kwargs):
255 """Ensure 'idenfitier' is defined for each variant"""
256 del kwargs
257 sub_suite_config = suite_def[sub_suite]
258 for variant in sub_suite_config.get('variants', []):
Jeff Yoonda581c32020-03-06 03:56:05259 if isinstance(variant, str):
260 if variant not in all_variants:
261 raise BBGenErr('Missing variant definition for %s in variants.pyl'
262 % variant)
263 variant = all_variants[variant]
264
Jeff Yoon67c3e832020-02-08 07:39:38265 if not 'identifier' in variant:
266 raise BBGenErr('Missing required identifier field in matrix '
267 'compound suite %s, %s' % (suite, sub_suite))
268
269
Kenneth Russelleb60cbd22017-12-05 07:54:28270class BBJSONGenerator(object):
271 def __init__(self):
272 self.this_dir = THIS_DIR
273 self.args = None
274 self.waterfalls = None
275 self.test_suites = None
276 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01277 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41278 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05279 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28280
281 def generate_abs_file_path(self, relative_path):
282 return os.path.join(self.this_dir, relative_path) # pragma: no cover
283
Stephen Martinis7eb8b612018-09-21 00:17:50284 def print_line(self, line):
285 # Exists so that tests can mock
286 print line # pragma: no cover
287
Kenneth Russelleb60cbd22017-12-05 07:54:28288 def read_file(self, relative_path):
289 with open(self.generate_abs_file_path(
290 relative_path)) as fp: # pragma: no cover
291 return fp.read() # pragma: no cover
292
293 def write_file(self, relative_path, contents):
294 with open(self.generate_abs_file_path(
295 relative_path), 'wb') as fp: # pragma: no cover
296 fp.write(contents) # pragma: no cover
297
Zhiling Huangbe008172018-03-08 19:13:11298 def pyl_file_path(self, filename):
299 if self.args and self.args.pyl_files_dir:
300 return os.path.join(self.args.pyl_files_dir, filename)
301 return filename
302
Kenneth Russelleb60cbd22017-12-05 07:54:28303 def load_pyl_file(self, filename):
304 try:
Zhiling Huangbe008172018-03-08 19:13:11305 return ast.literal_eval(self.read_file(
306 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28307 except (SyntaxError, ValueError) as e: # pragma: no cover
308 raise BBGenErr('Failed to parse pyl file "%s": %s' %
309 (filename, e)) # pragma: no cover
310
Kenneth Russell8a386d42018-06-02 09:48:01311 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
312 # Currently it is only mandatory for bots which run GPU tests. Change these to
313 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28314 def is_android(self, tester_config):
315 return tester_config.get('os_type') == 'android'
316
Ben Pastenea9e583b2019-01-16 02:57:26317 def is_chromeos(self, tester_config):
318 return tester_config.get('os_type') == 'chromeos'
319
Kenneth Russell8a386d42018-06-02 09:48:01320 def is_linux(self, tester_config):
321 return tester_config.get('os_type') == 'linux'
322
Kai Ninomiya40de9f52019-10-18 21:38:49323 def is_mac(self, tester_config):
324 return tester_config.get('os_type') == 'mac'
325
326 def is_win(self, tester_config):
327 return tester_config.get('os_type') == 'win'
328
329 def is_win64(self, tester_config):
330 return (tester_config.get('os_type') == 'win' and
331 tester_config.get('browser_config') == 'release_x64')
332
Kenneth Russelleb60cbd22017-12-05 07:54:28333 def get_exception_for_test(self, test_name, test_config):
334 # gtests may have both "test" and "name" fields, and usually, if the "name"
335 # field is specified, it means that the same test is being repurposed
336 # multiple times with different command line arguments. To handle this case,
337 # prefer to lookup per the "name" field of the test itself, as opposed to
338 # the "test_name", which is actually the "test" field.
339 if 'name' in test_config:
340 return self.exceptions.get(test_config['name'])
341 else:
342 return self.exceptions.get(test_name)
343
Nico Weberb0b3f5862018-07-13 18:45:15344 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28345 # Currently, the only reason a test should not run on a given tester is that
346 # it's in the exceptions. (Once the GPU waterfall generation script is
347 # incorporated here, the rules will become more complex.)
348 exception = self.get_exception_for_test(test_name, test_config)
349 if not exception:
350 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28351 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28352 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28353 if remove_from:
354 if tester_name in remove_from:
355 return False
356 # TODO(kbr): this code path was added for some tests (including
357 # android_webview_unittests) on one machine (Nougat Phone
358 # Tester) which exists with the same name on two waterfalls,
359 # chromium.android and chromium.fyi; the tests are run on one
360 # but not the other. Once the bots are all uniquely named (a
361 # different ongoing project) this code should be removed.
362 # TODO(kbr): add coverage.
363 return (tester_name + ' ' + waterfall['name']
364 not in remove_from) # pragma: no cover
365 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28366
Nico Weber79dc5f6852018-07-13 19:38:49367 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28368 exception = self.get_exception_for_test(test_name, test)
369 if not exception:
370 return None
Nico Weber79dc5f6852018-07-13 19:38:49371 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28372
Brian Sheedye6ea0ee2019-07-11 02:54:37373 def get_test_replacements(self, test, test_name, tester_name):
374 exception = self.get_exception_for_test(test_name, test)
375 if not exception:
376 return None
377 return exception.get('replacements', {}).get(tester_name)
378
Kenneth Russell8a386d42018-06-02 09:48:01379 def merge_command_line_args(self, arr, prefix, splitter):
380 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01381 idx = 0
382 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01383 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01384 while idx < len(arr):
385 flag = arr[idx]
386 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01387 if flag.startswith(prefix):
388 arg = flag[prefix_len:]
389 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01390 if first_idx < 0:
391 first_idx = idx
392 else:
393 delete_current_entry = True
394 if delete_current_entry:
395 del arr[idx]
396 else:
397 idx += 1
398 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01399 arr[first_idx] = prefix + splitter.join(accumulated_args)
400 return arr
401
402 def maybe_fixup_args_array(self, arr):
403 # The incoming array of strings may be an array of command line
404 # arguments. To make it easier to turn on certain features per-bot or
405 # per-test-suite, look specifically for certain flags and merge them
406 # appropriately.
407 # --enable-features=Feature1 --enable-features=Feature2
408 # are merged to:
409 # --enable-features=Feature1,Feature2
410 # and:
411 # --extra-browser-args=arg1 --extra-browser-args=arg2
412 # are merged to:
413 # --extra-browser-args=arg1 arg2
414 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
415 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01416 return arr
417
Brian Sheedya31578e2020-05-18 20:24:36418 def substitute_magic_args(self, test_config):
419 """Substitutes any magic substitution args present in |test_config|.
420
421 Substitutions are done in-place.
422
423 See buildbot_json_magic_substitutions.py for more information on this
424 feature.
425
426 Args:
427 test_config: A dict containing a configuration for a specific test on
428 a specific builder, e.g. the output of update_and_cleanup_test.
429 """
430 substituted_array = []
431 for arg in test_config.get('args', []):
432 if arg.startswith(magic_substitutions.MAGIC_SUBSTITUTION_PREFIX):
433 function = arg.replace(
434 magic_substitutions.MAGIC_SUBSTITUTION_PREFIX, '')
435 if hasattr(magic_substitutions, function):
436 substituted_array.extend(
437 getattr(magic_substitutions, function)(test_config))
438 else:
439 raise BBGenErr(
440 'Magic substitution function %s does not exist' % function)
441 else:
442 substituted_array.append(arg)
443 if substituted_array:
444 test_config['args'] = self.maybe_fixup_args_array(substituted_array)
445
Kenneth Russelleb60cbd22017-12-05 07:54:28446 def dictionary_merge(self, a, b, path=None, update=True):
447 """http://stackoverflow.com/questions/7204805/
448 python-dictionaries-of-dictionaries-merge
449 merges b into a
450 """
451 if path is None:
452 path = []
453 for key in b:
454 if key in a:
455 if isinstance(a[key], dict) and isinstance(b[key], dict):
456 self.dictionary_merge(a[key], b[key], path + [str(key)])
457 elif a[key] == b[key]:
458 pass # same leaf value
459 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06460 # Args arrays are lists of strings. Just concatenate them,
461 # and don't sort them, in order to keep some needed
462 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28463 if all(isinstance(x, str)
464 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01465 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28466 else:
467 # TODO(kbr): this only works properly if the two arrays are
468 # the same length, which is currently always the case in the
469 # swarming dimension_sets that we have to merge. It will fail
470 # to merge / override 'args' arrays which are different
471 # length.
472 for idx in xrange(len(b[key])):
473 try:
474 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
475 path + [str(key), str(idx)],
476 update=update)
Jeff Yoon8154e582019-12-03 23:30:01477 except (IndexError, TypeError):
478 raise BBGenErr('Error merging lists by key "%s" from source %s '
479 'into target %s at index %s. Verify target list '
480 'length is equal or greater than source'
481 % (str(key), str(b), str(a), str(idx)))
John Budorick5bc387fe2019-05-09 20:02:53482 elif update:
483 if b[key] is None:
484 del a[key]
485 else:
486 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28487 else:
488 raise BBGenErr('Conflict at %s' % '.'.join(
489 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53490 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28491 a[key] = b[key]
492 return a
493
John Budorickab108712018-09-01 00:12:21494 def initialize_args_for_test(
495 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21496 args = []
497 args.extend(generated_test.get('args', []))
498 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22499
Kenneth Russell8a386d42018-06-02 09:48:01500 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21501 val = generated_test.pop(key, [])
502 if fn(tester_config):
503 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01504
505 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
506 add_conditional_args('linux_args', self.is_linux)
507 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36508 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49509 add_conditional_args('mac_args', self.is_mac)
510 add_conditional_args('win_args', self.is_win)
511 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01512
John Budorickab108712018-09-01 00:12:21513 for key in additional_arg_keys or []:
514 args.extend(generated_test.pop(key, []))
515 args.extend(tester_config.get(key, []))
516
517 if args:
518 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01519
Kenneth Russelleb60cbd22017-12-05 07:54:28520 def initialize_swarming_dictionary_for_test(self, generated_test,
521 tester_config):
522 if 'swarming' not in generated_test:
523 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28524 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
525 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38526 'can_use_on_swarming_builders': tester_config.get('use_swarming',
527 True)
Dirk Pranke81ff51c2017-12-09 19:24:28528 })
Kenneth Russelleb60cbd22017-12-05 07:54:28529 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03530 if ('dimension_sets' not in generated_test['swarming'] and
531 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28532 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
533 tester_config['swarming']['dimension_sets'])
534 self.dictionary_merge(generated_test['swarming'],
535 tester_config['swarming'])
536 # Apply any Android-specific Swarming dimensions after the generic ones.
537 if 'android_swarming' in generated_test:
538 if self.is_android(tester_config): # pragma: no cover
539 self.dictionary_merge(
540 generated_test['swarming'],
541 generated_test['android_swarming']) # pragma: no cover
542 del generated_test['android_swarming'] # pragma: no cover
543
544 def clean_swarming_dictionary(self, swarming_dict):
545 # Clean out redundant entries from a test's "swarming" dictionary.
546 # This is really only needed to retain 100% parity with the
547 # handwritten JSON files, and can be removed once all the files are
548 # autogenerated.
549 if 'shards' in swarming_dict:
550 if swarming_dict['shards'] == 1: # pragma: no cover
551 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24552 if 'hard_timeout' in swarming_dict:
553 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
554 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43555 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28556 # Remove all other keys.
557 for k in swarming_dict.keys(): # pragma: no cover
558 if k != 'can_use_on_swarming_builders': # pragma: no cover
559 del swarming_dict[k] # pragma: no cover
560
Stephen Martinis0382bc12018-09-17 22:29:07561 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
562 waterfall):
563 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01564 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07565 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28566 # See if there are any exceptions that need to be merged into this
567 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49568 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28569 if modifications:
570 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23571 if 'swarming' in test:
572 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28573 # Ensure all Android Swarming tests run only on userdebug builds if another
574 # build type was not specified.
575 if 'swarming' in test and self.is_android(tester_config):
576 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22577 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28578 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37579 self.replace_test_args(test, test_name, tester_name)
Ben Pastenee012aea42019-05-14 22:32:28580
Kenneth Russelleb60cbd22017-12-05 07:54:28581 return test
582
Brian Sheedye6ea0ee2019-07-11 02:54:37583 def replace_test_args(self, test, test_name, tester_name):
584 replacements = self.get_test_replacements(
585 test, test_name, tester_name) or {}
586 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
587 for key, replacement_dict in replacements.iteritems():
588 if key not in valid_replacement_keys:
589 raise BBGenErr(
590 'Given replacement key %s for %s on %s is not in the list of valid '
591 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
592 for replacement_key, replacement_val in replacement_dict.iteritems():
593 found_key = False
594 for i, test_key in enumerate(test.get(key, [])):
595 # Handle both the key/value being replaced being defined as two
596 # separate items or as key=value.
597 if test_key == replacement_key:
598 found_key = True
599 # Handle flags without values.
600 if replacement_val == None:
601 del test[key][i]
602 else:
603 test[key][i+1] = replacement_val
604 break
605 elif test_key.startswith(replacement_key + '='):
606 found_key = True
607 if replacement_val == None:
608 del test[key][i]
609 else:
610 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
611 break
612 if not found_key:
613 raise BBGenErr('Could not find %s in existing list of values for key '
614 '%s in %s on %s' % (replacement_key, key, test_name,
615 tester_name))
616
Shenghua Zhangaba8bad2018-02-07 02:12:09617 def add_common_test_properties(self, test, tester_config):
618 if tester_config.get('use_multi_dimension_trigger_script'):
Kenneth Russell73c3bd8b2018-10-19 22:30:19619 # Assumes update_and_cleanup_test has already been called, so the
620 # builder's mixins have been flattened into the test.
Shenghua Zhangaba8bad2018-02-07 02:12:09621 test['trigger_script'] = {
622 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
623 'args': [
624 '--multiple-trigger-configs',
Kenneth Russell73c3bd8b2018-10-19 22:30:19625 json.dumps(test['swarming']['dimension_sets'] +
Shenghua Zhangaba8bad2018-02-07 02:12:09626 tester_config.get('alternate_swarming_dimensions', [])),
627 '--multiple-dimension-script-verbose',
628 'True'
629 ],
630 }
Ben Pastenea9e583b2019-01-16 02:57:26631 elif self.is_chromeos(tester_config) and tester_config.get('use_swarming',
632 True):
633 # The presence of the "device_type" dimension indicates that the tests
Brian Sheedy9493da892020-05-13 22:58:06634 # are targeting CrOS hardware and so need the special trigger script.
635 dimension_sets = test['swarming']['dimension_sets']
Ben Pastenea9e583b2019-01-16 02:57:26636 if all('device_type' in ds for ds in dimension_sets):
637 test['trigger_script'] = {
638 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
639 }
Shenghua Zhangaba8bad2018-02-07 02:12:09640
Ben Pastene858f4be2019-01-09 23:52:09641 def add_android_presentation_args(self, tester_config, test_name, result):
642 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38643 bucket = tester_config.get('results_bucket', 'chromium-result-details')
644 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09645 if (result['swarming']['can_use_on_swarming_builders'] and not
646 tester_config.get('skip_merge_script', False)):
647 result['merge'] = {
648 'args': [
649 '--bucket',
John Budorick262ae112019-07-12 19:24:38650 bucket,
Ben Pastene858f4be2019-01-09 23:52:09651 '--test-name',
652 test_name
653 ],
654 'script': '//build/android/pylib/results/presentation/'
655 'test_results_presentation.py',
656 }
657 if not tester_config.get('skip_cipd_packages', False):
Ben Pastenee5949ea82019-01-10 21:45:26658 cipd_packages = result['swarming'].get('cipd_packages', [])
659 cipd_packages.append(
Ben Pastene858f4be2019-01-09 23:52:09660 {
661 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
662 'location': 'bin',
663 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
664 }
Ben Pastenee5949ea82019-01-10 21:45:26665 )
666 result['swarming']['cipd_packages'] = cipd_packages
Ben Pastene858f4be2019-01-09 23:52:09667 if not tester_config.get('skip_output_links', False):
668 result['swarming']['output_links'] = [
669 {
670 'link': [
671 'https://luci-logdog.appspot.com/v/?s',
672 '=android%2Fswarming%2Flogcats%2F',
673 '${TASK_ID}%2F%2B%2Funified_logcats',
674 ],
675 'name': 'shard #${SHARD_INDEX} logcats',
676 },
677 ]
678 if args:
679 result['args'] = args
680
Kenneth Russelleb60cbd22017-12-05 07:54:28681 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
682 test_config):
683 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15684 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28685 return None
686 result = copy.deepcopy(test_config)
687 if 'test' in result:
688 result['name'] = test_name
689 else:
690 result['test'] = test_name
691 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21692
693 self.initialize_args_for_test(
694 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28695 if self.is_android(tester_config) and tester_config.get('use_swarming',
696 True):
Ben Pastene858f4be2019-01-09 23:52:09697 self.add_android_presentation_args(tester_config, test_name, result)
698 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42699
Stephen Martinis0382bc12018-09-17 22:29:07700 result = self.update_and_cleanup_test(
701 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09702 self.add_common_test_properties(result, tester_config)
Brian Sheedya31578e2020-05-18 20:24:36703 self.substitute_magic_args(result)
Stephen Martinisbc7b7772019-05-01 22:01:43704
705 if not result.get('merge'):
706 # TODO(https://crbug.com/958376): Consider adding the ability to not have
707 # this default.
708 result['merge'] = {
709 'script': '//testing/merge_scripts/standard_gtest_merge.py',
710 'args': [],
711 }
Kenneth Russelleb60cbd22017-12-05 07:54:28712 return result
713
714 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
715 test_name, test_config):
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 = copy.deepcopy(test_config)
720 result['isolate_name'] = result.get('isolate_name', test_name)
Jeff Yoonb8bfdbf32020-03-13 19:14:43721 result['name'] = result.get('name', test_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28722 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01723 self.initialize_args_for_test(result, tester_config)
Ben Pastene858f4be2019-01-09 23:52:09724 if tester_config.get('use_android_presentation', False):
725 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07726 result = self.update_and_cleanup_test(
727 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09728 self.add_common_test_properties(result, tester_config)
Brian Sheedya31578e2020-05-18 20:24:36729 self.substitute_magic_args(result)
Stephen Martinisf50047062019-05-06 22:26:17730
731 if not result.get('merge'):
732 # TODO(https://crbug.com/958376): Consider adding the ability to not have
733 # this default.
734 result['merge'] = {
735 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
736 'args': [],
737 }
Kenneth Russelleb60cbd22017-12-05 07:54:28738 return result
739
740 def generate_script_test(self, waterfall, tester_name, tester_config,
741 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44742 # TODO(https://crbug.com/953072): Remove this check whenever a better
743 # long-term solution is implemented.
744 if (waterfall.get('forbid_script_tests', False) or
745 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
746 raise BBGenErr('Attempted to generate a script test on tester ' +
747 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01748 if not self.should_run_on_tester(waterfall, tester_name, test_name,
749 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28750 return None
751 result = {
752 'name': test_name,
753 'script': test_config['script']
754 }
Stephen Martinis0382bc12018-09-17 22:29:07755 result = self.update_and_cleanup_test(
756 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedya31578e2020-05-18 20:24:36757 self.substitute_magic_args(result)
Kenneth Russelleb60cbd22017-12-05 07:54:28758 return result
759
760 def generate_junit_test(self, waterfall, tester_name, tester_config,
761 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01762 if not self.should_run_on_tester(waterfall, tester_name, test_name,
763 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28764 return None
John Budorickdef6acb2019-09-17 22:51:09765 result = copy.deepcopy(test_config)
766 result.update({
John Budorickcadc4952019-09-16 23:51:37767 'name': test_name,
768 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09769 })
770 self.initialize_args_for_test(result, tester_config)
771 result = self.update_and_cleanup_test(
772 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedya31578e2020-05-18 20:24:36773 self.substitute_magic_args(result)
Kenneth Russelleb60cbd22017-12-05 07:54:28774 return result
775
776 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
777 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01778 if not self.should_run_on_tester(waterfall, tester_name, test_name,
779 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28780 return None
781 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28782 if 'test' in result and result['test'] != test_name:
783 result['name'] = test_name
784 else:
785 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07786 result = self.update_and_cleanup_test(
787 result, test_name, tester_name, tester_config, waterfall)
Brian Sheedya31578e2020-05-18 20:24:36788 self.substitute_magic_args(result)
Kenneth Russelleb60cbd22017-12-05 07:54:28789 return result
790
Stephen Martinis2a0667022018-09-25 22:31:14791 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01792 substitutions = {
793 # Any machine in waterfalls.pyl which desires to run GPU tests
794 # must provide the os_type key.
795 'os_type': tester_config['os_type'],
796 'gpu_vendor_id': '0',
797 'gpu_device_id': '0',
798 }
Stephen Martinis2a0667022018-09-25 22:31:14799 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01800 if 'gpu' in dimension_set:
801 # First remove the driver version, then split into vendor and device.
802 gpu = dimension_set['gpu']
Kenneth Russell384a1732019-03-16 02:36:02803 # Handle certain specialized named GPUs.
804 if gpu.startswith('nvidia-quadro-p400'):
805 gpu = ['10de', '1cb3']
806 elif gpu.startswith('intel-hd-630'):
807 gpu = ['8086', '5912']
Brian Sheedyf9387db7b2019-08-05 19:26:10808 elif gpu.startswith('intel-uhd-630'):
809 gpu = ['8086', '3e92']
Kenneth Russell384a1732019-03-16 02:36:02810 else:
811 gpu = gpu.split('-')[0].split(':')
Kenneth Russell8a386d42018-06-02 09:48:01812 substitutions['gpu_vendor_id'] = gpu[0]
813 substitutions['gpu_device_id'] = gpu[1]
814 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
815
816 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56817 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01818 # These are all just specializations of isolated script tests with
819 # a bunch of boilerplate command line arguments added.
820
821 # The step name must end in 'test' or 'tests' in order for the
822 # results to automatically show up on the flakiness dashboard.
823 # (At least, this was true some time ago.) Continue to use this
824 # naming convention for the time being to minimize changes.
825 step_name = test_config.get('name', test_name)
826 if not (step_name.endswith('test') or step_name.endswith('tests')):
827 step_name = '%s_tests' % step_name
828 result = self.generate_isolated_script_test(
829 waterfall, tester_name, tester_config, step_name, test_config)
830 if not result:
831 return None
Chong Gub75754b32020-03-13 16:39:20832 result['isolate_name'] = test_config.get(
833 'isolate_name', 'telemetry_gpu_integration_test')
Chan Liab7d8dd82020-04-24 23:42:19834
Chan Lia3ad1502020-04-28 05:32:11835 # Populate test_id_prefix.
Chan Liab7d8dd82020-04-24 23:42:19836 gn_entry = (
837 self.gn_isolate_map.get(result['isolate_name']) or
838 self.gn_isolate_map.get('telemetry_gpu_integration_test'))
Chan Lia3ad1502020-04-28 05:32:11839 result['test_id_prefix'] = 'ninja:%s/%s/' % (gn_entry['label'], step_name)
Chan Liab7d8dd82020-04-24 23:42:19840
Kenneth Russell8a386d42018-06-02 09:48:01841 args = result.get('args', [])
842 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14843
844 # These tests upload and download results from cloud storage and therefore
845 # aren't idempotent yet. https://crbug.com/549140.
846 result['swarming']['idempotent'] = False
847
Kenneth Russell44910c32018-12-03 23:35:11848 # The GPU tests act much like integration tests for the entire browser, and
849 # tend to uncover flakiness bugs more readily than other test suites. In
850 # order to surface any flakiness more readily to the developer of the CL
851 # which is introducing it, we disable retries with patch on the commit
852 # queue.
853 result['should_retry_with_patch'] = False
854
Bo Liu555a0f92019-03-29 12:11:56855 browser = ('android-webview-instrumentation'
856 if is_android_webview else tester_config['browser_config'])
Kenneth Russell8a386d42018-06-02 09:48:01857 args = [
Bo Liu555a0f92019-03-29 12:11:56858 test_to_run,
859 '--show-stdout',
860 '--browser=%s' % browser,
861 # --passthrough displays more of the logging in Telemetry when
862 # run via typ, in particular some of the warnings about tests
863 # being expected to fail, but passing.
864 '--passthrough',
865 '-v',
866 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
Kenneth Russell8a386d42018-06-02 09:48:01867 ] + args
868 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14869 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01870 return result
871
Kenneth Russelleb60cbd22017-12-05 07:54:28872 def get_test_generator_map(self):
873 return {
Bo Liu555a0f92019-03-29 12:11:56874 'android_webview_gpu_telemetry_tests':
875 GPUTelemetryTestGenerator(self, is_android_webview=True),
876 'cts_tests':
877 CTSGenerator(self),
878 'gpu_telemetry_tests':
879 GPUTelemetryTestGenerator(self),
880 'gtest_tests':
881 GTestGenerator(self),
882 'instrumentation_tests':
883 InstrumentationTestGenerator(self),
884 'isolated_scripts':
885 IsolatedScriptTestGenerator(self),
886 'junit_tests':
887 JUnitGenerator(self),
888 'scripts':
889 ScriptGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28890 }
891
Kenneth Russell8a386d42018-06-02 09:48:01892 def get_test_type_remapper(self):
893 return {
894 # These are a specialization of isolated_scripts with a bunch of
895 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:56896 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:01897 'gpu_telemetry_tests': 'isolated_scripts',
898 }
899
Jeff Yoon67c3e832020-02-08 07:39:38900 def check_composition_type_test_suites(self, test_type,
901 additional_validators=None):
902 """Pre-pass to catch errors reliabily for compound/matrix suites"""
903 validators = [check_compound_references,
904 check_basic_references,
905 check_conflicting_definitions]
906 if additional_validators:
907 validators += additional_validators
908
909 target_suites = self.test_suites.get(test_type, {})
910 other_test_type = ('compound_suites'
911 if test_type == 'matrix_compound_suites'
912 else 'matrix_compound_suites')
913 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:01914 basic_suites = self.test_suites.get('basic_suites', {})
915
Jeff Yoon67c3e832020-02-08 07:39:38916 for suite, suite_def in target_suites.iteritems():
Jeff Yoon8154e582019-12-03 23:30:01917 if suite in basic_suites:
918 raise BBGenErr('%s names may not duplicate basic test suite names '
919 '(error found while processsing %s)'
920 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:01921
Jeff Yoon67c3e832020-02-08 07:39:38922 seen_tests = {}
923 for sub_suite in suite_def:
924 for validator in validators:
925 validator(
926 basic_suites=basic_suites,
927 other_test_suites=other_suites,
928 seen_tests=seen_tests,
929 sub_suite=sub_suite,
930 suite=suite,
931 suite_def=suite_def,
932 target_test_suites=target_suites,
933 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:05934 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:38935 )
Kenneth Russelleb60cbd22017-12-05 07:54:28936
Stephen Martinis54d64ad2018-09-21 22:16:20937 def flatten_test_suites(self):
938 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:01939 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
940 for category in test_types:
941 for name, value in self.test_suites.get(category, {}).iteritems():
942 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:20943 self.test_suites = new_test_suites
944
Chan Lia3ad1502020-04-28 05:32:11945 def resolve_test_id_prefixes(self):
Nodir Turakulovfce34292019-12-18 17:05:41946 for suite in self.test_suites['basic_suites'].itervalues():
947 for key, test in suite.iteritems():
948 if not isinstance(test, dict):
949 # Some test definitions are just strings, such as CTS.
950 # Skip them.
951 continue
952
953 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
954 # https://source.chromium.org/chromium/chromium/tools/build/+/master:scripts/slave/recipe_modules/chromium_tests/generators.py;l=89;drc=14c062ba0eb418d3c4623dde41a753241b9df06b
955 # TODO(crbug.com/1035124): clean this up.
956 isolate_name = test.get('test') or test.get('isolate_name') or key
957 gn_entry = self.gn_isolate_map.get(isolate_name)
958 if gn_entry:
Corentin Wallez55b8e772020-04-24 17:39:28959 label = gn_entry['label']
960
961 if label.count(':') != 1:
962 raise BBGenErr(
963 'Malformed GN label "%s" in gn_isolate_map for key "%s",'
964 ' implicit names (like //f/b meaning //f/b:b) are disallowed.' %
965 (label, isolate_name))
966 if label.split(':')[1] != isolate_name:
967 raise BBGenErr(
968 'gn_isolate_map key name "%s" doesn\'t match GN target name in'
969 ' label "%s" see http://crbug.com/1071091 for details.' %
970 (isolate_name, label))
971
Chan Lia3ad1502020-04-28 05:32:11972 test['test_id_prefix'] = 'ninja:%s/' % label
Nodir Turakulovfce34292019-12-18 17:05:41973 else: # pragma: no cover
974 # Some tests do not have an entry gn_isolate_map.pyl, such as
975 # telemetry tests.
976 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
977 pass
978
Kenneth Russelleb60cbd22017-12-05 07:54:28979 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:01980 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:20981
Jeff Yoon8154e582019-12-03 23:30:01982 compound_suites = self.test_suites.get('compound_suites', {})
983 # check_composition_type_test_suites() checks that all basic suites
984 # referenced by compound suites exist.
985 basic_suites = self.test_suites.get('basic_suites')
986
987 for name, value in compound_suites.iteritems():
988 # Resolve this to a dictionary.
989 full_suite = {}
990 for entry in value:
991 suite = basic_suites[entry]
992 full_suite.update(suite)
993 compound_suites[name] = full_suite
994
Jeff Yoon67c3e832020-02-08 07:39:38995 def resolve_variants(self, basic_test_definition, variants):
996 """ Merge variant-defined configurations to each test case definition in a
997 test suite.
998
999 The output maps a unique test name to an array of configurations because
1000 there may exist more than one definition for a test name using variants. The
1001 test name is referenced while mapping machines to test suites, so unpacking
1002 the array is done by the generators.
1003
1004 Args:
1005 basic_test_definition: a {} defined test suite in the format
1006 test_name:test_config
1007 variants: an [] of {} defining configurations to be applied to each test
1008 case in the basic test_definition
1009
1010 Return:
1011 a {} of test_name:[{}], where each {} is a merged configuration
1012 """
1013
1014 # Each test in a basic test suite will have a definition per variant.
1015 test_suite = {}
1016 for test_name, test_config in basic_test_definition.iteritems():
1017 definitions = []
1018 for variant in variants:
Jeff Yoonda581c32020-03-06 03:56:051019 # Unpack the variant from variants.pyl if it's string based.
1020 if isinstance(variant, str):
1021 variant = self.variants[variant]
1022
Jeff Yoon67c3e832020-02-08 07:39:381023 # Clone a copy of test_config so that we can have a uniquely updated
1024 # version of it per variant
1025 cloned_config = copy.deepcopy(test_config)
1026 # The variant definition needs to be re-used for each test, so we'll
1027 # create a clone and work with it as well.
1028 cloned_variant = copy.deepcopy(variant)
1029
1030 cloned_config['args'] = (cloned_config.get('args', []) +
1031 cloned_variant.get('args', []))
1032 cloned_config['mixins'] = (cloned_config.get('mixins', []) +
1033 cloned_variant.get('mixins', []))
1034
1035 basic_swarming_def = cloned_config.get('swarming', {})
1036 variant_swarming_def = cloned_variant.get('swarming', {})
1037 if basic_swarming_def and variant_swarming_def:
1038 if ('dimension_sets' in basic_swarming_def and
1039 'dimension_sets' in variant_swarming_def):
1040 # Retain swarming dimension set merge behavior when both variant and
1041 # the basic test configuration both define it
1042 self.dictionary_merge(basic_swarming_def, variant_swarming_def)
1043 # Remove dimension_sets from the variant definition, so that it does
1044 # not replace what's been done by dictionary_merge in the update
1045 # call below.
1046 del variant_swarming_def['dimension_sets']
1047
1048 # Update the swarming definition with whatever is defined for swarming
1049 # by the variant.
1050 basic_swarming_def.update(variant_swarming_def)
1051 cloned_config['swarming'] = basic_swarming_def
1052
1053 # The identifier is used to make the name of the test unique.
1054 # Generators in the recipe uniquely identify a test by it's name, so we
1055 # don't want to have the same name for each variant.
1056 cloned_config['name'] = '{}_{}'.format(test_name,
1057 cloned_variant['identifier'])
Jeff Yoon67c3e832020-02-08 07:39:381058 definitions.append(cloned_config)
1059 test_suite[test_name] = definitions
1060 return test_suite
1061
Jeff Yoon8154e582019-12-03 23:30:011062 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381063 self.check_composition_type_test_suites('matrix_compound_suites',
1064 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011065
1066 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381067 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011068 # referenced by matrix suites exist.
1069 basic_suites = self.test_suites.get('basic_suites')
1070
Jeff Yoon67c3e832020-02-08 07:39:381071 for test_name, matrix_config in matrix_compound_suites.iteritems():
Jeff Yoon8154e582019-12-03 23:30:011072 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381073
1074 for test_suite, mtx_test_suite_config in matrix_config.iteritems():
1075 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1076
1077 if 'variants' in mtx_test_suite_config:
1078 result = self.resolve_variants(basic_test_def,
1079 mtx_test_suite_config['variants'])
1080 full_suite.update(result)
1081 matrix_compound_suites[test_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281082
1083 def link_waterfalls_to_test_suites(self):
1084 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431085 for tester_name, tester in waterfall['machines'].iteritems():
1086 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281087 if not value in self.test_suites:
1088 # Hard / impossible to cover this in the unit test.
1089 raise self.unknown_test_suite(
1090 value, tester_name, waterfall['name']) # pragma: no cover
1091 tester['test_suites'][suite] = self.test_suites[value]
1092
1093 def load_configuration_files(self):
1094 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
1095 self.test_suites = self.load_pyl_file('test_suites.pyl')
1096 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:011097 self.mixins = self.load_pyl_file('mixins.pyl')
Nodir Turakulovfce34292019-12-18 17:05:411098 self.gn_isolate_map = self.load_pyl_file('gn_isolate_map.pyl')
Jeff Yoonda581c32020-03-06 03:56:051099 self.variants = self.load_pyl_file('variants.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:281100
1101 def resolve_configuration_files(self):
Chan Lia3ad1502020-04-28 05:32:111102 self.resolve_test_id_prefixes()
Kenneth Russelleb60cbd22017-12-05 07:54:281103 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011104 self.resolve_matrix_compound_test_suites()
1105 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281106 self.link_waterfalls_to_test_suites()
1107
Nico Weberd18b8962018-05-16 19:39:381108 def unknown_bot(self, bot_name, waterfall_name):
1109 return BBGenErr(
1110 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1111
Kenneth Russelleb60cbd22017-12-05 07:54:281112 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1113 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381114 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281115 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1116
1117 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1118 return BBGenErr(
1119 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1120 ' on waterfall ' + waterfall_name)
1121
Stephen Martinisb72f6d22018-10-04 23:29:011122 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071123 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321124
1125 Checks in the waterfall, builder, and test objects for mixins.
1126 """
1127 def valid_mixin(mixin_name):
1128 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011129 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321130 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381131
Stephen Martinisb6a50492018-09-12 23:59:321132 def must_be_list(mixins, typ, name):
1133 """Asserts that given mixins are a list."""
1134 if not isinstance(mixins, list):
1135 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1136
Brian Sheedy7658c982020-01-08 02:27:581137 test_name = test.get('name')
1138 remove_mixins = set()
1139 if 'remove_mixins' in builder:
1140 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1141 for rm in builder['remove_mixins']:
1142 valid_mixin(rm)
1143 remove_mixins.add(rm)
1144 if 'remove_mixins' in test:
1145 must_be_list(test['remove_mixins'], 'test', test_name)
1146 for rm in test['remove_mixins']:
1147 valid_mixin(rm)
1148 remove_mixins.add(rm)
1149 del test['remove_mixins']
1150
Stephen Martinisb72f6d22018-10-04 23:29:011151 if 'mixins' in waterfall:
1152 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1153 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581154 if mixin in remove_mixins:
1155 continue
Stephen Martinisb6a50492018-09-12 23:59:321156 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011157 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321158
Stephen Martinisb72f6d22018-10-04 23:29:011159 if 'mixins' in builder:
1160 must_be_list(builder['mixins'], 'builder', builder_name)
1161 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581162 if mixin in remove_mixins:
1163 continue
Stephen Martinisb6a50492018-09-12 23:59:321164 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011165 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321166
Stephen Martinisb72f6d22018-10-04 23:29:011167 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071168 return test
1169
Stephen Martinis2a0667022018-09-25 22:31:141170 if not test_name:
1171 test_name = test.get('test')
1172 if not test_name: # pragma: no cover
1173 # Not the best name, but we should say something.
1174 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:011175 must_be_list(test['mixins'], 'test', test_name)
1176 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581177 # We don't bother checking if the given mixin is in remove_mixins here
1178 # since this is already the lowest level, so if a mixin is added here that
1179 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071180 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011181 test = self.apply_mixin(self.mixins[mixin], test)
Jeff Yoon67c3e832020-02-08 07:39:381182 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071183 return test
Stephen Martinisb6a50492018-09-12 23:59:321184
Stephen Martinisb72f6d22018-10-04 23:29:011185 def apply_mixin(self, mixin, test):
1186 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321187
Stephen Martinis0382bc12018-09-17 22:29:071188 Mixins will not override an existing key. This is to ensure exceptions can
1189 override a setting a mixin applies.
1190
Stephen Martinisb72f6d22018-10-04 23:29:011191 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:321192 'dimension_sets', which is how normal test suites specify their dimensions,
1193 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
1194 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011195
Stephen Martinisb6a50492018-09-12 23:59:321196 """
1197 new_test = copy.deepcopy(test)
1198 mixin = copy.deepcopy(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011199 if 'swarming' in mixin:
1200 swarming_mixin = mixin['swarming']
1201 new_test.setdefault('swarming', {})
1202 if 'dimensions' in swarming_mixin:
1203 new_test['swarming'].setdefault('dimension_sets', [{}])
1204 for dimension_set in new_test['swarming']['dimension_sets']:
1205 dimension_set.update(swarming_mixin['dimensions'])
1206 del swarming_mixin['dimensions']
Stephen Martinisb72f6d22018-10-04 23:29:011207 # python dict update doesn't do recursion at all. Just hard code the
1208 # nested update we need (mixin['swarming'] shouldn't clobber
1209 # test['swarming'], but should update it).
1210 new_test['swarming'].update(swarming_mixin)
1211 del mixin['swarming']
1212
Wezc0e835b702018-10-30 00:38:411213 if '$mixin_append' in mixin:
1214 # Values specified under $mixin_append should be appended to existing
1215 # lists, rather than replacing them.
1216 mixin_append = mixin['$mixin_append']
1217 for key in mixin_append:
1218 new_test.setdefault(key, [])
1219 if not isinstance(mixin_append[key], list):
1220 raise BBGenErr(
1221 'Key "' + key + '" in $mixin_append must be a list.')
1222 if not isinstance(new_test[key], list):
1223 raise BBGenErr(
1224 'Cannot apply $mixin_append to non-list "' + key + '".')
1225 new_test[key].extend(mixin_append[key])
1226 if 'args' in mixin_append:
1227 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1228 del mixin['$mixin_append']
1229
Stephen Martinisb72f6d22018-10-04 23:29:011230 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321231 return new_test
1232
Greg Gutermanf60eb052020-03-12 17:40:011233 def generate_output_tests(self, waterfall):
1234 """Generates the tests for a waterfall.
1235
1236 Args:
1237 waterfall: a dictionary parsed from a master pyl file
1238 Returns:
1239 A dictionary mapping builders to test specs
1240 """
1241 return {
1242 name: self.get_tests_for_config(waterfall, name, config)
1243 for name, config
1244 in waterfall['machines'].iteritems()
1245 }
1246
1247 def get_tests_for_config(self, waterfall, name, config):
Greg Guterman5c6144152020-02-28 20:08:531248 generator_map = self.get_test_generator_map()
1249 test_type_remapper = self.get_test_type_remapper()
Kenneth Russelleb60cbd22017-12-05 07:54:281250
Greg Gutermanf60eb052020-03-12 17:40:011251 tests = {}
1252 # Copy only well-understood entries in the machine's configuration
1253 # verbatim into the generated JSON.
1254 if 'additional_compile_targets' in config:
1255 tests['additional_compile_targets'] = config[
1256 'additional_compile_targets']
1257 for test_type, input_tests in config.get('test_suites', {}).iteritems():
1258 if test_type not in generator_map:
1259 raise self.unknown_test_suite_type(
1260 test_type, name, waterfall['name']) # pragma: no cover
1261 test_generator = generator_map[test_type]
1262 # Let multiple kinds of generators generate the same kinds
1263 # of tests. For example, gpu_telemetry_tests are a
1264 # specialization of isolated_scripts.
1265 new_tests = test_generator.generate(
1266 waterfall, name, config, input_tests)
1267 remapped_test_type = test_type_remapper.get(test_type, test_type)
1268 tests[remapped_test_type] = test_generator.sort(
1269 tests.get(remapped_test_type, []) + new_tests)
1270
1271 return tests
1272
1273 def jsonify(self, all_tests):
1274 return json.dumps(
1275 all_tests, indent=2, separators=(',', ': '),
1276 sort_keys=True) + '\n'
1277
1278 def generate_outputs(self): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281279 self.load_configuration_files()
1280 self.resolve_configuration_files()
1281 filters = self.args.waterfall_filters
Greg Gutermanf60eb052020-03-12 17:40:011282 result = collections.defaultdict(dict)
1283
1284 required_fields = ('project', 'bucket', 'name')
1285 for waterfall in self.waterfalls:
1286 for field in required_fields:
1287 # Verify required fields
1288 if field not in waterfall:
1289 raise BBGenErr("Waterfall %s has no %s" % (waterfall['name'], field))
1290
1291 # Handle filter flag, if specified
1292 if filters and waterfall['name'] not in filters:
1293 continue
1294
1295 # Join config files and hardcoded values together
1296 all_tests = self.generate_output_tests(waterfall)
1297 result[waterfall['name']] = all_tests
1298
1299 # Deduce per-bucket mappings
1300 # This will be the standard after masternames are gone
1301 bucket_filename = waterfall['project'] + '.' + waterfall['bucket']
1302 for buildername in waterfall['machines'].keys():
1303 result[bucket_filename][buildername] = all_tests[buildername]
1304
1305 # Add do not edit warning
1306 for tests in result.values():
1307 tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1308 tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1309
1310 return result
1311
1312 def write_json_result(self, result): # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281313 suffix = '.json'
1314 if self.args.new_files:
1315 suffix = '.new' + suffix
Greg Gutermanf60eb052020-03-12 17:40:011316
1317 for filename, contents in result.items():
1318 jsonstr = self.jsonify(contents)
1319 self.write_file(self.pyl_file_path(filename + suffix), jsonstr)
Kenneth Russelleb60cbd22017-12-05 07:54:281320
Nico Weberd18b8962018-05-16 19:39:381321 def get_valid_bot_names(self):
John Budorick699282e2019-02-13 01:27:331322 # Extract bot names from infra/config/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421323 # NOTE: This reference can cause issues; if a file changes there, the
1324 # presubmit here won't be run by default. A manually maintained list there
1325 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1326 # references to configs outside of this directory are added, please change
1327 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1328 # never ends up in an invalid state.
Garrett Beaty2a02de3c2020-05-15 13:57:351329 project_star = glob.glob(
1330 os.path.join(self.args.infra_config_dir, 'project.star'))
1331 if project_star:
1332 is_master_pattern = re.compile('is_master\s*=\s*(True|False)')
1333 for l in self.read_file(project_star[0]).splitlines():
1334 match = is_master_pattern.search(l)
1335 if match:
1336 if match.group(1) == 'False':
1337 return None
1338 break
Nico Weberd18b8962018-05-16 19:39:381339 bot_names = set()
Garrett Beatyd5ca75962020-05-07 16:58:311340 milo_configs = glob.glob(
1341 os.path.join(self.args.infra_config_dir, 'generated', 'luci-milo*.cfg'))
John Budorickc12abd12018-08-14 19:37:431342 for c in milo_configs:
1343 for l in self.read_file(c).splitlines():
1344 if (not 'name: "buildbucket/luci.chromium.' in l and
Garrett Beatyd5ca75962020-05-07 16:58:311345 not 'name: "buildbucket/luci.chrome.' in l):
John Budorickc12abd12018-08-14 19:37:431346 continue
1347 # l looks like
1348 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1349 # Extract win_chromium_dbg_ng part.
1350 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381351 return bot_names
1352
Ben Pastene9a010082019-09-25 20:41:371353 def get_builders_that_do_not_actually_exist(self):
Kenneth Russell8a386d42018-06-02 09:48:011354 # Some of the bots on the chromium.gpu.fyi waterfall in particular
1355 # are defined only to be mirrored into trybots, and don't actually
1356 # exist on any of the waterfalls or consoles.
1357 return [
Michael Spangeb07eba62019-05-14 22:22:581358 'GPU FYI Fuchsia Builder',
Yuly Novikoveb26b812019-07-26 02:08:191359 'ANGLE GPU Android Release (Nexus 5X)',
Jamie Madillda894ce2019-04-08 17:19:171360 'ANGLE GPU Linux Release (Intel HD 630)',
1361 'ANGLE GPU Linux Release (NVIDIA)',
1362 'ANGLE GPU Mac Release (Intel)',
1363 'ANGLE GPU Mac Retina Release (AMD)',
1364 'ANGLE GPU Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491365 'ANGLE GPU Win10 x64 Release (Intel HD 630)',
1366 'ANGLE GPU Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011367 'Optional Android Release (Nexus 5X)',
1368 'Optional Linux Release (Intel HD 630)',
1369 'Optional Linux Release (NVIDIA)',
1370 'Optional Mac Release (Intel)',
1371 'Optional Mac Retina Release (AMD)',
1372 'Optional Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491373 'Optional Win10 x64 Release (Intel HD 630)',
1374 'Optional Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011375 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:081376 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:291377 'linux-blink-rel-dummy',
1378 'mac10.10-blink-rel-dummy',
1379 'mac10.11-blink-rel-dummy',
1380 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:201381 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291382 'mac10.13-blink-rel-dummy',
John Chenad978322019-12-16 18:07:211383 'mac10.14-blink-rel-dummy',
Ilia Samsonov7efe05e2020-05-07 19:00:461384 'mac10.15-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291385 'win7-blink-rel-dummy',
1386 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:081387 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:331388 'WebKit Linux composite_after_paint Dummy Builder',
Scott Violet744e04662019-08-19 23:51:531389 'WebKit Linux layout_ng_disabled Builder',
Stephen Martinis769b25112018-08-30 18:52:061390 # chromium, due to https://crbug.com/878915
1391 'win-dbg',
1392 'win32-dbg',
Stephen Martinis47d77132019-04-24 23:51:331393 'win-archive-dbg',
1394 'win32-archive-dbg',
Sajjad Mirza2924a012019-12-20 03:46:541395 # TODO(crbug.com/1033753) Delete these when coverage is enabled by default
1396 # on Windows tryjobs.
1397 'GPU Win x64 Builder Code Coverage',
1398 'Win x64 Builder Code Coverage',
1399 'Win10 Tests x64 Code Coverage',
1400 'Win10 x64 Release (NVIDIA) Code Coverage',
Sajjad Mirzafa15665e2020-02-10 23:41:041401 # TODO(crbug.com/1024915) Delete these when coverage is enabled by default
1402 # on Mac OS tryjobs.
1403 'Mac Builder Code Coverage',
1404 'Mac10.13 Tests Code Coverage',
1405 'GPU Mac Builder Code Coverage',
1406 'Mac Release (Intel) Code Coverage',
1407 'Mac Retina Release (AMD) Code Coverage',
Kenneth Russell8a386d42018-06-02 09:48:011408 ]
1409
Ben Pastene9a010082019-09-25 20:41:371410 def get_internal_waterfalls(self):
1411 # Similar to get_builders_that_do_not_actually_exist above, but for
1412 # waterfalls defined in internal configs.
Jeff Yoon8acfdce2020-04-20 22:38:071413 return ['chrome', 'chrome.pgo']
Ben Pastene9a010082019-09-25 20:41:371414
Stephen Martinisf83893722018-09-19 00:02:181415 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201416 self.check_input_files_sorting(verbose)
1417
Kenneth Russelleb60cbd22017-12-05 07:54:281418 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011419 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381420 self.check_composition_type_test_suites('matrix_compound_suites',
1421 [check_matrix_identifier])
Chan Lia3ad1502020-04-28 05:32:111422 self.resolve_test_id_prefixes()
Stephen Martinis54d64ad2018-09-21 22:16:201423 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381424
1425 # All bots should exist.
1426 bot_names = self.get_valid_bot_names()
Ben Pastene9a010082019-09-25 20:41:371427 builders_that_dont_exist = self.get_builders_that_do_not_actually_exist()
Garrett Beaty2a02de3c2020-05-15 13:57:351428 if bot_names is not None:
1429 internal_waterfalls = self.get_internal_waterfalls()
1430 for waterfall in self.waterfalls:
1431 # TODO(crbug.com/991417): Remove the need for this exception.
1432 if waterfall['name'] in internal_waterfalls:
Kenneth Russell8a386d42018-06-02 09:48:011433 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351434 for bot_name in waterfall['machines']:
1435 if bot_name in builders_that_dont_exist:
Kenneth Russell78fd8702018-05-17 01:15:521436 continue # pragma: no cover
Garrett Beaty2a02de3c2020-05-15 13:57:351437 if bot_name not in bot_names:
1438 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
1439 # TODO(thakis): Remove this once these bots move to luci.
1440 continue # pragma: no cover
1441 if waterfall['name'] in ['tryserver.webrtc',
1442 'webrtc.chromium.fyi.experimental']:
1443 # These waterfalls have their bot configs in a different repo.
1444 # so we don't know about their bot names.
1445 continue # pragma: no cover
1446 if waterfall['name'] in ['client.devtools-frontend.integration',
1447 'tryserver.devtools-frontend',
1448 'chromium.devtools-frontend']:
1449 continue # pragma: no cover
1450 raise self.unknown_bot(bot_name, waterfall['name'])
Nico Weberd18b8962018-05-16 19:39:381451
Kenneth Russelleb60cbd22017-12-05 07:54:281452 # All test suites must be referenced.
1453 suites_seen = set()
1454 generator_map = self.get_test_generator_map()
1455 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431456 for bot_name, tester in waterfall['machines'].iteritems():
1457 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281458 if suite_type not in generator_map:
1459 raise self.unknown_test_suite_type(suite_type, bot_name,
1460 waterfall['name'])
1461 if suite not in self.test_suites:
1462 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1463 suites_seen.add(suite)
1464 # Since we didn't resolve the configuration files, this set
1465 # includes both composition test suites and regular ones.
1466 resolved_suites = set()
1467 for suite_name in suites_seen:
1468 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011469 for sub_suite in suite:
1470 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281471 resolved_suites.add(suite_name)
1472 # At this point, every key in test_suites.pyl should be referenced.
1473 missing_suites = set(self.test_suites.keys()) - resolved_suites
1474 if missing_suites:
1475 raise BBGenErr('The following test suites were unreferenced by bots on '
1476 'the waterfalls: ' + str(missing_suites))
1477
1478 # All test suite exceptions must refer to bots on the waterfall.
1479 all_bots = set()
1480 missing_bots = set()
1481 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431482 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281483 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281484 # In order to disambiguate between bots with the same name on
1485 # different waterfalls, support has been added to various
1486 # exceptions for concatenating the waterfall name after the bot
1487 # name.
1488 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281489 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381490 removals = (exception.get('remove_from', []) +
1491 exception.get('remove_gtest_from', []) +
1492 exception.get('modifications', {}).keys())
1493 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281494 if removal not in all_bots:
1495 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411496
Ben Pastene9a010082019-09-25 20:41:371497 missing_bots = missing_bots - set(builders_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281498 if missing_bots:
1499 raise BBGenErr('The following nonexistent machines were referenced in '
1500 'the test suite exceptions: ' + str(missing_bots))
1501
Stephen Martinis0382bc12018-09-17 22:29:071502 # All mixins must be referenced
1503 seen_mixins = set()
1504 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011505 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071506 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011507 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071508 for suite in self.test_suites.values():
1509 if isinstance(suite, list):
1510 # Don't care about this, it's a composition, which shouldn't include a
1511 # swarming mixin.
1512 continue
1513
1514 for test in suite.values():
1515 if not isinstance(test, dict):
1516 # Some test suites have top level keys, which currently can't be
1517 # swarming mixin entries. Ignore them
1518 continue
1519
Stephen Martinisb72f6d22018-10-04 23:29:011520 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071521
Stephen Martinisb72f6d22018-10-04 23:29:011522 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071523 if missing_mixins:
1524 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1525 ' referenced in a waterfall, machine, or test suite.' % (
1526 str(missing_mixins)))
1527
Jeff Yoonda581c32020-03-06 03:56:051528 # All variant references must be referenced
1529 seen_variants = set()
1530 for suite in self.test_suites.values():
1531 if isinstance(suite, list):
1532 continue
1533
1534 for test in suite.values():
1535 if isinstance(test, dict):
1536 for variant in test.get('variants', []):
1537 if isinstance(variant, str):
1538 seen_variants.add(variant)
1539
1540 missing_variants = set(self.variants.keys()) - seen_variants
1541 if missing_variants:
1542 raise BBGenErr('The following variants were unreferenced: %s. They must '
1543 'be referenced in a matrix test suite under the variants '
1544 'key.' % str(missing_variants))
1545
Stephen Martinis54d64ad2018-09-21 22:16:201546
1547 def type_assert(self, node, typ, filename, verbose=False):
1548 """Asserts that the Python AST node |node| is of type |typ|.
1549
1550 If verbose is set, it prints out some helpful context lines, showing where
1551 exactly the error occurred in the file.
1552 """
1553 if not isinstance(node, typ):
1554 if verbose:
1555 lines = [""] + self.read_file(filename).splitlines()
1556
1557 context = 2
1558 lines_start = max(node.lineno - context, 0)
1559 # Add one to include the last line
1560 lines_end = min(node.lineno + context, len(lines)) + 1
1561 lines = (
1562 ['== %s ==\n' % filename] +
1563 ["<snip>\n"] +
1564 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1565 lines[lines_start:lines_start + context])] +
1566 ['-' * 80 + '\n'] +
1567 ['%d %s' % (node.lineno, lines[node.lineno])] +
1568 ['-' * (node.col_offset + 3) + '^' + '-' * (
1569 80 - node.col_offset - 4) + '\n'] +
1570 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1571 lines[node.lineno + 1:lines_end])] +
1572 ["<snip>\n"]
1573 )
1574 # Print out a useful message when a type assertion fails.
1575 for l in lines:
1576 self.print_line(l.strip())
1577
1578 node_dumped = ast.dump(node, annotate_fields=False)
1579 # If the node is huge, truncate it so everything fits in a terminal
1580 # window.
1581 if len(node_dumped) > 60: # pragma: no cover
1582 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1583 raise BBGenErr(
1584 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1585 ' be %s, is %s' % (
1586 filename, node_dumped,
1587 node.lineno, typ, type(node)))
1588
Stephen Martinis5bef0fc2020-01-06 22:47:531589 def check_ast_list_formatted(self, keys, filename, verbose,
Stephen Martinis1384ff92020-01-07 19:52:151590 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531591 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201592
Stephen Martinis5bef0fc2020-01-06 22:47:531593 Currently only checks to ensure they're correctly sorted, and that there
1594 are no duplicates.
1595
1596 Args:
1597 keys: An python list of AST nodes.
1598
1599 It's a list of AST nodes instead of a list of strings because
1600 when verbose is set, it tries to print out context of where the
1601 diffs are in the file.
1602 filename: The name of the file this node is from.
1603 verbose: If set, print out diff information about how the keys are
1604 incorrectly formatted.
1605 check_sorting: If true, checks if the list is sorted.
1606 Returns:
1607 If the keys are correctly formatted.
1608 """
1609 if not keys:
1610 return True
1611
1612 assert isinstance(keys[0], ast.Str)
1613
1614 keys_strs = [k.s for k in keys]
1615 # Keys to diff against. Used below.
1616 keys_to_diff_against = None
1617 # If the list is properly formatted.
1618 list_formatted = True
1619
1620 # Duplicates are always bad.
1621 if len(set(keys_strs)) != len(keys_strs):
1622 list_formatted = False
1623 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1624
1625 if check_sorting and sorted(keys_strs) != keys_strs:
1626 list_formatted = False
1627 if list_formatted:
1628 return True
1629
1630 if verbose:
1631 line_num = keys[0].lineno
1632 keys = [k.s for k in keys]
1633 if check_sorting:
1634 # If we have duplicates, sorting this will take care of it anyways.
1635 keys_to_diff_against = sorted(set(keys))
1636 # else, keys_to_diff_against is set above already
1637
1638 self.print_line('=' * 80)
1639 self.print_line('(First line of keys is %s)' % line_num)
1640 for line in difflib.context_diff(
1641 keys, keys_to_diff_against,
1642 fromfile='current (%r)' % filename, tofile='sorted', lineterm=''):
1643 self.print_line(line)
1644 self.print_line('=' * 80)
1645
1646 return False
1647
Stephen Martinis1384ff92020-01-07 19:52:151648 def check_ast_dict_formatted(self, node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531649 """Checks if an ast dictionary's keys are correctly formatted.
1650
1651 Just a simple wrapper around check_ast_list_formatted.
1652 Args:
1653 node: An AST node. Assumed to be a dictionary.
1654 filename: The name of the file this node is from.
1655 verbose: If set, print out diff information about how the keys are
1656 incorrectly formatted.
1657 check_sorting: If true, checks if the list is sorted.
1658 Returns:
1659 If the dictionary is correctly formatted.
1660 """
Stephen Martinis54d64ad2018-09-21 22:16:201661 keys = []
1662 # The keys of this dict are ordered as ordered in the file; normal python
1663 # dictionary keys are given an arbitrary order, but since we parsed the
1664 # file itself, the order as given in the file is preserved.
1665 for key in node.keys:
1666 self.type_assert(key, ast.Str, filename, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531667 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201668
Stephen Martinis1384ff92020-01-07 19:52:151669 return self.check_ast_list_formatted(keys, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181670
1671 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201672 # TODO(https://crbug.com/886993): Add the ability for this script to
1673 # actually format the files, rather than just complain if they're
1674 # incorrectly formatted.
1675 bad_files = set()
Stephen Martinis5bef0fc2020-01-06 22:47:531676 def parse_file(filename):
1677 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201678
Stephen Martinis5bef0fc2020-01-06 22:47:531679 Returns an AST node representing the value in the pyl file."""
Stephen Martinisf83893722018-09-19 00:02:181680 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1681
Stephen Martinisf83893722018-09-19 00:02:181682 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201683 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181684 module = parsed.body
1685
1686 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201687 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181688 if len(module) != 1: # pragma: no cover
1689 raise BBGenErr('Invalid .pyl file %s' % filename)
1690 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201691 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181692
Stephen Martinis5bef0fc2020-01-06 22:47:531693 return expr.value
1694
1695 # Handle this separately
1696 filename = 'waterfalls.pyl'
1697 value = parse_file(filename)
1698 # Value should be a list.
1699 self.type_assert(value, ast.List, filename, verbose)
1700
1701 keys = []
1702 for val in value.elts:
1703 self.type_assert(val, ast.Dict, filename, verbose)
1704 waterfall_name = None
1705 for key, val in zip(val.keys, val.values):
1706 self.type_assert(key, ast.Str, filename, verbose)
1707 if key.s == 'machines':
1708 if not self.check_ast_dict_formatted(val, filename, verbose):
1709 bad_files.add(filename)
1710
1711 if key.s == "name":
1712 self.type_assert(val, ast.Str, filename, verbose)
1713 waterfall_name = val
1714 assert waterfall_name
1715 keys.append(waterfall_name)
1716
Stephen Martinis1384ff92020-01-07 19:52:151717 if not self.check_ast_list_formatted(keys, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531718 bad_files.add(filename)
1719
1720 for filename in (
1721 'mixins.pyl',
1722 'test_suites.pyl',
1723 'test_suite_exceptions.pyl',
1724 ):
1725 value = parse_file(filename)
Stephen Martinisf83893722018-09-19 00:02:181726 # Value should be a dictionary.
Stephen Martinis54d64ad2018-09-21 22:16:201727 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181728
Stephen Martinis5bef0fc2020-01-06 22:47:531729 if not self.check_ast_dict_formatted(
1730 value, filename, verbose):
1731 bad_files.add(filename)
1732
Stephen Martinis54d64ad2018-09-21 22:16:201733 if filename == 'test_suites.pyl':
Jeff Yoon8154e582019-12-03 23:30:011734 expected_keys = ['basic_suites',
1735 'compound_suites',
1736 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201737 actual_keys = [node.s for node in value.keys]
1738 assert all(key in expected_keys for key in actual_keys), (
1739 'Invalid %r file; expected keys %r, got %r' % (
1740 filename, expected_keys, actual_keys))
1741 suite_dicts = [node for node in value.values]
1742 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011743 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201744 for suite_group in suite_dicts:
Stephen Martinis5bef0fc2020-01-06 22:47:531745 if not self.check_ast_dict_formatted(
Stephen Martinis54d64ad2018-09-21 22:16:201746 suite_group, filename, verbose):
1747 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181748
Stephen Martinis5bef0fc2020-01-06 22:47:531749 for key, suite in zip(value.keys, value.values):
1750 # The compound suites are checked in
1751 # 'check_composition_type_test_suites()'
1752 if key.s == 'basic_suites':
1753 for group in suite.values:
Stephen Martinis1384ff92020-01-07 19:52:151754 if not self.check_ast_dict_formatted(group, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531755 bad_files.add(filename)
1756 break
Stephen Martinis54d64ad2018-09-21 22:16:201757
Stephen Martinis5bef0fc2020-01-06 22:47:531758 elif filename == 'test_suite_exceptions.pyl':
1759 # Check the values for each test.
1760 for test in value.values:
1761 for kind, node in zip(test.keys, test.values):
1762 if isinstance(node, ast.Dict):
Stephen Martinis1384ff92020-01-07 19:52:151763 if not self.check_ast_dict_formatted(node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531764 bad_files.add(filename)
1765 elif kind.s == 'remove_from':
1766 # Don't care about sorting; these are usually grouped, since the
1767 # same bug can affect multiple builders. Do want to make sure
1768 # there aren't duplicates.
1769 if not self.check_ast_list_formatted(node.elts, filename, verbose,
1770 check_sorting=False):
1771 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181772
1773 if bad_files:
1774 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201775 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531776 'unsorted, or have duplicates. Re-run this with --verbose to see '
1777 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181778
Kenneth Russelleb60cbd22017-12-05 07:54:281779 def check_output_file_consistency(self, verbose=False):
1780 self.load_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011781 # All waterfalls/bucket .json files must have been written
1782 # by this script already.
Kenneth Russelleb60cbd22017-12-05 07:54:281783 self.resolve_configuration_files()
Greg Gutermanf60eb052020-03-12 17:40:011784 ungenerated_files = set()
1785 for filename, expected_contents in self.generate_outputs().items():
1786 expected = self.jsonify(expected_contents)
1787 file_path = filename + '.json'
Zhiling Huangbe008172018-03-08 19:13:111788 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281789 if expected != current:
Greg Gutermanf60eb052020-03-12 17:40:011790 ungenerated_files.add(filename)
John Budorick826d5ed2017-12-28 19:27:321791 if verbose: # pragma: no cover
Greg Gutermanf60eb052020-03-12 17:40:011792 self.print_line('File ' + filename +
1793 '.json did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321794 'contents:')
1795 for line in difflib.unified_diff(
1796 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501797 current.splitlines(),
1798 fromfile='expected', tofile='current'):
1799 self.print_line(line)
Greg Gutermanf60eb052020-03-12 17:40:011800
1801 if ungenerated_files:
1802 raise BBGenErr(
1803 'The following files have not been properly '
1804 'autogenerated by generate_buildbot_json.py: ' +
1805 ', '.join([filename + '.json' for filename in ungenerated_files]))
Kenneth Russelleb60cbd22017-12-05 07:54:281806
1807 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501808 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281809 self.check_output_file_consistency(verbose) # pragma: no cover
1810
1811 def parse_args(self, argv): # pragma: no cover
Karen Qiane24b7ee2019-02-12 23:37:061812
1813 # RawTextHelpFormatter allows for styling of help statement
1814 parser = argparse.ArgumentParser(formatter_class=
1815 argparse.RawTextHelpFormatter)
1816
1817 group = parser.add_mutually_exclusive_group()
1818 group.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281819 '-c', '--check', action='store_true', help=
1820 'Do consistency checks of configuration and generated files and then '
1821 'exit. Used during presubmit. Causes the tool to not generate any files.')
Karen Qiane24b7ee2019-02-12 23:37:061822 group.add_argument(
1823 '--query', type=str, help=
1824 ("Returns raw JSON information of buildbots and tests.\n" +
1825 "Examples:\n" +
1826 " List all bots (all info):\n" +
1827 " --query bots\n\n" +
1828 " List all bots and only their associated tests:\n" +
1829 " --query bots/tests\n\n" +
1830 " List all information about 'bot1' " +
1831 "(make sure you have quotes):\n" +
1832 " --query bot/'bot1'\n\n" +
1833 " List tests running for 'bot1' (make sure you have quotes):\n" +
1834 " --query bot/'bot1'/tests\n\n" +
1835 " List all tests:\n" +
1836 " --query tests\n\n" +
1837 " List all tests and the bots running them:\n" +
1838 " --query tests/bots\n\n"+
1839 " List all tests that satisfy multiple parameters\n" +
1840 " (separation of parameters by '&' symbol):\n" +
1841 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
1842 " List all tests that run with a specific flag:\n" +
1843 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
1844 " List specific test (make sure you have quotes):\n"
1845 " --query test/'test1'\n\n"
1846 " List all bots running 'test1' " +
1847 "(make sure you have quotes):\n" +
1848 " --query test/'test1'/bots" ))
Kenneth Russelleb60cbd22017-12-05 07:54:281849 parser.add_argument(
1850 '-n', '--new-files', action='store_true', help=
1851 'Write output files as .new.json. Useful during development so old and '
1852 'new files can be looked at side-by-side.')
1853 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501854 '-v', '--verbose', action='store_true', help=
1855 'Increases verbosity. Affects consistency checks.')
1856 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281857 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1858 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111859 parser.add_argument(
1860 '--pyl-files-dir', type=os.path.realpath,
1861 help='Path to the directory containing the input .pyl files.')
Karen Qiane24b7ee2019-02-12 23:37:061862 parser.add_argument(
1863 '--json', help=
1864 ("Outputs results into a json file. Only works with query function.\n" +
1865 "Examples:\n" +
1866 " Outputs file into specified json file: \n" +
1867 " --json <file-name-here.json>"))
Garrett Beatyd5ca75962020-05-07 16:58:311868 parser.add_argument(
1869 '--infra-config-dir',
1870 help='Path to the LUCI services configuration directory',
1871 default=os.path.abspath(
1872 os.path.join(os.path.dirname(__file__),
1873 '..', '..', 'infra', 'config')))
Kenneth Russelleb60cbd22017-12-05 07:54:281874 self.args = parser.parse_args(argv)
Karen Qiane24b7ee2019-02-12 23:37:061875 if self.args.json and not self.args.query:
1876 parser.error("The --json flag can only be used with --query.")
Garrett Beatyd5ca75962020-05-07 16:58:311877 self.args.infra_config_dir = os.path.abspath(self.args.infra_config_dir)
Karen Qiane24b7ee2019-02-12 23:37:061878
1879 def does_test_match(self, test_info, params_dict):
1880 """Checks to see if the test matches the parameters given.
1881
1882 Compares the provided test_info with the params_dict to see
1883 if the bot matches the parameters given. If so, returns True.
1884 Else, returns false.
1885
1886 Args:
1887 test_info (dict): Information about a specific bot provided
1888 in the format shown in waterfalls.pyl
1889 params_dict (dict): Dictionary of parameters and their values
1890 to look for in the bot
1891 Ex: {
1892 'device_os':'android',
1893 '--flag':True,
1894 'mixins': ['mixin1', 'mixin2'],
1895 'ex_key':'ex_value'
1896 }
1897
1898 """
1899 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
1900 'kvm', 'pool', 'integrity'] # dimension parameters
1901 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
1902 'can_use_on_swarming_builders']
1903 for param in params_dict:
1904 # if dimension parameter
1905 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
1906 if not 'swarming' in test_info:
1907 return False
1908 swarming = test_info['swarming']
1909 if param in SWARMING_PARAMS:
1910 if not param in swarming:
1911 return False
1912 if not str(swarming[param]) == params_dict[param]:
1913 return False
1914 else:
1915 if not 'dimension_sets' in swarming:
1916 return False
1917 d_set = swarming['dimension_sets']
1918 # only looking at the first dimension set
1919 if not param in d_set[0]:
1920 return False
1921 if not d_set[0][param] == params_dict[param]:
1922 return False
1923
1924 # if flag
1925 elif param.startswith('--'):
1926 if not 'args' in test_info:
1927 return False
1928 if not param in test_info['args']:
1929 return False
1930
1931 # not dimension parameter/flag/mixin
1932 else:
1933 if not param in test_info:
1934 return False
1935 if not test_info[param] == params_dict[param]:
1936 return False
1937 return True
1938 def error_msg(self, msg):
1939 """Prints an error message.
1940
1941 In addition to a catered error message, also prints
1942 out where the user can find more help. Then, program exits.
1943 """
1944 self.print_line(msg + (' If you need more information, ' +
1945 'please run with -h or --help to see valid commands.'))
1946 sys.exit(1)
1947
1948 def find_bots_that_run_test(self, test, bots):
1949 matching_bots = []
1950 for bot in bots:
1951 bot_info = bots[bot]
1952 tests = self.flatten_tests_for_bot(bot_info)
1953 for test_info in tests:
1954 test_name = ""
1955 if 'name' in test_info:
1956 test_name = test_info['name']
1957 elif 'test' in test_info:
1958 test_name = test_info['test']
1959 if not test_name == test:
1960 continue
1961 matching_bots.append(bot)
1962 return matching_bots
1963
1964 def find_tests_with_params(self, tests, params_dict):
1965 matching_tests = []
1966 for test_name in tests:
1967 test_info = tests[test_name]
1968 if not self.does_test_match(test_info, params_dict):
1969 continue
1970 if not test_name in matching_tests:
1971 matching_tests.append(test_name)
1972 return matching_tests
1973
1974 def flatten_waterfalls_for_query(self, waterfalls):
1975 bots = {}
1976 for waterfall in waterfalls:
Greg Gutermanf60eb052020-03-12 17:40:011977 waterfall_tests = self.generate_output_tests(waterfall)
1978 for bot in waterfall_tests:
1979 bot_info = waterfall_tests[bot]
1980 bots[bot] = bot_info
Karen Qiane24b7ee2019-02-12 23:37:061981 return bots
1982
1983 def flatten_tests_for_bot(self, bot_info):
1984 """Returns a list of flattened tests.
1985
1986 Returns a list of tests not grouped by test category
1987 for a specific bot.
1988 """
1989 TEST_CATS = self.get_test_generator_map().keys()
1990 tests = []
1991 for test_cat in TEST_CATS:
1992 if not test_cat in bot_info:
1993 continue
1994 test_cat_tests = bot_info[test_cat]
1995 tests = tests + test_cat_tests
1996 return tests
1997
1998 def flatten_tests_for_query(self, test_suites):
1999 """Returns a flattened dictionary of tests.
2000
2001 Returns a dictionary of tests associate with their
2002 configuration, not grouped by their test suite.
2003 """
2004 tests = {}
2005 for test_suite in test_suites.itervalues():
2006 for test in test_suite:
2007 test_info = test_suite[test]
2008 test_name = test
2009 if 'name' in test_info:
2010 test_name = test_info['name']
2011 tests[test_name] = test_info
2012 return tests
2013
2014 def parse_query_filter_params(self, params):
2015 """Parses the filter parameters.
2016
2017 Creates a dictionary from the parameters provided
2018 to filter the bot array.
2019 """
2020 params_dict = {}
2021 for p in params:
2022 # flag
2023 if p.startswith("--"):
2024 params_dict[p] = True
2025 else:
2026 pair = p.split(":")
2027 if len(pair) != 2:
2028 self.error_msg('Invalid command.')
2029 # regular parameters
2030 if pair[1].lower() == "true":
2031 params_dict[pair[0]] = True
2032 elif pair[1].lower() == "false":
2033 params_dict[pair[0]] = False
2034 else:
2035 params_dict[pair[0]] = pair[1]
2036 return params_dict
2037
2038 def get_test_suites_dict(self, bots):
2039 """Returns a dictionary of bots and their tests.
2040
2041 Returns a dictionary of bots and a list of their associated tests.
2042 """
2043 test_suite_dict = dict()
2044 for bot in bots:
2045 bot_info = bots[bot]
2046 tests = self.flatten_tests_for_bot(bot_info)
2047 test_suite_dict[bot] = tests
2048 return test_suite_dict
2049
2050 def output_query_result(self, result, json_file=None):
2051 """Outputs the result of the query.
2052
2053 If a json file parameter name is provided, then
2054 the result is output into the json file. If not,
2055 then the result is printed to the console.
2056 """
2057 output = json.dumps(result, indent=2)
2058 if json_file:
2059 self.write_file(json_file, output)
2060 else:
2061 self.print_line(output)
2062 return
2063
2064 def query(self, args):
2065 """Queries tests or bots.
2066
2067 Depending on the arguments provided, outputs a json of
2068 tests or bots matching the appropriate optional parameters provided.
2069 """
2070 # split up query statement
2071 query = args.query.split('/')
2072 self.load_configuration_files()
2073 self.resolve_configuration_files()
2074
2075 # flatten bots json
2076 tests = self.test_suites
2077 bots = self.flatten_waterfalls_for_query(self.waterfalls)
2078
2079 cmd_class = query[0]
2080
2081 # For queries starting with 'bots'
2082 if cmd_class == "bots":
2083 if len(query) == 1:
2084 return self.output_query_result(bots, args.json)
2085 # query with specific parameters
2086 elif len(query) == 2:
2087 if query[1] == 'tests':
2088 test_suites_dict = self.get_test_suites_dict(bots)
2089 return self.output_query_result(test_suites_dict, args.json)
2090 else:
2091 self.error_msg("This query should be in the format: bots/tests.")
2092
2093 else:
2094 self.error_msg("This query should have 0 or 1 '/', found %s instead."
2095 % str(len(query)-1))
2096
2097 # For queries starting with 'bot'
2098 elif cmd_class == "bot":
2099 if not len(query) == 2 and not len(query) == 3:
2100 self.error_msg("Command should have 1 or 2 '/', found %s instead."
2101 % str(len(query)-1))
2102 bot_id = query[1]
2103 if not bot_id in bots:
2104 self.error_msg("No bot named '" + bot_id + "' found.")
2105 bot_info = bots[bot_id]
2106 if len(query) == 2:
2107 return self.output_query_result(bot_info, args.json)
2108 if not query[2] == 'tests':
2109 self.error_msg("The query should be in the format:" +
2110 "bot/<bot-name>/tests.")
2111
2112 bot_tests = self.flatten_tests_for_bot(bot_info)
2113 return self.output_query_result(bot_tests, args.json)
2114
2115 # For queries starting with 'tests'
2116 elif cmd_class == "tests":
2117 if not len(query) == 1 and not len(query) == 2:
2118 self.error_msg("The query should have 0 or 1 '/', found %s instead."
2119 % str(len(query)-1))
2120 flattened_tests = self.flatten_tests_for_query(tests)
2121 if len(query) == 1:
2122 return self.output_query_result(flattened_tests, args.json)
2123
2124 # create params dict
2125 params = query[1].split('&')
2126 params_dict = self.parse_query_filter_params(params)
2127 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2128 return self.output_query_result(matching_bots)
2129
2130 # For queries starting with 'test'
2131 elif cmd_class == "test":
2132 if not len(query) == 2 and not len(query) == 3:
2133 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2134 % str(len(query)-1))
2135 test_id = query[1]
2136 if len(query) == 2:
2137 flattened_tests = self.flatten_tests_for_query(tests)
2138 for test in flattened_tests:
2139 if test == test_id:
2140 return self.output_query_result(flattened_tests[test], args.json)
2141 self.error_msg("There is no test named %s." % test_id)
2142 if not query[2] == 'bots':
2143 self.error_msg("The query should be in the format: " +
2144 "test/<test-name>/bots")
2145 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2146 return self.output_query_result(bots_for_test)
2147
2148 else:
2149 self.error_msg("Your command did not match any valid commands." +
2150 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:282151
2152 def main(self, argv): # pragma: no cover
2153 self.parse_args(argv)
2154 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502155 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062156 elif self.args.query:
2157 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282158 else:
Greg Gutermanf60eb052020-03-12 17:40:012159 self.write_json_result(self.generate_outputs())
Kenneth Russelleb60cbd22017-12-05 07:54:282160 return 0
2161
2162if __name__ == "__main__": # pragma: no cover
2163 generator = BBJSONGenerator()
John Budorick699282e2019-02-13 01:27:332164 sys.exit(generator.main(sys.argv[1:]))