blob: 2522040ec59ccff2c8b20fcee1b3d4216517b6c4 [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):
John Budorick826d5ed2017-12-28 19:27:3226
27 def __init__(self, message, cause=None):
28 super(BBGenErr, self).__init__(BBGenErr._create_message(message, cause))
29
30 @staticmethod
31 def _create_message(message, cause):
32 msg = message
33 if cause:
34 msg += '\n\nCaused by:\n'
35 msg += '\n'.join(' %s' % l for l in traceback.format_exc().splitlines())
36 return msg
Kenneth Russelleb60cbd22017-12-05 07:54:2837
38
Kenneth Russell8ceeabf2017-12-11 17:53:2839# This class is only present to accommodate certain machines on
40# chromium.android.fyi which run certain tests as instrumentation
41# tests, but not as gtests. If this discrepancy were fixed then the
42# notion could be removed.
43class TestSuiteTypes(object):
44 GTEST = 'gtest'
45
46
Kenneth Russelleb60cbd22017-12-05 07:54:2847class BaseGenerator(object):
48 def __init__(self, bb_gen):
49 self.bb_gen = bb_gen
50
Kenneth Russell8ceeabf2017-12-11 17:53:2851 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2852 raise NotImplementedError()
53
54 def sort(self, tests):
55 raise NotImplementedError()
56
57
Kenneth Russell8ceeabf2017-12-11 17:53:2858def cmp_tests(a, b):
59 # Prefer to compare based on the "test" key.
60 val = cmp(a['test'], b['test'])
61 if val != 0:
62 return val
63 if 'name' in a and 'name' in b:
64 return cmp(a['name'], b['name']) # pragma: no cover
65 if 'name' not in a and 'name' not in b:
66 return 0 # pragma: no cover
67 # Prefer to put variants of the same test after the first one.
68 if 'name' in a:
69 return 1
70 # 'name' is in b.
71 return -1 # pragma: no cover
72
73
Kenneth Russelleb60cbd22017-12-05 07:54:2874class GTestGenerator(BaseGenerator):
75 def __init__(self, bb_gen):
76 super(GTestGenerator, self).__init__(bb_gen)
77
Kenneth Russell8ceeabf2017-12-11 17:53:2878 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2879 # The relative ordering of some of the tests is important to
80 # minimize differences compared to the handwritten JSON files, since
81 # Python's sorts are stable and there are some tests with the same
82 # key (see gles2_conform_d3d9_test and similar variants). Avoid
83 # losing the order by avoiding coalescing the dictionaries into one.
84 gtests = []
85 for test_name, test_config in sorted(input_tests.iteritems()):
John Budorick826d5ed2017-12-28 19:27:3286 try:
87 test = self.bb_gen.generate_gtest(
88 waterfall, tester_name, tester_config, test_name, test_config)
89 if test:
90 # generate_gtest may veto the test generation on this tester.
91 gtests.append(test)
92 except Exception as e:
93 raise BBGenErr('Failed to generate %s' % test_name, cause=e)
Kenneth Russelleb60cbd22017-12-05 07:54:2894 return gtests
95
96 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:2897 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:2898
99
100class IsolatedScriptTestGenerator(BaseGenerator):
101 def __init__(self, bb_gen):
102 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
103
Kenneth Russell8ceeabf2017-12-11 17:53:28104 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28105 isolated_scripts = []
106 for test_name, test_config in sorted(input_tests.iteritems()):
107 test = self.bb_gen.generate_isolated_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28108 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28109 if test:
110 isolated_scripts.append(test)
111 return isolated_scripts
112
113 def sort(self, tests):
114 return sorted(tests, key=lambda x: x['name'])
115
116
117class ScriptGenerator(BaseGenerator):
118 def __init__(self, bb_gen):
119 super(ScriptGenerator, self).__init__(bb_gen)
120
Kenneth Russell8ceeabf2017-12-11 17:53:28121 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28122 scripts = []
123 for test_name, test_config in sorted(input_tests.iteritems()):
124 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28125 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28126 if test:
127 scripts.append(test)
128 return scripts
129
130 def sort(self, tests):
131 return sorted(tests, key=lambda x: x['name'])
132
133
134class JUnitGenerator(BaseGenerator):
135 def __init__(self, bb_gen):
136 super(JUnitGenerator, self).__init__(bb_gen)
137
Kenneth Russell8ceeabf2017-12-11 17:53:28138 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28139 scripts = []
140 for test_name, test_config in sorted(input_tests.iteritems()):
141 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28142 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28143 if test:
144 scripts.append(test)
145 return scripts
146
147 def sort(self, tests):
148 return sorted(tests, key=lambda x: x['test'])
149
150
151class CTSGenerator(BaseGenerator):
152 def __init__(self, bb_gen):
153 super(CTSGenerator, self).__init__(bb_gen)
154
Kenneth Russell8ceeabf2017-12-11 17:53:28155 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28156 # These only contain one entry and it's the contents of the input tests'
157 # dictionary, verbatim.
158 cts_tests = []
159 cts_tests.append(input_tests)
160 return cts_tests
161
162 def sort(self, tests):
163 return tests
164
165
166class InstrumentationTestGenerator(BaseGenerator):
167 def __init__(self, bb_gen):
168 super(InstrumentationTestGenerator, self).__init__(bb_gen)
169
Kenneth Russell8ceeabf2017-12-11 17:53:28170 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28171 scripts = []
172 for test_name, test_config in sorted(input_tests.iteritems()):
173 test = self.bb_gen.generate_instrumentation_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28174 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28175 if test:
176 scripts.append(test)
177 return scripts
178
179 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28180 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28181
182
183class BBJSONGenerator(object):
184 def __init__(self):
185 self.this_dir = THIS_DIR
186 self.args = None
187 self.waterfalls = None
188 self.test_suites = None
189 self.exceptions = None
190
191 def generate_abs_file_path(self, relative_path):
192 return os.path.join(self.this_dir, relative_path) # pragma: no cover
193
194 def read_file(self, relative_path):
195 with open(self.generate_abs_file_path(
196 relative_path)) as fp: # pragma: no cover
197 return fp.read() # pragma: no cover
198
199 def write_file(self, relative_path, contents):
200 with open(self.generate_abs_file_path(
201 relative_path), 'wb') as fp: # pragma: no cover
202 fp.write(contents) # pragma: no cover
203
204 def load_pyl_file(self, filename):
205 try:
206 return ast.literal_eval(self.read_file(filename))
207 except (SyntaxError, ValueError) as e: # pragma: no cover
208 raise BBGenErr('Failed to parse pyl file "%s": %s' %
209 (filename, e)) # pragma: no cover
210
211 def is_android(self, tester_config):
212 return tester_config.get('os_type') == 'android'
213
214 def get_exception_for_test(self, test_name, test_config):
215 # gtests may have both "test" and "name" fields, and usually, if the "name"
216 # field is specified, it means that the same test is being repurposed
217 # multiple times with different command line arguments. To handle this case,
218 # prefer to lookup per the "name" field of the test itself, as opposed to
219 # the "test_name", which is actually the "test" field.
220 if 'name' in test_config:
221 return self.exceptions.get(test_config['name'])
222 else:
223 return self.exceptions.get(test_name)
224
225 def should_run_on_tester(self, waterfall, tester_name, tester_config,
Kenneth Russell8ceeabf2017-12-11 17:53:28226 test_name, test_config, test_suite_type=None):
227 # TODO(kbr): until this script is merged with the GPU test generator, some
Kenneth Russelleb60cbd22017-12-05 07:54:28228 # arguments will be unused.
Kenneth Russelleb60cbd22017-12-05 07:54:28229 del tester_config
230 # Currently, the only reason a test should not run on a given tester is that
231 # it's in the exceptions. (Once the GPU waterfall generation script is
232 # incorporated here, the rules will become more complex.)
233 exception = self.get_exception_for_test(test_name, test_config)
234 if not exception:
235 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28236 remove_from = None
237 if test_suite_type:
238 # First look for a specific removal for the test suite type,
239 # e.g. 'remove_gtest_from'.
240 remove_from = exception.get('remove_' + test_suite_type + '_from')
241 if remove_from and tester_name in remove_from:
242 # TODO(kbr): add coverage.
243 return False # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28244 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28245 if remove_from:
246 if tester_name in remove_from:
247 return False
248 # TODO(kbr): this code path was added for some tests (including
249 # android_webview_unittests) on one machine (Nougat Phone
250 # Tester) which exists with the same name on two waterfalls,
251 # chromium.android and chromium.fyi; the tests are run on one
252 # but not the other. Once the bots are all uniquely named (a
253 # different ongoing project) this code should be removed.
254 # TODO(kbr): add coverage.
255 return (tester_name + ' ' + waterfall['name']
256 not in remove_from) # pragma: no cover
257 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28258
Kenneth Russell8ceeabf2017-12-11 17:53:28259 def get_test_modifications(self, test, test_name, tester_name, waterfall):
Kenneth Russelleb60cbd22017-12-05 07:54:28260 exception = self.get_exception_for_test(test_name, test)
261 if not exception:
262 return None
Kenneth Russell8ceeabf2017-12-11 17:53:28263 mods = exception.get('modifications', {}).get(tester_name)
264 if mods:
265 return mods
266 # TODO(kbr): this code path was added for exactly one test
267 # (cronet_test_instrumentation_apk) on a few bots on
268 # chromium.android.fyi. Once the bots are all uniquely named (a
269 # different ongoing project) this code should be removed.
270 return exception.get('modifications', {}).get(tester_name + ' ' +
271 waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28272
273 def get_test_key_removals(self, test_name, tester_name):
274 exception = self.exceptions.get(test_name)
275 if not exception:
276 return []
277 return exception.get('key_removals', {}).get(tester_name, [])
278
279 def dictionary_merge(self, a, b, path=None, update=True):
280 """http://stackoverflow.com/questions/7204805/
281 python-dictionaries-of-dictionaries-merge
282 merges b into a
283 """
284 if path is None:
285 path = []
286 for key in b:
287 if key in a:
288 if isinstance(a[key], dict) and isinstance(b[key], dict):
289 self.dictionary_merge(a[key], b[key], path + [str(key)])
290 elif a[key] == b[key]:
291 pass # same leaf value
292 elif isinstance(a[key], list) and isinstance(b[key], list):
Kenneth Russell8ceeabf2017-12-11 17:53:28293 if all(isinstance(x, str)
294 for x in itertools.chain(a[key], b[key])):
John Budorick5fae58f2018-01-17 17:33:43295 a[key] = sorted(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28296 else:
297 # TODO(kbr): this only works properly if the two arrays are
298 # the same length, which is currently always the case in the
299 # swarming dimension_sets that we have to merge. It will fail
300 # to merge / override 'args' arrays which are different
301 # length.
302 for idx in xrange(len(b[key])):
303 try:
304 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
305 path + [str(key), str(idx)],
306 update=update)
307 except (IndexError, TypeError): # pragma: no cover
308 raise BBGenErr('Error merging list keys ' + str(key) +
309 ' and indices ' + str(idx) + ' between ' +
310 str(a) + ' and ' + str(b)) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28311 elif update: # pragma: no cover
312 a[key] = b[key] # pragma: no cover
313 else:
314 raise BBGenErr('Conflict at %s' % '.'.join(
315 path + [str(key)])) # pragma: no cover
316 else:
317 a[key] = b[key]
318 return a
319
John Budorickedfe7f872018-01-23 15:27:22320 def initialize_args_for_test(self, generated_test, tester_config):
321 if 'args' in tester_config:
322 if 'args' not in generated_test:
323 generated_test['args'] = []
324 generated_test['args'].extend(tester_config['args'])
325
Kenneth Russelleb60cbd22017-12-05 07:54:28326 def initialize_swarming_dictionary_for_test(self, generated_test,
327 tester_config):
328 if 'swarming' not in generated_test:
329 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28330 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
331 generated_test['swarming'].update({
332 'can_use_on_swarming_builders': tester_config.get('use_swarming', True)
333 })
Kenneth Russelleb60cbd22017-12-05 07:54:28334 if 'swarming' in tester_config:
335 if 'dimension_sets' not in generated_test['swarming']:
336 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
337 tester_config['swarming']['dimension_sets'])
338 self.dictionary_merge(generated_test['swarming'],
339 tester_config['swarming'])
340 # Apply any Android-specific Swarming dimensions after the generic ones.
341 if 'android_swarming' in generated_test:
342 if self.is_android(tester_config): # pragma: no cover
343 self.dictionary_merge(
344 generated_test['swarming'],
345 generated_test['android_swarming']) # pragma: no cover
346 del generated_test['android_swarming'] # pragma: no cover
347
348 def clean_swarming_dictionary(self, swarming_dict):
349 # Clean out redundant entries from a test's "swarming" dictionary.
350 # This is really only needed to retain 100% parity with the
351 # handwritten JSON files, and can be removed once all the files are
352 # autogenerated.
353 if 'shards' in swarming_dict:
354 if swarming_dict['shards'] == 1: # pragma: no cover
355 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24356 if 'hard_timeout' in swarming_dict:
357 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
358 del swarming_dict['hard_timeout'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28359 if not swarming_dict['can_use_on_swarming_builders']:
360 # Remove all other keys.
361 for k in swarming_dict.keys(): # pragma: no cover
362 if k != 'can_use_on_swarming_builders': # pragma: no cover
363 del swarming_dict[k] # pragma: no cover
364
Kenneth Russell8ceeabf2017-12-11 17:53:28365 def update_and_cleanup_test(self, test, test_name, tester_name, waterfall):
Kenneth Russelleb60cbd22017-12-05 07:54:28366 # See if there are any exceptions that need to be merged into this
367 # test's specification.
Kenneth Russell8ceeabf2017-12-11 17:53:28368 modifications = self.get_test_modifications(test, test_name, tester_name,
369 waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28370 if modifications:
371 test = self.dictionary_merge(test, modifications)
372 for k in self.get_test_key_removals(test_name, tester_name):
373 del test[k]
Dirk Pranke1b767092017-12-07 04:44:23374 if 'swarming' in test:
375 self.clean_swarming_dictionary(test['swarming'])
Kenneth Russelleb60cbd22017-12-05 07:54:28376 return test
377
Shenghua Zhangaba8bad2018-02-07 02:12:09378 def add_common_test_properties(self, test, tester_config):
379 if tester_config.get('use_multi_dimension_trigger_script'):
380 test['trigger_script'] = {
381 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
382 'args': [
383 '--multiple-trigger-configs',
384 json.dumps(tester_config['swarming']['dimension_sets'] +
385 tester_config.get('alternate_swarming_dimensions', [])),
386 '--multiple-dimension-script-verbose',
387 'True'
388 ],
389 }
390
Kenneth Russelleb60cbd22017-12-05 07:54:28391 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
392 test_config):
393 if not self.should_run_on_tester(
Kenneth Russell8ceeabf2017-12-11 17:53:28394 waterfall, tester_name, tester_config, test_name, test_config,
395 TestSuiteTypes.GTEST):
Kenneth Russelleb60cbd22017-12-05 07:54:28396 return None
397 result = copy.deepcopy(test_config)
398 if 'test' in result:
399 result['name'] = test_name
400 else:
401 result['test'] = test_name
402 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickedfe7f872018-01-23 15:27:22403 self.initialize_args_for_test(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28404 if self.is_android(tester_config) and tester_config.get('use_swarming',
405 True):
John Budorick826d5ed2017-12-28 19:27:32406 if 'args' not in result:
407 result['args'] = []
408 result['args'].append('--gs-results-bucket=chromium-result-details')
Kenneth Russell8ceeabf2017-12-11 17:53:28409 if result['swarming']['can_use_on_swarming_builders'] and not \
410 tester_config.get('skip_merge_script', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28411 result['merge'] = {
412 'args': [
413 '--bucket',
414 'chromium-result-details',
415 '--test-name',
416 test_name
417 ],
418 'script': '//build/android/pylib/results/presentation/' \
419 'test_results_presentation.py',
420 } # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:28421 if not tester_config.get('skip_cipd_packages', False):
422 result['swarming']['cipd_packages'] = [
423 {
424 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
425 'location': 'bin',
426 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
427 }
428 ]
Kenneth Russelleb60cbd22017-12-05 07:54:28429 if not tester_config.get('skip_output_links', False):
430 result['swarming']['output_links'] = [
431 {
432 'link': [
433 'https://luci-logdog.appspot.com/v/?s',
434 '=android%2Fswarming%2Flogcats%2F',
435 '${TASK_ID}%2F%2B%2Funified_logcats',
436 ],
437 'name': 'shard #${SHARD_INDEX} logcats',
438 },
439 ]
Benjamin Pastene766d48f52017-12-18 21:47:42440 if not tester_config.get('skip_device_recovery', False):
John Budorick826d5ed2017-12-28 19:27:32441 result['args'].append('--recover-devices')
Benjamin Pastene766d48f52017-12-18 21:47:42442
Kenneth Russell8ceeabf2017-12-11 17:53:28443 result = self.update_and_cleanup_test(result, test_name, tester_name,
444 waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09445 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28446 return result
447
448 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
449 test_name, test_config):
450 if not self.should_run_on_tester(waterfall, tester_name, tester_config,
451 test_name, test_config):
452 return None
453 result = copy.deepcopy(test_config)
454 result['isolate_name'] = result.get('isolate_name', test_name)
455 result['name'] = test_name
456 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28457 result = self.update_and_cleanup_test(result, test_name, tester_name,
458 waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09459 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28460 return result
461
462 def generate_script_test(self, waterfall, tester_name, tester_config,
463 test_name, test_config):
464 if not self.should_run_on_tester(waterfall, tester_name, tester_config,
465 test_name, test_config):
466 return None
467 result = {
468 'name': test_name,
469 'script': test_config['script']
470 }
Kenneth Russell8ceeabf2017-12-11 17:53:28471 result = self.update_and_cleanup_test(result, test_name, tester_name,
472 waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28473 return result
474
475 def generate_junit_test(self, waterfall, tester_name, tester_config,
476 test_name, test_config):
477 if not self.should_run_on_tester(waterfall, tester_name, tester_config,
478 test_name, test_config):
479 return None
480 result = {
481 'test': test_name,
482 }
483 return result
484
485 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
486 test_name, test_config):
487 if not self.should_run_on_tester(waterfall, tester_name, tester_config,
488 test_name, test_config):
489 return None
490 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28491 if 'test' in result and result['test'] != test_name:
492 result['name'] = test_name
493 else:
494 result['test'] = test_name
495 result = self.update_and_cleanup_test(result, test_name, tester_name,
496 waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28497 return result
498
499 def get_test_generator_map(self):
500 return {
501 'cts_tests': CTSGenerator(self),
502 'gtest_tests': GTestGenerator(self),
503 'instrumentation_tests': InstrumentationTestGenerator(self),
504 'isolated_scripts': IsolatedScriptTestGenerator(self),
505 'junit_tests': JUnitGenerator(self),
506 'scripts': ScriptGenerator(self),
507 }
508
509 def check_composition_test_suites(self):
510 # Pre-pass to catch errors reliably.
511 for name, value in self.test_suites.iteritems():
512 if isinstance(value, list):
513 for entry in value:
514 if isinstance(self.test_suites[entry], list):
515 raise BBGenErr('Composition test suites may not refer to other ' \
516 'composition test suites (error found while ' \
517 'processing %s)' % name)
518
519 def resolve_composition_test_suites(self):
520 self.check_composition_test_suites()
521 for name, value in self.test_suites.iteritems():
522 if isinstance(value, list):
523 # Resolve this to a dictionary.
524 full_suite = {}
525 for entry in value:
526 suite = self.test_suites[entry]
527 full_suite.update(suite)
528 self.test_suites[name] = full_suite
529
530 def link_waterfalls_to_test_suites(self):
531 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43532 for tester_name, tester in waterfall['machines'].iteritems():
533 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28534 if not value in self.test_suites:
535 # Hard / impossible to cover this in the unit test.
536 raise self.unknown_test_suite(
537 value, tester_name, waterfall['name']) # pragma: no cover
538 tester['test_suites'][suite] = self.test_suites[value]
539
540 def load_configuration_files(self):
541 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
542 self.test_suites = self.load_pyl_file('test_suites.pyl')
543 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
544
545 def resolve_configuration_files(self):
546 self.resolve_composition_test_suites()
547 self.link_waterfalls_to_test_suites()
548
John Budorick826d5ed2017-12-28 19:27:32549 def generation_error(self, suite_type, bot_name, waterfall_name, cause):
550 return BBGenErr(
551 'Failed to generate %s from %s:%s' % (
552 suite_type, waterfall_name, bot_name),
553 cause=cause)
554
Kenneth Russelleb60cbd22017-12-05 07:54:28555 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
556 return BBGenErr(
557 'Test suite %s from machine %s on waterfall %s not present in ' \
558 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
559
560 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
561 return BBGenErr(
562 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
563 ' on waterfall ' + waterfall_name)
564
565 def generate_waterfall_json(self, waterfall):
566 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28567 generator_map = self.get_test_generator_map()
Kenneth Russell139f8642017-12-05 08:51:43568 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28569 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43570 # Copy only well-understood entries in the machine's configuration
571 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28572 if 'additional_compile_targets' in config:
573 tests['additional_compile_targets'] = config[
574 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43575 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28576 if test_type not in generator_map:
577 raise self.unknown_test_suite_type(
578 test_type, name, waterfall['name']) # pragma: no cover
579 test_generator = generator_map[test_type]
John Budorick826d5ed2017-12-28 19:27:32580 try:
581 tests[test_type] = test_generator.sort(test_generator.generate(
582 waterfall, name, config, input_tests))
583 except Exception as e:
584 raise self.generation_error(test_type, name, waterfall['name'], e)
Kenneth Russelleb60cbd22017-12-05 07:54:28585 all_tests[name] = tests
586 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
587 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
588 return json.dumps(all_tests, indent=2, separators=(',', ': '),
589 sort_keys=True) + '\n'
590
591 def generate_waterfalls(self): # pragma: no cover
592 self.load_configuration_files()
593 self.resolve_configuration_files()
594 filters = self.args.waterfall_filters
595 suffix = '.json'
596 if self.args.new_files:
597 suffix = '.new' + suffix
598 for waterfall in self.waterfalls:
599 should_gen = not filters or waterfall['name'] in filters
600 if should_gen:
601 self.write_file(waterfall['name'] + suffix,
602 self.generate_waterfall_json(waterfall))
603
604 def check_input_file_consistency(self):
605 self.load_configuration_files()
606 self.check_composition_test_suites()
607 # All test suites must be referenced.
608 suites_seen = set()
609 generator_map = self.get_test_generator_map()
610 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43611 for bot_name, tester in waterfall['machines'].iteritems():
612 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28613 if suite_type not in generator_map:
614 raise self.unknown_test_suite_type(suite_type, bot_name,
615 waterfall['name'])
616 if suite not in self.test_suites:
617 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
618 suites_seen.add(suite)
619 # Since we didn't resolve the configuration files, this set
620 # includes both composition test suites and regular ones.
621 resolved_suites = set()
622 for suite_name in suites_seen:
623 suite = self.test_suites[suite_name]
624 if isinstance(suite, list):
625 for sub_suite in suite:
626 resolved_suites.add(sub_suite)
627 resolved_suites.add(suite_name)
628 # At this point, every key in test_suites.pyl should be referenced.
629 missing_suites = set(self.test_suites.keys()) - resolved_suites
630 if missing_suites:
631 raise BBGenErr('The following test suites were unreferenced by bots on '
632 'the waterfalls: ' + str(missing_suites))
633
634 # All test suite exceptions must refer to bots on the waterfall.
635 all_bots = set()
636 missing_bots = set()
637 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43638 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28639 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:28640 # In order to disambiguate between bots with the same name on
641 # different waterfalls, support has been added to various
642 # exceptions for concatenating the waterfall name after the bot
643 # name.
644 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28645 for exception in self.exceptions.itervalues():
646 for removal in exception.get('remove_from', []):
647 if removal not in all_bots:
648 missing_bots.add(removal)
649 for mod in exception.get('modifications', {}).iterkeys():
650 if mod not in all_bots:
651 missing_bots.add(mod)
652 if missing_bots:
653 raise BBGenErr('The following nonexistent machines were referenced in '
654 'the test suite exceptions: ' + str(missing_bots))
655
656 def check_output_file_consistency(self, verbose=False):
657 self.load_configuration_files()
658 # All waterfalls must have been written by this script already.
659 self.resolve_configuration_files()
660 ungenerated_waterfalls = set()
661 for waterfall in self.waterfalls:
662 expected = self.generate_waterfall_json(waterfall)
663 current = self.read_file(waterfall['name'] + '.json')
664 if expected != current:
665 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:32666 if verbose: # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28667 print ('Waterfall ' + waterfall['name'] +
668 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:32669 'contents:')
670 for line in difflib.unified_diff(
671 expected.splitlines(),
672 current.splitlines()):
673 print line
Kenneth Russelleb60cbd22017-12-05 07:54:28674 if ungenerated_waterfalls:
675 raise BBGenErr('The following waterfalls have not been properly '
676 'autogenerated by generate_buildbot_json.py: ' +
677 str(ungenerated_waterfalls))
678
679 def check_consistency(self, verbose=False):
680 self.check_input_file_consistency() # pragma: no cover
681 self.check_output_file_consistency(verbose) # pragma: no cover
682
683 def parse_args(self, argv): # pragma: no cover
684 parser = argparse.ArgumentParser()
685 parser.add_argument(
686 '-c', '--check', action='store_true', help=
687 'Do consistency checks of configuration and generated files and then '
688 'exit. Used during presubmit. Causes the tool to not generate any files.')
689 parser.add_argument(
690 '-n', '--new-files', action='store_true', help=
691 'Write output files as .new.json. Useful during development so old and '
692 'new files can be looked at side-by-side.')
693 parser.add_argument(
694 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
695 help='Optional list of waterfalls to generate.')
696 self.args = parser.parse_args(argv)
697
698 def main(self, argv): # pragma: no cover
699 self.parse_args(argv)
700 if self.args.check:
701 self.check_consistency()
702 else:
703 self.generate_waterfalls()
704 return 0
705
706if __name__ == "__main__": # pragma: no cover
707 generator = BBJSONGenerator()
708 sys.exit(generator.main(sys.argv[1:]))