blob: 410defc6efd030783c8cf0c51ce96605b5fd92ef [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()):
Nico Weber79dc5f6852018-07-13 19:38:4997 test = self.bb_gen.generate_gtest(
98 waterfall, tester_name, tester_config, test_name, test_config)
99 if test:
100 # generate_gtest may veto the test generation on this tester.
101 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28102 return gtests
103
104 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28105 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28106
107
108class IsolatedScriptTestGenerator(BaseGenerator):
109 def __init__(self, bb_gen):
110 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
111
Kenneth Russell8ceeabf2017-12-11 17:53:28112 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28113 isolated_scripts = []
114 for test_name, test_config in sorted(input_tests.iteritems()):
115 test = self.bb_gen.generate_isolated_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28116 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28117 if test:
118 isolated_scripts.append(test)
119 return isolated_scripts
120
121 def sort(self, tests):
122 return sorted(tests, key=lambda x: x['name'])
123
124
125class ScriptGenerator(BaseGenerator):
126 def __init__(self, bb_gen):
127 super(ScriptGenerator, self).__init__(bb_gen)
128
Kenneth Russell8ceeabf2017-12-11 17:53:28129 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28130 scripts = []
131 for test_name, test_config in sorted(input_tests.iteritems()):
132 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28133 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28134 if test:
135 scripts.append(test)
136 return scripts
137
138 def sort(self, tests):
139 return sorted(tests, key=lambda x: x['name'])
140
141
142class JUnitGenerator(BaseGenerator):
143 def __init__(self, bb_gen):
144 super(JUnitGenerator, self).__init__(bb_gen)
145
Kenneth Russell8ceeabf2017-12-11 17:53:28146 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28147 scripts = []
148 for test_name, test_config in sorted(input_tests.iteritems()):
149 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28150 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28151 if test:
152 scripts.append(test)
153 return scripts
154
155 def sort(self, tests):
156 return sorted(tests, key=lambda x: x['test'])
157
158
159class CTSGenerator(BaseGenerator):
160 def __init__(self, bb_gen):
161 super(CTSGenerator, self).__init__(bb_gen)
162
Kenneth Russell8ceeabf2017-12-11 17:53:28163 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28164 # These only contain one entry and it's the contents of the input tests'
165 # dictionary, verbatim.
166 cts_tests = []
167 cts_tests.append(input_tests)
168 return cts_tests
169
170 def sort(self, tests):
171 return tests
172
173
174class InstrumentationTestGenerator(BaseGenerator):
175 def __init__(self, bb_gen):
176 super(InstrumentationTestGenerator, self).__init__(bb_gen)
177
Kenneth Russell8ceeabf2017-12-11 17:53:28178 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28179 scripts = []
180 for test_name, test_config in sorted(input_tests.iteritems()):
181 test = self.bb_gen.generate_instrumentation_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28182 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28183 if test:
184 scripts.append(test)
185 return scripts
186
187 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28188 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28189
190
191class BBJSONGenerator(object):
192 def __init__(self):
193 self.this_dir = THIS_DIR
194 self.args = None
195 self.waterfalls = None
196 self.test_suites = None
197 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01198 self.mixins = None
Nodir Turakulovfce34292019-12-18 17:05:41199 self.gn_isolate_map = None
Kenneth Russelleb60cbd22017-12-05 07:54:28200
201 def generate_abs_file_path(self, relative_path):
202 return os.path.join(self.this_dir, relative_path) # pragma: no cover
203
Stephen Martinis7eb8b612018-09-21 00:17:50204 def print_line(self, line):
205 # Exists so that tests can mock
206 print line # pragma: no cover
207
Kenneth Russelleb60cbd22017-12-05 07:54:28208 def read_file(self, relative_path):
209 with open(self.generate_abs_file_path(
210 relative_path)) as fp: # pragma: no cover
211 return fp.read() # pragma: no cover
212
213 def write_file(self, relative_path, contents):
214 with open(self.generate_abs_file_path(
215 relative_path), 'wb') as fp: # pragma: no cover
216 fp.write(contents) # pragma: no cover
217
Zhiling Huangbe008172018-03-08 19:13:11218 def pyl_file_path(self, filename):
219 if self.args and self.args.pyl_files_dir:
220 return os.path.join(self.args.pyl_files_dir, filename)
221 return filename
222
Kenneth Russelleb60cbd22017-12-05 07:54:28223 def load_pyl_file(self, filename):
224 try:
Zhiling Huangbe008172018-03-08 19:13:11225 return ast.literal_eval(self.read_file(
226 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28227 except (SyntaxError, ValueError) as e: # pragma: no cover
228 raise BBGenErr('Failed to parse pyl file "%s": %s' %
229 (filename, e)) # pragma: no cover
230
Kenneth Russell8a386d42018-06-02 09:48:01231 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
232 # Currently it is only mandatory for bots which run GPU tests. Change these to
233 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28234 def is_android(self, tester_config):
235 return tester_config.get('os_type') == 'android'
236
Ben Pastenea9e583b2019-01-16 02:57:26237 def is_chromeos(self, tester_config):
238 return tester_config.get('os_type') == 'chromeos'
239
Kenneth Russell8a386d42018-06-02 09:48:01240 def is_linux(self, tester_config):
241 return tester_config.get('os_type') == 'linux'
242
Kai Ninomiya40de9f52019-10-18 21:38:49243 def is_mac(self, tester_config):
244 return tester_config.get('os_type') == 'mac'
245
246 def is_win(self, tester_config):
247 return tester_config.get('os_type') == 'win'
248
249 def is_win64(self, tester_config):
250 return (tester_config.get('os_type') == 'win' and
251 tester_config.get('browser_config') == 'release_x64')
252
Kenneth Russelleb60cbd22017-12-05 07:54:28253 def get_exception_for_test(self, test_name, test_config):
254 # gtests may have both "test" and "name" fields, and usually, if the "name"
255 # field is specified, it means that the same test is being repurposed
256 # multiple times with different command line arguments. To handle this case,
257 # prefer to lookup per the "name" field of the test itself, as opposed to
258 # the "test_name", which is actually the "test" field.
259 if 'name' in test_config:
260 return self.exceptions.get(test_config['name'])
261 else:
262 return self.exceptions.get(test_name)
263
Nico Weberb0b3f5862018-07-13 18:45:15264 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28265 # Currently, the only reason a test should not run on a given tester is that
266 # it's in the exceptions. (Once the GPU waterfall generation script is
267 # incorporated here, the rules will become more complex.)
268 exception = self.get_exception_for_test(test_name, test_config)
269 if not exception:
270 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28271 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28272 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28273 if remove_from:
274 if tester_name in remove_from:
275 return False
276 # TODO(kbr): this code path was added for some tests (including
277 # android_webview_unittests) on one machine (Nougat Phone
278 # Tester) which exists with the same name on two waterfalls,
279 # chromium.android and chromium.fyi; the tests are run on one
280 # but not the other. Once the bots are all uniquely named (a
281 # different ongoing project) this code should be removed.
282 # TODO(kbr): add coverage.
283 return (tester_name + ' ' + waterfall['name']
284 not in remove_from) # pragma: no cover
285 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28286
Nico Weber79dc5f6852018-07-13 19:38:49287 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28288 exception = self.get_exception_for_test(test_name, test)
289 if not exception:
290 return None
Nico Weber79dc5f6852018-07-13 19:38:49291 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28292
Brian Sheedye6ea0ee2019-07-11 02:54:37293 def get_test_replacements(self, test, test_name, tester_name):
294 exception = self.get_exception_for_test(test_name, test)
295 if not exception:
296 return None
297 return exception.get('replacements', {}).get(tester_name)
298
Kenneth Russell8a386d42018-06-02 09:48:01299 def merge_command_line_args(self, arr, prefix, splitter):
300 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01301 idx = 0
302 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01303 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01304 while idx < len(arr):
305 flag = arr[idx]
306 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01307 if flag.startswith(prefix):
308 arg = flag[prefix_len:]
309 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01310 if first_idx < 0:
311 first_idx = idx
312 else:
313 delete_current_entry = True
314 if delete_current_entry:
315 del arr[idx]
316 else:
317 idx += 1
318 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01319 arr[first_idx] = prefix + splitter.join(accumulated_args)
320 return arr
321
322 def maybe_fixup_args_array(self, arr):
323 # The incoming array of strings may be an array of command line
324 # arguments. To make it easier to turn on certain features per-bot or
325 # per-test-suite, look specifically for certain flags and merge them
326 # appropriately.
327 # --enable-features=Feature1 --enable-features=Feature2
328 # are merged to:
329 # --enable-features=Feature1,Feature2
330 # and:
331 # --extra-browser-args=arg1 --extra-browser-args=arg2
332 # are merged to:
333 # --extra-browser-args=arg1 arg2
334 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
335 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01336 return arr
337
Kenneth Russelleb60cbd22017-12-05 07:54:28338 def dictionary_merge(self, a, b, path=None, update=True):
339 """http://stackoverflow.com/questions/7204805/
340 python-dictionaries-of-dictionaries-merge
341 merges b into a
342 """
343 if path is None:
344 path = []
345 for key in b:
346 if key in a:
347 if isinstance(a[key], dict) and isinstance(b[key], dict):
348 self.dictionary_merge(a[key], b[key], path + [str(key)])
349 elif a[key] == b[key]:
350 pass # same leaf value
351 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06352 # Args arrays are lists of strings. Just concatenate them,
353 # and don't sort them, in order to keep some needed
354 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28355 if all(isinstance(x, str)
356 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01357 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28358 else:
359 # TODO(kbr): this only works properly if the two arrays are
360 # the same length, which is currently always the case in the
361 # swarming dimension_sets that we have to merge. It will fail
362 # to merge / override 'args' arrays which are different
363 # length.
364 for idx in xrange(len(b[key])):
365 try:
366 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
367 path + [str(key), str(idx)],
368 update=update)
Jeff Yoon8154e582019-12-03 23:30:01369 except (IndexError, TypeError):
370 raise BBGenErr('Error merging lists by key "%s" from source %s '
371 'into target %s at index %s. Verify target list '
372 'length is equal or greater than source'
373 % (str(key), str(b), str(a), str(idx)))
John Budorick5bc387fe2019-05-09 20:02:53374 elif update:
375 if b[key] is None:
376 del a[key]
377 else:
378 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28379 else:
380 raise BBGenErr('Conflict at %s' % '.'.join(
381 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53382 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28383 a[key] = b[key]
384 return a
385
John Budorickab108712018-09-01 00:12:21386 def initialize_args_for_test(
387 self, generated_test, tester_config, additional_arg_keys=None):
388
389 args = []
390 args.extend(generated_test.get('args', []))
391 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22392
Kenneth Russell8a386d42018-06-02 09:48:01393 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21394 val = generated_test.pop(key, [])
395 if fn(tester_config):
396 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01397
398 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
399 add_conditional_args('linux_args', self.is_linux)
400 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36401 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49402 add_conditional_args('mac_args', self.is_mac)
403 add_conditional_args('win_args', self.is_win)
404 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01405
John Budorickab108712018-09-01 00:12:21406 for key in additional_arg_keys or []:
407 args.extend(generated_test.pop(key, []))
408 args.extend(tester_config.get(key, []))
409
410 if args:
411 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01412
Kenneth Russelleb60cbd22017-12-05 07:54:28413 def initialize_swarming_dictionary_for_test(self, generated_test,
414 tester_config):
415 if 'swarming' not in generated_test:
416 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28417 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
418 generated_test['swarming'].update({
419 'can_use_on_swarming_builders': tester_config.get('use_swarming', True)
420 })
Kenneth Russelleb60cbd22017-12-05 07:54:28421 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03422 if ('dimension_sets' not in generated_test['swarming'] and
423 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28424 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
425 tester_config['swarming']['dimension_sets'])
426 self.dictionary_merge(generated_test['swarming'],
427 tester_config['swarming'])
428 # Apply any Android-specific Swarming dimensions after the generic ones.
429 if 'android_swarming' in generated_test:
430 if self.is_android(tester_config): # pragma: no cover
431 self.dictionary_merge(
432 generated_test['swarming'],
433 generated_test['android_swarming']) # pragma: no cover
434 del generated_test['android_swarming'] # pragma: no cover
435
436 def clean_swarming_dictionary(self, swarming_dict):
437 # Clean out redundant entries from a test's "swarming" dictionary.
438 # This is really only needed to retain 100% parity with the
439 # handwritten JSON files, and can be removed once all the files are
440 # autogenerated.
441 if 'shards' in swarming_dict:
442 if swarming_dict['shards'] == 1: # pragma: no cover
443 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24444 if 'hard_timeout' in swarming_dict:
445 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
446 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43447 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28448 # Remove all other keys.
449 for k in swarming_dict.keys(): # pragma: no cover
450 if k != 'can_use_on_swarming_builders': # pragma: no cover
451 del swarming_dict[k] # pragma: no cover
452
Stephen Martinis0382bc12018-09-17 22:29:07453 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
454 waterfall):
455 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01456 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07457 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28458 # See if there are any exceptions that need to be merged into this
459 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49460 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28461 if modifications:
462 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23463 if 'swarming' in test:
464 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28465 # Ensure all Android Swarming tests run only on userdebug builds if another
466 # build type was not specified.
467 if 'swarming' in test and self.is_android(tester_config):
468 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22469 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28470 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37471 self.replace_test_args(test, test_name, tester_name)
Ben Pastenee012aea42019-05-14 22:32:28472
Kenneth Russelleb60cbd22017-12-05 07:54:28473 return test
474
Brian Sheedye6ea0ee2019-07-11 02:54:37475 def replace_test_args(self, test, test_name, tester_name):
476 replacements = self.get_test_replacements(
477 test, test_name, tester_name) or {}
478 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
479 for key, replacement_dict in replacements.iteritems():
480 if key not in valid_replacement_keys:
481 raise BBGenErr(
482 'Given replacement key %s for %s on %s is not in the list of valid '
483 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
484 for replacement_key, replacement_val in replacement_dict.iteritems():
485 found_key = False
486 for i, test_key in enumerate(test.get(key, [])):
487 # Handle both the key/value being replaced being defined as two
488 # separate items or as key=value.
489 if test_key == replacement_key:
490 found_key = True
491 # Handle flags without values.
492 if replacement_val == None:
493 del test[key][i]
494 else:
495 test[key][i+1] = replacement_val
496 break
497 elif test_key.startswith(replacement_key + '='):
498 found_key = True
499 if replacement_val == None:
500 del test[key][i]
501 else:
502 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
503 break
504 if not found_key:
505 raise BBGenErr('Could not find %s in existing list of values for key '
506 '%s in %s on %s' % (replacement_key, key, test_name,
507 tester_name))
508
Shenghua Zhangaba8bad2018-02-07 02:12:09509 def add_common_test_properties(self, test, tester_config):
510 if tester_config.get('use_multi_dimension_trigger_script'):
Kenneth Russell73c3bd8b2018-10-19 22:30:19511 # Assumes update_and_cleanup_test has already been called, so the
512 # builder's mixins have been flattened into the test.
Shenghua Zhangaba8bad2018-02-07 02:12:09513 test['trigger_script'] = {
514 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
515 'args': [
516 '--multiple-trigger-configs',
Kenneth Russell73c3bd8b2018-10-19 22:30:19517 json.dumps(test['swarming']['dimension_sets'] +
Shenghua Zhangaba8bad2018-02-07 02:12:09518 tester_config.get('alternate_swarming_dimensions', [])),
519 '--multiple-dimension-script-verbose',
520 'True'
521 ],
522 }
Ben Pastenea9e583b2019-01-16 02:57:26523 elif self.is_chromeos(tester_config) and tester_config.get('use_swarming',
524 True):
525 # The presence of the "device_type" dimension indicates that the tests
526 # are targetting CrOS hardware and so need the special trigger script.
527 dimension_sets = tester_config['swarming']['dimension_sets']
528 if all('device_type' in ds for ds in dimension_sets):
529 test['trigger_script'] = {
530 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
531 }
Shenghua Zhangaba8bad2018-02-07 02:12:09532
Ben Pastene858f4be2019-01-09 23:52:09533 def add_android_presentation_args(self, tester_config, test_name, result):
534 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38535 bucket = tester_config.get('results_bucket', 'chromium-result-details')
536 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09537 if (result['swarming']['can_use_on_swarming_builders'] and not
538 tester_config.get('skip_merge_script', False)):
539 result['merge'] = {
540 'args': [
541 '--bucket',
John Budorick262ae112019-07-12 19:24:38542 bucket,
Ben Pastene858f4be2019-01-09 23:52:09543 '--test-name',
544 test_name
545 ],
546 'script': '//build/android/pylib/results/presentation/'
547 'test_results_presentation.py',
548 }
549 if not tester_config.get('skip_cipd_packages', False):
Ben Pastenee5949ea82019-01-10 21:45:26550 cipd_packages = result['swarming'].get('cipd_packages', [])
551 cipd_packages.append(
Ben Pastene858f4be2019-01-09 23:52:09552 {
553 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
554 'location': 'bin',
555 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
556 }
Ben Pastenee5949ea82019-01-10 21:45:26557 )
558 result['swarming']['cipd_packages'] = cipd_packages
Ben Pastene858f4be2019-01-09 23:52:09559 if not tester_config.get('skip_output_links', False):
560 result['swarming']['output_links'] = [
561 {
562 'link': [
563 'https://luci-logdog.appspot.com/v/?s',
564 '=android%2Fswarming%2Flogcats%2F',
565 '${TASK_ID}%2F%2B%2Funified_logcats',
566 ],
567 'name': 'shard #${SHARD_INDEX} logcats',
568 },
569 ]
570 if args:
571 result['args'] = args
572
Kenneth Russelleb60cbd22017-12-05 07:54:28573 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
574 test_config):
575 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15576 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28577 return None
578 result = copy.deepcopy(test_config)
579 if 'test' in result:
580 result['name'] = test_name
581 else:
582 result['test'] = test_name
583 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21584
585 self.initialize_args_for_test(
586 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28587 if self.is_android(tester_config) and tester_config.get('use_swarming',
588 True):
Ben Pastene858f4be2019-01-09 23:52:09589 self.add_android_presentation_args(tester_config, test_name, result)
590 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42591
Stephen Martinis0382bc12018-09-17 22:29:07592 result = self.update_and_cleanup_test(
593 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09594 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43595
596 if not result.get('merge'):
597 # TODO(https://crbug.com/958376): Consider adding the ability to not have
598 # this default.
599 result['merge'] = {
600 'script': '//testing/merge_scripts/standard_gtest_merge.py',
601 'args': [],
602 }
Kenneth Russelleb60cbd22017-12-05 07:54:28603 return result
604
605 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
606 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01607 if not self.should_run_on_tester(waterfall, tester_name, test_name,
608 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28609 return None
610 result = copy.deepcopy(test_config)
611 result['isolate_name'] = result.get('isolate_name', test_name)
612 result['name'] = test_name
613 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01614 self.initialize_args_for_test(result, tester_config)
Ben Pastene858f4be2019-01-09 23:52:09615 if tester_config.get('use_android_presentation', False):
616 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07617 result = self.update_and_cleanup_test(
618 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09619 self.add_common_test_properties(result, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17620
621 if not result.get('merge'):
622 # TODO(https://crbug.com/958376): Consider adding the ability to not have
623 # this default.
624 result['merge'] = {
625 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
626 'args': [],
627 }
Kenneth Russelleb60cbd22017-12-05 07:54:28628 return result
629
630 def generate_script_test(self, waterfall, tester_name, tester_config,
631 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44632 # TODO(https://crbug.com/953072): Remove this check whenever a better
633 # long-term solution is implemented.
634 if (waterfall.get('forbid_script_tests', False) or
635 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
636 raise BBGenErr('Attempted to generate a script test on tester ' +
637 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01638 if not self.should_run_on_tester(waterfall, tester_name, test_name,
639 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28640 return None
641 result = {
642 'name': test_name,
643 'script': test_config['script']
644 }
Stephen Martinis0382bc12018-09-17 22:29:07645 result = self.update_and_cleanup_test(
646 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28647 return result
648
649 def generate_junit_test(self, waterfall, tester_name, tester_config,
650 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01651 if not self.should_run_on_tester(waterfall, tester_name, test_name,
652 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28653 return None
John Budorickdef6acb2019-09-17 22:51:09654 result = copy.deepcopy(test_config)
655 result.update({
John Budorickcadc4952019-09-16 23:51:37656 'name': test_name,
657 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09658 })
659 self.initialize_args_for_test(result, tester_config)
660 result = self.update_and_cleanup_test(
661 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28662 return result
663
664 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
665 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01666 if not self.should_run_on_tester(waterfall, tester_name, test_name,
667 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28668 return None
669 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28670 if 'test' in result and result['test'] != test_name:
671 result['name'] = test_name
672 else:
673 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07674 result = self.update_and_cleanup_test(
675 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28676 return result
677
Stephen Martinis2a0667022018-09-25 22:31:14678 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01679 substitutions = {
680 # Any machine in waterfalls.pyl which desires to run GPU tests
681 # must provide the os_type key.
682 'os_type': tester_config['os_type'],
683 'gpu_vendor_id': '0',
684 'gpu_device_id': '0',
685 }
Stephen Martinis2a0667022018-09-25 22:31:14686 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01687 if 'gpu' in dimension_set:
688 # First remove the driver version, then split into vendor and device.
689 gpu = dimension_set['gpu']
Kenneth Russell384a1732019-03-16 02:36:02690 # Handle certain specialized named GPUs.
691 if gpu.startswith('nvidia-quadro-p400'):
692 gpu = ['10de', '1cb3']
693 elif gpu.startswith('intel-hd-630'):
694 gpu = ['8086', '5912']
Brian Sheedyf9387db7b2019-08-05 19:26:10695 elif gpu.startswith('intel-uhd-630'):
696 gpu = ['8086', '3e92']
Kenneth Russell384a1732019-03-16 02:36:02697 else:
698 gpu = gpu.split('-')[0].split(':')
Kenneth Russell8a386d42018-06-02 09:48:01699 substitutions['gpu_vendor_id'] = gpu[0]
700 substitutions['gpu_device_id'] = gpu[1]
701 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
702
703 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56704 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01705 # These are all just specializations of isolated script tests with
706 # a bunch of boilerplate command line arguments added.
707
708 # The step name must end in 'test' or 'tests' in order for the
709 # results to automatically show up on the flakiness dashboard.
710 # (At least, this was true some time ago.) Continue to use this
711 # naming convention for the time being to minimize changes.
712 step_name = test_config.get('name', test_name)
713 if not (step_name.endswith('test') or step_name.endswith('tests')):
714 step_name = '%s_tests' % step_name
715 result = self.generate_isolated_script_test(
716 waterfall, tester_name, tester_config, step_name, test_config)
717 if not result:
718 return None
719 result['isolate_name'] = 'telemetry_gpu_integration_test'
720 args = result.get('args', [])
721 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14722
723 # These tests upload and download results from cloud storage and therefore
724 # aren't idempotent yet. https://crbug.com/549140.
725 result['swarming']['idempotent'] = False
726
Kenneth Russell44910c32018-12-03 23:35:11727 # The GPU tests act much like integration tests for the entire browser, and
728 # tend to uncover flakiness bugs more readily than other test suites. In
729 # order to surface any flakiness more readily to the developer of the CL
730 # which is introducing it, we disable retries with patch on the commit
731 # queue.
732 result['should_retry_with_patch'] = False
733
Bo Liu555a0f92019-03-29 12:11:56734 browser = ('android-webview-instrumentation'
735 if is_android_webview else tester_config['browser_config'])
Kenneth Russell8a386d42018-06-02 09:48:01736 args = [
Bo Liu555a0f92019-03-29 12:11:56737 test_to_run,
738 '--show-stdout',
739 '--browser=%s' % browser,
740 # --passthrough displays more of the logging in Telemetry when
741 # run via typ, in particular some of the warnings about tests
742 # being expected to fail, but passing.
743 '--passthrough',
744 '-v',
745 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
Kenneth Russell8a386d42018-06-02 09:48:01746 ] + args
747 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14748 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01749 return result
750
Kenneth Russelleb60cbd22017-12-05 07:54:28751 def get_test_generator_map(self):
752 return {
Bo Liu555a0f92019-03-29 12:11:56753 'android_webview_gpu_telemetry_tests':
754 GPUTelemetryTestGenerator(self, is_android_webview=True),
755 'cts_tests':
756 CTSGenerator(self),
757 'gpu_telemetry_tests':
758 GPUTelemetryTestGenerator(self),
759 'gtest_tests':
760 GTestGenerator(self),
761 'instrumentation_tests':
762 InstrumentationTestGenerator(self),
763 'isolated_scripts':
764 IsolatedScriptTestGenerator(self),
765 'junit_tests':
766 JUnitGenerator(self),
767 'scripts':
768 ScriptGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28769 }
770
Kenneth Russell8a386d42018-06-02 09:48:01771 def get_test_type_remapper(self):
772 return {
773 # These are a specialization of isolated_scripts with a bunch of
774 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:56775 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:01776 'gpu_telemetry_tests': 'isolated_scripts',
777 }
778
Jeff Yoon8154e582019-12-03 23:30:01779 def check_composition_type_test_suites(self, test_type):
Kenneth Russelleb60cbd22017-12-05 07:54:28780 # Pre-pass to catch errors reliably.
Jeff Yoon8154e582019-12-03 23:30:01781 target_test_suites = self.test_suites.get(test_type, {})
782 # This check is used by both matrix and composition test suites.
783 # Neither can reference each other nor themselves, so we switch depending
784 # on the type that's being checked.
785 if test_type == 'matrix_compound_suites':
786 other_type = 'compound_suites'
787 else:
788 other_type = 'matrix_compound_suites'
789 other_test_suites = self.test_suites.get(other_type, {})
790 basic_suites = self.test_suites.get('basic_suites', {})
791
792 for suite, suite_def in target_test_suites.iteritems():
793 if suite in basic_suites:
794 raise BBGenErr('%s names may not duplicate basic test suite names '
795 '(error found while processsing %s)'
796 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:01797 seen_tests = {} # Maps a test to a test definition.
Jeff Yoon8154e582019-12-03 23:30:01798 for sub_suite in suite_def:
799 if sub_suite in other_test_suites or sub_suite in target_test_suites:
800 raise BBGenErr('%s may not refer to other composition type test '
801 'suites (error found while processing %s)'
802 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:01803 if sub_suite not in basic_suites:
Jeff Yoon8154e582019-12-03 23:30:01804 raise BBGenErr('Unable to find reference to %s while processing %s'
805 % (sub_suite, suite))
Nodir Turakulov28232afd2019-12-17 18:02:01806
807 # Ensure that if a test is reachable via multiple basic suites,
808 # all of them have an identical definition of the test.
809 for test_name in basic_suites[sub_suite]:
810 if (test_name in seen_tests and
811 basic_suites[sub_suite][test_name] !=
812 basic_suites[seen_tests[test_name]][test_name]):
813 raise BBGenErr('Conflicting test definitions for %s from %s '
814 'and %s in %s (error found while processing %s)'
815 % (test_name, seen_tests[test_name], sub_suite,
816 test_type, suite))
817 seen_tests[test_name] = sub_suite
Kenneth Russelleb60cbd22017-12-05 07:54:28818
Stephen Martinis54d64ad2018-09-21 22:16:20819 def flatten_test_suites(self):
820 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:01821 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
822 for category in test_types:
823 for name, value in self.test_suites.get(category, {}).iteritems():
824 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:20825 self.test_suites = new_test_suites
826
Nodir Turakulovfce34292019-12-18 17:05:41827 def resolve_full_test_targets(self):
828 for suite in self.test_suites['basic_suites'].itervalues():
829 for key, test in suite.iteritems():
830 if not isinstance(test, dict):
831 # Some test definitions are just strings, such as CTS.
832 # Skip them.
833 continue
834
835 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
836 # https://source.chromium.org/chromium/chromium/tools/build/+/master:scripts/slave/recipe_modules/chromium_tests/generators.py;l=89;drc=14c062ba0eb418d3c4623dde41a753241b9df06b
837 # TODO(crbug.com/1035124): clean this up.
838 isolate_name = test.get('test') or test.get('isolate_name') or key
839 gn_entry = self.gn_isolate_map.get(isolate_name)
840 if gn_entry:
841 test['test_target'] = gn_entry['label']
842 else: # pragma: no cover
843 # Some tests do not have an entry gn_isolate_map.pyl, such as
844 # telemetry tests.
845 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
846 pass
847
Kenneth Russelleb60cbd22017-12-05 07:54:28848 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:01849 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:20850
Jeff Yoon8154e582019-12-03 23:30:01851 compound_suites = self.test_suites.get('compound_suites', {})
852 # check_composition_type_test_suites() checks that all basic suites
853 # referenced by compound suites exist.
854 basic_suites = self.test_suites.get('basic_suites')
855
856 for name, value in compound_suites.iteritems():
857 # Resolve this to a dictionary.
858 full_suite = {}
859 for entry in value:
860 suite = basic_suites[entry]
861 full_suite.update(suite)
862 compound_suites[name] = full_suite
863
864 def resolve_matrix_compound_test_suites(self):
865 self.check_composition_type_test_suites('matrix_compound_suites')
866
867 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
868 # check_composition_type_test_suites() checks that all basic suites
869 # referenced by matrix suites exist.
870 basic_suites = self.test_suites.get('basic_suites')
871
872 for name, value in matrix_compound_suites.iteritems():
873 # Resolve this to a dictionary.
874 full_suite = {}
875 for test_suite_name, test_suite_config in value.iteritems():
876 swarming_defined = bool(
877 'swarming' in test_suite_config
878 and 'dimension_sets' in test_suite_config['swarming'])
879 test = copy.deepcopy(basic_suites[test_suite_name])
880 for test_config in test.values():
881 if (swarming_defined
882 and 'swarming' in test_config
883 and 'dimension_sets' in test_config['swarming']):
884 self.dictionary_merge(test_config['swarming'],
885 test_suite_config['swarming'])
886 else:
887 test_config.update(test_suite_config)
888 full_suite.update(test)
889 matrix_compound_suites[name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:28890
891 def link_waterfalls_to_test_suites(self):
892 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43893 for tester_name, tester in waterfall['machines'].iteritems():
894 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28895 if not value in self.test_suites:
896 # Hard / impossible to cover this in the unit test.
897 raise self.unknown_test_suite(
898 value, tester_name, waterfall['name']) # pragma: no cover
899 tester['test_suites'][suite] = self.test_suites[value]
900
901 def load_configuration_files(self):
902 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
903 self.test_suites = self.load_pyl_file('test_suites.pyl')
904 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:01905 self.mixins = self.load_pyl_file('mixins.pyl')
Nodir Turakulovfce34292019-12-18 17:05:41906 self.gn_isolate_map = self.load_pyl_file('gn_isolate_map.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:28907
908 def resolve_configuration_files(self):
Nodir Turakulovfce34292019-12-18 17:05:41909 self.resolve_full_test_targets()
Kenneth Russelleb60cbd22017-12-05 07:54:28910 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:01911 self.resolve_matrix_compound_test_suites()
912 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:28913 self.link_waterfalls_to_test_suites()
914
Nico Weberd18b8962018-05-16 19:39:38915 def unknown_bot(self, bot_name, waterfall_name):
916 return BBGenErr(
917 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
918
Kenneth Russelleb60cbd22017-12-05 07:54:28919 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
920 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38921 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28922 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
923
924 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
925 return BBGenErr(
926 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
927 ' on waterfall ' + waterfall_name)
928
Stephen Martinisb72f6d22018-10-04 23:29:01929 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:07930 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:32931
932 Checks in the waterfall, builder, and test objects for mixins.
933 """
934 def valid_mixin(mixin_name):
935 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:01936 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:32937 raise BBGenErr("bad mixin %s" % mixin_name)
938 def must_be_list(mixins, typ, name):
939 """Asserts that given mixins are a list."""
940 if not isinstance(mixins, list):
941 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
942
Stephen Martinisb72f6d22018-10-04 23:29:01943 if 'mixins' in waterfall:
944 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
945 for mixin in waterfall['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32946 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01947 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32948
Stephen Martinisb72f6d22018-10-04 23:29:01949 if 'mixins' in builder:
950 must_be_list(builder['mixins'], 'builder', builder_name)
951 for mixin in builder['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32952 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01953 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32954
Stephen Martinisb72f6d22018-10-04 23:29:01955 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:07956 return test
957
Stephen Martinis2a0667022018-09-25 22:31:14958 test_name = test.get('name')
959 if not test_name:
960 test_name = test.get('test')
961 if not test_name: # pragma: no cover
962 # Not the best name, but we should say something.
963 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:01964 must_be_list(test['mixins'], 'test', test_name)
965 for mixin in test['mixins']:
Stephen Martinis0382bc12018-09-17 22:29:07966 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01967 test = self.apply_mixin(self.mixins[mixin], test)
968 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:07969 return test
Stephen Martinisb6a50492018-09-12 23:59:32970
Stephen Martinisb72f6d22018-10-04 23:29:01971 def apply_mixin(self, mixin, test):
972 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:32973
Stephen Martinis0382bc12018-09-17 22:29:07974 Mixins will not override an existing key. This is to ensure exceptions can
975 override a setting a mixin applies.
976
Stephen Martinisb72f6d22018-10-04 23:29:01977 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:32978 'dimension_sets', which is how normal test suites specify their dimensions,
979 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
980 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:01981
Stephen Martinisb6a50492018-09-12 23:59:32982 """
983 new_test = copy.deepcopy(test)
984 mixin = copy.deepcopy(mixin)
985
Stephen Martinisb72f6d22018-10-04 23:29:01986 if 'swarming' in mixin:
987 swarming_mixin = mixin['swarming']
988 new_test.setdefault('swarming', {})
989 if 'dimensions' in swarming_mixin:
990 new_test['swarming'].setdefault('dimension_sets', [{}])
991 for dimension_set in new_test['swarming']['dimension_sets']:
992 dimension_set.update(swarming_mixin['dimensions'])
993 del swarming_mixin['dimensions']
Stephen Martinisb6a50492018-09-12 23:59:32994
Stephen Martinisb72f6d22018-10-04 23:29:01995 # python dict update doesn't do recursion at all. Just hard code the
996 # nested update we need (mixin['swarming'] shouldn't clobber
997 # test['swarming'], but should update it).
998 new_test['swarming'].update(swarming_mixin)
999 del mixin['swarming']
1000
Wezc0e835b702018-10-30 00:38:411001 if '$mixin_append' in mixin:
1002 # Values specified under $mixin_append should be appended to existing
1003 # lists, rather than replacing them.
1004 mixin_append = mixin['$mixin_append']
1005 for key in mixin_append:
1006 new_test.setdefault(key, [])
1007 if not isinstance(mixin_append[key], list):
1008 raise BBGenErr(
1009 'Key "' + key + '" in $mixin_append must be a list.')
1010 if not isinstance(new_test[key], list):
1011 raise BBGenErr(
1012 'Cannot apply $mixin_append to non-list "' + key + '".')
1013 new_test[key].extend(mixin_append[key])
1014 if 'args' in mixin_append:
1015 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1016 del mixin['$mixin_append']
1017
Stephen Martinisb72f6d22018-10-04 23:29:011018 new_test.update(mixin)
Stephen Martinis0382bc12018-09-17 22:29:071019
Stephen Martinisb6a50492018-09-12 23:59:321020 return new_test
1021
Kenneth Russelleb60cbd22017-12-05 07:54:281022 def generate_waterfall_json(self, waterfall):
1023 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:281024 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:011025 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:431026 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281027 tests = {}
Kenneth Russell139f8642017-12-05 08:51:431028 # Copy only well-understood entries in the machine's configuration
1029 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:281030 if 'additional_compile_targets' in config:
1031 tests['additional_compile_targets'] = config[
1032 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:431033 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281034 if test_type not in generator_map:
1035 raise self.unknown_test_suite_type(
1036 test_type, name, waterfall['name']) # pragma: no cover
1037 test_generator = generator_map[test_type]
Nico Weber79dc5f6852018-07-13 19:38:491038 # Let multiple kinds of generators generate the same kinds
1039 # of tests. For example, gpu_telemetry_tests are a
1040 # specialization of isolated_scripts.
1041 new_tests = test_generator.generate(
1042 waterfall, name, config, input_tests)
1043 remapped_test_type = test_type_remapper.get(test_type, test_type)
1044 tests[remapped_test_type] = test_generator.sort(
1045 tests.get(remapped_test_type, []) + new_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:281046 all_tests[name] = tests
1047 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1048 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1049 return json.dumps(all_tests, indent=2, separators=(',', ': '),
1050 sort_keys=True) + '\n'
1051
1052 def generate_waterfalls(self): # pragma: no cover
1053 self.load_configuration_files()
1054 self.resolve_configuration_files()
1055 filters = self.args.waterfall_filters
1056 suffix = '.json'
1057 if self.args.new_files:
1058 suffix = '.new' + suffix
1059 for waterfall in self.waterfalls:
1060 should_gen = not filters or waterfall['name'] in filters
1061 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:111062 file_path = waterfall['name'] + suffix
1063 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:281064 self.generate_waterfall_json(waterfall))
1065
Nico Weberd18b8962018-05-16 19:39:381066 def get_valid_bot_names(self):
John Budorick699282e2019-02-13 01:27:331067 # Extract bot names from infra/config/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421068 # NOTE: This reference can cause issues; if a file changes there, the
1069 # presubmit here won't be run by default. A manually maintained list there
1070 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1071 # references to configs outside of this directory are added, please change
1072 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1073 # never ends up in an invalid state.
Nico Weberd18b8962018-05-16 19:39:381074 bot_names = set()
John Budorickc12abd12018-08-14 19:37:431075 infra_config_dir = os.path.abspath(
1076 os.path.join(os.path.dirname(__file__),
John Budorick699282e2019-02-13 01:27:331077 '..', '..', 'infra', 'config'))
John Budorickc12abd12018-08-14 19:37:431078 milo_configs = [
Garrett Beatybb8322bf2019-10-17 20:53:051079 os.path.join(infra_config_dir, 'generated', 'luci-milo.cfg'),
Garrett Beatye95b81722019-10-24 17:12:181080 os.path.join(infra_config_dir, 'generated', 'luci-milo-dev.cfg'),
John Budorickc12abd12018-08-14 19:37:431081 ]
1082 for c in milo_configs:
1083 for l in self.read_file(c).splitlines():
1084 if (not 'name: "buildbucket/luci.chromium.' in l and
Hans Wennborg98ffd7d92019-02-06 14:14:341085 not 'name: "buildbucket/luci.chrome.' in l and
John Budorickb1833612018-12-07 04:36:411086 not 'name: "buildbot/chromium.' in l and
1087 not 'name: "buildbot/tryserver.chromium.' in l):
John Budorickc12abd12018-08-14 19:37:431088 continue
1089 # l looks like
1090 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1091 # Extract win_chromium_dbg_ng part.
1092 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381093 return bot_names
1094
Ben Pastene9a010082019-09-25 20:41:371095 def get_builders_that_do_not_actually_exist(self):
Kenneth Russell8a386d42018-06-02 09:48:011096 # Some of the bots on the chromium.gpu.fyi waterfall in particular
1097 # are defined only to be mirrored into trybots, and don't actually
1098 # exist on any of the waterfalls or consoles.
1099 return [
Michael Spangeb07eba62019-05-14 22:22:581100 'GPU FYI Fuchsia Builder',
Yuly Novikoveb26b812019-07-26 02:08:191101 'ANGLE GPU Android Release (Nexus 5X)',
Jamie Madillda894ce2019-04-08 17:19:171102 'ANGLE GPU Linux Release (Intel HD 630)',
1103 'ANGLE GPU Linux Release (NVIDIA)',
1104 'ANGLE GPU Mac Release (Intel)',
1105 'ANGLE GPU Mac Retina Release (AMD)',
1106 'ANGLE GPU Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491107 'ANGLE GPU Win10 x64 Release (Intel HD 630)',
1108 'ANGLE GPU Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011109 'Optional Android Release (Nexus 5X)',
1110 'Optional Linux Release (Intel HD 630)',
1111 'Optional Linux Release (NVIDIA)',
1112 'Optional Mac Release (Intel)',
1113 'Optional Mac Retina Release (AMD)',
1114 'Optional Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491115 'Optional Win10 x64 Release (Intel HD 630)',
1116 'Optional Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011117 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:081118 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:291119 'linux-blink-rel-dummy',
1120 'mac10.10-blink-rel-dummy',
1121 'mac10.11-blink-rel-dummy',
1122 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:201123 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291124 'mac10.13-blink-rel-dummy',
John Chenad978322019-12-16 18:07:211125 'mac10.14-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291126 'win7-blink-rel-dummy',
1127 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:081128 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:331129 'WebKit Linux composite_after_paint Dummy Builder',
Scott Violet744e04662019-08-19 23:51:531130 'WebKit Linux layout_ng_disabled Builder',
Stephen Martinis769b25112018-08-30 18:52:061131 # chromium, due to https://crbug.com/878915
1132 'win-dbg',
1133 'win32-dbg',
Stephen Martinis47d77132019-04-24 23:51:331134 'win-archive-dbg',
1135 'win32-archive-dbg',
Sajjad Mirza2924a012019-12-20 03:46:541136 # TODO(crbug.com/1033753) Delete these when coverage is enabled by default
1137 # on Windows tryjobs.
1138 'GPU Win x64 Builder Code Coverage',
1139 'Win x64 Builder Code Coverage',
1140 'Win10 Tests x64 Code Coverage',
1141 'Win10 x64 Release (NVIDIA) Code Coverage',
Kenneth Russell8a386d42018-06-02 09:48:011142 ]
1143
Ben Pastene9a010082019-09-25 20:41:371144 def get_internal_waterfalls(self):
1145 # Similar to get_builders_that_do_not_actually_exist above, but for
1146 # waterfalls defined in internal configs.
1147 return ['chrome']
1148
Stephen Martinisf83893722018-09-19 00:02:181149 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201150 self.check_input_files_sorting(verbose)
1151
Kenneth Russelleb60cbd22017-12-05 07:54:281152 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011153 self.check_composition_type_test_suites('compound_suites')
1154 self.check_composition_type_test_suites('matrix_compound_suites')
Nodir Turakulovfce34292019-12-18 17:05:411155 self.resolve_full_test_targets()
Stephen Martinis54d64ad2018-09-21 22:16:201156 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381157
1158 # All bots should exist.
1159 bot_names = self.get_valid_bot_names()
Ben Pastene9a010082019-09-25 20:41:371160 internal_waterfalls = self.get_internal_waterfalls()
1161 builders_that_dont_exist = self.get_builders_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:381162 for waterfall in self.waterfalls:
Ben Pastene9a010082019-09-25 20:41:371163 # TODO(crbug.com/991417): Remove the need for this exception.
1164 if waterfall['name'] in internal_waterfalls:
1165 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381166 for bot_name in waterfall['machines']:
Ben Pastene9a010082019-09-25 20:41:371167 if bot_name in builders_that_dont_exist:
Kenneth Russell8a386d42018-06-02 09:48:011168 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381169 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:081170 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:381171 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:521172 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:321173 if waterfall['name'] in ['tryserver.webrtc',
1174 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:381175 # These waterfalls have their bot configs in a different repo.
1176 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:521177 continue # pragma: no cover
Jeff Yoon8154e582019-12-03 23:30:011178 if waterfall['name'] in ['client.devtools-frontend.integration',
1179 'tryserver.devtools-frontend']:
Tamer Tas2c506412019-08-20 07:44:411180 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381181 raise self.unknown_bot(bot_name, waterfall['name'])
1182
Kenneth Russelleb60cbd22017-12-05 07:54:281183 # All test suites must be referenced.
1184 suites_seen = set()
1185 generator_map = self.get_test_generator_map()
1186 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431187 for bot_name, tester in waterfall['machines'].iteritems():
1188 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281189 if suite_type not in generator_map:
1190 raise self.unknown_test_suite_type(suite_type, bot_name,
1191 waterfall['name'])
1192 if suite not in self.test_suites:
1193 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1194 suites_seen.add(suite)
1195 # Since we didn't resolve the configuration files, this set
1196 # includes both composition test suites and regular ones.
1197 resolved_suites = set()
1198 for suite_name in suites_seen:
1199 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011200 for sub_suite in suite:
1201 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281202 resolved_suites.add(suite_name)
1203 # At this point, every key in test_suites.pyl should be referenced.
1204 missing_suites = set(self.test_suites.keys()) - resolved_suites
1205 if missing_suites:
1206 raise BBGenErr('The following test suites were unreferenced by bots on '
1207 'the waterfalls: ' + str(missing_suites))
1208
1209 # All test suite exceptions must refer to bots on the waterfall.
1210 all_bots = set()
1211 missing_bots = set()
1212 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431213 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281214 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281215 # In order to disambiguate between bots with the same name on
1216 # different waterfalls, support has been added to various
1217 # exceptions for concatenating the waterfall name after the bot
1218 # name.
1219 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281220 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381221 removals = (exception.get('remove_from', []) +
1222 exception.get('remove_gtest_from', []) +
1223 exception.get('modifications', {}).keys())
1224 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281225 if removal not in all_bots:
1226 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411227
Ben Pastene9a010082019-09-25 20:41:371228 missing_bots = missing_bots - set(builders_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281229 if missing_bots:
1230 raise BBGenErr('The following nonexistent machines were referenced in '
1231 'the test suite exceptions: ' + str(missing_bots))
1232
Stephen Martinis0382bc12018-09-17 22:29:071233 # All mixins must be referenced
1234 seen_mixins = set()
1235 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011236 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071237 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011238 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071239 for suite in self.test_suites.values():
1240 if isinstance(suite, list):
1241 # Don't care about this, it's a composition, which shouldn't include a
1242 # swarming mixin.
1243 continue
1244
1245 for test in suite.values():
1246 if not isinstance(test, dict):
1247 # Some test suites have top level keys, which currently can't be
1248 # swarming mixin entries. Ignore them
1249 continue
1250
Stephen Martinisb72f6d22018-10-04 23:29:011251 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071252
Stephen Martinisb72f6d22018-10-04 23:29:011253 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071254 if missing_mixins:
1255 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1256 ' referenced in a waterfall, machine, or test suite.' % (
1257 str(missing_mixins)))
1258
Stephen Martinis54d64ad2018-09-21 22:16:201259
1260 def type_assert(self, node, typ, filename, verbose=False):
1261 """Asserts that the Python AST node |node| is of type |typ|.
1262
1263 If verbose is set, it prints out some helpful context lines, showing where
1264 exactly the error occurred in the file.
1265 """
1266 if not isinstance(node, typ):
1267 if verbose:
1268 lines = [""] + self.read_file(filename).splitlines()
1269
1270 context = 2
1271 lines_start = max(node.lineno - context, 0)
1272 # Add one to include the last line
1273 lines_end = min(node.lineno + context, len(lines)) + 1
1274 lines = (
1275 ['== %s ==\n' % filename] +
1276 ["<snip>\n"] +
1277 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1278 lines[lines_start:lines_start + context])] +
1279 ['-' * 80 + '\n'] +
1280 ['%d %s' % (node.lineno, lines[node.lineno])] +
1281 ['-' * (node.col_offset + 3) + '^' + '-' * (
1282 80 - node.col_offset - 4) + '\n'] +
1283 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1284 lines[node.lineno + 1:lines_end])] +
1285 ["<snip>\n"]
1286 )
1287 # Print out a useful message when a type assertion fails.
1288 for l in lines:
1289 self.print_line(l.strip())
1290
1291 node_dumped = ast.dump(node, annotate_fields=False)
1292 # If the node is huge, truncate it so everything fits in a terminal
1293 # window.
1294 if len(node_dumped) > 60: # pragma: no cover
1295 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1296 raise BBGenErr(
1297 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1298 ' be %s, is %s' % (
1299 filename, node_dumped,
1300 node.lineno, typ, type(node)))
1301
1302 def ensure_ast_dict_keys_sorted(self, node, filename, verbose):
1303 is_valid = True
1304
1305 keys = []
1306 # The keys of this dict are ordered as ordered in the file; normal python
1307 # dictionary keys are given an arbitrary order, but since we parsed the
1308 # file itself, the order as given in the file is preserved.
1309 for key in node.keys:
1310 self.type_assert(key, ast.Str, filename, verbose)
1311 keys.append(key.s)
1312
1313 keys_sorted = sorted(keys)
1314 if keys_sorted != keys:
1315 is_valid = False
1316 if verbose:
1317 for line in difflib.unified_diff(
1318 keys,
1319 keys_sorted, fromfile='current (%r)' % filename, tofile='sorted'):
1320 self.print_line(line)
1321
1322 if len(set(keys)) != len(keys):
1323 for i in range(len(keys_sorted)-1):
1324 if keys_sorted[i] == keys_sorted[i+1]:
1325 self.print_line('Key %s is duplicated' % keys_sorted[i])
1326 is_valid = False
1327 return is_valid
Stephen Martinisf83893722018-09-19 00:02:181328
1329 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201330 # TODO(https://crbug.com/886993): Add the ability for this script to
1331 # actually format the files, rather than just complain if they're
1332 # incorrectly formatted.
1333 bad_files = set()
1334
1335 for filename in (
Stephen Martinisb72f6d22018-10-04 23:29:011336 'mixins.pyl',
Stephen Martinis54d64ad2018-09-21 22:16:201337 'test_suites.pyl',
1338 'test_suite_exceptions.pyl',
1339 ):
Stephen Martinisf83893722018-09-19 00:02:181340 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1341
Stephen Martinisf83893722018-09-19 00:02:181342 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201343 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181344 module = parsed.body
1345
1346 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201347 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181348 if len(module) != 1: # pragma: no cover
1349 raise BBGenErr('Invalid .pyl file %s' % filename)
1350 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201351 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181352
1353 # Value should be a dictionary.
1354 value = expr.value
Stephen Martinis54d64ad2018-09-21 22:16:201355 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181356
Stephen Martinis54d64ad2018-09-21 22:16:201357 if filename == 'test_suites.pyl':
Jeff Yoon8154e582019-12-03 23:30:011358 expected_keys = ['basic_suites',
1359 'compound_suites',
1360 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201361 actual_keys = [node.s for node in value.keys]
1362 assert all(key in expected_keys for key in actual_keys), (
1363 'Invalid %r file; expected keys %r, got %r' % (
1364 filename, expected_keys, actual_keys))
1365 suite_dicts = [node for node in value.values]
1366 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011367 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201368 for suite_group in suite_dicts:
1369 if not self.ensure_ast_dict_keys_sorted(
1370 suite_group, filename, verbose):
1371 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181372
Stephen Martinis54d64ad2018-09-21 22:16:201373 else:
1374 if not self.ensure_ast_dict_keys_sorted(
1375 value, filename, verbose):
1376 bad_files.add(filename)
1377
1378 # waterfalls.pyl is slightly different, just do it manually here
1379 filename = 'waterfalls.pyl'
1380 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1381
1382 # Must be a module.
1383 self.type_assert(parsed, ast.Module, filename, verbose)
1384 module = parsed.body
1385
1386 # Only one expression in the module.
1387 self.type_assert(module, list, filename, verbose)
1388 if len(module) != 1: # pragma: no cover
1389 raise BBGenErr('Invalid .pyl file %s' % filename)
1390 expr = module[0]
1391 self.type_assert(expr, ast.Expr, filename, verbose)
1392
1393 # Value should be a list.
1394 value = expr.value
1395 self.type_assert(value, ast.List, filename, verbose)
1396
1397 keys = []
1398 for val in value.elts:
1399 self.type_assert(val, ast.Dict, filename, verbose)
1400 waterfall_name = None
1401 for key, val in zip(val.keys, val.values):
1402 self.type_assert(key, ast.Str, filename, verbose)
1403 if key.s == 'machines':
1404 if not self.ensure_ast_dict_keys_sorted(val, filename, verbose):
1405 bad_files.add(filename)
1406
1407 if key.s == "name":
1408 self.type_assert(val, ast.Str, filename, verbose)
1409 waterfall_name = val.s
1410 assert waterfall_name
1411 keys.append(waterfall_name)
1412
1413 if sorted(keys) != keys:
1414 bad_files.add(filename)
1415 if verbose: # pragma: no cover
1416 for line in difflib.unified_diff(
1417 keys,
1418 sorted(keys), fromfile='current', tofile='sorted'):
1419 self.print_line(line)
Stephen Martinisf83893722018-09-19 00:02:181420
1421 if bad_files:
1422 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201423 'The following files have invalid keys: %s\n. They are either '
1424 'unsorted, or have duplicates.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181425
Kenneth Russelleb60cbd22017-12-05 07:54:281426 def check_output_file_consistency(self, verbose=False):
1427 self.load_configuration_files()
1428 # All waterfalls must have been written by this script already.
1429 self.resolve_configuration_files()
1430 ungenerated_waterfalls = set()
1431 for waterfall in self.waterfalls:
1432 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111433 file_path = waterfall['name'] + '.json'
1434 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281435 if expected != current:
1436 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321437 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501438 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281439 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321440 'contents:')
1441 for line in difflib.unified_diff(
1442 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501443 current.splitlines(),
1444 fromfile='expected', tofile='current'):
1445 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281446 if ungenerated_waterfalls:
1447 raise BBGenErr('The following waterfalls have not been properly '
1448 'autogenerated by generate_buildbot_json.py: ' +
1449 str(ungenerated_waterfalls))
1450
1451 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501452 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281453 self.check_output_file_consistency(verbose) # pragma: no cover
1454
1455 def parse_args(self, argv): # pragma: no cover
Karen Qiane24b7ee2019-02-12 23:37:061456
1457 # RawTextHelpFormatter allows for styling of help statement
1458 parser = argparse.ArgumentParser(formatter_class=
1459 argparse.RawTextHelpFormatter)
1460
1461 group = parser.add_mutually_exclusive_group()
1462 group.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281463 '-c', '--check', action='store_true', help=
1464 'Do consistency checks of configuration and generated files and then '
1465 'exit. Used during presubmit. Causes the tool to not generate any files.')
Karen Qiane24b7ee2019-02-12 23:37:061466 group.add_argument(
1467 '--query', type=str, help=
1468 ("Returns raw JSON information of buildbots and tests.\n" +
1469 "Examples:\n" +
1470 " List all bots (all info):\n" +
1471 " --query bots\n\n" +
1472 " List all bots and only their associated tests:\n" +
1473 " --query bots/tests\n\n" +
1474 " List all information about 'bot1' " +
1475 "(make sure you have quotes):\n" +
1476 " --query bot/'bot1'\n\n" +
1477 " List tests running for 'bot1' (make sure you have quotes):\n" +
1478 " --query bot/'bot1'/tests\n\n" +
1479 " List all tests:\n" +
1480 " --query tests\n\n" +
1481 " List all tests and the bots running them:\n" +
1482 " --query tests/bots\n\n"+
1483 " List all tests that satisfy multiple parameters\n" +
1484 " (separation of parameters by '&' symbol):\n" +
1485 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
1486 " List all tests that run with a specific flag:\n" +
1487 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
1488 " List specific test (make sure you have quotes):\n"
1489 " --query test/'test1'\n\n"
1490 " List all bots running 'test1' " +
1491 "(make sure you have quotes):\n" +
1492 " --query test/'test1'/bots" ))
Kenneth Russelleb60cbd22017-12-05 07:54:281493 parser.add_argument(
1494 '-n', '--new-files', action='store_true', help=
1495 'Write output files as .new.json. Useful during development so old and '
1496 'new files can be looked at side-by-side.')
1497 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501498 '-v', '--verbose', action='store_true', help=
1499 'Increases verbosity. Affects consistency checks.')
1500 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281501 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1502 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111503 parser.add_argument(
1504 '--pyl-files-dir', type=os.path.realpath,
1505 help='Path to the directory containing the input .pyl files.')
Karen Qiane24b7ee2019-02-12 23:37:061506 parser.add_argument(
1507 '--json', help=
1508 ("Outputs results into a json file. Only works with query function.\n" +
1509 "Examples:\n" +
1510 " Outputs file into specified json file: \n" +
1511 " --json <file-name-here.json>"))
Kenneth Russelleb60cbd22017-12-05 07:54:281512 self.args = parser.parse_args(argv)
Karen Qiane24b7ee2019-02-12 23:37:061513 if self.args.json and not self.args.query:
1514 parser.error("The --json flag can only be used with --query.")
1515
1516 def does_test_match(self, test_info, params_dict):
1517 """Checks to see if the test matches the parameters given.
1518
1519 Compares the provided test_info with the params_dict to see
1520 if the bot matches the parameters given. If so, returns True.
1521 Else, returns false.
1522
1523 Args:
1524 test_info (dict): Information about a specific bot provided
1525 in the format shown in waterfalls.pyl
1526 params_dict (dict): Dictionary of parameters and their values
1527 to look for in the bot
1528 Ex: {
1529 'device_os':'android',
1530 '--flag':True,
1531 'mixins': ['mixin1', 'mixin2'],
1532 'ex_key':'ex_value'
1533 }
1534
1535 """
1536 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
1537 'kvm', 'pool', 'integrity'] # dimension parameters
1538 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
1539 'can_use_on_swarming_builders']
1540 for param in params_dict:
1541 # if dimension parameter
1542 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
1543 if not 'swarming' in test_info:
1544 return False
1545 swarming = test_info['swarming']
1546 if param in SWARMING_PARAMS:
1547 if not param in swarming:
1548 return False
1549 if not str(swarming[param]) == params_dict[param]:
1550 return False
1551 else:
1552 if not 'dimension_sets' in swarming:
1553 return False
1554 d_set = swarming['dimension_sets']
1555 # only looking at the first dimension set
1556 if not param in d_set[0]:
1557 return False
1558 if not d_set[0][param] == params_dict[param]:
1559 return False
1560
1561 # if flag
1562 elif param.startswith('--'):
1563 if not 'args' in test_info:
1564 return False
1565 if not param in test_info['args']:
1566 return False
1567
1568 # not dimension parameter/flag/mixin
1569 else:
1570 if not param in test_info:
1571 return False
1572 if not test_info[param] == params_dict[param]:
1573 return False
1574 return True
1575 def error_msg(self, msg):
1576 """Prints an error message.
1577
1578 In addition to a catered error message, also prints
1579 out where the user can find more help. Then, program exits.
1580 """
1581 self.print_line(msg + (' If you need more information, ' +
1582 'please run with -h or --help to see valid commands.'))
1583 sys.exit(1)
1584
1585 def find_bots_that_run_test(self, test, bots):
1586 matching_bots = []
1587 for bot in bots:
1588 bot_info = bots[bot]
1589 tests = self.flatten_tests_for_bot(bot_info)
1590 for test_info in tests:
1591 test_name = ""
1592 if 'name' in test_info:
1593 test_name = test_info['name']
1594 elif 'test' in test_info:
1595 test_name = test_info['test']
1596 if not test_name == test:
1597 continue
1598 matching_bots.append(bot)
1599 return matching_bots
1600
1601 def find_tests_with_params(self, tests, params_dict):
1602 matching_tests = []
1603 for test_name in tests:
1604 test_info = tests[test_name]
1605 if not self.does_test_match(test_info, params_dict):
1606 continue
1607 if not test_name in matching_tests:
1608 matching_tests.append(test_name)
1609 return matching_tests
1610
1611 def flatten_waterfalls_for_query(self, waterfalls):
1612 bots = {}
1613 for waterfall in waterfalls:
1614 waterfall_json = json.loads(self.generate_waterfall_json(waterfall))
1615 for bot in waterfall_json:
1616 bot_info = waterfall_json[bot]
1617 if 'AAAAA' not in bot:
1618 bots[bot] = bot_info
1619 return bots
1620
1621 def flatten_tests_for_bot(self, bot_info):
1622 """Returns a list of flattened tests.
1623
1624 Returns a list of tests not grouped by test category
1625 for a specific bot.
1626 """
1627 TEST_CATS = self.get_test_generator_map().keys()
1628 tests = []
1629 for test_cat in TEST_CATS:
1630 if not test_cat in bot_info:
1631 continue
1632 test_cat_tests = bot_info[test_cat]
1633 tests = tests + test_cat_tests
1634 return tests
1635
1636 def flatten_tests_for_query(self, test_suites):
1637 """Returns a flattened dictionary of tests.
1638
1639 Returns a dictionary of tests associate with their
1640 configuration, not grouped by their test suite.
1641 """
1642 tests = {}
1643 for test_suite in test_suites.itervalues():
1644 for test in test_suite:
1645 test_info = test_suite[test]
1646 test_name = test
1647 if 'name' in test_info:
1648 test_name = test_info['name']
1649 tests[test_name] = test_info
1650 return tests
1651
1652 def parse_query_filter_params(self, params):
1653 """Parses the filter parameters.
1654
1655 Creates a dictionary from the parameters provided
1656 to filter the bot array.
1657 """
1658 params_dict = {}
1659 for p in params:
1660 # flag
1661 if p.startswith("--"):
1662 params_dict[p] = True
1663 else:
1664 pair = p.split(":")
1665 if len(pair) != 2:
1666 self.error_msg('Invalid command.')
1667 # regular parameters
1668 if pair[1].lower() == "true":
1669 params_dict[pair[0]] = True
1670 elif pair[1].lower() == "false":
1671 params_dict[pair[0]] = False
1672 else:
1673 params_dict[pair[0]] = pair[1]
1674 return params_dict
1675
1676 def get_test_suites_dict(self, bots):
1677 """Returns a dictionary of bots and their tests.
1678
1679 Returns a dictionary of bots and a list of their associated tests.
1680 """
1681 test_suite_dict = dict()
1682 for bot in bots:
1683 bot_info = bots[bot]
1684 tests = self.flatten_tests_for_bot(bot_info)
1685 test_suite_dict[bot] = tests
1686 return test_suite_dict
1687
1688 def output_query_result(self, result, json_file=None):
1689 """Outputs the result of the query.
1690
1691 If a json file parameter name is provided, then
1692 the result is output into the json file. If not,
1693 then the result is printed to the console.
1694 """
1695 output = json.dumps(result, indent=2)
1696 if json_file:
1697 self.write_file(json_file, output)
1698 else:
1699 self.print_line(output)
1700 return
1701
1702 def query(self, args):
1703 """Queries tests or bots.
1704
1705 Depending on the arguments provided, outputs a json of
1706 tests or bots matching the appropriate optional parameters provided.
1707 """
1708 # split up query statement
1709 query = args.query.split('/')
1710 self.load_configuration_files()
1711 self.resolve_configuration_files()
1712
1713 # flatten bots json
1714 tests = self.test_suites
1715 bots = self.flatten_waterfalls_for_query(self.waterfalls)
1716
1717 cmd_class = query[0]
1718
1719 # For queries starting with 'bots'
1720 if cmd_class == "bots":
1721 if len(query) == 1:
1722 return self.output_query_result(bots, args.json)
1723 # query with specific parameters
1724 elif len(query) == 2:
1725 if query[1] == 'tests':
1726 test_suites_dict = self.get_test_suites_dict(bots)
1727 return self.output_query_result(test_suites_dict, args.json)
1728 else:
1729 self.error_msg("This query should be in the format: bots/tests.")
1730
1731 else:
1732 self.error_msg("This query should have 0 or 1 '/', found %s instead."
1733 % str(len(query)-1))
1734
1735 # For queries starting with 'bot'
1736 elif cmd_class == "bot":
1737 if not len(query) == 2 and not len(query) == 3:
1738 self.error_msg("Command should have 1 or 2 '/', found %s instead."
1739 % str(len(query)-1))
1740 bot_id = query[1]
1741 if not bot_id in bots:
1742 self.error_msg("No bot named '" + bot_id + "' found.")
1743 bot_info = bots[bot_id]
1744 if len(query) == 2:
1745 return self.output_query_result(bot_info, args.json)
1746 if not query[2] == 'tests':
1747 self.error_msg("The query should be in the format:" +
1748 "bot/<bot-name>/tests.")
1749
1750 bot_tests = self.flatten_tests_for_bot(bot_info)
1751 return self.output_query_result(bot_tests, args.json)
1752
1753 # For queries starting with 'tests'
1754 elif cmd_class == "tests":
1755 if not len(query) == 1 and not len(query) == 2:
1756 self.error_msg("The query should have 0 or 1 '/', found %s instead."
1757 % str(len(query)-1))
1758 flattened_tests = self.flatten_tests_for_query(tests)
1759 if len(query) == 1:
1760 return self.output_query_result(flattened_tests, args.json)
1761
1762 # create params dict
1763 params = query[1].split('&')
1764 params_dict = self.parse_query_filter_params(params)
1765 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
1766 return self.output_query_result(matching_bots)
1767
1768 # For queries starting with 'test'
1769 elif cmd_class == "test":
1770 if not len(query) == 2 and not len(query) == 3:
1771 self.error_msg("The query should have 1 or 2 '/', found %s instead."
1772 % str(len(query)-1))
1773 test_id = query[1]
1774 if len(query) == 2:
1775 flattened_tests = self.flatten_tests_for_query(tests)
1776 for test in flattened_tests:
1777 if test == test_id:
1778 return self.output_query_result(flattened_tests[test], args.json)
1779 self.error_msg("There is no test named %s." % test_id)
1780 if not query[2] == 'bots':
1781 self.error_msg("The query should be in the format: " +
1782 "test/<test-name>/bots")
1783 bots_for_test = self.find_bots_that_run_test(test_id, bots)
1784 return self.output_query_result(bots_for_test)
1785
1786 else:
1787 self.error_msg("Your command did not match any valid commands." +
1788 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:281789
1790 def main(self, argv): # pragma: no cover
1791 self.parse_args(argv)
1792 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:501793 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:061794 elif self.args.query:
1795 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:281796 else:
1797 self.generate_waterfalls()
1798 return 0
1799
1800if __name__ == "__main__": # pragma: no cover
1801 generator = BBJSONGenerator()
John Budorick699282e2019-02-13 01:27:331802 sys.exit(generator.main(sys.argv[1:]))