blob: 453eee4a7564b0656981e75126db1c3973eb9dc6 [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):
Bo Liu555a0f92019-03-29 12:11:5666
67 def __init__(self, bb_gen, is_android_webview=False):
Kenneth Russell8a386d42018-06-02 09:48:0168 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
Bo Liu555a0f92019-03-29 12:11:5669 self._is_android_webview = is_android_webview
Kenneth Russell8a386d42018-06-02 09:48:0170
71 def generate(self, waterfall, tester_name, tester_config, input_tests):
72 isolated_scripts = []
73 for test_name, test_config in sorted(input_tests.iteritems()):
74 test = self.bb_gen.generate_gpu_telemetry_test(
Bo Liu555a0f92019-03-29 12:11:5675 waterfall, tester_name, tester_config, test_name, test_config,
76 self._is_android_webview)
Kenneth Russell8a386d42018-06-02 09:48:0177 if test:
78 isolated_scripts.append(test)
79 return isolated_scripts
80
81 def sort(self, tests):
82 return sorted(tests, key=lambda x: x['name'])
83
84
Kenneth Russelleb60cbd22017-12-05 07:54:2885class GTestGenerator(BaseGenerator):
86 def __init__(self, bb_gen):
87 super(GTestGenerator, self).__init__(bb_gen)
88
Kenneth Russell8ceeabf2017-12-11 17:53:2889 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2890 # The relative ordering of some of the tests is important to
91 # minimize differences compared to the handwritten JSON files, since
92 # Python's sorts are stable and there are some tests with the same
93 # key (see gles2_conform_d3d9_test and similar variants). Avoid
94 # losing the order by avoiding coalescing the dictionaries into one.
95 gtests = []
96 for test_name, test_config in sorted(input_tests.iteritems()):
Nico Weber79dc5f6852018-07-13 19:38:4997 test = self.bb_gen.generate_gtest(
98 waterfall, tester_name, tester_config, test_name, test_config)
99 if test:
100 # generate_gtest may veto the test generation on this tester.
101 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:28102 return gtests
103
104 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28105 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28106
107
108class IsolatedScriptTestGenerator(BaseGenerator):
109 def __init__(self, bb_gen):
110 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
111
Kenneth Russell8ceeabf2017-12-11 17:53:28112 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28113 isolated_scripts = []
114 for test_name, test_config in sorted(input_tests.iteritems()):
115 test = self.bb_gen.generate_isolated_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28116 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28117 if test:
118 isolated_scripts.append(test)
119 return isolated_scripts
120
121 def sort(self, tests):
122 return sorted(tests, key=lambda x: x['name'])
123
124
125class ScriptGenerator(BaseGenerator):
126 def __init__(self, bb_gen):
127 super(ScriptGenerator, self).__init__(bb_gen)
128
Kenneth Russell8ceeabf2017-12-11 17:53:28129 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28130 scripts = []
131 for test_name, test_config in sorted(input_tests.iteritems()):
132 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28133 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28134 if test:
135 scripts.append(test)
136 return scripts
137
138 def sort(self, tests):
139 return sorted(tests, key=lambda x: x['name'])
140
141
142class JUnitGenerator(BaseGenerator):
143 def __init__(self, bb_gen):
144 super(JUnitGenerator, self).__init__(bb_gen)
145
Kenneth Russell8ceeabf2017-12-11 17:53:28146 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28147 scripts = []
148 for test_name, test_config in sorted(input_tests.iteritems()):
149 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28150 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28151 if test:
152 scripts.append(test)
153 return scripts
154
155 def sort(self, tests):
156 return sorted(tests, key=lambda x: x['test'])
157
158
159class CTSGenerator(BaseGenerator):
160 def __init__(self, bb_gen):
161 super(CTSGenerator, self).__init__(bb_gen)
162
Kenneth Russell8ceeabf2017-12-11 17:53:28163 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28164 # These only contain one entry and it's the contents of the input tests'
165 # dictionary, verbatim.
166 cts_tests = []
167 cts_tests.append(input_tests)
168 return cts_tests
169
170 def sort(self, tests):
171 return tests
172
173
174class InstrumentationTestGenerator(BaseGenerator):
175 def __init__(self, bb_gen):
176 super(InstrumentationTestGenerator, self).__init__(bb_gen)
177
Kenneth Russell8ceeabf2017-12-11 17:53:28178 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28179 scripts = []
180 for test_name, test_config in sorted(input_tests.iteritems()):
181 test = self.bb_gen.generate_instrumentation_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28182 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28183 if test:
184 scripts.append(test)
185 return scripts
186
187 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28188 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28189
190
191class BBJSONGenerator(object):
192 def __init__(self):
193 self.this_dir = THIS_DIR
194 self.args = None
195 self.waterfalls = None
196 self.test_suites = None
197 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01198 self.mixins = None
Kenneth Russelleb60cbd22017-12-05 07:54:28199
200 def generate_abs_file_path(self, relative_path):
201 return os.path.join(self.this_dir, relative_path) # pragma: no cover
202
Stephen Martinis7eb8b612018-09-21 00:17:50203 def print_line(self, line):
204 # Exists so that tests can mock
205 print line # pragma: no cover
206
Kenneth Russelleb60cbd22017-12-05 07:54:28207 def read_file(self, relative_path):
208 with open(self.generate_abs_file_path(
209 relative_path)) as fp: # pragma: no cover
210 return fp.read() # pragma: no cover
211
212 def write_file(self, relative_path, contents):
213 with open(self.generate_abs_file_path(
214 relative_path), 'wb') as fp: # pragma: no cover
215 fp.write(contents) # pragma: no cover
216
Zhiling Huangbe008172018-03-08 19:13:11217 def pyl_file_path(self, filename):
218 if self.args and self.args.pyl_files_dir:
219 return os.path.join(self.args.pyl_files_dir, filename)
220 return filename
221
Kenneth Russelleb60cbd22017-12-05 07:54:28222 def load_pyl_file(self, filename):
223 try:
Zhiling Huangbe008172018-03-08 19:13:11224 return ast.literal_eval(self.read_file(
225 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28226 except (SyntaxError, ValueError) as e: # pragma: no cover
227 raise BBGenErr('Failed to parse pyl file "%s": %s' %
228 (filename, e)) # pragma: no cover
229
Kenneth Russell8a386d42018-06-02 09:48:01230 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
231 # Currently it is only mandatory for bots which run GPU tests. Change these to
232 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28233 def is_android(self, tester_config):
234 return tester_config.get('os_type') == 'android'
235
Ben Pastenea9e583b2019-01-16 02:57:26236 def is_chromeos(self, tester_config):
237 return tester_config.get('os_type') == 'chromeos'
238
Kenneth Russell8a386d42018-06-02 09:48:01239 def is_linux(self, tester_config):
240 return tester_config.get('os_type') == 'linux'
241
Kai Ninomiya40de9f52019-10-18 21:38:49242 def is_mac(self, tester_config):
243 return tester_config.get('os_type') == 'mac'
244
245 def is_win(self, tester_config):
246 return tester_config.get('os_type') == 'win'
247
248 def is_win64(self, tester_config):
249 return (tester_config.get('os_type') == 'win' and
250 tester_config.get('browser_config') == 'release_x64')
251
Kenneth Russelleb60cbd22017-12-05 07:54:28252 def get_exception_for_test(self, test_name, test_config):
253 # gtests may have both "test" and "name" fields, and usually, if the "name"
254 # field is specified, it means that the same test is being repurposed
255 # multiple times with different command line arguments. To handle this case,
256 # prefer to lookup per the "name" field of the test itself, as opposed to
257 # the "test_name", which is actually the "test" field.
258 if 'name' in test_config:
259 return self.exceptions.get(test_config['name'])
260 else:
261 return self.exceptions.get(test_name)
262
Nico Weberb0b3f5862018-07-13 18:45:15263 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28264 # Currently, the only reason a test should not run on a given tester is that
265 # it's in the exceptions. (Once the GPU waterfall generation script is
266 # incorporated here, the rules will become more complex.)
267 exception = self.get_exception_for_test(test_name, test_config)
268 if not exception:
269 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28270 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28271 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28272 if remove_from:
273 if tester_name in remove_from:
274 return False
275 # TODO(kbr): this code path was added for some tests (including
276 # android_webview_unittests) on one machine (Nougat Phone
277 # Tester) which exists with the same name on two waterfalls,
278 # chromium.android and chromium.fyi; the tests are run on one
279 # but not the other. Once the bots are all uniquely named (a
280 # different ongoing project) this code should be removed.
281 # TODO(kbr): add coverage.
282 return (tester_name + ' ' + waterfall['name']
283 not in remove_from) # pragma: no cover
284 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28285
Nico Weber79dc5f6852018-07-13 19:38:49286 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28287 exception = self.get_exception_for_test(test_name, test)
288 if not exception:
289 return None
Nico Weber79dc5f6852018-07-13 19:38:49290 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28291
Brian Sheedye6ea0ee2019-07-11 02:54:37292 def get_test_replacements(self, test, test_name, tester_name):
293 exception = self.get_exception_for_test(test_name, test)
294 if not exception:
295 return None
296 return exception.get('replacements', {}).get(tester_name)
297
Kenneth Russell8a386d42018-06-02 09:48:01298 def merge_command_line_args(self, arr, prefix, splitter):
299 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01300 idx = 0
301 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01302 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01303 while idx < len(arr):
304 flag = arr[idx]
305 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01306 if flag.startswith(prefix):
307 arg = flag[prefix_len:]
308 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01309 if first_idx < 0:
310 first_idx = idx
311 else:
312 delete_current_entry = True
313 if delete_current_entry:
314 del arr[idx]
315 else:
316 idx += 1
317 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01318 arr[first_idx] = prefix + splitter.join(accumulated_args)
319 return arr
320
321 def maybe_fixup_args_array(self, arr):
322 # The incoming array of strings may be an array of command line
323 # arguments. To make it easier to turn on certain features per-bot or
324 # per-test-suite, look specifically for certain flags and merge them
325 # appropriately.
326 # --enable-features=Feature1 --enable-features=Feature2
327 # are merged to:
328 # --enable-features=Feature1,Feature2
329 # and:
330 # --extra-browser-args=arg1 --extra-browser-args=arg2
331 # are merged to:
332 # --extra-browser-args=arg1 arg2
333 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
334 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01335 return arr
336
Kenneth Russelleb60cbd22017-12-05 07:54:28337 def dictionary_merge(self, a, b, path=None, update=True):
338 """http://stackoverflow.com/questions/7204805/
339 python-dictionaries-of-dictionaries-merge
340 merges b into a
341 """
342 if path is None:
343 path = []
344 for key in b:
345 if key in a:
346 if isinstance(a[key], dict) and isinstance(b[key], dict):
347 self.dictionary_merge(a[key], b[key], path + [str(key)])
348 elif a[key] == b[key]:
349 pass # same leaf value
350 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06351 # Args arrays are lists of strings. Just concatenate them,
352 # and don't sort them, in order to keep some needed
353 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28354 if all(isinstance(x, str)
355 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01356 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28357 else:
358 # TODO(kbr): this only works properly if the two arrays are
359 # the same length, which is currently always the case in the
360 # swarming dimension_sets that we have to merge. It will fail
361 # to merge / override 'args' arrays which are different
362 # length.
363 for idx in xrange(len(b[key])):
364 try:
365 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
366 path + [str(key), str(idx)],
367 update=update)
368 except (IndexError, TypeError): # pragma: no cover
369 raise BBGenErr('Error merging list keys ' + str(key) +
370 ' and indices ' + str(idx) + ' between ' +
371 str(a) + ' and ' + str(b)) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53372 elif update:
373 if b[key] is None:
374 del a[key]
375 else:
376 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28377 else:
378 raise BBGenErr('Conflict at %s' % '.'.join(
379 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53380 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28381 a[key] = b[key]
382 return a
383
John Budorickab108712018-09-01 00:12:21384 def initialize_args_for_test(
385 self, generated_test, tester_config, additional_arg_keys=None):
386
387 args = []
388 args.extend(generated_test.get('args', []))
389 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22390
Kenneth Russell8a386d42018-06-02 09:48:01391 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21392 val = generated_test.pop(key, [])
393 if fn(tester_config):
394 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01395
396 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
397 add_conditional_args('linux_args', self.is_linux)
398 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36399 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49400 add_conditional_args('mac_args', self.is_mac)
401 add_conditional_args('win_args', self.is_win)
402 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01403
John Budorickab108712018-09-01 00:12:21404 for key in additional_arg_keys or []:
405 args.extend(generated_test.pop(key, []))
406 args.extend(tester_config.get(key, []))
407
408 if args:
409 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01410
Kenneth Russelleb60cbd22017-12-05 07:54:28411 def initialize_swarming_dictionary_for_test(self, generated_test,
412 tester_config):
413 if 'swarming' not in generated_test:
414 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28415 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
416 generated_test['swarming'].update({
417 'can_use_on_swarming_builders': tester_config.get('use_swarming', True)
418 })
Kenneth Russelleb60cbd22017-12-05 07:54:28419 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03420 if ('dimension_sets' not in generated_test['swarming'] and
421 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28422 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
423 tester_config['swarming']['dimension_sets'])
424 self.dictionary_merge(generated_test['swarming'],
425 tester_config['swarming'])
426 # Apply any Android-specific Swarming dimensions after the generic ones.
427 if 'android_swarming' in generated_test:
428 if self.is_android(tester_config): # pragma: no cover
429 self.dictionary_merge(
430 generated_test['swarming'],
431 generated_test['android_swarming']) # pragma: no cover
432 del generated_test['android_swarming'] # pragma: no cover
433
434 def clean_swarming_dictionary(self, swarming_dict):
435 # Clean out redundant entries from a test's "swarming" dictionary.
436 # This is really only needed to retain 100% parity with the
437 # handwritten JSON files, and can be removed once all the files are
438 # autogenerated.
439 if 'shards' in swarming_dict:
440 if swarming_dict['shards'] == 1: # pragma: no cover
441 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24442 if 'hard_timeout' in swarming_dict:
443 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
444 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43445 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28446 # Remove all other keys.
447 for k in swarming_dict.keys(): # pragma: no cover
448 if k != 'can_use_on_swarming_builders': # pragma: no cover
449 del swarming_dict[k] # pragma: no cover
450
Stephen Martinis0382bc12018-09-17 22:29:07451 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
452 waterfall):
453 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01454 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07455 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28456 # See if there are any exceptions that need to be merged into this
457 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49458 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28459 if modifications:
460 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23461 if 'swarming' in test:
462 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28463 # Ensure all Android Swarming tests run only on userdebug builds if another
464 # build type was not specified.
465 if 'swarming' in test and self.is_android(tester_config):
466 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22467 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28468 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37469 self.replace_test_args(test, test_name, tester_name)
Ben Pastenee012aea42019-05-14 22:32:28470
Kenneth Russelleb60cbd22017-12-05 07:54:28471 return test
472
Brian Sheedye6ea0ee2019-07-11 02:54:37473 def replace_test_args(self, test, test_name, tester_name):
474 replacements = self.get_test_replacements(
475 test, test_name, tester_name) or {}
476 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
477 for key, replacement_dict in replacements.iteritems():
478 if key not in valid_replacement_keys:
479 raise BBGenErr(
480 'Given replacement key %s for %s on %s is not in the list of valid '
481 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
482 for replacement_key, replacement_val in replacement_dict.iteritems():
483 found_key = False
484 for i, test_key in enumerate(test.get(key, [])):
485 # Handle both the key/value being replaced being defined as two
486 # separate items or as key=value.
487 if test_key == replacement_key:
488 found_key = True
489 # Handle flags without values.
490 if replacement_val == None:
491 del test[key][i]
492 else:
493 test[key][i+1] = replacement_val
494 break
495 elif test_key.startswith(replacement_key + '='):
496 found_key = True
497 if replacement_val == None:
498 del test[key][i]
499 else:
500 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
501 break
502 if not found_key:
503 raise BBGenErr('Could not find %s in existing list of values for key '
504 '%s in %s on %s' % (replacement_key, key, test_name,
505 tester_name))
506
Shenghua Zhangaba8bad2018-02-07 02:12:09507 def add_common_test_properties(self, test, tester_config):
508 if tester_config.get('use_multi_dimension_trigger_script'):
Kenneth Russell73c3bd8b2018-10-19 22:30:19509 # Assumes update_and_cleanup_test has already been called, so the
510 # builder's mixins have been flattened into the test.
Shenghua Zhangaba8bad2018-02-07 02:12:09511 test['trigger_script'] = {
512 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
513 'args': [
514 '--multiple-trigger-configs',
Kenneth Russell73c3bd8b2018-10-19 22:30:19515 json.dumps(test['swarming']['dimension_sets'] +
Shenghua Zhangaba8bad2018-02-07 02:12:09516 tester_config.get('alternate_swarming_dimensions', [])),
517 '--multiple-dimension-script-verbose',
518 'True'
519 ],
520 }
Ben Pastenea9e583b2019-01-16 02:57:26521 elif self.is_chromeos(tester_config) and tester_config.get('use_swarming',
522 True):
523 # The presence of the "device_type" dimension indicates that the tests
524 # are targetting CrOS hardware and so need the special trigger script.
525 dimension_sets = tester_config['swarming']['dimension_sets']
526 if all('device_type' in ds for ds in dimension_sets):
527 test['trigger_script'] = {
528 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
529 }
Shenghua Zhangaba8bad2018-02-07 02:12:09530
Ben Pastene858f4be2019-01-09 23:52:09531 def add_android_presentation_args(self, tester_config, test_name, result):
532 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38533 bucket = tester_config.get('results_bucket', 'chromium-result-details')
534 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09535 if (result['swarming']['can_use_on_swarming_builders'] and not
536 tester_config.get('skip_merge_script', False)):
537 result['merge'] = {
538 'args': [
539 '--bucket',
John Budorick262ae112019-07-12 19:24:38540 bucket,
Ben Pastene858f4be2019-01-09 23:52:09541 '--test-name',
542 test_name
543 ],
544 'script': '//build/android/pylib/results/presentation/'
545 'test_results_presentation.py',
546 }
547 if not tester_config.get('skip_cipd_packages', False):
Ben Pastenee5949ea82019-01-10 21:45:26548 cipd_packages = result['swarming'].get('cipd_packages', [])
549 cipd_packages.append(
Ben Pastene858f4be2019-01-09 23:52:09550 {
551 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
552 'location': 'bin',
553 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
554 }
Ben Pastenee5949ea82019-01-10 21:45:26555 )
556 result['swarming']['cipd_packages'] = cipd_packages
Ben Pastene858f4be2019-01-09 23:52:09557 if not tester_config.get('skip_output_links', False):
558 result['swarming']['output_links'] = [
559 {
560 'link': [
561 'https://luci-logdog.appspot.com/v/?s',
562 '=android%2Fswarming%2Flogcats%2F',
563 '${TASK_ID}%2F%2B%2Funified_logcats',
564 ],
565 'name': 'shard #${SHARD_INDEX} logcats',
566 },
567 ]
568 if args:
569 result['args'] = args
570
Kenneth Russelleb60cbd22017-12-05 07:54:28571 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
572 test_config):
573 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15574 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28575 return None
576 result = copy.deepcopy(test_config)
577 if 'test' in result:
578 result['name'] = test_name
579 else:
580 result['test'] = test_name
581 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21582
583 self.initialize_args_for_test(
584 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28585 if self.is_android(tester_config) and tester_config.get('use_swarming',
586 True):
Ben Pastene858f4be2019-01-09 23:52:09587 self.add_android_presentation_args(tester_config, test_name, result)
588 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42589
Stephen Martinis0382bc12018-09-17 22:29:07590 result = self.update_and_cleanup_test(
591 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09592 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43593
594 if not result.get('merge'):
595 # TODO(https://crbug.com/958376): Consider adding the ability to not have
596 # this default.
597 result['merge'] = {
598 'script': '//testing/merge_scripts/standard_gtest_merge.py',
599 'args': [],
600 }
Kenneth Russelleb60cbd22017-12-05 07:54:28601 return result
602
603 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
604 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01605 if not self.should_run_on_tester(waterfall, tester_name, test_name,
606 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28607 return None
608 result = copy.deepcopy(test_config)
609 result['isolate_name'] = result.get('isolate_name', test_name)
610 result['name'] = test_name
611 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01612 self.initialize_args_for_test(result, tester_config)
Ben Pastene858f4be2019-01-09 23:52:09613 if tester_config.get('use_android_presentation', False):
614 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07615 result = self.update_and_cleanup_test(
616 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09617 self.add_common_test_properties(result, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17618
619 if not result.get('merge'):
620 # TODO(https://crbug.com/958376): Consider adding the ability to not have
621 # this default.
622 result['merge'] = {
623 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
624 'args': [],
625 }
Kenneth Russelleb60cbd22017-12-05 07:54:28626 return result
627
628 def generate_script_test(self, waterfall, tester_name, tester_config,
629 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44630 # TODO(https://crbug.com/953072): Remove this check whenever a better
631 # long-term solution is implemented.
632 if (waterfall.get('forbid_script_tests', False) or
633 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
634 raise BBGenErr('Attempted to generate a script test on tester ' +
635 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01636 if not self.should_run_on_tester(waterfall, tester_name, test_name,
637 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28638 return None
639 result = {
640 'name': test_name,
641 'script': test_config['script']
642 }
Stephen Martinis0382bc12018-09-17 22:29:07643 result = self.update_and_cleanup_test(
644 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28645 return result
646
647 def generate_junit_test(self, waterfall, tester_name, tester_config,
648 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01649 if not self.should_run_on_tester(waterfall, tester_name, test_name,
650 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28651 return None
John Budorickdef6acb2019-09-17 22:51:09652 result = copy.deepcopy(test_config)
653 result.update({
John Budorickcadc4952019-09-16 23:51:37654 'name': test_name,
655 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09656 })
657 self.initialize_args_for_test(result, tester_config)
658 result = self.update_and_cleanup_test(
659 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28660 return result
661
662 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
663 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01664 if not self.should_run_on_tester(waterfall, tester_name, test_name,
665 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28666 return None
667 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28668 if 'test' in result and result['test'] != test_name:
669 result['name'] = test_name
670 else:
671 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07672 result = self.update_and_cleanup_test(
673 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28674 return result
675
Stephen Martinis2a0667022018-09-25 22:31:14676 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01677 substitutions = {
678 # Any machine in waterfalls.pyl which desires to run GPU tests
679 # must provide the os_type key.
680 'os_type': tester_config['os_type'],
681 'gpu_vendor_id': '0',
682 'gpu_device_id': '0',
683 }
Stephen Martinis2a0667022018-09-25 22:31:14684 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01685 if 'gpu' in dimension_set:
686 # First remove the driver version, then split into vendor and device.
687 gpu = dimension_set['gpu']
Kenneth Russell384a1732019-03-16 02:36:02688 # Handle certain specialized named GPUs.
689 if gpu.startswith('nvidia-quadro-p400'):
690 gpu = ['10de', '1cb3']
691 elif gpu.startswith('intel-hd-630'):
692 gpu = ['8086', '5912']
Brian Sheedyf9387db7b2019-08-05 19:26:10693 elif gpu.startswith('intel-uhd-630'):
694 gpu = ['8086', '3e92']
Kenneth Russell384a1732019-03-16 02:36:02695 else:
696 gpu = gpu.split('-')[0].split(':')
Kenneth Russell8a386d42018-06-02 09:48:01697 substitutions['gpu_vendor_id'] = gpu[0]
698 substitutions['gpu_device_id'] = gpu[1]
699 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
700
701 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56702 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01703 # These are all just specializations of isolated script tests with
704 # a bunch of boilerplate command line arguments added.
705
706 # The step name must end in 'test' or 'tests' in order for the
707 # results to automatically show up on the flakiness dashboard.
708 # (At least, this was true some time ago.) Continue to use this
709 # naming convention for the time being to minimize changes.
710 step_name = test_config.get('name', test_name)
711 if not (step_name.endswith('test') or step_name.endswith('tests')):
712 step_name = '%s_tests' % step_name
713 result = self.generate_isolated_script_test(
714 waterfall, tester_name, tester_config, step_name, test_config)
715 if not result:
716 return None
717 result['isolate_name'] = 'telemetry_gpu_integration_test'
718 args = result.get('args', [])
719 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14720
721 # These tests upload and download results from cloud storage and therefore
722 # aren't idempotent yet. https://crbug.com/549140.
723 result['swarming']['idempotent'] = False
724
Kenneth Russell44910c32018-12-03 23:35:11725 # The GPU tests act much like integration tests for the entire browser, and
726 # tend to uncover flakiness bugs more readily than other test suites. In
727 # order to surface any flakiness more readily to the developer of the CL
728 # which is introducing it, we disable retries with patch on the commit
729 # queue.
730 result['should_retry_with_patch'] = False
731
Bo Liu555a0f92019-03-29 12:11:56732 browser = ('android-webview-instrumentation'
733 if is_android_webview else tester_config['browser_config'])
Kenneth Russell8a386d42018-06-02 09:48:01734 args = [
Bo Liu555a0f92019-03-29 12:11:56735 test_to_run,
736 '--show-stdout',
737 '--browser=%s' % browser,
738 # --passthrough displays more of the logging in Telemetry when
739 # run via typ, in particular some of the warnings about tests
740 # being expected to fail, but passing.
741 '--passthrough',
742 '-v',
743 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
Kenneth Russell8a386d42018-06-02 09:48:01744 ] + args
745 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14746 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01747 return result
748
Kenneth Russelleb60cbd22017-12-05 07:54:28749 def get_test_generator_map(self):
750 return {
Bo Liu555a0f92019-03-29 12:11:56751 'android_webview_gpu_telemetry_tests':
752 GPUTelemetryTestGenerator(self, is_android_webview=True),
753 'cts_tests':
754 CTSGenerator(self),
755 'gpu_telemetry_tests':
756 GPUTelemetryTestGenerator(self),
757 'gtest_tests':
758 GTestGenerator(self),
759 'instrumentation_tests':
760 InstrumentationTestGenerator(self),
761 'isolated_scripts':
762 IsolatedScriptTestGenerator(self),
763 'junit_tests':
764 JUnitGenerator(self),
765 'scripts':
766 ScriptGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28767 }
768
Kenneth Russell8a386d42018-06-02 09:48:01769 def get_test_type_remapper(self):
770 return {
771 # These are a specialization of isolated_scripts with a bunch of
772 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:56773 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:01774 'gpu_telemetry_tests': 'isolated_scripts',
775 }
776
Kenneth Russelleb60cbd22017-12-05 07:54:28777 def check_composition_test_suites(self):
778 # Pre-pass to catch errors reliably.
Andrew Luo0f1dee02019-09-06 16:50:47779 for suite, suite_def in self.test_suites.iteritems():
780 if isinstance(suite_def, list):
781 seen_tests = {}
782 for sub_suite in suite_def:
783 if isinstance(self.test_suites[sub_suite], list):
Nico Weberd18b8962018-05-16 19:39:38784 raise BBGenErr('Composition test suites may not refer to other '
785 'composition test suites (error found while '
Andrew Luo0f1dee02019-09-06 16:50:47786 'processing %s)' % suite)
787 else:
788 # test name -> basic_suite that it came from
789 basic_tests = {k: sub_suite for k in self.test_suites[sub_suite]}
790 for test_name, test_suite in basic_tests.iteritems():
791 if (test_name in seen_tests and
792 self.test_suites[test_suite][test_name] !=
793 self.test_suites[seen_tests[test_name]][test_name]):
794 raise BBGenErr('Conflicting test definitions for %s from %s '
795 'and %s in Composition test suite (error found '
796 'while processing %s)' % (test_name,
797 seen_tests[test_name], test_suite, suite))
798 seen_tests.update(basic_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28799
Stephen Martinis54d64ad2018-09-21 22:16:20800 def flatten_test_suites(self):
801 new_test_suites = {}
802 for name, value in self.test_suites.get('basic_suites', {}).iteritems():
803 new_test_suites[name] = value
804 for name, value in self.test_suites.get('compound_suites', {}).iteritems():
805 if name in new_test_suites:
806 raise BBGenErr('Composition test suite names may not duplicate basic '
807 'test suite names (error found while processsing %s' % (
808 name))
809 new_test_suites[name] = value
810 self.test_suites = new_test_suites
811
Kenneth Russelleb60cbd22017-12-05 07:54:28812 def resolve_composition_test_suites(self):
Stephen Martinis54d64ad2018-09-21 22:16:20813 self.flatten_test_suites()
814
Kenneth Russelleb60cbd22017-12-05 07:54:28815 self.check_composition_test_suites()
816 for name, value in self.test_suites.iteritems():
817 if isinstance(value, list):
818 # Resolve this to a dictionary.
819 full_suite = {}
820 for entry in value:
821 suite = self.test_suites[entry]
822 full_suite.update(suite)
823 self.test_suites[name] = full_suite
824
825 def link_waterfalls_to_test_suites(self):
826 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43827 for tester_name, tester in waterfall['machines'].iteritems():
828 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28829 if not value in self.test_suites:
830 # Hard / impossible to cover this in the unit test.
831 raise self.unknown_test_suite(
832 value, tester_name, waterfall['name']) # pragma: no cover
833 tester['test_suites'][suite] = self.test_suites[value]
834
835 def load_configuration_files(self):
836 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
837 self.test_suites = self.load_pyl_file('test_suites.pyl')
838 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:01839 self.mixins = self.load_pyl_file('mixins.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:28840
841 def resolve_configuration_files(self):
842 self.resolve_composition_test_suites()
843 self.link_waterfalls_to_test_suites()
844
Nico Weberd18b8962018-05-16 19:39:38845 def unknown_bot(self, bot_name, waterfall_name):
846 return BBGenErr(
847 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
848
Kenneth Russelleb60cbd22017-12-05 07:54:28849 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
850 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38851 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28852 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
853
854 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
855 return BBGenErr(
856 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
857 ' on waterfall ' + waterfall_name)
858
Stephen Martinisb72f6d22018-10-04 23:29:01859 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:07860 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:32861
862 Checks in the waterfall, builder, and test objects for mixins.
863 """
864 def valid_mixin(mixin_name):
865 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:01866 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:32867 raise BBGenErr("bad mixin %s" % mixin_name)
868 def must_be_list(mixins, typ, name):
869 """Asserts that given mixins are a list."""
870 if not isinstance(mixins, list):
871 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
872
Stephen Martinisb72f6d22018-10-04 23:29:01873 if 'mixins' in waterfall:
874 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
875 for mixin in waterfall['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32876 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01877 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32878
Stephen Martinisb72f6d22018-10-04 23:29:01879 if 'mixins' in builder:
880 must_be_list(builder['mixins'], 'builder', builder_name)
881 for mixin in builder['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32882 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01883 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32884
Stephen Martinisb72f6d22018-10-04 23:29:01885 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:07886 return test
887
Stephen Martinis2a0667022018-09-25 22:31:14888 test_name = test.get('name')
889 if not test_name:
890 test_name = test.get('test')
891 if not test_name: # pragma: no cover
892 # Not the best name, but we should say something.
893 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:01894 must_be_list(test['mixins'], 'test', test_name)
895 for mixin in test['mixins']:
Stephen Martinis0382bc12018-09-17 22:29:07896 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01897 test = self.apply_mixin(self.mixins[mixin], test)
898 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:07899 return test
Stephen Martinisb6a50492018-09-12 23:59:32900
Stephen Martinisb72f6d22018-10-04 23:29:01901 def apply_mixin(self, mixin, test):
902 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:32903
Stephen Martinis0382bc12018-09-17 22:29:07904 Mixins will not override an existing key. This is to ensure exceptions can
905 override a setting a mixin applies.
906
Stephen Martinisb72f6d22018-10-04 23:29:01907 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:32908 'dimension_sets', which is how normal test suites specify their dimensions,
909 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
910 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:01911
Stephen Martinisb6a50492018-09-12 23:59:32912 """
913 new_test = copy.deepcopy(test)
914 mixin = copy.deepcopy(mixin)
915
Stephen Martinisb72f6d22018-10-04 23:29:01916 if 'swarming' in mixin:
917 swarming_mixin = mixin['swarming']
918 new_test.setdefault('swarming', {})
919 if 'dimensions' in swarming_mixin:
920 new_test['swarming'].setdefault('dimension_sets', [{}])
921 for dimension_set in new_test['swarming']['dimension_sets']:
922 dimension_set.update(swarming_mixin['dimensions'])
923 del swarming_mixin['dimensions']
Stephen Martinisb6a50492018-09-12 23:59:32924
Stephen Martinisb72f6d22018-10-04 23:29:01925 # python dict update doesn't do recursion at all. Just hard code the
926 # nested update we need (mixin['swarming'] shouldn't clobber
927 # test['swarming'], but should update it).
928 new_test['swarming'].update(swarming_mixin)
929 del mixin['swarming']
930
Wezc0e835b702018-10-30 00:38:41931 if '$mixin_append' in mixin:
932 # Values specified under $mixin_append should be appended to existing
933 # lists, rather than replacing them.
934 mixin_append = mixin['$mixin_append']
935 for key in mixin_append:
936 new_test.setdefault(key, [])
937 if not isinstance(mixin_append[key], list):
938 raise BBGenErr(
939 'Key "' + key + '" in $mixin_append must be a list.')
940 if not isinstance(new_test[key], list):
941 raise BBGenErr(
942 'Cannot apply $mixin_append to non-list "' + key + '".')
943 new_test[key].extend(mixin_append[key])
944 if 'args' in mixin_append:
945 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
946 del mixin['$mixin_append']
947
Stephen Martinisb72f6d22018-10-04 23:29:01948 new_test.update(mixin)
Stephen Martinis0382bc12018-09-17 22:29:07949
Stephen Martinisb6a50492018-09-12 23:59:32950 return new_test
951
Kenneth Russelleb60cbd22017-12-05 07:54:28952 def generate_waterfall_json(self, waterfall):
953 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28954 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:01955 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:43956 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28957 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43958 # Copy only well-understood entries in the machine's configuration
959 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28960 if 'additional_compile_targets' in config:
961 tests['additional_compile_targets'] = config[
962 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43963 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28964 if test_type not in generator_map:
965 raise self.unknown_test_suite_type(
966 test_type, name, waterfall['name']) # pragma: no cover
967 test_generator = generator_map[test_type]
Nico Weber79dc5f6852018-07-13 19:38:49968 # Let multiple kinds of generators generate the same kinds
969 # of tests. For example, gpu_telemetry_tests are a
970 # specialization of isolated_scripts.
971 new_tests = test_generator.generate(
972 waterfall, name, config, input_tests)
973 remapped_test_type = test_type_remapper.get(test_type, test_type)
974 tests[remapped_test_type] = test_generator.sort(
975 tests.get(remapped_test_type, []) + new_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28976 all_tests[name] = tests
977 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
978 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
979 return json.dumps(all_tests, indent=2, separators=(',', ': '),
980 sort_keys=True) + '\n'
981
982 def generate_waterfalls(self): # pragma: no cover
983 self.load_configuration_files()
984 self.resolve_configuration_files()
985 filters = self.args.waterfall_filters
986 suffix = '.json'
987 if self.args.new_files:
988 suffix = '.new' + suffix
989 for waterfall in self.waterfalls:
990 should_gen = not filters or waterfall['name'] in filters
991 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:11992 file_path = waterfall['name'] + suffix
993 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:28994 self.generate_waterfall_json(waterfall))
995
Nico Weberd18b8962018-05-16 19:39:38996 def get_valid_bot_names(self):
John Budorick699282e2019-02-13 01:27:33997 # Extract bot names from infra/config/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:42998 # NOTE: This reference can cause issues; if a file changes there, the
999 # presubmit here won't be run by default. A manually maintained list there
1000 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1001 # references to configs outside of this directory are added, please change
1002 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1003 # never ends up in an invalid state.
Nico Weberd18b8962018-05-16 19:39:381004 bot_names = set()
John Budorickc12abd12018-08-14 19:37:431005 infra_config_dir = os.path.abspath(
1006 os.path.join(os.path.dirname(__file__),
John Budorick699282e2019-02-13 01:27:331007 '..', '..', 'infra', 'config'))
John Budorickc12abd12018-08-14 19:37:431008 milo_configs = [
Garrett Beatybb8322bf2019-10-17 20:53:051009 os.path.join(infra_config_dir, 'generated', 'luci-milo.cfg'),
John Budorickc12abd12018-08-14 19:37:431010 os.path.join(infra_config_dir, 'luci-milo-dev.cfg'),
1011 ]
1012 for c in milo_configs:
1013 for l in self.read_file(c).splitlines():
1014 if (not 'name: "buildbucket/luci.chromium.' in l and
Hans Wennborg98ffd7d92019-02-06 14:14:341015 not 'name: "buildbucket/luci.chrome.' in l and
John Budorickb1833612018-12-07 04:36:411016 not 'name: "buildbot/chromium.' in l and
1017 not 'name: "buildbot/tryserver.chromium.' in l):
John Budorickc12abd12018-08-14 19:37:431018 continue
1019 # l looks like
1020 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1021 # Extract win_chromium_dbg_ng part.
1022 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381023 return bot_names
1024
Ben Pastene9a010082019-09-25 20:41:371025 def get_builders_that_do_not_actually_exist(self):
Kenneth Russell8a386d42018-06-02 09:48:011026 # Some of the bots on the chromium.gpu.fyi waterfall in particular
1027 # are defined only to be mirrored into trybots, and don't actually
1028 # exist on any of the waterfalls or consoles.
1029 return [
Michael Spangeb07eba62019-05-14 22:22:581030 'GPU FYI Fuchsia Builder',
Yuly Novikoveb26b812019-07-26 02:08:191031 'ANGLE GPU Android Release (Nexus 5X)',
Jamie Madillda894ce2019-04-08 17:19:171032 'ANGLE GPU Linux Release (Intel HD 630)',
1033 'ANGLE GPU Linux Release (NVIDIA)',
1034 'ANGLE GPU Mac Release (Intel)',
1035 'ANGLE GPU Mac Retina Release (AMD)',
1036 'ANGLE GPU Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491037 'ANGLE GPU Win10 x64 Release (Intel HD 630)',
1038 'ANGLE GPU Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011039 'Optional Android Release (Nexus 5X)',
1040 'Optional Linux Release (Intel HD 630)',
1041 'Optional Linux Release (NVIDIA)',
1042 'Optional Mac Release (Intel)',
1043 'Optional Mac Retina Release (AMD)',
1044 'Optional Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491045 'Optional Win10 x64 Release (Intel HD 630)',
1046 'Optional Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011047 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:081048 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:291049 'linux-blink-rel-dummy',
1050 'mac10.10-blink-rel-dummy',
1051 'mac10.11-blink-rel-dummy',
1052 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:201053 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291054 'mac10.13-blink-rel-dummy',
1055 'win7-blink-rel-dummy',
1056 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:081057 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:331058 'WebKit Linux composite_after_paint Dummy Builder',
Scott Violet744e04662019-08-19 23:51:531059 'WebKit Linux layout_ng_disabled Builder',
Stephen Martinis769b25112018-08-30 18:52:061060 # chromium, due to https://crbug.com/878915
1061 'win-dbg',
1062 'win32-dbg',
Stephen Martinis47d77132019-04-24 23:51:331063 'win-archive-dbg',
1064 'win32-archive-dbg',
Kenneth Russell8a386d42018-06-02 09:48:011065 ]
1066
Ben Pastene9a010082019-09-25 20:41:371067 def get_internal_waterfalls(self):
1068 # Similar to get_builders_that_do_not_actually_exist above, but for
1069 # waterfalls defined in internal configs.
1070 return ['chrome']
1071
Stephen Martinisf83893722018-09-19 00:02:181072 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201073 self.check_input_files_sorting(verbose)
1074
Kenneth Russelleb60cbd22017-12-05 07:54:281075 self.load_configuration_files()
Stephen Martinis54d64ad2018-09-21 22:16:201076 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281077 self.check_composition_test_suites()
Nico Weberd18b8962018-05-16 19:39:381078
1079 # All bots should exist.
1080 bot_names = self.get_valid_bot_names()
Ben Pastene9a010082019-09-25 20:41:371081 internal_waterfalls = self.get_internal_waterfalls()
1082 builders_that_dont_exist = self.get_builders_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:381083 for waterfall in self.waterfalls:
Ben Pastene9a010082019-09-25 20:41:371084 # TODO(crbug.com/991417): Remove the need for this exception.
1085 if waterfall['name'] in internal_waterfalls:
1086 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381087 for bot_name in waterfall['machines']:
Ben Pastene9a010082019-09-25 20:41:371088 if bot_name in builders_that_dont_exist:
Kenneth Russell8a386d42018-06-02 09:48:011089 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381090 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:081091 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:381092 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:521093 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:321094 if waterfall['name'] in ['tryserver.webrtc',
1095 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:381096 # These waterfalls have their bot configs in a different repo.
1097 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:521098 continue # pragma: no cover
Tamer Tas2c506412019-08-20 07:44:411099 if waterfall['name'] in ['client.devtools-frontend.integration']:
1100 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381101 raise self.unknown_bot(bot_name, waterfall['name'])
1102
Kenneth Russelleb60cbd22017-12-05 07:54:281103 # All test suites must be referenced.
1104 suites_seen = set()
1105 generator_map = self.get_test_generator_map()
1106 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431107 for bot_name, tester in waterfall['machines'].iteritems():
1108 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281109 if suite_type not in generator_map:
1110 raise self.unknown_test_suite_type(suite_type, bot_name,
1111 waterfall['name'])
1112 if suite not in self.test_suites:
1113 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1114 suites_seen.add(suite)
1115 # Since we didn't resolve the configuration files, this set
1116 # includes both composition test suites and regular ones.
1117 resolved_suites = set()
1118 for suite_name in suites_seen:
1119 suite = self.test_suites[suite_name]
1120 if isinstance(suite, list):
1121 for sub_suite in suite:
1122 resolved_suites.add(sub_suite)
1123 resolved_suites.add(suite_name)
1124 # At this point, every key in test_suites.pyl should be referenced.
1125 missing_suites = set(self.test_suites.keys()) - resolved_suites
1126 if missing_suites:
1127 raise BBGenErr('The following test suites were unreferenced by bots on '
1128 'the waterfalls: ' + str(missing_suites))
1129
1130 # All test suite exceptions must refer to bots on the waterfall.
1131 all_bots = set()
1132 missing_bots = set()
1133 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431134 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281135 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281136 # In order to disambiguate between bots with the same name on
1137 # different waterfalls, support has been added to various
1138 # exceptions for concatenating the waterfall name after the bot
1139 # name.
1140 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281141 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381142 removals = (exception.get('remove_from', []) +
1143 exception.get('remove_gtest_from', []) +
1144 exception.get('modifications', {}).keys())
1145 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281146 if removal not in all_bots:
1147 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411148
Ben Pastene9a010082019-09-25 20:41:371149 missing_bots = missing_bots - set(builders_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281150 if missing_bots:
1151 raise BBGenErr('The following nonexistent machines were referenced in '
1152 'the test suite exceptions: ' + str(missing_bots))
1153
Stephen Martinis0382bc12018-09-17 22:29:071154 # All mixins must be referenced
1155 seen_mixins = set()
1156 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011157 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071158 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011159 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071160 for suite in self.test_suites.values():
1161 if isinstance(suite, list):
1162 # Don't care about this, it's a composition, which shouldn't include a
1163 # swarming mixin.
1164 continue
1165
1166 for test in suite.values():
1167 if not isinstance(test, dict):
1168 # Some test suites have top level keys, which currently can't be
1169 # swarming mixin entries. Ignore them
1170 continue
1171
Stephen Martinisb72f6d22018-10-04 23:29:011172 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071173
Stephen Martinisb72f6d22018-10-04 23:29:011174 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071175 if missing_mixins:
1176 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1177 ' referenced in a waterfall, machine, or test suite.' % (
1178 str(missing_mixins)))
1179
Stephen Martinis54d64ad2018-09-21 22:16:201180
1181 def type_assert(self, node, typ, filename, verbose=False):
1182 """Asserts that the Python AST node |node| is of type |typ|.
1183
1184 If verbose is set, it prints out some helpful context lines, showing where
1185 exactly the error occurred in the file.
1186 """
1187 if not isinstance(node, typ):
1188 if verbose:
1189 lines = [""] + self.read_file(filename).splitlines()
1190
1191 context = 2
1192 lines_start = max(node.lineno - context, 0)
1193 # Add one to include the last line
1194 lines_end = min(node.lineno + context, len(lines)) + 1
1195 lines = (
1196 ['== %s ==\n' % filename] +
1197 ["<snip>\n"] +
1198 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1199 lines[lines_start:lines_start + context])] +
1200 ['-' * 80 + '\n'] +
1201 ['%d %s' % (node.lineno, lines[node.lineno])] +
1202 ['-' * (node.col_offset + 3) + '^' + '-' * (
1203 80 - node.col_offset - 4) + '\n'] +
1204 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1205 lines[node.lineno + 1:lines_end])] +
1206 ["<snip>\n"]
1207 )
1208 # Print out a useful message when a type assertion fails.
1209 for l in lines:
1210 self.print_line(l.strip())
1211
1212 node_dumped = ast.dump(node, annotate_fields=False)
1213 # If the node is huge, truncate it so everything fits in a terminal
1214 # window.
1215 if len(node_dumped) > 60: # pragma: no cover
1216 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1217 raise BBGenErr(
1218 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1219 ' be %s, is %s' % (
1220 filename, node_dumped,
1221 node.lineno, typ, type(node)))
1222
1223 def ensure_ast_dict_keys_sorted(self, node, filename, verbose):
1224 is_valid = True
1225
1226 keys = []
1227 # The keys of this dict are ordered as ordered in the file; normal python
1228 # dictionary keys are given an arbitrary order, but since we parsed the
1229 # file itself, the order as given in the file is preserved.
1230 for key in node.keys:
1231 self.type_assert(key, ast.Str, filename, verbose)
1232 keys.append(key.s)
1233
1234 keys_sorted = sorted(keys)
1235 if keys_sorted != keys:
1236 is_valid = False
1237 if verbose:
1238 for line in difflib.unified_diff(
1239 keys,
1240 keys_sorted, fromfile='current (%r)' % filename, tofile='sorted'):
1241 self.print_line(line)
1242
1243 if len(set(keys)) != len(keys):
1244 for i in range(len(keys_sorted)-1):
1245 if keys_sorted[i] == keys_sorted[i+1]:
1246 self.print_line('Key %s is duplicated' % keys_sorted[i])
1247 is_valid = False
1248 return is_valid
Stephen Martinisf83893722018-09-19 00:02:181249
1250 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201251 # TODO(https://crbug.com/886993): Add the ability for this script to
1252 # actually format the files, rather than just complain if they're
1253 # incorrectly formatted.
1254 bad_files = set()
1255
1256 for filename in (
Stephen Martinisb72f6d22018-10-04 23:29:011257 'mixins.pyl',
Stephen Martinis54d64ad2018-09-21 22:16:201258 'test_suites.pyl',
1259 'test_suite_exceptions.pyl',
1260 ):
Stephen Martinisf83893722018-09-19 00:02:181261 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1262
Stephen Martinisf83893722018-09-19 00:02:181263 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201264 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181265 module = parsed.body
1266
1267 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201268 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181269 if len(module) != 1: # pragma: no cover
1270 raise BBGenErr('Invalid .pyl file %s' % filename)
1271 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201272 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181273
1274 # Value should be a dictionary.
1275 value = expr.value
Stephen Martinis54d64ad2018-09-21 22:16:201276 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181277
Stephen Martinis54d64ad2018-09-21 22:16:201278 if filename == 'test_suites.pyl':
1279 expected_keys = ['basic_suites', 'compound_suites']
1280 actual_keys = [node.s for node in value.keys]
1281 assert all(key in expected_keys for key in actual_keys), (
1282 'Invalid %r file; expected keys %r, got %r' % (
1283 filename, expected_keys, actual_keys))
1284 suite_dicts = [node for node in value.values]
1285 # Only two keys should mean only 1 or 2 values
1286 assert len(suite_dicts) <= 2
1287 for suite_group in suite_dicts:
1288 if not self.ensure_ast_dict_keys_sorted(
1289 suite_group, filename, verbose):
1290 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181291
Stephen Martinis54d64ad2018-09-21 22:16:201292 else:
1293 if not self.ensure_ast_dict_keys_sorted(
1294 value, filename, verbose):
1295 bad_files.add(filename)
1296
1297 # waterfalls.pyl is slightly different, just do it manually here
1298 filename = 'waterfalls.pyl'
1299 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1300
1301 # Must be a module.
1302 self.type_assert(parsed, ast.Module, filename, verbose)
1303 module = parsed.body
1304
1305 # Only one expression in the module.
1306 self.type_assert(module, list, filename, verbose)
1307 if len(module) != 1: # pragma: no cover
1308 raise BBGenErr('Invalid .pyl file %s' % filename)
1309 expr = module[0]
1310 self.type_assert(expr, ast.Expr, filename, verbose)
1311
1312 # Value should be a list.
1313 value = expr.value
1314 self.type_assert(value, ast.List, filename, verbose)
1315
1316 keys = []
1317 for val in value.elts:
1318 self.type_assert(val, ast.Dict, filename, verbose)
1319 waterfall_name = None
1320 for key, val in zip(val.keys, val.values):
1321 self.type_assert(key, ast.Str, filename, verbose)
1322 if key.s == 'machines':
1323 if not self.ensure_ast_dict_keys_sorted(val, filename, verbose):
1324 bad_files.add(filename)
1325
1326 if key.s == "name":
1327 self.type_assert(val, ast.Str, filename, verbose)
1328 waterfall_name = val.s
1329 assert waterfall_name
1330 keys.append(waterfall_name)
1331
1332 if sorted(keys) != keys:
1333 bad_files.add(filename)
1334 if verbose: # pragma: no cover
1335 for line in difflib.unified_diff(
1336 keys,
1337 sorted(keys), fromfile='current', tofile='sorted'):
1338 self.print_line(line)
Stephen Martinisf83893722018-09-19 00:02:181339
1340 if bad_files:
1341 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201342 'The following files have invalid keys: %s\n. They are either '
1343 'unsorted, or have duplicates.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181344
Kenneth Russelleb60cbd22017-12-05 07:54:281345 def check_output_file_consistency(self, verbose=False):
1346 self.load_configuration_files()
1347 # All waterfalls must have been written by this script already.
1348 self.resolve_configuration_files()
1349 ungenerated_waterfalls = set()
1350 for waterfall in self.waterfalls:
1351 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111352 file_path = waterfall['name'] + '.json'
1353 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281354 if expected != current:
1355 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321356 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501357 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281358 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321359 'contents:')
1360 for line in difflib.unified_diff(
1361 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501362 current.splitlines(),
1363 fromfile='expected', tofile='current'):
1364 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281365 if ungenerated_waterfalls:
1366 raise BBGenErr('The following waterfalls have not been properly '
1367 'autogenerated by generate_buildbot_json.py: ' +
1368 str(ungenerated_waterfalls))
1369
1370 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501371 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281372 self.check_output_file_consistency(verbose) # pragma: no cover
1373
1374 def parse_args(self, argv): # pragma: no cover
Karen Qiane24b7ee2019-02-12 23:37:061375
1376 # RawTextHelpFormatter allows for styling of help statement
1377 parser = argparse.ArgumentParser(formatter_class=
1378 argparse.RawTextHelpFormatter)
1379
1380 group = parser.add_mutually_exclusive_group()
1381 group.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281382 '-c', '--check', action='store_true', help=
1383 'Do consistency checks of configuration and generated files and then '
1384 'exit. Used during presubmit. Causes the tool to not generate any files.')
Karen Qiane24b7ee2019-02-12 23:37:061385 group.add_argument(
1386 '--query', type=str, help=
1387 ("Returns raw JSON information of buildbots and tests.\n" +
1388 "Examples:\n" +
1389 " List all bots (all info):\n" +
1390 " --query bots\n\n" +
1391 " List all bots and only their associated tests:\n" +
1392 " --query bots/tests\n\n" +
1393 " List all information about 'bot1' " +
1394 "(make sure you have quotes):\n" +
1395 " --query bot/'bot1'\n\n" +
1396 " List tests running for 'bot1' (make sure you have quotes):\n" +
1397 " --query bot/'bot1'/tests\n\n" +
1398 " List all tests:\n" +
1399 " --query tests\n\n" +
1400 " List all tests and the bots running them:\n" +
1401 " --query tests/bots\n\n"+
1402 " List all tests that satisfy multiple parameters\n" +
1403 " (separation of parameters by '&' symbol):\n" +
1404 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
1405 " List all tests that run with a specific flag:\n" +
1406 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
1407 " List specific test (make sure you have quotes):\n"
1408 " --query test/'test1'\n\n"
1409 " List all bots running 'test1' " +
1410 "(make sure you have quotes):\n" +
1411 " --query test/'test1'/bots" ))
Kenneth Russelleb60cbd22017-12-05 07:54:281412 parser.add_argument(
1413 '-n', '--new-files', action='store_true', help=
1414 'Write output files as .new.json. Useful during development so old and '
1415 'new files can be looked at side-by-side.')
1416 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501417 '-v', '--verbose', action='store_true', help=
1418 'Increases verbosity. Affects consistency checks.')
1419 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281420 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1421 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111422 parser.add_argument(
1423 '--pyl-files-dir', type=os.path.realpath,
1424 help='Path to the directory containing the input .pyl files.')
Karen Qiane24b7ee2019-02-12 23:37:061425 parser.add_argument(
1426 '--json', help=
1427 ("Outputs results into a json file. Only works with query function.\n" +
1428 "Examples:\n" +
1429 " Outputs file into specified json file: \n" +
1430 " --json <file-name-here.json>"))
Kenneth Russelleb60cbd22017-12-05 07:54:281431 self.args = parser.parse_args(argv)
Karen Qiane24b7ee2019-02-12 23:37:061432 if self.args.json and not self.args.query:
1433 parser.error("The --json flag can only be used with --query.")
1434
1435 def does_test_match(self, test_info, params_dict):
1436 """Checks to see if the test matches the parameters given.
1437
1438 Compares the provided test_info with the params_dict to see
1439 if the bot matches the parameters given. If so, returns True.
1440 Else, returns false.
1441
1442 Args:
1443 test_info (dict): Information about a specific bot provided
1444 in the format shown in waterfalls.pyl
1445 params_dict (dict): Dictionary of parameters and their values
1446 to look for in the bot
1447 Ex: {
1448 'device_os':'android',
1449 '--flag':True,
1450 'mixins': ['mixin1', 'mixin2'],
1451 'ex_key':'ex_value'
1452 }
1453
1454 """
1455 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
1456 'kvm', 'pool', 'integrity'] # dimension parameters
1457 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
1458 'can_use_on_swarming_builders']
1459 for param in params_dict:
1460 # if dimension parameter
1461 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
1462 if not 'swarming' in test_info:
1463 return False
1464 swarming = test_info['swarming']
1465 if param in SWARMING_PARAMS:
1466 if not param in swarming:
1467 return False
1468 if not str(swarming[param]) == params_dict[param]:
1469 return False
1470 else:
1471 if not 'dimension_sets' in swarming:
1472 return False
1473 d_set = swarming['dimension_sets']
1474 # only looking at the first dimension set
1475 if not param in d_set[0]:
1476 return False
1477 if not d_set[0][param] == params_dict[param]:
1478 return False
1479
1480 # if flag
1481 elif param.startswith('--'):
1482 if not 'args' in test_info:
1483 return False
1484 if not param in test_info['args']:
1485 return False
1486
1487 # not dimension parameter/flag/mixin
1488 else:
1489 if not param in test_info:
1490 return False
1491 if not test_info[param] == params_dict[param]:
1492 return False
1493 return True
1494 def error_msg(self, msg):
1495 """Prints an error message.
1496
1497 In addition to a catered error message, also prints
1498 out where the user can find more help. Then, program exits.
1499 """
1500 self.print_line(msg + (' If you need more information, ' +
1501 'please run with -h or --help to see valid commands.'))
1502 sys.exit(1)
1503
1504 def find_bots_that_run_test(self, test, bots):
1505 matching_bots = []
1506 for bot in bots:
1507 bot_info = bots[bot]
1508 tests = self.flatten_tests_for_bot(bot_info)
1509 for test_info in tests:
1510 test_name = ""
1511 if 'name' in test_info:
1512 test_name = test_info['name']
1513 elif 'test' in test_info:
1514 test_name = test_info['test']
1515 if not test_name == test:
1516 continue
1517 matching_bots.append(bot)
1518 return matching_bots
1519
1520 def find_tests_with_params(self, tests, params_dict):
1521 matching_tests = []
1522 for test_name in tests:
1523 test_info = tests[test_name]
1524 if not self.does_test_match(test_info, params_dict):
1525 continue
1526 if not test_name in matching_tests:
1527 matching_tests.append(test_name)
1528 return matching_tests
1529
1530 def flatten_waterfalls_for_query(self, waterfalls):
1531 bots = {}
1532 for waterfall in waterfalls:
1533 waterfall_json = json.loads(self.generate_waterfall_json(waterfall))
1534 for bot in waterfall_json:
1535 bot_info = waterfall_json[bot]
1536 if 'AAAAA' not in bot:
1537 bots[bot] = bot_info
1538 return bots
1539
1540 def flatten_tests_for_bot(self, bot_info):
1541 """Returns a list of flattened tests.
1542
1543 Returns a list of tests not grouped by test category
1544 for a specific bot.
1545 """
1546 TEST_CATS = self.get_test_generator_map().keys()
1547 tests = []
1548 for test_cat in TEST_CATS:
1549 if not test_cat in bot_info:
1550 continue
1551 test_cat_tests = bot_info[test_cat]
1552 tests = tests + test_cat_tests
1553 return tests
1554
1555 def flatten_tests_for_query(self, test_suites):
1556 """Returns a flattened dictionary of tests.
1557
1558 Returns a dictionary of tests associate with their
1559 configuration, not grouped by their test suite.
1560 """
1561 tests = {}
1562 for test_suite in test_suites.itervalues():
1563 for test in test_suite:
1564 test_info = test_suite[test]
1565 test_name = test
1566 if 'name' in test_info:
1567 test_name = test_info['name']
1568 tests[test_name] = test_info
1569 return tests
1570
1571 def parse_query_filter_params(self, params):
1572 """Parses the filter parameters.
1573
1574 Creates a dictionary from the parameters provided
1575 to filter the bot array.
1576 """
1577 params_dict = {}
1578 for p in params:
1579 # flag
1580 if p.startswith("--"):
1581 params_dict[p] = True
1582 else:
1583 pair = p.split(":")
1584 if len(pair) != 2:
1585 self.error_msg('Invalid command.')
1586 # regular parameters
1587 if pair[1].lower() == "true":
1588 params_dict[pair[0]] = True
1589 elif pair[1].lower() == "false":
1590 params_dict[pair[0]] = False
1591 else:
1592 params_dict[pair[0]] = pair[1]
1593 return params_dict
1594
1595 def get_test_suites_dict(self, bots):
1596 """Returns a dictionary of bots and their tests.
1597
1598 Returns a dictionary of bots and a list of their associated tests.
1599 """
1600 test_suite_dict = dict()
1601 for bot in bots:
1602 bot_info = bots[bot]
1603 tests = self.flatten_tests_for_bot(bot_info)
1604 test_suite_dict[bot] = tests
1605 return test_suite_dict
1606
1607 def output_query_result(self, result, json_file=None):
1608 """Outputs the result of the query.
1609
1610 If a json file parameter name is provided, then
1611 the result is output into the json file. If not,
1612 then the result is printed to the console.
1613 """
1614 output = json.dumps(result, indent=2)
1615 if json_file:
1616 self.write_file(json_file, output)
1617 else:
1618 self.print_line(output)
1619 return
1620
1621 def query(self, args):
1622 """Queries tests or bots.
1623
1624 Depending on the arguments provided, outputs a json of
1625 tests or bots matching the appropriate optional parameters provided.
1626 """
1627 # split up query statement
1628 query = args.query.split('/')
1629 self.load_configuration_files()
1630 self.resolve_configuration_files()
1631
1632 # flatten bots json
1633 tests = self.test_suites
1634 bots = self.flatten_waterfalls_for_query(self.waterfalls)
1635
1636 cmd_class = query[0]
1637
1638 # For queries starting with 'bots'
1639 if cmd_class == "bots":
1640 if len(query) == 1:
1641 return self.output_query_result(bots, args.json)
1642 # query with specific parameters
1643 elif len(query) == 2:
1644 if query[1] == 'tests':
1645 test_suites_dict = self.get_test_suites_dict(bots)
1646 return self.output_query_result(test_suites_dict, args.json)
1647 else:
1648 self.error_msg("This query should be in the format: bots/tests.")
1649
1650 else:
1651 self.error_msg("This query should have 0 or 1 '/', found %s instead."
1652 % str(len(query)-1))
1653
1654 # For queries starting with 'bot'
1655 elif cmd_class == "bot":
1656 if not len(query) == 2 and not len(query) == 3:
1657 self.error_msg("Command should have 1 or 2 '/', found %s instead."
1658 % str(len(query)-1))
1659 bot_id = query[1]
1660 if not bot_id in bots:
1661 self.error_msg("No bot named '" + bot_id + "' found.")
1662 bot_info = bots[bot_id]
1663 if len(query) == 2:
1664 return self.output_query_result(bot_info, args.json)
1665 if not query[2] == 'tests':
1666 self.error_msg("The query should be in the format:" +
1667 "bot/<bot-name>/tests.")
1668
1669 bot_tests = self.flatten_tests_for_bot(bot_info)
1670 return self.output_query_result(bot_tests, args.json)
1671
1672 # For queries starting with 'tests'
1673 elif cmd_class == "tests":
1674 if not len(query) == 1 and not len(query) == 2:
1675 self.error_msg("The query should have 0 or 1 '/', found %s instead."
1676 % str(len(query)-1))
1677 flattened_tests = self.flatten_tests_for_query(tests)
1678 if len(query) == 1:
1679 return self.output_query_result(flattened_tests, args.json)
1680
1681 # create params dict
1682 params = query[1].split('&')
1683 params_dict = self.parse_query_filter_params(params)
1684 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
1685 return self.output_query_result(matching_bots)
1686
1687 # For queries starting with 'test'
1688 elif cmd_class == "test":
1689 if not len(query) == 2 and not len(query) == 3:
1690 self.error_msg("The query should have 1 or 2 '/', found %s instead."
1691 % str(len(query)-1))
1692 test_id = query[1]
1693 if len(query) == 2:
1694 flattened_tests = self.flatten_tests_for_query(tests)
1695 for test in flattened_tests:
1696 if test == test_id:
1697 return self.output_query_result(flattened_tests[test], args.json)
1698 self.error_msg("There is no test named %s." % test_id)
1699 if not query[2] == 'bots':
1700 self.error_msg("The query should be in the format: " +
1701 "test/<test-name>/bots")
1702 bots_for_test = self.find_bots_that_run_test(test_id, bots)
1703 return self.output_query_result(bots_for_test)
1704
1705 else:
1706 self.error_msg("Your command did not match any valid commands." +
1707 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:281708
1709 def main(self, argv): # pragma: no cover
1710 self.parse_args(argv)
1711 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:501712 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:061713 elif self.args.query:
1714 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:281715 else:
1716 self.generate_waterfalls()
1717 return 0
1718
1719if __name__ == "__main__": # pragma: no cover
1720 generator = BBJSONGenerator()
John Budorick699282e2019-02-13 01:27:331721 sys.exit(generator.main(sys.argv[1:]))