blob: fab7e60b93cbcbcc339fc8a2afd1053be959de36 [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 Martinisb6a50492018-09-12 23:59:32195 self.swarming_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
200 def read_file(self, relative_path):
201 with open(self.generate_abs_file_path(
202 relative_path)) as fp: # pragma: no cover
203 return fp.read() # pragma: no cover
204
205 def write_file(self, relative_path, contents):
206 with open(self.generate_abs_file_path(
207 relative_path), 'wb') as fp: # pragma: no cover
208 fp.write(contents) # pragma: no cover
209
Zhiling Huangbe008172018-03-08 19:13:11210 def pyl_file_path(self, filename):
211 if self.args and self.args.pyl_files_dir:
212 return os.path.join(self.args.pyl_files_dir, filename)
213 return filename
214
Kenneth Russelleb60cbd22017-12-05 07:54:28215 def load_pyl_file(self, filename):
216 try:
Zhiling Huangbe008172018-03-08 19:13:11217 return ast.literal_eval(self.read_file(
218 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28219 except (SyntaxError, ValueError) as e: # pragma: no cover
220 raise BBGenErr('Failed to parse pyl file "%s": %s' %
221 (filename, e)) # pragma: no cover
222
Kenneth Russell8a386d42018-06-02 09:48:01223 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
224 # Currently it is only mandatory for bots which run GPU tests. Change these to
225 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28226 def is_android(self, tester_config):
227 return tester_config.get('os_type') == 'android'
228
Kenneth Russell8a386d42018-06-02 09:48:01229 def is_linux(self, tester_config):
230 return tester_config.get('os_type') == 'linux'
231
Kenneth Russelleb60cbd22017-12-05 07:54:28232 def get_exception_for_test(self, test_name, test_config):
233 # gtests may have both "test" and "name" fields, and usually, if the "name"
234 # field is specified, it means that the same test is being repurposed
235 # multiple times with different command line arguments. To handle this case,
236 # prefer to lookup per the "name" field of the test itself, as opposed to
237 # the "test_name", which is actually the "test" field.
238 if 'name' in test_config:
239 return self.exceptions.get(test_config['name'])
240 else:
241 return self.exceptions.get(test_name)
242
Nico Weberb0b3f5862018-07-13 18:45:15243 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28244 # Currently, the only reason a test should not run on a given tester is that
245 # it's in the exceptions. (Once the GPU waterfall generation script is
246 # incorporated here, the rules will become more complex.)
247 exception = self.get_exception_for_test(test_name, test_config)
248 if not exception:
249 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28250 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28251 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28252 if remove_from:
253 if tester_name in remove_from:
254 return False
255 # TODO(kbr): this code path was added for some tests (including
256 # android_webview_unittests) on one machine (Nougat Phone
257 # Tester) which exists with the same name on two waterfalls,
258 # chromium.android and chromium.fyi; the tests are run on one
259 # but not the other. Once the bots are all uniquely named (a
260 # different ongoing project) this code should be removed.
261 # TODO(kbr): add coverage.
262 return (tester_name + ' ' + waterfall['name']
263 not in remove_from) # pragma: no cover
264 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28265
Nico Weber79dc5f6852018-07-13 19:38:49266 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28267 exception = self.get_exception_for_test(test_name, test)
268 if not exception:
269 return None
Nico Weber79dc5f6852018-07-13 19:38:49270 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28271
Kenneth Russell8a386d42018-06-02 09:48:01272 def merge_command_line_args(self, arr, prefix, splitter):
273 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01274 idx = 0
275 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01276 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01277 while idx < len(arr):
278 flag = arr[idx]
279 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01280 if flag.startswith(prefix):
281 arg = flag[prefix_len:]
282 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01283 if first_idx < 0:
284 first_idx = idx
285 else:
286 delete_current_entry = True
287 if delete_current_entry:
288 del arr[idx]
289 else:
290 idx += 1
291 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01292 arr[first_idx] = prefix + splitter.join(accumulated_args)
293 return arr
294
295 def maybe_fixup_args_array(self, arr):
296 # The incoming array of strings may be an array of command line
297 # arguments. To make it easier to turn on certain features per-bot or
298 # per-test-suite, look specifically for certain flags and merge them
299 # appropriately.
300 # --enable-features=Feature1 --enable-features=Feature2
301 # are merged to:
302 # --enable-features=Feature1,Feature2
303 # and:
304 # --extra-browser-args=arg1 --extra-browser-args=arg2
305 # are merged to:
306 # --extra-browser-args=arg1 arg2
307 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
308 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01309 return arr
310
Kenneth Russelleb60cbd22017-12-05 07:54:28311 def dictionary_merge(self, a, b, path=None, update=True):
312 """http://stackoverflow.com/questions/7204805/
313 python-dictionaries-of-dictionaries-merge
314 merges b into a
315 """
316 if path is None:
317 path = []
318 for key in b:
319 if key in a:
320 if isinstance(a[key], dict) and isinstance(b[key], dict):
321 self.dictionary_merge(a[key], b[key], path + [str(key)])
322 elif a[key] == b[key]:
323 pass # same leaf value
324 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06325 # Args arrays are lists of strings. Just concatenate them,
326 # and don't sort them, in order to keep some needed
327 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28328 if all(isinstance(x, str)
329 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01330 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28331 else:
332 # TODO(kbr): this only works properly if the two arrays are
333 # the same length, which is currently always the case in the
334 # swarming dimension_sets that we have to merge. It will fail
335 # to merge / override 'args' arrays which are different
336 # length.
337 for idx in xrange(len(b[key])):
338 try:
339 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
340 path + [str(key), str(idx)],
341 update=update)
342 except (IndexError, TypeError): # pragma: no cover
343 raise BBGenErr('Error merging list keys ' + str(key) +
344 ' and indices ' + str(idx) + ' between ' +
345 str(a) + ' and ' + str(b)) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28346 elif update: # pragma: no cover
347 a[key] = b[key] # pragma: no cover
348 else:
349 raise BBGenErr('Conflict at %s' % '.'.join(
350 path + [str(key)])) # pragma: no cover
351 else:
352 a[key] = b[key]
353 return a
354
John Budorickab108712018-09-01 00:12:21355 def initialize_args_for_test(
356 self, generated_test, tester_config, additional_arg_keys=None):
357
358 args = []
359 args.extend(generated_test.get('args', []))
360 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22361
Kenneth Russell8a386d42018-06-02 09:48:01362 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21363 val = generated_test.pop(key, [])
364 if fn(tester_config):
365 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01366
367 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
368 add_conditional_args('linux_args', self.is_linux)
369 add_conditional_args('android_args', self.is_android)
370
John Budorickab108712018-09-01 00:12:21371 for key in additional_arg_keys or []:
372 args.extend(generated_test.pop(key, []))
373 args.extend(tester_config.get(key, []))
374
375 if args:
376 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01377
Kenneth Russelleb60cbd22017-12-05 07:54:28378 def initialize_swarming_dictionary_for_test(self, generated_test,
379 tester_config):
380 if 'swarming' not in generated_test:
381 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28382 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
383 generated_test['swarming'].update({
384 'can_use_on_swarming_builders': tester_config.get('use_swarming', True)
385 })
Kenneth Russelleb60cbd22017-12-05 07:54:28386 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03387 if ('dimension_sets' not in generated_test['swarming'] and
388 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28389 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
390 tester_config['swarming']['dimension_sets'])
391 self.dictionary_merge(generated_test['swarming'],
392 tester_config['swarming'])
393 # Apply any Android-specific Swarming dimensions after the generic ones.
394 if 'android_swarming' in generated_test:
395 if self.is_android(tester_config): # pragma: no cover
396 self.dictionary_merge(
397 generated_test['swarming'],
398 generated_test['android_swarming']) # pragma: no cover
399 del generated_test['android_swarming'] # pragma: no cover
400
401 def clean_swarming_dictionary(self, swarming_dict):
402 # Clean out redundant entries from a test's "swarming" dictionary.
403 # This is really only needed to retain 100% parity with the
404 # handwritten JSON files, and can be removed once all the files are
405 # autogenerated.
406 if 'shards' in swarming_dict:
407 if swarming_dict['shards'] == 1: # pragma: no cover
408 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24409 if 'hard_timeout' in swarming_dict:
410 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
411 del swarming_dict['hard_timeout'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28412 if not swarming_dict['can_use_on_swarming_builders']:
413 # Remove all other keys.
414 for k in swarming_dict.keys(): # pragma: no cover
415 if k != 'can_use_on_swarming_builders': # pragma: no cover
416 del swarming_dict[k] # pragma: no cover
417
Stephen Martinis0382bc12018-09-17 22:29:07418 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
419 waterfall):
420 # Apply swarming mixins.
421 test = self.apply_all_swarming_mixins(
422 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28423 # See if there are any exceptions that need to be merged into this
424 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49425 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28426 if modifications:
427 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23428 if 'swarming' in test:
429 self.clean_swarming_dictionary(test['swarming'])
Kenneth Russelleb60cbd22017-12-05 07:54:28430 return test
431
Shenghua Zhangaba8bad2018-02-07 02:12:09432 def add_common_test_properties(self, test, tester_config):
433 if tester_config.get('use_multi_dimension_trigger_script'):
434 test['trigger_script'] = {
435 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
436 'args': [
437 '--multiple-trigger-configs',
438 json.dumps(tester_config['swarming']['dimension_sets'] +
439 tester_config.get('alternate_swarming_dimensions', [])),
440 '--multiple-dimension-script-verbose',
441 'True'
442 ],
443 }
444
Kenneth Russelleb60cbd22017-12-05 07:54:28445 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
446 test_config):
447 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15448 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28449 return None
450 result = copy.deepcopy(test_config)
451 if 'test' in result:
452 result['name'] = test_name
453 else:
454 result['test'] = test_name
455 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21456
457 self.initialize_args_for_test(
458 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28459 if self.is_android(tester_config) and tester_config.get('use_swarming',
460 True):
Kenneth Russell8a386d42018-06-02 09:48:01461 args = result.get('args', [])
Kenneth Russell5612d64a2018-06-02 21:12:30462 args.append('--gs-results-bucket=chromium-result-details')
Nico Weberd18b8962018-05-16 19:39:38463 if (result['swarming']['can_use_on_swarming_builders'] and not
464 tester_config.get('skip_merge_script', False)):
Kenneth Russelleb60cbd22017-12-05 07:54:28465 result['merge'] = {
466 'args': [
467 '--bucket',
468 'chromium-result-details',
469 '--test-name',
470 test_name
471 ],
Nico Weberd18b8962018-05-16 19:39:38472 'script': '//build/android/pylib/results/presentation/'
Kenneth Russelleb60cbd22017-12-05 07:54:28473 'test_results_presentation.py',
474 } # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:28475 if not tester_config.get('skip_cipd_packages', False):
476 result['swarming']['cipd_packages'] = [
477 {
478 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
479 'location': 'bin',
480 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
481 }
482 ]
Kenneth Russelleb60cbd22017-12-05 07:54:28483 if not tester_config.get('skip_output_links', False):
484 result['swarming']['output_links'] = [
485 {
486 'link': [
487 'https://luci-logdog.appspot.com/v/?s',
488 '=android%2Fswarming%2Flogcats%2F',
489 '${TASK_ID}%2F%2B%2Funified_logcats',
490 ],
491 'name': 'shard #${SHARD_INDEX} logcats',
492 },
493 ]
Kenneth Russell5612d64a2018-06-02 21:12:30494 args.append('--recover-devices')
Kenneth Russell8a386d42018-06-02 09:48:01495 if args:
496 result['args'] = args
Benjamin Pastene766d48f52017-12-18 21:47:42497
Stephen Martinis0382bc12018-09-17 22:29:07498 result = self.update_and_cleanup_test(
499 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09500 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28501 return result
502
503 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
504 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01505 if not self.should_run_on_tester(waterfall, tester_name, test_name,
506 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28507 return None
508 result = copy.deepcopy(test_config)
509 result['isolate_name'] = result.get('isolate_name', test_name)
510 result['name'] = test_name
511 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01512 self.initialize_args_for_test(result, tester_config)
Stephen Martinis0382bc12018-09-17 22:29:07513 result = self.update_and_cleanup_test(
514 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09515 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28516 return result
517
518 def generate_script_test(self, waterfall, tester_name, tester_config,
519 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01520 if not self.should_run_on_tester(waterfall, tester_name, test_name,
521 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28522 return None
523 result = {
524 'name': test_name,
525 'script': test_config['script']
526 }
Stephen Martinis0382bc12018-09-17 22:29:07527 result = self.update_and_cleanup_test(
528 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28529 return result
530
531 def generate_junit_test(self, waterfall, tester_name, tester_config,
532 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01533 del tester_config
534 if not self.should_run_on_tester(waterfall, tester_name, test_name,
535 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28536 return None
537 result = {
538 'test': test_name,
539 }
540 return result
541
542 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
543 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01544 if not self.should_run_on_tester(waterfall, tester_name, test_name,
545 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28546 return None
547 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28548 if 'test' in result and result['test'] != test_name:
549 result['name'] = test_name
550 else:
551 result['test'] = test_name
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
Kenneth Russell8a386d42018-06-02 09:48:01556 def substitute_gpu_args(self, tester_config, args):
557 substitutions = {
558 # Any machine in waterfalls.pyl which desires to run GPU tests
559 # must provide the os_type key.
560 'os_type': tester_config['os_type'],
561 'gpu_vendor_id': '0',
562 'gpu_device_id': '0',
563 }
564 dimension_set = tester_config['swarming']['dimension_sets'][0]
565 if 'gpu' in dimension_set:
566 # First remove the driver version, then split into vendor and device.
567 gpu = dimension_set['gpu']
568 gpu = gpu.split('-')[0].split(':')
569 substitutions['gpu_vendor_id'] = gpu[0]
570 substitutions['gpu_device_id'] = gpu[1]
571 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
572
573 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
574 test_name, test_config):
575 # These are all just specializations of isolated script tests with
576 # a bunch of boilerplate command line arguments added.
577
578 # The step name must end in 'test' or 'tests' in order for the
579 # results to automatically show up on the flakiness dashboard.
580 # (At least, this was true some time ago.) Continue to use this
581 # naming convention for the time being to minimize changes.
582 step_name = test_config.get('name', test_name)
583 if not (step_name.endswith('test') or step_name.endswith('tests')):
584 step_name = '%s_tests' % step_name
585 result = self.generate_isolated_script_test(
586 waterfall, tester_name, tester_config, step_name, test_config)
587 if not result:
588 return None
589 result['isolate_name'] = 'telemetry_gpu_integration_test'
590 args = result.get('args', [])
591 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14592
593 # These tests upload and download results from cloud storage and therefore
594 # aren't idempotent yet. https://crbug.com/549140.
595 result['swarming']['idempotent'] = False
596
Kenneth Russell8a386d42018-06-02 09:48:01597 args = [
598 test_to_run,
599 '--show-stdout',
600 '--browser=%s' % tester_config['browser_config'],
601 # --passthrough displays more of the logging in Telemetry when
602 # run via typ, in particular some of the warnings about tests
603 # being expected to fail, but passing.
604 '--passthrough',
605 '-v',
606 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
607 ] + args
608 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
609 tester_config, args))
610 return result
611
Kenneth Russelleb60cbd22017-12-05 07:54:28612 def get_test_generator_map(self):
613 return {
614 'cts_tests': CTSGenerator(self),
Kenneth Russell8a386d42018-06-02 09:48:01615 'gpu_telemetry_tests': GPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28616 'gtest_tests': GTestGenerator(self),
617 'instrumentation_tests': InstrumentationTestGenerator(self),
618 'isolated_scripts': IsolatedScriptTestGenerator(self),
619 'junit_tests': JUnitGenerator(self),
620 'scripts': ScriptGenerator(self),
621 }
622
Kenneth Russell8a386d42018-06-02 09:48:01623 def get_test_type_remapper(self):
624 return {
625 # These are a specialization of isolated_scripts with a bunch of
626 # boilerplate command line arguments added to each one.
627 'gpu_telemetry_tests': 'isolated_scripts',
628 }
629
Kenneth Russelleb60cbd22017-12-05 07:54:28630 def check_composition_test_suites(self):
631 # Pre-pass to catch errors reliably.
632 for name, value in self.test_suites.iteritems():
633 if isinstance(value, list):
634 for entry in value:
635 if isinstance(self.test_suites[entry], list):
Nico Weberd18b8962018-05-16 19:39:38636 raise BBGenErr('Composition test suites may not refer to other '
637 'composition test suites (error found while '
Kenneth Russelleb60cbd22017-12-05 07:54:28638 'processing %s)' % name)
639
640 def resolve_composition_test_suites(self):
641 self.check_composition_test_suites()
642 for name, value in self.test_suites.iteritems():
643 if isinstance(value, list):
644 # Resolve this to a dictionary.
645 full_suite = {}
646 for entry in value:
647 suite = self.test_suites[entry]
648 full_suite.update(suite)
649 self.test_suites[name] = full_suite
650
651 def link_waterfalls_to_test_suites(self):
652 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43653 for tester_name, tester in waterfall['machines'].iteritems():
654 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28655 if not value in self.test_suites:
656 # Hard / impossible to cover this in the unit test.
657 raise self.unknown_test_suite(
658 value, tester_name, waterfall['name']) # pragma: no cover
659 tester['test_suites'][suite] = self.test_suites[value]
660
661 def load_configuration_files(self):
662 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
663 self.test_suites = self.load_pyl_file('test_suites.pyl')
664 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb6a50492018-09-12 23:59:32665 self.swarming_mixins = self.load_pyl_file('swarming_mixins.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:28666
667 def resolve_configuration_files(self):
668 self.resolve_composition_test_suites()
669 self.link_waterfalls_to_test_suites()
670
Nico Weberd18b8962018-05-16 19:39:38671 def unknown_bot(self, bot_name, waterfall_name):
672 return BBGenErr(
673 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
674
Kenneth Russelleb60cbd22017-12-05 07:54:28675 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
676 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38677 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28678 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
679
680 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
681 return BBGenErr(
682 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
683 ' on waterfall ' + waterfall_name)
684
Stephen Martinis0382bc12018-09-17 22:29:07685 def apply_all_swarming_mixins(self, test, waterfall, builder_name, builder):
686 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:32687
688 Checks in the waterfall, builder, and test objects for mixins.
689 """
690 def valid_mixin(mixin_name):
691 """Asserts that the mixin is valid."""
692 if mixin_name not in self.swarming_mixins:
693 raise BBGenErr("bad mixin %s" % mixin_name)
694 def must_be_list(mixins, typ, name):
695 """Asserts that given mixins are a list."""
696 if not isinstance(mixins, list):
697 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
698
699 if 'swarming_mixins' in waterfall:
700 must_be_list(waterfall['swarming_mixins'], 'waterfall', waterfall['name'])
701 for mixin in waterfall['swarming_mixins']:
702 valid_mixin(mixin)
Stephen Martinis0382bc12018-09-17 22:29:07703 test = self.apply_swarming_mixin(self.swarming_mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32704
705 if 'swarming_mixins' in builder:
706 must_be_list(builder['swarming_mixins'], 'builder', builder_name)
707 for mixin in builder['swarming_mixins']:
708 valid_mixin(mixin)
Stephen Martinisb6a50492018-09-12 23:59:32709 test = self.apply_swarming_mixin(self.swarming_mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32710
Stephen Martinis0382bc12018-09-17 22:29:07711 if not 'swarming_mixins' in test:
712 return test
713
714 must_be_list(test['swarming_mixins'], 'test', test['test'])
715 for mixin in test['swarming_mixins']:
716 valid_mixin(mixin)
717 test = self.apply_swarming_mixin(self.swarming_mixins[mixin], test)
718 del test['swarming_mixins']
719 return test
Stephen Martinisb6a50492018-09-12 23:59:32720
721 def apply_swarming_mixin(self, mixin, test):
722 """Applies a swarming mixin to a test.
723
Stephen Martinis0382bc12018-09-17 22:29:07724 Mixins will not override an existing key. This is to ensure exceptions can
725 override a setting a mixin applies.
726
Stephen Martinisb6a50492018-09-12 23:59:32727 Dimensions are handled in a special way. Instead of specifying
728 'dimension_sets', which is how normal test suites specify their dimensions,
729 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
730 is then applied to every dimension set in the test.
731 """
732 new_test = copy.deepcopy(test)
733 mixin = copy.deepcopy(mixin)
734
Stephen Martinis0382bc12018-09-17 22:29:07735 new_test.setdefault('swarming', {})
Stephen Martinisb6a50492018-09-12 23:59:32736 if 'dimensions' in mixin:
Stephen Martinis0382bc12018-09-17 22:29:07737 new_test['swarming'].setdefault('dimension_sets', [{}])
Stephen Martinisb6a50492018-09-12 23:59:32738 for dimension_set in new_test['swarming']['dimension_sets']:
739 dimension_set.update(mixin['dimensions'])
740 del mixin['dimensions']
741
Stephen Martinis0382bc12018-09-17 22:29:07742 new_test['swarming'].update(mixin)
743
Stephen Martinisb6a50492018-09-12 23:59:32744 return new_test
745
Kenneth Russelleb60cbd22017-12-05 07:54:28746 def generate_waterfall_json(self, waterfall):
747 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28748 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:01749 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:43750 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28751 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43752 # Copy only well-understood entries in the machine's configuration
753 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28754 if 'additional_compile_targets' in config:
755 tests['additional_compile_targets'] = config[
756 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43757 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28758 if test_type not in generator_map:
759 raise self.unknown_test_suite_type(
760 test_type, name, waterfall['name']) # pragma: no cover
761 test_generator = generator_map[test_type]
Nico Weber79dc5f6852018-07-13 19:38:49762 # Let multiple kinds of generators generate the same kinds
763 # of tests. For example, gpu_telemetry_tests are a
764 # specialization of isolated_scripts.
765 new_tests = test_generator.generate(
766 waterfall, name, config, input_tests)
767 remapped_test_type = test_type_remapper.get(test_type, test_type)
768 tests[remapped_test_type] = test_generator.sort(
769 tests.get(remapped_test_type, []) + new_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28770 all_tests[name] = tests
771 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
772 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
773 return json.dumps(all_tests, indent=2, separators=(',', ': '),
774 sort_keys=True) + '\n'
775
776 def generate_waterfalls(self): # pragma: no cover
777 self.load_configuration_files()
778 self.resolve_configuration_files()
779 filters = self.args.waterfall_filters
780 suffix = '.json'
781 if self.args.new_files:
782 suffix = '.new' + suffix
783 for waterfall in self.waterfalls:
784 should_gen = not filters or waterfall['name'] in filters
785 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:11786 file_path = waterfall['name'] + suffix
787 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:28788 self.generate_waterfall_json(waterfall))
789
Nico Weberd18b8962018-05-16 19:39:38790 def get_valid_bot_names(self):
791 # Extract bot names from infra/config/global/luci-milo.cfg.
792 bot_names = set()
John Budorickc12abd12018-08-14 19:37:43793 infra_config_dir = os.path.abspath(
794 os.path.join(os.path.dirname(__file__),
795 '..', '..', 'infra', 'config', 'global'))
796 milo_configs = [
797 os.path.join(infra_config_dir, 'luci-milo.cfg'),
798 os.path.join(infra_config_dir, 'luci-milo-dev.cfg'),
799 ]
800 for c in milo_configs:
801 for l in self.read_file(c).splitlines():
802 if (not 'name: "buildbucket/luci.chromium.' in l and
803 not 'name: "buildbot/chromium.' in l):
804 continue
805 # l looks like
806 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
807 # Extract win_chromium_dbg_ng part.
808 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:38809 return bot_names
810
Kenneth Russell8a386d42018-06-02 09:48:01811 def get_bots_that_do_not_actually_exist(self):
812 # Some of the bots on the chromium.gpu.fyi waterfall in particular
813 # are defined only to be mirrored into trybots, and don't actually
814 # exist on any of the waterfalls or consoles.
815 return [
816 'Optional Android Release (Nexus 5X)',
817 'Optional Linux Release (Intel HD 630)',
818 'Optional Linux Release (NVIDIA)',
819 'Optional Mac Release (Intel)',
820 'Optional Mac Retina Release (AMD)',
821 'Optional Mac Retina Release (NVIDIA)',
822 'Optional Win10 Release (Intel HD 630)',
823 'Optional Win10 Release (NVIDIA)',
824 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:08825 # chromium.fyi
826 'chromeos-amd64-generic-rel-vm-tests',
Dirk Pranke85369442018-06-16 02:01:29827 'linux-blink-rel-dummy',
828 'mac10.10-blink-rel-dummy',
829 'mac10.11-blink-rel-dummy',
830 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d2018-07-17 21:39:20831 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:29832 'mac10.13-blink-rel-dummy',
833 'win7-blink-rel-dummy',
834 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:08835 'Dummy WebKit Mac10.13',
836 'WebKit Linux layout_ng Dummy Builder',
837 'WebKit Linux root_layer_scrolls Dummy Builder',
838 'WebKit Linux slimming_paint_v2 Dummy Builder',
Stephen Martinis769b25112018-08-30 18:52:06839 # chromium, due to https://crbug.com/878915
840 'win-dbg',
841 'win32-dbg',
Kenneth Russell8a386d42018-06-02 09:48:01842 ]
843
Stephen Martinisf83893722018-09-19 00:02:18844 def check_input_file_consistency(self, verbose=False):
Kenneth Russelleb60cbd22017-12-05 07:54:28845 self.load_configuration_files()
846 self.check_composition_test_suites()
Nico Weberd18b8962018-05-16 19:39:38847
848 # All bots should exist.
849 bot_names = self.get_valid_bot_names()
Kenneth Russell8a386d42018-06-02 09:48:01850 bots_that_dont_exist = self.get_bots_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:38851 for waterfall in self.waterfalls:
852 for bot_name in waterfall['machines']:
Kenneth Russell8a386d42018-06-02 09:48:01853 if bot_name in bots_that_dont_exist:
854 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38855 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:08856 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:38857 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:52858 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:32859 if waterfall['name'] in ['tryserver.webrtc',
860 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:38861 # These waterfalls have their bot configs in a different repo.
862 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:52863 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38864 raise self.unknown_bot(bot_name, waterfall['name'])
865
Kenneth Russelleb60cbd22017-12-05 07:54:28866 # All test suites must be referenced.
867 suites_seen = set()
868 generator_map = self.get_test_generator_map()
869 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43870 for bot_name, tester in waterfall['machines'].iteritems():
871 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28872 if suite_type not in generator_map:
873 raise self.unknown_test_suite_type(suite_type, bot_name,
874 waterfall['name'])
875 if suite not in self.test_suites:
876 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
877 suites_seen.add(suite)
878 # Since we didn't resolve the configuration files, this set
879 # includes both composition test suites and regular ones.
880 resolved_suites = set()
881 for suite_name in suites_seen:
882 suite = self.test_suites[suite_name]
883 if isinstance(suite, list):
884 for sub_suite in suite:
885 resolved_suites.add(sub_suite)
886 resolved_suites.add(suite_name)
887 # At this point, every key in test_suites.pyl should be referenced.
888 missing_suites = set(self.test_suites.keys()) - resolved_suites
889 if missing_suites:
890 raise BBGenErr('The following test suites were unreferenced by bots on '
891 'the waterfalls: ' + str(missing_suites))
892
893 # All test suite exceptions must refer to bots on the waterfall.
894 all_bots = set()
895 missing_bots = set()
896 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43897 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28898 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:28899 # In order to disambiguate between bots with the same name on
900 # different waterfalls, support has been added to various
901 # exceptions for concatenating the waterfall name after the bot
902 # name.
903 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28904 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:38905 removals = (exception.get('remove_from', []) +
906 exception.get('remove_gtest_from', []) +
907 exception.get('modifications', {}).keys())
908 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:28909 if removal not in all_bots:
910 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:41911
912 missing_bots = missing_bots - set(bots_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:28913 if missing_bots:
914 raise BBGenErr('The following nonexistent machines were referenced in '
915 'the test suite exceptions: ' + str(missing_bots))
916
Stephen Martinis0382bc12018-09-17 22:29:07917 # All mixins must be referenced
918 seen_mixins = set()
919 for waterfall in self.waterfalls:
920 seen_mixins = seen_mixins.union(waterfall.get('swarming_mixins', set()))
921 for bot_name, tester in waterfall['machines'].iteritems():
922 seen_mixins = seen_mixins.union(tester.get('swarming_mixins', set()))
923 for suite in self.test_suites.values():
924 if isinstance(suite, list):
925 # Don't care about this, it's a composition, which shouldn't include a
926 # swarming mixin.
927 continue
928
929 for test in suite.values():
930 if not isinstance(test, dict):
931 # Some test suites have top level keys, which currently can't be
932 # swarming mixin entries. Ignore them
933 continue
934
935 seen_mixins = seen_mixins.union(test.get('swarming_mixins', set()))
936
937 missing_mixins = set(self.swarming_mixins.keys()) - seen_mixins
938 if missing_mixins:
939 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
940 ' referenced in a waterfall, machine, or test suite.' % (
941 str(missing_mixins)))
942
Stephen Martinisf83893722018-09-19 00:02:18943 self.check_input_files_sorting(verbose)
944
945 def check_input_files_sorting(self, verbose=False):
946 bad_files = []
947 # FIXME: Expand to other files. It's unclear if every other file should be
948 # similarly sorted.
949 for filename in ('swarming_mixins.pyl',):
950 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
951
952 def type_assert(itm, typ): # pragma: no cover
953 if not isinstance(itm, typ):
954 raise BBGenErr(
955 'Invalid .pyl file %s. %s expected to be %s, is %s' % (
956 filename, itm, typ, type(itm)))
957
958 # Must be a module.
959 type_assert(parsed, ast.Module)
960 module = parsed.body
961
962 # Only one expression in the module.
963 type_assert(module, list)
964 if len(module) != 1: # pragma: no cover
965 raise BBGenErr('Invalid .pyl file %s' % filename)
966 expr = module[0]
967 type_assert(expr, ast.Expr)
968
969 # Value should be a dictionary.
970 value = expr.value
971 type_assert(value, ast.Dict)
972
973 keys = []
974 # The keys of this dict are ordered as ordered in the file; normal python
975 # dictionary keys are given an arbitrary order, but since we parsed the
976 # file itself, the order as given in the file is preserved.
977 for key in value.keys:
978 type_assert(key, ast.Str)
979 keys.append(key.s)
980
981 if sorted(keys) != keys:
982 bad_files.append(filename)
983 if verbose: # pragma: no cover
984 for line in difflib.unified_diff(
985 sorted(keys),
986 keys):
987 print line
988
989 if bad_files:
990 raise BBGenErr(
991 'The following files have unsorted top level keys: %s' % (
992 ', '.join(bad_files)))
993
994
Kenneth Russelleb60cbd22017-12-05 07:54:28995 def check_output_file_consistency(self, verbose=False):
996 self.load_configuration_files()
997 # All waterfalls must have been written by this script already.
998 self.resolve_configuration_files()
999 ungenerated_waterfalls = set()
1000 for waterfall in self.waterfalls:
1001 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111002 file_path = waterfall['name'] + '.json'
1003 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281004 if expected != current:
1005 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321006 if verbose: # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281007 print ('Waterfall ' + waterfall['name'] +
1008 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321009 'contents:')
1010 for line in difflib.unified_diff(
1011 expected.splitlines(),
1012 current.splitlines()):
1013 print line
Kenneth Russelleb60cbd22017-12-05 07:54:281014 if ungenerated_waterfalls:
1015 raise BBGenErr('The following waterfalls have not been properly '
1016 'autogenerated by generate_buildbot_json.py: ' +
1017 str(ungenerated_waterfalls))
1018
1019 def check_consistency(self, verbose=False):
1020 self.check_input_file_consistency() # pragma: no cover
1021 self.check_output_file_consistency(verbose) # pragma: no cover
1022
1023 def parse_args(self, argv): # pragma: no cover
1024 parser = argparse.ArgumentParser()
1025 parser.add_argument(
1026 '-c', '--check', action='store_true', help=
1027 'Do consistency checks of configuration and generated files and then '
1028 'exit. Used during presubmit. Causes the tool to not generate any files.')
1029 parser.add_argument(
1030 '-n', '--new-files', action='store_true', help=
1031 'Write output files as .new.json. Useful during development so old and '
1032 'new files can be looked at side-by-side.')
1033 parser.add_argument(
1034 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1035 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111036 parser.add_argument(
1037 '--pyl-files-dir', type=os.path.realpath,
1038 help='Path to the directory containing the input .pyl files.')
Kenneth Russelleb60cbd22017-12-05 07:54:281039 self.args = parser.parse_args(argv)
1040
1041 def main(self, argv): # pragma: no cover
1042 self.parse_args(argv)
1043 if self.args.check:
1044 self.check_consistency()
1045 else:
1046 self.generate_waterfalls()
1047 return 0
1048
1049if __name__ == "__main__": # pragma: no cover
1050 generator = BBJSONGenerator()
1051 sys.exit(generator.main(sys.argv[1:]))