blob: d6c0f3029e2ab457f53ad51e6b799e26338d4ff3 [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 Russell44910c32018-12-03 23:35:11603 # The GPU tests act much like integration tests for the entire browser, and
604 # tend to uncover flakiness bugs more readily than other test suites. In
605 # order to surface any flakiness more readily to the developer of the CL
606 # which is introducing it, we disable retries with patch on the commit
607 # queue.
608 result['should_retry_with_patch'] = False
609
Kenneth Russell8a386d42018-06-02 09:48:01610 args = [
611 test_to_run,
612 '--show-stdout',
613 '--browser=%s' % tester_config['browser_config'],
614 # --passthrough displays more of the logging in Telemetry when
615 # run via typ, in particular some of the warnings about tests
616 # being expected to fail, but passing.
617 '--passthrough',
618 '-v',
619 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
620 ] + args
621 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14622 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01623 return result
624
Kenneth Russelleb60cbd22017-12-05 07:54:28625 def get_test_generator_map(self):
626 return {
627 'cts_tests': CTSGenerator(self),
Kenneth Russell8a386d42018-06-02 09:48:01628 'gpu_telemetry_tests': GPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28629 'gtest_tests': GTestGenerator(self),
630 'instrumentation_tests': InstrumentationTestGenerator(self),
631 'isolated_scripts': IsolatedScriptTestGenerator(self),
632 'junit_tests': JUnitGenerator(self),
633 'scripts': ScriptGenerator(self),
634 }
635
Kenneth Russell8a386d42018-06-02 09:48:01636 def get_test_type_remapper(self):
637 return {
638 # These are a specialization of isolated_scripts with a bunch of
639 # boilerplate command line arguments added to each one.
640 'gpu_telemetry_tests': 'isolated_scripts',
641 }
642
Kenneth Russelleb60cbd22017-12-05 07:54:28643 def check_composition_test_suites(self):
644 # Pre-pass to catch errors reliably.
645 for name, value in self.test_suites.iteritems():
646 if isinstance(value, list):
647 for entry in value:
648 if isinstance(self.test_suites[entry], list):
Nico Weberd18b8962018-05-16 19:39:38649 raise BBGenErr('Composition test suites may not refer to other '
650 'composition test suites (error found while '
Kenneth Russelleb60cbd22017-12-05 07:54:28651 'processing %s)' % name)
652
Stephen Martinis54d64ad2018-09-21 22:16:20653 def flatten_test_suites(self):
654 new_test_suites = {}
655 for name, value in self.test_suites.get('basic_suites', {}).iteritems():
656 new_test_suites[name] = value
657 for name, value in self.test_suites.get('compound_suites', {}).iteritems():
658 if name in new_test_suites:
659 raise BBGenErr('Composition test suite names may not duplicate basic '
660 'test suite names (error found while processsing %s' % (
661 name))
662 new_test_suites[name] = value
663 self.test_suites = new_test_suites
664
Kenneth Russelleb60cbd22017-12-05 07:54:28665 def resolve_composition_test_suites(self):
Stephen Martinis54d64ad2018-09-21 22:16:20666 self.flatten_test_suites()
667
Kenneth Russelleb60cbd22017-12-05 07:54:28668 self.check_composition_test_suites()
669 for name, value in self.test_suites.iteritems():
670 if isinstance(value, list):
671 # Resolve this to a dictionary.
672 full_suite = {}
673 for entry in value:
674 suite = self.test_suites[entry]
675 full_suite.update(suite)
676 self.test_suites[name] = full_suite
677
678 def link_waterfalls_to_test_suites(self):
679 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43680 for tester_name, tester in waterfall['machines'].iteritems():
681 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28682 if not value in self.test_suites:
683 # Hard / impossible to cover this in the unit test.
684 raise self.unknown_test_suite(
685 value, tester_name, waterfall['name']) # pragma: no cover
686 tester['test_suites'][suite] = self.test_suites[value]
687
688 def load_configuration_files(self):
689 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
690 self.test_suites = self.load_pyl_file('test_suites.pyl')
691 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:01692 self.mixins = self.load_pyl_file('mixins.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:28693
694 def resolve_configuration_files(self):
695 self.resolve_composition_test_suites()
696 self.link_waterfalls_to_test_suites()
697
Nico Weberd18b8962018-05-16 19:39:38698 def unknown_bot(self, bot_name, waterfall_name):
699 return BBGenErr(
700 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
701
Kenneth Russelleb60cbd22017-12-05 07:54:28702 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
703 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38704 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28705 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
706
707 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
708 return BBGenErr(
709 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
710 ' on waterfall ' + waterfall_name)
711
Stephen Martinisb72f6d22018-10-04 23:29:01712 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:07713 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:32714
715 Checks in the waterfall, builder, and test objects for mixins.
716 """
717 def valid_mixin(mixin_name):
718 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:01719 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:32720 raise BBGenErr("bad mixin %s" % mixin_name)
721 def must_be_list(mixins, typ, name):
722 """Asserts that given mixins are a list."""
723 if not isinstance(mixins, list):
724 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
725
Stephen Martinisb72f6d22018-10-04 23:29:01726 if 'mixins' in waterfall:
727 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
728 for mixin in waterfall['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32729 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01730 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32731
Stephen Martinisb72f6d22018-10-04 23:29:01732 if 'mixins' in builder:
733 must_be_list(builder['mixins'], 'builder', builder_name)
734 for mixin in builder['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32735 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01736 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32737
Stephen Martinisb72f6d22018-10-04 23:29:01738 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:07739 return test
740
Stephen Martinis2a0667022018-09-25 22:31:14741 test_name = test.get('name')
742 if not test_name:
743 test_name = test.get('test')
744 if not test_name: # pragma: no cover
745 # Not the best name, but we should say something.
746 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:01747 must_be_list(test['mixins'], 'test', test_name)
748 for mixin in test['mixins']:
Stephen Martinis0382bc12018-09-17 22:29:07749 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01750 test = self.apply_mixin(self.mixins[mixin], test)
751 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:07752 return test
Stephen Martinisb6a50492018-09-12 23:59:32753
Stephen Martinisb72f6d22018-10-04 23:29:01754 def apply_mixin(self, mixin, test):
755 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:32756
Stephen Martinis0382bc12018-09-17 22:29:07757 Mixins will not override an existing key. This is to ensure exceptions can
758 override a setting a mixin applies.
759
Stephen Martinisb72f6d22018-10-04 23:29:01760 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:32761 'dimension_sets', which is how normal test suites specify their dimensions,
762 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
763 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:01764
Stephen Martinisb6a50492018-09-12 23:59:32765 """
Stephen Martinisb6a50492018-09-12 23:59:32766 new_test = copy.deepcopy(test)
767 mixin = copy.deepcopy(mixin)
768
Stephen Martinisb72f6d22018-10-04 23:29:01769 if 'swarming' in mixin:
770 swarming_mixin = mixin['swarming']
771 new_test.setdefault('swarming', {})
772 if 'dimensions' in swarming_mixin:
773 new_test['swarming'].setdefault('dimension_sets', [{}])
774 for dimension_set in new_test['swarming']['dimension_sets']:
775 dimension_set.update(swarming_mixin['dimensions'])
776 del swarming_mixin['dimensions']
Stephen Martinisb6a50492018-09-12 23:59:32777
Stephen Martinisb72f6d22018-10-04 23:29:01778 # python dict update doesn't do recursion at all. Just hard code the
779 # nested update we need (mixin['swarming'] shouldn't clobber
780 # test['swarming'], but should update it).
781 new_test['swarming'].update(swarming_mixin)
782 del mixin['swarming']
783
Wezc0e835b702018-10-30 00:38:41784 if '$mixin_append' in mixin:
785 # Values specified under $mixin_append should be appended to existing
786 # lists, rather than replacing them.
787 mixin_append = mixin['$mixin_append']
788 for key in mixin_append:
789 new_test.setdefault(key, [])
790 if not isinstance(mixin_append[key], list):
791 raise BBGenErr(
792 'Key "' + key + '" in $mixin_append must be a list.')
793 if not isinstance(new_test[key], list):
794 raise BBGenErr(
795 'Cannot apply $mixin_append to non-list "' + key + '".')
796 new_test[key].extend(mixin_append[key])
797 if 'args' in mixin_append:
798 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
799 del mixin['$mixin_append']
800
Stephen Martinisb72f6d22018-10-04 23:29:01801 new_test.update(mixin)
Stephen Martinis0382bc12018-09-17 22:29:07802
Stephen Martinisb6a50492018-09-12 23:59:32803 return new_test
804
Kenneth Russelleb60cbd22017-12-05 07:54:28805 def generate_waterfall_json(self, waterfall):
806 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28807 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:01808 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:43809 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28810 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43811 # Copy only well-understood entries in the machine's configuration
812 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28813 if 'additional_compile_targets' in config:
814 tests['additional_compile_targets'] = config[
815 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43816 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28817 if test_type not in generator_map:
818 raise self.unknown_test_suite_type(
819 test_type, name, waterfall['name']) # pragma: no cover
820 test_generator = generator_map[test_type]
Nico Weber79dc5f6852018-07-13 19:38:49821 # Let multiple kinds of generators generate the same kinds
822 # of tests. For example, gpu_telemetry_tests are a
823 # specialization of isolated_scripts.
824 new_tests = test_generator.generate(
825 waterfall, name, config, input_tests)
826 remapped_test_type = test_type_remapper.get(test_type, test_type)
827 tests[remapped_test_type] = test_generator.sort(
828 tests.get(remapped_test_type, []) + new_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28829 all_tests[name] = tests
830 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
831 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
832 return json.dumps(all_tests, indent=2, separators=(',', ': '),
833 sort_keys=True) + '\n'
834
835 def generate_waterfalls(self): # pragma: no cover
836 self.load_configuration_files()
837 self.resolve_configuration_files()
838 filters = self.args.waterfall_filters
839 suffix = '.json'
840 if self.args.new_files:
841 suffix = '.new' + suffix
842 for waterfall in self.waterfalls:
843 should_gen = not filters or waterfall['name'] in filters
844 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:11845 file_path = waterfall['name'] + suffix
846 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:28847 self.generate_waterfall_json(waterfall))
848
Nico Weberd18b8962018-05-16 19:39:38849 def get_valid_bot_names(self):
850 # Extract bot names from infra/config/global/luci-milo.cfg.
851 bot_names = set()
John Budorickc12abd12018-08-14 19:37:43852 infra_config_dir = os.path.abspath(
853 os.path.join(os.path.dirname(__file__),
854 '..', '..', 'infra', 'config', 'global'))
855 milo_configs = [
856 os.path.join(infra_config_dir, 'luci-milo.cfg'),
857 os.path.join(infra_config_dir, 'luci-milo-dev.cfg'),
858 ]
859 for c in milo_configs:
860 for l in self.read_file(c).splitlines():
861 if (not 'name: "buildbucket/luci.chromium.' in l and
862 not 'name: "buildbot/chromium.' in l):
863 continue
864 # l looks like
865 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
866 # Extract win_chromium_dbg_ng part.
867 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:38868 return bot_names
869
Kenneth Russell8a386d42018-06-02 09:48:01870 def get_bots_that_do_not_actually_exist(self):
871 # Some of the bots on the chromium.gpu.fyi waterfall in particular
872 # are defined only to be mirrored into trybots, and don't actually
873 # exist on any of the waterfalls or consoles.
874 return [
Jamie Madilldc7feeb82018-11-14 04:54:56875 'ANGLE GPU Win10 Release (Intel HD 630)',
876 'ANGLE GPU Win10 Release (NVIDIA)',
Corentin Wallez7d3f4fa22018-11-19 23:35:44877 'Dawn GPU Linux Release (Intel HD 630)',
878 'Dawn GPU Linux Release (NVIDIA)',
879 'Dawn GPU Mac Release (Intel)',
880 'Dawn GPU Mac Retina Release (AMD)',
881 'Dawn GPU Mac Retina Release (NVIDIA)',
Jamie Madilldc7feeb82018-11-14 04:54:56882 'Dawn GPU Win10 Release (Intel HD 630)',
883 'Dawn GPU Win10 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:01884 'Optional Android Release (Nexus 5X)',
885 'Optional Linux Release (Intel HD 630)',
886 'Optional Linux Release (NVIDIA)',
887 'Optional Mac Release (Intel)',
888 'Optional Mac Retina Release (AMD)',
889 'Optional Mac Retina Release (NVIDIA)',
890 'Optional Win10 Release (Intel HD 630)',
891 'Optional Win10 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:01892 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:08893 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:29894 'linux-blink-rel-dummy',
895 'mac10.10-blink-rel-dummy',
896 'mac10.11-blink-rel-dummy',
897 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:20898 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:29899 'mac10.13-blink-rel-dummy',
900 'win7-blink-rel-dummy',
901 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:08902 'Dummy WebKit Mac10.13',
903 'WebKit Linux layout_ng Dummy Builder',
904 'WebKit Linux root_layer_scrolls Dummy Builder',
905 'WebKit Linux slimming_paint_v2 Dummy Builder',
Stephen Martinis769b25112018-08-30 18:52:06906 # chromium, due to https://crbug.com/878915
907 'win-dbg',
908 'win32-dbg',
Kenneth Russell8a386d42018-06-02 09:48:01909 ]
910
Stephen Martinisf83893722018-09-19 00:02:18911 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:20912 self.check_input_files_sorting(verbose)
913
Kenneth Russelleb60cbd22017-12-05 07:54:28914 self.load_configuration_files()
Stephen Martinis54d64ad2018-09-21 22:16:20915 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:28916 self.check_composition_test_suites()
Nico Weberd18b8962018-05-16 19:39:38917
918 # All bots should exist.
919 bot_names = self.get_valid_bot_names()
Kenneth Russell8a386d42018-06-02 09:48:01920 bots_that_dont_exist = self.get_bots_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:38921 for waterfall in self.waterfalls:
922 for bot_name in waterfall['machines']:
Kenneth Russell8a386d42018-06-02 09:48:01923 if bot_name in bots_that_dont_exist:
924 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38925 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:08926 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:38927 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:52928 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:32929 if waterfall['name'] in ['tryserver.webrtc',
930 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:38931 # These waterfalls have their bot configs in a different repo.
932 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:52933 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38934 raise self.unknown_bot(bot_name, waterfall['name'])
935
Kenneth Russelleb60cbd22017-12-05 07:54:28936 # All test suites must be referenced.
937 suites_seen = set()
938 generator_map = self.get_test_generator_map()
939 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43940 for bot_name, tester in waterfall['machines'].iteritems():
941 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28942 if suite_type not in generator_map:
943 raise self.unknown_test_suite_type(suite_type, bot_name,
944 waterfall['name'])
945 if suite not in self.test_suites:
946 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
947 suites_seen.add(suite)
948 # Since we didn't resolve the configuration files, this set
949 # includes both composition test suites and regular ones.
950 resolved_suites = set()
951 for suite_name in suites_seen:
952 suite = self.test_suites[suite_name]
953 if isinstance(suite, list):
954 for sub_suite in suite:
955 resolved_suites.add(sub_suite)
956 resolved_suites.add(suite_name)
957 # At this point, every key in test_suites.pyl should be referenced.
958 missing_suites = set(self.test_suites.keys()) - resolved_suites
959 if missing_suites:
960 raise BBGenErr('The following test suites were unreferenced by bots on '
961 'the waterfalls: ' + str(missing_suites))
962
963 # All test suite exceptions must refer to bots on the waterfall.
964 all_bots = set()
965 missing_bots = set()
966 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43967 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28968 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:28969 # In order to disambiguate between bots with the same name on
970 # different waterfalls, support has been added to various
971 # exceptions for concatenating the waterfall name after the bot
972 # name.
973 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28974 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:38975 removals = (exception.get('remove_from', []) +
976 exception.get('remove_gtest_from', []) +
977 exception.get('modifications', {}).keys())
978 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:28979 if removal not in all_bots:
980 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:41981
982 missing_bots = missing_bots - set(bots_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:28983 if missing_bots:
984 raise BBGenErr('The following nonexistent machines were referenced in '
985 'the test suite exceptions: ' + str(missing_bots))
986
Stephen Martinis0382bc12018-09-17 22:29:07987 # All mixins must be referenced
988 seen_mixins = set()
989 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:01990 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:07991 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:01992 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:07993 for suite in self.test_suites.values():
994 if isinstance(suite, list):
995 # Don't care about this, it's a composition, which shouldn't include a
996 # swarming mixin.
997 continue
998
999 for test in suite.values():
1000 if not isinstance(test, dict):
1001 # Some test suites have top level keys, which currently can't be
1002 # swarming mixin entries. Ignore them
1003 continue
1004
Stephen Martinisb72f6d22018-10-04 23:29:011005 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071006
Stephen Martinisb72f6d22018-10-04 23:29:011007 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071008 if missing_mixins:
1009 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1010 ' referenced in a waterfall, machine, or test suite.' % (
1011 str(missing_mixins)))
1012
Stephen Martinis54d64ad2018-09-21 22:16:201013
1014 def type_assert(self, node, typ, filename, verbose=False):
1015 """Asserts that the Python AST node |node| is of type |typ|.
1016
1017 If verbose is set, it prints out some helpful context lines, showing where
1018 exactly the error occurred in the file.
1019 """
1020 if not isinstance(node, typ):
1021 if verbose:
1022 lines = [""] + self.read_file(filename).splitlines()
1023
1024 context = 2
1025 lines_start = max(node.lineno - context, 0)
1026 # Add one to include the last line
1027 lines_end = min(node.lineno + context, len(lines)) + 1
1028 lines = (
1029 ['== %s ==\n' % filename] +
1030 ["<snip>\n"] +
1031 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1032 lines[lines_start:lines_start + context])] +
1033 ['-' * 80 + '\n'] +
1034 ['%d %s' % (node.lineno, lines[node.lineno])] +
1035 ['-' * (node.col_offset + 3) + '^' + '-' * (
1036 80 - node.col_offset - 4) + '\n'] +
1037 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1038 lines[node.lineno + 1:lines_end])] +
1039 ["<snip>\n"]
1040 )
1041 # Print out a useful message when a type assertion fails.
1042 for l in lines:
1043 self.print_line(l.strip())
1044
1045 node_dumped = ast.dump(node, annotate_fields=False)
1046 # If the node is huge, truncate it so everything fits in a terminal
1047 # window.
1048 if len(node_dumped) > 60: # pragma: no cover
1049 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1050 raise BBGenErr(
1051 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1052 ' be %s, is %s' % (
1053 filename, node_dumped,
1054 node.lineno, typ, type(node)))
1055
1056 def ensure_ast_dict_keys_sorted(self, node, filename, verbose):
1057 is_valid = True
1058
1059 keys = []
1060 # The keys of this dict are ordered as ordered in the file; normal python
1061 # dictionary keys are given an arbitrary order, but since we parsed the
1062 # file itself, the order as given in the file is preserved.
1063 for key in node.keys:
1064 self.type_assert(key, ast.Str, filename, verbose)
1065 keys.append(key.s)
1066
1067 keys_sorted = sorted(keys)
1068 if keys_sorted != keys:
1069 is_valid = False
1070 if verbose:
1071 for line in difflib.unified_diff(
1072 keys,
1073 keys_sorted, fromfile='current (%r)' % filename, tofile='sorted'):
1074 self.print_line(line)
1075
1076 if len(set(keys)) != len(keys):
1077 for i in range(len(keys_sorted)-1):
1078 if keys_sorted[i] == keys_sorted[i+1]:
1079 self.print_line('Key %s is duplicated' % keys_sorted[i])
1080 is_valid = False
1081 return is_valid
Stephen Martinisf83893722018-09-19 00:02:181082
1083 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201084 # TODO(https://crbug.com/886993): Add the ability for this script to
1085 # actually format the files, rather than just complain if they're
1086 # incorrectly formatted.
1087 bad_files = set()
1088
1089 for filename in (
Stephen Martinisb72f6d22018-10-04 23:29:011090 'mixins.pyl',
Stephen Martinis54d64ad2018-09-21 22:16:201091 'test_suites.pyl',
1092 'test_suite_exceptions.pyl',
1093 ):
Stephen Martinisf83893722018-09-19 00:02:181094 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1095
Stephen Martinisf83893722018-09-19 00:02:181096 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201097 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181098 module = parsed.body
1099
1100 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201101 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181102 if len(module) != 1: # pragma: no cover
1103 raise BBGenErr('Invalid .pyl file %s' % filename)
1104 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201105 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181106
1107 # Value should be a dictionary.
1108 value = expr.value
Stephen Martinis54d64ad2018-09-21 22:16:201109 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181110
Stephen Martinis54d64ad2018-09-21 22:16:201111 if filename == 'test_suites.pyl':
1112 expected_keys = ['basic_suites', 'compound_suites']
1113 actual_keys = [node.s for node in value.keys]
1114 assert all(key in expected_keys for key in actual_keys), (
1115 'Invalid %r file; expected keys %r, got %r' % (
1116 filename, expected_keys, actual_keys))
1117 suite_dicts = [node for node in value.values]
1118 # Only two keys should mean only 1 or 2 values
1119 assert len(suite_dicts) <= 2
1120 for suite_group in suite_dicts:
1121 if not self.ensure_ast_dict_keys_sorted(
1122 suite_group, filename, verbose):
1123 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181124
Stephen Martinis54d64ad2018-09-21 22:16:201125 else:
1126 if not self.ensure_ast_dict_keys_sorted(
1127 value, filename, verbose):
1128 bad_files.add(filename)
1129
1130 # waterfalls.pyl is slightly different, just do it manually here
1131 filename = 'waterfalls.pyl'
1132 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1133
1134 # Must be a module.
1135 self.type_assert(parsed, ast.Module, filename, verbose)
1136 module = parsed.body
1137
1138 # Only one expression in the module.
1139 self.type_assert(module, list, filename, verbose)
1140 if len(module) != 1: # pragma: no cover
1141 raise BBGenErr('Invalid .pyl file %s' % filename)
1142 expr = module[0]
1143 self.type_assert(expr, ast.Expr, filename, verbose)
1144
1145 # Value should be a list.
1146 value = expr.value
1147 self.type_assert(value, ast.List, filename, verbose)
1148
1149 keys = []
1150 for val in value.elts:
1151 self.type_assert(val, ast.Dict, filename, verbose)
1152 waterfall_name = None
1153 for key, val in zip(val.keys, val.values):
1154 self.type_assert(key, ast.Str, filename, verbose)
1155 if key.s == 'machines':
1156 if not self.ensure_ast_dict_keys_sorted(val, filename, verbose):
1157 bad_files.add(filename)
1158
1159 if key.s == "name":
1160 self.type_assert(val, ast.Str, filename, verbose)
1161 waterfall_name = val.s
1162 assert waterfall_name
1163 keys.append(waterfall_name)
1164
1165 if sorted(keys) != keys:
1166 bad_files.add(filename)
1167 if verbose: # pragma: no cover
1168 for line in difflib.unified_diff(
1169 keys,
1170 sorted(keys), fromfile='current', tofile='sorted'):
1171 self.print_line(line)
Stephen Martinisf83893722018-09-19 00:02:181172
1173 if bad_files:
1174 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201175 'The following files have invalid keys: %s\n. They are either '
1176 'unsorted, or have duplicates.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181177
Kenneth Russelleb60cbd22017-12-05 07:54:281178 def check_output_file_consistency(self, verbose=False):
1179 self.load_configuration_files()
1180 # All waterfalls must have been written by this script already.
1181 self.resolve_configuration_files()
1182 ungenerated_waterfalls = set()
1183 for waterfall in self.waterfalls:
1184 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111185 file_path = waterfall['name'] + '.json'
1186 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281187 if expected != current:
1188 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321189 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501190 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281191 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321192 'contents:')
1193 for line in difflib.unified_diff(
1194 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501195 current.splitlines(),
1196 fromfile='expected', tofile='current'):
1197 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281198 if ungenerated_waterfalls:
1199 raise BBGenErr('The following waterfalls have not been properly '
1200 'autogenerated by generate_buildbot_json.py: ' +
1201 str(ungenerated_waterfalls))
1202
1203 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501204 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281205 self.check_output_file_consistency(verbose) # pragma: no cover
1206
1207 def parse_args(self, argv): # pragma: no cover
1208 parser = argparse.ArgumentParser()
1209 parser.add_argument(
1210 '-c', '--check', action='store_true', help=
1211 'Do consistency checks of configuration and generated files and then '
1212 'exit. Used during presubmit. Causes the tool to not generate any files.')
1213 parser.add_argument(
1214 '-n', '--new-files', action='store_true', help=
1215 'Write output files as .new.json. Useful during development so old and '
1216 'new files can be looked at side-by-side.')
1217 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501218 '-v', '--verbose', action='store_true', help=
1219 'Increases verbosity. Affects consistency checks.')
1220 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281221 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1222 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111223 parser.add_argument(
1224 '--pyl-files-dir', type=os.path.realpath,
1225 help='Path to the directory containing the input .pyl files.')
Kenneth Russelleb60cbd22017-12-05 07:54:281226 self.args = parser.parse_args(argv)
1227
1228 def main(self, argv): # pragma: no cover
1229 self.parse_args(argv)
1230 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:501231 self.check_consistency(verbose=self.args.verbose)
Kenneth Russelleb60cbd22017-12-05 07:54:281232 else:
1233 self.generate_waterfalls()
1234 return 0
1235
1236if __name__ == "__main__": # pragma: no cover
1237 generator = BBJSONGenerator()
1238 sys.exit(generator.main(sys.argv[1:]))