blob: 56180056bae6583b158b8d0d2457dbd829a3f41b [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
Kenneth Russelleb60cbd22017-12-05 07:54:28356 elif update: # pragma: no cover
357 a[key] = b[key] # pragma: no cover
358 else:
359 raise BBGenErr('Conflict at %s' % '.'.join(
360 path + [str(key)])) # pragma: no cover
361 else:
362 a[key] = b[key]
363 return a
364
John Budorickab108712018-09-01 00:12:21365 def initialize_args_for_test(
366 self, generated_test, tester_config, additional_arg_keys=None):
367
368 args = []
369 args.extend(generated_test.get('args', []))
370 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22371
Kenneth Russell8a386d42018-06-02 09:48:01372 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21373 val = generated_test.pop(key, [])
374 if fn(tester_config):
375 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01376
377 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
378 add_conditional_args('linux_args', self.is_linux)
379 add_conditional_args('android_args', self.is_android)
380
John Budorickab108712018-09-01 00:12:21381 for key in additional_arg_keys or []:
382 args.extend(generated_test.pop(key, []))
383 args.extend(tester_config.get(key, []))
384
385 if args:
386 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01387
Kenneth Russelleb60cbd22017-12-05 07:54:28388 def initialize_swarming_dictionary_for_test(self, generated_test,
389 tester_config):
390 if 'swarming' not in generated_test:
391 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28392 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
393 generated_test['swarming'].update({
394 'can_use_on_swarming_builders': tester_config.get('use_swarming', True)
395 })
Kenneth Russelleb60cbd22017-12-05 07:54:28396 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03397 if ('dimension_sets' not in generated_test['swarming'] and
398 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28399 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
400 tester_config['swarming']['dimension_sets'])
401 self.dictionary_merge(generated_test['swarming'],
402 tester_config['swarming'])
403 # Apply any Android-specific Swarming dimensions after the generic ones.
404 if 'android_swarming' in generated_test:
405 if self.is_android(tester_config): # pragma: no cover
406 self.dictionary_merge(
407 generated_test['swarming'],
408 generated_test['android_swarming']) # pragma: no cover
409 del generated_test['android_swarming'] # pragma: no cover
410
411 def clean_swarming_dictionary(self, swarming_dict):
412 # Clean out redundant entries from a test's "swarming" dictionary.
413 # This is really only needed to retain 100% parity with the
414 # handwritten JSON files, and can be removed once all the files are
415 # autogenerated.
416 if 'shards' in swarming_dict:
417 if swarming_dict['shards'] == 1: # pragma: no cover
418 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24419 if 'hard_timeout' in swarming_dict:
420 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
421 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43422 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28423 # Remove all other keys.
424 for k in swarming_dict.keys(): # pragma: no cover
425 if k != 'can_use_on_swarming_builders': # pragma: no cover
426 del swarming_dict[k] # pragma: no cover
427
Stephen Martinis0382bc12018-09-17 22:29:07428 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
429 waterfall):
430 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01431 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07432 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28433 # See if there are any exceptions that need to be merged into this
434 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49435 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28436 if modifications:
437 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23438 if 'swarming' in test:
439 self.clean_swarming_dictionary(test['swarming'])
Kenneth Russelleb60cbd22017-12-05 07:54:28440 return test
441
Shenghua Zhangaba8bad2018-02-07 02:12:09442 def add_common_test_properties(self, test, tester_config):
443 if tester_config.get('use_multi_dimension_trigger_script'):
Kenneth Russell73c3bd8b2018-10-19 22:30:19444 # Assumes update_and_cleanup_test has already been called, so the
445 # builder's mixins have been flattened into the test.
Shenghua Zhangaba8bad2018-02-07 02:12:09446 test['trigger_script'] = {
447 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
448 'args': [
449 '--multiple-trigger-configs',
Kenneth Russell73c3bd8b2018-10-19 22:30:19450 json.dumps(test['swarming']['dimension_sets'] +
Shenghua Zhangaba8bad2018-02-07 02:12:09451 tester_config.get('alternate_swarming_dimensions', [])),
452 '--multiple-dimension-script-verbose',
453 'True'
454 ],
455 }
Ben Pastenea9e583b2019-01-16 02:57:26456 elif self.is_chromeos(tester_config) and tester_config.get('use_swarming',
457 True):
458 # The presence of the "device_type" dimension indicates that the tests
459 # are targetting CrOS hardware and so need the special trigger script.
460 dimension_sets = tester_config['swarming']['dimension_sets']
461 if all('device_type' in ds for ds in dimension_sets):
462 test['trigger_script'] = {
463 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
464 }
Shenghua Zhangaba8bad2018-02-07 02:12:09465
Ben Pastene858f4be2019-01-09 23:52:09466 def add_android_presentation_args(self, tester_config, test_name, result):
467 args = result.get('args', [])
468 args.append('--gs-results-bucket=chromium-result-details')
469 if (result['swarming']['can_use_on_swarming_builders'] and not
470 tester_config.get('skip_merge_script', False)):
471 result['merge'] = {
472 'args': [
473 '--bucket',
474 'chromium-result-details',
475 '--test-name',
476 test_name
477 ],
478 'script': '//build/android/pylib/results/presentation/'
479 'test_results_presentation.py',
480 }
481 if not tester_config.get('skip_cipd_packages', False):
Ben Pastenee5949ea82019-01-10 21:45:26482 cipd_packages = result['swarming'].get('cipd_packages', [])
483 cipd_packages.append(
Ben Pastene858f4be2019-01-09 23:52:09484 {
485 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
486 'location': 'bin',
487 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
488 }
Ben Pastenee5949ea82019-01-10 21:45:26489 )
490 result['swarming']['cipd_packages'] = cipd_packages
Ben Pastene858f4be2019-01-09 23:52:09491 if not tester_config.get('skip_output_links', False):
492 result['swarming']['output_links'] = [
493 {
494 'link': [
495 'https://luci-logdog.appspot.com/v/?s',
496 '=android%2Fswarming%2Flogcats%2F',
497 '${TASK_ID}%2F%2B%2Funified_logcats',
498 ],
499 'name': 'shard #${SHARD_INDEX} logcats',
500 },
501 ]
502 if args:
503 result['args'] = args
504
Kenneth Russelleb60cbd22017-12-05 07:54:28505 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
506 test_config):
507 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15508 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28509 return None
510 result = copy.deepcopy(test_config)
511 if 'test' in result:
512 result['name'] = test_name
513 else:
514 result['test'] = test_name
515 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21516
517 self.initialize_args_for_test(
518 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28519 if self.is_android(tester_config) and tester_config.get('use_swarming',
520 True):
Ben Pastene858f4be2019-01-09 23:52:09521 self.add_android_presentation_args(tester_config, test_name, result)
522 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42523
Stephen Martinis0382bc12018-09-17 22:29:07524 result = self.update_and_cleanup_test(
525 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09526 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43527
528 if not result.get('merge'):
529 # TODO(https://crbug.com/958376): Consider adding the ability to not have
530 # this default.
531 result['merge'] = {
532 'script': '//testing/merge_scripts/standard_gtest_merge.py',
533 'args': [],
534 }
Kenneth Russelleb60cbd22017-12-05 07:54:28535 return result
536
537 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
538 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01539 if not self.should_run_on_tester(waterfall, tester_name, test_name,
540 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28541 return None
542 result = copy.deepcopy(test_config)
543 result['isolate_name'] = result.get('isolate_name', test_name)
544 result['name'] = test_name
545 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01546 self.initialize_args_for_test(result, tester_config)
Ben Pastene858f4be2019-01-09 23:52:09547 if tester_config.get('use_android_presentation', False):
548 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07549 result = self.update_and_cleanup_test(
550 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09551 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28552 return result
553
554 def generate_script_test(self, waterfall, tester_name, tester_config,
555 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44556 # TODO(https://crbug.com/953072): Remove this check whenever a better
557 # long-term solution is implemented.
558 if (waterfall.get('forbid_script_tests', False) or
559 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
560 raise BBGenErr('Attempted to generate a script test on tester ' +
561 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01562 if not self.should_run_on_tester(waterfall, tester_name, test_name,
563 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28564 return None
565 result = {
566 'name': test_name,
567 'script': test_config['script']
568 }
Stephen Martinis0382bc12018-09-17 22:29:07569 result = self.update_and_cleanup_test(
570 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28571 return result
572
573 def generate_junit_test(self, waterfall, tester_name, tester_config,
574 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01575 del tester_config
576 if not self.should_run_on_tester(waterfall, tester_name, test_name,
577 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28578 return None
579 result = {
580 'test': test_name,
581 }
582 return result
583
584 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
585 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01586 if not self.should_run_on_tester(waterfall, tester_name, test_name,
587 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28588 return None
589 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28590 if 'test' in result and result['test'] != test_name:
591 result['name'] = test_name
592 else:
593 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07594 result = self.update_and_cleanup_test(
595 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28596 return result
597
Stephen Martinis2a0667022018-09-25 22:31:14598 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01599 substitutions = {
600 # Any machine in waterfalls.pyl which desires to run GPU tests
601 # must provide the os_type key.
602 'os_type': tester_config['os_type'],
603 'gpu_vendor_id': '0',
604 'gpu_device_id': '0',
605 }
Stephen Martinis2a0667022018-09-25 22:31:14606 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01607 if 'gpu' in dimension_set:
608 # First remove the driver version, then split into vendor and device.
609 gpu = dimension_set['gpu']
Kenneth Russell384a1732019-03-16 02:36:02610 # Handle certain specialized named GPUs.
611 if gpu.startswith('nvidia-quadro-p400'):
612 gpu = ['10de', '1cb3']
613 elif gpu.startswith('intel-hd-630'):
614 gpu = ['8086', '5912']
615 else:
616 gpu = gpu.split('-')[0].split(':')
Kenneth Russell8a386d42018-06-02 09:48:01617 substitutions['gpu_vendor_id'] = gpu[0]
618 substitutions['gpu_device_id'] = gpu[1]
619 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
620
621 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56622 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01623 # These are all just specializations of isolated script tests with
624 # a bunch of boilerplate command line arguments added.
625
626 # The step name must end in 'test' or 'tests' in order for the
627 # results to automatically show up on the flakiness dashboard.
628 # (At least, this was true some time ago.) Continue to use this
629 # naming convention for the time being to minimize changes.
630 step_name = test_config.get('name', test_name)
631 if not (step_name.endswith('test') or step_name.endswith('tests')):
632 step_name = '%s_tests' % step_name
633 result = self.generate_isolated_script_test(
634 waterfall, tester_name, tester_config, step_name, test_config)
635 if not result:
636 return None
637 result['isolate_name'] = 'telemetry_gpu_integration_test'
638 args = result.get('args', [])
639 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14640
641 # These tests upload and download results from cloud storage and therefore
642 # aren't idempotent yet. https://crbug.com/549140.
643 result['swarming']['idempotent'] = False
644
Kenneth Russell44910c32018-12-03 23:35:11645 # The GPU tests act much like integration tests for the entire browser, and
646 # tend to uncover flakiness bugs more readily than other test suites. In
647 # order to surface any flakiness more readily to the developer of the CL
648 # which is introducing it, we disable retries with patch on the commit
649 # queue.
650 result['should_retry_with_patch'] = False
651
Bo Liu555a0f92019-03-29 12:11:56652 browser = ('android-webview-instrumentation'
653 if is_android_webview else tester_config['browser_config'])
Kenneth Russell8a386d42018-06-02 09:48:01654 args = [
Bo Liu555a0f92019-03-29 12:11:56655 test_to_run,
656 '--show-stdout',
657 '--browser=%s' % browser,
658 # --passthrough displays more of the logging in Telemetry when
659 # run via typ, in particular some of the warnings about tests
660 # being expected to fail, but passing.
661 '--passthrough',
662 '-v',
663 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
Kenneth Russell8a386d42018-06-02 09:48:01664 ] + args
665 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14666 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01667 return result
668
Kenneth Russelleb60cbd22017-12-05 07:54:28669 def get_test_generator_map(self):
670 return {
Bo Liu555a0f92019-03-29 12:11:56671 'android_webview_gpu_telemetry_tests':
672 GPUTelemetryTestGenerator(self, is_android_webview=True),
673 'cts_tests':
674 CTSGenerator(self),
675 'gpu_telemetry_tests':
676 GPUTelemetryTestGenerator(self),
677 'gtest_tests':
678 GTestGenerator(self),
679 'instrumentation_tests':
680 InstrumentationTestGenerator(self),
681 'isolated_scripts':
682 IsolatedScriptTestGenerator(self),
683 'junit_tests':
684 JUnitGenerator(self),
685 'scripts':
686 ScriptGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28687 }
688
Kenneth Russell8a386d42018-06-02 09:48:01689 def get_test_type_remapper(self):
690 return {
691 # These are a specialization of isolated_scripts with a bunch of
692 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:56693 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:01694 'gpu_telemetry_tests': 'isolated_scripts',
695 }
696
Kenneth Russelleb60cbd22017-12-05 07:54:28697 def check_composition_test_suites(self):
698 # Pre-pass to catch errors reliably.
699 for name, value in self.test_suites.iteritems():
700 if isinstance(value, list):
701 for entry in value:
702 if isinstance(self.test_suites[entry], list):
Nico Weberd18b8962018-05-16 19:39:38703 raise BBGenErr('Composition test suites may not refer to other '
704 'composition test suites (error found while '
Kenneth Russelleb60cbd22017-12-05 07:54:28705 'processing %s)' % name)
706
Stephen Martinis54d64ad2018-09-21 22:16:20707 def flatten_test_suites(self):
708 new_test_suites = {}
709 for name, value in self.test_suites.get('basic_suites', {}).iteritems():
710 new_test_suites[name] = value
711 for name, value in self.test_suites.get('compound_suites', {}).iteritems():
712 if name in new_test_suites:
713 raise BBGenErr('Composition test suite names may not duplicate basic '
714 'test suite names (error found while processsing %s' % (
715 name))
716 new_test_suites[name] = value
717 self.test_suites = new_test_suites
718
Kenneth Russelleb60cbd22017-12-05 07:54:28719 def resolve_composition_test_suites(self):
Stephen Martinis54d64ad2018-09-21 22:16:20720 self.flatten_test_suites()
721
Kenneth Russelleb60cbd22017-12-05 07:54:28722 self.check_composition_test_suites()
723 for name, value in self.test_suites.iteritems():
724 if isinstance(value, list):
725 # Resolve this to a dictionary.
726 full_suite = {}
727 for entry in value:
728 suite = self.test_suites[entry]
729 full_suite.update(suite)
730 self.test_suites[name] = full_suite
731
732 def link_waterfalls_to_test_suites(self):
733 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43734 for tester_name, tester in waterfall['machines'].iteritems():
735 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28736 if not value in self.test_suites:
737 # Hard / impossible to cover this in the unit test.
738 raise self.unknown_test_suite(
739 value, tester_name, waterfall['name']) # pragma: no cover
740 tester['test_suites'][suite] = self.test_suites[value]
741
742 def load_configuration_files(self):
743 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
744 self.test_suites = self.load_pyl_file('test_suites.pyl')
745 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:01746 self.mixins = self.load_pyl_file('mixins.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:28747
748 def resolve_configuration_files(self):
749 self.resolve_composition_test_suites()
750 self.link_waterfalls_to_test_suites()
751
Nico Weberd18b8962018-05-16 19:39:38752 def unknown_bot(self, bot_name, waterfall_name):
753 return BBGenErr(
754 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
755
Kenneth Russelleb60cbd22017-12-05 07:54:28756 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
757 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38758 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28759 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
760
761 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
762 return BBGenErr(
763 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
764 ' on waterfall ' + waterfall_name)
765
Stephen Martinisb72f6d22018-10-04 23:29:01766 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:07767 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:32768
769 Checks in the waterfall, builder, and test objects for mixins.
770 """
771 def valid_mixin(mixin_name):
772 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:01773 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:32774 raise BBGenErr("bad mixin %s" % mixin_name)
775 def must_be_list(mixins, typ, name):
776 """Asserts that given mixins are a list."""
777 if not isinstance(mixins, list):
778 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
779
Stephen Martinisb72f6d22018-10-04 23:29:01780 if 'mixins' in waterfall:
781 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
782 for mixin in waterfall['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32783 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01784 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32785
Stephen Martinisb72f6d22018-10-04 23:29:01786 if 'mixins' in builder:
787 must_be_list(builder['mixins'], 'builder', builder_name)
788 for mixin in builder['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32789 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01790 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32791
Stephen Martinisb72f6d22018-10-04 23:29:01792 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:07793 return test
794
Stephen Martinis2a0667022018-09-25 22:31:14795 test_name = test.get('name')
796 if not test_name:
797 test_name = test.get('test')
798 if not test_name: # pragma: no cover
799 # Not the best name, but we should say something.
800 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:01801 must_be_list(test['mixins'], 'test', test_name)
802 for mixin in test['mixins']:
Stephen Martinis0382bc12018-09-17 22:29:07803 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01804 test = self.apply_mixin(self.mixins[mixin], test)
805 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:07806 return test
Stephen Martinisb6a50492018-09-12 23:59:32807
Stephen Martinisb72f6d22018-10-04 23:29:01808 def apply_mixin(self, mixin, test):
809 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:32810
Stephen Martinis0382bc12018-09-17 22:29:07811 Mixins will not override an existing key. This is to ensure exceptions can
812 override a setting a mixin applies.
813
Stephen Martinisb72f6d22018-10-04 23:29:01814 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:32815 'dimension_sets', which is how normal test suites specify their dimensions,
816 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
817 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:01818
Stephen Martinisb6a50492018-09-12 23:59:32819 """
820 new_test = copy.deepcopy(test)
821 mixin = copy.deepcopy(mixin)
822
Stephen Martinisb72f6d22018-10-04 23:29:01823 if 'swarming' in mixin:
824 swarming_mixin = mixin['swarming']
825 new_test.setdefault('swarming', {})
826 if 'dimensions' in swarming_mixin:
827 new_test['swarming'].setdefault('dimension_sets', [{}])
828 for dimension_set in new_test['swarming']['dimension_sets']:
829 dimension_set.update(swarming_mixin['dimensions'])
830 del swarming_mixin['dimensions']
Stephen Martinisb6a50492018-09-12 23:59:32831
Stephen Martinisb72f6d22018-10-04 23:29:01832 # python dict update doesn't do recursion at all. Just hard code the
833 # nested update we need (mixin['swarming'] shouldn't clobber
834 # test['swarming'], but should update it).
835 new_test['swarming'].update(swarming_mixin)
836 del mixin['swarming']
837
Wezc0e835b702018-10-30 00:38:41838 if '$mixin_append' in mixin:
839 # Values specified under $mixin_append should be appended to existing
840 # lists, rather than replacing them.
841 mixin_append = mixin['$mixin_append']
842 for key in mixin_append:
843 new_test.setdefault(key, [])
844 if not isinstance(mixin_append[key], list):
845 raise BBGenErr(
846 'Key "' + key + '" in $mixin_append must be a list.')
847 if not isinstance(new_test[key], list):
848 raise BBGenErr(
849 'Cannot apply $mixin_append to non-list "' + key + '".')
850 new_test[key].extend(mixin_append[key])
851 if 'args' in mixin_append:
852 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
853 del mixin['$mixin_append']
854
Stephen Martinisb72f6d22018-10-04 23:29:01855 new_test.update(mixin)
Stephen Martinis0382bc12018-09-17 22:29:07856
Stephen Martinisb6a50492018-09-12 23:59:32857 return new_test
858
Kenneth Russelleb60cbd22017-12-05 07:54:28859 def generate_waterfall_json(self, waterfall):
860 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28861 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:01862 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:43863 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28864 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43865 # Copy only well-understood entries in the machine's configuration
866 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28867 if 'additional_compile_targets' in config:
868 tests['additional_compile_targets'] = config[
869 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43870 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28871 if test_type not in generator_map:
872 raise self.unknown_test_suite_type(
873 test_type, name, waterfall['name']) # pragma: no cover
874 test_generator = generator_map[test_type]
Nico Weber79dc5f6852018-07-13 19:38:49875 # Let multiple kinds of generators generate the same kinds
876 # of tests. For example, gpu_telemetry_tests are a
877 # specialization of isolated_scripts.
878 new_tests = test_generator.generate(
879 waterfall, name, config, input_tests)
880 remapped_test_type = test_type_remapper.get(test_type, test_type)
881 tests[remapped_test_type] = test_generator.sort(
882 tests.get(remapped_test_type, []) + new_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28883 all_tests[name] = tests
884 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
885 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
886 return json.dumps(all_tests, indent=2, separators=(',', ': '),
887 sort_keys=True) + '\n'
888
889 def generate_waterfalls(self): # pragma: no cover
890 self.load_configuration_files()
891 self.resolve_configuration_files()
892 filters = self.args.waterfall_filters
893 suffix = '.json'
894 if self.args.new_files:
895 suffix = '.new' + suffix
896 for waterfall in self.waterfalls:
897 should_gen = not filters or waterfall['name'] in filters
898 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:11899 file_path = waterfall['name'] + suffix
900 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:28901 self.generate_waterfall_json(waterfall))
902
Nico Weberd18b8962018-05-16 19:39:38903 def get_valid_bot_names(self):
John Budorick699282e2019-02-13 01:27:33904 # Extract bot names from infra/config/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:42905 # NOTE: This reference can cause issues; if a file changes there, the
906 # presubmit here won't be run by default. A manually maintained list there
907 # tries to run presubmit here when luci-milo.cfg is changed. If any other
908 # references to configs outside of this directory are added, please change
909 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
910 # never ends up in an invalid state.
Nico Weberd18b8962018-05-16 19:39:38911 bot_names = set()
John Budorickc12abd12018-08-14 19:37:43912 infra_config_dir = os.path.abspath(
913 os.path.join(os.path.dirname(__file__),
John Budorick699282e2019-02-13 01:27:33914 '..', '..', 'infra', 'config'))
John Budorickc12abd12018-08-14 19:37:43915 milo_configs = [
916 os.path.join(infra_config_dir, 'luci-milo.cfg'),
917 os.path.join(infra_config_dir, 'luci-milo-dev.cfg'),
918 ]
919 for c in milo_configs:
920 for l in self.read_file(c).splitlines():
921 if (not 'name: "buildbucket/luci.chromium.' in l and
Hans Wennborg98ffd7d92019-02-06 14:14:34922 not 'name: "buildbucket/luci.chrome.' in l and
John Budorickb1833612018-12-07 04:36:41923 not 'name: "buildbot/chromium.' in l and
924 not 'name: "buildbot/tryserver.chromium.' in l):
John Budorickc12abd12018-08-14 19:37:43925 continue
926 # l looks like
927 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
928 # Extract win_chromium_dbg_ng part.
929 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:38930 return bot_names
931
Kenneth Russell8a386d42018-06-02 09:48:01932 def get_bots_that_do_not_actually_exist(self):
933 # Some of the bots on the chromium.gpu.fyi waterfall in particular
934 # are defined only to be mirrored into trybots, and don't actually
935 # exist on any of the waterfalls or consoles.
936 return [
Jamie Madillda894ce2019-04-08 17:19:17937 'ANGLE GPU Linux Release (Intel HD 630)',
938 'ANGLE GPU Linux Release (NVIDIA)',
939 'ANGLE GPU Mac Release (Intel)',
940 'ANGLE GPU Mac Retina Release (AMD)',
941 'ANGLE GPU Mac Retina Release (NVIDIA)',
Jamie Madilldc7feeb82018-11-14 04:54:56942 'ANGLE GPU Win10 Release (Intel HD 630)',
943 'ANGLE GPU Win10 Release (NVIDIA)',
Corentin Wallez7d3f4fa22018-11-19 23:35:44944 'Dawn GPU Linux Release (Intel HD 630)',
945 'Dawn GPU Linux Release (NVIDIA)',
946 'Dawn GPU Mac Release (Intel)',
947 'Dawn GPU Mac Retina Release (AMD)',
948 'Dawn GPU Mac Retina Release (NVIDIA)',
Jamie Madilldc7feeb82018-11-14 04:54:56949 'Dawn GPU Win10 Release (Intel HD 630)',
950 'Dawn GPU Win10 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:01951 'Optional Android Release (Nexus 5X)',
952 'Optional Linux Release (Intel HD 630)',
953 'Optional Linux Release (NVIDIA)',
954 'Optional Mac Release (Intel)',
955 'Optional Mac Retina Release (AMD)',
956 'Optional Mac Retina Release (NVIDIA)',
957 'Optional Win10 Release (Intel HD 630)',
958 'Optional Win10 Release (NVIDIA)',
959 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:08960 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:29961 'linux-blink-rel-dummy',
962 'mac10.10-blink-rel-dummy',
963 'mac10.11-blink-rel-dummy',
964 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:20965 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:29966 'mac10.13-blink-rel-dummy',
967 'win7-blink-rel-dummy',
968 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:08969 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:33970 'WebKit Linux composite_after_paint Dummy Builder',
Nico Weber7fc8b9da2018-06-08 19:22:08971 'WebKit Linux layout_ng Dummy Builder',
972 'WebKit Linux root_layer_scrolls Dummy Builder',
Stephen Martinis769b25112018-08-30 18:52:06973 # chromium, due to https://crbug.com/878915
974 'win-dbg',
975 'win32-dbg',
Stephen Martinis47d77132019-04-24 23:51:33976 'win-archive-dbg',
977 'win32-archive-dbg',
Stephen Martinis07a9f742019-03-20 19:16:56978 # chromium.mac, see https://crbug.com/943804
979 'mac-dummy-rel',
Ben Pastene7687c0112019-03-05 22:43:14980 # Defined in internal configs.
981 'chromeos-amd64-generic-google-rel',
Anushruth9420fddf2019-04-04 00:24:59982 'chromeos-betty-google-rel',
Stephen Martinis47d77132019-04-24 23:51:33983 # chromium, https://crbug.com/888810
984 'android-archive-dbg',
985 'android-archive-rel',
986 'linux-archive-dbg',
987 'linux-archive-rel',
988 'mac-archive-dbg',
989 'mac-archive-rel',
990 'win-archive-rel',
991 'win32-archive-rel',
Yuke Liaobc9ff982019-04-30 06:56:16992 # code coverage, see see https://crbug.com/930364
993 'Linux Builder Code Coverage',
994 'Linux Tests Code Coverage',
995 'GPU Linux Builder Code Coverage',
996 'Linux Release Code Coverage (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:01997 ]
998
Stephen Martinisf83893722018-09-19 00:02:18999 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201000 self.check_input_files_sorting(verbose)
1001
Kenneth Russelleb60cbd22017-12-05 07:54:281002 self.load_configuration_files()
Stephen Martinis54d64ad2018-09-21 22:16:201003 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281004 self.check_composition_test_suites()
Nico Weberd18b8962018-05-16 19:39:381005
1006 # All bots should exist.
1007 bot_names = self.get_valid_bot_names()
Kenneth Russell8a386d42018-06-02 09:48:011008 bots_that_dont_exist = self.get_bots_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:381009 for waterfall in self.waterfalls:
1010 for bot_name in waterfall['machines']:
Kenneth Russell8a386d42018-06-02 09:48:011011 if bot_name in bots_that_dont_exist:
1012 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381013 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:081014 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:381015 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:521016 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:321017 if waterfall['name'] in ['tryserver.webrtc',
1018 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:381019 # These waterfalls have their bot configs in a different repo.
1020 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:521021 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381022 raise self.unknown_bot(bot_name, waterfall['name'])
1023
Kenneth Russelleb60cbd22017-12-05 07:54:281024 # All test suites must be referenced.
1025 suites_seen = set()
1026 generator_map = self.get_test_generator_map()
1027 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431028 for bot_name, tester in waterfall['machines'].iteritems():
1029 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281030 if suite_type not in generator_map:
1031 raise self.unknown_test_suite_type(suite_type, bot_name,
1032 waterfall['name'])
1033 if suite not in self.test_suites:
1034 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1035 suites_seen.add(suite)
1036 # Since we didn't resolve the configuration files, this set
1037 # includes both composition test suites and regular ones.
1038 resolved_suites = set()
1039 for suite_name in suites_seen:
1040 suite = self.test_suites[suite_name]
1041 if isinstance(suite, list):
1042 for sub_suite in suite:
1043 resolved_suites.add(sub_suite)
1044 resolved_suites.add(suite_name)
1045 # At this point, every key in test_suites.pyl should be referenced.
1046 missing_suites = set(self.test_suites.keys()) - resolved_suites
1047 if missing_suites:
1048 raise BBGenErr('The following test suites were unreferenced by bots on '
1049 'the waterfalls: ' + str(missing_suites))
1050
1051 # All test suite exceptions must refer to bots on the waterfall.
1052 all_bots = set()
1053 missing_bots = set()
1054 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431055 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281056 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281057 # In order to disambiguate between bots with the same name on
1058 # different waterfalls, support has been added to various
1059 # exceptions for concatenating the waterfall name after the bot
1060 # name.
1061 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281062 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381063 removals = (exception.get('remove_from', []) +
1064 exception.get('remove_gtest_from', []) +
1065 exception.get('modifications', {}).keys())
1066 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281067 if removal not in all_bots:
1068 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411069
1070 missing_bots = missing_bots - set(bots_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281071 if missing_bots:
1072 raise BBGenErr('The following nonexistent machines were referenced in '
1073 'the test suite exceptions: ' + str(missing_bots))
1074
Stephen Martinis0382bc12018-09-17 22:29:071075 # All mixins must be referenced
1076 seen_mixins = set()
1077 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011078 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071079 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011080 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071081 for suite in self.test_suites.values():
1082 if isinstance(suite, list):
1083 # Don't care about this, it's a composition, which shouldn't include a
1084 # swarming mixin.
1085 continue
1086
1087 for test in suite.values():
1088 if not isinstance(test, dict):
1089 # Some test suites have top level keys, which currently can't be
1090 # swarming mixin entries. Ignore them
1091 continue
1092
Stephen Martinisb72f6d22018-10-04 23:29:011093 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071094
Stephen Martinisb72f6d22018-10-04 23:29:011095 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071096 if missing_mixins:
1097 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1098 ' referenced in a waterfall, machine, or test suite.' % (
1099 str(missing_mixins)))
1100
Stephen Martinis54d64ad2018-09-21 22:16:201101
1102 def type_assert(self, node, typ, filename, verbose=False):
1103 """Asserts that the Python AST node |node| is of type |typ|.
1104
1105 If verbose is set, it prints out some helpful context lines, showing where
1106 exactly the error occurred in the file.
1107 """
1108 if not isinstance(node, typ):
1109 if verbose:
1110 lines = [""] + self.read_file(filename).splitlines()
1111
1112 context = 2
1113 lines_start = max(node.lineno - context, 0)
1114 # Add one to include the last line
1115 lines_end = min(node.lineno + context, len(lines)) + 1
1116 lines = (
1117 ['== %s ==\n' % filename] +
1118 ["<snip>\n"] +
1119 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1120 lines[lines_start:lines_start + context])] +
1121 ['-' * 80 + '\n'] +
1122 ['%d %s' % (node.lineno, lines[node.lineno])] +
1123 ['-' * (node.col_offset + 3) + '^' + '-' * (
1124 80 - node.col_offset - 4) + '\n'] +
1125 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1126 lines[node.lineno + 1:lines_end])] +
1127 ["<snip>\n"]
1128 )
1129 # Print out a useful message when a type assertion fails.
1130 for l in lines:
1131 self.print_line(l.strip())
1132
1133 node_dumped = ast.dump(node, annotate_fields=False)
1134 # If the node is huge, truncate it so everything fits in a terminal
1135 # window.
1136 if len(node_dumped) > 60: # pragma: no cover
1137 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1138 raise BBGenErr(
1139 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1140 ' be %s, is %s' % (
1141 filename, node_dumped,
1142 node.lineno, typ, type(node)))
1143
1144 def ensure_ast_dict_keys_sorted(self, node, filename, verbose):
1145 is_valid = True
1146
1147 keys = []
1148 # The keys of this dict are ordered as ordered in the file; normal python
1149 # dictionary keys are given an arbitrary order, but since we parsed the
1150 # file itself, the order as given in the file is preserved.
1151 for key in node.keys:
1152 self.type_assert(key, ast.Str, filename, verbose)
1153 keys.append(key.s)
1154
1155 keys_sorted = sorted(keys)
1156 if keys_sorted != keys:
1157 is_valid = False
1158 if verbose:
1159 for line in difflib.unified_diff(
1160 keys,
1161 keys_sorted, fromfile='current (%r)' % filename, tofile='sorted'):
1162 self.print_line(line)
1163
1164 if len(set(keys)) != len(keys):
1165 for i in range(len(keys_sorted)-1):
1166 if keys_sorted[i] == keys_sorted[i+1]:
1167 self.print_line('Key %s is duplicated' % keys_sorted[i])
1168 is_valid = False
1169 return is_valid
Stephen Martinisf83893722018-09-19 00:02:181170
1171 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201172 # TODO(https://crbug.com/886993): Add the ability for this script to
1173 # actually format the files, rather than just complain if they're
1174 # incorrectly formatted.
1175 bad_files = set()
1176
1177 for filename in (
Stephen Martinisb72f6d22018-10-04 23:29:011178 'mixins.pyl',
Stephen Martinis54d64ad2018-09-21 22:16:201179 'test_suites.pyl',
1180 'test_suite_exceptions.pyl',
1181 ):
Stephen Martinisf83893722018-09-19 00:02:181182 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1183
Stephen Martinisf83893722018-09-19 00:02:181184 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201185 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181186 module = parsed.body
1187
1188 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201189 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181190 if len(module) != 1: # pragma: no cover
1191 raise BBGenErr('Invalid .pyl file %s' % filename)
1192 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201193 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181194
1195 # Value should be a dictionary.
1196 value = expr.value
Stephen Martinis54d64ad2018-09-21 22:16:201197 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181198
Stephen Martinis54d64ad2018-09-21 22:16:201199 if filename == 'test_suites.pyl':
1200 expected_keys = ['basic_suites', 'compound_suites']
1201 actual_keys = [node.s for node in value.keys]
1202 assert all(key in expected_keys for key in actual_keys), (
1203 'Invalid %r file; expected keys %r, got %r' % (
1204 filename, expected_keys, actual_keys))
1205 suite_dicts = [node for node in value.values]
1206 # Only two keys should mean only 1 or 2 values
1207 assert len(suite_dicts) <= 2
1208 for suite_group in suite_dicts:
1209 if not self.ensure_ast_dict_keys_sorted(
1210 suite_group, filename, verbose):
1211 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181212
Stephen Martinis54d64ad2018-09-21 22:16:201213 else:
1214 if not self.ensure_ast_dict_keys_sorted(
1215 value, filename, verbose):
1216 bad_files.add(filename)
1217
1218 # waterfalls.pyl is slightly different, just do it manually here
1219 filename = 'waterfalls.pyl'
1220 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1221
1222 # Must be a module.
1223 self.type_assert(parsed, ast.Module, filename, verbose)
1224 module = parsed.body
1225
1226 # Only one expression in the module.
1227 self.type_assert(module, list, filename, verbose)
1228 if len(module) != 1: # pragma: no cover
1229 raise BBGenErr('Invalid .pyl file %s' % filename)
1230 expr = module[0]
1231 self.type_assert(expr, ast.Expr, filename, verbose)
1232
1233 # Value should be a list.
1234 value = expr.value
1235 self.type_assert(value, ast.List, filename, verbose)
1236
1237 keys = []
1238 for val in value.elts:
1239 self.type_assert(val, ast.Dict, filename, verbose)
1240 waterfall_name = None
1241 for key, val in zip(val.keys, val.values):
1242 self.type_assert(key, ast.Str, filename, verbose)
1243 if key.s == 'machines':
1244 if not self.ensure_ast_dict_keys_sorted(val, filename, verbose):
1245 bad_files.add(filename)
1246
1247 if key.s == "name":
1248 self.type_assert(val, ast.Str, filename, verbose)
1249 waterfall_name = val.s
1250 assert waterfall_name
1251 keys.append(waterfall_name)
1252
1253 if sorted(keys) != keys:
1254 bad_files.add(filename)
1255 if verbose: # pragma: no cover
1256 for line in difflib.unified_diff(
1257 keys,
1258 sorted(keys), fromfile='current', tofile='sorted'):
1259 self.print_line(line)
Stephen Martinisf83893722018-09-19 00:02:181260
1261 if bad_files:
1262 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201263 'The following files have invalid keys: %s\n. They are either '
1264 'unsorted, or have duplicates.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181265
Kenneth Russelleb60cbd22017-12-05 07:54:281266 def check_output_file_consistency(self, verbose=False):
1267 self.load_configuration_files()
1268 # All waterfalls must have been written by this script already.
1269 self.resolve_configuration_files()
1270 ungenerated_waterfalls = set()
1271 for waterfall in self.waterfalls:
1272 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111273 file_path = waterfall['name'] + '.json'
1274 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281275 if expected != current:
1276 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321277 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501278 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281279 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321280 'contents:')
1281 for line in difflib.unified_diff(
1282 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501283 current.splitlines(),
1284 fromfile='expected', tofile='current'):
1285 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281286 if ungenerated_waterfalls:
1287 raise BBGenErr('The following waterfalls have not been properly '
1288 'autogenerated by generate_buildbot_json.py: ' +
1289 str(ungenerated_waterfalls))
1290
1291 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501292 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281293 self.check_output_file_consistency(verbose) # pragma: no cover
1294
1295 def parse_args(self, argv): # pragma: no cover
Karen Qiane24b7ee2019-02-12 23:37:061296
1297 # RawTextHelpFormatter allows for styling of help statement
1298 parser = argparse.ArgumentParser(formatter_class=
1299 argparse.RawTextHelpFormatter)
1300
1301 group = parser.add_mutually_exclusive_group()
1302 group.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281303 '-c', '--check', action='store_true', help=
1304 'Do consistency checks of configuration and generated files and then '
1305 'exit. Used during presubmit. Causes the tool to not generate any files.')
Karen Qiane24b7ee2019-02-12 23:37:061306 group.add_argument(
1307 '--query', type=str, help=
1308 ("Returns raw JSON information of buildbots and tests.\n" +
1309 "Examples:\n" +
1310 " List all bots (all info):\n" +
1311 " --query bots\n\n" +
1312 " List all bots and only their associated tests:\n" +
1313 " --query bots/tests\n\n" +
1314 " List all information about 'bot1' " +
1315 "(make sure you have quotes):\n" +
1316 " --query bot/'bot1'\n\n" +
1317 " List tests running for 'bot1' (make sure you have quotes):\n" +
1318 " --query bot/'bot1'/tests\n\n" +
1319 " List all tests:\n" +
1320 " --query tests\n\n" +
1321 " List all tests and the bots running them:\n" +
1322 " --query tests/bots\n\n"+
1323 " List all tests that satisfy multiple parameters\n" +
1324 " (separation of parameters by '&' symbol):\n" +
1325 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
1326 " List all tests that run with a specific flag:\n" +
1327 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
1328 " List specific test (make sure you have quotes):\n"
1329 " --query test/'test1'\n\n"
1330 " List all bots running 'test1' " +
1331 "(make sure you have quotes):\n" +
1332 " --query test/'test1'/bots" ))
Kenneth Russelleb60cbd22017-12-05 07:54:281333 parser.add_argument(
1334 '-n', '--new-files', action='store_true', help=
1335 'Write output files as .new.json. Useful during development so old and '
1336 'new files can be looked at side-by-side.')
1337 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501338 '-v', '--verbose', action='store_true', help=
1339 'Increases verbosity. Affects consistency checks.')
1340 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281341 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1342 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111343 parser.add_argument(
1344 '--pyl-files-dir', type=os.path.realpath,
1345 help='Path to the directory containing the input .pyl files.')
Karen Qiane24b7ee2019-02-12 23:37:061346 parser.add_argument(
1347 '--json', help=
1348 ("Outputs results into a json file. Only works with query function.\n" +
1349 "Examples:\n" +
1350 " Outputs file into specified json file: \n" +
1351 " --json <file-name-here.json>"))
Kenneth Russelleb60cbd22017-12-05 07:54:281352 self.args = parser.parse_args(argv)
Karen Qiane24b7ee2019-02-12 23:37:061353 if self.args.json and not self.args.query:
1354 parser.error("The --json flag can only be used with --query.")
1355
1356 def does_test_match(self, test_info, params_dict):
1357 """Checks to see if the test matches the parameters given.
1358
1359 Compares the provided test_info with the params_dict to see
1360 if the bot matches the parameters given. If so, returns True.
1361 Else, returns false.
1362
1363 Args:
1364 test_info (dict): Information about a specific bot provided
1365 in the format shown in waterfalls.pyl
1366 params_dict (dict): Dictionary of parameters and their values
1367 to look for in the bot
1368 Ex: {
1369 'device_os':'android',
1370 '--flag':True,
1371 'mixins': ['mixin1', 'mixin2'],
1372 'ex_key':'ex_value'
1373 }
1374
1375 """
1376 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
1377 'kvm', 'pool', 'integrity'] # dimension parameters
1378 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
1379 'can_use_on_swarming_builders']
1380 for param in params_dict:
1381 # if dimension parameter
1382 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
1383 if not 'swarming' in test_info:
1384 return False
1385 swarming = test_info['swarming']
1386 if param in SWARMING_PARAMS:
1387 if not param in swarming:
1388 return False
1389 if not str(swarming[param]) == params_dict[param]:
1390 return False
1391 else:
1392 if not 'dimension_sets' in swarming:
1393 return False
1394 d_set = swarming['dimension_sets']
1395 # only looking at the first dimension set
1396 if not param in d_set[0]:
1397 return False
1398 if not d_set[0][param] == params_dict[param]:
1399 return False
1400
1401 # if flag
1402 elif param.startswith('--'):
1403 if not 'args' in test_info:
1404 return False
1405 if not param in test_info['args']:
1406 return False
1407
1408 # not dimension parameter/flag/mixin
1409 else:
1410 if not param in test_info:
1411 return False
1412 if not test_info[param] == params_dict[param]:
1413 return False
1414 return True
1415 def error_msg(self, msg):
1416 """Prints an error message.
1417
1418 In addition to a catered error message, also prints
1419 out where the user can find more help. Then, program exits.
1420 """
1421 self.print_line(msg + (' If you need more information, ' +
1422 'please run with -h or --help to see valid commands.'))
1423 sys.exit(1)
1424
1425 def find_bots_that_run_test(self, test, bots):
1426 matching_bots = []
1427 for bot in bots:
1428 bot_info = bots[bot]
1429 tests = self.flatten_tests_for_bot(bot_info)
1430 for test_info in tests:
1431 test_name = ""
1432 if 'name' in test_info:
1433 test_name = test_info['name']
1434 elif 'test' in test_info:
1435 test_name = test_info['test']
1436 if not test_name == test:
1437 continue
1438 matching_bots.append(bot)
1439 return matching_bots
1440
1441 def find_tests_with_params(self, tests, params_dict):
1442 matching_tests = []
1443 for test_name in tests:
1444 test_info = tests[test_name]
1445 if not self.does_test_match(test_info, params_dict):
1446 continue
1447 if not test_name in matching_tests:
1448 matching_tests.append(test_name)
1449 return matching_tests
1450
1451 def flatten_waterfalls_for_query(self, waterfalls):
1452 bots = {}
1453 for waterfall in waterfalls:
1454 waterfall_json = json.loads(self.generate_waterfall_json(waterfall))
1455 for bot in waterfall_json:
1456 bot_info = waterfall_json[bot]
1457 if 'AAAAA' not in bot:
1458 bots[bot] = bot_info
1459 return bots
1460
1461 def flatten_tests_for_bot(self, bot_info):
1462 """Returns a list of flattened tests.
1463
1464 Returns a list of tests not grouped by test category
1465 for a specific bot.
1466 """
1467 TEST_CATS = self.get_test_generator_map().keys()
1468 tests = []
1469 for test_cat in TEST_CATS:
1470 if not test_cat in bot_info:
1471 continue
1472 test_cat_tests = bot_info[test_cat]
1473 tests = tests + test_cat_tests
1474 return tests
1475
1476 def flatten_tests_for_query(self, test_suites):
1477 """Returns a flattened dictionary of tests.
1478
1479 Returns a dictionary of tests associate with their
1480 configuration, not grouped by their test suite.
1481 """
1482 tests = {}
1483 for test_suite in test_suites.itervalues():
1484 for test in test_suite:
1485 test_info = test_suite[test]
1486 test_name = test
1487 if 'name' in test_info:
1488 test_name = test_info['name']
1489 tests[test_name] = test_info
1490 return tests
1491
1492 def parse_query_filter_params(self, params):
1493 """Parses the filter parameters.
1494
1495 Creates a dictionary from the parameters provided
1496 to filter the bot array.
1497 """
1498 params_dict = {}
1499 for p in params:
1500 # flag
1501 if p.startswith("--"):
1502 params_dict[p] = True
1503 else:
1504 pair = p.split(":")
1505 if len(pair) != 2:
1506 self.error_msg('Invalid command.')
1507 # regular parameters
1508 if pair[1].lower() == "true":
1509 params_dict[pair[0]] = True
1510 elif pair[1].lower() == "false":
1511 params_dict[pair[0]] = False
1512 else:
1513 params_dict[pair[0]] = pair[1]
1514 return params_dict
1515
1516 def get_test_suites_dict(self, bots):
1517 """Returns a dictionary of bots and their tests.
1518
1519 Returns a dictionary of bots and a list of their associated tests.
1520 """
1521 test_suite_dict = dict()
1522 for bot in bots:
1523 bot_info = bots[bot]
1524 tests = self.flatten_tests_for_bot(bot_info)
1525 test_suite_dict[bot] = tests
1526 return test_suite_dict
1527
1528 def output_query_result(self, result, json_file=None):
1529 """Outputs the result of the query.
1530
1531 If a json file parameter name is provided, then
1532 the result is output into the json file. If not,
1533 then the result is printed to the console.
1534 """
1535 output = json.dumps(result, indent=2)
1536 if json_file:
1537 self.write_file(json_file, output)
1538 else:
1539 self.print_line(output)
1540 return
1541
1542 def query(self, args):
1543 """Queries tests or bots.
1544
1545 Depending on the arguments provided, outputs a json of
1546 tests or bots matching the appropriate optional parameters provided.
1547 """
1548 # split up query statement
1549 query = args.query.split('/')
1550 self.load_configuration_files()
1551 self.resolve_configuration_files()
1552
1553 # flatten bots json
1554 tests = self.test_suites
1555 bots = self.flatten_waterfalls_for_query(self.waterfalls)
1556
1557 cmd_class = query[0]
1558
1559 # For queries starting with 'bots'
1560 if cmd_class == "bots":
1561 if len(query) == 1:
1562 return self.output_query_result(bots, args.json)
1563 # query with specific parameters
1564 elif len(query) == 2:
1565 if query[1] == 'tests':
1566 test_suites_dict = self.get_test_suites_dict(bots)
1567 return self.output_query_result(test_suites_dict, args.json)
1568 else:
1569 self.error_msg("This query should be in the format: bots/tests.")
1570
1571 else:
1572 self.error_msg("This query should have 0 or 1 '/', found %s instead."
1573 % str(len(query)-1))
1574
1575 # For queries starting with 'bot'
1576 elif cmd_class == "bot":
1577 if not len(query) == 2 and not len(query) == 3:
1578 self.error_msg("Command should have 1 or 2 '/', found %s instead."
1579 % str(len(query)-1))
1580 bot_id = query[1]
1581 if not bot_id in bots:
1582 self.error_msg("No bot named '" + bot_id + "' found.")
1583 bot_info = bots[bot_id]
1584 if len(query) == 2:
1585 return self.output_query_result(bot_info, args.json)
1586 if not query[2] == 'tests':
1587 self.error_msg("The query should be in the format:" +
1588 "bot/<bot-name>/tests.")
1589
1590 bot_tests = self.flatten_tests_for_bot(bot_info)
1591 return self.output_query_result(bot_tests, args.json)
1592
1593 # For queries starting with 'tests'
1594 elif cmd_class == "tests":
1595 if not len(query) == 1 and not len(query) == 2:
1596 self.error_msg("The query should have 0 or 1 '/', found %s instead."
1597 % str(len(query)-1))
1598 flattened_tests = self.flatten_tests_for_query(tests)
1599 if len(query) == 1:
1600 return self.output_query_result(flattened_tests, args.json)
1601
1602 # create params dict
1603 params = query[1].split('&')
1604 params_dict = self.parse_query_filter_params(params)
1605 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
1606 return self.output_query_result(matching_bots)
1607
1608 # For queries starting with 'test'
1609 elif cmd_class == "test":
1610 if not len(query) == 2 and not len(query) == 3:
1611 self.error_msg("The query should have 1 or 2 '/', found %s instead."
1612 % str(len(query)-1))
1613 test_id = query[1]
1614 if len(query) == 2:
1615 flattened_tests = self.flatten_tests_for_query(tests)
1616 for test in flattened_tests:
1617 if test == test_id:
1618 return self.output_query_result(flattened_tests[test], args.json)
1619 self.error_msg("There is no test named %s." % test_id)
1620 if not query[2] == 'bots':
1621 self.error_msg("The query should be in the format: " +
1622 "test/<test-name>/bots")
1623 bots_for_test = self.find_bots_that_run_test(test_id, bots)
1624 return self.output_query_result(bots_for_test)
1625
1626 else:
1627 self.error_msg("Your command did not match any valid commands." +
1628 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:281629
1630 def main(self, argv): # pragma: no cover
1631 self.parse_args(argv)
1632 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:501633 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:061634 elif self.args.query:
1635 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:281636 else:
1637 self.generate_waterfalls()
1638 return 0
1639
1640if __name__ == "__main__": # pragma: no cover
1641 generator = BBJSONGenerator()
John Budorick699282e2019-02-13 01:27:331642 sys.exit(generator.main(sys.argv[1:]))