blob: bf291f13fb3ae69dde6cf014062b05d46ad60f7a [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
Kenneth Russelleb60cbd22017-12-05 07:54:28242 def get_exception_for_test(self, test_name, test_config):
243 # gtests may have both "test" and "name" fields, and usually, if the "name"
244 # field is specified, it means that the same test is being repurposed
245 # multiple times with different command line arguments. To handle this case,
246 # prefer to lookup per the "name" field of the test itself, as opposed to
247 # the "test_name", which is actually the "test" field.
248 if 'name' in test_config:
249 return self.exceptions.get(test_config['name'])
250 else:
251 return self.exceptions.get(test_name)
252
Nico Weberb0b3f5862018-07-13 18:45:15253 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28254 # Currently, the only reason a test should not run on a given tester is that
255 # it's in the exceptions. (Once the GPU waterfall generation script is
256 # incorporated here, the rules will become more complex.)
257 exception = self.get_exception_for_test(test_name, test_config)
258 if not exception:
259 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28260 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28261 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28262 if remove_from:
263 if tester_name in remove_from:
264 return False
265 # TODO(kbr): this code path was added for some tests (including
266 # android_webview_unittests) on one machine (Nougat Phone
267 # Tester) which exists with the same name on two waterfalls,
268 # chromium.android and chromium.fyi; the tests are run on one
269 # but not the other. Once the bots are all uniquely named (a
270 # different ongoing project) this code should be removed.
271 # TODO(kbr): add coverage.
272 return (tester_name + ' ' + waterfall['name']
273 not in remove_from) # pragma: no cover
274 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28275
Nico Weber79dc5f6852018-07-13 19:38:49276 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28277 exception = self.get_exception_for_test(test_name, test)
278 if not exception:
279 return None
Nico Weber79dc5f6852018-07-13 19:38:49280 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28281
Kenneth Russell8a386d42018-06-02 09:48:01282 def merge_command_line_args(self, arr, prefix, splitter):
283 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01284 idx = 0
285 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01286 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01287 while idx < len(arr):
288 flag = arr[idx]
289 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01290 if flag.startswith(prefix):
291 arg = flag[prefix_len:]
292 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01293 if first_idx < 0:
294 first_idx = idx
295 else:
296 delete_current_entry = True
297 if delete_current_entry:
298 del arr[idx]
299 else:
300 idx += 1
301 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01302 arr[first_idx] = prefix + splitter.join(accumulated_args)
303 return arr
304
305 def maybe_fixup_args_array(self, arr):
306 # The incoming array of strings may be an array of command line
307 # arguments. To make it easier to turn on certain features per-bot or
308 # per-test-suite, look specifically for certain flags and merge them
309 # appropriately.
310 # --enable-features=Feature1 --enable-features=Feature2
311 # are merged to:
312 # --enable-features=Feature1,Feature2
313 # and:
314 # --extra-browser-args=arg1 --extra-browser-args=arg2
315 # are merged to:
316 # --extra-browser-args=arg1 arg2
317 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
318 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01319 return arr
320
Kenneth Russelleb60cbd22017-12-05 07:54:28321 def dictionary_merge(self, a, b, path=None, update=True):
322 """http://stackoverflow.com/questions/7204805/
323 python-dictionaries-of-dictionaries-merge
324 merges b into a
325 """
326 if path is None:
327 path = []
328 for key in b:
329 if key in a:
330 if isinstance(a[key], dict) and isinstance(b[key], dict):
331 self.dictionary_merge(a[key], b[key], path + [str(key)])
332 elif a[key] == b[key]:
333 pass # same leaf value
334 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06335 # Args arrays are lists of strings. Just concatenate them,
336 # and don't sort them, in order to keep some needed
337 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28338 if all(isinstance(x, str)
339 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01340 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28341 else:
342 # TODO(kbr): this only works properly if the two arrays are
343 # the same length, which is currently always the case in the
344 # swarming dimension_sets that we have to merge. It will fail
345 # to merge / override 'args' arrays which are different
346 # length.
347 for idx in xrange(len(b[key])):
348 try:
349 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
350 path + [str(key), str(idx)],
351 update=update)
352 except (IndexError, TypeError): # pragma: no cover
353 raise BBGenErr('Error merging list keys ' + str(key) +
354 ' and indices ' + str(idx) + ' between ' +
355 str(a) + ' and ' + str(b)) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53356 elif update:
357 if b[key] is None:
358 del a[key]
359 else:
360 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28361 else:
362 raise BBGenErr('Conflict at %s' % '.'.join(
363 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53364 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28365 a[key] = b[key]
366 return a
367
John Budorickab108712018-09-01 00:12:21368 def initialize_args_for_test(
369 self, generated_test, tester_config, additional_arg_keys=None):
370
371 args = []
372 args.extend(generated_test.get('args', []))
373 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22374
Kenneth Russell8a386d42018-06-02 09:48:01375 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21376 val = generated_test.pop(key, [])
377 if fn(tester_config):
378 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01379
380 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
381 add_conditional_args('linux_args', self.is_linux)
382 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36383 add_conditional_args('chromeos_args', self.is_chromeos)
Kenneth Russell8a386d42018-06-02 09:48:01384
John Budorickab108712018-09-01 00:12:21385 for key in additional_arg_keys or []:
386 args.extend(generated_test.pop(key, []))
387 args.extend(tester_config.get(key, []))
388
389 if args:
390 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01391
Kenneth Russelleb60cbd22017-12-05 07:54:28392 def initialize_swarming_dictionary_for_test(self, generated_test,
393 tester_config):
394 if 'swarming' not in generated_test:
395 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28396 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
397 generated_test['swarming'].update({
398 'can_use_on_swarming_builders': tester_config.get('use_swarming', True)
399 })
Kenneth Russelleb60cbd22017-12-05 07:54:28400 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03401 if ('dimension_sets' not in generated_test['swarming'] and
402 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28403 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
404 tester_config['swarming']['dimension_sets'])
405 self.dictionary_merge(generated_test['swarming'],
406 tester_config['swarming'])
407 # Apply any Android-specific Swarming dimensions after the generic ones.
408 if 'android_swarming' in generated_test:
409 if self.is_android(tester_config): # pragma: no cover
410 self.dictionary_merge(
411 generated_test['swarming'],
412 generated_test['android_swarming']) # pragma: no cover
413 del generated_test['android_swarming'] # pragma: no cover
414
415 def clean_swarming_dictionary(self, swarming_dict):
416 # Clean out redundant entries from a test's "swarming" dictionary.
417 # This is really only needed to retain 100% parity with the
418 # handwritten JSON files, and can be removed once all the files are
419 # autogenerated.
420 if 'shards' in swarming_dict:
421 if swarming_dict['shards'] == 1: # pragma: no cover
422 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24423 if 'hard_timeout' in swarming_dict:
424 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
425 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43426 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28427 # Remove all other keys.
428 for k in swarming_dict.keys(): # pragma: no cover
429 if k != 'can_use_on_swarming_builders': # pragma: no cover
430 del swarming_dict[k] # pragma: no cover
431
Stephen Martinis0382bc12018-09-17 22:29:07432 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
433 waterfall):
434 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01435 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07436 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28437 # See if there are any exceptions that need to be merged into this
438 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49439 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28440 if modifications:
441 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23442 if 'swarming' in test:
443 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28444 # Ensure all Android Swarming tests run only on userdebug builds if another
445 # build type was not specified.
446 if 'swarming' in test and self.is_android(tester_config):
447 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22448 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28449 d['device_os_type'] = 'userdebug'
450
Kenneth Russelleb60cbd22017-12-05 07:54:28451 return test
452
Shenghua Zhangaba8bad2018-02-07 02:12:09453 def add_common_test_properties(self, test, tester_config):
454 if tester_config.get('use_multi_dimension_trigger_script'):
Kenneth Russell73c3bd8b2018-10-19 22:30:19455 # Assumes update_and_cleanup_test has already been called, so the
456 # builder's mixins have been flattened into the test.
Shenghua Zhangaba8bad2018-02-07 02:12:09457 test['trigger_script'] = {
458 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
459 'args': [
460 '--multiple-trigger-configs',
Kenneth Russell73c3bd8b2018-10-19 22:30:19461 json.dumps(test['swarming']['dimension_sets'] +
Shenghua Zhangaba8bad2018-02-07 02:12:09462 tester_config.get('alternate_swarming_dimensions', [])),
463 '--multiple-dimension-script-verbose',
464 'True'
465 ],
466 }
Ben Pastenea9e583b2019-01-16 02:57:26467 elif self.is_chromeos(tester_config) and tester_config.get('use_swarming',
468 True):
469 # The presence of the "device_type" dimension indicates that the tests
470 # are targetting CrOS hardware and so need the special trigger script.
471 dimension_sets = tester_config['swarming']['dimension_sets']
472 if all('device_type' in ds for ds in dimension_sets):
473 test['trigger_script'] = {
474 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
475 }
Shenghua Zhangaba8bad2018-02-07 02:12:09476
Ben Pastene858f4be2019-01-09 23:52:09477 def add_android_presentation_args(self, tester_config, test_name, result):
478 args = result.get('args', [])
479 args.append('--gs-results-bucket=chromium-result-details')
480 if (result['swarming']['can_use_on_swarming_builders'] and not
481 tester_config.get('skip_merge_script', False)):
482 result['merge'] = {
483 'args': [
484 '--bucket',
485 'chromium-result-details',
486 '--test-name',
487 test_name
488 ],
489 'script': '//build/android/pylib/results/presentation/'
490 'test_results_presentation.py',
491 }
492 if not tester_config.get('skip_cipd_packages', False):
Ben Pastenee5949ea82019-01-10 21:45:26493 cipd_packages = result['swarming'].get('cipd_packages', [])
494 cipd_packages.append(
Ben Pastene858f4be2019-01-09 23:52:09495 {
496 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
497 'location': 'bin',
498 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
499 }
Ben Pastenee5949ea82019-01-10 21:45:26500 )
501 result['swarming']['cipd_packages'] = cipd_packages
Ben Pastene858f4be2019-01-09 23:52:09502 if not tester_config.get('skip_output_links', False):
503 result['swarming']['output_links'] = [
504 {
505 'link': [
506 'https://luci-logdog.appspot.com/v/?s',
507 '=android%2Fswarming%2Flogcats%2F',
508 '${TASK_ID}%2F%2B%2Funified_logcats',
509 ],
510 'name': 'shard #${SHARD_INDEX} logcats',
511 },
512 ]
513 if args:
514 result['args'] = args
515
Kenneth Russelleb60cbd22017-12-05 07:54:28516 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
517 test_config):
518 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15519 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28520 return None
521 result = copy.deepcopy(test_config)
522 if 'test' in result:
523 result['name'] = test_name
524 else:
525 result['test'] = test_name
526 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21527
528 self.initialize_args_for_test(
529 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28530 if self.is_android(tester_config) and tester_config.get('use_swarming',
531 True):
Ben Pastene858f4be2019-01-09 23:52:09532 self.add_android_presentation_args(tester_config, test_name, result)
533 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42534
Stephen Martinis0382bc12018-09-17 22:29:07535 result = self.update_and_cleanup_test(
536 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09537 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43538
539 if not result.get('merge'):
540 # TODO(https://crbug.com/958376): Consider adding the ability to not have
541 # this default.
542 result['merge'] = {
543 'script': '//testing/merge_scripts/standard_gtest_merge.py',
544 'args': [],
545 }
Kenneth Russelleb60cbd22017-12-05 07:54:28546 return result
547
548 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
549 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01550 if not self.should_run_on_tester(waterfall, tester_name, test_name,
551 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28552 return None
553 result = copy.deepcopy(test_config)
554 result['isolate_name'] = result.get('isolate_name', test_name)
555 result['name'] = test_name
556 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01557 self.initialize_args_for_test(result, tester_config)
Ben Pastene858f4be2019-01-09 23:52:09558 if tester_config.get('use_android_presentation', False):
559 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07560 result = self.update_and_cleanup_test(
561 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09562 self.add_common_test_properties(result, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17563
564 if not result.get('merge'):
565 # TODO(https://crbug.com/958376): Consider adding the ability to not have
566 # this default.
567 result['merge'] = {
568 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
569 'args': [],
570 }
Kenneth Russelleb60cbd22017-12-05 07:54:28571 return result
572
573 def generate_script_test(self, waterfall, tester_name, tester_config,
574 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44575 # TODO(https://crbug.com/953072): Remove this check whenever a better
576 # long-term solution is implemented.
577 if (waterfall.get('forbid_script_tests', False) or
578 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
579 raise BBGenErr('Attempted to generate a script test on tester ' +
580 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01581 if not self.should_run_on_tester(waterfall, tester_name, test_name,
582 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28583 return None
584 result = {
585 'name': test_name,
586 'script': test_config['script']
587 }
Stephen Martinis0382bc12018-09-17 22:29:07588 result = self.update_and_cleanup_test(
589 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28590 return result
591
592 def generate_junit_test(self, waterfall, tester_name, tester_config,
593 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01594 del tester_config
595 if not self.should_run_on_tester(waterfall, tester_name, test_name,
596 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28597 return None
598 result = {
599 'test': test_name,
600 }
601 return result
602
603 def generate_instrumentation_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)
Kenneth Russell8ceeabf2017-12-11 17:53:28609 if 'test' in result and result['test'] != test_name:
610 result['name'] = test_name
611 else:
612 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07613 result = self.update_and_cleanup_test(
614 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28615 return result
616
Stephen Martinis2a0667022018-09-25 22:31:14617 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01618 substitutions = {
619 # Any machine in waterfalls.pyl which desires to run GPU tests
620 # must provide the os_type key.
621 'os_type': tester_config['os_type'],
622 'gpu_vendor_id': '0',
623 'gpu_device_id': '0',
624 }
Stephen Martinis2a0667022018-09-25 22:31:14625 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01626 if 'gpu' in dimension_set:
627 # First remove the driver version, then split into vendor and device.
628 gpu = dimension_set['gpu']
Kenneth Russell384a1732019-03-16 02:36:02629 # Handle certain specialized named GPUs.
630 if gpu.startswith('nvidia-quadro-p400'):
631 gpu = ['10de', '1cb3']
632 elif gpu.startswith('intel-hd-630'):
633 gpu = ['8086', '5912']
634 else:
635 gpu = gpu.split('-')[0].split(':')
Kenneth Russell8a386d42018-06-02 09:48:01636 substitutions['gpu_vendor_id'] = gpu[0]
637 substitutions['gpu_device_id'] = gpu[1]
638 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
639
640 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56641 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01642 # These are all just specializations of isolated script tests with
643 # a bunch of boilerplate command line arguments added.
644
645 # The step name must end in 'test' or 'tests' in order for the
646 # results to automatically show up on the flakiness dashboard.
647 # (At least, this was true some time ago.) Continue to use this
648 # naming convention for the time being to minimize changes.
649 step_name = test_config.get('name', test_name)
650 if not (step_name.endswith('test') or step_name.endswith('tests')):
651 step_name = '%s_tests' % step_name
652 result = self.generate_isolated_script_test(
653 waterfall, tester_name, tester_config, step_name, test_config)
654 if not result:
655 return None
656 result['isolate_name'] = 'telemetry_gpu_integration_test'
657 args = result.get('args', [])
658 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14659
660 # These tests upload and download results from cloud storage and therefore
661 # aren't idempotent yet. https://crbug.com/549140.
662 result['swarming']['idempotent'] = False
663
Kenneth Russell44910c32018-12-03 23:35:11664 # The GPU tests act much like integration tests for the entire browser, and
665 # tend to uncover flakiness bugs more readily than other test suites. In
666 # order to surface any flakiness more readily to the developer of the CL
667 # which is introducing it, we disable retries with patch on the commit
668 # queue.
669 result['should_retry_with_patch'] = False
670
Bo Liu555a0f92019-03-29 12:11:56671 browser = ('android-webview-instrumentation'
672 if is_android_webview else tester_config['browser_config'])
Kenneth Russell8a386d42018-06-02 09:48:01673 args = [
Bo Liu555a0f92019-03-29 12:11:56674 test_to_run,
675 '--show-stdout',
676 '--browser=%s' % browser,
677 # --passthrough displays more of the logging in Telemetry when
678 # run via typ, in particular some of the warnings about tests
679 # being expected to fail, but passing.
680 '--passthrough',
681 '-v',
682 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
Kenneth Russell8a386d42018-06-02 09:48:01683 ] + args
684 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14685 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01686 return result
687
Kenneth Russelleb60cbd22017-12-05 07:54:28688 def get_test_generator_map(self):
689 return {
Bo Liu555a0f92019-03-29 12:11:56690 'android_webview_gpu_telemetry_tests':
691 GPUTelemetryTestGenerator(self, is_android_webview=True),
692 'cts_tests':
693 CTSGenerator(self),
694 'gpu_telemetry_tests':
695 GPUTelemetryTestGenerator(self),
696 'gtest_tests':
697 GTestGenerator(self),
698 'instrumentation_tests':
699 InstrumentationTestGenerator(self),
700 'isolated_scripts':
701 IsolatedScriptTestGenerator(self),
702 'junit_tests':
703 JUnitGenerator(self),
704 'scripts':
705 ScriptGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28706 }
707
Kenneth Russell8a386d42018-06-02 09:48:01708 def get_test_type_remapper(self):
709 return {
710 # These are a specialization of isolated_scripts with a bunch of
711 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:56712 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:01713 'gpu_telemetry_tests': 'isolated_scripts',
714 }
715
Kenneth Russelleb60cbd22017-12-05 07:54:28716 def check_composition_test_suites(self):
717 # Pre-pass to catch errors reliably.
718 for name, value in self.test_suites.iteritems():
719 if isinstance(value, list):
720 for entry in value:
721 if isinstance(self.test_suites[entry], list):
Nico Weberd18b8962018-05-16 19:39:38722 raise BBGenErr('Composition test suites may not refer to other '
723 'composition test suites (error found while '
Kenneth Russelleb60cbd22017-12-05 07:54:28724 'processing %s)' % name)
725
Stephen Martinis54d64ad2018-09-21 22:16:20726 def flatten_test_suites(self):
727 new_test_suites = {}
728 for name, value in self.test_suites.get('basic_suites', {}).iteritems():
729 new_test_suites[name] = value
730 for name, value in self.test_suites.get('compound_suites', {}).iteritems():
731 if name in new_test_suites:
732 raise BBGenErr('Composition test suite names may not duplicate basic '
733 'test suite names (error found while processsing %s' % (
734 name))
735 new_test_suites[name] = value
736 self.test_suites = new_test_suites
737
Kenneth Russelleb60cbd22017-12-05 07:54:28738 def resolve_composition_test_suites(self):
Stephen Martinis54d64ad2018-09-21 22:16:20739 self.flatten_test_suites()
740
Kenneth Russelleb60cbd22017-12-05 07:54:28741 self.check_composition_test_suites()
742 for name, value in self.test_suites.iteritems():
743 if isinstance(value, list):
744 # Resolve this to a dictionary.
745 full_suite = {}
746 for entry in value:
747 suite = self.test_suites[entry]
748 full_suite.update(suite)
749 self.test_suites[name] = full_suite
750
751 def link_waterfalls_to_test_suites(self):
752 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43753 for tester_name, tester in waterfall['machines'].iteritems():
754 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28755 if not value in self.test_suites:
756 # Hard / impossible to cover this in the unit test.
757 raise self.unknown_test_suite(
758 value, tester_name, waterfall['name']) # pragma: no cover
759 tester['test_suites'][suite] = self.test_suites[value]
760
761 def load_configuration_files(self):
762 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
763 self.test_suites = self.load_pyl_file('test_suites.pyl')
764 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:01765 self.mixins = self.load_pyl_file('mixins.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:28766
767 def resolve_configuration_files(self):
768 self.resolve_composition_test_suites()
769 self.link_waterfalls_to_test_suites()
770
Nico Weberd18b8962018-05-16 19:39:38771 def unknown_bot(self, bot_name, waterfall_name):
772 return BBGenErr(
773 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
774
Kenneth Russelleb60cbd22017-12-05 07:54:28775 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
776 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38777 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28778 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
779
780 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
781 return BBGenErr(
782 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
783 ' on waterfall ' + waterfall_name)
784
Stephen Martinisb72f6d22018-10-04 23:29:01785 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:07786 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:32787
788 Checks in the waterfall, builder, and test objects for mixins.
789 """
790 def valid_mixin(mixin_name):
791 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:01792 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:32793 raise BBGenErr("bad mixin %s" % mixin_name)
794 def must_be_list(mixins, typ, name):
795 """Asserts that given mixins are a list."""
796 if not isinstance(mixins, list):
797 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
798
Stephen Martinisb72f6d22018-10-04 23:29:01799 if 'mixins' in waterfall:
800 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
801 for mixin in waterfall['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32802 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01803 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32804
Stephen Martinisb72f6d22018-10-04 23:29:01805 if 'mixins' in builder:
806 must_be_list(builder['mixins'], 'builder', builder_name)
807 for mixin in builder['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32808 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01809 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32810
Stephen Martinisb72f6d22018-10-04 23:29:01811 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:07812 return test
813
Stephen Martinis2a0667022018-09-25 22:31:14814 test_name = test.get('name')
815 if not test_name:
816 test_name = test.get('test')
817 if not test_name: # pragma: no cover
818 # Not the best name, but we should say something.
819 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:01820 must_be_list(test['mixins'], 'test', test_name)
821 for mixin in test['mixins']:
Stephen Martinis0382bc12018-09-17 22:29:07822 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01823 test = self.apply_mixin(self.mixins[mixin], test)
824 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:07825 return test
Stephen Martinisb6a50492018-09-12 23:59:32826
Stephen Martinisb72f6d22018-10-04 23:29:01827 def apply_mixin(self, mixin, test):
828 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:32829
Stephen Martinis0382bc12018-09-17 22:29:07830 Mixins will not override an existing key. This is to ensure exceptions can
831 override a setting a mixin applies.
832
Stephen Martinisb72f6d22018-10-04 23:29:01833 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:32834 'dimension_sets', which is how normal test suites specify their dimensions,
835 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
836 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:01837
Stephen Martinisb6a50492018-09-12 23:59:32838 """
839 new_test = copy.deepcopy(test)
840 mixin = copy.deepcopy(mixin)
841
Stephen Martinisb72f6d22018-10-04 23:29:01842 if 'swarming' in mixin:
843 swarming_mixin = mixin['swarming']
844 new_test.setdefault('swarming', {})
845 if 'dimensions' in swarming_mixin:
846 new_test['swarming'].setdefault('dimension_sets', [{}])
847 for dimension_set in new_test['swarming']['dimension_sets']:
848 dimension_set.update(swarming_mixin['dimensions'])
849 del swarming_mixin['dimensions']
Stephen Martinisb6a50492018-09-12 23:59:32850
Stephen Martinisb72f6d22018-10-04 23:29:01851 # python dict update doesn't do recursion at all. Just hard code the
852 # nested update we need (mixin['swarming'] shouldn't clobber
853 # test['swarming'], but should update it).
854 new_test['swarming'].update(swarming_mixin)
855 del mixin['swarming']
856
Wezc0e835b702018-10-30 00:38:41857 if '$mixin_append' in mixin:
858 # Values specified under $mixin_append should be appended to existing
859 # lists, rather than replacing them.
860 mixin_append = mixin['$mixin_append']
861 for key in mixin_append:
862 new_test.setdefault(key, [])
863 if not isinstance(mixin_append[key], list):
864 raise BBGenErr(
865 'Key "' + key + '" in $mixin_append must be a list.')
866 if not isinstance(new_test[key], list):
867 raise BBGenErr(
868 'Cannot apply $mixin_append to non-list "' + key + '".')
869 new_test[key].extend(mixin_append[key])
870 if 'args' in mixin_append:
871 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
872 del mixin['$mixin_append']
873
Stephen Martinisb72f6d22018-10-04 23:29:01874 new_test.update(mixin)
Stephen Martinis0382bc12018-09-17 22:29:07875
Stephen Martinisb6a50492018-09-12 23:59:32876 return new_test
877
Kenneth Russelleb60cbd22017-12-05 07:54:28878 def generate_waterfall_json(self, waterfall):
879 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28880 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:01881 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:43882 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28883 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43884 # Copy only well-understood entries in the machine's configuration
885 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28886 if 'additional_compile_targets' in config:
887 tests['additional_compile_targets'] = config[
888 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43889 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28890 if test_type not in generator_map:
891 raise self.unknown_test_suite_type(
892 test_type, name, waterfall['name']) # pragma: no cover
893 test_generator = generator_map[test_type]
Nico Weber79dc5f6852018-07-13 19:38:49894 # Let multiple kinds of generators generate the same kinds
895 # of tests. For example, gpu_telemetry_tests are a
896 # specialization of isolated_scripts.
897 new_tests = test_generator.generate(
898 waterfall, name, config, input_tests)
899 remapped_test_type = test_type_remapper.get(test_type, test_type)
900 tests[remapped_test_type] = test_generator.sort(
901 tests.get(remapped_test_type, []) + new_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28902 all_tests[name] = tests
903 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
904 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
905 return json.dumps(all_tests, indent=2, separators=(',', ': '),
906 sort_keys=True) + '\n'
907
908 def generate_waterfalls(self): # pragma: no cover
909 self.load_configuration_files()
910 self.resolve_configuration_files()
911 filters = self.args.waterfall_filters
912 suffix = '.json'
913 if self.args.new_files:
914 suffix = '.new' + suffix
915 for waterfall in self.waterfalls:
916 should_gen = not filters or waterfall['name'] in filters
917 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:11918 file_path = waterfall['name'] + suffix
919 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:28920 self.generate_waterfall_json(waterfall))
921
Nico Weberd18b8962018-05-16 19:39:38922 def get_valid_bot_names(self):
John Budorick699282e2019-02-13 01:27:33923 # Extract bot names from infra/config/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:42924 # NOTE: This reference can cause issues; if a file changes there, the
925 # presubmit here won't be run by default. A manually maintained list there
926 # tries to run presubmit here when luci-milo.cfg is changed. If any other
927 # references to configs outside of this directory are added, please change
928 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
929 # never ends up in an invalid state.
Nico Weberd18b8962018-05-16 19:39:38930 bot_names = set()
John Budorickc12abd12018-08-14 19:37:43931 infra_config_dir = os.path.abspath(
932 os.path.join(os.path.dirname(__file__),
John Budorick699282e2019-02-13 01:27:33933 '..', '..', 'infra', 'config'))
John Budorickc12abd12018-08-14 19:37:43934 milo_configs = [
935 os.path.join(infra_config_dir, 'luci-milo.cfg'),
936 os.path.join(infra_config_dir, 'luci-milo-dev.cfg'),
937 ]
938 for c in milo_configs:
939 for l in self.read_file(c).splitlines():
940 if (not 'name: "buildbucket/luci.chromium.' in l and
Hans Wennborg98ffd7d92019-02-06 14:14:34941 not 'name: "buildbucket/luci.chrome.' in l and
John Budorickb1833612018-12-07 04:36:41942 not 'name: "buildbot/chromium.' in l and
943 not 'name: "buildbot/tryserver.chromium.' in l):
John Budorickc12abd12018-08-14 19:37:43944 continue
945 # l looks like
946 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
947 # Extract win_chromium_dbg_ng part.
948 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:38949 return bot_names
950
Kenneth Russell8a386d42018-06-02 09:48:01951 def get_bots_that_do_not_actually_exist(self):
952 # Some of the bots on the chromium.gpu.fyi waterfall in particular
953 # are defined only to be mirrored into trybots, and don't actually
954 # exist on any of the waterfalls or consoles.
955 return [
Michael Spangeb07eba62019-05-14 22:22:58956 'GPU FYI Fuchsia Builder',
Jamie Madillda894ce2019-04-08 17:19:17957 'ANGLE GPU Linux Release (Intel HD 630)',
958 'ANGLE GPU Linux Release (NVIDIA)',
959 'ANGLE GPU Mac Release (Intel)',
960 'ANGLE GPU Mac Retina Release (AMD)',
961 'ANGLE GPU Mac Retina Release (NVIDIA)',
Jamie Madilldc7feeb82018-11-14 04:54:56962 'ANGLE GPU Win10 Release (Intel HD 630)',
963 'ANGLE GPU Win10 Release (NVIDIA)',
Corentin Wallez7d3f4fa22018-11-19 23:35:44964 'Dawn GPU Linux Release (Intel HD 630)',
965 'Dawn GPU Linux Release (NVIDIA)',
966 'Dawn GPU Mac Release (Intel)',
967 'Dawn GPU Mac Retina Release (AMD)',
968 'Dawn GPU Mac Retina Release (NVIDIA)',
Jamie Madilldc7feeb82018-11-14 04:54:56969 'Dawn GPU Win10 Release (Intel HD 630)',
970 'Dawn GPU Win10 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:01971 'Optional Android Release (Nexus 5X)',
972 'Optional Linux Release (Intel HD 630)',
973 'Optional Linux Release (NVIDIA)',
974 'Optional Mac Release (Intel)',
975 'Optional Mac Retina Release (AMD)',
976 'Optional Mac Retina Release (NVIDIA)',
977 'Optional Win10 Release (Intel HD 630)',
978 'Optional Win10 Release (NVIDIA)',
979 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:08980 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:29981 'linux-blink-rel-dummy',
982 'mac10.10-blink-rel-dummy',
983 'mac10.11-blink-rel-dummy',
984 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:20985 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:29986 'mac10.13-blink-rel-dummy',
987 'win7-blink-rel-dummy',
988 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:08989 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:33990 'WebKit Linux composite_after_paint Dummy Builder',
Nico Weber7fc8b9da2018-06-08 19:22:08991 'WebKit Linux layout_ng Dummy Builder',
992 'WebKit Linux root_layer_scrolls Dummy Builder',
Stephen Martinis769b25112018-08-30 18:52:06993 # chromium, due to https://crbug.com/878915
994 'win-dbg',
995 'win32-dbg',
Stephen Martinis47d77132019-04-24 23:51:33996 'win-archive-dbg',
997 'win32-archive-dbg',
Stephen Martinis07a9f742019-03-20 19:16:56998 # chromium.mac, see https://crbug.com/943804
999 'mac-dummy-rel',
Ben Pastene7687c0112019-03-05 22:43:141000 # Defined in internal configs.
1001 'chromeos-amd64-generic-google-rel',
Anushruth9420fddf2019-04-04 00:24:591002 'chromeos-betty-google-rel',
Yuke Liaobc9ff982019-04-30 06:56:161003 # code coverage, see see https://crbug.com/930364
1004 'Linux Builder Code Coverage',
1005 'Linux Tests Code Coverage',
1006 'GPU Linux Builder Code Coverage',
1007 'Linux Release Code Coverage (NVIDIA)',
John Budoricka107e5b42019-05-21 23:28:081008 # chromium.memory. exists, but is omitted from consoles for now.
1009 # https://crbug.com/790202
1010 'android-asan'
Kenneth Russell8a386d42018-06-02 09:48:011011 ]
1012
Stephen Martinisf83893722018-09-19 00:02:181013 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201014 self.check_input_files_sorting(verbose)
1015
Kenneth Russelleb60cbd22017-12-05 07:54:281016 self.load_configuration_files()
Stephen Martinis54d64ad2018-09-21 22:16:201017 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281018 self.check_composition_test_suites()
Nico Weberd18b8962018-05-16 19:39:381019
1020 # All bots should exist.
1021 bot_names = self.get_valid_bot_names()
Kenneth Russell8a386d42018-06-02 09:48:011022 bots_that_dont_exist = self.get_bots_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:381023 for waterfall in self.waterfalls:
1024 for bot_name in waterfall['machines']:
Kenneth Russell8a386d42018-06-02 09:48:011025 if bot_name in bots_that_dont_exist:
1026 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381027 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:081028 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:381029 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:521030 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:321031 if waterfall['name'] in ['tryserver.webrtc',
1032 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:381033 # These waterfalls have their bot configs in a different repo.
1034 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:521035 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381036 raise self.unknown_bot(bot_name, waterfall['name'])
1037
Kenneth Russelleb60cbd22017-12-05 07:54:281038 # All test suites must be referenced.
1039 suites_seen = set()
1040 generator_map = self.get_test_generator_map()
1041 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431042 for bot_name, tester in waterfall['machines'].iteritems():
1043 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281044 if suite_type not in generator_map:
1045 raise self.unknown_test_suite_type(suite_type, bot_name,
1046 waterfall['name'])
1047 if suite not in self.test_suites:
1048 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1049 suites_seen.add(suite)
1050 # Since we didn't resolve the configuration files, this set
1051 # includes both composition test suites and regular ones.
1052 resolved_suites = set()
1053 for suite_name in suites_seen:
1054 suite = self.test_suites[suite_name]
1055 if isinstance(suite, list):
1056 for sub_suite in suite:
1057 resolved_suites.add(sub_suite)
1058 resolved_suites.add(suite_name)
1059 # At this point, every key in test_suites.pyl should be referenced.
1060 missing_suites = set(self.test_suites.keys()) - resolved_suites
1061 if missing_suites:
1062 raise BBGenErr('The following test suites were unreferenced by bots on '
1063 'the waterfalls: ' + str(missing_suites))
1064
1065 # All test suite exceptions must refer to bots on the waterfall.
1066 all_bots = set()
1067 missing_bots = set()
1068 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431069 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281070 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281071 # In order to disambiguate between bots with the same name on
1072 # different waterfalls, support has been added to various
1073 # exceptions for concatenating the waterfall name after the bot
1074 # name.
1075 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281076 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381077 removals = (exception.get('remove_from', []) +
1078 exception.get('remove_gtest_from', []) +
1079 exception.get('modifications', {}).keys())
1080 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281081 if removal not in all_bots:
1082 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411083
1084 missing_bots = missing_bots - set(bots_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281085 if missing_bots:
1086 raise BBGenErr('The following nonexistent machines were referenced in '
1087 'the test suite exceptions: ' + str(missing_bots))
1088
Stephen Martinis0382bc12018-09-17 22:29:071089 # All mixins must be referenced
1090 seen_mixins = set()
1091 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011092 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071093 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011094 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071095 for suite in self.test_suites.values():
1096 if isinstance(suite, list):
1097 # Don't care about this, it's a composition, which shouldn't include a
1098 # swarming mixin.
1099 continue
1100
1101 for test in suite.values():
1102 if not isinstance(test, dict):
1103 # Some test suites have top level keys, which currently can't be
1104 # swarming mixin entries. Ignore them
1105 continue
1106
Stephen Martinisb72f6d22018-10-04 23:29:011107 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071108
Stephen Martinisb72f6d22018-10-04 23:29:011109 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071110 if missing_mixins:
1111 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1112 ' referenced in a waterfall, machine, or test suite.' % (
1113 str(missing_mixins)))
1114
Stephen Martinis54d64ad2018-09-21 22:16:201115
1116 def type_assert(self, node, typ, filename, verbose=False):
1117 """Asserts that the Python AST node |node| is of type |typ|.
1118
1119 If verbose is set, it prints out some helpful context lines, showing where
1120 exactly the error occurred in the file.
1121 """
1122 if not isinstance(node, typ):
1123 if verbose:
1124 lines = [""] + self.read_file(filename).splitlines()
1125
1126 context = 2
1127 lines_start = max(node.lineno - context, 0)
1128 # Add one to include the last line
1129 lines_end = min(node.lineno + context, len(lines)) + 1
1130 lines = (
1131 ['== %s ==\n' % filename] +
1132 ["<snip>\n"] +
1133 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1134 lines[lines_start:lines_start + context])] +
1135 ['-' * 80 + '\n'] +
1136 ['%d %s' % (node.lineno, lines[node.lineno])] +
1137 ['-' * (node.col_offset + 3) + '^' + '-' * (
1138 80 - node.col_offset - 4) + '\n'] +
1139 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1140 lines[node.lineno + 1:lines_end])] +
1141 ["<snip>\n"]
1142 )
1143 # Print out a useful message when a type assertion fails.
1144 for l in lines:
1145 self.print_line(l.strip())
1146
1147 node_dumped = ast.dump(node, annotate_fields=False)
1148 # If the node is huge, truncate it so everything fits in a terminal
1149 # window.
1150 if len(node_dumped) > 60: # pragma: no cover
1151 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1152 raise BBGenErr(
1153 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1154 ' be %s, is %s' % (
1155 filename, node_dumped,
1156 node.lineno, typ, type(node)))
1157
1158 def ensure_ast_dict_keys_sorted(self, node, filename, verbose):
1159 is_valid = True
1160
1161 keys = []
1162 # The keys of this dict are ordered as ordered in the file; normal python
1163 # dictionary keys are given an arbitrary order, but since we parsed the
1164 # file itself, the order as given in the file is preserved.
1165 for key in node.keys:
1166 self.type_assert(key, ast.Str, filename, verbose)
1167 keys.append(key.s)
1168
1169 keys_sorted = sorted(keys)
1170 if keys_sorted != keys:
1171 is_valid = False
1172 if verbose:
1173 for line in difflib.unified_diff(
1174 keys,
1175 keys_sorted, fromfile='current (%r)' % filename, tofile='sorted'):
1176 self.print_line(line)
1177
1178 if len(set(keys)) != len(keys):
1179 for i in range(len(keys_sorted)-1):
1180 if keys_sorted[i] == keys_sorted[i+1]:
1181 self.print_line('Key %s is duplicated' % keys_sorted[i])
1182 is_valid = False
1183 return is_valid
Stephen Martinisf83893722018-09-19 00:02:181184
1185 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201186 # TODO(https://crbug.com/886993): Add the ability for this script to
1187 # actually format the files, rather than just complain if they're
1188 # incorrectly formatted.
1189 bad_files = set()
1190
1191 for filename in (
Stephen Martinisb72f6d22018-10-04 23:29:011192 'mixins.pyl',
Stephen Martinis54d64ad2018-09-21 22:16:201193 'test_suites.pyl',
1194 'test_suite_exceptions.pyl',
1195 ):
Stephen Martinisf83893722018-09-19 00:02:181196 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1197
Stephen Martinisf83893722018-09-19 00:02:181198 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201199 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181200 module = parsed.body
1201
1202 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201203 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181204 if len(module) != 1: # pragma: no cover
1205 raise BBGenErr('Invalid .pyl file %s' % filename)
1206 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201207 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181208
1209 # Value should be a dictionary.
1210 value = expr.value
Stephen Martinis54d64ad2018-09-21 22:16:201211 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181212
Stephen Martinis54d64ad2018-09-21 22:16:201213 if filename == 'test_suites.pyl':
1214 expected_keys = ['basic_suites', 'compound_suites']
1215 actual_keys = [node.s for node in value.keys]
1216 assert all(key in expected_keys for key in actual_keys), (
1217 'Invalid %r file; expected keys %r, got %r' % (
1218 filename, expected_keys, actual_keys))
1219 suite_dicts = [node for node in value.values]
1220 # Only two keys should mean only 1 or 2 values
1221 assert len(suite_dicts) <= 2
1222 for suite_group in suite_dicts:
1223 if not self.ensure_ast_dict_keys_sorted(
1224 suite_group, filename, verbose):
1225 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181226
Stephen Martinis54d64ad2018-09-21 22:16:201227 else:
1228 if not self.ensure_ast_dict_keys_sorted(
1229 value, filename, verbose):
1230 bad_files.add(filename)
1231
1232 # waterfalls.pyl is slightly different, just do it manually here
1233 filename = 'waterfalls.pyl'
1234 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1235
1236 # Must be a module.
1237 self.type_assert(parsed, ast.Module, filename, verbose)
1238 module = parsed.body
1239
1240 # Only one expression in the module.
1241 self.type_assert(module, list, filename, verbose)
1242 if len(module) != 1: # pragma: no cover
1243 raise BBGenErr('Invalid .pyl file %s' % filename)
1244 expr = module[0]
1245 self.type_assert(expr, ast.Expr, filename, verbose)
1246
1247 # Value should be a list.
1248 value = expr.value
1249 self.type_assert(value, ast.List, filename, verbose)
1250
1251 keys = []
1252 for val in value.elts:
1253 self.type_assert(val, ast.Dict, filename, verbose)
1254 waterfall_name = None
1255 for key, val in zip(val.keys, val.values):
1256 self.type_assert(key, ast.Str, filename, verbose)
1257 if key.s == 'machines':
1258 if not self.ensure_ast_dict_keys_sorted(val, filename, verbose):
1259 bad_files.add(filename)
1260
1261 if key.s == "name":
1262 self.type_assert(val, ast.Str, filename, verbose)
1263 waterfall_name = val.s
1264 assert waterfall_name
1265 keys.append(waterfall_name)
1266
1267 if sorted(keys) != keys:
1268 bad_files.add(filename)
1269 if verbose: # pragma: no cover
1270 for line in difflib.unified_diff(
1271 keys,
1272 sorted(keys), fromfile='current', tofile='sorted'):
1273 self.print_line(line)
Stephen Martinisf83893722018-09-19 00:02:181274
1275 if bad_files:
1276 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201277 'The following files have invalid keys: %s\n. They are either '
1278 'unsorted, or have duplicates.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181279
Kenneth Russelleb60cbd22017-12-05 07:54:281280 def check_output_file_consistency(self, verbose=False):
1281 self.load_configuration_files()
1282 # All waterfalls must have been written by this script already.
1283 self.resolve_configuration_files()
1284 ungenerated_waterfalls = set()
1285 for waterfall in self.waterfalls:
1286 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111287 file_path = waterfall['name'] + '.json'
1288 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281289 if expected != current:
1290 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321291 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501292 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281293 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321294 'contents:')
1295 for line in difflib.unified_diff(
1296 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501297 current.splitlines(),
1298 fromfile='expected', tofile='current'):
1299 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281300 if ungenerated_waterfalls:
1301 raise BBGenErr('The following waterfalls have not been properly '
1302 'autogenerated by generate_buildbot_json.py: ' +
1303 str(ungenerated_waterfalls))
1304
1305 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501306 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281307 self.check_output_file_consistency(verbose) # pragma: no cover
1308
1309 def parse_args(self, argv): # pragma: no cover
Karen Qiane24b7ee2019-02-12 23:37:061310
1311 # RawTextHelpFormatter allows for styling of help statement
1312 parser = argparse.ArgumentParser(formatter_class=
1313 argparse.RawTextHelpFormatter)
1314
1315 group = parser.add_mutually_exclusive_group()
1316 group.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281317 '-c', '--check', action='store_true', help=
1318 'Do consistency checks of configuration and generated files and then '
1319 'exit. Used during presubmit. Causes the tool to not generate any files.')
Karen Qiane24b7ee2019-02-12 23:37:061320 group.add_argument(
1321 '--query', type=str, help=
1322 ("Returns raw JSON information of buildbots and tests.\n" +
1323 "Examples:\n" +
1324 " List all bots (all info):\n" +
1325 " --query bots\n\n" +
1326 " List all bots and only their associated tests:\n" +
1327 " --query bots/tests\n\n" +
1328 " List all information about 'bot1' " +
1329 "(make sure you have quotes):\n" +
1330 " --query bot/'bot1'\n\n" +
1331 " List tests running for 'bot1' (make sure you have quotes):\n" +
1332 " --query bot/'bot1'/tests\n\n" +
1333 " List all tests:\n" +
1334 " --query tests\n\n" +
1335 " List all tests and the bots running them:\n" +
1336 " --query tests/bots\n\n"+
1337 " List all tests that satisfy multiple parameters\n" +
1338 " (separation of parameters by '&' symbol):\n" +
1339 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
1340 " List all tests that run with a specific flag:\n" +
1341 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
1342 " List specific test (make sure you have quotes):\n"
1343 " --query test/'test1'\n\n"
1344 " List all bots running 'test1' " +
1345 "(make sure you have quotes):\n" +
1346 " --query test/'test1'/bots" ))
Kenneth Russelleb60cbd22017-12-05 07:54:281347 parser.add_argument(
1348 '-n', '--new-files', action='store_true', help=
1349 'Write output files as .new.json. Useful during development so old and '
1350 'new files can be looked at side-by-side.')
1351 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501352 '-v', '--verbose', action='store_true', help=
1353 'Increases verbosity. Affects consistency checks.')
1354 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281355 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1356 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111357 parser.add_argument(
1358 '--pyl-files-dir', type=os.path.realpath,
1359 help='Path to the directory containing the input .pyl files.')
Karen Qiane24b7ee2019-02-12 23:37:061360 parser.add_argument(
1361 '--json', help=
1362 ("Outputs results into a json file. Only works with query function.\n" +
1363 "Examples:\n" +
1364 " Outputs file into specified json file: \n" +
1365 " --json <file-name-here.json>"))
Kenneth Russelleb60cbd22017-12-05 07:54:281366 self.args = parser.parse_args(argv)
Karen Qiane24b7ee2019-02-12 23:37:061367 if self.args.json and not self.args.query:
1368 parser.error("The --json flag can only be used with --query.")
1369
1370 def does_test_match(self, test_info, params_dict):
1371 """Checks to see if the test matches the parameters given.
1372
1373 Compares the provided test_info with the params_dict to see
1374 if the bot matches the parameters given. If so, returns True.
1375 Else, returns false.
1376
1377 Args:
1378 test_info (dict): Information about a specific bot provided
1379 in the format shown in waterfalls.pyl
1380 params_dict (dict): Dictionary of parameters and their values
1381 to look for in the bot
1382 Ex: {
1383 'device_os':'android',
1384 '--flag':True,
1385 'mixins': ['mixin1', 'mixin2'],
1386 'ex_key':'ex_value'
1387 }
1388
1389 """
1390 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
1391 'kvm', 'pool', 'integrity'] # dimension parameters
1392 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
1393 'can_use_on_swarming_builders']
1394 for param in params_dict:
1395 # if dimension parameter
1396 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
1397 if not 'swarming' in test_info:
1398 return False
1399 swarming = test_info['swarming']
1400 if param in SWARMING_PARAMS:
1401 if not param in swarming:
1402 return False
1403 if not str(swarming[param]) == params_dict[param]:
1404 return False
1405 else:
1406 if not 'dimension_sets' in swarming:
1407 return False
1408 d_set = swarming['dimension_sets']
1409 # only looking at the first dimension set
1410 if not param in d_set[0]:
1411 return False
1412 if not d_set[0][param] == params_dict[param]:
1413 return False
1414
1415 # if flag
1416 elif param.startswith('--'):
1417 if not 'args' in test_info:
1418 return False
1419 if not param in test_info['args']:
1420 return False
1421
1422 # not dimension parameter/flag/mixin
1423 else:
1424 if not param in test_info:
1425 return False
1426 if not test_info[param] == params_dict[param]:
1427 return False
1428 return True
1429 def error_msg(self, msg):
1430 """Prints an error message.
1431
1432 In addition to a catered error message, also prints
1433 out where the user can find more help. Then, program exits.
1434 """
1435 self.print_line(msg + (' If you need more information, ' +
1436 'please run with -h or --help to see valid commands.'))
1437 sys.exit(1)
1438
1439 def find_bots_that_run_test(self, test, bots):
1440 matching_bots = []
1441 for bot in bots:
1442 bot_info = bots[bot]
1443 tests = self.flatten_tests_for_bot(bot_info)
1444 for test_info in tests:
1445 test_name = ""
1446 if 'name' in test_info:
1447 test_name = test_info['name']
1448 elif 'test' in test_info:
1449 test_name = test_info['test']
1450 if not test_name == test:
1451 continue
1452 matching_bots.append(bot)
1453 return matching_bots
1454
1455 def find_tests_with_params(self, tests, params_dict):
1456 matching_tests = []
1457 for test_name in tests:
1458 test_info = tests[test_name]
1459 if not self.does_test_match(test_info, params_dict):
1460 continue
1461 if not test_name in matching_tests:
1462 matching_tests.append(test_name)
1463 return matching_tests
1464
1465 def flatten_waterfalls_for_query(self, waterfalls):
1466 bots = {}
1467 for waterfall in waterfalls:
1468 waterfall_json = json.loads(self.generate_waterfall_json(waterfall))
1469 for bot in waterfall_json:
1470 bot_info = waterfall_json[bot]
1471 if 'AAAAA' not in bot:
1472 bots[bot] = bot_info
1473 return bots
1474
1475 def flatten_tests_for_bot(self, bot_info):
1476 """Returns a list of flattened tests.
1477
1478 Returns a list of tests not grouped by test category
1479 for a specific bot.
1480 """
1481 TEST_CATS = self.get_test_generator_map().keys()
1482 tests = []
1483 for test_cat in TEST_CATS:
1484 if not test_cat in bot_info:
1485 continue
1486 test_cat_tests = bot_info[test_cat]
1487 tests = tests + test_cat_tests
1488 return tests
1489
1490 def flatten_tests_for_query(self, test_suites):
1491 """Returns a flattened dictionary of tests.
1492
1493 Returns a dictionary of tests associate with their
1494 configuration, not grouped by their test suite.
1495 """
1496 tests = {}
1497 for test_suite in test_suites.itervalues():
1498 for test in test_suite:
1499 test_info = test_suite[test]
1500 test_name = test
1501 if 'name' in test_info:
1502 test_name = test_info['name']
1503 tests[test_name] = test_info
1504 return tests
1505
1506 def parse_query_filter_params(self, params):
1507 """Parses the filter parameters.
1508
1509 Creates a dictionary from the parameters provided
1510 to filter the bot array.
1511 """
1512 params_dict = {}
1513 for p in params:
1514 # flag
1515 if p.startswith("--"):
1516 params_dict[p] = True
1517 else:
1518 pair = p.split(":")
1519 if len(pair) != 2:
1520 self.error_msg('Invalid command.')
1521 # regular parameters
1522 if pair[1].lower() == "true":
1523 params_dict[pair[0]] = True
1524 elif pair[1].lower() == "false":
1525 params_dict[pair[0]] = False
1526 else:
1527 params_dict[pair[0]] = pair[1]
1528 return params_dict
1529
1530 def get_test_suites_dict(self, bots):
1531 """Returns a dictionary of bots and their tests.
1532
1533 Returns a dictionary of bots and a list of their associated tests.
1534 """
1535 test_suite_dict = dict()
1536 for bot in bots:
1537 bot_info = bots[bot]
1538 tests = self.flatten_tests_for_bot(bot_info)
1539 test_suite_dict[bot] = tests
1540 return test_suite_dict
1541
1542 def output_query_result(self, result, json_file=None):
1543 """Outputs the result of the query.
1544
1545 If a json file parameter name is provided, then
1546 the result is output into the json file. If not,
1547 then the result is printed to the console.
1548 """
1549 output = json.dumps(result, indent=2)
1550 if json_file:
1551 self.write_file(json_file, output)
1552 else:
1553 self.print_line(output)
1554 return
1555
1556 def query(self, args):
1557 """Queries tests or bots.
1558
1559 Depending on the arguments provided, outputs a json of
1560 tests or bots matching the appropriate optional parameters provided.
1561 """
1562 # split up query statement
1563 query = args.query.split('/')
1564 self.load_configuration_files()
1565 self.resolve_configuration_files()
1566
1567 # flatten bots json
1568 tests = self.test_suites
1569 bots = self.flatten_waterfalls_for_query(self.waterfalls)
1570
1571 cmd_class = query[0]
1572
1573 # For queries starting with 'bots'
1574 if cmd_class == "bots":
1575 if len(query) == 1:
1576 return self.output_query_result(bots, args.json)
1577 # query with specific parameters
1578 elif len(query) == 2:
1579 if query[1] == 'tests':
1580 test_suites_dict = self.get_test_suites_dict(bots)
1581 return self.output_query_result(test_suites_dict, args.json)
1582 else:
1583 self.error_msg("This query should be in the format: bots/tests.")
1584
1585 else:
1586 self.error_msg("This query should have 0 or 1 '/', found %s instead."
1587 % str(len(query)-1))
1588
1589 # For queries starting with 'bot'
1590 elif cmd_class == "bot":
1591 if not len(query) == 2 and not len(query) == 3:
1592 self.error_msg("Command should have 1 or 2 '/', found %s instead."
1593 % str(len(query)-1))
1594 bot_id = query[1]
1595 if not bot_id in bots:
1596 self.error_msg("No bot named '" + bot_id + "' found.")
1597 bot_info = bots[bot_id]
1598 if len(query) == 2:
1599 return self.output_query_result(bot_info, args.json)
1600 if not query[2] == 'tests':
1601 self.error_msg("The query should be in the format:" +
1602 "bot/<bot-name>/tests.")
1603
1604 bot_tests = self.flatten_tests_for_bot(bot_info)
1605 return self.output_query_result(bot_tests, args.json)
1606
1607 # For queries starting with 'tests'
1608 elif cmd_class == "tests":
1609 if not len(query) == 1 and not len(query) == 2:
1610 self.error_msg("The query should have 0 or 1 '/', found %s instead."
1611 % str(len(query)-1))
1612 flattened_tests = self.flatten_tests_for_query(tests)
1613 if len(query) == 1:
1614 return self.output_query_result(flattened_tests, args.json)
1615
1616 # create params dict
1617 params = query[1].split('&')
1618 params_dict = self.parse_query_filter_params(params)
1619 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
1620 return self.output_query_result(matching_bots)
1621
1622 # For queries starting with 'test'
1623 elif cmd_class == "test":
1624 if not len(query) == 2 and not len(query) == 3:
1625 self.error_msg("The query should have 1 or 2 '/', found %s instead."
1626 % str(len(query)-1))
1627 test_id = query[1]
1628 if len(query) == 2:
1629 flattened_tests = self.flatten_tests_for_query(tests)
1630 for test in flattened_tests:
1631 if test == test_id:
1632 return self.output_query_result(flattened_tests[test], args.json)
1633 self.error_msg("There is no test named %s." % test_id)
1634 if not query[2] == 'bots':
1635 self.error_msg("The query should be in the format: " +
1636 "test/<test-name>/bots")
1637 bots_for_test = self.find_bots_that_run_test(test_id, bots)
1638 return self.output_query_result(bots_for_test)
1639
1640 else:
1641 self.error_msg("Your command did not match any valid commands." +
1642 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:281643
1644 def main(self, argv): # pragma: no cover
1645 self.parse_args(argv)
1646 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:501647 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:061648 elif self.args.query:
1649 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:281650 else:
1651 self.generate_waterfalls()
1652 return 0
1653
1654if __name__ == "__main__": # pragma: no cover
1655 generator = BBJSONGenerator()
John Budorick699282e2019-02-13 01:27:331656 sys.exit(generator.main(sys.argv[1:]))