blob: 548ee0125020b3cf4402943009fb07e950323a03 [file] [log] [blame]
Kenneth Russelleb60cbd22017-12-05 07:54:281#!/usr/bin/env python
2# Copyright 2016 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Script to generate the majority of the JSON files in the src/testing/buildbot
7directory. Maintaining these files by hand is too unwieldy.
8"""
9
10import argparse
11import ast
12import collections
13import copy
John Budorick826d5ed2017-12-28 19:27:3214import difflib
Kenneth Russell8ceeabf2017-12-11 17:53:2815import itertools
Kenneth Russelleb60cbd22017-12-05 07:54:2816import json
17import os
18import string
19import sys
John Budorick826d5ed2017-12-28 19:27:3220import traceback
Kenneth Russelleb60cbd22017-12-05 07:54:2821
22THIS_DIR = os.path.dirname(os.path.abspath(__file__))
23
24
25class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4926 def __init__(self, message):
27 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2828
29
Kenneth Russell8ceeabf2017-12-11 17:53:2830# This class is only present to accommodate certain machines on
31# chromium.android.fyi which run certain tests as instrumentation
32# tests, but not as gtests. If this discrepancy were fixed then the
33# notion could be removed.
34class TestSuiteTypes(object):
35 GTEST = 'gtest'
36
37
Kenneth Russelleb60cbd22017-12-05 07:54:2838class BaseGenerator(object):
39 def __init__(self, bb_gen):
40 self.bb_gen = bb_gen
41
Kenneth Russell8ceeabf2017-12-11 17:53:2842 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2843 raise NotImplementedError()
44
45 def sort(self, tests):
46 raise NotImplementedError()
47
48
Kenneth Russell8ceeabf2017-12-11 17:53:2849def cmp_tests(a, b):
50 # Prefer to compare based on the "test" key.
51 val = cmp(a['test'], b['test'])
52 if val != 0:
53 return val
54 if 'name' in a and 'name' in b:
55 return cmp(a['name'], b['name']) # pragma: no cover
56 if 'name' not in a and 'name' not in b:
57 return 0 # pragma: no cover
58 # Prefer to put variants of the same test after the first one.
59 if 'name' in a:
60 return 1
61 # 'name' is in b.
62 return -1 # pragma: no cover
63
64
Kenneth Russell8a386d42018-06-02 09:48:0165class GPUTelemetryTestGenerator(BaseGenerator):
66 def __init__(self, bb_gen):
67 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
68
69 def generate(self, waterfall, tester_name, tester_config, input_tests):
70 isolated_scripts = []
71 for test_name, test_config in sorted(input_tests.iteritems()):
72 test = self.bb_gen.generate_gpu_telemetry_test(
73 waterfall, tester_name, tester_config, test_name, test_config)
74 if test:
75 isolated_scripts.append(test)
76 return isolated_scripts
77
78 def sort(self, tests):
79 return sorted(tests, key=lambda x: x['name'])
80
81
Kenneth Russelleb60cbd22017-12-05 07:54:2882class GTestGenerator(BaseGenerator):
83 def __init__(self, bb_gen):
84 super(GTestGenerator, self).__init__(bb_gen)
85
Kenneth Russell8ceeabf2017-12-11 17:53:2886 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2887 # The relative ordering of some of the tests is important to
88 # minimize differences compared to the handwritten JSON files, since
89 # Python's sorts are stable and there are some tests with the same
90 # key (see gles2_conform_d3d9_test and similar variants). Avoid
91 # losing the order by avoiding coalescing the dictionaries into one.
92 gtests = []
93 for test_name, test_config in sorted(input_tests.iteritems()):
Nico Weber79dc5f6852018-07-13 19:38:4994 test = self.bb_gen.generate_gtest(
95 waterfall, tester_name, tester_config, test_name, test_config)
96 if test:
97 # generate_gtest may veto the test generation on this tester.
98 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:2899 return gtests
100
101 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28102 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28103
104
105class IsolatedScriptTestGenerator(BaseGenerator):
106 def __init__(self, bb_gen):
107 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
108
Kenneth Russell8ceeabf2017-12-11 17:53:28109 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28110 isolated_scripts = []
111 for test_name, test_config in sorted(input_tests.iteritems()):
112 test = self.bb_gen.generate_isolated_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28113 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28114 if test:
115 isolated_scripts.append(test)
116 return isolated_scripts
117
118 def sort(self, tests):
119 return sorted(tests, key=lambda x: x['name'])
120
121
122class ScriptGenerator(BaseGenerator):
123 def __init__(self, bb_gen):
124 super(ScriptGenerator, self).__init__(bb_gen)
125
Kenneth Russell8ceeabf2017-12-11 17:53:28126 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28127 scripts = []
128 for test_name, test_config in sorted(input_tests.iteritems()):
129 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28130 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28131 if test:
132 scripts.append(test)
133 return scripts
134
135 def sort(self, tests):
136 return sorted(tests, key=lambda x: x['name'])
137
138
139class JUnitGenerator(BaseGenerator):
140 def __init__(self, bb_gen):
141 super(JUnitGenerator, self).__init__(bb_gen)
142
Kenneth Russell8ceeabf2017-12-11 17:53:28143 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28144 scripts = []
145 for test_name, test_config in sorted(input_tests.iteritems()):
146 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28147 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28148 if test:
149 scripts.append(test)
150 return scripts
151
152 def sort(self, tests):
153 return sorted(tests, key=lambda x: x['test'])
154
155
156class CTSGenerator(BaseGenerator):
157 def __init__(self, bb_gen):
158 super(CTSGenerator, self).__init__(bb_gen)
159
Kenneth Russell8ceeabf2017-12-11 17:53:28160 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28161 # These only contain one entry and it's the contents of the input tests'
162 # dictionary, verbatim.
163 cts_tests = []
164 cts_tests.append(input_tests)
165 return cts_tests
166
167 def sort(self, tests):
168 return tests
169
170
171class InstrumentationTestGenerator(BaseGenerator):
172 def __init__(self, bb_gen):
173 super(InstrumentationTestGenerator, self).__init__(bb_gen)
174
Kenneth Russell8ceeabf2017-12-11 17:53:28175 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28176 scripts = []
177 for test_name, test_config in sorted(input_tests.iteritems()):
178 test = self.bb_gen.generate_instrumentation_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28179 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28180 if test:
181 scripts.append(test)
182 return scripts
183
184 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28185 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28186
187
188class BBJSONGenerator(object):
189 def __init__(self):
190 self.this_dir = THIS_DIR
191 self.args = None
192 self.waterfalls = None
193 self.test_suites = None
194 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01195 self.mixins = None
Kenneth Russelleb60cbd22017-12-05 07:54:28196
197 def generate_abs_file_path(self, relative_path):
198 return os.path.join(self.this_dir, relative_path) # pragma: no cover
199
Stephen Martinis7eb8b612018-09-21 00:17:50200 def print_line(self, line):
201 # Exists so that tests can mock
202 print line # pragma: no cover
203
Kenneth Russelleb60cbd22017-12-05 07:54:28204 def read_file(self, relative_path):
205 with open(self.generate_abs_file_path(
206 relative_path)) as fp: # pragma: no cover
207 return fp.read() # pragma: no cover
208
209 def write_file(self, relative_path, contents):
210 with open(self.generate_abs_file_path(
211 relative_path), 'wb') as fp: # pragma: no cover
212 fp.write(contents) # pragma: no cover
213
Zhiling Huangbe008172018-03-08 19:13:11214 def pyl_file_path(self, filename):
215 if self.args and self.args.pyl_files_dir:
216 return os.path.join(self.args.pyl_files_dir, filename)
217 return filename
218
Kenneth Russelleb60cbd22017-12-05 07:54:28219 def load_pyl_file(self, filename):
220 try:
Zhiling Huangbe008172018-03-08 19:13:11221 return ast.literal_eval(self.read_file(
222 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28223 except (SyntaxError, ValueError) as e: # pragma: no cover
224 raise BBGenErr('Failed to parse pyl file "%s": %s' %
225 (filename, e)) # pragma: no cover
226
Kenneth Russell8a386d42018-06-02 09:48:01227 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
228 # Currently it is only mandatory for bots which run GPU tests. Change these to
229 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28230 def is_android(self, tester_config):
231 return tester_config.get('os_type') == 'android'
232
Kenneth Russell8a386d42018-06-02 09:48:01233 def is_linux(self, tester_config):
234 return tester_config.get('os_type') == 'linux'
235
Kenneth Russelleb60cbd22017-12-05 07:54:28236 def get_exception_for_test(self, test_name, test_config):
237 # gtests may have both "test" and "name" fields, and usually, if the "name"
238 # field is specified, it means that the same test is being repurposed
239 # multiple times with different command line arguments. To handle this case,
240 # prefer to lookup per the "name" field of the test itself, as opposed to
241 # the "test_name", which is actually the "test" field.
242 if 'name' in test_config:
243 return self.exceptions.get(test_config['name'])
244 else:
245 return self.exceptions.get(test_name)
246
Nico Weberb0b3f5862018-07-13 18:45:15247 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28248 # Currently, the only reason a test should not run on a given tester is that
249 # it's in the exceptions. (Once the GPU waterfall generation script is
250 # incorporated here, the rules will become more complex.)
251 exception = self.get_exception_for_test(test_name, test_config)
252 if not exception:
253 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28254 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28255 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28256 if remove_from:
257 if tester_name in remove_from:
258 return False
259 # TODO(kbr): this code path was added for some tests (including
260 # android_webview_unittests) on one machine (Nougat Phone
261 # Tester) which exists with the same name on two waterfalls,
262 # chromium.android and chromium.fyi; the tests are run on one
263 # but not the other. Once the bots are all uniquely named (a
264 # different ongoing project) this code should be removed.
265 # TODO(kbr): add coverage.
266 return (tester_name + ' ' + waterfall['name']
267 not in remove_from) # pragma: no cover
268 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28269
Nico Weber79dc5f6852018-07-13 19:38:49270 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28271 exception = self.get_exception_for_test(test_name, test)
272 if not exception:
273 return None
Nico Weber79dc5f6852018-07-13 19:38:49274 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28275
Kenneth Russell8a386d42018-06-02 09:48:01276 def merge_command_line_args(self, arr, prefix, splitter):
277 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01278 idx = 0
279 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01280 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01281 while idx < len(arr):
282 flag = arr[idx]
283 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01284 if flag.startswith(prefix):
285 arg = flag[prefix_len:]
286 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01287 if first_idx < 0:
288 first_idx = idx
289 else:
290 delete_current_entry = True
291 if delete_current_entry:
292 del arr[idx]
293 else:
294 idx += 1
295 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01296 arr[first_idx] = prefix + splitter.join(accumulated_args)
297 return arr
298
299 def maybe_fixup_args_array(self, arr):
300 # The incoming array of strings may be an array of command line
301 # arguments. To make it easier to turn on certain features per-bot or
302 # per-test-suite, look specifically for certain flags and merge them
303 # appropriately.
304 # --enable-features=Feature1 --enable-features=Feature2
305 # are merged to:
306 # --enable-features=Feature1,Feature2
307 # and:
308 # --extra-browser-args=arg1 --extra-browser-args=arg2
309 # are merged to:
310 # --extra-browser-args=arg1 arg2
311 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
312 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01313 return arr
314
Kenneth Russelleb60cbd22017-12-05 07:54:28315 def dictionary_merge(self, a, b, path=None, update=True):
316 """http://stackoverflow.com/questions/7204805/
317 python-dictionaries-of-dictionaries-merge
318 merges b into a
319 """
320 if path is None:
321 path = []
322 for key in b:
323 if key in a:
324 if isinstance(a[key], dict) and isinstance(b[key], dict):
325 self.dictionary_merge(a[key], b[key], path + [str(key)])
326 elif a[key] == b[key]:
327 pass # same leaf value
328 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06329 # Args arrays are lists of strings. Just concatenate them,
330 # and don't sort them, in order to keep some needed
331 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28332 if all(isinstance(x, str)
333 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01334 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28335 else:
336 # TODO(kbr): this only works properly if the two arrays are
337 # the same length, which is currently always the case in the
338 # swarming dimension_sets that we have to merge. It will fail
339 # to merge / override 'args' arrays which are different
340 # length.
341 for idx in xrange(len(b[key])):
342 try:
343 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
344 path + [str(key), str(idx)],
345 update=update)
346 except (IndexError, TypeError): # pragma: no cover
347 raise BBGenErr('Error merging list keys ' + str(key) +
348 ' and indices ' + str(idx) + ' between ' +
349 str(a) + ' and ' + str(b)) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28350 elif update: # pragma: no cover
351 a[key] = b[key] # pragma: no cover
352 else:
353 raise BBGenErr('Conflict at %s' % '.'.join(
354 path + [str(key)])) # pragma: no cover
355 else:
356 a[key] = b[key]
357 return a
358
John Budorickab108712018-09-01 00:12:21359 def initialize_args_for_test(
360 self, generated_test, tester_config, additional_arg_keys=None):
361
362 args = []
363 args.extend(generated_test.get('args', []))
364 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22365
Kenneth Russell8a386d42018-06-02 09:48:01366 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21367 val = generated_test.pop(key, [])
368 if fn(tester_config):
369 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01370
371 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
372 add_conditional_args('linux_args', self.is_linux)
373 add_conditional_args('android_args', self.is_android)
374
John Budorickab108712018-09-01 00:12:21375 for key in additional_arg_keys or []:
376 args.extend(generated_test.pop(key, []))
377 args.extend(tester_config.get(key, []))
378
379 if args:
380 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01381
Kenneth Russelleb60cbd22017-12-05 07:54:28382 def initialize_swarming_dictionary_for_test(self, generated_test,
383 tester_config):
384 if 'swarming' not in generated_test:
385 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28386 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
387 generated_test['swarming'].update({
388 'can_use_on_swarming_builders': tester_config.get('use_swarming', True)
389 })
Kenneth Russelleb60cbd22017-12-05 07:54:28390 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03391 if ('dimension_sets' not in generated_test['swarming'] and
392 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28393 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
394 tester_config['swarming']['dimension_sets'])
395 self.dictionary_merge(generated_test['swarming'],
396 tester_config['swarming'])
397 # Apply any Android-specific Swarming dimensions after the generic ones.
398 if 'android_swarming' in generated_test:
399 if self.is_android(tester_config): # pragma: no cover
400 self.dictionary_merge(
401 generated_test['swarming'],
402 generated_test['android_swarming']) # pragma: no cover
403 del generated_test['android_swarming'] # pragma: no cover
404
405 def clean_swarming_dictionary(self, swarming_dict):
406 # Clean out redundant entries from a test's "swarming" dictionary.
407 # This is really only needed to retain 100% parity with the
408 # handwritten JSON files, and can be removed once all the files are
409 # autogenerated.
410 if 'shards' in swarming_dict:
411 if swarming_dict['shards'] == 1: # pragma: no cover
412 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24413 if 'hard_timeout' in swarming_dict:
414 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
415 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43416 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28417 # Remove all other keys.
418 for k in swarming_dict.keys(): # pragma: no cover
419 if k != 'can_use_on_swarming_builders': # pragma: no cover
420 del swarming_dict[k] # pragma: no cover
421
Stephen Martinis0382bc12018-09-17 22:29:07422 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
423 waterfall):
424 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01425 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07426 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28427 # See if there are any exceptions that need to be merged into this
428 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49429 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28430 if modifications:
431 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23432 if 'swarming' in test:
433 self.clean_swarming_dictionary(test['swarming'])
Kenneth Russelleb60cbd22017-12-05 07:54:28434 return test
435
Shenghua Zhangaba8bad2018-02-07 02:12:09436 def add_common_test_properties(self, test, tester_config):
437 if tester_config.get('use_multi_dimension_trigger_script'):
Kenneth Russell73c3bd8b2018-10-19 22:30:19438 # Assumes update_and_cleanup_test has already been called, so the
439 # builder's mixins have been flattened into the test.
Shenghua Zhangaba8bad2018-02-07 02:12:09440 test['trigger_script'] = {
441 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
442 'args': [
443 '--multiple-trigger-configs',
Kenneth Russell73c3bd8b2018-10-19 22:30:19444 json.dumps(test['swarming']['dimension_sets'] +
Shenghua Zhangaba8bad2018-02-07 02:12:09445 tester_config.get('alternate_swarming_dimensions', [])),
446 '--multiple-dimension-script-verbose',
447 'True'
448 ],
449 }
450
Kenneth Russelleb60cbd22017-12-05 07:54:28451 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
452 test_config):
453 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15454 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28455 return None
456 result = copy.deepcopy(test_config)
457 if 'test' in result:
458 result['name'] = test_name
459 else:
460 result['test'] = test_name
461 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21462
463 self.initialize_args_for_test(
464 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28465 if self.is_android(tester_config) and tester_config.get('use_swarming',
466 True):
Kenneth Russell8a386d42018-06-02 09:48:01467 args = result.get('args', [])
Kenneth Russell5612d64a2018-06-02 21:12:30468 args.append('--gs-results-bucket=chromium-result-details')
Nico Weberd18b8962018-05-16 19:39:38469 if (result['swarming']['can_use_on_swarming_builders'] and not
470 tester_config.get('skip_merge_script', False)):
Kenneth Russelleb60cbd22017-12-05 07:54:28471 result['merge'] = {
472 'args': [
473 '--bucket',
474 'chromium-result-details',
475 '--test-name',
476 test_name
477 ],
Nico Weberd18b8962018-05-16 19:39:38478 'script': '//build/android/pylib/results/presentation/'
Kenneth Russelleb60cbd22017-12-05 07:54:28479 'test_results_presentation.py',
480 } # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:28481 if not tester_config.get('skip_cipd_packages', False):
482 result['swarming']['cipd_packages'] = [
483 {
484 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
485 'location': 'bin',
486 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
487 }
488 ]
Kenneth Russelleb60cbd22017-12-05 07:54:28489 if not tester_config.get('skip_output_links', False):
490 result['swarming']['output_links'] = [
491 {
492 'link': [
493 'https://luci-logdog.appspot.com/v/?s',
494 '=android%2Fswarming%2Flogcats%2F',
495 '${TASK_ID}%2F%2B%2Funified_logcats',
496 ],
497 'name': 'shard #${SHARD_INDEX} logcats',
498 },
499 ]
Kenneth Russell5612d64a2018-06-02 21:12:30500 args.append('--recover-devices')
Kenneth Russell8a386d42018-06-02 09:48:01501 if args:
502 result['args'] = args
Benjamin Pastene766d48f52017-12-18 21:47:42503
Stephen Martinis0382bc12018-09-17 22:29:07504 result = self.update_and_cleanup_test(
505 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09506 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28507 return result
508
509 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
510 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01511 if not self.should_run_on_tester(waterfall, tester_name, test_name,
512 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28513 return None
514 result = copy.deepcopy(test_config)
515 result['isolate_name'] = result.get('isolate_name', test_name)
516 result['name'] = test_name
517 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01518 self.initialize_args_for_test(result, tester_config)
Stephen Martinis0382bc12018-09-17 22:29:07519 result = self.update_and_cleanup_test(
520 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09521 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28522 return result
523
524 def generate_script_test(self, waterfall, tester_name, tester_config,
525 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01526 if not self.should_run_on_tester(waterfall, tester_name, test_name,
527 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28528 return None
529 result = {
530 'name': test_name,
531 'script': test_config['script']
532 }
Stephen Martinis0382bc12018-09-17 22:29:07533 result = self.update_and_cleanup_test(
534 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28535 return result
536
537 def generate_junit_test(self, waterfall, tester_name, tester_config,
538 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01539 del tester_config
540 if not self.should_run_on_tester(waterfall, tester_name, test_name,
541 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28542 return None
543 result = {
544 'test': test_name,
545 }
546 return result
547
548 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
549 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01550 if not self.should_run_on_tester(waterfall, tester_name, test_name,
551 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28552 return None
553 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28554 if 'test' in result and result['test'] != test_name:
555 result['name'] = test_name
556 else:
557 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07558 result = self.update_and_cleanup_test(
559 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28560 return result
561
Stephen Martinis2a0667022018-09-25 22:31:14562 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01563 substitutions = {
564 # Any machine in waterfalls.pyl which desires to run GPU tests
565 # must provide the os_type key.
566 'os_type': tester_config['os_type'],
567 'gpu_vendor_id': '0',
568 'gpu_device_id': '0',
569 }
Stephen Martinis2a0667022018-09-25 22:31:14570 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01571 if 'gpu' in dimension_set:
572 # First remove the driver version, then split into vendor and device.
573 gpu = dimension_set['gpu']
574 gpu = gpu.split('-')[0].split(':')
575 substitutions['gpu_vendor_id'] = gpu[0]
576 substitutions['gpu_device_id'] = gpu[1]
577 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
578
579 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
580 test_name, test_config):
581 # These are all just specializations of isolated script tests with
582 # a bunch of boilerplate command line arguments added.
583
584 # The step name must end in 'test' or 'tests' in order for the
585 # results to automatically show up on the flakiness dashboard.
586 # (At least, this was true some time ago.) Continue to use this
587 # naming convention for the time being to minimize changes.
588 step_name = test_config.get('name', test_name)
589 if not (step_name.endswith('test') or step_name.endswith('tests')):
590 step_name = '%s_tests' % step_name
591 result = self.generate_isolated_script_test(
592 waterfall, tester_name, tester_config, step_name, test_config)
593 if not result:
594 return None
595 result['isolate_name'] = 'telemetry_gpu_integration_test'
596 args = result.get('args', [])
597 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14598
599 # These tests upload and download results from cloud storage and therefore
600 # aren't idempotent yet. https://crbug.com/549140.
601 result['swarming']['idempotent'] = False
602
Kenneth Russell8a386d42018-06-02 09:48:01603 args = [
604 test_to_run,
605 '--show-stdout',
606 '--browser=%s' % tester_config['browser_config'],
607 # --passthrough displays more of the logging in Telemetry when
608 # run via typ, in particular some of the warnings about tests
609 # being expected to fail, but passing.
610 '--passthrough',
611 '-v',
612 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
613 ] + args
614 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14615 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01616 return result
617
Kenneth Russelleb60cbd22017-12-05 07:54:28618 def get_test_generator_map(self):
619 return {
620 'cts_tests': CTSGenerator(self),
Kenneth Russell8a386d42018-06-02 09:48:01621 'gpu_telemetry_tests': GPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28622 'gtest_tests': GTestGenerator(self),
623 'instrumentation_tests': InstrumentationTestGenerator(self),
624 'isolated_scripts': IsolatedScriptTestGenerator(self),
625 'junit_tests': JUnitGenerator(self),
626 'scripts': ScriptGenerator(self),
627 }
628
Kenneth Russell8a386d42018-06-02 09:48:01629 def get_test_type_remapper(self):
630 return {
631 # These are a specialization of isolated_scripts with a bunch of
632 # boilerplate command line arguments added to each one.
633 'gpu_telemetry_tests': 'isolated_scripts',
634 }
635
Kenneth Russelleb60cbd22017-12-05 07:54:28636 def check_composition_test_suites(self):
637 # Pre-pass to catch errors reliably.
638 for name, value in self.test_suites.iteritems():
639 if isinstance(value, list):
640 for entry in value:
641 if isinstance(self.test_suites[entry], list):
Nico Weberd18b8962018-05-16 19:39:38642 raise BBGenErr('Composition test suites may not refer to other '
643 'composition test suites (error found while '
Kenneth Russelleb60cbd22017-12-05 07:54:28644 'processing %s)' % name)
645
Stephen Martinis54d64ad2018-09-21 22:16:20646 def flatten_test_suites(self):
647 new_test_suites = {}
648 for name, value in self.test_suites.get('basic_suites', {}).iteritems():
649 new_test_suites[name] = value
650 for name, value in self.test_suites.get('compound_suites', {}).iteritems():
651 if name in new_test_suites:
652 raise BBGenErr('Composition test suite names may not duplicate basic '
653 'test suite names (error found while processsing %s' % (
654 name))
655 new_test_suites[name] = value
656 self.test_suites = new_test_suites
657
Kenneth Russelleb60cbd22017-12-05 07:54:28658 def resolve_composition_test_suites(self):
Stephen Martinis54d64ad2018-09-21 22:16:20659 self.flatten_test_suites()
660
Kenneth Russelleb60cbd22017-12-05 07:54:28661 self.check_composition_test_suites()
662 for name, value in self.test_suites.iteritems():
663 if isinstance(value, list):
664 # Resolve this to a dictionary.
665 full_suite = {}
666 for entry in value:
667 suite = self.test_suites[entry]
668 full_suite.update(suite)
669 self.test_suites[name] = full_suite
670
671 def link_waterfalls_to_test_suites(self):
672 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43673 for tester_name, tester in waterfall['machines'].iteritems():
674 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28675 if not value in self.test_suites:
676 # Hard / impossible to cover this in the unit test.
677 raise self.unknown_test_suite(
678 value, tester_name, waterfall['name']) # pragma: no cover
679 tester['test_suites'][suite] = self.test_suites[value]
680
681 def load_configuration_files(self):
682 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
683 self.test_suites = self.load_pyl_file('test_suites.pyl')
684 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:01685 self.mixins = self.load_pyl_file('mixins.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:28686
687 def resolve_configuration_files(self):
688 self.resolve_composition_test_suites()
689 self.link_waterfalls_to_test_suites()
690
Nico Weberd18b8962018-05-16 19:39:38691 def unknown_bot(self, bot_name, waterfall_name):
692 return BBGenErr(
693 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
694
Kenneth Russelleb60cbd22017-12-05 07:54:28695 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
696 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38697 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28698 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
699
700 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
701 return BBGenErr(
702 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
703 ' on waterfall ' + waterfall_name)
704
Stephen Martinisb72f6d22018-10-04 23:29:01705 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:07706 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:32707
708 Checks in the waterfall, builder, and test objects for mixins.
709 """
710 def valid_mixin(mixin_name):
711 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:01712 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:32713 raise BBGenErr("bad mixin %s" % mixin_name)
714 def must_be_list(mixins, typ, name):
715 """Asserts that given mixins are a list."""
716 if not isinstance(mixins, list):
717 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
718
Stephen Martinisb72f6d22018-10-04 23:29:01719 if 'mixins' in waterfall:
720 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
721 for mixin in waterfall['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32722 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01723 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32724
Stephen Martinisb72f6d22018-10-04 23:29:01725 if 'mixins' in builder:
726 must_be_list(builder['mixins'], 'builder', builder_name)
727 for mixin in builder['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32728 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01729 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32730
Stephen Martinisb72f6d22018-10-04 23:29:01731 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:07732 return test
733
Stephen Martinis2a0667022018-09-25 22:31:14734 test_name = test.get('name')
735 if not test_name:
736 test_name = test.get('test')
737 if not test_name: # pragma: no cover
738 # Not the best name, but we should say something.
739 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:01740 must_be_list(test['mixins'], 'test', test_name)
741 for mixin in test['mixins']:
Stephen Martinis0382bc12018-09-17 22:29:07742 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01743 test = self.apply_mixin(self.mixins[mixin], test)
744 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:07745 return test
Stephen Martinisb6a50492018-09-12 23:59:32746
Stephen Martinisb72f6d22018-10-04 23:29:01747 def apply_mixin(self, mixin, test):
748 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:32749
Stephen Martinis0382bc12018-09-17 22:29:07750 Mixins will not override an existing key. This is to ensure exceptions can
751 override a setting a mixin applies.
752
Stephen Martinisb72f6d22018-10-04 23:29:01753 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:32754 'dimension_sets', which is how normal test suites specify their dimensions,
755 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
756 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:01757
Stephen Martinisb6a50492018-09-12 23:59:32758 """
Stephen Martinisb6a50492018-09-12 23:59:32759 new_test = copy.deepcopy(test)
760 mixin = copy.deepcopy(mixin)
761
Stephen Martinisb72f6d22018-10-04 23:29:01762 if 'swarming' in mixin:
763 swarming_mixin = mixin['swarming']
764 new_test.setdefault('swarming', {})
765 if 'dimensions' in swarming_mixin:
766 new_test['swarming'].setdefault('dimension_sets', [{}])
767 for dimension_set in new_test['swarming']['dimension_sets']:
768 dimension_set.update(swarming_mixin['dimensions'])
769 del swarming_mixin['dimensions']
Stephen Martinisb6a50492018-09-12 23:59:32770
Stephen Martinisb72f6d22018-10-04 23:29:01771 # python dict update doesn't do recursion at all. Just hard code the
772 # nested update we need (mixin['swarming'] shouldn't clobber
773 # test['swarming'], but should update it).
774 new_test['swarming'].update(swarming_mixin)
775 del mixin['swarming']
776
Wezc0e835b702018-10-30 00:38:41777 if '$mixin_append' in mixin:
778 # Values specified under $mixin_append should be appended to existing
779 # lists, rather than replacing them.
780 mixin_append = mixin['$mixin_append']
781 for key in mixin_append:
782 new_test.setdefault(key, [])
783 if not isinstance(mixin_append[key], list):
784 raise BBGenErr(
785 'Key "' + key + '" in $mixin_append must be a list.')
786 if not isinstance(new_test[key], list):
787 raise BBGenErr(
788 'Cannot apply $mixin_append to non-list "' + key + '".')
789 new_test[key].extend(mixin_append[key])
790 if 'args' in mixin_append:
791 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
792 del mixin['$mixin_append']
793
Stephen Martinisb72f6d22018-10-04 23:29:01794 new_test.update(mixin)
Stephen Martinis0382bc12018-09-17 22:29:07795
Stephen Martinisb6a50492018-09-12 23:59:32796 return new_test
797
Kenneth Russelleb60cbd22017-12-05 07:54:28798 def generate_waterfall_json(self, waterfall):
799 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28800 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:01801 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:43802 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28803 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43804 # Copy only well-understood entries in the machine's configuration
805 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28806 if 'additional_compile_targets' in config:
807 tests['additional_compile_targets'] = config[
808 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43809 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28810 if test_type not in generator_map:
811 raise self.unknown_test_suite_type(
812 test_type, name, waterfall['name']) # pragma: no cover
813 test_generator = generator_map[test_type]
Nico Weber79dc5f6852018-07-13 19:38:49814 # Let multiple kinds of generators generate the same kinds
815 # of tests. For example, gpu_telemetry_tests are a
816 # specialization of isolated_scripts.
817 new_tests = test_generator.generate(
818 waterfall, name, config, input_tests)
819 remapped_test_type = test_type_remapper.get(test_type, test_type)
820 tests[remapped_test_type] = test_generator.sort(
821 tests.get(remapped_test_type, []) + new_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28822 all_tests[name] = tests
823 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
824 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
825 return json.dumps(all_tests, indent=2, separators=(',', ': '),
826 sort_keys=True) + '\n'
827
828 def generate_waterfalls(self): # pragma: no cover
829 self.load_configuration_files()
830 self.resolve_configuration_files()
831 filters = self.args.waterfall_filters
832 suffix = '.json'
833 if self.args.new_files:
834 suffix = '.new' + suffix
835 for waterfall in self.waterfalls:
836 should_gen = not filters or waterfall['name'] in filters
837 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:11838 file_path = waterfall['name'] + suffix
839 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:28840 self.generate_waterfall_json(waterfall))
841
Nico Weberd18b8962018-05-16 19:39:38842 def get_valid_bot_names(self):
843 # Extract bot names from infra/config/global/luci-milo.cfg.
844 bot_names = set()
John Budorickc12abd12018-08-14 19:37:43845 infra_config_dir = os.path.abspath(
846 os.path.join(os.path.dirname(__file__),
847 '..', '..', 'infra', 'config', 'global'))
848 milo_configs = [
849 os.path.join(infra_config_dir, 'luci-milo.cfg'),
850 os.path.join(infra_config_dir, 'luci-milo-dev.cfg'),
851 ]
852 for c in milo_configs:
853 for l in self.read_file(c).splitlines():
854 if (not 'name: "buildbucket/luci.chromium.' in l and
855 not 'name: "buildbot/chromium.' in l):
856 continue
857 # l looks like
858 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
859 # Extract win_chromium_dbg_ng part.
860 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:38861 return bot_names
862
Kenneth Russell8a386d42018-06-02 09:48:01863 def get_bots_that_do_not_actually_exist(self):
864 # Some of the bots on the chromium.gpu.fyi waterfall in particular
865 # are defined only to be mirrored into trybots, and don't actually
866 # exist on any of the waterfalls or consoles.
867 return [
868 'Optional Android Release (Nexus 5X)',
869 'Optional Linux Release (Intel HD 630)',
870 'Optional Linux Release (NVIDIA)',
871 'Optional Mac Release (Intel)',
872 'Optional Mac Retina Release (AMD)',
873 'Optional Mac Retina Release (NVIDIA)',
874 'Optional Win10 Release (Intel HD 630)',
875 'Optional Win10 Release (NVIDIA)',
876 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:08877 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:29878 'linux-blink-rel-dummy',
879 'mac10.10-blink-rel-dummy',
880 'mac10.11-blink-rel-dummy',
881 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:20882 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:29883 'mac10.13-blink-rel-dummy',
884 'win7-blink-rel-dummy',
885 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:08886 'Dummy WebKit Mac10.13',
887 'WebKit Linux layout_ng Dummy Builder',
888 'WebKit Linux root_layer_scrolls Dummy Builder',
889 'WebKit Linux slimming_paint_v2 Dummy Builder',
Stephen Martinis769b25112018-08-30 18:52:06890 # chromium, due to https://crbug.com/878915
891 'win-dbg',
892 'win32-dbg',
Kenneth Russell8a386d42018-06-02 09:48:01893 ]
894
Stephen Martinisf83893722018-09-19 00:02:18895 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:20896 self.check_input_files_sorting(verbose)
897
Kenneth Russelleb60cbd22017-12-05 07:54:28898 self.load_configuration_files()
Stephen Martinis54d64ad2018-09-21 22:16:20899 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:28900 self.check_composition_test_suites()
Nico Weberd18b8962018-05-16 19:39:38901
902 # All bots should exist.
903 bot_names = self.get_valid_bot_names()
Kenneth Russell8a386d42018-06-02 09:48:01904 bots_that_dont_exist = self.get_bots_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:38905 for waterfall in self.waterfalls:
906 for bot_name in waterfall['machines']:
Kenneth Russell8a386d42018-06-02 09:48:01907 if bot_name in bots_that_dont_exist:
908 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38909 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:08910 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:38911 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:52912 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:32913 if waterfall['name'] in ['tryserver.webrtc',
914 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:38915 # These waterfalls have their bot configs in a different repo.
916 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:52917 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38918 raise self.unknown_bot(bot_name, waterfall['name'])
919
Kenneth Russelleb60cbd22017-12-05 07:54:28920 # All test suites must be referenced.
921 suites_seen = set()
922 generator_map = self.get_test_generator_map()
923 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43924 for bot_name, tester in waterfall['machines'].iteritems():
925 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28926 if suite_type not in generator_map:
927 raise self.unknown_test_suite_type(suite_type, bot_name,
928 waterfall['name'])
929 if suite not in self.test_suites:
930 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
931 suites_seen.add(suite)
932 # Since we didn't resolve the configuration files, this set
933 # includes both composition test suites and regular ones.
934 resolved_suites = set()
935 for suite_name in suites_seen:
936 suite = self.test_suites[suite_name]
937 if isinstance(suite, list):
938 for sub_suite in suite:
939 resolved_suites.add(sub_suite)
940 resolved_suites.add(suite_name)
941 # At this point, every key in test_suites.pyl should be referenced.
942 missing_suites = set(self.test_suites.keys()) - resolved_suites
943 if missing_suites:
944 raise BBGenErr('The following test suites were unreferenced by bots on '
945 'the waterfalls: ' + str(missing_suites))
946
947 # All test suite exceptions must refer to bots on the waterfall.
948 all_bots = set()
949 missing_bots = set()
950 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43951 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28952 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:28953 # In order to disambiguate between bots with the same name on
954 # different waterfalls, support has been added to various
955 # exceptions for concatenating the waterfall name after the bot
956 # name.
957 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28958 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:38959 removals = (exception.get('remove_from', []) +
960 exception.get('remove_gtest_from', []) +
961 exception.get('modifications', {}).keys())
962 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:28963 if removal not in all_bots:
964 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:41965
966 missing_bots = missing_bots - set(bots_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:28967 if missing_bots:
968 raise BBGenErr('The following nonexistent machines were referenced in '
969 'the test suite exceptions: ' + str(missing_bots))
970
Stephen Martinis0382bc12018-09-17 22:29:07971 # All mixins must be referenced
972 seen_mixins = set()
973 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:01974 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:07975 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:01976 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:07977 for suite in self.test_suites.values():
978 if isinstance(suite, list):
979 # Don't care about this, it's a composition, which shouldn't include a
980 # swarming mixin.
981 continue
982
983 for test in suite.values():
984 if not isinstance(test, dict):
985 # Some test suites have top level keys, which currently can't be
986 # swarming mixin entries. Ignore them
987 continue
988
Stephen Martinisb72f6d22018-10-04 23:29:01989 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:07990
Stephen Martinisb72f6d22018-10-04 23:29:01991 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:07992 if missing_mixins:
993 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
994 ' referenced in a waterfall, machine, or test suite.' % (
995 str(missing_mixins)))
996
Stephen Martinis54d64ad2018-09-21 22:16:20997
998 def type_assert(self, node, typ, filename, verbose=False):
999 """Asserts that the Python AST node |node| is of type |typ|.
1000
1001 If verbose is set, it prints out some helpful context lines, showing where
1002 exactly the error occurred in the file.
1003 """
1004 if not isinstance(node, typ):
1005 if verbose:
1006 lines = [""] + self.read_file(filename).splitlines()
1007
1008 context = 2
1009 lines_start = max(node.lineno - context, 0)
1010 # Add one to include the last line
1011 lines_end = min(node.lineno + context, len(lines)) + 1
1012 lines = (
1013 ['== %s ==\n' % filename] +
1014 ["<snip>\n"] +
1015 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1016 lines[lines_start:lines_start + context])] +
1017 ['-' * 80 + '\n'] +
1018 ['%d %s' % (node.lineno, lines[node.lineno])] +
1019 ['-' * (node.col_offset + 3) + '^' + '-' * (
1020 80 - node.col_offset - 4) + '\n'] +
1021 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1022 lines[node.lineno + 1:lines_end])] +
1023 ["<snip>\n"]
1024 )
1025 # Print out a useful message when a type assertion fails.
1026 for l in lines:
1027 self.print_line(l.strip())
1028
1029 node_dumped = ast.dump(node, annotate_fields=False)
1030 # If the node is huge, truncate it so everything fits in a terminal
1031 # window.
1032 if len(node_dumped) > 60: # pragma: no cover
1033 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1034 raise BBGenErr(
1035 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1036 ' be %s, is %s' % (
1037 filename, node_dumped,
1038 node.lineno, typ, type(node)))
1039
1040 def ensure_ast_dict_keys_sorted(self, node, filename, verbose):
1041 is_valid = True
1042
1043 keys = []
1044 # The keys of this dict are ordered as ordered in the file; normal python
1045 # dictionary keys are given an arbitrary order, but since we parsed the
1046 # file itself, the order as given in the file is preserved.
1047 for key in node.keys:
1048 self.type_assert(key, ast.Str, filename, verbose)
1049 keys.append(key.s)
1050
1051 keys_sorted = sorted(keys)
1052 if keys_sorted != keys:
1053 is_valid = False
1054 if verbose:
1055 for line in difflib.unified_diff(
1056 keys,
1057 keys_sorted, fromfile='current (%r)' % filename, tofile='sorted'):
1058 self.print_line(line)
1059
1060 if len(set(keys)) != len(keys):
1061 for i in range(len(keys_sorted)-1):
1062 if keys_sorted[i] == keys_sorted[i+1]:
1063 self.print_line('Key %s is duplicated' % keys_sorted[i])
1064 is_valid = False
1065 return is_valid
Stephen Martinisf83893722018-09-19 00:02:181066
1067 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201068 # TODO(https://crbug.com/886993): Add the ability for this script to
1069 # actually format the files, rather than just complain if they're
1070 # incorrectly formatted.
1071 bad_files = set()
1072
1073 for filename in (
Stephen Martinisb72f6d22018-10-04 23:29:011074 'mixins.pyl',
Stephen Martinis54d64ad2018-09-21 22:16:201075 'test_suites.pyl',
1076 'test_suite_exceptions.pyl',
1077 ):
Stephen Martinisf83893722018-09-19 00:02:181078 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1079
Stephen Martinisf83893722018-09-19 00:02:181080 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201081 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181082 module = parsed.body
1083
1084 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201085 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181086 if len(module) != 1: # pragma: no cover
1087 raise BBGenErr('Invalid .pyl file %s' % filename)
1088 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201089 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181090
1091 # Value should be a dictionary.
1092 value = expr.value
Stephen Martinis54d64ad2018-09-21 22:16:201093 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181094
Stephen Martinis54d64ad2018-09-21 22:16:201095 if filename == 'test_suites.pyl':
1096 expected_keys = ['basic_suites', 'compound_suites']
1097 actual_keys = [node.s for node in value.keys]
1098 assert all(key in expected_keys for key in actual_keys), (
1099 'Invalid %r file; expected keys %r, got %r' % (
1100 filename, expected_keys, actual_keys))
1101 suite_dicts = [node for node in value.values]
1102 # Only two keys should mean only 1 or 2 values
1103 assert len(suite_dicts) <= 2
1104 for suite_group in suite_dicts:
1105 if not self.ensure_ast_dict_keys_sorted(
1106 suite_group, filename, verbose):
1107 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181108
Stephen Martinis54d64ad2018-09-21 22:16:201109 else:
1110 if not self.ensure_ast_dict_keys_sorted(
1111 value, filename, verbose):
1112 bad_files.add(filename)
1113
1114 # waterfalls.pyl is slightly different, just do it manually here
1115 filename = 'waterfalls.pyl'
1116 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1117
1118 # Must be a module.
1119 self.type_assert(parsed, ast.Module, filename, verbose)
1120 module = parsed.body
1121
1122 # Only one expression in the module.
1123 self.type_assert(module, list, filename, verbose)
1124 if len(module) != 1: # pragma: no cover
1125 raise BBGenErr('Invalid .pyl file %s' % filename)
1126 expr = module[0]
1127 self.type_assert(expr, ast.Expr, filename, verbose)
1128
1129 # Value should be a list.
1130 value = expr.value
1131 self.type_assert(value, ast.List, filename, verbose)
1132
1133 keys = []
1134 for val in value.elts:
1135 self.type_assert(val, ast.Dict, filename, verbose)
1136 waterfall_name = None
1137 for key, val in zip(val.keys, val.values):
1138 self.type_assert(key, ast.Str, filename, verbose)
1139 if key.s == 'machines':
1140 if not self.ensure_ast_dict_keys_sorted(val, filename, verbose):
1141 bad_files.add(filename)
1142
1143 if key.s == "name":
1144 self.type_assert(val, ast.Str, filename, verbose)
1145 waterfall_name = val.s
1146 assert waterfall_name
1147 keys.append(waterfall_name)
1148
1149 if sorted(keys) != keys:
1150 bad_files.add(filename)
1151 if verbose: # pragma: no cover
1152 for line in difflib.unified_diff(
1153 keys,
1154 sorted(keys), fromfile='current', tofile='sorted'):
1155 self.print_line(line)
Stephen Martinisf83893722018-09-19 00:02:181156
1157 if bad_files:
1158 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201159 'The following files have invalid keys: %s\n. They are either '
1160 'unsorted, or have duplicates.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181161
Kenneth Russelleb60cbd22017-12-05 07:54:281162 def check_output_file_consistency(self, verbose=False):
1163 self.load_configuration_files()
1164 # All waterfalls must have been written by this script already.
1165 self.resolve_configuration_files()
1166 ungenerated_waterfalls = set()
1167 for waterfall in self.waterfalls:
1168 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111169 file_path = waterfall['name'] + '.json'
1170 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281171 if expected != current:
1172 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321173 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501174 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281175 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321176 'contents:')
1177 for line in difflib.unified_diff(
1178 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501179 current.splitlines(),
1180 fromfile='expected', tofile='current'):
1181 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281182 if ungenerated_waterfalls:
1183 raise BBGenErr('The following waterfalls have not been properly '
1184 'autogenerated by generate_buildbot_json.py: ' +
1185 str(ungenerated_waterfalls))
1186
1187 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501188 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281189 self.check_output_file_consistency(verbose) # pragma: no cover
1190
1191 def parse_args(self, argv): # pragma: no cover
1192 parser = argparse.ArgumentParser()
1193 parser.add_argument(
1194 '-c', '--check', action='store_true', help=
1195 'Do consistency checks of configuration and generated files and then '
1196 'exit. Used during presubmit. Causes the tool to not generate any files.')
1197 parser.add_argument(
1198 '-n', '--new-files', action='store_true', help=
1199 'Write output files as .new.json. Useful during development so old and '
1200 'new files can be looked at side-by-side.')
1201 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501202 '-v', '--verbose', action='store_true', help=
1203 'Increases verbosity. Affects consistency checks.')
1204 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281205 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1206 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111207 parser.add_argument(
1208 '--pyl-files-dir', type=os.path.realpath,
1209 help='Path to the directory containing the input .pyl files.')
Kenneth Russelleb60cbd22017-12-05 07:54:281210 self.args = parser.parse_args(argv)
1211
1212 def main(self, argv): # pragma: no cover
1213 self.parse_args(argv)
1214 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:501215 self.check_consistency(verbose=self.args.verbose)
Kenneth Russelleb60cbd22017-12-05 07:54:281216 else:
1217 self.generate_waterfalls()
1218 return 0
1219
1220if __name__ == "__main__": # pragma: no cover
1221 generator = BBJSONGenerator()
1222 sys.exit(generator.main(sys.argv[1:]))