blob: 0e2de83ea2f6ac3b2a81f29c55e437a527af46be [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
Kenneth Russell8a386d42018-06-02 09:48:01233 def is_linux(self, tester_config):
234 return tester_config.get('os_type') == 'linux'
235
Kenneth Russelleb60cbd22017-12-05 07:54:28236 def get_exception_for_test(self, test_name, test_config):
237 # gtests may have both "test" and "name" fields, and usually, if the "name"
238 # field is specified, it means that the same test is being repurposed
239 # multiple times with different command line arguments. To handle this case,
240 # prefer to lookup per the "name" field of the test itself, as opposed to
241 # the "test_name", which is actually the "test" field.
242 if 'name' in test_config:
243 return self.exceptions.get(test_config['name'])
244 else:
245 return self.exceptions.get(test_name)
246
Nico Weberb0b3f5862018-07-13 18:45:15247 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28248 # Currently, the only reason a test should not run on a given tester is that
249 # it's in the exceptions. (Once the GPU waterfall generation script is
250 # incorporated here, the rules will become more complex.)
251 exception = self.get_exception_for_test(test_name, test_config)
252 if not exception:
253 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28254 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28255 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28256 if remove_from:
257 if tester_name in remove_from:
258 return False
259 # TODO(kbr): this code path was added for some tests (including
260 # android_webview_unittests) on one machine (Nougat Phone
261 # Tester) which exists with the same name on two waterfalls,
262 # chromium.android and chromium.fyi; the tests are run on one
263 # but not the other. Once the bots are all uniquely named (a
264 # different ongoing project) this code should be removed.
265 # TODO(kbr): add coverage.
266 return (tester_name + ' ' + waterfall['name']
267 not in remove_from) # pragma: no cover
268 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28269
Nico Weber79dc5f6852018-07-13 19:38:49270 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28271 exception = self.get_exception_for_test(test_name, test)
272 if not exception:
273 return None
Nico Weber79dc5f6852018-07-13 19:38:49274 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28275
Kenneth Russell8a386d42018-06-02 09:48:01276 def merge_command_line_args(self, arr, prefix, splitter):
277 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01278 idx = 0
279 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01280 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01281 while idx < len(arr):
282 flag = arr[idx]
283 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01284 if flag.startswith(prefix):
285 arg = flag[prefix_len:]
286 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01287 if first_idx < 0:
288 first_idx = idx
289 else:
290 delete_current_entry = True
291 if delete_current_entry:
292 del arr[idx]
293 else:
294 idx += 1
295 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01296 arr[first_idx] = prefix + splitter.join(accumulated_args)
297 return arr
298
299 def maybe_fixup_args_array(self, arr):
300 # The incoming array of strings may be an array of command line
301 # arguments. To make it easier to turn on certain features per-bot or
302 # per-test-suite, look specifically for certain flags and merge them
303 # appropriately.
304 # --enable-features=Feature1 --enable-features=Feature2
305 # are merged to:
306 # --enable-features=Feature1,Feature2
307 # and:
308 # --extra-browser-args=arg1 --extra-browser-args=arg2
309 # are merged to:
310 # --extra-browser-args=arg1 arg2
311 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
312 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01313 return arr
314
Kenneth Russelleb60cbd22017-12-05 07:54:28315 def dictionary_merge(self, a, b, path=None, update=True):
316 """http://stackoverflow.com/questions/7204805/
317 python-dictionaries-of-dictionaries-merge
318 merges b into a
319 """
320 if path is None:
321 path = []
322 for key in b:
323 if key in a:
324 if isinstance(a[key], dict) and isinstance(b[key], dict):
325 self.dictionary_merge(a[key], b[key], path + [str(key)])
326 elif a[key] == b[key]:
327 pass # same leaf value
328 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06329 # Args arrays are lists of strings. Just concatenate them,
330 # and don't sort them, in order to keep some needed
331 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28332 if all(isinstance(x, str)
333 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01334 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28335 else:
336 # TODO(kbr): this only works properly if the two arrays are
337 # the same length, which is currently always the case in the
338 # swarming dimension_sets that we have to merge. It will fail
339 # to merge / override 'args' arrays which are different
340 # length.
341 for idx in xrange(len(b[key])):
342 try:
343 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
344 path + [str(key), str(idx)],
345 update=update)
346 except (IndexError, TypeError): # pragma: no cover
347 raise BBGenErr('Error merging list keys ' + str(key) +
348 ' and indices ' + str(idx) + ' between ' +
349 str(a) + ' and ' + str(b)) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28350 elif update: # pragma: no cover
351 a[key] = b[key] # pragma: no cover
352 else:
353 raise BBGenErr('Conflict at %s' % '.'.join(
354 path + [str(key)])) # pragma: no cover
355 else:
356 a[key] = b[key]
357 return a
358
John Budorickab108712018-09-01 00:12:21359 def initialize_args_for_test(
360 self, generated_test, tester_config, additional_arg_keys=None):
361
362 args = []
363 args.extend(generated_test.get('args', []))
364 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22365
Kenneth Russell8a386d42018-06-02 09:48:01366 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21367 val = generated_test.pop(key, [])
368 if fn(tester_config):
369 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01370
371 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
372 add_conditional_args('linux_args', self.is_linux)
373 add_conditional_args('android_args', self.is_android)
374
John Budorickab108712018-09-01 00:12:21375 for key in additional_arg_keys or []:
376 args.extend(generated_test.pop(key, []))
377 args.extend(tester_config.get(key, []))
378
379 if args:
380 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01381
Kenneth Russelleb60cbd22017-12-05 07:54:28382 def initialize_swarming_dictionary_for_test(self, generated_test,
383 tester_config):
384 if 'swarming' not in generated_test:
385 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28386 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
387 generated_test['swarming'].update({
388 'can_use_on_swarming_builders': tester_config.get('use_swarming', True)
389 })
Kenneth Russelleb60cbd22017-12-05 07:54:28390 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03391 if ('dimension_sets' not in generated_test['swarming'] and
392 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28393 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
394 tester_config['swarming']['dimension_sets'])
395 self.dictionary_merge(generated_test['swarming'],
396 tester_config['swarming'])
397 # Apply any Android-specific Swarming dimensions after the generic ones.
398 if 'android_swarming' in generated_test:
399 if self.is_android(tester_config): # pragma: no cover
400 self.dictionary_merge(
401 generated_test['swarming'],
402 generated_test['android_swarming']) # pragma: no cover
403 del generated_test['android_swarming'] # pragma: no cover
404
405 def clean_swarming_dictionary(self, swarming_dict):
406 # Clean out redundant entries from a test's "swarming" dictionary.
407 # This is really only needed to retain 100% parity with the
408 # handwritten JSON files, and can be removed once all the files are
409 # autogenerated.
410 if 'shards' in swarming_dict:
411 if swarming_dict['shards'] == 1: # pragma: no cover
412 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24413 if 'hard_timeout' in swarming_dict:
414 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
415 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43416 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28417 # Remove all other keys.
418 for k in swarming_dict.keys(): # pragma: no cover
419 if k != 'can_use_on_swarming_builders': # pragma: no cover
420 del swarming_dict[k] # pragma: no cover
421
Stephen Martinis0382bc12018-09-17 22:29:07422 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
423 waterfall):
424 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01425 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07426 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28427 # See if there are any exceptions that need to be merged into this
428 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49429 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28430 if modifications:
431 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23432 if 'swarming' in test:
433 self.clean_swarming_dictionary(test['swarming'])
Kenneth Russelleb60cbd22017-12-05 07:54:28434 return test
435
Shenghua Zhangaba8bad2018-02-07 02:12:09436 def add_common_test_properties(self, test, tester_config):
437 if tester_config.get('use_multi_dimension_trigger_script'):
Kenneth Russell73c3bd8b2018-10-19 22:30:19438 # Assumes update_and_cleanup_test has already been called, so the
439 # builder's mixins have been flattened into the test.
Shenghua Zhangaba8bad2018-02-07 02:12:09440 test['trigger_script'] = {
441 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
442 'args': [
443 '--multiple-trigger-configs',
Kenneth Russell73c3bd8b2018-10-19 22:30:19444 json.dumps(test['swarming']['dimension_sets'] +
Shenghua Zhangaba8bad2018-02-07 02:12:09445 tester_config.get('alternate_swarming_dimensions', [])),
446 '--multiple-dimension-script-verbose',
447 'True'
448 ],
449 }
450
Ben Pastene858f4be2019-01-09 23:52:09451 def add_android_presentation_args(self, tester_config, test_name, result):
452 args = result.get('args', [])
453 args.append('--gs-results-bucket=chromium-result-details')
454 if (result['swarming']['can_use_on_swarming_builders'] and not
455 tester_config.get('skip_merge_script', False)):
456 result['merge'] = {
457 'args': [
458 '--bucket',
459 'chromium-result-details',
460 '--test-name',
461 test_name
462 ],
463 'script': '//build/android/pylib/results/presentation/'
464 'test_results_presentation.py',
465 }
466 if not tester_config.get('skip_cipd_packages', False):
467 result['swarming']['cipd_packages'] = [
468 {
469 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
470 'location': 'bin',
471 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
472 }
473 ]
474 if not tester_config.get('skip_output_links', False):
475 result['swarming']['output_links'] = [
476 {
477 'link': [
478 'https://luci-logdog.appspot.com/v/?s',
479 '=android%2Fswarming%2Flogcats%2F',
480 '${TASK_ID}%2F%2B%2Funified_logcats',
481 ],
482 'name': 'shard #${SHARD_INDEX} logcats',
483 },
484 ]
485 if args:
486 result['args'] = args
487
Kenneth Russelleb60cbd22017-12-05 07:54:28488 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
489 test_config):
490 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15491 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28492 return None
493 result = copy.deepcopy(test_config)
494 if 'test' in result:
495 result['name'] = test_name
496 else:
497 result['test'] = test_name
498 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21499
500 self.initialize_args_for_test(
501 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28502 if self.is_android(tester_config) and tester_config.get('use_swarming',
503 True):
Ben Pastene858f4be2019-01-09 23:52:09504 self.add_android_presentation_args(tester_config, test_name, result)
505 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42506
Stephen Martinis0382bc12018-09-17 22:29:07507 result = self.update_and_cleanup_test(
508 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09509 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28510 return result
511
512 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
513 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01514 if not self.should_run_on_tester(waterfall, tester_name, test_name,
515 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28516 return None
517 result = copy.deepcopy(test_config)
518 result['isolate_name'] = result.get('isolate_name', test_name)
519 result['name'] = test_name
520 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01521 self.initialize_args_for_test(result, tester_config)
Ben Pastene858f4be2019-01-09 23:52:09522 if tester_config.get('use_android_presentation', False):
523 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07524 result = self.update_and_cleanup_test(
525 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09526 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28527 return result
528
529 def generate_script_test(self, waterfall, tester_name, tester_config,
530 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01531 if not self.should_run_on_tester(waterfall, tester_name, test_name,
532 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28533 return None
534 result = {
535 'name': test_name,
536 'script': test_config['script']
537 }
Stephen Martinis0382bc12018-09-17 22:29:07538 result = self.update_and_cleanup_test(
539 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28540 return result
541
542 def generate_junit_test(self, waterfall, tester_name, tester_config,
543 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01544 del tester_config
545 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 'test': test_name,
550 }
551 return result
552
553 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
554 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01555 if not self.should_run_on_tester(waterfall, tester_name, test_name,
556 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28557 return None
558 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28559 if 'test' in result and result['test'] != test_name:
560 result['name'] = test_name
561 else:
562 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07563 result = self.update_and_cleanup_test(
564 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28565 return result
566
Stephen Martinis2a0667022018-09-25 22:31:14567 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01568 substitutions = {
569 # Any machine in waterfalls.pyl which desires to run GPU tests
570 # must provide the os_type key.
571 'os_type': tester_config['os_type'],
572 'gpu_vendor_id': '0',
573 'gpu_device_id': '0',
574 }
Stephen Martinis2a0667022018-09-25 22:31:14575 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01576 if 'gpu' in dimension_set:
577 # First remove the driver version, then split into vendor and device.
578 gpu = dimension_set['gpu']
579 gpu = gpu.split('-')[0].split(':')
580 substitutions['gpu_vendor_id'] = gpu[0]
581 substitutions['gpu_device_id'] = gpu[1]
582 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
583
584 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
585 test_name, test_config):
586 # These are all just specializations of isolated script tests with
587 # a bunch of boilerplate command line arguments added.
588
589 # The step name must end in 'test' or 'tests' in order for the
590 # results to automatically show up on the flakiness dashboard.
591 # (At least, this was true some time ago.) Continue to use this
592 # naming convention for the time being to minimize changes.
593 step_name = test_config.get('name', test_name)
594 if not (step_name.endswith('test') or step_name.endswith('tests')):
595 step_name = '%s_tests' % step_name
596 result = self.generate_isolated_script_test(
597 waterfall, tester_name, tester_config, step_name, test_config)
598 if not result:
599 return None
600 result['isolate_name'] = 'telemetry_gpu_integration_test'
601 args = result.get('args', [])
602 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14603
604 # These tests upload and download results from cloud storage and therefore
605 # aren't idempotent yet. https://crbug.com/549140.
606 result['swarming']['idempotent'] = False
607
Kenneth Russell44910c32018-12-03 23:35:11608 # The GPU tests act much like integration tests for the entire browser, and
609 # tend to uncover flakiness bugs more readily than other test suites. In
610 # order to surface any flakiness more readily to the developer of the CL
611 # which is introducing it, we disable retries with patch on the commit
612 # queue.
613 result['should_retry_with_patch'] = False
614
Kenneth Russell8a386d42018-06-02 09:48:01615 args = [
616 test_to_run,
617 '--show-stdout',
618 '--browser=%s' % tester_config['browser_config'],
619 # --passthrough displays more of the logging in Telemetry when
620 # run via typ, in particular some of the warnings about tests
621 # being expected to fail, but passing.
622 '--passthrough',
623 '-v',
624 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
625 ] + args
626 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14627 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01628 return result
629
Kenneth Russelleb60cbd22017-12-05 07:54:28630 def get_test_generator_map(self):
631 return {
632 'cts_tests': CTSGenerator(self),
Kenneth Russell8a386d42018-06-02 09:48:01633 'gpu_telemetry_tests': GPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28634 'gtest_tests': GTestGenerator(self),
635 'instrumentation_tests': InstrumentationTestGenerator(self),
636 'isolated_scripts': IsolatedScriptTestGenerator(self),
637 'junit_tests': JUnitGenerator(self),
638 'scripts': ScriptGenerator(self),
639 }
640
Kenneth Russell8a386d42018-06-02 09:48:01641 def get_test_type_remapper(self):
642 return {
643 # These are a specialization of isolated_scripts with a bunch of
644 # boilerplate command line arguments added to each one.
645 'gpu_telemetry_tests': 'isolated_scripts',
646 }
647
Kenneth Russelleb60cbd22017-12-05 07:54:28648 def check_composition_test_suites(self):
649 # Pre-pass to catch errors reliably.
650 for name, value in self.test_suites.iteritems():
651 if isinstance(value, list):
652 for entry in value:
653 if isinstance(self.test_suites[entry], list):
Nico Weberd18b8962018-05-16 19:39:38654 raise BBGenErr('Composition test suites may not refer to other '
655 'composition test suites (error found while '
Kenneth Russelleb60cbd22017-12-05 07:54:28656 'processing %s)' % name)
657
Stephen Martinis54d64ad2018-09-21 22:16:20658 def flatten_test_suites(self):
659 new_test_suites = {}
660 for name, value in self.test_suites.get('basic_suites', {}).iteritems():
661 new_test_suites[name] = value
662 for name, value in self.test_suites.get('compound_suites', {}).iteritems():
663 if name in new_test_suites:
664 raise BBGenErr('Composition test suite names may not duplicate basic '
665 'test suite names (error found while processsing %s' % (
666 name))
667 new_test_suites[name] = value
668 self.test_suites = new_test_suites
669
Kenneth Russelleb60cbd22017-12-05 07:54:28670 def resolve_composition_test_suites(self):
Stephen Martinis54d64ad2018-09-21 22:16:20671 self.flatten_test_suites()
672
Kenneth Russelleb60cbd22017-12-05 07:54:28673 self.check_composition_test_suites()
674 for name, value in self.test_suites.iteritems():
675 if isinstance(value, list):
676 # Resolve this to a dictionary.
677 full_suite = {}
678 for entry in value:
679 suite = self.test_suites[entry]
680 full_suite.update(suite)
681 self.test_suites[name] = full_suite
682
683 def link_waterfalls_to_test_suites(self):
684 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43685 for tester_name, tester in waterfall['machines'].iteritems():
686 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28687 if not value in self.test_suites:
688 # Hard / impossible to cover this in the unit test.
689 raise self.unknown_test_suite(
690 value, tester_name, waterfall['name']) # pragma: no cover
691 tester['test_suites'][suite] = self.test_suites[value]
692
693 def load_configuration_files(self):
694 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
695 self.test_suites = self.load_pyl_file('test_suites.pyl')
696 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:01697 self.mixins = self.load_pyl_file('mixins.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:28698
699 def resolve_configuration_files(self):
700 self.resolve_composition_test_suites()
701 self.link_waterfalls_to_test_suites()
702
Nico Weberd18b8962018-05-16 19:39:38703 def unknown_bot(self, bot_name, waterfall_name):
704 return BBGenErr(
705 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
706
Kenneth Russelleb60cbd22017-12-05 07:54:28707 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
708 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38709 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28710 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
711
712 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
713 return BBGenErr(
714 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
715 ' on waterfall ' + waterfall_name)
716
Stephen Martinisb72f6d22018-10-04 23:29:01717 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:07718 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:32719
720 Checks in the waterfall, builder, and test objects for mixins.
721 """
722 def valid_mixin(mixin_name):
723 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:01724 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:32725 raise BBGenErr("bad mixin %s" % mixin_name)
726 def must_be_list(mixins, typ, name):
727 """Asserts that given mixins are a list."""
728 if not isinstance(mixins, list):
729 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
730
Stephen Martinisb72f6d22018-10-04 23:29:01731 if 'mixins' in waterfall:
732 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
733 for mixin in waterfall['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32734 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01735 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32736
Stephen Martinisb72f6d22018-10-04 23:29:01737 if 'mixins' in builder:
738 must_be_list(builder['mixins'], 'builder', builder_name)
739 for mixin in builder['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32740 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01741 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32742
Stephen Martinisb72f6d22018-10-04 23:29:01743 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:07744 return test
745
Stephen Martinis2a0667022018-09-25 22:31:14746 test_name = test.get('name')
747 if not test_name:
748 test_name = test.get('test')
749 if not test_name: # pragma: no cover
750 # Not the best name, but we should say something.
751 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:01752 must_be_list(test['mixins'], 'test', test_name)
753 for mixin in test['mixins']:
Stephen Martinis0382bc12018-09-17 22:29:07754 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01755 test = self.apply_mixin(self.mixins[mixin], test)
756 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:07757 return test
Stephen Martinisb6a50492018-09-12 23:59:32758
Stephen Martinisb72f6d22018-10-04 23:29:01759 def apply_mixin(self, mixin, test):
760 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:32761
Stephen Martinis0382bc12018-09-17 22:29:07762 Mixins will not override an existing key. This is to ensure exceptions can
763 override a setting a mixin applies.
764
Stephen Martinisb72f6d22018-10-04 23:29:01765 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:32766 'dimension_sets', which is how normal test suites specify their dimensions,
767 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
768 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:01769
Stephen Martinisb6a50492018-09-12 23:59:32770 """
Stephen Martinisb6a50492018-09-12 23:59:32771 new_test = copy.deepcopy(test)
772 mixin = copy.deepcopy(mixin)
773
Stephen Martinisb72f6d22018-10-04 23:29:01774 if 'swarming' in mixin:
775 swarming_mixin = mixin['swarming']
776 new_test.setdefault('swarming', {})
777 if 'dimensions' in swarming_mixin:
778 new_test['swarming'].setdefault('dimension_sets', [{}])
779 for dimension_set in new_test['swarming']['dimension_sets']:
780 dimension_set.update(swarming_mixin['dimensions'])
781 del swarming_mixin['dimensions']
Stephen Martinisb6a50492018-09-12 23:59:32782
Stephen Martinisb72f6d22018-10-04 23:29:01783 # python dict update doesn't do recursion at all. Just hard code the
784 # nested update we need (mixin['swarming'] shouldn't clobber
785 # test['swarming'], but should update it).
786 new_test['swarming'].update(swarming_mixin)
787 del mixin['swarming']
788
Wezc0e835b702018-10-30 00:38:41789 if '$mixin_append' in mixin:
790 # Values specified under $mixin_append should be appended to existing
791 # lists, rather than replacing them.
792 mixin_append = mixin['$mixin_append']
793 for key in mixin_append:
794 new_test.setdefault(key, [])
795 if not isinstance(mixin_append[key], list):
796 raise BBGenErr(
797 'Key "' + key + '" in $mixin_append must be a list.')
798 if not isinstance(new_test[key], list):
799 raise BBGenErr(
800 'Cannot apply $mixin_append to non-list "' + key + '".')
801 new_test[key].extend(mixin_append[key])
802 if 'args' in mixin_append:
803 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
804 del mixin['$mixin_append']
805
Stephen Martinisb72f6d22018-10-04 23:29:01806 new_test.update(mixin)
Stephen Martinis0382bc12018-09-17 22:29:07807
Stephen Martinisb6a50492018-09-12 23:59:32808 return new_test
809
Kenneth Russelleb60cbd22017-12-05 07:54:28810 def generate_waterfall_json(self, waterfall):
811 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28812 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:01813 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:43814 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28815 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43816 # Copy only well-understood entries in the machine's configuration
817 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28818 if 'additional_compile_targets' in config:
819 tests['additional_compile_targets'] = config[
820 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43821 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28822 if test_type not in generator_map:
823 raise self.unknown_test_suite_type(
824 test_type, name, waterfall['name']) # pragma: no cover
825 test_generator = generator_map[test_type]
Nico Weber79dc5f6852018-07-13 19:38:49826 # Let multiple kinds of generators generate the same kinds
827 # of tests. For example, gpu_telemetry_tests are a
828 # specialization of isolated_scripts.
829 new_tests = test_generator.generate(
830 waterfall, name, config, input_tests)
831 remapped_test_type = test_type_remapper.get(test_type, test_type)
832 tests[remapped_test_type] = test_generator.sort(
833 tests.get(remapped_test_type, []) + new_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28834 all_tests[name] = tests
835 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
836 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
837 return json.dumps(all_tests, indent=2, separators=(',', ': '),
838 sort_keys=True) + '\n'
839
840 def generate_waterfalls(self): # pragma: no cover
841 self.load_configuration_files()
842 self.resolve_configuration_files()
843 filters = self.args.waterfall_filters
844 suffix = '.json'
845 if self.args.new_files:
846 suffix = '.new' + suffix
847 for waterfall in self.waterfalls:
848 should_gen = not filters or waterfall['name'] in filters
849 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:11850 file_path = waterfall['name'] + suffix
851 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:28852 self.generate_waterfall_json(waterfall))
853
Nico Weberd18b8962018-05-16 19:39:38854 def get_valid_bot_names(self):
855 # Extract bot names from infra/config/global/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:42856 # NOTE: This reference can cause issues; if a file changes there, the
857 # presubmit here won't be run by default. A manually maintained list there
858 # tries to run presubmit here when luci-milo.cfg is changed. If any other
859 # references to configs outside of this directory are added, please change
860 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
861 # never ends up in an invalid state.
Nico Weberd18b8962018-05-16 19:39:38862 bot_names = set()
John Budorickc12abd12018-08-14 19:37:43863 infra_config_dir = os.path.abspath(
864 os.path.join(os.path.dirname(__file__),
865 '..', '..', 'infra', 'config', 'global'))
866 milo_configs = [
867 os.path.join(infra_config_dir, 'luci-milo.cfg'),
868 os.path.join(infra_config_dir, 'luci-milo-dev.cfg'),
869 ]
870 for c in milo_configs:
871 for l in self.read_file(c).splitlines():
872 if (not 'name: "buildbucket/luci.chromium.' in l and
John Budorickb1833612018-12-07 04:36:41873 not 'name: "buildbot/chromium.' in l and
874 not 'name: "buildbot/tryserver.chromium.' in l):
John Budorickc12abd12018-08-14 19:37:43875 continue
876 # l looks like
877 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
878 # Extract win_chromium_dbg_ng part.
879 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:38880 return bot_names
881
Kenneth Russell8a386d42018-06-02 09:48:01882 def get_bots_that_do_not_actually_exist(self):
883 # Some of the bots on the chromium.gpu.fyi waterfall in particular
884 # are defined only to be mirrored into trybots, and don't actually
885 # exist on any of the waterfalls or consoles.
886 return [
Jamie Madilldc7feeb82018-11-14 04:54:56887 'ANGLE GPU Win10 Release (Intel HD 630)',
888 'ANGLE GPU Win10 Release (NVIDIA)',
Corentin Wallez7d3f4fa22018-11-19 23:35:44889 'Dawn GPU Linux Release (Intel HD 630)',
890 'Dawn GPU Linux Release (NVIDIA)',
891 'Dawn GPU Mac Release (Intel)',
892 'Dawn GPU Mac Retina Release (AMD)',
893 'Dawn GPU Mac Retina Release (NVIDIA)',
Jamie Madilldc7feeb82018-11-14 04:54:56894 'Dawn GPU Win10 Release (Intel HD 630)',
895 'Dawn GPU Win10 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:01896 'Optional Android Release (Nexus 5X)',
897 'Optional Linux Release (Intel HD 630)',
898 'Optional Linux Release (NVIDIA)',
899 'Optional Mac Release (Intel)',
900 'Optional Mac Retina Release (AMD)',
901 'Optional Mac Retina Release (NVIDIA)',
902 'Optional Win10 Release (Intel HD 630)',
903 'Optional Win10 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:01904 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:08905 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:29906 'linux-blink-rel-dummy',
907 'mac10.10-blink-rel-dummy',
908 'mac10.11-blink-rel-dummy',
909 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:20910 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:29911 'mac10.13-blink-rel-dummy',
912 'win7-blink-rel-dummy',
913 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:08914 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:33915 'WebKit Linux composite_after_paint Dummy Builder',
Nico Weber7fc8b9da2018-06-08 19:22:08916 'WebKit Linux layout_ng Dummy Builder',
917 'WebKit Linux root_layer_scrolls Dummy Builder',
Stephen Martinis769b25112018-08-30 18:52:06918 # chromium, due to https://crbug.com/878915
919 'win-dbg',
920 'win32-dbg',
Kenneth Russell8a386d42018-06-02 09:48:01921 ]
922
Stephen Martinisf83893722018-09-19 00:02:18923 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:20924 self.check_input_files_sorting(verbose)
925
Kenneth Russelleb60cbd22017-12-05 07:54:28926 self.load_configuration_files()
Stephen Martinis54d64ad2018-09-21 22:16:20927 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:28928 self.check_composition_test_suites()
Nico Weberd18b8962018-05-16 19:39:38929
930 # All bots should exist.
931 bot_names = self.get_valid_bot_names()
Kenneth Russell8a386d42018-06-02 09:48:01932 bots_that_dont_exist = self.get_bots_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:38933 for waterfall in self.waterfalls:
934 for bot_name in waterfall['machines']:
Kenneth Russell8a386d42018-06-02 09:48:01935 if bot_name in bots_that_dont_exist:
936 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38937 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:08938 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:38939 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:52940 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:32941 if waterfall['name'] in ['tryserver.webrtc',
942 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:38943 # These waterfalls have their bot configs in a different repo.
944 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:52945 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38946 raise self.unknown_bot(bot_name, waterfall['name'])
947
Kenneth Russelleb60cbd22017-12-05 07:54:28948 # All test suites must be referenced.
949 suites_seen = set()
950 generator_map = self.get_test_generator_map()
951 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43952 for bot_name, tester in waterfall['machines'].iteritems():
953 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28954 if suite_type not in generator_map:
955 raise self.unknown_test_suite_type(suite_type, bot_name,
956 waterfall['name'])
957 if suite not in self.test_suites:
958 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
959 suites_seen.add(suite)
960 # Since we didn't resolve the configuration files, this set
961 # includes both composition test suites and regular ones.
962 resolved_suites = set()
963 for suite_name in suites_seen:
964 suite = self.test_suites[suite_name]
965 if isinstance(suite, list):
966 for sub_suite in suite:
967 resolved_suites.add(sub_suite)
968 resolved_suites.add(suite_name)
969 # At this point, every key in test_suites.pyl should be referenced.
970 missing_suites = set(self.test_suites.keys()) - resolved_suites
971 if missing_suites:
972 raise BBGenErr('The following test suites were unreferenced by bots on '
973 'the waterfalls: ' + str(missing_suites))
974
975 # All test suite exceptions must refer to bots on the waterfall.
976 all_bots = set()
977 missing_bots = set()
978 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43979 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28980 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:28981 # In order to disambiguate between bots with the same name on
982 # different waterfalls, support has been added to various
983 # exceptions for concatenating the waterfall name after the bot
984 # name.
985 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28986 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:38987 removals = (exception.get('remove_from', []) +
988 exception.get('remove_gtest_from', []) +
989 exception.get('modifications', {}).keys())
990 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:28991 if removal not in all_bots:
992 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:41993
994 missing_bots = missing_bots - set(bots_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:28995 if missing_bots:
996 raise BBGenErr('The following nonexistent machines were referenced in '
997 'the test suite exceptions: ' + str(missing_bots))
998
Stephen Martinis0382bc12018-09-17 22:29:07999 # All mixins must be referenced
1000 seen_mixins = set()
1001 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011002 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071003 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011004 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071005 for suite in self.test_suites.values():
1006 if isinstance(suite, list):
1007 # Don't care about this, it's a composition, which shouldn't include a
1008 # swarming mixin.
1009 continue
1010
1011 for test in suite.values():
1012 if not isinstance(test, dict):
1013 # Some test suites have top level keys, which currently can't be
1014 # swarming mixin entries. Ignore them
1015 continue
1016
Stephen Martinisb72f6d22018-10-04 23:29:011017 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071018
Stephen Martinisb72f6d22018-10-04 23:29:011019 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071020 if missing_mixins:
1021 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1022 ' referenced in a waterfall, machine, or test suite.' % (
1023 str(missing_mixins)))
1024
Stephen Martinis54d64ad2018-09-21 22:16:201025
1026 def type_assert(self, node, typ, filename, verbose=False):
1027 """Asserts that the Python AST node |node| is of type |typ|.
1028
1029 If verbose is set, it prints out some helpful context lines, showing where
1030 exactly the error occurred in the file.
1031 """
1032 if not isinstance(node, typ):
1033 if verbose:
1034 lines = [""] + self.read_file(filename).splitlines()
1035
1036 context = 2
1037 lines_start = max(node.lineno - context, 0)
1038 # Add one to include the last line
1039 lines_end = min(node.lineno + context, len(lines)) + 1
1040 lines = (
1041 ['== %s ==\n' % filename] +
1042 ["<snip>\n"] +
1043 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1044 lines[lines_start:lines_start + context])] +
1045 ['-' * 80 + '\n'] +
1046 ['%d %s' % (node.lineno, lines[node.lineno])] +
1047 ['-' * (node.col_offset + 3) + '^' + '-' * (
1048 80 - node.col_offset - 4) + '\n'] +
1049 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1050 lines[node.lineno + 1:lines_end])] +
1051 ["<snip>\n"]
1052 )
1053 # Print out a useful message when a type assertion fails.
1054 for l in lines:
1055 self.print_line(l.strip())
1056
1057 node_dumped = ast.dump(node, annotate_fields=False)
1058 # If the node is huge, truncate it so everything fits in a terminal
1059 # window.
1060 if len(node_dumped) > 60: # pragma: no cover
1061 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1062 raise BBGenErr(
1063 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1064 ' be %s, is %s' % (
1065 filename, node_dumped,
1066 node.lineno, typ, type(node)))
1067
1068 def ensure_ast_dict_keys_sorted(self, node, filename, verbose):
1069 is_valid = True
1070
1071 keys = []
1072 # The keys of this dict are ordered as ordered in the file; normal python
1073 # dictionary keys are given an arbitrary order, but since we parsed the
1074 # file itself, the order as given in the file is preserved.
1075 for key in node.keys:
1076 self.type_assert(key, ast.Str, filename, verbose)
1077 keys.append(key.s)
1078
1079 keys_sorted = sorted(keys)
1080 if keys_sorted != keys:
1081 is_valid = False
1082 if verbose:
1083 for line in difflib.unified_diff(
1084 keys,
1085 keys_sorted, fromfile='current (%r)' % filename, tofile='sorted'):
1086 self.print_line(line)
1087
1088 if len(set(keys)) != len(keys):
1089 for i in range(len(keys_sorted)-1):
1090 if keys_sorted[i] == keys_sorted[i+1]:
1091 self.print_line('Key %s is duplicated' % keys_sorted[i])
1092 is_valid = False
1093 return is_valid
Stephen Martinisf83893722018-09-19 00:02:181094
1095 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201096 # TODO(https://crbug.com/886993): Add the ability for this script to
1097 # actually format the files, rather than just complain if they're
1098 # incorrectly formatted.
1099 bad_files = set()
1100
1101 for filename in (
Stephen Martinisb72f6d22018-10-04 23:29:011102 'mixins.pyl',
Stephen Martinis54d64ad2018-09-21 22:16:201103 'test_suites.pyl',
1104 'test_suite_exceptions.pyl',
1105 ):
Stephen Martinisf83893722018-09-19 00:02:181106 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1107
Stephen Martinisf83893722018-09-19 00:02:181108 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201109 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181110 module = parsed.body
1111
1112 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201113 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181114 if len(module) != 1: # pragma: no cover
1115 raise BBGenErr('Invalid .pyl file %s' % filename)
1116 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201117 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181118
1119 # Value should be a dictionary.
1120 value = expr.value
Stephen Martinis54d64ad2018-09-21 22:16:201121 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181122
Stephen Martinis54d64ad2018-09-21 22:16:201123 if filename == 'test_suites.pyl':
1124 expected_keys = ['basic_suites', 'compound_suites']
1125 actual_keys = [node.s for node in value.keys]
1126 assert all(key in expected_keys for key in actual_keys), (
1127 'Invalid %r file; expected keys %r, got %r' % (
1128 filename, expected_keys, actual_keys))
1129 suite_dicts = [node for node in value.values]
1130 # Only two keys should mean only 1 or 2 values
1131 assert len(suite_dicts) <= 2
1132 for suite_group in suite_dicts:
1133 if not self.ensure_ast_dict_keys_sorted(
1134 suite_group, filename, verbose):
1135 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181136
Stephen Martinis54d64ad2018-09-21 22:16:201137 else:
1138 if not self.ensure_ast_dict_keys_sorted(
1139 value, filename, verbose):
1140 bad_files.add(filename)
1141
1142 # waterfalls.pyl is slightly different, just do it manually here
1143 filename = 'waterfalls.pyl'
1144 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1145
1146 # Must be a module.
1147 self.type_assert(parsed, ast.Module, filename, verbose)
1148 module = parsed.body
1149
1150 # Only one expression in the module.
1151 self.type_assert(module, list, filename, verbose)
1152 if len(module) != 1: # pragma: no cover
1153 raise BBGenErr('Invalid .pyl file %s' % filename)
1154 expr = module[0]
1155 self.type_assert(expr, ast.Expr, filename, verbose)
1156
1157 # Value should be a list.
1158 value = expr.value
1159 self.type_assert(value, ast.List, filename, verbose)
1160
1161 keys = []
1162 for val in value.elts:
1163 self.type_assert(val, ast.Dict, filename, verbose)
1164 waterfall_name = None
1165 for key, val in zip(val.keys, val.values):
1166 self.type_assert(key, ast.Str, filename, verbose)
1167 if key.s == 'machines':
1168 if not self.ensure_ast_dict_keys_sorted(val, filename, verbose):
1169 bad_files.add(filename)
1170
1171 if key.s == "name":
1172 self.type_assert(val, ast.Str, filename, verbose)
1173 waterfall_name = val.s
1174 assert waterfall_name
1175 keys.append(waterfall_name)
1176
1177 if sorted(keys) != keys:
1178 bad_files.add(filename)
1179 if verbose: # pragma: no cover
1180 for line in difflib.unified_diff(
1181 keys,
1182 sorted(keys), fromfile='current', tofile='sorted'):
1183 self.print_line(line)
Stephen Martinisf83893722018-09-19 00:02:181184
1185 if bad_files:
1186 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201187 'The following files have invalid keys: %s\n. They are either '
1188 'unsorted, or have duplicates.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181189
Kenneth Russelleb60cbd22017-12-05 07:54:281190 def check_output_file_consistency(self, verbose=False):
1191 self.load_configuration_files()
1192 # All waterfalls must have been written by this script already.
1193 self.resolve_configuration_files()
1194 ungenerated_waterfalls = set()
1195 for waterfall in self.waterfalls:
1196 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111197 file_path = waterfall['name'] + '.json'
1198 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281199 if expected != current:
1200 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321201 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501202 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281203 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321204 'contents:')
1205 for line in difflib.unified_diff(
1206 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501207 current.splitlines(),
1208 fromfile='expected', tofile='current'):
1209 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281210 if ungenerated_waterfalls:
1211 raise BBGenErr('The following waterfalls have not been properly '
1212 'autogenerated by generate_buildbot_json.py: ' +
1213 str(ungenerated_waterfalls))
1214
1215 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501216 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281217 self.check_output_file_consistency(verbose) # pragma: no cover
1218
1219 def parse_args(self, argv): # pragma: no cover
1220 parser = argparse.ArgumentParser()
1221 parser.add_argument(
1222 '-c', '--check', action='store_true', help=
1223 'Do consistency checks of configuration and generated files and then '
1224 'exit. Used during presubmit. Causes the tool to not generate any files.')
1225 parser.add_argument(
1226 '-n', '--new-files', action='store_true', help=
1227 'Write output files as .new.json. Useful during development so old and '
1228 'new files can be looked at side-by-side.')
1229 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501230 '-v', '--verbose', action='store_true', help=
1231 'Increases verbosity. Affects consistency checks.')
1232 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281233 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1234 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111235 parser.add_argument(
1236 '--pyl-files-dir', type=os.path.realpath,
1237 help='Path to the directory containing the input .pyl files.')
Kenneth Russelleb60cbd22017-12-05 07:54:281238 self.args = parser.parse_args(argv)
1239
1240 def main(self, argv): # pragma: no cover
1241 self.parse_args(argv)
1242 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:501243 self.check_consistency(verbose=self.args.verbose)
Kenneth Russelleb60cbd22017-12-05 07:54:281244 else:
1245 self.generate_waterfalls()
1246 return 0
1247
1248if __name__ == "__main__": # pragma: no cover
1249 generator = BBJSONGenerator()
1250 sys.exit(generator.main(sys.argv[1:]))