blob: 9c4fa94cc7ca08d5645955d2f5f2e27f299dbb0b [file] [log] [blame]
Kenneth Russelleb60cbd22017-12-05 07:54:281#!/usr/bin/env python
2# Copyright 2016 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Script to generate the majority of the JSON files in the src/testing/buildbot
7directory. Maintaining these files by hand is too unwieldy.
8"""
9
10import argparse
11import ast
12import collections
13import copy
John Budorick826d5ed2017-12-28 19:27:3214import difflib
Kenneth Russell8ceeabf2017-12-11 17:53:2815import itertools
Kenneth Russelleb60cbd22017-12-05 07:54:2816import json
17import os
18import string
19import sys
John Budorick826d5ed2017-12-28 19:27:3220import traceback
Kenneth Russelleb60cbd22017-12-05 07:54:2821
22THIS_DIR = os.path.dirname(os.path.abspath(__file__))
23
24
25class BBGenErr(Exception):
Nico Weber79dc5f6852018-07-13 19:38:4926 def __init__(self, message):
27 super(BBGenErr, self).__init__(message)
Kenneth Russelleb60cbd22017-12-05 07:54:2828
29
Kenneth Russell8ceeabf2017-12-11 17:53:2830# This class is only present to accommodate certain machines on
31# chromium.android.fyi which run certain tests as instrumentation
32# tests, but not as gtests. If this discrepancy were fixed then the
33# notion could be removed.
34class TestSuiteTypes(object):
35 GTEST = 'gtest'
36
37
Kenneth Russelleb60cbd22017-12-05 07:54:2838class BaseGenerator(object):
39 def __init__(self, bb_gen):
40 self.bb_gen = bb_gen
41
Kenneth Russell8ceeabf2017-12-11 17:53:2842 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2843 raise NotImplementedError()
44
45 def sort(self, tests):
46 raise NotImplementedError()
47
48
Kenneth Russell8ceeabf2017-12-11 17:53:2849def cmp_tests(a, b):
50 # Prefer to compare based on the "test" key.
51 val = cmp(a['test'], b['test'])
52 if val != 0:
53 return val
54 if 'name' in a and 'name' in b:
55 return cmp(a['name'], b['name']) # pragma: no cover
56 if 'name' not in a and 'name' not in b:
57 return 0 # pragma: no cover
58 # Prefer to put variants of the same test after the first one.
59 if 'name' in a:
60 return 1
61 # 'name' is in b.
62 return -1 # pragma: no cover
63
64
Kenneth Russell8a386d42018-06-02 09:48:0165class GPUTelemetryTestGenerator(BaseGenerator):
66 def __init__(self, bb_gen):
67 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
68
69 def generate(self, waterfall, tester_name, tester_config, input_tests):
70 isolated_scripts = []
71 for test_name, test_config in sorted(input_tests.iteritems()):
72 test = self.bb_gen.generate_gpu_telemetry_test(
73 waterfall, tester_name, tester_config, test_name, test_config)
74 if test:
75 isolated_scripts.append(test)
76 return isolated_scripts
77
78 def sort(self, tests):
79 return sorted(tests, key=lambda x: x['name'])
80
81
Kenneth Russelleb60cbd22017-12-05 07:54:2882class GTestGenerator(BaseGenerator):
83 def __init__(self, bb_gen):
84 super(GTestGenerator, self).__init__(bb_gen)
85
Kenneth Russell8ceeabf2017-12-11 17:53:2886 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2887 # The relative ordering of some of the tests is important to
88 # minimize differences compared to the handwritten JSON files, since
89 # Python's sorts are stable and there are some tests with the same
90 # key (see gles2_conform_d3d9_test and similar variants). Avoid
91 # losing the order by avoiding coalescing the dictionaries into one.
92 gtests = []
93 for test_name, test_config in sorted(input_tests.iteritems()):
Nico Weber79dc5f6852018-07-13 19:38:4994 test = self.bb_gen.generate_gtest(
95 waterfall, tester_name, tester_config, test_name, test_config)
96 if test:
97 # generate_gtest may veto the test generation on this tester.
98 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:2899 return gtests
100
101 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28102 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28103
104
105class IsolatedScriptTestGenerator(BaseGenerator):
106 def __init__(self, bb_gen):
107 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
108
Kenneth Russell8ceeabf2017-12-11 17:53:28109 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28110 isolated_scripts = []
111 for test_name, test_config in sorted(input_tests.iteritems()):
112 test = self.bb_gen.generate_isolated_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28113 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28114 if test:
115 isolated_scripts.append(test)
116 return isolated_scripts
117
118 def sort(self, tests):
119 return sorted(tests, key=lambda x: x['name'])
120
121
122class ScriptGenerator(BaseGenerator):
123 def __init__(self, bb_gen):
124 super(ScriptGenerator, self).__init__(bb_gen)
125
Kenneth Russell8ceeabf2017-12-11 17:53:28126 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28127 scripts = []
128 for test_name, test_config in sorted(input_tests.iteritems()):
129 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28130 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28131 if test:
132 scripts.append(test)
133 return scripts
134
135 def sort(self, tests):
136 return sorted(tests, key=lambda x: x['name'])
137
138
139class JUnitGenerator(BaseGenerator):
140 def __init__(self, bb_gen):
141 super(JUnitGenerator, self).__init__(bb_gen)
142
Kenneth Russell8ceeabf2017-12-11 17:53:28143 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28144 scripts = []
145 for test_name, test_config in sorted(input_tests.iteritems()):
146 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28147 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28148 if test:
149 scripts.append(test)
150 return scripts
151
152 def sort(self, tests):
153 return sorted(tests, key=lambda x: x['test'])
154
155
156class CTSGenerator(BaseGenerator):
157 def __init__(self, bb_gen):
158 super(CTSGenerator, self).__init__(bb_gen)
159
Kenneth Russell8ceeabf2017-12-11 17:53:28160 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28161 # These only contain one entry and it's the contents of the input tests'
162 # dictionary, verbatim.
163 cts_tests = []
164 cts_tests.append(input_tests)
165 return cts_tests
166
167 def sort(self, tests):
168 return tests
169
170
171class InstrumentationTestGenerator(BaseGenerator):
172 def __init__(self, bb_gen):
173 super(InstrumentationTestGenerator, self).__init__(bb_gen)
174
Kenneth Russell8ceeabf2017-12-11 17:53:28175 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28176 scripts = []
177 for test_name, test_config in sorted(input_tests.iteritems()):
178 test = self.bb_gen.generate_instrumentation_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28179 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28180 if test:
181 scripts.append(test)
182 return scripts
183
184 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28185 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28186
187
188class BBJSONGenerator(object):
189 def __init__(self):
190 self.this_dir = THIS_DIR
191 self.args = None
192 self.waterfalls = None
193 self.test_suites = None
194 self.exceptions = None
195
196 def generate_abs_file_path(self, relative_path):
197 return os.path.join(self.this_dir, relative_path) # pragma: no cover
198
199 def read_file(self, relative_path):
200 with open(self.generate_abs_file_path(
201 relative_path)) as fp: # pragma: no cover
202 return fp.read() # pragma: no cover
203
204 def write_file(self, relative_path, contents):
205 with open(self.generate_abs_file_path(
206 relative_path), 'wb') as fp: # pragma: no cover
207 fp.write(contents) # pragma: no cover
208
Zhiling Huangbe008172018-03-08 19:13:11209 def pyl_file_path(self, filename):
210 if self.args and self.args.pyl_files_dir:
211 return os.path.join(self.args.pyl_files_dir, filename)
212 return filename
213
Kenneth Russelleb60cbd22017-12-05 07:54:28214 def load_pyl_file(self, filename):
215 try:
Zhiling Huangbe008172018-03-08 19:13:11216 return ast.literal_eval(self.read_file(
217 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28218 except (SyntaxError, ValueError) as e: # pragma: no cover
219 raise BBGenErr('Failed to parse pyl file "%s": %s' %
220 (filename, e)) # pragma: no cover
221
Kenneth Russell8a386d42018-06-02 09:48:01222 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
223 # Currently it is only mandatory for bots which run GPU tests. Change these to
224 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28225 def is_android(self, tester_config):
226 return tester_config.get('os_type') == 'android'
227
Kenneth Russell8a386d42018-06-02 09:48:01228 def is_linux(self, tester_config):
229 return tester_config.get('os_type') == 'linux'
230
Kenneth Russelleb60cbd22017-12-05 07:54:28231 def get_exception_for_test(self, test_name, test_config):
232 # gtests may have both "test" and "name" fields, and usually, if the "name"
233 # field is specified, it means that the same test is being repurposed
234 # multiple times with different command line arguments. To handle this case,
235 # prefer to lookup per the "name" field of the test itself, as opposed to
236 # the "test_name", which is actually the "test" field.
237 if 'name' in test_config:
238 return self.exceptions.get(test_config['name'])
239 else:
240 return self.exceptions.get(test_name)
241
Nico Weberb0b3f5862018-07-13 18:45:15242 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28243 # Currently, the only reason a test should not run on a given tester is that
244 # it's in the exceptions. (Once the GPU waterfall generation script is
245 # incorporated here, the rules will become more complex.)
246 exception = self.get_exception_for_test(test_name, test_config)
247 if not exception:
248 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28249 remove_from = None
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
Nico Weber79dc5f6852018-07-13 19:38:49265 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28266 exception = self.get_exception_for_test(test_name, test)
267 if not exception:
268 return None
Nico Weber79dc5f6852018-07-13 19:38:49269 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28270
Kenneth Russell8a386d42018-06-02 09:48:01271 def merge_command_line_args(self, arr, prefix, splitter):
272 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01273 idx = 0
274 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01275 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01276 while idx < len(arr):
277 flag = arr[idx]
278 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01279 if flag.startswith(prefix):
280 arg = flag[prefix_len:]
281 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01282 if first_idx < 0:
283 first_idx = idx
284 else:
285 delete_current_entry = True
286 if delete_current_entry:
287 del arr[idx]
288 else:
289 idx += 1
290 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01291 arr[first_idx] = prefix + splitter.join(accumulated_args)
292 return arr
293
294 def maybe_fixup_args_array(self, arr):
295 # The incoming array of strings may be an array of command line
296 # arguments. To make it easier to turn on certain features per-bot or
297 # per-test-suite, look specifically for certain flags and merge them
298 # appropriately.
299 # --enable-features=Feature1 --enable-features=Feature2
300 # are merged to:
301 # --enable-features=Feature1,Feature2
302 # and:
303 # --extra-browser-args=arg1 --extra-browser-args=arg2
304 # are merged to:
305 # --extra-browser-args=arg1 arg2
306 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
307 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01308 return arr
309
Kenneth Russelleb60cbd22017-12-05 07:54:28310 def dictionary_merge(self, a, b, path=None, update=True):
311 """http://stackoverflow.com/questions/7204805/
312 python-dictionaries-of-dictionaries-merge
313 merges b into a
314 """
315 if path is None:
316 path = []
317 for key in b:
318 if key in a:
319 if isinstance(a[key], dict) and isinstance(b[key], dict):
320 self.dictionary_merge(a[key], b[key], path + [str(key)])
321 elif a[key] == b[key]:
322 pass # same leaf value
323 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06324 # Args arrays are lists of strings. Just concatenate them,
325 # and don't sort them, in order to keep some needed
326 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28327 if all(isinstance(x, str)
328 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01329 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28330 else:
331 # TODO(kbr): this only works properly if the two arrays are
332 # the same length, which is currently always the case in the
333 # swarming dimension_sets that we have to merge. It will fail
334 # to merge / override 'args' arrays which are different
335 # length.
336 for idx in xrange(len(b[key])):
337 try:
338 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
339 path + [str(key), str(idx)],
340 update=update)
341 except (IndexError, TypeError): # pragma: no cover
342 raise BBGenErr('Error merging list keys ' + str(key) +
343 ' and indices ' + str(idx) + ' between ' +
344 str(a) + ' and ' + str(b)) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28345 elif update: # pragma: no cover
346 a[key] = b[key] # pragma: no cover
347 else:
348 raise BBGenErr('Conflict at %s' % '.'.join(
349 path + [str(key)])) # pragma: no cover
350 else:
351 a[key] = b[key]
352 return a
353
John Budorickab108712018-09-01 00:12:21354 def initialize_args_for_test(
355 self, generated_test, tester_config, additional_arg_keys=None):
356
357 args = []
358 args.extend(generated_test.get('args', []))
359 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22360
Kenneth Russell8a386d42018-06-02 09:48:01361 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21362 val = generated_test.pop(key, [])
363 if fn(tester_config):
364 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01365
366 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
367 add_conditional_args('linux_args', self.is_linux)
368 add_conditional_args('android_args', self.is_android)
369
John Budorickab108712018-09-01 00:12:21370 for key in additional_arg_keys or []:
371 args.extend(generated_test.pop(key, []))
372 args.extend(tester_config.get(key, []))
373
374 if args:
375 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01376
Kenneth Russelleb60cbd22017-12-05 07:54:28377 def initialize_swarming_dictionary_for_test(self, generated_test,
378 tester_config):
379 if 'swarming' not in generated_test:
380 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28381 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
382 generated_test['swarming'].update({
383 'can_use_on_swarming_builders': tester_config.get('use_swarming', True)
384 })
Kenneth Russelleb60cbd22017-12-05 07:54:28385 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03386 if ('dimension_sets' not in generated_test['swarming'] and
387 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28388 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
389 tester_config['swarming']['dimension_sets'])
390 self.dictionary_merge(generated_test['swarming'],
391 tester_config['swarming'])
392 # Apply any Android-specific Swarming dimensions after the generic ones.
393 if 'android_swarming' in generated_test:
394 if self.is_android(tester_config): # pragma: no cover
395 self.dictionary_merge(
396 generated_test['swarming'],
397 generated_test['android_swarming']) # pragma: no cover
398 del generated_test['android_swarming'] # pragma: no cover
399
400 def clean_swarming_dictionary(self, swarming_dict):
401 # Clean out redundant entries from a test's "swarming" dictionary.
402 # This is really only needed to retain 100% parity with the
403 # handwritten JSON files, and can be removed once all the files are
404 # autogenerated.
405 if 'shards' in swarming_dict:
406 if swarming_dict['shards'] == 1: # pragma: no cover
407 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24408 if 'hard_timeout' in swarming_dict:
409 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
410 del swarming_dict['hard_timeout'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28411 if not swarming_dict['can_use_on_swarming_builders']:
412 # Remove all other keys.
413 for k in swarming_dict.keys(): # pragma: no cover
414 if k != 'can_use_on_swarming_builders': # pragma: no cover
415 del swarming_dict[k] # pragma: no cover
416
Nico Weber79dc5f6852018-07-13 19:38:49417 def update_and_cleanup_test(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28418 # See if there are any exceptions that need to be merged into this
419 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49420 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28421 if modifications:
422 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23423 if 'swarming' in test:
424 self.clean_swarming_dictionary(test['swarming'])
Kenneth Russelleb60cbd22017-12-05 07:54:28425 return test
426
Shenghua Zhangaba8bad2018-02-07 02:12:09427 def add_common_test_properties(self, test, tester_config):
428 if tester_config.get('use_multi_dimension_trigger_script'):
429 test['trigger_script'] = {
430 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
431 'args': [
432 '--multiple-trigger-configs',
433 json.dumps(tester_config['swarming']['dimension_sets'] +
434 tester_config.get('alternate_swarming_dimensions', [])),
435 '--multiple-dimension-script-verbose',
436 'True'
437 ],
438 }
439
Kenneth Russelleb60cbd22017-12-05 07:54:28440 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
441 test_config):
442 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15443 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28444 return None
445 result = copy.deepcopy(test_config)
446 if 'test' in result:
447 result['name'] = test_name
448 else:
449 result['test'] = test_name
450 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21451
452 self.initialize_args_for_test(
453 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28454 if self.is_android(tester_config) and tester_config.get('use_swarming',
455 True):
Kenneth Russell8a386d42018-06-02 09:48:01456 args = result.get('args', [])
Kenneth Russell5612d64a2018-06-02 21:12:30457 args.append('--gs-results-bucket=chromium-result-details')
Nico Weberd18b8962018-05-16 19:39:38458 if (result['swarming']['can_use_on_swarming_builders'] and not
459 tester_config.get('skip_merge_script', False)):
Kenneth Russelleb60cbd22017-12-05 07:54:28460 result['merge'] = {
461 'args': [
462 '--bucket',
463 'chromium-result-details',
464 '--test-name',
465 test_name
466 ],
Nico Weberd18b8962018-05-16 19:39:38467 'script': '//build/android/pylib/results/presentation/'
Kenneth Russelleb60cbd22017-12-05 07:54:28468 'test_results_presentation.py',
469 } # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:28470 if not tester_config.get('skip_cipd_packages', False):
471 result['swarming']['cipd_packages'] = [
472 {
473 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
474 'location': 'bin',
475 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
476 }
477 ]
Kenneth Russelleb60cbd22017-12-05 07:54:28478 if not tester_config.get('skip_output_links', False):
479 result['swarming']['output_links'] = [
480 {
481 'link': [
482 'https://luci-logdog.appspot.com/v/?s',
483 '=android%2Fswarming%2Flogcats%2F',
484 '${TASK_ID}%2F%2B%2Funified_logcats',
485 ],
486 'name': 'shard #${SHARD_INDEX} logcats',
487 },
488 ]
Kenneth Russell5612d64a2018-06-02 21:12:30489 args.append('--recover-devices')
Kenneth Russell8a386d42018-06-02 09:48:01490 if args:
491 result['args'] = args
Benjamin Pastene766d48f52017-12-18 21:47:42492
Nico Weber79dc5f6852018-07-13 19:38:49493 result = self.update_and_cleanup_test(result, test_name, tester_name)
Shenghua Zhangaba8bad2018-02-07 02:12:09494 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28495 return result
496
497 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
498 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01499 if not self.should_run_on_tester(waterfall, tester_name, test_name,
500 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28501 return None
502 result = copy.deepcopy(test_config)
503 result['isolate_name'] = result.get('isolate_name', test_name)
504 result['name'] = test_name
505 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01506 self.initialize_args_for_test(result, tester_config)
Nico Weber79dc5f6852018-07-13 19:38:49507 result = self.update_and_cleanup_test(result, test_name, tester_name)
Shenghua Zhangaba8bad2018-02-07 02:12:09508 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28509 return result
510
511 def generate_script_test(self, waterfall, tester_name, tester_config,
512 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01513 del tester_config
514 if not self.should_run_on_tester(waterfall, tester_name, test_name,
515 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28516 return None
517 result = {
518 'name': test_name,
519 'script': test_config['script']
520 }
Nico Weber79dc5f6852018-07-13 19:38:49521 result = self.update_and_cleanup_test(result, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28522 return result
523
524 def generate_junit_test(self, waterfall, tester_name, tester_config,
525 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01526 del tester_config
527 if not self.should_run_on_tester(waterfall, tester_name, test_name,
528 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28529 return None
530 result = {
531 'test': test_name,
532 }
533 return result
534
535 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
536 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01537 del tester_config
538 if not self.should_run_on_tester(waterfall, tester_name, test_name,
539 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28540 return None
541 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28542 if 'test' in result and result['test'] != test_name:
543 result['name'] = test_name
544 else:
545 result['test'] = test_name
Nico Weber79dc5f6852018-07-13 19:38:49546 result = self.update_and_cleanup_test(result, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28547 return result
548
Kenneth Russell8a386d42018-06-02 09:48:01549 def substitute_gpu_args(self, tester_config, args):
550 substitutions = {
551 # Any machine in waterfalls.pyl which desires to run GPU tests
552 # must provide the os_type key.
553 'os_type': tester_config['os_type'],
554 'gpu_vendor_id': '0',
555 'gpu_device_id': '0',
556 }
557 dimension_set = tester_config['swarming']['dimension_sets'][0]
558 if 'gpu' in dimension_set:
559 # First remove the driver version, then split into vendor and device.
560 gpu = dimension_set['gpu']
561 gpu = gpu.split('-')[0].split(':')
562 substitutions['gpu_vendor_id'] = gpu[0]
563 substitutions['gpu_device_id'] = gpu[1]
564 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
565
566 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
567 test_name, test_config):
568 # These are all just specializations of isolated script tests with
569 # a bunch of boilerplate command line arguments added.
570
571 # The step name must end in 'test' or 'tests' in order for the
572 # results to automatically show up on the flakiness dashboard.
573 # (At least, this was true some time ago.) Continue to use this
574 # naming convention for the time being to minimize changes.
575 step_name = test_config.get('name', test_name)
576 if not (step_name.endswith('test') or step_name.endswith('tests')):
577 step_name = '%s_tests' % step_name
578 result = self.generate_isolated_script_test(
579 waterfall, tester_name, tester_config, step_name, test_config)
580 if not result:
581 return None
582 result['isolate_name'] = 'telemetry_gpu_integration_test'
583 args = result.get('args', [])
584 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14585
586 # These tests upload and download results from cloud storage and therefore
587 # aren't idempotent yet. https://crbug.com/549140.
588 result['swarming']['idempotent'] = False
589
Kenneth Russell8a386d42018-06-02 09:48:01590 args = [
591 test_to_run,
592 '--show-stdout',
593 '--browser=%s' % tester_config['browser_config'],
594 # --passthrough displays more of the logging in Telemetry when
595 # run via typ, in particular some of the warnings about tests
596 # being expected to fail, but passing.
597 '--passthrough',
598 '-v',
599 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
600 ] + args
601 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
602 tester_config, args))
603 return result
604
Kenneth Russelleb60cbd22017-12-05 07:54:28605 def get_test_generator_map(self):
606 return {
607 'cts_tests': CTSGenerator(self),
Kenneth Russell8a386d42018-06-02 09:48:01608 'gpu_telemetry_tests': GPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28609 'gtest_tests': GTestGenerator(self),
610 'instrumentation_tests': InstrumentationTestGenerator(self),
611 'isolated_scripts': IsolatedScriptTestGenerator(self),
612 'junit_tests': JUnitGenerator(self),
613 'scripts': ScriptGenerator(self),
614 }
615
Kenneth Russell8a386d42018-06-02 09:48:01616 def get_test_type_remapper(self):
617 return {
618 # These are a specialization of isolated_scripts with a bunch of
619 # boilerplate command line arguments added to each one.
620 'gpu_telemetry_tests': 'isolated_scripts',
621 }
622
Kenneth Russelleb60cbd22017-12-05 07:54:28623 def check_composition_test_suites(self):
624 # Pre-pass to catch errors reliably.
625 for name, value in self.test_suites.iteritems():
626 if isinstance(value, list):
627 for entry in value:
628 if isinstance(self.test_suites[entry], list):
Nico Weberd18b8962018-05-16 19:39:38629 raise BBGenErr('Composition test suites may not refer to other '
630 'composition test suites (error found while '
Kenneth Russelleb60cbd22017-12-05 07:54:28631 'processing %s)' % name)
632
633 def resolve_composition_test_suites(self):
634 self.check_composition_test_suites()
635 for name, value in self.test_suites.iteritems():
636 if isinstance(value, list):
637 # Resolve this to a dictionary.
638 full_suite = {}
639 for entry in value:
640 suite = self.test_suites[entry]
641 full_suite.update(suite)
642 self.test_suites[name] = full_suite
643
644 def link_waterfalls_to_test_suites(self):
645 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43646 for tester_name, tester in waterfall['machines'].iteritems():
647 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28648 if not value in self.test_suites:
649 # Hard / impossible to cover this in the unit test.
650 raise self.unknown_test_suite(
651 value, tester_name, waterfall['name']) # pragma: no cover
652 tester['test_suites'][suite] = self.test_suites[value]
653
654 def load_configuration_files(self):
655 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
656 self.test_suites = self.load_pyl_file('test_suites.pyl')
657 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
658
659 def resolve_configuration_files(self):
660 self.resolve_composition_test_suites()
661 self.link_waterfalls_to_test_suites()
662
Nico Weberd18b8962018-05-16 19:39:38663 def unknown_bot(self, bot_name, waterfall_name):
664 return BBGenErr(
665 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
666
Kenneth Russelleb60cbd22017-12-05 07:54:28667 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
668 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38669 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28670 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
671
672 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
673 return BBGenErr(
674 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
675 ' on waterfall ' + waterfall_name)
676
677 def generate_waterfall_json(self, waterfall):
678 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28679 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:01680 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:43681 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28682 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43683 # Copy only well-understood entries in the machine's configuration
684 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28685 if 'additional_compile_targets' in config:
686 tests['additional_compile_targets'] = config[
687 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43688 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28689 if test_type not in generator_map:
690 raise self.unknown_test_suite_type(
691 test_type, name, waterfall['name']) # pragma: no cover
692 test_generator = generator_map[test_type]
Nico Weber79dc5f6852018-07-13 19:38:49693 # Let multiple kinds of generators generate the same kinds
694 # of tests. For example, gpu_telemetry_tests are a
695 # specialization of isolated_scripts.
696 new_tests = test_generator.generate(
697 waterfall, name, config, input_tests)
698 remapped_test_type = test_type_remapper.get(test_type, test_type)
699 tests[remapped_test_type] = test_generator.sort(
700 tests.get(remapped_test_type, []) + new_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28701 all_tests[name] = tests
702 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
703 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
704 return json.dumps(all_tests, indent=2, separators=(',', ': '),
705 sort_keys=True) + '\n'
706
707 def generate_waterfalls(self): # pragma: no cover
708 self.load_configuration_files()
709 self.resolve_configuration_files()
710 filters = self.args.waterfall_filters
711 suffix = '.json'
712 if self.args.new_files:
713 suffix = '.new' + suffix
714 for waterfall in self.waterfalls:
715 should_gen = not filters or waterfall['name'] in filters
716 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:11717 file_path = waterfall['name'] + suffix
718 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:28719 self.generate_waterfall_json(waterfall))
720
Nico Weberd18b8962018-05-16 19:39:38721 def get_valid_bot_names(self):
722 # Extract bot names from infra/config/global/luci-milo.cfg.
723 bot_names = set()
John Budorickc12abd12018-08-14 19:37:43724 infra_config_dir = os.path.abspath(
725 os.path.join(os.path.dirname(__file__),
726 '..', '..', 'infra', 'config', 'global'))
727 milo_configs = [
728 os.path.join(infra_config_dir, 'luci-milo.cfg'),
729 os.path.join(infra_config_dir, 'luci-milo-dev.cfg'),
730 ]
731 for c in milo_configs:
732 for l in self.read_file(c).splitlines():
733 if (not 'name: "buildbucket/luci.chromium.' in l and
734 not 'name: "buildbot/chromium.' in l):
735 continue
736 # l looks like
737 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
738 # Extract win_chromium_dbg_ng part.
739 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:38740 return bot_names
741
Kenneth Russell8a386d42018-06-02 09:48:01742 def get_bots_that_do_not_actually_exist(self):
743 # Some of the bots on the chromium.gpu.fyi waterfall in particular
744 # are defined only to be mirrored into trybots, and don't actually
745 # exist on any of the waterfalls or consoles.
746 return [
747 'Optional Android Release (Nexus 5X)',
748 'Optional Linux Release (Intel HD 630)',
749 'Optional Linux Release (NVIDIA)',
750 'Optional Mac Release (Intel)',
751 'Optional Mac Retina Release (AMD)',
752 'Optional Mac Retina Release (NVIDIA)',
753 'Optional Win10 Release (Intel HD 630)',
754 'Optional Win10 Release (NVIDIA)',
755 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:08756 # chromium.fyi
757 'chromeos-amd64-generic-rel-vm-tests',
Dirk Pranke85369442018-06-16 02:01:29758 'linux-blink-rel-dummy',
759 'mac10.10-blink-rel-dummy',
760 'mac10.11-blink-rel-dummy',
761 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:20762 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:29763 'mac10.13-blink-rel-dummy',
764 'win7-blink-rel-dummy',
765 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:08766 'Dummy WebKit Mac10.13',
767 'WebKit Linux layout_ng Dummy Builder',
768 'WebKit Linux root_layer_scrolls Dummy Builder',
769 'WebKit Linux slimming_paint_v2 Dummy Builder',
Stephen Martinis769b25112018-08-30 18:52:06770 # chromium, due to https://crbug.com/878915
771 'win-dbg',
772 'win32-dbg',
Kenneth Russell8a386d42018-06-02 09:48:01773 ]
774
Kenneth Russelleb60cbd22017-12-05 07:54:28775 def check_input_file_consistency(self):
776 self.load_configuration_files()
777 self.check_composition_test_suites()
Nico Weberd18b8962018-05-16 19:39:38778
779 # All bots should exist.
780 bot_names = self.get_valid_bot_names()
Kenneth Russell8a386d42018-06-02 09:48:01781 bots_that_dont_exist = self.get_bots_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:38782 for waterfall in self.waterfalls:
783 for bot_name in waterfall['machines']:
Kenneth Russell8a386d42018-06-02 09:48:01784 if bot_name in bots_that_dont_exist:
785 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38786 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:08787 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:38788 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:52789 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:32790 if waterfall['name'] in ['tryserver.webrtc',
791 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:38792 # These waterfalls have their bot configs in a different repo.
793 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:52794 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38795 raise self.unknown_bot(bot_name, waterfall['name'])
796
Kenneth Russelleb60cbd22017-12-05 07:54:28797 # All test suites must be referenced.
798 suites_seen = set()
799 generator_map = self.get_test_generator_map()
800 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43801 for bot_name, tester in waterfall['machines'].iteritems():
802 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28803 if suite_type not in generator_map:
804 raise self.unknown_test_suite_type(suite_type, bot_name,
805 waterfall['name'])
806 if suite not in self.test_suites:
807 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
808 suites_seen.add(suite)
809 # Since we didn't resolve the configuration files, this set
810 # includes both composition test suites and regular ones.
811 resolved_suites = set()
812 for suite_name in suites_seen:
813 suite = self.test_suites[suite_name]
814 if isinstance(suite, list):
815 for sub_suite in suite:
816 resolved_suites.add(sub_suite)
817 resolved_suites.add(suite_name)
818 # At this point, every key in test_suites.pyl should be referenced.
819 missing_suites = set(self.test_suites.keys()) - resolved_suites
820 if missing_suites:
821 raise BBGenErr('The following test suites were unreferenced by bots on '
822 'the waterfalls: ' + str(missing_suites))
823
824 # All test suite exceptions must refer to bots on the waterfall.
825 all_bots = set()
826 missing_bots = set()
827 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43828 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28829 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:28830 # In order to disambiguate between bots with the same name on
831 # different waterfalls, support has been added to various
832 # exceptions for concatenating the waterfall name after the bot
833 # name.
834 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28835 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:38836 removals = (exception.get('remove_from', []) +
837 exception.get('remove_gtest_from', []) +
838 exception.get('modifications', {}).keys())
839 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:28840 if removal not in all_bots:
841 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:41842
843 missing_bots = missing_bots - set(bots_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:28844 if missing_bots:
845 raise BBGenErr('The following nonexistent machines were referenced in '
846 'the test suite exceptions: ' + str(missing_bots))
847
848 def check_output_file_consistency(self, verbose=False):
849 self.load_configuration_files()
850 # All waterfalls must have been written by this script already.
851 self.resolve_configuration_files()
852 ungenerated_waterfalls = set()
853 for waterfall in self.waterfalls:
854 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:11855 file_path = waterfall['name'] + '.json'
856 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:28857 if expected != current:
858 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:32859 if verbose: # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28860 print ('Waterfall ' + waterfall['name'] +
861 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:32862 'contents:')
863 for line in difflib.unified_diff(
864 expected.splitlines(),
865 current.splitlines()):
866 print line
Kenneth Russelleb60cbd22017-12-05 07:54:28867 if ungenerated_waterfalls:
868 raise BBGenErr('The following waterfalls have not been properly '
869 'autogenerated by generate_buildbot_json.py: ' +
870 str(ungenerated_waterfalls))
871
872 def check_consistency(self, verbose=False):
873 self.check_input_file_consistency() # pragma: no cover
874 self.check_output_file_consistency(verbose) # pragma: no cover
875
876 def parse_args(self, argv): # pragma: no cover
877 parser = argparse.ArgumentParser()
878 parser.add_argument(
879 '-c', '--check', action='store_true', help=
880 'Do consistency checks of configuration and generated files and then '
881 'exit. Used during presubmit. Causes the tool to not generate any files.')
882 parser.add_argument(
883 '-n', '--new-files', action='store_true', help=
884 'Write output files as .new.json. Useful during development so old and '
885 'new files can be looked at side-by-side.')
886 parser.add_argument(
887 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
888 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:11889 parser.add_argument(
890 '--pyl-files-dir', type=os.path.realpath,
891 help='Path to the directory containing the input .pyl files.')
Kenneth Russelleb60cbd22017-12-05 07:54:28892 self.args = parser.parse_args(argv)
893
894 def main(self, argv): # pragma: no cover
895 self.parse_args(argv)
896 if self.args.check:
897 self.check_consistency()
898 else:
899 self.generate_waterfalls()
900 return 0
901
902if __name__ == "__main__": # pragma: no cover
903 generator = BBJSONGenerator()
904 sys.exit(generator.main(sys.argv[1:]))