blob: 36fb1558f04bc1618631d881492226282ec27995 [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
Zhiling Huangbe008172018-03-08 19:13:11204 def pyl_file_path(self, filename):
205 if self.args and self.args.pyl_files_dir:
206 return os.path.join(self.args.pyl_files_dir, filename)
207 return filename
208
Kenneth Russelleb60cbd22017-12-05 07:54:28209 def load_pyl_file(self, filename):
210 try:
Zhiling Huangbe008172018-03-08 19:13:11211 return ast.literal_eval(self.read_file(
212 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28213 except (SyntaxError, ValueError) as e: # pragma: no cover
214 raise BBGenErr('Failed to parse pyl file "%s": %s' %
215 (filename, e)) # pragma: no cover
216
217 def is_android(self, tester_config):
218 return tester_config.get('os_type') == 'android'
219
220 def get_exception_for_test(self, test_name, test_config):
221 # gtests may have both "test" and "name" fields, and usually, if the "name"
222 # field is specified, it means that the same test is being repurposed
223 # multiple times with different command line arguments. To handle this case,
224 # prefer to lookup per the "name" field of the test itself, as opposed to
225 # the "test_name", which is actually the "test" field.
226 if 'name' in test_config:
227 return self.exceptions.get(test_config['name'])
228 else:
229 return self.exceptions.get(test_name)
230
231 def should_run_on_tester(self, waterfall, tester_name, tester_config,
Kenneth Russell8ceeabf2017-12-11 17:53:28232 test_name, test_config, test_suite_type=None):
233 # TODO(kbr): until this script is merged with the GPU test generator, some
Kenneth Russelleb60cbd22017-12-05 07:54:28234 # arguments will be unused.
Kenneth Russelleb60cbd22017-12-05 07:54:28235 del tester_config
236 # Currently, the only reason a test should not run on a given tester is that
237 # it's in the exceptions. (Once the GPU waterfall generation script is
238 # incorporated here, the rules will become more complex.)
239 exception = self.get_exception_for_test(test_name, test_config)
240 if not exception:
241 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28242 remove_from = None
243 if test_suite_type:
244 # First look for a specific removal for the test suite type,
245 # e.g. 'remove_gtest_from'.
246 remove_from = exception.get('remove_' + test_suite_type + '_from')
247 if remove_from and tester_name in remove_from:
248 # TODO(kbr): add coverage.
249 return False # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28250 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28251 if remove_from:
252 if tester_name in remove_from:
253 return False
254 # TODO(kbr): this code path was added for some tests (including
255 # android_webview_unittests) on one machine (Nougat Phone
256 # Tester) which exists with the same name on two waterfalls,
257 # chromium.android and chromium.fyi; the tests are run on one
258 # but not the other. Once the bots are all uniquely named (a
259 # different ongoing project) this code should be removed.
260 # TODO(kbr): add coverage.
261 return (tester_name + ' ' + waterfall['name']
262 not in remove_from) # pragma: no cover
263 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28264
Kenneth Russell8ceeabf2017-12-11 17:53:28265 def get_test_modifications(self, test, test_name, tester_name, waterfall):
Kenneth Russelleb60cbd22017-12-05 07:54:28266 exception = self.get_exception_for_test(test_name, test)
267 if not exception:
268 return None
Kenneth Russell8ceeabf2017-12-11 17:53:28269 mods = exception.get('modifications', {}).get(tester_name)
270 if mods:
271 return mods
272 # TODO(kbr): this code path was added for exactly one test
273 # (cronet_test_instrumentation_apk) on a few bots on
274 # chromium.android.fyi. Once the bots are all uniquely named (a
275 # different ongoing project) this code should be removed.
276 return exception.get('modifications', {}).get(tester_name + ' ' +
277 waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28278
279 def get_test_key_removals(self, test_name, tester_name):
280 exception = self.exceptions.get(test_name)
281 if not exception:
282 return []
283 return exception.get('key_removals', {}).get(tester_name, [])
284
285 def dictionary_merge(self, a, b, path=None, update=True):
286 """http://stackoverflow.com/questions/7204805/
287 python-dictionaries-of-dictionaries-merge
288 merges b into a
289 """
290 if path is None:
291 path = []
292 for key in b:
293 if key in a:
294 if isinstance(a[key], dict) and isinstance(b[key], dict):
295 self.dictionary_merge(a[key], b[key], path + [str(key)])
296 elif a[key] == b[key]:
297 pass # same leaf value
298 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06299 # Args arrays are lists of strings. Just concatenate them,
300 # and don't sort them, in order to keep some needed
301 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28302 if all(isinstance(x, str)
303 for x in itertools.chain(a[key], b[key])):
Stephen Martinis3bed2ab2018-04-23 19:42:06304 a[key] = a[key] + b[key]
Kenneth Russell8ceeabf2017-12-11 17:53:28305 else:
306 # TODO(kbr): this only works properly if the two arrays are
307 # the same length, which is currently always the case in the
308 # swarming dimension_sets that we have to merge. It will fail
309 # to merge / override 'args' arrays which are different
310 # length.
311 for idx in xrange(len(b[key])):
312 try:
313 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
314 path + [str(key), str(idx)],
315 update=update)
316 except (IndexError, TypeError): # pragma: no cover
317 raise BBGenErr('Error merging list keys ' + str(key) +
318 ' and indices ' + str(idx) + ' between ' +
319 str(a) + ' and ' + str(b)) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28320 elif update: # pragma: no cover
321 a[key] = b[key] # pragma: no cover
322 else:
323 raise BBGenErr('Conflict at %s' % '.'.join(
324 path + [str(key)])) # pragma: no cover
325 else:
326 a[key] = b[key]
327 return a
328
John Budorickedfe7f872018-01-23 15:27:22329 def initialize_args_for_test(self, generated_test, tester_config):
330 if 'args' in tester_config:
331 if 'args' not in generated_test:
332 generated_test['args'] = []
333 generated_test['args'].extend(tester_config['args'])
334
Kenneth Russelleb60cbd22017-12-05 07:54:28335 def initialize_swarming_dictionary_for_test(self, generated_test,
336 tester_config):
337 if 'swarming' not in generated_test:
338 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28339 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
340 generated_test['swarming'].update({
341 'can_use_on_swarming_builders': tester_config.get('use_swarming', True)
342 })
Kenneth Russelleb60cbd22017-12-05 07:54:28343 if 'swarming' in tester_config:
344 if 'dimension_sets' not in generated_test['swarming']:
345 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
346 tester_config['swarming']['dimension_sets'])
347 self.dictionary_merge(generated_test['swarming'],
348 tester_config['swarming'])
349 # Apply any Android-specific Swarming dimensions after the generic ones.
350 if 'android_swarming' in generated_test:
351 if self.is_android(tester_config): # pragma: no cover
352 self.dictionary_merge(
353 generated_test['swarming'],
354 generated_test['android_swarming']) # pragma: no cover
355 del generated_test['android_swarming'] # pragma: no cover
356
357 def clean_swarming_dictionary(self, swarming_dict):
358 # Clean out redundant entries from a test's "swarming" dictionary.
359 # This is really only needed to retain 100% parity with the
360 # handwritten JSON files, and can be removed once all the files are
361 # autogenerated.
362 if 'shards' in swarming_dict:
363 if swarming_dict['shards'] == 1: # pragma: no cover
364 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24365 if 'hard_timeout' in swarming_dict:
366 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
367 del swarming_dict['hard_timeout'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28368 if not swarming_dict['can_use_on_swarming_builders']:
369 # Remove all other keys.
370 for k in swarming_dict.keys(): # pragma: no cover
371 if k != 'can_use_on_swarming_builders': # pragma: no cover
372 del swarming_dict[k] # pragma: no cover
373
Kenneth Russell8ceeabf2017-12-11 17:53:28374 def update_and_cleanup_test(self, test, test_name, tester_name, waterfall):
Kenneth Russelleb60cbd22017-12-05 07:54:28375 # See if there are any exceptions that need to be merged into this
376 # test's specification.
Kenneth Russell8ceeabf2017-12-11 17:53:28377 modifications = self.get_test_modifications(test, test_name, tester_name,
378 waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28379 if modifications:
380 test = self.dictionary_merge(test, modifications)
381 for k in self.get_test_key_removals(test_name, tester_name):
382 del test[k]
Dirk Pranke1b767092017-12-07 04:44:23383 if 'swarming' in test:
384 self.clean_swarming_dictionary(test['swarming'])
Kenneth Russelleb60cbd22017-12-05 07:54:28385 return test
386
Shenghua Zhangaba8bad2018-02-07 02:12:09387 def add_common_test_properties(self, test, tester_config):
388 if tester_config.get('use_multi_dimension_trigger_script'):
389 test['trigger_script'] = {
390 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
391 'args': [
392 '--multiple-trigger-configs',
393 json.dumps(tester_config['swarming']['dimension_sets'] +
394 tester_config.get('alternate_swarming_dimensions', [])),
395 '--multiple-dimension-script-verbose',
396 'True'
397 ],
398 }
399
Kenneth Russelleb60cbd22017-12-05 07:54:28400 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
401 test_config):
402 if not self.should_run_on_tester(
Kenneth Russell8ceeabf2017-12-11 17:53:28403 waterfall, tester_name, tester_config, test_name, test_config,
404 TestSuiteTypes.GTEST):
Kenneth Russelleb60cbd22017-12-05 07:54:28405 return None
406 result = copy.deepcopy(test_config)
407 if 'test' in result:
408 result['name'] = test_name
409 else:
410 result['test'] = test_name
411 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickedfe7f872018-01-23 15:27:22412 self.initialize_args_for_test(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28413 if self.is_android(tester_config) and tester_config.get('use_swarming',
414 True):
John Budorick826d5ed2017-12-28 19:27:32415 if 'args' not in result:
416 result['args'] = []
417 result['args'].append('--gs-results-bucket=chromium-result-details')
Kenneth Russell8ceeabf2017-12-11 17:53:28418 if result['swarming']['can_use_on_swarming_builders'] and not \
419 tester_config.get('skip_merge_script', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28420 result['merge'] = {
421 'args': [
422 '--bucket',
423 'chromium-result-details',
424 '--test-name',
425 test_name
426 ],
427 'script': '//build/android/pylib/results/presentation/' \
428 'test_results_presentation.py',
429 } # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:28430 if not tester_config.get('skip_cipd_packages', False):
431 result['swarming']['cipd_packages'] = [
432 {
433 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
434 'location': 'bin',
435 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
436 }
437 ]
Kenneth Russelleb60cbd22017-12-05 07:54:28438 if not tester_config.get('skip_output_links', False):
439 result['swarming']['output_links'] = [
440 {
441 'link': [
442 'https://luci-logdog.appspot.com/v/?s',
443 '=android%2Fswarming%2Flogcats%2F',
444 '${TASK_ID}%2F%2B%2Funified_logcats',
445 ],
446 'name': 'shard #${SHARD_INDEX} logcats',
447 },
448 ]
Benjamin Pastene766d48f52017-12-18 21:47:42449 if not tester_config.get('skip_device_recovery', False):
John Budorick826d5ed2017-12-28 19:27:32450 result['args'].append('--recover-devices')
Benjamin Pastene766d48f52017-12-18 21:47:42451
Kenneth Russell8ceeabf2017-12-11 17:53:28452 result = self.update_and_cleanup_test(result, test_name, tester_name,
453 waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09454 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28455 return result
456
457 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
458 test_name, test_config):
459 if not self.should_run_on_tester(waterfall, tester_name, tester_config,
460 test_name, test_config):
461 return None
462 result = copy.deepcopy(test_config)
463 result['isolate_name'] = result.get('isolate_name', test_name)
464 result['name'] = test_name
465 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28466 result = self.update_and_cleanup_test(result, test_name, tester_name,
467 waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09468 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28469 return result
470
471 def generate_script_test(self, waterfall, tester_name, tester_config,
472 test_name, test_config):
473 if not self.should_run_on_tester(waterfall, tester_name, tester_config,
474 test_name, test_config):
475 return None
476 result = {
477 'name': test_name,
478 'script': test_config['script']
479 }
Kenneth Russell8ceeabf2017-12-11 17:53:28480 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 generate_junit_test(self, waterfall, tester_name, tester_config,
485 test_name, test_config):
486 if not self.should_run_on_tester(waterfall, tester_name, tester_config,
487 test_name, test_config):
488 return None
489 result = {
490 'test': test_name,
491 }
492 return result
493
494 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
495 test_name, test_config):
496 if not self.should_run_on_tester(waterfall, tester_name, tester_config,
497 test_name, test_config):
498 return None
499 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28500 if 'test' in result and result['test'] != test_name:
501 result['name'] = test_name
502 else:
503 result['test'] = test_name
504 result = self.update_and_cleanup_test(result, test_name, tester_name,
505 waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28506 return result
507
508 def get_test_generator_map(self):
509 return {
510 'cts_tests': CTSGenerator(self),
511 'gtest_tests': GTestGenerator(self),
512 'instrumentation_tests': InstrumentationTestGenerator(self),
513 'isolated_scripts': IsolatedScriptTestGenerator(self),
514 'junit_tests': JUnitGenerator(self),
515 'scripts': ScriptGenerator(self),
516 }
517
518 def check_composition_test_suites(self):
519 # Pre-pass to catch errors reliably.
520 for name, value in self.test_suites.iteritems():
521 if isinstance(value, list):
522 for entry in value:
523 if isinstance(self.test_suites[entry], list):
524 raise BBGenErr('Composition test suites may not refer to other ' \
525 'composition test suites (error found while ' \
526 'processing %s)' % name)
527
528 def resolve_composition_test_suites(self):
529 self.check_composition_test_suites()
530 for name, value in self.test_suites.iteritems():
531 if isinstance(value, list):
532 # Resolve this to a dictionary.
533 full_suite = {}
534 for entry in value:
535 suite = self.test_suites[entry]
536 full_suite.update(suite)
537 self.test_suites[name] = full_suite
538
539 def link_waterfalls_to_test_suites(self):
540 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43541 for tester_name, tester in waterfall['machines'].iteritems():
542 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28543 if not value in self.test_suites:
544 # Hard / impossible to cover this in the unit test.
545 raise self.unknown_test_suite(
546 value, tester_name, waterfall['name']) # pragma: no cover
547 tester['test_suites'][suite] = self.test_suites[value]
548
549 def load_configuration_files(self):
550 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
551 self.test_suites = self.load_pyl_file('test_suites.pyl')
552 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
553
554 def resolve_configuration_files(self):
555 self.resolve_composition_test_suites()
556 self.link_waterfalls_to_test_suites()
557
John Budorick826d5ed2017-12-28 19:27:32558 def generation_error(self, suite_type, bot_name, waterfall_name, cause):
559 return BBGenErr(
560 'Failed to generate %s from %s:%s' % (
561 suite_type, waterfall_name, bot_name),
562 cause=cause)
563
Kenneth Russelleb60cbd22017-12-05 07:54:28564 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
565 return BBGenErr(
566 'Test suite %s from machine %s on waterfall %s not present in ' \
567 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
568
569 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
570 return BBGenErr(
571 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
572 ' on waterfall ' + waterfall_name)
573
574 def generate_waterfall_json(self, waterfall):
575 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28576 generator_map = self.get_test_generator_map()
Kenneth Russell139f8642017-12-05 08:51:43577 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28578 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43579 # Copy only well-understood entries in the machine's configuration
580 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28581 if 'additional_compile_targets' in config:
582 tests['additional_compile_targets'] = config[
583 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43584 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28585 if test_type not in generator_map:
586 raise self.unknown_test_suite_type(
587 test_type, name, waterfall['name']) # pragma: no cover
588 test_generator = generator_map[test_type]
John Budorick826d5ed2017-12-28 19:27:32589 try:
590 tests[test_type] = test_generator.sort(test_generator.generate(
591 waterfall, name, config, input_tests))
592 except Exception as e:
593 raise self.generation_error(test_type, name, waterfall['name'], e)
Kenneth Russelleb60cbd22017-12-05 07:54:28594 all_tests[name] = tests
595 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
596 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
597 return json.dumps(all_tests, indent=2, separators=(',', ': '),
598 sort_keys=True) + '\n'
599
600 def generate_waterfalls(self): # pragma: no cover
601 self.load_configuration_files()
602 self.resolve_configuration_files()
603 filters = self.args.waterfall_filters
604 suffix = '.json'
605 if self.args.new_files:
606 suffix = '.new' + suffix
607 for waterfall in self.waterfalls:
608 should_gen = not filters or waterfall['name'] in filters
609 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:11610 file_path = waterfall['name'] + suffix
611 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:28612 self.generate_waterfall_json(waterfall))
613
614 def check_input_file_consistency(self):
615 self.load_configuration_files()
616 self.check_composition_test_suites()
617 # All test suites must be referenced.
618 suites_seen = set()
619 generator_map = self.get_test_generator_map()
620 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43621 for bot_name, tester in waterfall['machines'].iteritems():
622 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28623 if suite_type not in generator_map:
624 raise self.unknown_test_suite_type(suite_type, bot_name,
625 waterfall['name'])
626 if suite not in self.test_suites:
627 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
628 suites_seen.add(suite)
629 # Since we didn't resolve the configuration files, this set
630 # includes both composition test suites and regular ones.
631 resolved_suites = set()
632 for suite_name in suites_seen:
633 suite = self.test_suites[suite_name]
634 if isinstance(suite, list):
635 for sub_suite in suite:
636 resolved_suites.add(sub_suite)
637 resolved_suites.add(suite_name)
638 # At this point, every key in test_suites.pyl should be referenced.
639 missing_suites = set(self.test_suites.keys()) - resolved_suites
640 if missing_suites:
641 raise BBGenErr('The following test suites were unreferenced by bots on '
642 'the waterfalls: ' + str(missing_suites))
643
644 # All test suite exceptions must refer to bots on the waterfall.
645 all_bots = set()
646 missing_bots = set()
647 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43648 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28649 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:28650 # In order to disambiguate between bots with the same name on
651 # different waterfalls, support has been added to various
652 # exceptions for concatenating the waterfall name after the bot
653 # name.
654 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28655 for exception in self.exceptions.itervalues():
656 for removal in exception.get('remove_from', []):
657 if removal not in all_bots:
658 missing_bots.add(removal)
659 for mod in exception.get('modifications', {}).iterkeys():
660 if mod not in all_bots:
661 missing_bots.add(mod)
662 if missing_bots:
663 raise BBGenErr('The following nonexistent machines were referenced in '
664 'the test suite exceptions: ' + str(missing_bots))
665
666 def check_output_file_consistency(self, verbose=False):
667 self.load_configuration_files()
668 # All waterfalls must have been written by this script already.
669 self.resolve_configuration_files()
670 ungenerated_waterfalls = set()
671 for waterfall in self.waterfalls:
672 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:11673 file_path = waterfall['name'] + '.json'
674 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:28675 if expected != current:
676 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:32677 if verbose: # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28678 print ('Waterfall ' + waterfall['name'] +
679 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:32680 'contents:')
681 for line in difflib.unified_diff(
682 expected.splitlines(),
683 current.splitlines()):
684 print line
Kenneth Russelleb60cbd22017-12-05 07:54:28685 if ungenerated_waterfalls:
686 raise BBGenErr('The following waterfalls have not been properly '
687 'autogenerated by generate_buildbot_json.py: ' +
688 str(ungenerated_waterfalls))
689
690 def check_consistency(self, verbose=False):
691 self.check_input_file_consistency() # pragma: no cover
692 self.check_output_file_consistency(verbose) # pragma: no cover
693
694 def parse_args(self, argv): # pragma: no cover
695 parser = argparse.ArgumentParser()
696 parser.add_argument(
697 '-c', '--check', action='store_true', help=
698 'Do consistency checks of configuration and generated files and then '
699 'exit. Used during presubmit. Causes the tool to not generate any files.')
700 parser.add_argument(
701 '-n', '--new-files', action='store_true', help=
702 'Write output files as .new.json. Useful during development so old and '
703 'new files can be looked at side-by-side.')
704 parser.add_argument(
705 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
706 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:11707 parser.add_argument(
708 '--pyl-files-dir', type=os.path.realpath,
709 help='Path to the directory containing the input .pyl files.')
Kenneth Russelleb60cbd22017-12-05 07:54:28710 self.args = parser.parse_args(argv)
711
712 def main(self, argv): # pragma: no cover
713 self.parse_args(argv)
714 if self.args.check:
715 self.check_consistency()
716 else:
717 self.generate_waterfalls()
718 return 0
719
720if __name__ == "__main__": # pragma: no cover
721 generator = BBJSONGenerator()
722 sys.exit(generator.main(sys.argv[1:]))