blob: e59990144a2cd86667f3038086cbd97ab9bd1fc8 [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
Kenneth Russell8ceeabf2017-12-11 17:53:2815import itertools
Kenneth Russelleb60cbd22017-12-05 07:54:2816import json
17import os
18import string
19import sys
John Budorick826d5ed2017-12-28 19:27:3220import traceback
Kenneth Russelleb60cbd22017-12-05 07:54:2821
22THIS_DIR = os.path.dirname(os.path.abspath(__file__))
23
24
25class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4926 def __init__(self, message):
27 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2828
29
Kenneth Russell8ceeabf2017-12-11 17:53:2830# This class is only present to accommodate certain machines on
31# chromium.android.fyi which run certain tests as instrumentation
32# tests, but not as gtests. If this discrepancy were fixed then the
33# notion could be removed.
34class TestSuiteTypes(object):
35 GTEST = 'gtest'
36
37
Kenneth Russelleb60cbd22017-12-05 07:54:2838class BaseGenerator(object):
39 def __init__(self, bb_gen):
40 self.bb_gen = bb_gen
41
Kenneth Russell8ceeabf2017-12-11 17:53:2842 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2843 raise NotImplementedError()
44
45 def sort(self, tests):
46 raise NotImplementedError()
47
48
Kenneth Russell8ceeabf2017-12-11 17:53:2849def cmp_tests(a, b):
50 # Prefer to compare based on the "test" key.
51 val = cmp(a['test'], b['test'])
52 if val != 0:
53 return val
54 if 'name' in a and 'name' in b:
55 return cmp(a['name'], b['name']) # pragma: no cover
56 if 'name' not in a and 'name' not in b:
57 return 0 # pragma: no cover
58 # Prefer to put variants of the same test after the first one.
59 if 'name' in a:
60 return 1
61 # 'name' is in b.
62 return -1 # pragma: no cover
63
64
Kenneth Russell8a386d42018-06-02 09:48:0165class GPUTelemetryTestGenerator(BaseGenerator):
Bo Liu555a0f92019-03-29 12:11:5666
67 def __init__(self, bb_gen, is_android_webview=False):
Kenneth Russell8a386d42018-06-02 09:48:0168 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5669 self._is_android_webview = is_android_webview
Kenneth Russell8a386d42018-06-02 09:48:0170
71 def generate(self, waterfall, tester_name, tester_config, input_tests):
72 isolated_scripts = []
73 for test_name, test_config in sorted(input_tests.iteritems()):
74 test = self.bb_gen.generate_gpu_telemetry_test(
Bo Liu555a0f92019-03-29 12:11:5675 waterfall, tester_name, tester_config, test_name, test_config,
76 self._is_android_webview)
Kenneth Russell8a386d42018-06-02 09:48:0177 if test:
78 isolated_scripts.append(test)
79 return isolated_scripts
80
81 def sort(self, tests):
82 return sorted(tests, key=lambda x: x['name'])
83
84
Kenneth Russelleb60cbd22017-12-05 07:54:2885class GTestGenerator(BaseGenerator):
86 def __init__(self, bb_gen):
87 super(GTestGenerator, self).__init__(bb_gen)
88
Kenneth Russell8ceeabf2017-12-11 17:53:2889 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2890 # The relative ordering of some of the tests is important to
91 # minimize differences compared to the handwritten JSON files, since
92 # Python's sorts are stable and there are some tests with the same
93 # key (see gles2_conform_d3d9_test and similar variants). Avoid
94 # losing the order by avoiding coalescing the dictionaries into one.
95 gtests = []
96 for test_name, test_config in sorted(input_tests.iteritems()):
Jeff Yoon67c3e832020-02-08 07:39:3897 # Variants allow more than one definition for a given test, and is defined
98 # in array format from resolve_variants().
99 if not isinstance(test_config, list):
100 test_config = [test_config]
101
102 for config in test_config:
103 test = self.bb_gen.generate_gtest(
104 waterfall, tester_name, tester_config, test_name, config)
105 if test:
106 # generate_gtest may veto the test generation on this tester.
107 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28108 return gtests
109
110 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28111 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28112
113
114class IsolatedScriptTestGenerator(BaseGenerator):
115 def __init__(self, bb_gen):
116 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
117
Kenneth Russell8ceeabf2017-12-11 17:53:28118 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28119 isolated_scripts = []
120 for test_name, test_config in sorted(input_tests.iteritems()):
121 test = self.bb_gen.generate_isolated_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28122 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28123 if test:
124 isolated_scripts.append(test)
125 return isolated_scripts
126
127 def sort(self, tests):
128 return sorted(tests, key=lambda x: x['name'])
129
130
131class ScriptGenerator(BaseGenerator):
132 def __init__(self, bb_gen):
133 super(ScriptGenerator, self).__init__(bb_gen)
134
Kenneth Russell8ceeabf2017-12-11 17:53:28135 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28136 scripts = []
137 for test_name, test_config in sorted(input_tests.iteritems()):
138 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28139 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28140 if test:
141 scripts.append(test)
142 return scripts
143
144 def sort(self, tests):
145 return sorted(tests, key=lambda x: x['name'])
146
147
148class JUnitGenerator(BaseGenerator):
149 def __init__(self, bb_gen):
150 super(JUnitGenerator, self).__init__(bb_gen)
151
Kenneth Russell8ceeabf2017-12-11 17:53:28152 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28153 scripts = []
154 for test_name, test_config in sorted(input_tests.iteritems()):
155 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28156 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28157 if test:
158 scripts.append(test)
159 return scripts
160
161 def sort(self, tests):
162 return sorted(tests, key=lambda x: x['test'])
163
164
165class CTSGenerator(BaseGenerator):
166 def __init__(self, bb_gen):
167 super(CTSGenerator, self).__init__(bb_gen)
168
Kenneth Russell8ceeabf2017-12-11 17:53:28169 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28170 # These only contain one entry and it's the contents of the input tests'
171 # dictionary, verbatim.
172 cts_tests = []
173 cts_tests.append(input_tests)
174 return cts_tests
175
176 def sort(self, tests):
177 return tests
178
179
180class InstrumentationTestGenerator(BaseGenerator):
181 def __init__(self, bb_gen):
182 super(InstrumentationTestGenerator, self).__init__(bb_gen)
183
Kenneth Russell8ceeabf2017-12-11 17:53:28184 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28185 scripts = []
186 for test_name, test_config in sorted(input_tests.iteritems()):
187 test = self.bb_gen.generate_instrumentation_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28188 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28189 if test:
190 scripts.append(test)
191 return scripts
192
193 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28194 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28195
196
Jeff Yoon67c3e832020-02-08 07:39:38197def check_compound_references(other_test_suites=None,
198 sub_suite=None,
199 suite=None,
200 target_test_suites=None,
201 test_type=None,
202 **kwargs):
203 """Ensure comound reference's don't target other compounds"""
204 del kwargs
205 if sub_suite in other_test_suites or sub_suite in target_test_suites:
206 raise BBGenErr('%s may not refer to other composition type test '
207 'suites (error found while processing %s)'
208 % (test_type, suite))
209
210def check_basic_references(basic_suites=None,
211 sub_suite=None,
212 suite=None,
213 **kwargs):
214 """Ensure test has a basic suite reference"""
215 del kwargs
216 if sub_suite not in basic_suites:
217 raise BBGenErr('Unable to find reference to %s while processing %s'
218 % (sub_suite, suite))
219
220def check_conflicting_definitions(basic_suites=None,
221 seen_tests=None,
222 sub_suite=None,
223 suite=None,
224 test_type=None,
225 **kwargs):
226 """Ensure that if a test is reachable via multiple basic suites,
227 all of them have an identical definition of the tests.
228 """
229 del kwargs
230 for test_name in basic_suites[sub_suite]:
231 if (test_name in seen_tests and
232 basic_suites[sub_suite][test_name] !=
233 basic_suites[seen_tests[test_name]][test_name]):
234 raise BBGenErr('Conflicting test definitions for %s from %s '
235 'and %s in %s (error found while processing %s)'
236 % (test_name, seen_tests[test_name], sub_suite,
237 test_type, suite))
238 seen_tests[test_name] = sub_suite
239
240def check_matrix_identifier(sub_suite=None,
241 suite=None,
242 suite_def=None,
Jeff Yoonda581c32020-03-06 03:56:05243 all_variants=None,
Jeff Yoon67c3e832020-02-08 07:39:38244 **kwargs):
245 """Ensure 'idenfitier' is defined for each variant"""
246 del kwargs
247 sub_suite_config = suite_def[sub_suite]
248 for variant in sub_suite_config.get('variants', []):
Jeff Yoonda581c32020-03-06 03:56:05249 if isinstance(variant, str):
250 if variant not in all_variants:
251 raise BBGenErr('Missing variant definition for %s in variants.pyl'
252 % variant)
253 variant = all_variants[variant]
254
Jeff Yoon67c3e832020-02-08 07:39:38255 if not 'identifier' in variant:
256 raise BBGenErr('Missing required identifier field in matrix '
257 'compound suite %s, %s' % (suite, sub_suite))
258
259
Kenneth Russelleb60cbd22017-12-05 07:54:28260class BBJSONGenerator(object):
261 def __init__(self):
262 self.this_dir = THIS_DIR
263 self.args = None
264 self.waterfalls = None
265 self.test_suites = None
266 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01267 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41268 self.gn_isolate_map = None
Jeff Yoonda581c32020-03-06 03:56:05269 self.variants = None
Kenneth Russelleb60cbd22017-12-05 07:54:28270
271 def generate_abs_file_path(self, relative_path):
272 return os.path.join(self.this_dir, relative_path) # pragma: no cover
273
Stephen Martinis7eb8b612018-09-21 00:17:50274 def print_line(self, line):
275 # Exists so that tests can mock
276 print line # pragma: no cover
277
Kenneth Russelleb60cbd22017-12-05 07:54:28278 def read_file(self, relative_path):
279 with open(self.generate_abs_file_path(
280 relative_path)) as fp: # pragma: no cover
281 return fp.read() # pragma: no cover
282
283 def write_file(self, relative_path, contents):
284 with open(self.generate_abs_file_path(
285 relative_path), 'wb') as fp: # pragma: no cover
286 fp.write(contents) # pragma: no cover
287
Zhiling Huangbe008172018-03-08 19:13:11288 def pyl_file_path(self, filename):
289 if self.args and self.args.pyl_files_dir:
290 return os.path.join(self.args.pyl_files_dir, filename)
291 return filename
292
Kenneth Russelleb60cbd22017-12-05 07:54:28293 def load_pyl_file(self, filename):
294 try:
Zhiling Huangbe008172018-03-08 19:13:11295 return ast.literal_eval(self.read_file(
296 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28297 except (SyntaxError, ValueError) as e: # pragma: no cover
298 raise BBGenErr('Failed to parse pyl file "%s": %s' %
299 (filename, e)) # pragma: no cover
300
Kenneth Russell8a386d42018-06-02 09:48:01301 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
302 # Currently it is only mandatory for bots which run GPU tests. Change these to
303 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28304 def is_android(self, tester_config):
305 return tester_config.get('os_type') == 'android'
306
Ben Pastenea9e583b2019-01-16 02:57:26307 def is_chromeos(self, tester_config):
308 return tester_config.get('os_type') == 'chromeos'
309
Kenneth Russell8a386d42018-06-02 09:48:01310 def is_linux(self, tester_config):
311 return tester_config.get('os_type') == 'linux'
312
Kai Ninomiya40de9f52019-10-18 21:38:49313 def is_mac(self, tester_config):
314 return tester_config.get('os_type') == 'mac'
315
316 def is_win(self, tester_config):
317 return tester_config.get('os_type') == 'win'
318
319 def is_win64(self, tester_config):
320 return (tester_config.get('os_type') == 'win' and
321 tester_config.get('browser_config') == 'release_x64')
322
Kenneth Russelleb60cbd22017-12-05 07:54:28323 def get_exception_for_test(self, test_name, test_config):
324 # gtests may have both "test" and "name" fields, and usually, if the "name"
325 # field is specified, it means that the same test is being repurposed
326 # multiple times with different command line arguments. To handle this case,
327 # prefer to lookup per the "name" field of the test itself, as opposed to
328 # the "test_name", which is actually the "test" field.
329 if 'name' in test_config:
330 return self.exceptions.get(test_config['name'])
331 else:
332 return self.exceptions.get(test_name)
333
Nico Weberb0b3f5862018-07-13 18:45:15334 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28335 # Currently, the only reason a test should not run on a given tester is that
336 # it's in the exceptions. (Once the GPU waterfall generation script is
337 # incorporated here, the rules will become more complex.)
338 exception = self.get_exception_for_test(test_name, test_config)
339 if not exception:
340 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28341 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28342 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28343 if remove_from:
344 if tester_name in remove_from:
345 return False
346 # TODO(kbr): this code path was added for some tests (including
347 # android_webview_unittests) on one machine (Nougat Phone
348 # Tester) which exists with the same name on two waterfalls,
349 # chromium.android and chromium.fyi; the tests are run on one
350 # but not the other. Once the bots are all uniquely named (a
351 # different ongoing project) this code should be removed.
352 # TODO(kbr): add coverage.
353 return (tester_name + ' ' + waterfall['name']
354 not in remove_from) # pragma: no cover
355 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28356
Nico Weber79dc5f6852018-07-13 19:38:49357 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28358 exception = self.get_exception_for_test(test_name, test)
359 if not exception:
360 return None
Nico Weber79dc5f6852018-07-13 19:38:49361 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28362
Brian Sheedye6ea0ee2019-07-11 02:54:37363 def get_test_replacements(self, test, test_name, tester_name):
364 exception = self.get_exception_for_test(test_name, test)
365 if not exception:
366 return None
367 return exception.get('replacements', {}).get(tester_name)
368
Kenneth Russell8a386d42018-06-02 09:48:01369 def merge_command_line_args(self, arr, prefix, splitter):
370 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01371 idx = 0
372 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01373 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01374 while idx < len(arr):
375 flag = arr[idx]
376 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01377 if flag.startswith(prefix):
378 arg = flag[prefix_len:]
379 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01380 if first_idx < 0:
381 first_idx = idx
382 else:
383 delete_current_entry = True
384 if delete_current_entry:
385 del arr[idx]
386 else:
387 idx += 1
388 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01389 arr[first_idx] = prefix + splitter.join(accumulated_args)
390 return arr
391
392 def maybe_fixup_args_array(self, arr):
393 # The incoming array of strings may be an array of command line
394 # arguments. To make it easier to turn on certain features per-bot or
395 # per-test-suite, look specifically for certain flags and merge them
396 # appropriately.
397 # --enable-features=Feature1 --enable-features=Feature2
398 # are merged to:
399 # --enable-features=Feature1,Feature2
400 # and:
401 # --extra-browser-args=arg1 --extra-browser-args=arg2
402 # are merged to:
403 # --extra-browser-args=arg1 arg2
404 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
405 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01406 return arr
407
Kenneth Russelleb60cbd22017-12-05 07:54:28408 def dictionary_merge(self, a, b, path=None, update=True):
409 """http://stackoverflow.com/questions/7204805/
410 python-dictionaries-of-dictionaries-merge
411 merges b into a
412 """
413 if path is None:
414 path = []
415 for key in b:
416 if key in a:
417 if isinstance(a[key], dict) and isinstance(b[key], dict):
418 self.dictionary_merge(a[key], b[key], path + [str(key)])
419 elif a[key] == b[key]:
420 pass # same leaf value
421 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06422 # Args arrays are lists of strings. Just concatenate them,
423 # and don't sort them, in order to keep some needed
424 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28425 if all(isinstance(x, str)
426 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01427 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28428 else:
429 # TODO(kbr): this only works properly if the two arrays are
430 # the same length, which is currently always the case in the
431 # swarming dimension_sets that we have to merge. It will fail
432 # to merge / override 'args' arrays which are different
433 # length.
434 for idx in xrange(len(b[key])):
435 try:
436 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
437 path + [str(key), str(idx)],
438 update=update)
Jeff Yoon8154e582019-12-03 23:30:01439 except (IndexError, TypeError):
440 raise BBGenErr('Error merging lists by key "%s" from source %s '
441 'into target %s at index %s. Verify target list '
442 'length is equal or greater than source'
443 % (str(key), str(b), str(a), str(idx)))
John Budorick5bc387fe2019-05-09 20:02:53444 elif update:
445 if b[key] is None:
446 del a[key]
447 else:
448 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28449 else:
450 raise BBGenErr('Conflict at %s' % '.'.join(
451 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53452 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28453 a[key] = b[key]
454 return a
455
John Budorickab108712018-09-01 00:12:21456 def initialize_args_for_test(
457 self, generated_test, tester_config, additional_arg_keys=None):
John Budorickab108712018-09-01 00:12:21458 args = []
459 args.extend(generated_test.get('args', []))
460 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22461
Kenneth Russell8a386d42018-06-02 09:48:01462 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21463 val = generated_test.pop(key, [])
464 if fn(tester_config):
465 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01466
467 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
468 add_conditional_args('linux_args', self.is_linux)
469 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36470 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49471 add_conditional_args('mac_args', self.is_mac)
472 add_conditional_args('win_args', self.is_win)
473 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01474
John Budorickab108712018-09-01 00:12:21475 for key in additional_arg_keys or []:
476 args.extend(generated_test.pop(key, []))
477 args.extend(tester_config.get(key, []))
478
479 if args:
480 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01481
Kenneth Russelleb60cbd22017-12-05 07:54:28482 def initialize_swarming_dictionary_for_test(self, generated_test,
483 tester_config):
484 if 'swarming' not in generated_test:
485 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28486 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
487 generated_test['swarming'].update({
Jeff Yoon67c3e832020-02-08 07:39:38488 'can_use_on_swarming_builders': tester_config.get('use_swarming',
489 True)
Dirk Pranke81ff51c2017-12-09 19:24:28490 })
Kenneth Russelleb60cbd22017-12-05 07:54:28491 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03492 if ('dimension_sets' not in generated_test['swarming'] and
493 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28494 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
495 tester_config['swarming']['dimension_sets'])
496 self.dictionary_merge(generated_test['swarming'],
497 tester_config['swarming'])
498 # Apply any Android-specific Swarming dimensions after the generic ones.
499 if 'android_swarming' in generated_test:
500 if self.is_android(tester_config): # pragma: no cover
501 self.dictionary_merge(
502 generated_test['swarming'],
503 generated_test['android_swarming']) # pragma: no cover
504 del generated_test['android_swarming'] # pragma: no cover
505
506 def clean_swarming_dictionary(self, swarming_dict):
507 # Clean out redundant entries from a test's "swarming" dictionary.
508 # This is really only needed to retain 100% parity with the
509 # handwritten JSON files, and can be removed once all the files are
510 # autogenerated.
511 if 'shards' in swarming_dict:
512 if swarming_dict['shards'] == 1: # pragma: no cover
513 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24514 if 'hard_timeout' in swarming_dict:
515 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
516 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43517 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28518 # Remove all other keys.
519 for k in swarming_dict.keys(): # pragma: no cover
520 if k != 'can_use_on_swarming_builders': # pragma: no cover
521 del swarming_dict[k] # pragma: no cover
522
Stephen Martinis0382bc12018-09-17 22:29:07523 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
524 waterfall):
525 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01526 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07527 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28528 # See if there are any exceptions that need to be merged into this
529 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49530 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28531 if modifications:
532 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23533 if 'swarming' in test:
534 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28535 # Ensure all Android Swarming tests run only on userdebug builds if another
536 # build type was not specified.
537 if 'swarming' in test and self.is_android(tester_config):
538 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22539 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28540 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37541 self.replace_test_args(test, test_name, tester_name)
Ben Pastenee012aea42019-05-14 22:32:28542
Kenneth Russelleb60cbd22017-12-05 07:54:28543 return test
544
Brian Sheedye6ea0ee2019-07-11 02:54:37545 def replace_test_args(self, test, test_name, tester_name):
546 replacements = self.get_test_replacements(
547 test, test_name, tester_name) or {}
548 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
549 for key, replacement_dict in replacements.iteritems():
550 if key not in valid_replacement_keys:
551 raise BBGenErr(
552 'Given replacement key %s for %s on %s is not in the list of valid '
553 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
554 for replacement_key, replacement_val in replacement_dict.iteritems():
555 found_key = False
556 for i, test_key in enumerate(test.get(key, [])):
557 # Handle both the key/value being replaced being defined as two
558 # separate items or as key=value.
559 if test_key == replacement_key:
560 found_key = True
561 # Handle flags without values.
562 if replacement_val == None:
563 del test[key][i]
564 else:
565 test[key][i+1] = replacement_val
566 break
567 elif test_key.startswith(replacement_key + '='):
568 found_key = True
569 if replacement_val == None:
570 del test[key][i]
571 else:
572 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
573 break
574 if not found_key:
575 raise BBGenErr('Could not find %s in existing list of values for key '
576 '%s in %s on %s' % (replacement_key, key, test_name,
577 tester_name))
578
Shenghua Zhangaba8bad2018-02-07 02:12:09579 def add_common_test_properties(self, test, tester_config):
580 if tester_config.get('use_multi_dimension_trigger_script'):
Kenneth Russell73c3bd8b2018-10-19 22:30:19581 # Assumes update_and_cleanup_test has already been called, so the
582 # builder's mixins have been flattened into the test.
Shenghua Zhangaba8bad2018-02-07 02:12:09583 test['trigger_script'] = {
584 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
585 'args': [
586 '--multiple-trigger-configs',
Kenneth Russell73c3bd8b2018-10-19 22:30:19587 json.dumps(test['swarming']['dimension_sets'] +
Shenghua Zhangaba8bad2018-02-07 02:12:09588 tester_config.get('alternate_swarming_dimensions', [])),
589 '--multiple-dimension-script-verbose',
590 'True'
591 ],
592 }
Ben Pastenea9e583b2019-01-16 02:57:26593 elif self.is_chromeos(tester_config) and tester_config.get('use_swarming',
594 True):
595 # The presence of the "device_type" dimension indicates that the tests
596 # are targetting CrOS hardware and so need the special trigger script.
597 dimension_sets = tester_config['swarming']['dimension_sets']
598 if all('device_type' in ds for ds in dimension_sets):
599 test['trigger_script'] = {
600 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
601 }
Shenghua Zhangaba8bad2018-02-07 02:12:09602
Ben Pastene858f4be2019-01-09 23:52:09603 def add_android_presentation_args(self, tester_config, test_name, result):
604 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38605 bucket = tester_config.get('results_bucket', 'chromium-result-details')
606 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09607 if (result['swarming']['can_use_on_swarming_builders'] and not
608 tester_config.get('skip_merge_script', False)):
609 result['merge'] = {
610 'args': [
611 '--bucket',
John Budorick262ae112019-07-12 19:24:38612 bucket,
Ben Pastene858f4be2019-01-09 23:52:09613 '--test-name',
614 test_name
615 ],
616 'script': '//build/android/pylib/results/presentation/'
617 'test_results_presentation.py',
618 }
619 if not tester_config.get('skip_cipd_packages', False):
Ben Pastenee5949ea82019-01-10 21:45:26620 cipd_packages = result['swarming'].get('cipd_packages', [])
621 cipd_packages.append(
Ben Pastene858f4be2019-01-09 23:52:09622 {
623 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
624 'location': 'bin',
625 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
626 }
Ben Pastenee5949ea82019-01-10 21:45:26627 )
628 result['swarming']['cipd_packages'] = cipd_packages
Ben Pastene858f4be2019-01-09 23:52:09629 if not tester_config.get('skip_output_links', False):
630 result['swarming']['output_links'] = [
631 {
632 'link': [
633 'https://luci-logdog.appspot.com/v/?s',
634 '=android%2Fswarming%2Flogcats%2F',
635 '${TASK_ID}%2F%2B%2Funified_logcats',
636 ],
637 'name': 'shard #${SHARD_INDEX} logcats',
638 },
639 ]
640 if args:
641 result['args'] = args
642
Kenneth Russelleb60cbd22017-12-05 07:54:28643 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
644 test_config):
645 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15646 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28647 return None
648 result = copy.deepcopy(test_config)
649 if 'test' in result:
650 result['name'] = test_name
651 else:
652 result['test'] = test_name
653 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21654
655 self.initialize_args_for_test(
656 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28657 if self.is_android(tester_config) and tester_config.get('use_swarming',
658 True):
Ben Pastene858f4be2019-01-09 23:52:09659 self.add_android_presentation_args(tester_config, test_name, result)
660 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42661
Stephen Martinis0382bc12018-09-17 22:29:07662 result = self.update_and_cleanup_test(
663 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09664 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43665
666 if not result.get('merge'):
667 # TODO(https://crbug.com/958376): Consider adding the ability to not have
668 # this default.
669 result['merge'] = {
670 'script': '//testing/merge_scripts/standard_gtest_merge.py',
671 'args': [],
672 }
Kenneth Russelleb60cbd22017-12-05 07:54:28673 return result
674
675 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
676 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01677 if not self.should_run_on_tester(waterfall, tester_name, test_name,
678 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28679 return None
680 result = copy.deepcopy(test_config)
681 result['isolate_name'] = result.get('isolate_name', test_name)
682 result['name'] = test_name
683 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01684 self.initialize_args_for_test(result, tester_config)
Ben Pastene858f4be2019-01-09 23:52:09685 if tester_config.get('use_android_presentation', False):
686 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07687 result = self.update_and_cleanup_test(
688 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09689 self.add_common_test_properties(result, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17690
691 if not result.get('merge'):
692 # TODO(https://crbug.com/958376): Consider adding the ability to not have
693 # this default.
694 result['merge'] = {
695 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
696 'args': [],
697 }
Kenneth Russelleb60cbd22017-12-05 07:54:28698 return result
699
700 def generate_script_test(self, waterfall, tester_name, tester_config,
701 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44702 # TODO(https://crbug.com/953072): Remove this check whenever a better
703 # long-term solution is implemented.
704 if (waterfall.get('forbid_script_tests', False) or
705 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
706 raise BBGenErr('Attempted to generate a script test on tester ' +
707 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01708 if not self.should_run_on_tester(waterfall, tester_name, test_name,
709 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28710 return None
711 result = {
712 'name': test_name,
713 'script': test_config['script']
714 }
Stephen Martinis0382bc12018-09-17 22:29:07715 result = self.update_and_cleanup_test(
716 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28717 return result
718
719 def generate_junit_test(self, waterfall, tester_name, tester_config,
720 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01721 if not self.should_run_on_tester(waterfall, tester_name, test_name,
722 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28723 return None
John Budorickdef6acb2019-09-17 22:51:09724 result = copy.deepcopy(test_config)
725 result.update({
John Budorickcadc4952019-09-16 23:51:37726 'name': test_name,
727 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09728 })
729 self.initialize_args_for_test(result, tester_config)
730 result = self.update_and_cleanup_test(
731 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28732 return result
733
734 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
735 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01736 if not self.should_run_on_tester(waterfall, tester_name, test_name,
737 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28738 return None
739 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28740 if 'test' in result and result['test'] != test_name:
741 result['name'] = test_name
742 else:
743 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07744 result = self.update_and_cleanup_test(
745 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28746 return result
747
Stephen Martinis2a0667022018-09-25 22:31:14748 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01749 substitutions = {
750 # Any machine in waterfalls.pyl which desires to run GPU tests
751 # must provide the os_type key.
752 'os_type': tester_config['os_type'],
753 'gpu_vendor_id': '0',
754 'gpu_device_id': '0',
755 }
Stephen Martinis2a0667022018-09-25 22:31:14756 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01757 if 'gpu' in dimension_set:
758 # First remove the driver version, then split into vendor and device.
759 gpu = dimension_set['gpu']
Kenneth Russell384a1732019-03-16 02:36:02760 # Handle certain specialized named GPUs.
761 if gpu.startswith('nvidia-quadro-p400'):
762 gpu = ['10de', '1cb3']
763 elif gpu.startswith('intel-hd-630'):
764 gpu = ['8086', '5912']
Brian Sheedyf9387db7b2019-08-05 19:26:10765 elif gpu.startswith('intel-uhd-630'):
766 gpu = ['8086', '3e92']
Kenneth Russell384a1732019-03-16 02:36:02767 else:
768 gpu = gpu.split('-')[0].split(':')
Kenneth Russell8a386d42018-06-02 09:48:01769 substitutions['gpu_vendor_id'] = gpu[0]
770 substitutions['gpu_device_id'] = gpu[1]
771 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
772
773 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56774 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01775 # These are all just specializations of isolated script tests with
776 # a bunch of boilerplate command line arguments added.
777
778 # The step name must end in 'test' or 'tests' in order for the
779 # results to automatically show up on the flakiness dashboard.
780 # (At least, this was true some time ago.) Continue to use this
781 # naming convention for the time being to minimize changes.
782 step_name = test_config.get('name', test_name)
783 if not (step_name.endswith('test') or step_name.endswith('tests')):
784 step_name = '%s_tests' % step_name
785 result = self.generate_isolated_script_test(
786 waterfall, tester_name, tester_config, step_name, test_config)
787 if not result:
788 return None
789 result['isolate_name'] = 'telemetry_gpu_integration_test'
790 args = result.get('args', [])
791 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14792
793 # These tests upload and download results from cloud storage and therefore
794 # aren't idempotent yet. https://crbug.com/549140.
795 result['swarming']['idempotent'] = False
796
Kenneth Russell44910c32018-12-03 23:35:11797 # The GPU tests act much like integration tests for the entire browser, and
798 # tend to uncover flakiness bugs more readily than other test suites. In
799 # order to surface any flakiness more readily to the developer of the CL
800 # which is introducing it, we disable retries with patch on the commit
801 # queue.
802 result['should_retry_with_patch'] = False
803
Bo Liu555a0f92019-03-29 12:11:56804 browser = ('android-webview-instrumentation'
805 if is_android_webview else tester_config['browser_config'])
Kenneth Russell8a386d42018-06-02 09:48:01806 args = [
Bo Liu555a0f92019-03-29 12:11:56807 test_to_run,
808 '--show-stdout',
809 '--browser=%s' % browser,
810 # --passthrough displays more of the logging in Telemetry when
811 # run via typ, in particular some of the warnings about tests
812 # being expected to fail, but passing.
813 '--passthrough',
814 '-v',
815 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
Kenneth Russell8a386d42018-06-02 09:48:01816 ] + args
817 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14818 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01819 return result
820
Kenneth Russelleb60cbd22017-12-05 07:54:28821 def get_test_generator_map(self):
822 return {
Bo Liu555a0f92019-03-29 12:11:56823 'android_webview_gpu_telemetry_tests':
824 GPUTelemetryTestGenerator(self, is_android_webview=True),
825 'cts_tests':
826 CTSGenerator(self),
827 'gpu_telemetry_tests':
828 GPUTelemetryTestGenerator(self),
829 'gtest_tests':
830 GTestGenerator(self),
831 'instrumentation_tests':
832 InstrumentationTestGenerator(self),
833 'isolated_scripts':
834 IsolatedScriptTestGenerator(self),
835 'junit_tests':
836 JUnitGenerator(self),
837 'scripts':
838 ScriptGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28839 }
840
Kenneth Russell8a386d42018-06-02 09:48:01841 def get_test_type_remapper(self):
842 return {
843 # These are a specialization of isolated_scripts with a bunch of
844 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:56845 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:01846 'gpu_telemetry_tests': 'isolated_scripts',
847 }
848
Jeff Yoon67c3e832020-02-08 07:39:38849 def check_composition_type_test_suites(self, test_type,
850 additional_validators=None):
851 """Pre-pass to catch errors reliabily for compound/matrix suites"""
852 validators = [check_compound_references,
853 check_basic_references,
854 check_conflicting_definitions]
855 if additional_validators:
856 validators += additional_validators
857
858 target_suites = self.test_suites.get(test_type, {})
859 other_test_type = ('compound_suites'
860 if test_type == 'matrix_compound_suites'
861 else 'matrix_compound_suites')
862 other_suites = self.test_suites.get(other_test_type, {})
Jeff Yoon8154e582019-12-03 23:30:01863 basic_suites = self.test_suites.get('basic_suites', {})
864
Jeff Yoon67c3e832020-02-08 07:39:38865 for suite, suite_def in target_suites.iteritems():
Jeff Yoon8154e582019-12-03 23:30:01866 if suite in basic_suites:
867 raise BBGenErr('%s names may not duplicate basic test suite names '
868 '(error found while processsing %s)'
869 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:01870
Jeff Yoon67c3e832020-02-08 07:39:38871 seen_tests = {}
872 for sub_suite in suite_def:
873 for validator in validators:
874 validator(
875 basic_suites=basic_suites,
876 other_test_suites=other_suites,
877 seen_tests=seen_tests,
878 sub_suite=sub_suite,
879 suite=suite,
880 suite_def=suite_def,
881 target_test_suites=target_suites,
882 test_type=test_type,
Jeff Yoonda581c32020-03-06 03:56:05883 all_variants=self.variants
Jeff Yoon67c3e832020-02-08 07:39:38884 )
Kenneth Russelleb60cbd22017-12-05 07:54:28885
Stephen Martinis54d64ad2018-09-21 22:16:20886 def flatten_test_suites(self):
887 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:01888 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
889 for category in test_types:
890 for name, value in self.test_suites.get(category, {}).iteritems():
891 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:20892 self.test_suites = new_test_suites
893
Nodir Turakulovfce34292019-12-18 17:05:41894 def resolve_full_test_targets(self):
895 for suite in self.test_suites['basic_suites'].itervalues():
896 for key, test in suite.iteritems():
897 if not isinstance(test, dict):
898 # Some test definitions are just strings, such as CTS.
899 # Skip them.
900 continue
901
902 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
903 # https://source.chromium.org/chromium/chromium/tools/build/+/master:scripts/slave/recipe_modules/chromium_tests/generators.py;l=89;drc=14c062ba0eb418d3c4623dde41a753241b9df06b
904 # TODO(crbug.com/1035124): clean this up.
905 isolate_name = test.get('test') or test.get('isolate_name') or key
906 gn_entry = self.gn_isolate_map.get(isolate_name)
907 if gn_entry:
908 test['test_target'] = gn_entry['label']
909 else: # pragma: no cover
910 # Some tests do not have an entry gn_isolate_map.pyl, such as
911 # telemetry tests.
912 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
913 pass
914
Kenneth Russelleb60cbd22017-12-05 07:54:28915 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:01916 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:20917
Jeff Yoon8154e582019-12-03 23:30:01918 compound_suites = self.test_suites.get('compound_suites', {})
919 # check_composition_type_test_suites() checks that all basic suites
920 # referenced by compound suites exist.
921 basic_suites = self.test_suites.get('basic_suites')
922
923 for name, value in compound_suites.iteritems():
924 # Resolve this to a dictionary.
925 full_suite = {}
926 for entry in value:
927 suite = basic_suites[entry]
928 full_suite.update(suite)
929 compound_suites[name] = full_suite
930
Jeff Yoon67c3e832020-02-08 07:39:38931 def resolve_variants(self, basic_test_definition, variants):
932 """ Merge variant-defined configurations to each test case definition in a
933 test suite.
934
935 The output maps a unique test name to an array of configurations because
936 there may exist more than one definition for a test name using variants. The
937 test name is referenced while mapping machines to test suites, so unpacking
938 the array is done by the generators.
939
940 Args:
941 basic_test_definition: a {} defined test suite in the format
942 test_name:test_config
943 variants: an [] of {} defining configurations to be applied to each test
944 case in the basic test_definition
945
946 Return:
947 a {} of test_name:[{}], where each {} is a merged configuration
948 """
949
950 # Each test in a basic test suite will have a definition per variant.
951 test_suite = {}
952 for test_name, test_config in basic_test_definition.iteritems():
953 definitions = []
954 for variant in variants:
Jeff Yoonda581c32020-03-06 03:56:05955 # Unpack the variant from variants.pyl if it's string based.
956 if isinstance(variant, str):
957 variant = self.variants[variant]
958
Jeff Yoon67c3e832020-02-08 07:39:38959 # Clone a copy of test_config so that we can have a uniquely updated
960 # version of it per variant
961 cloned_config = copy.deepcopy(test_config)
962 # The variant definition needs to be re-used for each test, so we'll
963 # create a clone and work with it as well.
964 cloned_variant = copy.deepcopy(variant)
965
966 cloned_config['args'] = (cloned_config.get('args', []) +
967 cloned_variant.get('args', []))
968 cloned_config['mixins'] = (cloned_config.get('mixins', []) +
969 cloned_variant.get('mixins', []))
970
971 basic_swarming_def = cloned_config.get('swarming', {})
972 variant_swarming_def = cloned_variant.get('swarming', {})
973 if basic_swarming_def and variant_swarming_def:
974 if ('dimension_sets' in basic_swarming_def and
975 'dimension_sets' in variant_swarming_def):
976 # Retain swarming dimension set merge behavior when both variant and
977 # the basic test configuration both define it
978 self.dictionary_merge(basic_swarming_def, variant_swarming_def)
979 # Remove dimension_sets from the variant definition, so that it does
980 # not replace what's been done by dictionary_merge in the update
981 # call below.
982 del variant_swarming_def['dimension_sets']
983
984 # Update the swarming definition with whatever is defined for swarming
985 # by the variant.
986 basic_swarming_def.update(variant_swarming_def)
987 cloned_config['swarming'] = basic_swarming_def
988
989 # The identifier is used to make the name of the test unique.
990 # Generators in the recipe uniquely identify a test by it's name, so we
991 # don't want to have the same name for each variant.
992 cloned_config['name'] = '{}_{}'.format(test_name,
993 cloned_variant['identifier'])
994
995 definitions.append(cloned_config)
996 test_suite[test_name] = definitions
997 return test_suite
998
Jeff Yoon8154e582019-12-03 23:30:01999 def resolve_matrix_compound_test_suites(self):
Jeff Yoon67c3e832020-02-08 07:39:381000 self.check_composition_type_test_suites('matrix_compound_suites',
1001 [check_matrix_identifier])
Jeff Yoon8154e582019-12-03 23:30:011002
1003 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
Jeff Yoon67c3e832020-02-08 07:39:381004 # check_composition_type_test_suites() checks that all basic suites are
Jeff Yoon8154e582019-12-03 23:30:011005 # referenced by matrix suites exist.
1006 basic_suites = self.test_suites.get('basic_suites')
1007
Jeff Yoon67c3e832020-02-08 07:39:381008 for test_name, matrix_config in matrix_compound_suites.iteritems():
Jeff Yoon8154e582019-12-03 23:30:011009 full_suite = {}
Jeff Yoon67c3e832020-02-08 07:39:381010
1011 for test_suite, mtx_test_suite_config in matrix_config.iteritems():
1012 basic_test_def = copy.deepcopy(basic_suites[test_suite])
1013
1014 if 'variants' in mtx_test_suite_config:
1015 result = self.resolve_variants(basic_test_def,
1016 mtx_test_suite_config['variants'])
1017 full_suite.update(result)
1018 matrix_compound_suites[test_name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:281019
1020 def link_waterfalls_to_test_suites(self):
1021 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431022 for tester_name, tester in waterfall['machines'].iteritems():
1023 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281024 if not value in self.test_suites:
1025 # Hard / impossible to cover this in the unit test.
1026 raise self.unknown_test_suite(
1027 value, tester_name, waterfall['name']) # pragma: no cover
1028 tester['test_suites'][suite] = self.test_suites[value]
1029
1030 def load_configuration_files(self):
1031 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
1032 self.test_suites = self.load_pyl_file('test_suites.pyl')
1033 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:011034 self.mixins = self.load_pyl_file('mixins.pyl')
Nodir Turakulovfce34292019-12-18 17:05:411035 self.gn_isolate_map = self.load_pyl_file('gn_isolate_map.pyl')
Jeff Yoonda581c32020-03-06 03:56:051036 self.variants = self.load_pyl_file('variants.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:281037
1038 def resolve_configuration_files(self):
Nodir Turakulovfce34292019-12-18 17:05:411039 self.resolve_full_test_targets()
Kenneth Russelleb60cbd22017-12-05 07:54:281040 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:011041 self.resolve_matrix_compound_test_suites()
1042 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281043 self.link_waterfalls_to_test_suites()
1044
Nico Weberd18b8962018-05-16 19:39:381045 def unknown_bot(self, bot_name, waterfall_name):
1046 return BBGenErr(
1047 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
1048
Kenneth Russelleb60cbd22017-12-05 07:54:281049 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
1050 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:381051 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:281052 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
1053
1054 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
1055 return BBGenErr(
1056 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
1057 ' on waterfall ' + waterfall_name)
1058
Stephen Martinisb72f6d22018-10-04 23:29:011059 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:071060 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:321061
1062 Checks in the waterfall, builder, and test objects for mixins.
1063 """
1064 def valid_mixin(mixin_name):
1065 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:011066 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:321067 raise BBGenErr("bad mixin %s" % mixin_name)
Jeff Yoon67c3e832020-02-08 07:39:381068
Stephen Martinisb6a50492018-09-12 23:59:321069 def must_be_list(mixins, typ, name):
1070 """Asserts that given mixins are a list."""
1071 if not isinstance(mixins, list):
1072 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
1073
Brian Sheedy7658c982020-01-08 02:27:581074 test_name = test.get('name')
1075 remove_mixins = set()
1076 if 'remove_mixins' in builder:
1077 must_be_list(builder['remove_mixins'], 'builder', builder_name)
1078 for rm in builder['remove_mixins']:
1079 valid_mixin(rm)
1080 remove_mixins.add(rm)
1081 if 'remove_mixins' in test:
1082 must_be_list(test['remove_mixins'], 'test', test_name)
1083 for rm in test['remove_mixins']:
1084 valid_mixin(rm)
1085 remove_mixins.add(rm)
1086 del test['remove_mixins']
1087
Stephen Martinisb72f6d22018-10-04 23:29:011088 if 'mixins' in waterfall:
1089 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
1090 for mixin in waterfall['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581091 if mixin in remove_mixins:
1092 continue
Stephen Martinisb6a50492018-09-12 23:59:321093 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011094 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321095
Stephen Martinisb72f6d22018-10-04 23:29:011096 if 'mixins' in builder:
1097 must_be_list(builder['mixins'], 'builder', builder_name)
1098 for mixin in builder['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581099 if mixin in remove_mixins:
1100 continue
Stephen Martinisb6a50492018-09-12 23:59:321101 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011102 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:321103
Stephen Martinisb72f6d22018-10-04 23:29:011104 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:071105 return test
1106
Stephen Martinis2a0667022018-09-25 22:31:141107 if not test_name:
1108 test_name = test.get('test')
1109 if not test_name: # pragma: no cover
1110 # Not the best name, but we should say something.
1111 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:011112 must_be_list(test['mixins'], 'test', test_name)
1113 for mixin in test['mixins']:
Brian Sheedy7658c982020-01-08 02:27:581114 # We don't bother checking if the given mixin is in remove_mixins here
1115 # since this is already the lowest level, so if a mixin is added here that
1116 # we don't want, we can just delete its entry.
Stephen Martinis0382bc12018-09-17 22:29:071117 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011118 test = self.apply_mixin(self.mixins[mixin], test)
Jeff Yoon67c3e832020-02-08 07:39:381119 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:071120 return test
Stephen Martinisb6a50492018-09-12 23:59:321121
Stephen Martinisb72f6d22018-10-04 23:29:011122 def apply_mixin(self, mixin, test):
1123 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:321124
Stephen Martinis0382bc12018-09-17 22:29:071125 Mixins will not override an existing key. This is to ensure exceptions can
1126 override a setting a mixin applies.
1127
Stephen Martinisb72f6d22018-10-04 23:29:011128 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:321129 'dimension_sets', which is how normal test suites specify their dimensions,
1130 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
1131 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:011132
Stephen Martinisb6a50492018-09-12 23:59:321133 """
1134 new_test = copy.deepcopy(test)
1135 mixin = copy.deepcopy(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:011136 if 'swarming' in mixin:
1137 swarming_mixin = mixin['swarming']
1138 new_test.setdefault('swarming', {})
1139 if 'dimensions' in swarming_mixin:
1140 new_test['swarming'].setdefault('dimension_sets', [{}])
1141 for dimension_set in new_test['swarming']['dimension_sets']:
1142 dimension_set.update(swarming_mixin['dimensions'])
1143 del swarming_mixin['dimensions']
Stephen Martinisb72f6d22018-10-04 23:29:011144 # python dict update doesn't do recursion at all. Just hard code the
1145 # nested update we need (mixin['swarming'] shouldn't clobber
1146 # test['swarming'], but should update it).
1147 new_test['swarming'].update(swarming_mixin)
1148 del mixin['swarming']
1149
Wezc0e835b702018-10-30 00:38:411150 if '$mixin_append' in mixin:
1151 # Values specified under $mixin_append should be appended to existing
1152 # lists, rather than replacing them.
1153 mixin_append = mixin['$mixin_append']
1154 for key in mixin_append:
1155 new_test.setdefault(key, [])
1156 if not isinstance(mixin_append[key], list):
1157 raise BBGenErr(
1158 'Key "' + key + '" in $mixin_append must be a list.')
1159 if not isinstance(new_test[key], list):
1160 raise BBGenErr(
1161 'Cannot apply $mixin_append to non-list "' + key + '".')
1162 new_test[key].extend(mixin_append[key])
1163 if 'args' in mixin_append:
1164 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1165 del mixin['$mixin_append']
1166
Stephen Martinisb72f6d22018-10-04 23:29:011167 new_test.update(mixin)
Stephen Martinisb6a50492018-09-12 23:59:321168 return new_test
1169
Gregory Gutermand1dd3b82020-03-02 21:53:481170 def generate_waterfall_json(self, waterfall):
1171 all_tests = {}
Greg Guterman5c6144152020-02-28 20:08:531172 generator_map = self.get_test_generator_map()
1173 test_type_remapper = self.get_test_type_remapper()
Gregory Gutermand1dd3b82020-03-02 21:53:481174 for name, config in waterfall['machines'].iteritems():
1175 tests = {}
1176 # Copy only well-understood entries in the machine's configuration
1177 # verbatim into the generated JSON.
1178 if 'additional_compile_targets' in config:
1179 tests['additional_compile_targets'] = config[
1180 'additional_compile_targets']
1181 for test_type, input_tests in config.get('test_suites', {}).iteritems():
1182 if test_type not in generator_map:
1183 raise self.unknown_test_suite_type(
1184 test_type, name, waterfall['name']) # pragma: no cover
1185 test_generator = generator_map[test_type]
1186 # Let multiple kinds of generators generate the same kinds
1187 # of tests. For example, gpu_telemetry_tests are a
1188 # specialization of isolated_scripts.
1189 new_tests = test_generator.generate(
1190 waterfall, name, config, input_tests)
1191 remapped_test_type = test_type_remapper.get(test_type, test_type)
1192 tests[remapped_test_type] = test_generator.sort(
1193 tests.get(remapped_test_type, []) + new_tests)
1194 all_tests[name] = tests
1195 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1196 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1197 return json.dumps(all_tests, indent=2, separators=(',', ': '),
1198 sort_keys=True) + '\n'
Kenneth Russelleb60cbd22017-12-05 07:54:281199
1200 def generate_waterfalls(self): # pragma: no cover
1201 self.load_configuration_files()
1202 self.resolve_configuration_files()
1203 filters = self.args.waterfall_filters
1204 suffix = '.json'
1205 if self.args.new_files:
1206 suffix = '.new' + suffix
1207 for waterfall in self.waterfalls:
Gregory Gutermand1dd3b82020-03-02 21:53:481208 should_gen = not filters or waterfall['name'] in filters
1209 if should_gen:
1210 file_path = waterfall['name'] + suffix
1211 self.write_file(self.pyl_file_path(file_path),
1212 self.generate_waterfall_json(waterfall))
Kenneth Russelleb60cbd22017-12-05 07:54:281213
Nico Weberd18b8962018-05-16 19:39:381214 def get_valid_bot_names(self):
John Budorick699282e2019-02-13 01:27:331215 # Extract bot names from infra/config/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421216 # NOTE: This reference can cause issues; if a file changes there, the
1217 # presubmit here won't be run by default. A manually maintained list there
1218 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1219 # references to configs outside of this directory are added, please change
1220 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1221 # never ends up in an invalid state.
Nico Weberd18b8962018-05-16 19:39:381222 bot_names = set()
John Budorickc12abd12018-08-14 19:37:431223 infra_config_dir = os.path.abspath(
1224 os.path.join(os.path.dirname(__file__),
John Budorick699282e2019-02-13 01:27:331225 '..', '..', 'infra', 'config'))
John Budorickc12abd12018-08-14 19:37:431226 milo_configs = [
Garrett Beatybb8322bf2019-10-17 20:53:051227 os.path.join(infra_config_dir, 'generated', 'luci-milo.cfg'),
Garrett Beatye95b81722019-10-24 17:12:181228 os.path.join(infra_config_dir, 'generated', 'luci-milo-dev.cfg'),
John Budorickc12abd12018-08-14 19:37:431229 ]
1230 for c in milo_configs:
1231 for l in self.read_file(c).splitlines():
1232 if (not 'name: "buildbucket/luci.chromium.' in l and
Hans Wennborg98ffd7d92019-02-06 14:14:341233 not 'name: "buildbucket/luci.chrome.' in l and
John Budorickb1833612018-12-07 04:36:411234 not 'name: "buildbot/chromium.' in l and
1235 not 'name: "buildbot/tryserver.chromium.' in l):
John Budorickc12abd12018-08-14 19:37:431236 continue
1237 # l looks like
1238 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1239 # Extract win_chromium_dbg_ng part.
1240 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381241 return bot_names
1242
Ben Pastene9a010082019-09-25 20:41:371243 def get_builders_that_do_not_actually_exist(self):
Kenneth Russell8a386d42018-06-02 09:48:011244 # Some of the bots on the chromium.gpu.fyi waterfall in particular
1245 # are defined only to be mirrored into trybots, and don't actually
1246 # exist on any of the waterfalls or consoles.
1247 return [
Michael Spangeb07eba62019-05-14 22:22:581248 'GPU FYI Fuchsia Builder',
Yuly Novikoveb26b812019-07-26 02:08:191249 'ANGLE GPU Android Release (Nexus 5X)',
Jamie Madillda894ce2019-04-08 17:19:171250 'ANGLE GPU Linux Release (Intel HD 630)',
1251 'ANGLE GPU Linux Release (NVIDIA)',
1252 'ANGLE GPU Mac Release (Intel)',
1253 'ANGLE GPU Mac Retina Release (AMD)',
1254 'ANGLE GPU Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491255 'ANGLE GPU Win10 x64 Release (Intel HD 630)',
1256 'ANGLE GPU Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011257 'Optional Android Release (Nexus 5X)',
1258 'Optional Linux Release (Intel HD 630)',
1259 'Optional Linux Release (NVIDIA)',
1260 'Optional Mac Release (Intel)',
1261 'Optional Mac Retina Release (AMD)',
1262 'Optional Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491263 'Optional Win10 x64 Release (Intel HD 630)',
1264 'Optional Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011265 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:081266 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:291267 'linux-blink-rel-dummy',
1268 'mac10.10-blink-rel-dummy',
1269 'mac10.11-blink-rel-dummy',
1270 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:201271 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291272 'mac10.13-blink-rel-dummy',
John Chenad978322019-12-16 18:07:211273 'mac10.14-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291274 'win7-blink-rel-dummy',
1275 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:081276 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:331277 'WebKit Linux composite_after_paint Dummy Builder',
Scott Violet744e04662019-08-19 23:51:531278 'WebKit Linux layout_ng_disabled Builder',
Stephen Martinis769b25112018-08-30 18:52:061279 # chromium, due to https://crbug.com/878915
1280 'win-dbg',
1281 'win32-dbg',
Stephen Martinis47d77132019-04-24 23:51:331282 'win-archive-dbg',
1283 'win32-archive-dbg',
Sajjad Mirza2924a012019-12-20 03:46:541284 # TODO(crbug.com/1033753) Delete these when coverage is enabled by default
1285 # on Windows tryjobs.
1286 'GPU Win x64 Builder Code Coverage',
1287 'Win x64 Builder Code Coverage',
1288 'Win10 Tests x64 Code Coverage',
1289 'Win10 x64 Release (NVIDIA) Code Coverage',
Sajjad Mirzafa15665e2020-02-10 23:41:041290 # TODO(crbug.com/1024915) Delete these when coverage is enabled by default
1291 # on Mac OS tryjobs.
1292 'Mac Builder Code Coverage',
1293 'Mac10.13 Tests Code Coverage',
1294 'GPU Mac Builder Code Coverage',
1295 'Mac Release (Intel) Code Coverage',
1296 'Mac Retina Release (AMD) Code Coverage',
Kenneth Russell8a386d42018-06-02 09:48:011297 ]
1298
Ben Pastene9a010082019-09-25 20:41:371299 def get_internal_waterfalls(self):
1300 # Similar to get_builders_that_do_not_actually_exist above, but for
1301 # waterfalls defined in internal configs.
1302 return ['chrome']
1303
Stephen Martinisf83893722018-09-19 00:02:181304 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201305 self.check_input_files_sorting(verbose)
1306
Kenneth Russelleb60cbd22017-12-05 07:54:281307 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011308 self.check_composition_type_test_suites('compound_suites')
Jeff Yoon67c3e832020-02-08 07:39:381309 self.check_composition_type_test_suites('matrix_compound_suites',
1310 [check_matrix_identifier])
Nodir Turakulovfce34292019-12-18 17:05:411311 self.resolve_full_test_targets()
Stephen Martinis54d64ad2018-09-21 22:16:201312 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381313
1314 # All bots should exist.
1315 bot_names = self.get_valid_bot_names()
Ben Pastene9a010082019-09-25 20:41:371316 internal_waterfalls = self.get_internal_waterfalls()
1317 builders_that_dont_exist = self.get_builders_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:381318 for waterfall in self.waterfalls:
Ben Pastene9a010082019-09-25 20:41:371319 # TODO(crbug.com/991417): Remove the need for this exception.
1320 if waterfall['name'] in internal_waterfalls:
1321 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381322 for bot_name in waterfall['machines']:
Ben Pastene9a010082019-09-25 20:41:371323 if bot_name in builders_that_dont_exist:
Kenneth Russell8a386d42018-06-02 09:48:011324 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381325 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:081326 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:381327 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:521328 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:321329 if waterfall['name'] in ['tryserver.webrtc',
1330 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:381331 # These waterfalls have their bot configs in a different repo.
1332 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:521333 continue # pragma: no cover
Jeff Yoon8154e582019-12-03 23:30:011334 if waterfall['name'] in ['client.devtools-frontend.integration',
Liviu Raud287b1f2020-01-14 07:30:331335 'tryserver.devtools-frontend',
1336 'chromium.devtools-frontend']:
Tamer Tas2c506412019-08-20 07:44:411337 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381338 raise self.unknown_bot(bot_name, waterfall['name'])
1339
Kenneth Russelleb60cbd22017-12-05 07:54:281340 # All test suites must be referenced.
1341 suites_seen = set()
1342 generator_map = self.get_test_generator_map()
1343 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431344 for bot_name, tester in waterfall['machines'].iteritems():
1345 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281346 if suite_type not in generator_map:
1347 raise self.unknown_test_suite_type(suite_type, bot_name,
1348 waterfall['name'])
1349 if suite not in self.test_suites:
1350 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1351 suites_seen.add(suite)
1352 # Since we didn't resolve the configuration files, this set
1353 # includes both composition test suites and regular ones.
1354 resolved_suites = set()
1355 for suite_name in suites_seen:
1356 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011357 for sub_suite in suite:
1358 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281359 resolved_suites.add(suite_name)
1360 # At this point, every key in test_suites.pyl should be referenced.
1361 missing_suites = set(self.test_suites.keys()) - resolved_suites
1362 if missing_suites:
1363 raise BBGenErr('The following test suites were unreferenced by bots on '
1364 'the waterfalls: ' + str(missing_suites))
1365
1366 # All test suite exceptions must refer to bots on the waterfall.
1367 all_bots = set()
1368 missing_bots = set()
1369 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431370 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281371 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281372 # In order to disambiguate between bots with the same name on
1373 # different waterfalls, support has been added to various
1374 # exceptions for concatenating the waterfall name after the bot
1375 # name.
1376 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281377 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381378 removals = (exception.get('remove_from', []) +
1379 exception.get('remove_gtest_from', []) +
1380 exception.get('modifications', {}).keys())
1381 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281382 if removal not in all_bots:
1383 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411384
Ben Pastene9a010082019-09-25 20:41:371385 missing_bots = missing_bots - set(builders_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281386 if missing_bots:
1387 raise BBGenErr('The following nonexistent machines were referenced in '
1388 'the test suite exceptions: ' + str(missing_bots))
1389
Stephen Martinis0382bc12018-09-17 22:29:071390 # All mixins must be referenced
1391 seen_mixins = set()
1392 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011393 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071394 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011395 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071396 for suite in self.test_suites.values():
1397 if isinstance(suite, list):
1398 # Don't care about this, it's a composition, which shouldn't include a
1399 # swarming mixin.
1400 continue
1401
1402 for test in suite.values():
1403 if not isinstance(test, dict):
1404 # Some test suites have top level keys, which currently can't be
1405 # swarming mixin entries. Ignore them
1406 continue
1407
Stephen Martinisb72f6d22018-10-04 23:29:011408 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071409
Stephen Martinisb72f6d22018-10-04 23:29:011410 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071411 if missing_mixins:
1412 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1413 ' referenced in a waterfall, machine, or test suite.' % (
1414 str(missing_mixins)))
1415
Jeff Yoonda581c32020-03-06 03:56:051416 # All variant references must be referenced
1417 seen_variants = set()
1418 for suite in self.test_suites.values():
1419 if isinstance(suite, list):
1420 continue
1421
1422 for test in suite.values():
1423 if isinstance(test, dict):
1424 for variant in test.get('variants', []):
1425 if isinstance(variant, str):
1426 seen_variants.add(variant)
1427
1428 missing_variants = set(self.variants.keys()) - seen_variants
1429 if missing_variants:
1430 raise BBGenErr('The following variants were unreferenced: %s. They must '
1431 'be referenced in a matrix test suite under the variants '
1432 'key.' % str(missing_variants))
1433
Stephen Martinis54d64ad2018-09-21 22:16:201434
1435 def type_assert(self, node, typ, filename, verbose=False):
1436 """Asserts that the Python AST node |node| is of type |typ|.
1437
1438 If verbose is set, it prints out some helpful context lines, showing where
1439 exactly the error occurred in the file.
1440 """
1441 if not isinstance(node, typ):
1442 if verbose:
1443 lines = [""] + self.read_file(filename).splitlines()
1444
1445 context = 2
1446 lines_start = max(node.lineno - context, 0)
1447 # Add one to include the last line
1448 lines_end = min(node.lineno + context, len(lines)) + 1
1449 lines = (
1450 ['== %s ==\n' % filename] +
1451 ["<snip>\n"] +
1452 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1453 lines[lines_start:lines_start + context])] +
1454 ['-' * 80 + '\n'] +
1455 ['%d %s' % (node.lineno, lines[node.lineno])] +
1456 ['-' * (node.col_offset + 3) + '^' + '-' * (
1457 80 - node.col_offset - 4) + '\n'] +
1458 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1459 lines[node.lineno + 1:lines_end])] +
1460 ["<snip>\n"]
1461 )
1462 # Print out a useful message when a type assertion fails.
1463 for l in lines:
1464 self.print_line(l.strip())
1465
1466 node_dumped = ast.dump(node, annotate_fields=False)
1467 # If the node is huge, truncate it so everything fits in a terminal
1468 # window.
1469 if len(node_dumped) > 60: # pragma: no cover
1470 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1471 raise BBGenErr(
1472 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1473 ' be %s, is %s' % (
1474 filename, node_dumped,
1475 node.lineno, typ, type(node)))
1476
Stephen Martinis5bef0fc2020-01-06 22:47:531477 def check_ast_list_formatted(self, keys, filename, verbose,
Stephen Martinis1384ff92020-01-07 19:52:151478 check_sorting=True):
Stephen Martinis5bef0fc2020-01-06 22:47:531479 """Checks if a list of ast keys are correctly formatted.
Stephen Martinis54d64ad2018-09-21 22:16:201480
Stephen Martinis5bef0fc2020-01-06 22:47:531481 Currently only checks to ensure they're correctly sorted, and that there
1482 are no duplicates.
1483
1484 Args:
1485 keys: An python list of AST nodes.
1486
1487 It's a list of AST nodes instead of a list of strings because
1488 when verbose is set, it tries to print out context of where the
1489 diffs are in the file.
1490 filename: The name of the file this node is from.
1491 verbose: If set, print out diff information about how the keys are
1492 incorrectly formatted.
1493 check_sorting: If true, checks if the list is sorted.
1494 Returns:
1495 If the keys are correctly formatted.
1496 """
1497 if not keys:
1498 return True
1499
1500 assert isinstance(keys[0], ast.Str)
1501
1502 keys_strs = [k.s for k in keys]
1503 # Keys to diff against. Used below.
1504 keys_to_diff_against = None
1505 # If the list is properly formatted.
1506 list_formatted = True
1507
1508 # Duplicates are always bad.
1509 if len(set(keys_strs)) != len(keys_strs):
1510 list_formatted = False
1511 keys_to_diff_against = list(collections.OrderedDict.fromkeys(keys_strs))
1512
1513 if check_sorting and sorted(keys_strs) != keys_strs:
1514 list_formatted = False
1515 if list_formatted:
1516 return True
1517
1518 if verbose:
1519 line_num = keys[0].lineno
1520 keys = [k.s for k in keys]
1521 if check_sorting:
1522 # If we have duplicates, sorting this will take care of it anyways.
1523 keys_to_diff_against = sorted(set(keys))
1524 # else, keys_to_diff_against is set above already
1525
1526 self.print_line('=' * 80)
1527 self.print_line('(First line of keys is %s)' % line_num)
1528 for line in difflib.context_diff(
1529 keys, keys_to_diff_against,
1530 fromfile='current (%r)' % filename, tofile='sorted', lineterm=''):
1531 self.print_line(line)
1532 self.print_line('=' * 80)
1533
1534 return False
1535
Stephen Martinis1384ff92020-01-07 19:52:151536 def check_ast_dict_formatted(self, node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531537 """Checks if an ast dictionary's keys are correctly formatted.
1538
1539 Just a simple wrapper around check_ast_list_formatted.
1540 Args:
1541 node: An AST node. Assumed to be a dictionary.
1542 filename: The name of the file this node is from.
1543 verbose: If set, print out diff information about how the keys are
1544 incorrectly formatted.
1545 check_sorting: If true, checks if the list is sorted.
1546 Returns:
1547 If the dictionary is correctly formatted.
1548 """
Stephen Martinis54d64ad2018-09-21 22:16:201549 keys = []
1550 # The keys of this dict are ordered as ordered in the file; normal python
1551 # dictionary keys are given an arbitrary order, but since we parsed the
1552 # file itself, the order as given in the file is preserved.
1553 for key in node.keys:
1554 self.type_assert(key, ast.Str, filename, verbose)
Stephen Martinis5bef0fc2020-01-06 22:47:531555 keys.append(key)
Stephen Martinis54d64ad2018-09-21 22:16:201556
Stephen Martinis1384ff92020-01-07 19:52:151557 return self.check_ast_list_formatted(keys, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181558
1559 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201560 # TODO(https://crbug.com/886993): Add the ability for this script to
1561 # actually format the files, rather than just complain if they're
1562 # incorrectly formatted.
1563 bad_files = set()
Stephen Martinis5bef0fc2020-01-06 22:47:531564 def parse_file(filename):
1565 """Parses and validates a .pyl file.
Stephen Martinis54d64ad2018-09-21 22:16:201566
Stephen Martinis5bef0fc2020-01-06 22:47:531567 Returns an AST node representing the value in the pyl file."""
Stephen Martinisf83893722018-09-19 00:02:181568 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1569
Stephen Martinisf83893722018-09-19 00:02:181570 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201571 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181572 module = parsed.body
1573
1574 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201575 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181576 if len(module) != 1: # pragma: no cover
1577 raise BBGenErr('Invalid .pyl file %s' % filename)
1578 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201579 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181580
Stephen Martinis5bef0fc2020-01-06 22:47:531581 return expr.value
1582
1583 # Handle this separately
1584 filename = 'waterfalls.pyl'
1585 value = parse_file(filename)
1586 # Value should be a list.
1587 self.type_assert(value, ast.List, filename, verbose)
1588
1589 keys = []
1590 for val in value.elts:
1591 self.type_assert(val, ast.Dict, filename, verbose)
1592 waterfall_name = None
1593 for key, val in zip(val.keys, val.values):
1594 self.type_assert(key, ast.Str, filename, verbose)
1595 if key.s == 'machines':
1596 if not self.check_ast_dict_formatted(val, filename, verbose):
1597 bad_files.add(filename)
1598
1599 if key.s == "name":
1600 self.type_assert(val, ast.Str, filename, verbose)
1601 waterfall_name = val
1602 assert waterfall_name
1603 keys.append(waterfall_name)
1604
Stephen Martinis1384ff92020-01-07 19:52:151605 if not self.check_ast_list_formatted(keys, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531606 bad_files.add(filename)
1607
1608 for filename in (
1609 'mixins.pyl',
1610 'test_suites.pyl',
1611 'test_suite_exceptions.pyl',
1612 ):
1613 value = parse_file(filename)
Stephen Martinisf83893722018-09-19 00:02:181614 # Value should be a dictionary.
Stephen Martinis54d64ad2018-09-21 22:16:201615 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181616
Stephen Martinis5bef0fc2020-01-06 22:47:531617 if not self.check_ast_dict_formatted(
1618 value, filename, verbose):
1619 bad_files.add(filename)
1620
Stephen Martinis54d64ad2018-09-21 22:16:201621 if filename == 'test_suites.pyl':
Jeff Yoon8154e582019-12-03 23:30:011622 expected_keys = ['basic_suites',
1623 'compound_suites',
1624 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201625 actual_keys = [node.s for node in value.keys]
1626 assert all(key in expected_keys for key in actual_keys), (
1627 'Invalid %r file; expected keys %r, got %r' % (
1628 filename, expected_keys, actual_keys))
1629 suite_dicts = [node for node in value.values]
1630 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011631 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201632 for suite_group in suite_dicts:
Stephen Martinis5bef0fc2020-01-06 22:47:531633 if not self.check_ast_dict_formatted(
Stephen Martinis54d64ad2018-09-21 22:16:201634 suite_group, filename, verbose):
1635 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181636
Stephen Martinis5bef0fc2020-01-06 22:47:531637 for key, suite in zip(value.keys, value.values):
1638 # The compound suites are checked in
1639 # 'check_composition_type_test_suites()'
1640 if key.s == 'basic_suites':
1641 for group in suite.values:
Stephen Martinis1384ff92020-01-07 19:52:151642 if not self.check_ast_dict_formatted(group, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531643 bad_files.add(filename)
1644 break
Stephen Martinis54d64ad2018-09-21 22:16:201645
Stephen Martinis5bef0fc2020-01-06 22:47:531646 elif filename == 'test_suite_exceptions.pyl':
1647 # Check the values for each test.
1648 for test in value.values:
1649 for kind, node in zip(test.keys, test.values):
1650 if isinstance(node, ast.Dict):
Stephen Martinis1384ff92020-01-07 19:52:151651 if not self.check_ast_dict_formatted(node, filename, verbose):
Stephen Martinis5bef0fc2020-01-06 22:47:531652 bad_files.add(filename)
1653 elif kind.s == 'remove_from':
1654 # Don't care about sorting; these are usually grouped, since the
1655 # same bug can affect multiple builders. Do want to make sure
1656 # there aren't duplicates.
1657 if not self.check_ast_list_formatted(node.elts, filename, verbose,
1658 check_sorting=False):
1659 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181660
1661 if bad_files:
1662 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201663 'The following files have invalid keys: %s\n. They are either '
Stephen Martinis5bef0fc2020-01-06 22:47:531664 'unsorted, or have duplicates. Re-run this with --verbose to see '
1665 'more details.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181666
Kenneth Russelleb60cbd22017-12-05 07:54:281667 def check_output_file_consistency(self, verbose=False):
1668 self.load_configuration_files()
1669 # All waterfalls must have been written by this script already.
1670 self.resolve_configuration_files()
1671 ungenerated_waterfalls = set()
1672 for waterfall in self.waterfalls:
Gregory Gutermand1dd3b82020-03-02 21:53:481673 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111674 file_path = waterfall['name'] + '.json'
1675 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281676 if expected != current:
1677 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321678 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501679 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281680 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321681 'contents:')
1682 for line in difflib.unified_diff(
1683 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501684 current.splitlines(),
1685 fromfile='expected', tofile='current'):
1686 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281687 if ungenerated_waterfalls:
1688 raise BBGenErr('The following waterfalls have not been properly '
1689 'autogenerated by generate_buildbot_json.py: ' +
1690 str(ungenerated_waterfalls))
1691
1692 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501693 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281694 self.check_output_file_consistency(verbose) # pragma: no cover
1695
1696 def parse_args(self, argv): # pragma: no cover
Karen Qiane24b7ee2019-02-12 23:37:061697
1698 # RawTextHelpFormatter allows for styling of help statement
1699 parser = argparse.ArgumentParser(formatter_class=
1700 argparse.RawTextHelpFormatter)
1701
1702 group = parser.add_mutually_exclusive_group()
1703 group.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281704 '-c', '--check', action='store_true', help=
1705 'Do consistency checks of configuration and generated files and then '
1706 'exit. Used during presubmit. Causes the tool to not generate any files.')
Karen Qiane24b7ee2019-02-12 23:37:061707 group.add_argument(
1708 '--query', type=str, help=
1709 ("Returns raw JSON information of buildbots and tests.\n" +
1710 "Examples:\n" +
1711 " List all bots (all info):\n" +
1712 " --query bots\n\n" +
1713 " List all bots and only their associated tests:\n" +
1714 " --query bots/tests\n\n" +
1715 " List all information about 'bot1' " +
1716 "(make sure you have quotes):\n" +
1717 " --query bot/'bot1'\n\n" +
1718 " List tests running for 'bot1' (make sure you have quotes):\n" +
1719 " --query bot/'bot1'/tests\n\n" +
1720 " List all tests:\n" +
1721 " --query tests\n\n" +
1722 " List all tests and the bots running them:\n" +
1723 " --query tests/bots\n\n"+
1724 " List all tests that satisfy multiple parameters\n" +
1725 " (separation of parameters by '&' symbol):\n" +
1726 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
1727 " List all tests that run with a specific flag:\n" +
1728 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
1729 " List specific test (make sure you have quotes):\n"
1730 " --query test/'test1'\n\n"
1731 " List all bots running 'test1' " +
1732 "(make sure you have quotes):\n" +
1733 " --query test/'test1'/bots" ))
Kenneth Russelleb60cbd22017-12-05 07:54:281734 parser.add_argument(
1735 '-n', '--new-files', action='store_true', help=
1736 'Write output files as .new.json. Useful during development so old and '
1737 'new files can be looked at side-by-side.')
1738 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501739 '-v', '--verbose', action='store_true', help=
1740 'Increases verbosity. Affects consistency checks.')
1741 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281742 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1743 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111744 parser.add_argument(
1745 '--pyl-files-dir', type=os.path.realpath,
1746 help='Path to the directory containing the input .pyl files.')
Karen Qiane24b7ee2019-02-12 23:37:061747 parser.add_argument(
1748 '--json', help=
1749 ("Outputs results into a json file. Only works with query function.\n" +
1750 "Examples:\n" +
1751 " Outputs file into specified json file: \n" +
1752 " --json <file-name-here.json>"))
Kenneth Russelleb60cbd22017-12-05 07:54:281753 self.args = parser.parse_args(argv)
Karen Qiane24b7ee2019-02-12 23:37:061754 if self.args.json and not self.args.query:
1755 parser.error("The --json flag can only be used with --query.")
1756
1757 def does_test_match(self, test_info, params_dict):
1758 """Checks to see if the test matches the parameters given.
1759
1760 Compares the provided test_info with the params_dict to see
1761 if the bot matches the parameters given. If so, returns True.
1762 Else, returns false.
1763
1764 Args:
1765 test_info (dict): Information about a specific bot provided
1766 in the format shown in waterfalls.pyl
1767 params_dict (dict): Dictionary of parameters and their values
1768 to look for in the bot
1769 Ex: {
1770 'device_os':'android',
1771 '--flag':True,
1772 'mixins': ['mixin1', 'mixin2'],
1773 'ex_key':'ex_value'
1774 }
1775
1776 """
1777 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
1778 'kvm', 'pool', 'integrity'] # dimension parameters
1779 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
1780 'can_use_on_swarming_builders']
1781 for param in params_dict:
1782 # if dimension parameter
1783 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
1784 if not 'swarming' in test_info:
1785 return False
1786 swarming = test_info['swarming']
1787 if param in SWARMING_PARAMS:
1788 if not param in swarming:
1789 return False
1790 if not str(swarming[param]) == params_dict[param]:
1791 return False
1792 else:
1793 if not 'dimension_sets' in swarming:
1794 return False
1795 d_set = swarming['dimension_sets']
1796 # only looking at the first dimension set
1797 if not param in d_set[0]:
1798 return False
1799 if not d_set[0][param] == params_dict[param]:
1800 return False
1801
1802 # if flag
1803 elif param.startswith('--'):
1804 if not 'args' in test_info:
1805 return False
1806 if not param in test_info['args']:
1807 return False
1808
1809 # not dimension parameter/flag/mixin
1810 else:
1811 if not param in test_info:
1812 return False
1813 if not test_info[param] == params_dict[param]:
1814 return False
1815 return True
1816 def error_msg(self, msg):
1817 """Prints an error message.
1818
1819 In addition to a catered error message, also prints
1820 out where the user can find more help. Then, program exits.
1821 """
1822 self.print_line(msg + (' If you need more information, ' +
1823 'please run with -h or --help to see valid commands.'))
1824 sys.exit(1)
1825
1826 def find_bots_that_run_test(self, test, bots):
1827 matching_bots = []
1828 for bot in bots:
1829 bot_info = bots[bot]
1830 tests = self.flatten_tests_for_bot(bot_info)
1831 for test_info in tests:
1832 test_name = ""
1833 if 'name' in test_info:
1834 test_name = test_info['name']
1835 elif 'test' in test_info:
1836 test_name = test_info['test']
1837 if not test_name == test:
1838 continue
1839 matching_bots.append(bot)
1840 return matching_bots
1841
1842 def find_tests_with_params(self, tests, params_dict):
1843 matching_tests = []
1844 for test_name in tests:
1845 test_info = tests[test_name]
1846 if not self.does_test_match(test_info, params_dict):
1847 continue
1848 if not test_name in matching_tests:
1849 matching_tests.append(test_name)
1850 return matching_tests
1851
1852 def flatten_waterfalls_for_query(self, waterfalls):
1853 bots = {}
1854 for waterfall in waterfalls:
Gregory Gutermand1dd3b82020-03-02 21:53:481855 waterfall_json = json.loads(self.generate_waterfall_json(waterfall))
1856 for bot in waterfall_json:
1857 bot_info = waterfall_json[bot]
Karen Qiane24b7ee2019-02-12 23:37:061858 if 'AAAAA' not in bot:
1859 bots[bot] = bot_info
1860 return bots
1861
1862 def flatten_tests_for_bot(self, bot_info):
1863 """Returns a list of flattened tests.
1864
1865 Returns a list of tests not grouped by test category
1866 for a specific bot.
1867 """
1868 TEST_CATS = self.get_test_generator_map().keys()
1869 tests = []
1870 for test_cat in TEST_CATS:
1871 if not test_cat in bot_info:
1872 continue
1873 test_cat_tests = bot_info[test_cat]
1874 tests = tests + test_cat_tests
1875 return tests
1876
1877 def flatten_tests_for_query(self, test_suites):
1878 """Returns a flattened dictionary of tests.
1879
1880 Returns a dictionary of tests associate with their
1881 configuration, not grouped by their test suite.
1882 """
1883 tests = {}
1884 for test_suite in test_suites.itervalues():
1885 for test in test_suite:
1886 test_info = test_suite[test]
1887 test_name = test
1888 if 'name' in test_info:
1889 test_name = test_info['name']
1890 tests[test_name] = test_info
1891 return tests
1892
1893 def parse_query_filter_params(self, params):
1894 """Parses the filter parameters.
1895
1896 Creates a dictionary from the parameters provided
1897 to filter the bot array.
1898 """
1899 params_dict = {}
1900 for p in params:
1901 # flag
1902 if p.startswith("--"):
1903 params_dict[p] = True
1904 else:
1905 pair = p.split(":")
1906 if len(pair) != 2:
1907 self.error_msg('Invalid command.')
1908 # regular parameters
1909 if pair[1].lower() == "true":
1910 params_dict[pair[0]] = True
1911 elif pair[1].lower() == "false":
1912 params_dict[pair[0]] = False
1913 else:
1914 params_dict[pair[0]] = pair[1]
1915 return params_dict
1916
1917 def get_test_suites_dict(self, bots):
1918 """Returns a dictionary of bots and their tests.
1919
1920 Returns a dictionary of bots and a list of their associated tests.
1921 """
1922 test_suite_dict = dict()
1923 for bot in bots:
1924 bot_info = bots[bot]
1925 tests = self.flatten_tests_for_bot(bot_info)
1926 test_suite_dict[bot] = tests
1927 return test_suite_dict
1928
1929 def output_query_result(self, result, json_file=None):
1930 """Outputs the result of the query.
1931
1932 If a json file parameter name is provided, then
1933 the result is output into the json file. If not,
1934 then the result is printed to the console.
1935 """
1936 output = json.dumps(result, indent=2)
1937 if json_file:
1938 self.write_file(json_file, output)
1939 else:
1940 self.print_line(output)
1941 return
1942
1943 def query(self, args):
1944 """Queries tests or bots.
1945
1946 Depending on the arguments provided, outputs a json of
1947 tests or bots matching the appropriate optional parameters provided.
1948 """
1949 # split up query statement
1950 query = args.query.split('/')
1951 self.load_configuration_files()
1952 self.resolve_configuration_files()
1953
1954 # flatten bots json
1955 tests = self.test_suites
1956 bots = self.flatten_waterfalls_for_query(self.waterfalls)
1957
1958 cmd_class = query[0]
1959
1960 # For queries starting with 'bots'
1961 if cmd_class == "bots":
1962 if len(query) == 1:
1963 return self.output_query_result(bots, args.json)
1964 # query with specific parameters
1965 elif len(query) == 2:
1966 if query[1] == 'tests':
1967 test_suites_dict = self.get_test_suites_dict(bots)
1968 return self.output_query_result(test_suites_dict, args.json)
1969 else:
1970 self.error_msg("This query should be in the format: bots/tests.")
1971
1972 else:
1973 self.error_msg("This query should have 0 or 1 '/', found %s instead."
1974 % str(len(query)-1))
1975
1976 # For queries starting with 'bot'
1977 elif cmd_class == "bot":
1978 if not len(query) == 2 and not len(query) == 3:
1979 self.error_msg("Command should have 1 or 2 '/', found %s instead."
1980 % str(len(query)-1))
1981 bot_id = query[1]
1982 if not bot_id in bots:
1983 self.error_msg("No bot named '" + bot_id + "' found.")
1984 bot_info = bots[bot_id]
1985 if len(query) == 2:
1986 return self.output_query_result(bot_info, args.json)
1987 if not query[2] == 'tests':
1988 self.error_msg("The query should be in the format:" +
1989 "bot/<bot-name>/tests.")
1990
1991 bot_tests = self.flatten_tests_for_bot(bot_info)
1992 return self.output_query_result(bot_tests, args.json)
1993
1994 # For queries starting with 'tests'
1995 elif cmd_class == "tests":
1996 if not len(query) == 1 and not len(query) == 2:
1997 self.error_msg("The query should have 0 or 1 '/', found %s instead."
1998 % str(len(query)-1))
1999 flattened_tests = self.flatten_tests_for_query(tests)
2000 if len(query) == 1:
2001 return self.output_query_result(flattened_tests, args.json)
2002
2003 # create params dict
2004 params = query[1].split('&')
2005 params_dict = self.parse_query_filter_params(params)
2006 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
2007 return self.output_query_result(matching_bots)
2008
2009 # For queries starting with 'test'
2010 elif cmd_class == "test":
2011 if not len(query) == 2 and not len(query) == 3:
2012 self.error_msg("The query should have 1 or 2 '/', found %s instead."
2013 % str(len(query)-1))
2014 test_id = query[1]
2015 if len(query) == 2:
2016 flattened_tests = self.flatten_tests_for_query(tests)
2017 for test in flattened_tests:
2018 if test == test_id:
2019 return self.output_query_result(flattened_tests[test], args.json)
2020 self.error_msg("There is no test named %s." % test_id)
2021 if not query[2] == 'bots':
2022 self.error_msg("The query should be in the format: " +
2023 "test/<test-name>/bots")
2024 bots_for_test = self.find_bots_that_run_test(test_id, bots)
2025 return self.output_query_result(bots_for_test)
2026
2027 else:
2028 self.error_msg("Your command did not match any valid commands." +
2029 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:282030
2031 def main(self, argv): # pragma: no cover
2032 self.parse_args(argv)
2033 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:502034 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:062035 elif self.args.query:
2036 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:282037 else:
2038 self.generate_waterfalls()
2039 return 0
2040
2041if __name__ == "__main__": # pragma: no cover
2042 generator = BBJSONGenerator()
John Budorick699282e2019-02-13 01:27:332043 sys.exit(generator.main(sys.argv[1:]))