blob: 5b9532d791aa9106f4d30d0677a251b99924b0c1 [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 Russell8a386d42018-06-02 09:48:0174class GPUTelemetryTestGenerator(BaseGenerator):
75 def __init__(self, bb_gen):
76 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
77
78 def generate(self, waterfall, tester_name, tester_config, input_tests):
79 isolated_scripts = []
80 for test_name, test_config in sorted(input_tests.iteritems()):
81 test = self.bb_gen.generate_gpu_telemetry_test(
82 waterfall, tester_name, tester_config, test_name, test_config)
83 if test:
84 isolated_scripts.append(test)
85 return isolated_scripts
86
87 def sort(self, tests):
88 return sorted(tests, key=lambda x: x['name'])
89
90
Kenneth Russelleb60cbd22017-12-05 07:54:2891class GTestGenerator(BaseGenerator):
92 def __init__(self, bb_gen):
93 super(GTestGenerator, self).__init__(bb_gen)
94
Kenneth Russell8ceeabf2017-12-11 17:53:2895 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2896 # The relative ordering of some of the tests is important to
97 # minimize differences compared to the handwritten JSON files, since
98 # Python's sorts are stable and there are some tests with the same
99 # key (see gles2_conform_d3d9_test and similar variants). Avoid
100 # losing the order by avoiding coalescing the dictionaries into one.
101 gtests = []
102 for test_name, test_config in sorted(input_tests.iteritems()):
John Budorick826d5ed2017-12-28 19:27:32103 try:
104 test = self.bb_gen.generate_gtest(
105 waterfall, tester_name, tester_config, test_name, test_config)
106 if test:
107 # generate_gtest may veto the test generation on this tester.
108 gtests.append(test)
109 except Exception as e:
110 raise BBGenErr('Failed to generate %s' % test_name, cause=e)
Kenneth Russelleb60cbd22017-12-05 07:54:28111 return gtests
112
113 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28114 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28115
116
117class IsolatedScriptTestGenerator(BaseGenerator):
118 def __init__(self, bb_gen):
119 super(IsolatedScriptTestGenerator, 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 isolated_scripts = []
123 for test_name, test_config in sorted(input_tests.iteritems()):
124 test = self.bb_gen.generate_isolated_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 isolated_scripts.append(test)
128 return isolated_scripts
129
130 def sort(self, tests):
131 return sorted(tests, key=lambda x: x['name'])
132
133
134class ScriptGenerator(BaseGenerator):
135 def __init__(self, bb_gen):
136 super(ScriptGenerator, 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_script_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['name'])
149
150
151class JUnitGenerator(BaseGenerator):
152 def __init__(self, bb_gen):
153 super(JUnitGenerator, 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 scripts = []
157 for test_name, test_config in sorted(input_tests.iteritems()):
158 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28159 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28160 if test:
161 scripts.append(test)
162 return scripts
163
164 def sort(self, tests):
165 return sorted(tests, key=lambda x: x['test'])
166
167
168class CTSGenerator(BaseGenerator):
169 def __init__(self, bb_gen):
170 super(CTSGenerator, self).__init__(bb_gen)
171
Kenneth Russell8ceeabf2017-12-11 17:53:28172 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28173 # These only contain one entry and it's the contents of the input tests'
174 # dictionary, verbatim.
175 cts_tests = []
176 cts_tests.append(input_tests)
177 return cts_tests
178
179 def sort(self, tests):
180 return tests
181
182
183class InstrumentationTestGenerator(BaseGenerator):
184 def __init__(self, bb_gen):
185 super(InstrumentationTestGenerator, self).__init__(bb_gen)
186
Kenneth Russell8ceeabf2017-12-11 17:53:28187 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28188 scripts = []
189 for test_name, test_config in sorted(input_tests.iteritems()):
190 test = self.bb_gen.generate_instrumentation_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28191 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28192 if test:
193 scripts.append(test)
194 return scripts
195
196 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28197 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28198
199
200class BBJSONGenerator(object):
201 def __init__(self):
202 self.this_dir = THIS_DIR
203 self.args = None
204 self.waterfalls = None
205 self.test_suites = None
206 self.exceptions = None
207
208 def generate_abs_file_path(self, relative_path):
209 return os.path.join(self.this_dir, relative_path) # pragma: no cover
210
211 def read_file(self, relative_path):
212 with open(self.generate_abs_file_path(
213 relative_path)) as fp: # pragma: no cover
214 return fp.read() # pragma: no cover
215
216 def write_file(self, relative_path, contents):
217 with open(self.generate_abs_file_path(
218 relative_path), 'wb') as fp: # pragma: no cover
219 fp.write(contents) # pragma: no cover
220
Zhiling Huangbe008172018-03-08 19:13:11221 def pyl_file_path(self, filename):
222 if self.args and self.args.pyl_files_dir:
223 return os.path.join(self.args.pyl_files_dir, filename)
224 return filename
225
Kenneth Russelleb60cbd22017-12-05 07:54:28226 def load_pyl_file(self, filename):
227 try:
Zhiling Huangbe008172018-03-08 19:13:11228 return ast.literal_eval(self.read_file(
229 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28230 except (SyntaxError, ValueError) as e: # pragma: no cover
231 raise BBGenErr('Failed to parse pyl file "%s": %s' %
232 (filename, e)) # pragma: no cover
233
Kenneth Russell8a386d42018-06-02 09:48:01234 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
235 # Currently it is only mandatory for bots which run GPU tests. Change these to
236 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28237 def is_android(self, tester_config):
238 return tester_config.get('os_type') == 'android'
239
Kenneth Russell8a386d42018-06-02 09:48:01240 def is_linux(self, tester_config):
241 return tester_config.get('os_type') == 'linux'
242
Kenneth Russelleb60cbd22017-12-05 07:54:28243 def get_exception_for_test(self, test_name, test_config):
244 # gtests may have both "test" and "name" fields, and usually, if the "name"
245 # field is specified, it means that the same test is being repurposed
246 # multiple times with different command line arguments. To handle this case,
247 # prefer to lookup per the "name" field of the test itself, as opposed to
248 # the "test_name", which is actually the "test" field.
249 if 'name' in test_config:
250 return self.exceptions.get(test_config['name'])
251 else:
252 return self.exceptions.get(test_name)
253
Kenneth Russell8a386d42018-06-02 09:48:01254 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config,
255 test_suite_type=None):
Kenneth Russelleb60cbd22017-12-05 07:54:28256 # Currently, the only reason a test should not run on a given tester is that
257 # it's in the exceptions. (Once the GPU waterfall generation script is
258 # incorporated here, the rules will become more complex.)
259 exception = self.get_exception_for_test(test_name, test_config)
260 if not exception:
261 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28262 remove_from = None
263 if test_suite_type:
264 # First look for a specific removal for the test suite type,
265 # e.g. 'remove_gtest_from'.
266 remove_from = exception.get('remove_' + test_suite_type + '_from')
267 if remove_from and tester_name in remove_from:
268 # TODO(kbr): add coverage.
269 return False # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28270 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28271 if remove_from:
272 if tester_name in remove_from:
273 return False
274 # TODO(kbr): this code path was added for some tests (including
275 # android_webview_unittests) on one machine (Nougat Phone
276 # Tester) which exists with the same name on two waterfalls,
277 # chromium.android and chromium.fyi; the tests are run on one
278 # but not the other. Once the bots are all uniquely named (a
279 # different ongoing project) this code should be removed.
280 # TODO(kbr): add coverage.
281 return (tester_name + ' ' + waterfall['name']
282 not in remove_from) # pragma: no cover
283 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28284
Kenneth Russell8ceeabf2017-12-11 17:53:28285 def get_test_modifications(self, test, test_name, tester_name, waterfall):
Kenneth Russelleb60cbd22017-12-05 07:54:28286 exception = self.get_exception_for_test(test_name, test)
287 if not exception:
288 return None
Kenneth Russell8ceeabf2017-12-11 17:53:28289 mods = exception.get('modifications', {}).get(tester_name)
290 if mods:
291 return mods
292 # TODO(kbr): this code path was added for exactly one test
293 # (cronet_test_instrumentation_apk) on a few bots on
294 # chromium.android.fyi. Once the bots are all uniquely named (a
295 # different ongoing project) this code should be removed.
296 return exception.get('modifications', {}).get(tester_name + ' ' +
297 waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28298
299 def get_test_key_removals(self, test_name, tester_name):
300 exception = self.exceptions.get(test_name)
301 if not exception:
302 return []
303 return exception.get('key_removals', {}).get(tester_name, [])
304
Kenneth Russell8a386d42018-06-02 09:48:01305 def merge_command_line_args(self, arr, prefix, splitter):
306 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01307 idx = 0
308 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01309 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01310 while idx < len(arr):
311 flag = arr[idx]
312 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01313 if flag.startswith(prefix):
314 arg = flag[prefix_len:]
315 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01316 if first_idx < 0:
317 first_idx = idx
318 else:
319 delete_current_entry = True
320 if delete_current_entry:
321 del arr[idx]
322 else:
323 idx += 1
324 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01325 arr[first_idx] = prefix + splitter.join(accumulated_args)
326 return arr
327
328 def maybe_fixup_args_array(self, arr):
329 # The incoming array of strings may be an array of command line
330 # arguments. To make it easier to turn on certain features per-bot or
331 # per-test-suite, look specifically for certain flags and merge them
332 # appropriately.
333 # --enable-features=Feature1 --enable-features=Feature2
334 # are merged to:
335 # --enable-features=Feature1,Feature2
336 # and:
337 # --extra-browser-args=arg1 --extra-browser-args=arg2
338 # are merged to:
339 # --extra-browser-args=arg1 arg2
340 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
341 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01342 return arr
343
Kenneth Russelleb60cbd22017-12-05 07:54:28344 def dictionary_merge(self, a, b, path=None, update=True):
345 """http://stackoverflow.com/questions/7204805/
346 python-dictionaries-of-dictionaries-merge
347 merges b into a
348 """
349 if path is None:
350 path = []
351 for key in b:
352 if key in a:
353 if isinstance(a[key], dict) and isinstance(b[key], dict):
354 self.dictionary_merge(a[key], b[key], path + [str(key)])
355 elif a[key] == b[key]:
356 pass # same leaf value
357 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06358 # Args arrays are lists of strings. Just concatenate them,
359 # and don't sort them, in order to keep some needed
360 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28361 if all(isinstance(x, str)
362 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01363 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28364 else:
365 # TODO(kbr): this only works properly if the two arrays are
366 # the same length, which is currently always the case in the
367 # swarming dimension_sets that we have to merge. It will fail
368 # to merge / override 'args' arrays which are different
369 # length.
370 for idx in xrange(len(b[key])):
371 try:
372 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
373 path + [str(key), str(idx)],
374 update=update)
375 except (IndexError, TypeError): # pragma: no cover
376 raise BBGenErr('Error merging list keys ' + str(key) +
377 ' and indices ' + str(idx) + ' between ' +
378 str(a) + ' and ' + str(b)) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28379 elif update: # pragma: no cover
380 a[key] = b[key] # pragma: no cover
381 else:
382 raise BBGenErr('Conflict at %s' % '.'.join(
383 path + [str(key)])) # pragma: no cover
384 else:
385 a[key] = b[key]
386 return a
387
John Budorickedfe7f872018-01-23 15:27:22388 def initialize_args_for_test(self, generated_test, tester_config):
Kenneth Russell650995a2018-05-03 21:17:01389 if 'args' in tester_config or 'args' in generated_test:
390 generated_test['args'] = self.maybe_fixup_args_array(
391 generated_test.get('args', []) + tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22392
Kenneth Russell8a386d42018-06-02 09:48:01393 def add_conditional_args(key, fn):
394 if key in generated_test:
395 if fn(tester_config):
396 if not 'args' in generated_test:
397 generated_test['args'] = []
398 generated_test['args'] += generated_test[key]
399 # Don't put the conditional args in the JSON.
400 generated_test.pop(key)
401
402 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
403 add_conditional_args('linux_args', self.is_linux)
404 add_conditional_args('android_args', self.is_android)
405
406
Kenneth Russelleb60cbd22017-12-05 07:54:28407 def initialize_swarming_dictionary_for_test(self, generated_test,
408 tester_config):
409 if 'swarming' not in generated_test:
410 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28411 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
412 generated_test['swarming'].update({
413 'can_use_on_swarming_builders': tester_config.get('use_swarming', True)
414 })
Kenneth Russelleb60cbd22017-12-05 07:54:28415 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03416 if ('dimension_sets' not in generated_test['swarming'] and
417 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28418 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
419 tester_config['swarming']['dimension_sets'])
420 self.dictionary_merge(generated_test['swarming'],
421 tester_config['swarming'])
422 # Apply any Android-specific Swarming dimensions after the generic ones.
423 if 'android_swarming' in generated_test:
424 if self.is_android(tester_config): # pragma: no cover
425 self.dictionary_merge(
426 generated_test['swarming'],
427 generated_test['android_swarming']) # pragma: no cover
428 del generated_test['android_swarming'] # pragma: no cover
429
430 def clean_swarming_dictionary(self, swarming_dict):
431 # Clean out redundant entries from a test's "swarming" dictionary.
432 # This is really only needed to retain 100% parity with the
433 # handwritten JSON files, and can be removed once all the files are
434 # autogenerated.
435 if 'shards' in swarming_dict:
436 if swarming_dict['shards'] == 1: # pragma: no cover
437 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24438 if 'hard_timeout' in swarming_dict:
439 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
440 del swarming_dict['hard_timeout'] # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28441 if not swarming_dict['can_use_on_swarming_builders']:
442 # Remove all other keys.
443 for k in swarming_dict.keys(): # pragma: no cover
444 if k != 'can_use_on_swarming_builders': # pragma: no cover
445 del swarming_dict[k] # pragma: no cover
446
Kenneth Russell8ceeabf2017-12-11 17:53:28447 def update_and_cleanup_test(self, test, test_name, tester_name, waterfall):
Kenneth Russelleb60cbd22017-12-05 07:54:28448 # See if there are any exceptions that need to be merged into this
449 # test's specification.
Kenneth Russell8ceeabf2017-12-11 17:53:28450 modifications = self.get_test_modifications(test, test_name, tester_name,
451 waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28452 if modifications:
453 test = self.dictionary_merge(test, modifications)
454 for k in self.get_test_key_removals(test_name, tester_name):
455 del test[k]
Dirk Pranke1b767092017-12-07 04:44:23456 if 'swarming' in test:
457 self.clean_swarming_dictionary(test['swarming'])
Kenneth Russelleb60cbd22017-12-05 07:54:28458 return test
459
Shenghua Zhangaba8bad2018-02-07 02:12:09460 def add_common_test_properties(self, test, tester_config):
461 if tester_config.get('use_multi_dimension_trigger_script'):
462 test['trigger_script'] = {
463 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
464 'args': [
465 '--multiple-trigger-configs',
466 json.dumps(tester_config['swarming']['dimension_sets'] +
467 tester_config.get('alternate_swarming_dimensions', [])),
468 '--multiple-dimension-script-verbose',
469 'True'
470 ],
471 }
472
Kenneth Russelleb60cbd22017-12-05 07:54:28473 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
474 test_config):
475 if not self.should_run_on_tester(
Kenneth Russell8a386d42018-06-02 09:48:01476 waterfall, tester_name, test_name, test_config,
Kenneth Russell8ceeabf2017-12-11 17:53:28477 TestSuiteTypes.GTEST):
Kenneth Russelleb60cbd22017-12-05 07:54:28478 return None
479 result = copy.deepcopy(test_config)
480 if 'test' in result:
481 result['name'] = test_name
482 else:
483 result['test'] = test_name
484 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickedfe7f872018-01-23 15:27:22485 self.initialize_args_for_test(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28486 if self.is_android(tester_config) and tester_config.get('use_swarming',
487 True):
Kenneth Russell8a386d42018-06-02 09:48:01488 args = result.get('args', [])
Kenneth Russell5612d64a2018-06-02 21:12:30489 args.append('--gs-results-bucket=chromium-result-details')
Nico Weberd18b8962018-05-16 19:39:38490 if (result['swarming']['can_use_on_swarming_builders'] and not
491 tester_config.get('skip_merge_script', False)):
Kenneth Russelleb60cbd22017-12-05 07:54:28492 result['merge'] = {
493 'args': [
494 '--bucket',
495 'chromium-result-details',
496 '--test-name',
497 test_name
498 ],
Nico Weberd18b8962018-05-16 19:39:38499 'script': '//build/android/pylib/results/presentation/'
Kenneth Russelleb60cbd22017-12-05 07:54:28500 'test_results_presentation.py',
501 } # pragma: no cover
Kenneth Russell8ceeabf2017-12-11 17:53:28502 if not tester_config.get('skip_cipd_packages', False):
503 result['swarming']['cipd_packages'] = [
504 {
505 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
506 'location': 'bin',
507 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
508 }
509 ]
Kenneth Russelleb60cbd22017-12-05 07:54:28510 if not tester_config.get('skip_output_links', False):
511 result['swarming']['output_links'] = [
512 {
513 'link': [
514 'https://luci-logdog.appspot.com/v/?s',
515 '=android%2Fswarming%2Flogcats%2F',
516 '${TASK_ID}%2F%2B%2Funified_logcats',
517 ],
518 'name': 'shard #${SHARD_INDEX} logcats',
519 },
520 ]
Kenneth Russell5612d64a2018-06-02 21:12:30521 args.append('--recover-devices')
Kenneth Russell8a386d42018-06-02 09:48:01522 if args:
523 result['args'] = args
Benjamin Pastene766d48f52017-12-18 21:47:42524
Kenneth Russell8ceeabf2017-12-11 17:53:28525 result = self.update_and_cleanup_test(result, test_name, tester_name,
526 waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09527 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28528 return result
529
530 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
531 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01532 if not self.should_run_on_tester(waterfall, tester_name, test_name,
533 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28534 return None
535 result = copy.deepcopy(test_config)
536 result['isolate_name'] = result.get('isolate_name', test_name)
537 result['name'] = test_name
538 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01539 self.initialize_args_for_test(result, tester_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28540 result = self.update_and_cleanup_test(result, test_name, tester_name,
541 waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09542 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28543 return result
544
545 def generate_script_test(self, waterfall, tester_name, tester_config,
546 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01547 del tester_config
548 if not self.should_run_on_tester(waterfall, tester_name, test_name,
549 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28550 return None
551 result = {
552 'name': test_name,
553 'script': test_config['script']
554 }
Kenneth Russell8ceeabf2017-12-11 17:53:28555 result = self.update_and_cleanup_test(result, test_name, tester_name,
556 waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28557 return result
558
559 def generate_junit_test(self, waterfall, tester_name, tester_config,
560 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01561 del tester_config
562 if not self.should_run_on_tester(waterfall, tester_name, test_name,
563 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28564 return None
565 result = {
566 'test': test_name,
567 }
568 return result
569
570 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
571 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01572 del tester_config
573 if not self.should_run_on_tester(waterfall, tester_name, test_name,
574 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28575 return None
576 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28577 if 'test' in result and result['test'] != test_name:
578 result['name'] = test_name
579 else:
580 result['test'] = test_name
581 result = self.update_and_cleanup_test(result, test_name, tester_name,
582 waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28583 return result
584
Kenneth Russell8a386d42018-06-02 09:48:01585 def substitute_gpu_args(self, tester_config, args):
586 substitutions = {
587 # Any machine in waterfalls.pyl which desires to run GPU tests
588 # must provide the os_type key.
589 'os_type': tester_config['os_type'],
590 'gpu_vendor_id': '0',
591 'gpu_device_id': '0',
592 }
593 dimension_set = tester_config['swarming']['dimension_sets'][0]
594 if 'gpu' in dimension_set:
595 # First remove the driver version, then split into vendor and device.
596 gpu = dimension_set['gpu']
597 gpu = gpu.split('-')[0].split(':')
598 substitutions['gpu_vendor_id'] = gpu[0]
599 substitutions['gpu_device_id'] = gpu[1]
600 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
601
602 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
603 test_name, test_config):
604 # These are all just specializations of isolated script tests with
605 # a bunch of boilerplate command line arguments added.
606
607 # The step name must end in 'test' or 'tests' in order for the
608 # results to automatically show up on the flakiness dashboard.
609 # (At least, this was true some time ago.) Continue to use this
610 # naming convention for the time being to minimize changes.
611 step_name = test_config.get('name', test_name)
612 if not (step_name.endswith('test') or step_name.endswith('tests')):
613 step_name = '%s_tests' % step_name
614 result = self.generate_isolated_script_test(
615 waterfall, tester_name, tester_config, step_name, test_config)
616 if not result:
617 return None
618 result['isolate_name'] = 'telemetry_gpu_integration_test'
619 args = result.get('args', [])
620 test_to_run = result.pop('telemetry_test_name', test_name)
621 args = [
622 test_to_run,
623 '--show-stdout',
624 '--browser=%s' % tester_config['browser_config'],
625 # --passthrough displays more of the logging in Telemetry when
626 # run via typ, in particular some of the warnings about tests
627 # being expected to fail, but passing.
628 '--passthrough',
629 '-v',
630 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
631 ] + args
632 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
633 tester_config, args))
634 return result
635
Kenneth Russelleb60cbd22017-12-05 07:54:28636 def get_test_generator_map(self):
637 return {
638 'cts_tests': CTSGenerator(self),
Kenneth Russell8a386d42018-06-02 09:48:01639 'gpu_telemetry_tests': GPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28640 'gtest_tests': GTestGenerator(self),
641 'instrumentation_tests': InstrumentationTestGenerator(self),
642 'isolated_scripts': IsolatedScriptTestGenerator(self),
643 'junit_tests': JUnitGenerator(self),
644 'scripts': ScriptGenerator(self),
645 }
646
Kenneth Russell8a386d42018-06-02 09:48:01647 def get_test_type_remapper(self):
648 return {
649 # These are a specialization of isolated_scripts with a bunch of
650 # boilerplate command line arguments added to each one.
651 'gpu_telemetry_tests': 'isolated_scripts',
652 }
653
Kenneth Russelleb60cbd22017-12-05 07:54:28654 def check_composition_test_suites(self):
655 # Pre-pass to catch errors reliably.
656 for name, value in self.test_suites.iteritems():
657 if isinstance(value, list):
658 for entry in value:
659 if isinstance(self.test_suites[entry], list):
Nico Weberd18b8962018-05-16 19:39:38660 raise BBGenErr('Composition test suites may not refer to other '
661 'composition test suites (error found while '
Kenneth Russelleb60cbd22017-12-05 07:54:28662 'processing %s)' % name)
663
664 def resolve_composition_test_suites(self):
665 self.check_composition_test_suites()
666 for name, value in self.test_suites.iteritems():
667 if isinstance(value, list):
668 # Resolve this to a dictionary.
669 full_suite = {}
670 for entry in value:
671 suite = self.test_suites[entry]
672 full_suite.update(suite)
673 self.test_suites[name] = full_suite
674
675 def link_waterfalls_to_test_suites(self):
676 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43677 for tester_name, tester in waterfall['machines'].iteritems():
678 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28679 if not value in self.test_suites:
680 # Hard / impossible to cover this in the unit test.
681 raise self.unknown_test_suite(
682 value, tester_name, waterfall['name']) # pragma: no cover
683 tester['test_suites'][suite] = self.test_suites[value]
684
685 def load_configuration_files(self):
686 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
687 self.test_suites = self.load_pyl_file('test_suites.pyl')
688 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
689
690 def resolve_configuration_files(self):
691 self.resolve_composition_test_suites()
692 self.link_waterfalls_to_test_suites()
693
John Budorick826d5ed2017-12-28 19:27:32694 def generation_error(self, suite_type, bot_name, waterfall_name, cause):
695 return BBGenErr(
696 'Failed to generate %s from %s:%s' % (
697 suite_type, waterfall_name, bot_name),
698 cause=cause)
699
Nico Weberd18b8962018-05-16 19:39:38700 def unknown_bot(self, bot_name, waterfall_name):
701 return BBGenErr(
702 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
703
Kenneth Russelleb60cbd22017-12-05 07:54:28704 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
705 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38706 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28707 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
708
709 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
710 return BBGenErr(
711 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
712 ' on waterfall ' + waterfall_name)
713
714 def generate_waterfall_json(self, waterfall):
715 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28716 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:01717 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:43718 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28719 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43720 # Copy only well-understood entries in the machine's configuration
721 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28722 if 'additional_compile_targets' in config:
723 tests['additional_compile_targets'] = config[
724 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43725 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28726 if test_type not in generator_map:
727 raise self.unknown_test_suite_type(
728 test_type, name, waterfall['name']) # pragma: no cover
729 test_generator = generator_map[test_type]
John Budorick826d5ed2017-12-28 19:27:32730 try:
Kenneth Russell8a386d42018-06-02 09:48:01731 # Let multiple kinds of generators generate the same kinds
732 # of tests. For example, gpu_telemetry_tests are a
733 # specialization of isolated_scripts.
734 new_tests = test_generator.generate(
735 waterfall, name, config, input_tests)
736 remapped_test_type = test_type_remapper.get(test_type, test_type)
737 tests[remapped_test_type] = test_generator.sort(
738 tests.get(remapped_test_type, []) + new_tests)
John Budorick826d5ed2017-12-28 19:27:32739 except Exception as e:
740 raise self.generation_error(test_type, name, waterfall['name'], e)
Kenneth Russelleb60cbd22017-12-05 07:54:28741 all_tests[name] = tests
742 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
743 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
744 return json.dumps(all_tests, indent=2, separators=(',', ': '),
745 sort_keys=True) + '\n'
746
747 def generate_waterfalls(self): # pragma: no cover
748 self.load_configuration_files()
749 self.resolve_configuration_files()
750 filters = self.args.waterfall_filters
751 suffix = '.json'
752 if self.args.new_files:
753 suffix = '.new' + suffix
754 for waterfall in self.waterfalls:
755 should_gen = not filters or waterfall['name'] in filters
756 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:11757 file_path = waterfall['name'] + suffix
758 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:28759 self.generate_waterfall_json(waterfall))
760
Nico Weberd18b8962018-05-16 19:39:38761 def get_valid_bot_names(self):
762 # Extract bot names from infra/config/global/luci-milo.cfg.
763 bot_names = set()
Kenneth Russell78fd8702018-05-17 01:15:52764 for l in self.read_file(os.path.join(
765 '..', '..', 'infra', 'config', 'global', 'luci-milo.cfg')).splitlines():
Nico Weberd18b8962018-05-16 19:39:38766 if (not 'name: "buildbucket/luci.chromium.' in l and
767 not 'name: "buildbot/chromium.' in l):
768 continue
769 # l looks like `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
770 # Extract win_chromium_dbg_ng part.
771 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
772 return bot_names
773
Kenneth Russell8a386d42018-06-02 09:48:01774 def get_bots_that_do_not_actually_exist(self):
775 # Some of the bots on the chromium.gpu.fyi waterfall in particular
776 # are defined only to be mirrored into trybots, and don't actually
777 # exist on any of the waterfalls or consoles.
778 return [
779 'Optional Android Release (Nexus 5X)',
780 'Optional Linux Release (Intel HD 630)',
781 'Optional Linux Release (NVIDIA)',
782 'Optional Mac Release (Intel)',
783 'Optional Mac Retina Release (AMD)',
784 'Optional Mac Retina Release (NVIDIA)',
785 'Optional Win10 Release (Intel HD 630)',
786 'Optional Win10 Release (NVIDIA)',
787 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:08788 # chromium.android.fyi
789 'Unswarmed N5 Tests Dummy Builder',
790 'Unswarmed N5X Tests Dummy Builder',
791 # chromium.fyi
792 'chromeos-amd64-generic-rel-vm-tests',
793 'Dummy WebKit Mac10.13',
794 'WebKit Linux layout_ng Dummy Builder',
795 'WebKit Linux root_layer_scrolls Dummy Builder',
796 'WebKit Linux slimming_paint_v2 Dummy Builder',
Kenneth Russell8a386d42018-06-02 09:48:01797 ]
798
Kenneth Russelleb60cbd22017-12-05 07:54:28799 def check_input_file_consistency(self):
800 self.load_configuration_files()
801 self.check_composition_test_suites()
Nico Weberd18b8962018-05-16 19:39:38802
803 # All bots should exist.
804 bot_names = self.get_valid_bot_names()
Kenneth Russell8a386d42018-06-02 09:48:01805 bots_that_dont_exist = self.get_bots_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:38806 for waterfall in self.waterfalls:
807 for bot_name in waterfall['machines']:
Kenneth Russell8a386d42018-06-02 09:48:01808 if bot_name in bots_that_dont_exist:
809 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38810 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:08811 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:38812 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:52813 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38814 if waterfall['name'] in ['tryserver.webrtc']:
815 # These waterfalls have their bot configs in a different repo.
816 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:52817 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38818 raise self.unknown_bot(bot_name, waterfall['name'])
819
Kenneth Russelleb60cbd22017-12-05 07:54:28820 # All test suites must be referenced.
821 suites_seen = set()
822 generator_map = self.get_test_generator_map()
823 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43824 for bot_name, tester in waterfall['machines'].iteritems():
825 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28826 if suite_type not in generator_map:
827 raise self.unknown_test_suite_type(suite_type, bot_name,
828 waterfall['name'])
829 if suite not in self.test_suites:
830 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
831 suites_seen.add(suite)
832 # Since we didn't resolve the configuration files, this set
833 # includes both composition test suites and regular ones.
834 resolved_suites = set()
835 for suite_name in suites_seen:
836 suite = self.test_suites[suite_name]
837 if isinstance(suite, list):
838 for sub_suite in suite:
839 resolved_suites.add(sub_suite)
840 resolved_suites.add(suite_name)
841 # At this point, every key in test_suites.pyl should be referenced.
842 missing_suites = set(self.test_suites.keys()) - resolved_suites
843 if missing_suites:
844 raise BBGenErr('The following test suites were unreferenced by bots on '
845 'the waterfalls: ' + str(missing_suites))
846
847 # All test suite exceptions must refer to bots on the waterfall.
848 all_bots = set()
849 missing_bots = set()
850 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43851 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28852 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:28853 # In order to disambiguate between bots with the same name on
854 # different waterfalls, support has been added to various
855 # exceptions for concatenating the waterfall name after the bot
856 # name.
857 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:28858 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:38859 removals = (exception.get('remove_from', []) +
860 exception.get('remove_gtest_from', []) +
861 exception.get('modifications', {}).keys())
862 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:28863 if removal not in all_bots:
864 missing_bots.add(removal)
Kenneth Russelleb60cbd22017-12-05 07:54:28865 if missing_bots:
866 raise BBGenErr('The following nonexistent machines were referenced in '
867 'the test suite exceptions: ' + str(missing_bots))
868
869 def check_output_file_consistency(self, verbose=False):
870 self.load_configuration_files()
871 # All waterfalls must have been written by this script already.
872 self.resolve_configuration_files()
873 ungenerated_waterfalls = set()
874 for waterfall in self.waterfalls:
875 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:11876 file_path = waterfall['name'] + '.json'
877 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:28878 if expected != current:
879 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:32880 if verbose: # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28881 print ('Waterfall ' + waterfall['name'] +
882 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:32883 'contents:')
884 for line in difflib.unified_diff(
885 expected.splitlines(),
886 current.splitlines()):
887 print line
Kenneth Russelleb60cbd22017-12-05 07:54:28888 if ungenerated_waterfalls:
889 raise BBGenErr('The following waterfalls have not been properly '
890 'autogenerated by generate_buildbot_json.py: ' +
891 str(ungenerated_waterfalls))
892
893 def check_consistency(self, verbose=False):
894 self.check_input_file_consistency() # pragma: no cover
895 self.check_output_file_consistency(verbose) # pragma: no cover
896
897 def parse_args(self, argv): # pragma: no cover
898 parser = argparse.ArgumentParser()
899 parser.add_argument(
900 '-c', '--check', action='store_true', help=
901 'Do consistency checks of configuration and generated files and then '
902 'exit. Used during presubmit. Causes the tool to not generate any files.')
903 parser.add_argument(
904 '-n', '--new-files', action='store_true', help=
905 'Write output files as .new.json. Useful during development so old and '
906 'new files can be looked at side-by-side.')
907 parser.add_argument(
908 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
909 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:11910 parser.add_argument(
911 '--pyl-files-dir', type=os.path.realpath,
912 help='Path to the directory containing the input .pyl files.')
Kenneth Russelleb60cbd22017-12-05 07:54:28913 self.args = parser.parse_args(argv)
914
915 def main(self, argv): # pragma: no cover
916 self.parse_args(argv)
917 if self.args.check:
918 self.check_consistency()
919 else:
920 self.generate_waterfalls()
921 return 0
922
923if __name__ == "__main__": # pragma: no cover
924 generator = BBJSONGenerator()
925 sys.exit(generator.main(sys.argv[1:]))