blob: 75b26dbf5fa33850710ef879306da467a5443fd7 [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
378 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
379 test_config):
380 if not self.should_run_on_tester(
Kenneth Russell8ceeabf2017-12-11 17:53:28381 waterfall, tester_name, tester_config, test_name, test_config,
382 TestSuiteTypes.GTEST):
Kenneth Russelleb60cbd22017-12-05 07:54:28383 return None
384 result = copy.deepcopy(test_config)
385 if 'test' in result:
386 result['name'] = test_name
387 else:
388 result['test'] = test_name
389 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickedfe7f872018-01-23 15:27:22390 self.initialize_args_for_test(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28391 if self.is_android(tester_config) and tester_config.get('use_swarming',
392 True):
John Budorick826d5ed2017-12-28 19:27:32393 if 'args' not in result:
394 result['args'] = []
395 result['args'].append('--gs-results-bucket=chromium-result-details')
Kenneth Russell8ceeabf2017-12-11 17:53:28396 if result['swarming']['can_use_on_swarming_builders'] and not \
397 tester_config.get('skip_merge_script', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28398 result['merge'] = {
399 'args': [
400 '--bucket',
401 'chromium-result-details',
402 '--test-name',
403 test_name
404 ],
405 'script': '//build/android/pylib/results/presentation/' \
406 'test_results_presentation.py',
407 } # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:28408 if not tester_config.get('skip_cipd_packages', False):
409 result['swarming']['cipd_packages'] = [
410 {
411 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
412 'location': 'bin',
413 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
414 }
415 ]
Kenneth Russelleb60cbd22017-12-05 07:54:28416 if not tester_config.get('skip_output_links', False):
417 result['swarming']['output_links'] = [
418 {
419 'link': [
420 'https://luci-logdog.appspot.com/v/?s',
421 '=android%2Fswarming%2Flogcats%2F',
422 '${TASK_ID}%2F%2B%2Funified_logcats',
423 ],
424 'name': 'shard #${SHARD_INDEX} logcats',
425 },
426 ]
Benjamin Pastene766d48f52017-12-18 21:47:42427 if not tester_config.get('skip_device_recovery', False):
John Budorick826d5ed2017-12-28 19:27:32428 result['args'].append('--recover-devices')
Benjamin Pastene766d48f52017-12-18 21:47:42429
Kenneth Russell8ceeabf2017-12-11 17:53:28430 result = self.update_and_cleanup_test(result, test_name, tester_name,
431 waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28432 return result
433
434 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
435 test_name, test_config):
436 if not self.should_run_on_tester(waterfall, tester_name, tester_config,
437 test_name, test_config):
438 return None
439 result = copy.deepcopy(test_config)
440 result['isolate_name'] = result.get('isolate_name', test_name)
441 result['name'] = test_name
442 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28443 result = self.update_and_cleanup_test(result, test_name, tester_name,
444 waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28445 return result
446
447 def generate_script_test(self, waterfall, tester_name, tester_config,
448 test_name, test_config):
449 if not self.should_run_on_tester(waterfall, tester_name, tester_config,
450 test_name, test_config):
451 return None
452 result = {
453 'name': test_name,
454 'script': test_config['script']
455 }
Kenneth Russell8ceeabf2017-12-11 17:53:28456 result = self.update_and_cleanup_test(result, test_name, tester_name,
457 waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28458 return result
459
460 def generate_junit_test(self, waterfall, tester_name, tester_config,
461 test_name, test_config):
462 if not self.should_run_on_tester(waterfall, tester_name, tester_config,
463 test_name, test_config):
464 return None
465 result = {
466 'test': test_name,
467 }
468 return result
469
470 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
471 test_name, test_config):
472 if not self.should_run_on_tester(waterfall, tester_name, tester_config,
473 test_name, test_config):
474 return None
475 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28476 if 'test' in result and result['test'] != test_name:
477 result['name'] = test_name
478 else:
479 result['test'] = test_name
480 result = self.update_and_cleanup_test(result, test_name, tester_name,
481 waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28482 return result
483
484 def get_test_generator_map(self):
485 return {
486 'cts_tests': CTSGenerator(self),
487 'gtest_tests': GTestGenerator(self),
488 'instrumentation_tests': InstrumentationTestGenerator(self),
489 'isolated_scripts': IsolatedScriptTestGenerator(self),
490 'junit_tests': JUnitGenerator(self),
491 'scripts': ScriptGenerator(self),
492 }
493
494 def check_composition_test_suites(self):
495 # Pre-pass to catch errors reliably.
496 for name, value in self.test_suites.iteritems():
497 if isinstance(value, list):
498 for entry in value:
499 if isinstance(self.test_suites[entry], list):
500 raise BBGenErr('Composition test suites may not refer to other ' \
501 'composition test suites (error found while ' \
502 'processing %s)' % name)
503
504 def resolve_composition_test_suites(self):
505 self.check_composition_test_suites()
506 for name, value in self.test_suites.iteritems():
507 if isinstance(value, list):
508 # Resolve this to a dictionary.
509 full_suite = {}
510 for entry in value:
511 suite = self.test_suites[entry]
512 full_suite.update(suite)
513 self.test_suites[name] = full_suite
514
515 def link_waterfalls_to_test_suites(self):
516 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43517 for tester_name, tester in waterfall['machines'].iteritems():
518 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28519 if not value in self.test_suites:
520 # Hard / impossible to cover this in the unit test.
521 raise self.unknown_test_suite(
522 value, tester_name, waterfall['name']) # pragma: no cover
523 tester['test_suites'][suite] = self.test_suites[value]
524
525 def load_configuration_files(self):
526 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
527 self.test_suites = self.load_pyl_file('test_suites.pyl')
528 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
529
530 def resolve_configuration_files(self):
531 self.resolve_composition_test_suites()
532 self.link_waterfalls_to_test_suites()
533
John Budorick826d5ed2017-12-28 19:27:32534 def generation_error(self, suite_type, bot_name, waterfall_name, cause):
535 return BBGenErr(
536 'Failed to generate %s from %s:%s' % (
537 suite_type, waterfall_name, bot_name),
538 cause=cause)
539
Kenneth Russelleb60cbd22017-12-05 07:54:28540 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
541 return BBGenErr(
542 'Test suite %s from machine %s on waterfall %s not present in ' \
543 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
544
545 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
546 return BBGenErr(
547 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
548 ' on waterfall ' + waterfall_name)
549
550 def generate_waterfall_json(self, waterfall):
551 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28552 generator_map = self.get_test_generator_map()
Kenneth Russell139f8642017-12-05 08:51:43553 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28554 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43555 # Copy only well-understood entries in the machine's configuration
556 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28557 if 'additional_compile_targets' in config:
558 tests['additional_compile_targets'] = config[
559 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43560 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28561 if test_type not in generator_map:
562 raise self.unknown_test_suite_type(
563 test_type, name, waterfall['name']) # pragma: no cover
564 test_generator = generator_map[test_type]
John Budorick826d5ed2017-12-28 19:27:32565 try:
566 tests[test_type] = test_generator.sort(test_generator.generate(
567 waterfall, name, config, input_tests))
568 except Exception as e:
569 raise self.generation_error(test_type, name, waterfall['name'], e)
Kenneth Russelleb60cbd22017-12-05 07:54:28570 all_tests[name] = tests
571 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
572 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
573 return json.dumps(all_tests, indent=2, separators=(',', ': '),
574 sort_keys=True) + '\n'
575
576 def generate_waterfalls(self): # pragma: no cover
577 self.load_configuration_files()
578 self.resolve_configuration_files()
579 filters = self.args.waterfall_filters
580 suffix = '.json'
581 if self.args.new_files:
582 suffix = '.new' + suffix
583 for waterfall in self.waterfalls:
584 should_gen = not filters or waterfall['name'] in filters
585 if should_gen:
586 self.write_file(waterfall['name'] + suffix,
587 self.generate_waterfall_json(waterfall))
588
589 def check_input_file_consistency(self):
590 self.load_configuration_files()
591 self.check_composition_test_suites()
592 # All test suites must be referenced.
593 suites_seen = set()
594 generator_map = self.get_test_generator_map()
595 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43596 for bot_name, tester in waterfall['machines'].iteritems():
597 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28598 if suite_type not in generator_map:
599 raise self.unknown_test_suite_type(suite_type, bot_name,
600 waterfall['name'])
601 if suite not in self.test_suites:
602 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
603 suites_seen.add(suite)
604 # Since we didn't resolve the configuration files, this set
605 # includes both composition test suites and regular ones.
606 resolved_suites = set()
607 for suite_name in suites_seen:
608 suite = self.test_suites[suite_name]
609 if isinstance(suite, list):
610 for sub_suite in suite:
611 resolved_suites.add(sub_suite)
612 resolved_suites.add(suite_name)
613 # At this point, every key in test_suites.pyl should be referenced.
614 missing_suites = set(self.test_suites.keys()) - resolved_suites
615 if missing_suites:
616 raise BBGenErr('The following test suites were unreferenced by bots on '
617 'the waterfalls: ' + str(missing_suites))
618
619 # All test suite exceptions must refer to bots on the waterfall.
620 all_bots = set()
621 missing_bots = set()
622 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43623 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28624 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:28625 # In order to disambiguate between bots with the same name on
626 # different waterfalls, support has been added to various
627 # exceptions for concatenating the waterfall name after the bot
628 # name.
629 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28630 for exception in self.exceptions.itervalues():
631 for removal in exception.get('remove_from', []):
632 if removal not in all_bots:
633 missing_bots.add(removal)
634 for mod in exception.get('modifications', {}).iterkeys():
635 if mod not in all_bots:
636 missing_bots.add(mod)
637 if missing_bots:
638 raise BBGenErr('The following nonexistent machines were referenced in '
639 'the test suite exceptions: ' + str(missing_bots))
640
641 def check_output_file_consistency(self, verbose=False):
642 self.load_configuration_files()
643 # All waterfalls must have been written by this script already.
644 self.resolve_configuration_files()
645 ungenerated_waterfalls = set()
646 for waterfall in self.waterfalls:
647 expected = self.generate_waterfall_json(waterfall)
648 current = self.read_file(waterfall['name'] + '.json')
649 if expected != current:
650 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:32651 if verbose: # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28652 print ('Waterfall ' + waterfall['name'] +
653 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:32654 'contents:')
655 for line in difflib.unified_diff(
656 expected.splitlines(),
657 current.splitlines()):
658 print line
Kenneth Russelleb60cbd22017-12-05 07:54:28659 if ungenerated_waterfalls:
660 raise BBGenErr('The following waterfalls have not been properly '
661 'autogenerated by generate_buildbot_json.py: ' +
662 str(ungenerated_waterfalls))
663
664 def check_consistency(self, verbose=False):
665 self.check_input_file_consistency() # pragma: no cover
666 self.check_output_file_consistency(verbose) # pragma: no cover
667
668 def parse_args(self, argv): # pragma: no cover
669 parser = argparse.ArgumentParser()
670 parser.add_argument(
671 '-c', '--check', action='store_true', help=
672 'Do consistency checks of configuration and generated files and then '
673 'exit. Used during presubmit. Causes the tool to not generate any files.')
674 parser.add_argument(
675 '-n', '--new-files', action='store_true', help=
676 'Write output files as .new.json. Useful during development so old and '
677 'new files can be looked at side-by-side.')
678 parser.add_argument(
679 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
680 help='Optional list of waterfalls to generate.')
681 self.args = parser.parse_args(argv)
682
683 def main(self, argv): # pragma: no cover
684 self.parse_args(argv)
685 if self.args.check:
686 self.check_consistency()
687 else:
688 self.generate_waterfalls()
689 return 0
690
691if __name__ == "__main__": # pragma: no cover
692 generator = BBJSONGenerator()
693 sys.exit(generator.main(sys.argv[1:]))