blob: 96d82cefd8bb207ef84fc5258e8ae3123883e0e0 [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
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.
425 test = self.apply_all_swarming_mixins(
426 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'):
438 test['trigger_script'] = {
439 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
440 'args': [
441 '--multiple-trigger-configs',
442 json.dumps(tester_config['swarming']['dimension_sets'] +
443 tester_config.get('alternate_swarming_dimensions', [])),
444 '--multiple-dimension-script-verbose',
445 'True'
446 ],
447 }
448
Kenneth Russelleb60cbd22017-12-05 07:54:28449 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
450 test_config):
451 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15452 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28453 return None
454 result = copy.deepcopy(test_config)
455 if 'test' in result:
456 result['name'] = test_name
457 else:
458 result['test'] = test_name
459 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21460
461 self.initialize_args_for_test(
462 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28463 if self.is_android(tester_config) and tester_config.get('use_swarming',
464 True):
Kenneth Russell8a386d42018-06-02 09:48:01465 args = result.get('args', [])
Kenneth Russell5612d64a2018-06-02 21:12:30466 args.append('--gs-results-bucket=chromium-result-details')
Nico Weberd18b8962018-05-16 19:39:38467 if (result['swarming']['can_use_on_swarming_builders'] and not
468 tester_config.get('skip_merge_script', False)):
Kenneth Russelleb60cbd22017-12-05 07:54:28469 result['merge'] = {
470 'args': [
471 '--bucket',
472 'chromium-result-details',
473 '--test-name',
474 test_name
475 ],
Nico Weberd18b8962018-05-16 19:39:38476 'script': '//build/android/pylib/results/presentation/'
Kenneth Russelleb60cbd22017-12-05 07:54:28477 'test_results_presentation.py',
478 } # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:28479 if not tester_config.get('skip_cipd_packages', False):
480 result['swarming']['cipd_packages'] = [
481 {
482 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
483 'location': 'bin',
484 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
485 }
486 ]
Kenneth Russelleb60cbd22017-12-05 07:54:28487 if not tester_config.get('skip_output_links', False):
488 result['swarming']['output_links'] = [
489 {
490 'link': [
491 'https://luci-logdog.appspot.com/v/?s',
492 '=android%2Fswarming%2Flogcats%2F',
493 '${TASK_ID}%2F%2B%2Funified_logcats',
494 ],
495 'name': 'shard #${SHARD_INDEX} logcats',
496 },
497 ]
Kenneth Russell5612d64a2018-06-02 21:12:30498 args.append('--recover-devices')
Kenneth Russell8a386d42018-06-02 09:48:01499 if args:
500 result['args'] = args
Benjamin Pastene766d48f52017-12-18 21:47:42501
Stephen Martinis0382bc12018-09-17 22:29:07502 result = self.update_and_cleanup_test(
503 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09504 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28505 return result
506
507 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
508 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01509 if not self.should_run_on_tester(waterfall, tester_name, test_name,
510 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28511 return None
512 result = copy.deepcopy(test_config)
513 result['isolate_name'] = result.get('isolate_name', test_name)
514 result['name'] = test_name
515 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01516 self.initialize_args_for_test(result, tester_config)
Stephen Martinis0382bc12018-09-17 22:29:07517 result = self.update_and_cleanup_test(
518 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09519 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28520 return result
521
522 def generate_script_test(self, waterfall, tester_name, tester_config,
523 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01524 if not self.should_run_on_tester(waterfall, tester_name, test_name,
525 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28526 return None
527 result = {
528 'name': test_name,
529 'script': test_config['script']
530 }
Stephen Martinis0382bc12018-09-17 22:29:07531 result = self.update_and_cleanup_test(
532 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28533 return result
534
535 def generate_junit_test(self, waterfall, tester_name, tester_config,
536 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01537 del tester_config
538 if not self.should_run_on_tester(waterfall, tester_name, test_name,
539 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28540 return None
541 result = {
542 'test': test_name,
543 }
544 return result
545
546 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
547 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01548 if not self.should_run_on_tester(waterfall, tester_name, test_name,
549 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28550 return None
551 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28552 if 'test' in result and result['test'] != test_name:
553 result['name'] = test_name
554 else:
555 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07556 result = self.update_and_cleanup_test(
557 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28558 return result
559
Kenneth Russell8a386d42018-06-02 09:48:01560 def substitute_gpu_args(self, tester_config, args):
561 substitutions = {
562 # Any machine in waterfalls.pyl which desires to run GPU tests
563 # must provide the os_type key.
564 'os_type': tester_config['os_type'],
565 'gpu_vendor_id': '0',
566 'gpu_device_id': '0',
567 }
568 dimension_set = tester_config['swarming']['dimension_sets'][0]
569 if 'gpu' in dimension_set:
570 # First remove the driver version, then split into vendor and device.
571 gpu = dimension_set['gpu']
572 gpu = gpu.split('-')[0].split(':')
573 substitutions['gpu_vendor_id'] = gpu[0]
574 substitutions['gpu_device_id'] = gpu[1]
575 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
576
577 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
578 test_name, test_config):
579 # These are all just specializations of isolated script tests with
580 # a bunch of boilerplate command line arguments added.
581
582 # The step name must end in 'test' or 'tests' in order for the
583 # results to automatically show up on the flakiness dashboard.
584 # (At least, this was true some time ago.) Continue to use this
585 # naming convention for the time being to minimize changes.
586 step_name = test_config.get('name', test_name)
587 if not (step_name.endswith('test') or step_name.endswith('tests')):
588 step_name = '%s_tests' % step_name
589 result = self.generate_isolated_script_test(
590 waterfall, tester_name, tester_config, step_name, test_config)
591 if not result:
592 return None
593 result['isolate_name'] = 'telemetry_gpu_integration_test'
594 args = result.get('args', [])
595 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14596
597 # These tests upload and download results from cloud storage and therefore
598 # aren't idempotent yet. https://crbug.com/549140.
599 result['swarming']['idempotent'] = False
600
Kenneth Russell8a386d42018-06-02 09:48:01601 args = [
602 test_to_run,
603 '--show-stdout',
604 '--browser=%s' % tester_config['browser_config'],
605 # --passthrough displays more of the logging in Telemetry when
606 # run via typ, in particular some of the warnings about tests
607 # being expected to fail, but passing.
608 '--passthrough',
609 '-v',
610 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
611 ] + args
612 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
613 tester_config, args))
614 return result
615
Kenneth Russelleb60cbd22017-12-05 07:54:28616 def get_test_generator_map(self):
617 return {
618 'cts_tests': CTSGenerator(self),
Kenneth Russell8a386d42018-06-02 09:48:01619 'gpu_telemetry_tests': GPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28620 'gtest_tests': GTestGenerator(self),
621 'instrumentation_tests': InstrumentationTestGenerator(self),
622 'isolated_scripts': IsolatedScriptTestGenerator(self),
623 'junit_tests': JUnitGenerator(self),
624 'scripts': ScriptGenerator(self),
625 }
626
Kenneth Russell8a386d42018-06-02 09:48:01627 def get_test_type_remapper(self):
628 return {
629 # These are a specialization of isolated_scripts with a bunch of
630 # boilerplate command line arguments added to each one.
631 'gpu_telemetry_tests': 'isolated_scripts',
632 }
633
Kenneth Russelleb60cbd22017-12-05 07:54:28634 def check_composition_test_suites(self):
635 # Pre-pass to catch errors reliably.
636 for name, value in self.test_suites.iteritems():
637 if isinstance(value, list):
638 for entry in value:
639 if isinstance(self.test_suites[entry], list):
Nico Weberd18b8962018-05-16 19:39:38640 raise BBGenErr('Composition test suites may not refer to other '
641 'composition test suites (error found while '
Kenneth Russelleb60cbd22017-12-05 07:54:28642 'processing %s)' % name)
643
644 def resolve_composition_test_suites(self):
645 self.check_composition_test_suites()
646 for name, value in self.test_suites.iteritems():
647 if isinstance(value, list):
648 # Resolve this to a dictionary.
649 full_suite = {}
650 for entry in value:
651 suite = self.test_suites[entry]
652 full_suite.update(suite)
653 self.test_suites[name] = full_suite
654
655 def link_waterfalls_to_test_suites(self):
656 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43657 for tester_name, tester in waterfall['machines'].iteritems():
658 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28659 if not value in self.test_suites:
660 # Hard / impossible to cover this in the unit test.
661 raise self.unknown_test_suite(
662 value, tester_name, waterfall['name']) # pragma: no cover
663 tester['test_suites'][suite] = self.test_suites[value]
664
665 def load_configuration_files(self):
666 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
667 self.test_suites = self.load_pyl_file('test_suites.pyl')
668 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb6a50492018-09-12 23:59:32669 self.swarming_mixins = self.load_pyl_file('swarming_mixins.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:28670
671 def resolve_configuration_files(self):
672 self.resolve_composition_test_suites()
673 self.link_waterfalls_to_test_suites()
674
Nico Weberd18b8962018-05-16 19:39:38675 def unknown_bot(self, bot_name, waterfall_name):
676 return BBGenErr(
677 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
678
Kenneth Russelleb60cbd22017-12-05 07:54:28679 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
680 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38681 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28682 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
683
684 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
685 return BBGenErr(
686 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
687 ' on waterfall ' + waterfall_name)
688
Stephen Martinis0382bc12018-09-17 22:29:07689 def apply_all_swarming_mixins(self, test, waterfall, builder_name, builder):
690 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:32691
692 Checks in the waterfall, builder, and test objects for mixins.
693 """
694 def valid_mixin(mixin_name):
695 """Asserts that the mixin is valid."""
696 if mixin_name not in self.swarming_mixins:
697 raise BBGenErr("bad mixin %s" % mixin_name)
698 def must_be_list(mixins, typ, name):
699 """Asserts that given mixins are a list."""
700 if not isinstance(mixins, list):
701 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
702
703 if 'swarming_mixins' in waterfall:
704 must_be_list(waterfall['swarming_mixins'], 'waterfall', waterfall['name'])
705 for mixin in waterfall['swarming_mixins']:
706 valid_mixin(mixin)
Stephen Martinis0382bc12018-09-17 22:29:07707 test = self.apply_swarming_mixin(self.swarming_mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32708
709 if 'swarming_mixins' in builder:
710 must_be_list(builder['swarming_mixins'], 'builder', builder_name)
711 for mixin in builder['swarming_mixins']:
712 valid_mixin(mixin)
Stephen Martinisb6a50492018-09-12 23:59:32713 test = self.apply_swarming_mixin(self.swarming_mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32714
Stephen Martinis0382bc12018-09-17 22:29:07715 if not 'swarming_mixins' in test:
716 return test
717
718 must_be_list(test['swarming_mixins'], 'test', test['test'])
719 for mixin in test['swarming_mixins']:
720 valid_mixin(mixin)
721 test = self.apply_swarming_mixin(self.swarming_mixins[mixin], test)
722 del test['swarming_mixins']
723 return test
Stephen Martinisb6a50492018-09-12 23:59:32724
725 def apply_swarming_mixin(self, mixin, test):
726 """Applies a swarming mixin to a test.
727
Stephen Martinis0382bc12018-09-17 22:29:07728 Mixins will not override an existing key. This is to ensure exceptions can
729 override a setting a mixin applies.
730
Stephen Martinisb6a50492018-09-12 23:59:32731 Dimensions are handled in a special way. Instead of specifying
732 'dimension_sets', which is how normal test suites specify their dimensions,
733 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
734 is then applied to every dimension set in the test.
735 """
736 new_test = copy.deepcopy(test)
737 mixin = copy.deepcopy(mixin)
738
Stephen Martinis0382bc12018-09-17 22:29:07739 new_test.setdefault('swarming', {})
Stephen Martinisb6a50492018-09-12 23:59:32740 if 'dimensions' in mixin:
Stephen Martinis0382bc12018-09-17 22:29:07741 new_test['swarming'].setdefault('dimension_sets', [{}])
Stephen Martinisb6a50492018-09-12 23:59:32742 for dimension_set in new_test['swarming']['dimension_sets']:
743 dimension_set.update(mixin['dimensions'])
744 del mixin['dimensions']
745
Stephen Martinis0382bc12018-09-17 22:29:07746 new_test['swarming'].update(mixin)
747
Stephen Martinisb6a50492018-09-12 23:59:32748 return new_test
749
Kenneth Russelleb60cbd22017-12-05 07:54:28750 def generate_waterfall_json(self, waterfall):
751 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28752 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:01753 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:43754 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28755 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43756 # Copy only well-understood entries in the machine's configuration
757 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28758 if 'additional_compile_targets' in config:
759 tests['additional_compile_targets'] = config[
760 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43761 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28762 if test_type not in generator_map:
763 raise self.unknown_test_suite_type(
764 test_type, name, waterfall['name']) # pragma: no cover
765 test_generator = generator_map[test_type]
Nico Weber79dc5f6852018-07-13 19:38:49766 # Let multiple kinds of generators generate the same kinds
767 # of tests. For example, gpu_telemetry_tests are a
768 # specialization of isolated_scripts.
769 new_tests = test_generator.generate(
770 waterfall, name, config, input_tests)
771 remapped_test_type = test_type_remapper.get(test_type, test_type)
772 tests[remapped_test_type] = test_generator.sort(
773 tests.get(remapped_test_type, []) + new_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28774 all_tests[name] = tests
775 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
776 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
777 return json.dumps(all_tests, indent=2, separators=(',', ': '),
778 sort_keys=True) + '\n'
779
780 def generate_waterfalls(self): # pragma: no cover
781 self.load_configuration_files()
782 self.resolve_configuration_files()
783 filters = self.args.waterfall_filters
784 suffix = '.json'
785 if self.args.new_files:
786 suffix = '.new' + suffix
787 for waterfall in self.waterfalls:
788 should_gen = not filters or waterfall['name'] in filters
789 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:11790 file_path = waterfall['name'] + suffix
791 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:28792 self.generate_waterfall_json(waterfall))
793
Nico Weberd18b8962018-05-16 19:39:38794 def get_valid_bot_names(self):
795 # Extract bot names from infra/config/global/luci-milo.cfg.
796 bot_names = set()
John Budorickc12abd12018-08-14 19:37:43797 infra_config_dir = os.path.abspath(
798 os.path.join(os.path.dirname(__file__),
799 '..', '..', 'infra', 'config', 'global'))
800 milo_configs = [
801 os.path.join(infra_config_dir, 'luci-milo.cfg'),
802 os.path.join(infra_config_dir, 'luci-milo-dev.cfg'),
803 ]
804 for c in milo_configs:
805 for l in self.read_file(c).splitlines():
806 if (not 'name: "buildbucket/luci.chromium.' in l and
807 not 'name: "buildbot/chromium.' in l):
808 continue
809 # l looks like
810 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
811 # Extract win_chromium_dbg_ng part.
812 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:38813 return bot_names
814
Kenneth Russell8a386d42018-06-02 09:48:01815 def get_bots_that_do_not_actually_exist(self):
816 # Some of the bots on the chromium.gpu.fyi waterfall in particular
817 # are defined only to be mirrored into trybots, and don't actually
818 # exist on any of the waterfalls or consoles.
819 return [
820 'Optional Android Release (Nexus 5X)',
821 'Optional Linux Release (Intel HD 630)',
822 'Optional Linux Release (NVIDIA)',
823 'Optional Mac Release (Intel)',
824 'Optional Mac Retina Release (AMD)',
825 'Optional Mac Retina Release (NVIDIA)',
826 'Optional Win10 Release (Intel HD 630)',
827 'Optional Win10 Release (NVIDIA)',
828 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:08829 # chromium.fyi
830 'chromeos-amd64-generic-rel-vm-tests',
Dirk Pranke85369442018-06-16 02:01:29831 'linux-blink-rel-dummy',
832 'mac10.10-blink-rel-dummy',
833 'mac10.11-blink-rel-dummy',
834 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:20835 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:29836 'mac10.13-blink-rel-dummy',
837 'win7-blink-rel-dummy',
838 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:08839 'Dummy WebKit Mac10.13',
840 'WebKit Linux layout_ng Dummy Builder',
841 'WebKit Linux root_layer_scrolls Dummy Builder',
842 'WebKit Linux slimming_paint_v2 Dummy Builder',
Stephen Martinis769b25112018-08-30 18:52:06843 # chromium, due to https://crbug.com/878915
844 'win-dbg',
845 'win32-dbg',
Kenneth Russell8a386d42018-06-02 09:48:01846 ]
847
Stephen Martinisf83893722018-09-19 00:02:18848 def check_input_file_consistency(self, verbose=False):
Kenneth Russelleb60cbd22017-12-05 07:54:28849 self.load_configuration_files()
850 self.check_composition_test_suites()
Nico Weberd18b8962018-05-16 19:39:38851
852 # All bots should exist.
853 bot_names = self.get_valid_bot_names()
Kenneth Russell8a386d42018-06-02 09:48:01854 bots_that_dont_exist = self.get_bots_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:38855 for waterfall in self.waterfalls:
856 for bot_name in waterfall['machines']:
Kenneth Russell8a386d42018-06-02 09:48:01857 if bot_name in bots_that_dont_exist:
858 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38859 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:08860 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:38861 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:52862 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:32863 if waterfall['name'] in ['tryserver.webrtc',
864 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:38865 # These waterfalls have their bot configs in a different repo.
866 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:52867 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38868 raise self.unknown_bot(bot_name, waterfall['name'])
869
Kenneth Russelleb60cbd22017-12-05 07:54:28870 # All test suites must be referenced.
871 suites_seen = set()
872 generator_map = self.get_test_generator_map()
873 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43874 for bot_name, tester in waterfall['machines'].iteritems():
875 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28876 if suite_type not in generator_map:
877 raise self.unknown_test_suite_type(suite_type, bot_name,
878 waterfall['name'])
879 if suite not in self.test_suites:
880 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
881 suites_seen.add(suite)
882 # Since we didn't resolve the configuration files, this set
883 # includes both composition test suites and regular ones.
884 resolved_suites = set()
885 for suite_name in suites_seen:
886 suite = self.test_suites[suite_name]
887 if isinstance(suite, list):
888 for sub_suite in suite:
889 resolved_suites.add(sub_suite)
890 resolved_suites.add(suite_name)
891 # At this point, every key in test_suites.pyl should be referenced.
892 missing_suites = set(self.test_suites.keys()) - resolved_suites
893 if missing_suites:
894 raise BBGenErr('The following test suites were unreferenced by bots on '
895 'the waterfalls: ' + str(missing_suites))
896
897 # All test suite exceptions must refer to bots on the waterfall.
898 all_bots = set()
899 missing_bots = set()
900 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43901 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28902 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:28903 # In order to disambiguate between bots with the same name on
904 # different waterfalls, support has been added to various
905 # exceptions for concatenating the waterfall name after the bot
906 # name.
907 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28908 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:38909 removals = (exception.get('remove_from', []) +
910 exception.get('remove_gtest_from', []) +
911 exception.get('modifications', {}).keys())
912 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:28913 if removal not in all_bots:
914 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:41915
916 missing_bots = missing_bots - set(bots_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:28917 if missing_bots:
918 raise BBGenErr('The following nonexistent machines were referenced in '
919 'the test suite exceptions: ' + str(missing_bots))
920
Stephen Martinis0382bc12018-09-17 22:29:07921 # All mixins must be referenced
922 seen_mixins = set()
923 for waterfall in self.waterfalls:
924 seen_mixins = seen_mixins.union(waterfall.get('swarming_mixins', set()))
925 for bot_name, tester in waterfall['machines'].iteritems():
926 seen_mixins = seen_mixins.union(tester.get('swarming_mixins', set()))
927 for suite in self.test_suites.values():
928 if isinstance(suite, list):
929 # Don't care about this, it's a composition, which shouldn't include a
930 # swarming mixin.
931 continue
932
933 for test in suite.values():
934 if not isinstance(test, dict):
935 # Some test suites have top level keys, which currently can't be
936 # swarming mixin entries. Ignore them
937 continue
938
939 seen_mixins = seen_mixins.union(test.get('swarming_mixins', set()))
940
941 missing_mixins = set(self.swarming_mixins.keys()) - seen_mixins
942 if missing_mixins:
943 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
944 ' referenced in a waterfall, machine, or test suite.' % (
945 str(missing_mixins)))
946
Stephen Martinisf83893722018-09-19 00:02:18947 self.check_input_files_sorting(verbose)
948
949 def check_input_files_sorting(self, verbose=False):
950 bad_files = []
951 # FIXME: Expand to other files. It's unclear if every other file should be
952 # similarly sorted.
953 for filename in ('swarming_mixins.pyl',):
954 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
955
956 def type_assert(itm, typ): # pragma: no cover
957 if not isinstance(itm, typ):
958 raise BBGenErr(
959 'Invalid .pyl file %s. %s expected to be %s, is %s' % (
960 filename, itm, typ, type(itm)))
961
962 # Must be a module.
963 type_assert(parsed, ast.Module)
964 module = parsed.body
965
966 # Only one expression in the module.
967 type_assert(module, list)
968 if len(module) != 1: # pragma: no cover
969 raise BBGenErr('Invalid .pyl file %s' % filename)
970 expr = module[0]
971 type_assert(expr, ast.Expr)
972
973 # Value should be a dictionary.
974 value = expr.value
975 type_assert(value, ast.Dict)
976
977 keys = []
978 # The keys of this dict are ordered as ordered in the file; normal python
979 # dictionary keys are given an arbitrary order, but since we parsed the
980 # file itself, the order as given in the file is preserved.
981 for key in value.keys:
982 type_assert(key, ast.Str)
983 keys.append(key.s)
984
985 if sorted(keys) != keys:
986 bad_files.append(filename)
987 if verbose: # pragma: no cover
988 for line in difflib.unified_diff(
989 sorted(keys),
990 keys):
Stephen Martinis7eb8b612018-09-21 00:17:50991 self.print_line(line)
Stephen Martinisf83893722018-09-19 00:02:18992
993 if bad_files:
994 raise BBGenErr(
995 'The following files have unsorted top level keys: %s' % (
996 ', '.join(bad_files)))
997
998
Kenneth Russelleb60cbd22017-12-05 07:54:28999 def check_output_file_consistency(self, verbose=False):
1000 self.load_configuration_files()
1001 # All waterfalls must have been written by this script already.
1002 self.resolve_configuration_files()
1003 ungenerated_waterfalls = set()
1004 for waterfall in self.waterfalls:
1005 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111006 file_path = waterfall['name'] + '.json'
1007 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281008 if expected != current:
1009 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321010 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501011 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281012 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321013 'contents:')
1014 for line in difflib.unified_diff(
1015 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501016 current.splitlines(),
1017 fromfile='expected', tofile='current'):
1018 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281019 if ungenerated_waterfalls:
1020 raise BBGenErr('The following waterfalls have not been properly '
1021 'autogenerated by generate_buildbot_json.py: ' +
1022 str(ungenerated_waterfalls))
1023
1024 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501025 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281026 self.check_output_file_consistency(verbose) # pragma: no cover
1027
1028 def parse_args(self, argv): # pragma: no cover
1029 parser = argparse.ArgumentParser()
1030 parser.add_argument(
1031 '-c', '--check', action='store_true', help=
1032 'Do consistency checks of configuration and generated files and then '
1033 'exit. Used during presubmit. Causes the tool to not generate any files.')
1034 parser.add_argument(
1035 '-n', '--new-files', action='store_true', help=
1036 'Write output files as .new.json. Useful during development so old and '
1037 'new files can be looked at side-by-side.')
1038 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501039 '-v', '--verbose', action='store_true', help=
1040 'Increases verbosity. Affects consistency checks.')
1041 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281042 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1043 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111044 parser.add_argument(
1045 '--pyl-files-dir', type=os.path.realpath,
1046 help='Path to the directory containing the input .pyl files.')
Kenneth Russelleb60cbd22017-12-05 07:54:281047 self.args = parser.parse_args(argv)
1048
1049 def main(self, argv): # pragma: no cover
1050 self.parse_args(argv)
1051 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:501052 self.check_consistency(verbose=self.args.verbose)
Kenneth Russelleb60cbd22017-12-05 07:54:281053 else:
1054 self.generate_waterfalls()
1055 return 0
1056
1057if __name__ == "__main__": # pragma: no cover
1058 generator = BBJSONGenerator()
1059 sys.exit(generator.main(sys.argv[1:]))