blob: 739ca4209ca140ffffcb467644f67303a0ab15d7 [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):
66 def __init__(self, bb_gen):
67 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
68
69 def generate(self, waterfall, tester_name, tester_config, input_tests):
70 isolated_scripts = []
71 for test_name, test_config in sorted(input_tests.iteritems()):
72 test = self.bb_gen.generate_gpu_telemetry_test(
73 waterfall, tester_name, tester_config, test_name, test_config)
74 if test:
75 isolated_scripts.append(test)
76 return isolated_scripts
77
78 def sort(self, tests):
79 return sorted(tests, key=lambda x: x['name'])
80
81
Kenneth Russelleb60cbd22017-12-05 07:54:2882class GTestGenerator(BaseGenerator):
83 def __init__(self, bb_gen):
84 super(GTestGenerator, self).__init__(bb_gen)
85
Kenneth Russell8ceeabf2017-12-11 17:53:2886 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2887 # The relative ordering of some of the tests is important to
88 # minimize differences compared to the handwritten JSON files, since
89 # Python's sorts are stable and there are some tests with the same
90 # key (see gles2_conform_d3d9_test and similar variants). Avoid
91 # losing the order by avoiding coalescing the dictionaries into one.
92 gtests = []
93 for test_name, test_config in sorted(input_tests.iteritems()):
Nico Weber79dc5f6852018-07-13 19:38:4994 test = self.bb_gen.generate_gtest(
95 waterfall, tester_name, tester_config, test_name, test_config)
96 if test:
97 # generate_gtest may veto the test generation on this tester.
98 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:2899 return gtests
100
101 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28102 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28103
104
105class IsolatedScriptTestGenerator(BaseGenerator):
106 def __init__(self, bb_gen):
107 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
108
Kenneth Russell8ceeabf2017-12-11 17:53:28109 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28110 isolated_scripts = []
111 for test_name, test_config in sorted(input_tests.iteritems()):
112 test = self.bb_gen.generate_isolated_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28113 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28114 if test:
115 isolated_scripts.append(test)
116 return isolated_scripts
117
118 def sort(self, tests):
119 return sorted(tests, key=lambda x: x['name'])
120
121
122class ScriptGenerator(BaseGenerator):
123 def __init__(self, bb_gen):
124 super(ScriptGenerator, self).__init__(bb_gen)
125
Kenneth Russell8ceeabf2017-12-11 17:53:28126 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28127 scripts = []
128 for test_name, test_config in sorted(input_tests.iteritems()):
129 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28130 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28131 if test:
132 scripts.append(test)
133 return scripts
134
135 def sort(self, tests):
136 return sorted(tests, key=lambda x: x['name'])
137
138
139class JUnitGenerator(BaseGenerator):
140 def __init__(self, bb_gen):
141 super(JUnitGenerator, self).__init__(bb_gen)
142
Kenneth Russell8ceeabf2017-12-11 17:53:28143 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28144 scripts = []
145 for test_name, test_config in sorted(input_tests.iteritems()):
146 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28147 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28148 if test:
149 scripts.append(test)
150 return scripts
151
152 def sort(self, tests):
153 return sorted(tests, key=lambda x: x['test'])
154
155
156class CTSGenerator(BaseGenerator):
157 def __init__(self, bb_gen):
158 super(CTSGenerator, self).__init__(bb_gen)
159
Kenneth Russell8ceeabf2017-12-11 17:53:28160 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28161 # These only contain one entry and it's the contents of the input tests'
162 # dictionary, verbatim.
163 cts_tests = []
164 cts_tests.append(input_tests)
165 return cts_tests
166
167 def sort(self, tests):
168 return tests
169
170
171class InstrumentationTestGenerator(BaseGenerator):
172 def __init__(self, bb_gen):
173 super(InstrumentationTestGenerator, self).__init__(bb_gen)
174
Kenneth Russell8ceeabf2017-12-11 17:53:28175 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28176 scripts = []
177 for test_name, test_config in sorted(input_tests.iteritems()):
178 test = self.bb_gen.generate_instrumentation_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28179 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28180 if test:
181 scripts.append(test)
182 return scripts
183
184 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28185 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28186
187
188class BBJSONGenerator(object):
189 def __init__(self):
190 self.this_dir = THIS_DIR
191 self.args = None
192 self.waterfalls = None
193 self.test_suites = None
194 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01195 self.mixins = None
Kenneth Russelleb60cbd22017-12-05 07:54:28196
197 def generate_abs_file_path(self, relative_path):
198 return os.path.join(self.this_dir, relative_path) # pragma: no cover
199
Stephen Martinis7eb8b612018-09-21 00:17:50200 def print_line(self, line):
201 # Exists so that tests can mock
202 print line # pragma: no cover
203
Kenneth Russelleb60cbd22017-12-05 07:54:28204 def read_file(self, relative_path):
205 with open(self.generate_abs_file_path(
206 relative_path)) as fp: # pragma: no cover
207 return fp.read() # pragma: no cover
208
209 def write_file(self, relative_path, contents):
210 with open(self.generate_abs_file_path(
211 relative_path), 'wb') as fp: # pragma: no cover
212 fp.write(contents) # pragma: no cover
213
Zhiling Huangbe008172018-03-08 19:13:11214 def pyl_file_path(self, filename):
215 if self.args and self.args.pyl_files_dir:
216 return os.path.join(self.args.pyl_files_dir, filename)
217 return filename
218
Kenneth Russelleb60cbd22017-12-05 07:54:28219 def load_pyl_file(self, filename):
220 try:
Zhiling Huangbe008172018-03-08 19:13:11221 return ast.literal_eval(self.read_file(
222 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28223 except (SyntaxError, ValueError) as e: # pragma: no cover
224 raise BBGenErr('Failed to parse pyl file "%s": %s' %
225 (filename, e)) # pragma: no cover
226
Kenneth Russell8a386d42018-06-02 09:48:01227 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
228 # Currently it is only mandatory for bots which run GPU tests. Change these to
229 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28230 def is_android(self, tester_config):
231 return tester_config.get('os_type') == 'android'
232
Ben Pastenea9e583b2019-01-16 02:57:26233 def is_chromeos(self, tester_config):
234 return tester_config.get('os_type') == 'chromeos'
235
Kenneth Russell8a386d42018-06-02 09:48:01236 def is_linux(self, tester_config):
237 return tester_config.get('os_type') == 'linux'
238
Kenneth Russelleb60cbd22017-12-05 07:54:28239 def get_exception_for_test(self, test_name, test_config):
240 # gtests may have both "test" and "name" fields, and usually, if the "name"
241 # field is specified, it means that the same test is being repurposed
242 # multiple times with different command line arguments. To handle this case,
243 # prefer to lookup per the "name" field of the test itself, as opposed to
244 # the "test_name", which is actually the "test" field.
245 if 'name' in test_config:
246 return self.exceptions.get(test_config['name'])
247 else:
248 return self.exceptions.get(test_name)
249
Nico Weberb0b3f5862018-07-13 18:45:15250 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28251 # Currently, the only reason a test should not run on a given tester is that
252 # it's in the exceptions. (Once the GPU waterfall generation script is
253 # incorporated here, the rules will become more complex.)
254 exception = self.get_exception_for_test(test_name, test_config)
255 if not exception:
256 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28257 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28258 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28259 if remove_from:
260 if tester_name in remove_from:
261 return False
262 # TODO(kbr): this code path was added for some tests (including
263 # android_webview_unittests) on one machine (Nougat Phone
264 # Tester) which exists with the same name on two waterfalls,
265 # chromium.android and chromium.fyi; the tests are run on one
266 # but not the other. Once the bots are all uniquely named (a
267 # different ongoing project) this code should be removed.
268 # TODO(kbr): add coverage.
269 return (tester_name + ' ' + waterfall['name']
270 not in remove_from) # pragma: no cover
271 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28272
Nico Weber79dc5f6852018-07-13 19:38:49273 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28274 exception = self.get_exception_for_test(test_name, test)
275 if not exception:
276 return None
Nico Weber79dc5f6852018-07-13 19:38:49277 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28278
Kenneth Russell8a386d42018-06-02 09:48:01279 def merge_command_line_args(self, arr, prefix, splitter):
280 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01281 idx = 0
282 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01283 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01284 while idx < len(arr):
285 flag = arr[idx]
286 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01287 if flag.startswith(prefix):
288 arg = flag[prefix_len:]
289 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01290 if first_idx < 0:
291 first_idx = idx
292 else:
293 delete_current_entry = True
294 if delete_current_entry:
295 del arr[idx]
296 else:
297 idx += 1
298 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01299 arr[first_idx] = prefix + splitter.join(accumulated_args)
300 return arr
301
302 def maybe_fixup_args_array(self, arr):
303 # The incoming array of strings may be an array of command line
304 # arguments. To make it easier to turn on certain features per-bot or
305 # per-test-suite, look specifically for certain flags and merge them
306 # appropriately.
307 # --enable-features=Feature1 --enable-features=Feature2
308 # are merged to:
309 # --enable-features=Feature1,Feature2
310 # and:
311 # --extra-browser-args=arg1 --extra-browser-args=arg2
312 # are merged to:
313 # --extra-browser-args=arg1 arg2
314 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
315 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01316 return arr
317
Kenneth Russelleb60cbd22017-12-05 07:54:28318 def dictionary_merge(self, a, b, path=None, update=True):
319 """http://stackoverflow.com/questions/7204805/
320 python-dictionaries-of-dictionaries-merge
321 merges b into a
322 """
323 if path is None:
324 path = []
325 for key in b:
326 if key in a:
327 if isinstance(a[key], dict) and isinstance(b[key], dict):
328 self.dictionary_merge(a[key], b[key], path + [str(key)])
329 elif a[key] == b[key]:
330 pass # same leaf value
331 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06332 # Args arrays are lists of strings. Just concatenate them,
333 # and don't sort them, in order to keep some needed
334 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28335 if all(isinstance(x, str)
336 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01337 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28338 else:
339 # TODO(kbr): this only works properly if the two arrays are
340 # the same length, which is currently always the case in the
341 # swarming dimension_sets that we have to merge. It will fail
342 # to merge / override 'args' arrays which are different
343 # length.
344 for idx in xrange(len(b[key])):
345 try:
346 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
347 path + [str(key), str(idx)],
348 update=update)
349 except (IndexError, TypeError): # pragma: no cover
350 raise BBGenErr('Error merging list keys ' + str(key) +
351 ' and indices ' + str(idx) + ' between ' +
352 str(a) + ' and ' + str(b)) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28353 elif update: # pragma: no cover
354 a[key] = b[key] # pragma: no cover
355 else:
356 raise BBGenErr('Conflict at %s' % '.'.join(
357 path + [str(key)])) # pragma: no cover
358 else:
359 a[key] = b[key]
360 return a
361
John Budorickab108712018-09-01 00:12:21362 def initialize_args_for_test(
363 self, generated_test, tester_config, additional_arg_keys=None):
364
365 args = []
366 args.extend(generated_test.get('args', []))
367 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22368
Kenneth Russell8a386d42018-06-02 09:48:01369 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21370 val = generated_test.pop(key, [])
371 if fn(tester_config):
372 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01373
374 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
375 add_conditional_args('linux_args', self.is_linux)
376 add_conditional_args('android_args', self.is_android)
377
John Budorickab108712018-09-01 00:12:21378 for key in additional_arg_keys or []:
379 args.extend(generated_test.pop(key, []))
380 args.extend(tester_config.get(key, []))
381
382 if args:
383 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01384
Kenneth Russelleb60cbd22017-12-05 07:54:28385 def initialize_swarming_dictionary_for_test(self, generated_test,
386 tester_config):
387 if 'swarming' not in generated_test:
388 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28389 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
390 generated_test['swarming'].update({
391 'can_use_on_swarming_builders': tester_config.get('use_swarming', True)
392 })
Kenneth Russelleb60cbd22017-12-05 07:54:28393 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03394 if ('dimension_sets' not in generated_test['swarming'] and
395 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28396 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
397 tester_config['swarming']['dimension_sets'])
398 self.dictionary_merge(generated_test['swarming'],
399 tester_config['swarming'])
400 # Apply any Android-specific Swarming dimensions after the generic ones.
401 if 'android_swarming' in generated_test:
402 if self.is_android(tester_config): # pragma: no cover
403 self.dictionary_merge(
404 generated_test['swarming'],
405 generated_test['android_swarming']) # pragma: no cover
406 del generated_test['android_swarming'] # pragma: no cover
407
408 def clean_swarming_dictionary(self, swarming_dict):
409 # Clean out redundant entries from a test's "swarming" dictionary.
410 # This is really only needed to retain 100% parity with the
411 # handwritten JSON files, and can be removed once all the files are
412 # autogenerated.
413 if 'shards' in swarming_dict:
414 if swarming_dict['shards'] == 1: # pragma: no cover
415 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24416 if 'hard_timeout' in swarming_dict:
417 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
418 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43419 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28420 # Remove all other keys.
421 for k in swarming_dict.keys(): # pragma: no cover
422 if k != 'can_use_on_swarming_builders': # pragma: no cover
423 del swarming_dict[k] # pragma: no cover
424
Stephen Martinis0382bc12018-09-17 22:29:07425 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
426 waterfall):
427 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01428 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07429 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28430 # See if there are any exceptions that need to be merged into this
431 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49432 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28433 if modifications:
434 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23435 if 'swarming' in test:
436 self.clean_swarming_dictionary(test['swarming'])
Kenneth Russelleb60cbd22017-12-05 07:54:28437 return test
438
Shenghua Zhangaba8bad2018-02-07 02:12:09439 def add_common_test_properties(self, test, tester_config):
440 if tester_config.get('use_multi_dimension_trigger_script'):
Kenneth Russell73c3bd8b2018-10-19 22:30:19441 # Assumes update_and_cleanup_test has already been called, so the
442 # builder's mixins have been flattened into the test.
Shenghua Zhangaba8bad2018-02-07 02:12:09443 test['trigger_script'] = {
444 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
445 'args': [
446 '--multiple-trigger-configs',
Kenneth Russell73c3bd8b2018-10-19 22:30:19447 json.dumps(test['swarming']['dimension_sets'] +
Shenghua Zhangaba8bad2018-02-07 02:12:09448 tester_config.get('alternate_swarming_dimensions', [])),
449 '--multiple-dimension-script-verbose',
450 'True'
451 ],
452 }
Ben Pastenea9e583b2019-01-16 02:57:26453 elif self.is_chromeos(tester_config) and tester_config.get('use_swarming',
454 True):
455 # The presence of the "device_type" dimension indicates that the tests
456 # are targetting CrOS hardware and so need the special trigger script.
457 dimension_sets = tester_config['swarming']['dimension_sets']
458 if all('device_type' in ds for ds in dimension_sets):
459 test['trigger_script'] = {
460 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
461 }
Shenghua Zhangaba8bad2018-02-07 02:12:09462
Ben Pastene858f4be2019-01-09 23:52:09463 def add_android_presentation_args(self, tester_config, test_name, result):
464 args = result.get('args', [])
465 args.append('--gs-results-bucket=chromium-result-details')
466 if (result['swarming']['can_use_on_swarming_builders'] and not
467 tester_config.get('skip_merge_script', False)):
468 result['merge'] = {
469 'args': [
470 '--bucket',
471 'chromium-result-details',
472 '--test-name',
473 test_name
474 ],
475 'script': '//build/android/pylib/results/presentation/'
476 'test_results_presentation.py',
477 }
478 if not tester_config.get('skip_cipd_packages', False):
Ben Pastenee5949ea82019-01-10 21:45:26479 cipd_packages = result['swarming'].get('cipd_packages', [])
480 cipd_packages.append(
Ben Pastene858f4be2019-01-09 23:52:09481 {
482 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
483 'location': 'bin',
484 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
485 }
Ben Pastenee5949ea82019-01-10 21:45:26486 )
487 result['swarming']['cipd_packages'] = cipd_packages
Ben Pastene858f4be2019-01-09 23:52:09488 if not tester_config.get('skip_output_links', False):
489 result['swarming']['output_links'] = [
490 {
491 'link': [
492 'https://luci-logdog.appspot.com/v/?s',
493 '=android%2Fswarming%2Flogcats%2F',
494 '${TASK_ID}%2F%2B%2Funified_logcats',
495 ],
496 'name': 'shard #${SHARD_INDEX} logcats',
497 },
498 ]
499 if args:
500 result['args'] = args
501
Kenneth Russelleb60cbd22017-12-05 07:54:28502 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
503 test_config):
504 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15505 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28506 return None
507 result = copy.deepcopy(test_config)
508 if 'test' in result:
509 result['name'] = test_name
510 else:
511 result['test'] = test_name
512 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21513
514 self.initialize_args_for_test(
515 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28516 if self.is_android(tester_config) and tester_config.get('use_swarming',
517 True):
Ben Pastene858f4be2019-01-09 23:52:09518 self.add_android_presentation_args(tester_config, test_name, result)
519 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42520
Stephen Martinis0382bc12018-09-17 22:29:07521 result = self.update_and_cleanup_test(
522 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09523 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28524 return result
525
526 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
527 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01528 if not self.should_run_on_tester(waterfall, tester_name, test_name,
529 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28530 return None
531 result = copy.deepcopy(test_config)
532 result['isolate_name'] = result.get('isolate_name', test_name)
533 result['name'] = test_name
534 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01535 self.initialize_args_for_test(result, tester_config)
Ben Pastene858f4be2019-01-09 23:52:09536 if tester_config.get('use_android_presentation', False):
537 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07538 result = self.update_and_cleanup_test(
539 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09540 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28541 return result
542
543 def generate_script_test(self, waterfall, tester_name, tester_config,
544 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01545 if not self.should_run_on_tester(waterfall, tester_name, test_name,
546 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28547 return None
548 result = {
549 'name': test_name,
550 'script': test_config['script']
551 }
Stephen Martinis0382bc12018-09-17 22:29:07552 result = self.update_and_cleanup_test(
553 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28554 return result
555
556 def generate_junit_test(self, waterfall, tester_name, tester_config,
557 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01558 del tester_config
559 if not self.should_run_on_tester(waterfall, tester_name, test_name,
560 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28561 return None
562 result = {
563 'test': test_name,
564 }
565 return result
566
567 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
568 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01569 if not self.should_run_on_tester(waterfall, tester_name, test_name,
570 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28571 return None
572 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28573 if 'test' in result and result['test'] != test_name:
574 result['name'] = test_name
575 else:
576 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07577 result = self.update_and_cleanup_test(
578 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28579 return result
580
Stephen Martinis2a0667022018-09-25 22:31:14581 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01582 substitutions = {
583 # Any machine in waterfalls.pyl which desires to run GPU tests
584 # must provide the os_type key.
585 'os_type': tester_config['os_type'],
586 'gpu_vendor_id': '0',
587 'gpu_device_id': '0',
588 }
Stephen Martinis2a0667022018-09-25 22:31:14589 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01590 if 'gpu' in dimension_set:
591 # First remove the driver version, then split into vendor and device.
592 gpu = dimension_set['gpu']
593 gpu = gpu.split('-')[0].split(':')
594 substitutions['gpu_vendor_id'] = gpu[0]
595 substitutions['gpu_device_id'] = gpu[1]
596 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
597
598 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
599 test_name, test_config):
600 # These are all just specializations of isolated script tests with
601 # a bunch of boilerplate command line arguments added.
602
603 # The step name must end in 'test' or 'tests' in order for the
604 # results to automatically show up on the flakiness dashboard.
605 # (At least, this was true some time ago.) Continue to use this
606 # naming convention for the time being to minimize changes.
607 step_name = test_config.get('name', test_name)
608 if not (step_name.endswith('test') or step_name.endswith('tests')):
609 step_name = '%s_tests' % step_name
610 result = self.generate_isolated_script_test(
611 waterfall, tester_name, tester_config, step_name, test_config)
612 if not result:
613 return None
614 result['isolate_name'] = 'telemetry_gpu_integration_test'
615 args = result.get('args', [])
616 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14617
618 # These tests upload and download results from cloud storage and therefore
619 # aren't idempotent yet. https://crbug.com/549140.
620 result['swarming']['idempotent'] = False
621
Kenneth Russell44910c32018-12-03 23:35:11622 # The GPU tests act much like integration tests for the entire browser, and
623 # tend to uncover flakiness bugs more readily than other test suites. In
624 # order to surface any flakiness more readily to the developer of the CL
625 # which is introducing it, we disable retries with patch on the commit
626 # queue.
627 result['should_retry_with_patch'] = False
628
Kenneth Russell8a386d42018-06-02 09:48:01629 args = [
630 test_to_run,
631 '--show-stdout',
632 '--browser=%s' % tester_config['browser_config'],
633 # --passthrough displays more of the logging in Telemetry when
634 # run via typ, in particular some of the warnings about tests
635 # being expected to fail, but passing.
636 '--passthrough',
637 '-v',
638 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
639 ] + args
640 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14641 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01642 return result
643
Kenneth Russelleb60cbd22017-12-05 07:54:28644 def get_test_generator_map(self):
645 return {
646 'cts_tests': CTSGenerator(self),
Kenneth Russell8a386d42018-06-02 09:48:01647 'gpu_telemetry_tests': GPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28648 'gtest_tests': GTestGenerator(self),
649 'instrumentation_tests': InstrumentationTestGenerator(self),
650 'isolated_scripts': IsolatedScriptTestGenerator(self),
651 'junit_tests': JUnitGenerator(self),
652 'scripts': ScriptGenerator(self),
653 }
654
Kenneth Russell8a386d42018-06-02 09:48:01655 def get_test_type_remapper(self):
656 return {
657 # These are a specialization of isolated_scripts with a bunch of
658 # boilerplate command line arguments added to each one.
659 'gpu_telemetry_tests': 'isolated_scripts',
660 }
661
Kenneth Russelleb60cbd22017-12-05 07:54:28662 def check_composition_test_suites(self):
663 # Pre-pass to catch errors reliably.
664 for name, value in self.test_suites.iteritems():
665 if isinstance(value, list):
666 for entry in value:
667 if isinstance(self.test_suites[entry], list):
Nico Weberd18b8962018-05-16 19:39:38668 raise BBGenErr('Composition test suites may not refer to other '
669 'composition test suites (error found while '
Kenneth Russelleb60cbd22017-12-05 07:54:28670 'processing %s)' % name)
671
Stephen Martinis54d64ad2018-09-21 22:16:20672 def flatten_test_suites(self):
673 new_test_suites = {}
674 for name, value in self.test_suites.get('basic_suites', {}).iteritems():
675 new_test_suites[name] = value
676 for name, value in self.test_suites.get('compound_suites', {}).iteritems():
677 if name in new_test_suites:
678 raise BBGenErr('Composition test suite names may not duplicate basic '
679 'test suite names (error found while processsing %s' % (
680 name))
681 new_test_suites[name] = value
682 self.test_suites = new_test_suites
683
Kenneth Russelleb60cbd22017-12-05 07:54:28684 def resolve_composition_test_suites(self):
Stephen Martinis54d64ad2018-09-21 22:16:20685 self.flatten_test_suites()
686
Kenneth Russelleb60cbd22017-12-05 07:54:28687 self.check_composition_test_suites()
688 for name, value in self.test_suites.iteritems():
689 if isinstance(value, list):
690 # Resolve this to a dictionary.
691 full_suite = {}
692 for entry in value:
693 suite = self.test_suites[entry]
694 full_suite.update(suite)
695 self.test_suites[name] = full_suite
696
697 def link_waterfalls_to_test_suites(self):
698 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43699 for tester_name, tester in waterfall['machines'].iteritems():
700 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28701 if not value in self.test_suites:
702 # Hard / impossible to cover this in the unit test.
703 raise self.unknown_test_suite(
704 value, tester_name, waterfall['name']) # pragma: no cover
705 tester['test_suites'][suite] = self.test_suites[value]
706
707 def load_configuration_files(self):
708 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
709 self.test_suites = self.load_pyl_file('test_suites.pyl')
710 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:01711 self.mixins = self.load_pyl_file('mixins.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:28712
713 def resolve_configuration_files(self):
714 self.resolve_composition_test_suites()
715 self.link_waterfalls_to_test_suites()
716
Nico Weberd18b8962018-05-16 19:39:38717 def unknown_bot(self, bot_name, waterfall_name):
718 return BBGenErr(
719 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
720
Kenneth Russelleb60cbd22017-12-05 07:54:28721 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
722 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38723 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28724 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
725
726 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
727 return BBGenErr(
728 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
729 ' on waterfall ' + waterfall_name)
730
Stephen Martinisb72f6d22018-10-04 23:29:01731 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:07732 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:32733
734 Checks in the waterfall, builder, and test objects for mixins.
735 """
736 def valid_mixin(mixin_name):
737 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:01738 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:32739 raise BBGenErr("bad mixin %s" % mixin_name)
740 def must_be_list(mixins, typ, name):
741 """Asserts that given mixins are a list."""
742 if not isinstance(mixins, list):
743 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
744
Stephen Martinisb72f6d22018-10-04 23:29:01745 if 'mixins' in waterfall:
746 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
747 for mixin in waterfall['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32748 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01749 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32750
Stephen Martinisb72f6d22018-10-04 23:29:01751 if 'mixins' in builder:
752 must_be_list(builder['mixins'], 'builder', builder_name)
753 for mixin in builder['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32754 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01755 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32756
Stephen Martinisb72f6d22018-10-04 23:29:01757 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:07758 return test
759
Stephen Martinis2a0667022018-09-25 22:31:14760 test_name = test.get('name')
761 if not test_name:
762 test_name = test.get('test')
763 if not test_name: # pragma: no cover
764 # Not the best name, but we should say something.
765 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:01766 must_be_list(test['mixins'], 'test', test_name)
767 for mixin in test['mixins']:
Stephen Martinis0382bc12018-09-17 22:29:07768 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01769 test = self.apply_mixin(self.mixins[mixin], test)
770 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:07771 return test
Stephen Martinisb6a50492018-09-12 23:59:32772
Stephen Martinisb72f6d22018-10-04 23:29:01773 def apply_mixin(self, mixin, test):
774 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:32775
Stephen Martinis0382bc12018-09-17 22:29:07776 Mixins will not override an existing key. This is to ensure exceptions can
777 override a setting a mixin applies.
778
Stephen Martinisb72f6d22018-10-04 23:29:01779 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:32780 'dimension_sets', which is how normal test suites specify their dimensions,
781 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
782 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:01783
Stephen Martinisb6a50492018-09-12 23:59:32784 """
785 new_test = copy.deepcopy(test)
786 mixin = copy.deepcopy(mixin)
787
Stephen Martinisb72f6d22018-10-04 23:29:01788 if 'swarming' in mixin:
789 swarming_mixin = mixin['swarming']
790 new_test.setdefault('swarming', {})
791 if 'dimensions' in swarming_mixin:
792 new_test['swarming'].setdefault('dimension_sets', [{}])
793 for dimension_set in new_test['swarming']['dimension_sets']:
794 dimension_set.update(swarming_mixin['dimensions'])
795 del swarming_mixin['dimensions']
Stephen Martinisb6a50492018-09-12 23:59:32796
Stephen Martinisb72f6d22018-10-04 23:29:01797 # python dict update doesn't do recursion at all. Just hard code the
798 # nested update we need (mixin['swarming'] shouldn't clobber
799 # test['swarming'], but should update it).
800 new_test['swarming'].update(swarming_mixin)
801 del mixin['swarming']
802
Wezc0e835b702018-10-30 00:38:41803 if '$mixin_append' in mixin:
804 # Values specified under $mixin_append should be appended to existing
805 # lists, rather than replacing them.
806 mixin_append = mixin['$mixin_append']
807 for key in mixin_append:
808 new_test.setdefault(key, [])
809 if not isinstance(mixin_append[key], list):
810 raise BBGenErr(
811 'Key "' + key + '" in $mixin_append must be a list.')
812 if not isinstance(new_test[key], list):
813 raise BBGenErr(
814 'Cannot apply $mixin_append to non-list "' + key + '".')
815 new_test[key].extend(mixin_append[key])
816 if 'args' in mixin_append:
817 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
818 del mixin['$mixin_append']
819
Stephen Martinisb72f6d22018-10-04 23:29:01820 new_test.update(mixin)
Stephen Martinis0382bc12018-09-17 22:29:07821
Stephen Martinisb6a50492018-09-12 23:59:32822 return new_test
823
Kenneth Russelleb60cbd22017-12-05 07:54:28824 def generate_waterfall_json(self, waterfall):
825 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28826 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:01827 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:43828 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28829 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43830 # Copy only well-understood entries in the machine's configuration
831 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28832 if 'additional_compile_targets' in config:
833 tests['additional_compile_targets'] = config[
834 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43835 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28836 if test_type not in generator_map:
837 raise self.unknown_test_suite_type(
838 test_type, name, waterfall['name']) # pragma: no cover
839 test_generator = generator_map[test_type]
Nico Weber79dc5f6852018-07-13 19:38:49840 # Let multiple kinds of generators generate the same kinds
841 # of tests. For example, gpu_telemetry_tests are a
842 # specialization of isolated_scripts.
843 new_tests = test_generator.generate(
844 waterfall, name, config, input_tests)
845 remapped_test_type = test_type_remapper.get(test_type, test_type)
846 tests[remapped_test_type] = test_generator.sort(
847 tests.get(remapped_test_type, []) + new_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28848 all_tests[name] = tests
849 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
850 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
851 return json.dumps(all_tests, indent=2, separators=(',', ': '),
852 sort_keys=True) + '\n'
853
854 def generate_waterfalls(self): # pragma: no cover
855 self.load_configuration_files()
856 self.resolve_configuration_files()
857 filters = self.args.waterfall_filters
858 suffix = '.json'
859 if self.args.new_files:
860 suffix = '.new' + suffix
861 for waterfall in self.waterfalls:
862 should_gen = not filters or waterfall['name'] in filters
863 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:11864 file_path = waterfall['name'] + suffix
865 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:28866 self.generate_waterfall_json(waterfall))
867
Nico Weberd18b8962018-05-16 19:39:38868 def get_valid_bot_names(self):
869 # Extract bot names from infra/config/global/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:42870 # NOTE: This reference can cause issues; if a file changes there, the
871 # presubmit here won't be run by default. A manually maintained list there
872 # tries to run presubmit here when luci-milo.cfg is changed. If any other
873 # references to configs outside of this directory are added, please change
874 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
875 # never ends up in an invalid state.
Nico Weberd18b8962018-05-16 19:39:38876 bot_names = set()
John Budorickc12abd12018-08-14 19:37:43877 infra_config_dir = os.path.abspath(
878 os.path.join(os.path.dirname(__file__),
879 '..', '..', 'infra', 'config', 'global'))
880 milo_configs = [
881 os.path.join(infra_config_dir, 'luci-milo.cfg'),
882 os.path.join(infra_config_dir, 'luci-milo-dev.cfg'),
883 ]
884 for c in milo_configs:
885 for l in self.read_file(c).splitlines():
886 if (not 'name: "buildbucket/luci.chromium.' in l and
Hans Wennborg98ffd7d92019-02-06 14:14:34887 not 'name: "buildbucket/luci.chrome.' in l and
John Budorickb1833612018-12-07 04:36:41888 not 'name: "buildbot/chromium.' in l and
889 not 'name: "buildbot/tryserver.chromium.' in l):
John Budorickc12abd12018-08-14 19:37:43890 continue
891 # l looks like
892 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
893 # Extract win_chromium_dbg_ng part.
894 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:38895 return bot_names
896
Kenneth Russell8a386d42018-06-02 09:48:01897 def get_bots_that_do_not_actually_exist(self):
898 # Some of the bots on the chromium.gpu.fyi waterfall in particular
899 # are defined only to be mirrored into trybots, and don't actually
900 # exist on any of the waterfalls or consoles.
901 return [
Jamie Madilldc7feeb82018-11-14 04:54:56902 'ANGLE GPU Win10 Release (Intel HD 630)',
903 'ANGLE GPU Win10 Release (NVIDIA)',
Corentin Wallez7d3f4fa22018-11-19 23:35:44904 'Dawn GPU Linux Release (Intel HD 630)',
905 'Dawn GPU Linux Release (NVIDIA)',
906 'Dawn GPU Mac Release (Intel)',
907 'Dawn GPU Mac Retina Release (AMD)',
908 'Dawn GPU Mac Retina Release (NVIDIA)',
Jamie Madilldc7feeb82018-11-14 04:54:56909 'Dawn GPU Win10 Release (Intel HD 630)',
910 'Dawn GPU Win10 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:01911 'Optional Android Release (Nexus 5X)',
912 'Optional Linux Release (Intel HD 630)',
913 'Optional Linux Release (NVIDIA)',
914 'Optional Mac Release (Intel)',
915 'Optional Mac Retina Release (AMD)',
916 'Optional Mac Retina Release (NVIDIA)',
917 'Optional Win10 Release (Intel HD 630)',
918 'Optional Win10 Release (NVIDIA)',
919 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:08920 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:29921 'linux-blink-rel-dummy',
922 'mac10.10-blink-rel-dummy',
923 'mac10.11-blink-rel-dummy',
924 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:20925 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:29926 'mac10.13-blink-rel-dummy',
927 'win7-blink-rel-dummy',
928 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:08929 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:33930 'WebKit Linux composite_after_paint Dummy Builder',
Nico Weber7fc8b9da2018-06-08 19:22:08931 'WebKit Linux layout_ng Dummy Builder',
932 'WebKit Linux root_layer_scrolls Dummy Builder',
Stephen Martinis769b25112018-08-30 18:52:06933 # chromium, due to https://crbug.com/878915
934 'win-dbg',
935 'win32-dbg',
Kenneth Russell8a386d42018-06-02 09:48:01936 ]
937
Stephen Martinisf83893722018-09-19 00:02:18938 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:20939 self.check_input_files_sorting(verbose)
940
Kenneth Russelleb60cbd22017-12-05 07:54:28941 self.load_configuration_files()
Stephen Martinis54d64ad2018-09-21 22:16:20942 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:28943 self.check_composition_test_suites()
Nico Weberd18b8962018-05-16 19:39:38944
945 # All bots should exist.
946 bot_names = self.get_valid_bot_names()
Kenneth Russell8a386d42018-06-02 09:48:01947 bots_that_dont_exist = self.get_bots_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:38948 for waterfall in self.waterfalls:
949 for bot_name in waterfall['machines']:
Kenneth Russell8a386d42018-06-02 09:48:01950 if bot_name in bots_that_dont_exist:
951 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38952 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:08953 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:38954 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:52955 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:32956 if waterfall['name'] in ['tryserver.webrtc',
957 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:38958 # These waterfalls have their bot configs in a different repo.
959 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:52960 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38961 raise self.unknown_bot(bot_name, waterfall['name'])
962
Kenneth Russelleb60cbd22017-12-05 07:54:28963 # All test suites must be referenced.
964 suites_seen = set()
965 generator_map = self.get_test_generator_map()
966 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43967 for bot_name, tester in waterfall['machines'].iteritems():
968 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28969 if suite_type not in generator_map:
970 raise self.unknown_test_suite_type(suite_type, bot_name,
971 waterfall['name'])
972 if suite not in self.test_suites:
973 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
974 suites_seen.add(suite)
975 # Since we didn't resolve the configuration files, this set
976 # includes both composition test suites and regular ones.
977 resolved_suites = set()
978 for suite_name in suites_seen:
979 suite = self.test_suites[suite_name]
980 if isinstance(suite, list):
981 for sub_suite in suite:
982 resolved_suites.add(sub_suite)
983 resolved_suites.add(suite_name)
984 # At this point, every key in test_suites.pyl should be referenced.
985 missing_suites = set(self.test_suites.keys()) - resolved_suites
986 if missing_suites:
987 raise BBGenErr('The following test suites were unreferenced by bots on '
988 'the waterfalls: ' + str(missing_suites))
989
990 # All test suite exceptions must refer to bots on the waterfall.
991 all_bots = set()
992 missing_bots = set()
993 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43994 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28995 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:28996 # In order to disambiguate between bots with the same name on
997 # different waterfalls, support has been added to various
998 # exceptions for concatenating the waterfall name after the bot
999 # name.
1000 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281001 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381002 removals = (exception.get('remove_from', []) +
1003 exception.get('remove_gtest_from', []) +
1004 exception.get('modifications', {}).keys())
1005 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281006 if removal not in all_bots:
1007 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411008
1009 missing_bots = missing_bots - set(bots_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281010 if missing_bots:
1011 raise BBGenErr('The following nonexistent machines were referenced in '
1012 'the test suite exceptions: ' + str(missing_bots))
1013
Stephen Martinis0382bc12018-09-17 22:29:071014 # All mixins must be referenced
1015 seen_mixins = set()
1016 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011017 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071018 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011019 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071020 for suite in self.test_suites.values():
1021 if isinstance(suite, list):
1022 # Don't care about this, it's a composition, which shouldn't include a
1023 # swarming mixin.
1024 continue
1025
1026 for test in suite.values():
1027 if not isinstance(test, dict):
1028 # Some test suites have top level keys, which currently can't be
1029 # swarming mixin entries. Ignore them
1030 continue
1031
Stephen Martinisb72f6d22018-10-04 23:29:011032 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071033
Stephen Martinisb72f6d22018-10-04 23:29:011034 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071035 if missing_mixins:
1036 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1037 ' referenced in a waterfall, machine, or test suite.' % (
1038 str(missing_mixins)))
1039
Stephen Martinis54d64ad2018-09-21 22:16:201040
1041 def type_assert(self, node, typ, filename, verbose=False):
1042 """Asserts that the Python AST node |node| is of type |typ|.
1043
1044 If verbose is set, it prints out some helpful context lines, showing where
1045 exactly the error occurred in the file.
1046 """
1047 if not isinstance(node, typ):
1048 if verbose:
1049 lines = [""] + self.read_file(filename).splitlines()
1050
1051 context = 2
1052 lines_start = max(node.lineno - context, 0)
1053 # Add one to include the last line
1054 lines_end = min(node.lineno + context, len(lines)) + 1
1055 lines = (
1056 ['== %s ==\n' % filename] +
1057 ["<snip>\n"] +
1058 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1059 lines[lines_start:lines_start + context])] +
1060 ['-' * 80 + '\n'] +
1061 ['%d %s' % (node.lineno, lines[node.lineno])] +
1062 ['-' * (node.col_offset + 3) + '^' + '-' * (
1063 80 - node.col_offset - 4) + '\n'] +
1064 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1065 lines[node.lineno + 1:lines_end])] +
1066 ["<snip>\n"]
1067 )
1068 # Print out a useful message when a type assertion fails.
1069 for l in lines:
1070 self.print_line(l.strip())
1071
1072 node_dumped = ast.dump(node, annotate_fields=False)
1073 # If the node is huge, truncate it so everything fits in a terminal
1074 # window.
1075 if len(node_dumped) > 60: # pragma: no cover
1076 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1077 raise BBGenErr(
1078 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1079 ' be %s, is %s' % (
1080 filename, node_dumped,
1081 node.lineno, typ, type(node)))
1082
1083 def ensure_ast_dict_keys_sorted(self, node, filename, verbose):
1084 is_valid = True
1085
1086 keys = []
1087 # The keys of this dict are ordered as ordered in the file; normal python
1088 # dictionary keys are given an arbitrary order, but since we parsed the
1089 # file itself, the order as given in the file is preserved.
1090 for key in node.keys:
1091 self.type_assert(key, ast.Str, filename, verbose)
1092 keys.append(key.s)
1093
1094 keys_sorted = sorted(keys)
1095 if keys_sorted != keys:
1096 is_valid = False
1097 if verbose:
1098 for line in difflib.unified_diff(
1099 keys,
1100 keys_sorted, fromfile='current (%r)' % filename, tofile='sorted'):
1101 self.print_line(line)
1102
1103 if len(set(keys)) != len(keys):
1104 for i in range(len(keys_sorted)-1):
1105 if keys_sorted[i] == keys_sorted[i+1]:
1106 self.print_line('Key %s is duplicated' % keys_sorted[i])
1107 is_valid = False
1108 return is_valid
Stephen Martinisf83893722018-09-19 00:02:181109
1110 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201111 # TODO(https://crbug.com/886993): Add the ability for this script to
1112 # actually format the files, rather than just complain if they're
1113 # incorrectly formatted.
1114 bad_files = set()
1115
1116 for filename in (
Stephen Martinisb72f6d22018-10-04 23:29:011117 'mixins.pyl',
Stephen Martinis54d64ad2018-09-21 22:16:201118 'test_suites.pyl',
1119 'test_suite_exceptions.pyl',
1120 ):
Stephen Martinisf83893722018-09-19 00:02:181121 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1122
Stephen Martinisf83893722018-09-19 00:02:181123 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201124 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181125 module = parsed.body
1126
1127 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201128 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181129 if len(module) != 1: # pragma: no cover
1130 raise BBGenErr('Invalid .pyl file %s' % filename)
1131 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201132 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181133
1134 # Value should be a dictionary.
1135 value = expr.value
Stephen Martinis54d64ad2018-09-21 22:16:201136 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181137
Stephen Martinis54d64ad2018-09-21 22:16:201138 if filename == 'test_suites.pyl':
1139 expected_keys = ['basic_suites', 'compound_suites']
1140 actual_keys = [node.s for node in value.keys]
1141 assert all(key in expected_keys for key in actual_keys), (
1142 'Invalid %r file; expected keys %r, got %r' % (
1143 filename, expected_keys, actual_keys))
1144 suite_dicts = [node for node in value.values]
1145 # Only two keys should mean only 1 or 2 values
1146 assert len(suite_dicts) <= 2
1147 for suite_group in suite_dicts:
1148 if not self.ensure_ast_dict_keys_sorted(
1149 suite_group, filename, verbose):
1150 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181151
Stephen Martinis54d64ad2018-09-21 22:16:201152 else:
1153 if not self.ensure_ast_dict_keys_sorted(
1154 value, filename, verbose):
1155 bad_files.add(filename)
1156
1157 # waterfalls.pyl is slightly different, just do it manually here
1158 filename = 'waterfalls.pyl'
1159 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1160
1161 # Must be a module.
1162 self.type_assert(parsed, ast.Module, filename, verbose)
1163 module = parsed.body
1164
1165 # Only one expression in the module.
1166 self.type_assert(module, list, filename, verbose)
1167 if len(module) != 1: # pragma: no cover
1168 raise BBGenErr('Invalid .pyl file %s' % filename)
1169 expr = module[0]
1170 self.type_assert(expr, ast.Expr, filename, verbose)
1171
1172 # Value should be a list.
1173 value = expr.value
1174 self.type_assert(value, ast.List, filename, verbose)
1175
1176 keys = []
1177 for val in value.elts:
1178 self.type_assert(val, ast.Dict, filename, verbose)
1179 waterfall_name = None
1180 for key, val in zip(val.keys, val.values):
1181 self.type_assert(key, ast.Str, filename, verbose)
1182 if key.s == 'machines':
1183 if not self.ensure_ast_dict_keys_sorted(val, filename, verbose):
1184 bad_files.add(filename)
1185
1186 if key.s == "name":
1187 self.type_assert(val, ast.Str, filename, verbose)
1188 waterfall_name = val.s
1189 assert waterfall_name
1190 keys.append(waterfall_name)
1191
1192 if sorted(keys) != keys:
1193 bad_files.add(filename)
1194 if verbose: # pragma: no cover
1195 for line in difflib.unified_diff(
1196 keys,
1197 sorted(keys), fromfile='current', tofile='sorted'):
1198 self.print_line(line)
Stephen Martinisf83893722018-09-19 00:02:181199
1200 if bad_files:
1201 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201202 'The following files have invalid keys: %s\n. They are either '
1203 'unsorted, or have duplicates.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181204
Kenneth Russelleb60cbd22017-12-05 07:54:281205 def check_output_file_consistency(self, verbose=False):
1206 self.load_configuration_files()
1207 # All waterfalls must have been written by this script already.
1208 self.resolve_configuration_files()
1209 ungenerated_waterfalls = set()
1210 for waterfall in self.waterfalls:
1211 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111212 file_path = waterfall['name'] + '.json'
1213 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281214 if expected != current:
1215 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321216 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501217 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281218 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321219 'contents:')
1220 for line in difflib.unified_diff(
1221 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501222 current.splitlines(),
1223 fromfile='expected', tofile='current'):
1224 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281225 if ungenerated_waterfalls:
1226 raise BBGenErr('The following waterfalls have not been properly '
1227 'autogenerated by generate_buildbot_json.py: ' +
1228 str(ungenerated_waterfalls))
1229
1230 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501231 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281232 self.check_output_file_consistency(verbose) # pragma: no cover
1233
1234 def parse_args(self, argv): # pragma: no cover
1235 parser = argparse.ArgumentParser()
1236 parser.add_argument(
1237 '-c', '--check', action='store_true', help=
1238 'Do consistency checks of configuration and generated files and then '
1239 'exit. Used during presubmit. Causes the tool to not generate any files.')
1240 parser.add_argument(
1241 '-n', '--new-files', action='store_true', help=
1242 'Write output files as .new.json. Useful during development so old and '
1243 'new files can be looked at side-by-side.')
1244 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501245 '-v', '--verbose', action='store_true', help=
1246 'Increases verbosity. Affects consistency checks.')
1247 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281248 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1249 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111250 parser.add_argument(
1251 '--pyl-files-dir', type=os.path.realpath,
1252 help='Path to the directory containing the input .pyl files.')
Kenneth Russelleb60cbd22017-12-05 07:54:281253 self.args = parser.parse_args(argv)
1254
1255 def main(self, argv): # pragma: no cover
1256 self.parse_args(argv)
1257 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:501258 self.check_consistency(verbose=self.args.verbose)
Kenneth Russelleb60cbd22017-12-05 07:54:281259 else:
1260 self.generate_waterfalls()
1261 return 0
1262
1263if __name__ == "__main__": # pragma: no cover
1264 generator = BBJSONGenerator()
1265 sys.exit(generator.main(sys.argv[1:]))