blob: 3aadda730c225383929ad7b94e1087f06645e911 [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)
Stephen Martinisf50047062019-05-06 22:26:17552
553 if not result.get('merge'):
554 # TODO(https://crbug.com/958376): Consider adding the ability to not have
555 # this default.
556 result['merge'] = {
557 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
558 'args': [],
559 }
Kenneth Russelleb60cbd22017-12-05 07:54:28560 return result
561
562 def generate_script_test(self, waterfall, tester_name, tester_config,
563 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44564 # TODO(https://crbug.com/953072): Remove this check whenever a better
565 # long-term solution is implemented.
566 if (waterfall.get('forbid_script_tests', False) or
567 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
568 raise BBGenErr('Attempted to generate a script test on tester ' +
569 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01570 if not self.should_run_on_tester(waterfall, tester_name, test_name,
571 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28572 return None
573 result = {
574 'name': test_name,
575 'script': test_config['script']
576 }
Stephen Martinis0382bc12018-09-17 22:29:07577 result = self.update_and_cleanup_test(
578 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28579 return result
580
581 def generate_junit_test(self, waterfall, tester_name, tester_config,
582 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01583 del tester_config
584 if not self.should_run_on_tester(waterfall, tester_name, test_name,
585 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28586 return None
587 result = {
588 'test': test_name,
589 }
590 return result
591
592 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
593 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01594 if not self.should_run_on_tester(waterfall, tester_name, test_name,
595 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28596 return None
597 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28598 if 'test' in result and result['test'] != test_name:
599 result['name'] = test_name
600 else:
601 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07602 result = self.update_and_cleanup_test(
603 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28604 return result
605
Stephen Martinis2a0667022018-09-25 22:31:14606 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01607 substitutions = {
608 # Any machine in waterfalls.pyl which desires to run GPU tests
609 # must provide the os_type key.
610 'os_type': tester_config['os_type'],
611 'gpu_vendor_id': '0',
612 'gpu_device_id': '0',
613 }
Stephen Martinis2a0667022018-09-25 22:31:14614 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01615 if 'gpu' in dimension_set:
616 # First remove the driver version, then split into vendor and device.
617 gpu = dimension_set['gpu']
Kenneth Russell384a1732019-03-16 02:36:02618 # Handle certain specialized named GPUs.
619 if gpu.startswith('nvidia-quadro-p400'):
620 gpu = ['10de', '1cb3']
621 elif gpu.startswith('intel-hd-630'):
622 gpu = ['8086', '5912']
623 else:
624 gpu = gpu.split('-')[0].split(':')
Kenneth Russell8a386d42018-06-02 09:48:01625 substitutions['gpu_vendor_id'] = gpu[0]
626 substitutions['gpu_device_id'] = gpu[1]
627 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
628
629 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56630 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01631 # These are all just specializations of isolated script tests with
632 # a bunch of boilerplate command line arguments added.
633
634 # The step name must end in 'test' or 'tests' in order for the
635 # results to automatically show up on the flakiness dashboard.
636 # (At least, this was true some time ago.) Continue to use this
637 # naming convention for the time being to minimize changes.
638 step_name = test_config.get('name', test_name)
639 if not (step_name.endswith('test') or step_name.endswith('tests')):
640 step_name = '%s_tests' % step_name
641 result = self.generate_isolated_script_test(
642 waterfall, tester_name, tester_config, step_name, test_config)
643 if not result:
644 return None
645 result['isolate_name'] = 'telemetry_gpu_integration_test'
646 args = result.get('args', [])
647 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14648
649 # These tests upload and download results from cloud storage and therefore
650 # aren't idempotent yet. https://crbug.com/549140.
651 result['swarming']['idempotent'] = False
652
Kenneth Russell44910c32018-12-03 23:35:11653 # The GPU tests act much like integration tests for the entire browser, and
654 # tend to uncover flakiness bugs more readily than other test suites. In
655 # order to surface any flakiness more readily to the developer of the CL
656 # which is introducing it, we disable retries with patch on the commit
657 # queue.
658 result['should_retry_with_patch'] = False
659
Bo Liu555a0f92019-03-29 12:11:56660 browser = ('android-webview-instrumentation'
661 if is_android_webview else tester_config['browser_config'])
Kenneth Russell8a386d42018-06-02 09:48:01662 args = [
Bo Liu555a0f92019-03-29 12:11:56663 test_to_run,
664 '--show-stdout',
665 '--browser=%s' % browser,
666 # --passthrough displays more of the logging in Telemetry when
667 # run via typ, in particular some of the warnings about tests
668 # being expected to fail, but passing.
669 '--passthrough',
670 '-v',
671 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
Kenneth Russell8a386d42018-06-02 09:48:01672 ] + args
673 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14674 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01675 return result
676
Kenneth Russelleb60cbd22017-12-05 07:54:28677 def get_test_generator_map(self):
678 return {
Bo Liu555a0f92019-03-29 12:11:56679 'android_webview_gpu_telemetry_tests':
680 GPUTelemetryTestGenerator(self, is_android_webview=True),
681 'cts_tests':
682 CTSGenerator(self),
683 'gpu_telemetry_tests':
684 GPUTelemetryTestGenerator(self),
685 'gtest_tests':
686 GTestGenerator(self),
687 'instrumentation_tests':
688 InstrumentationTestGenerator(self),
689 'isolated_scripts':
690 IsolatedScriptTestGenerator(self),
691 'junit_tests':
692 JUnitGenerator(self),
693 'scripts':
694 ScriptGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28695 }
696
Kenneth Russell8a386d42018-06-02 09:48:01697 def get_test_type_remapper(self):
698 return {
699 # These are a specialization of isolated_scripts with a bunch of
700 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:56701 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:01702 'gpu_telemetry_tests': 'isolated_scripts',
703 }
704
Kenneth Russelleb60cbd22017-12-05 07:54:28705 def check_composition_test_suites(self):
706 # Pre-pass to catch errors reliably.
707 for name, value in self.test_suites.iteritems():
708 if isinstance(value, list):
709 for entry in value:
710 if isinstance(self.test_suites[entry], list):
Nico Weberd18b8962018-05-16 19:39:38711 raise BBGenErr('Composition test suites may not refer to other '
712 'composition test suites (error found while '
Kenneth Russelleb60cbd22017-12-05 07:54:28713 'processing %s)' % name)
714
Stephen Martinis54d64ad2018-09-21 22:16:20715 def flatten_test_suites(self):
716 new_test_suites = {}
717 for name, value in self.test_suites.get('basic_suites', {}).iteritems():
718 new_test_suites[name] = value
719 for name, value in self.test_suites.get('compound_suites', {}).iteritems():
720 if name in new_test_suites:
721 raise BBGenErr('Composition test suite names may not duplicate basic '
722 'test suite names (error found while processsing %s' % (
723 name))
724 new_test_suites[name] = value
725 self.test_suites = new_test_suites
726
Kenneth Russelleb60cbd22017-12-05 07:54:28727 def resolve_composition_test_suites(self):
Stephen Martinis54d64ad2018-09-21 22:16:20728 self.flatten_test_suites()
729
Kenneth Russelleb60cbd22017-12-05 07:54:28730 self.check_composition_test_suites()
731 for name, value in self.test_suites.iteritems():
732 if isinstance(value, list):
733 # Resolve this to a dictionary.
734 full_suite = {}
735 for entry in value:
736 suite = self.test_suites[entry]
737 full_suite.update(suite)
738 self.test_suites[name] = full_suite
739
740 def link_waterfalls_to_test_suites(self):
741 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43742 for tester_name, tester in waterfall['machines'].iteritems():
743 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28744 if not value in self.test_suites:
745 # Hard / impossible to cover this in the unit test.
746 raise self.unknown_test_suite(
747 value, tester_name, waterfall['name']) # pragma: no cover
748 tester['test_suites'][suite] = self.test_suites[value]
749
750 def load_configuration_files(self):
751 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
752 self.test_suites = self.load_pyl_file('test_suites.pyl')
753 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:01754 self.mixins = self.load_pyl_file('mixins.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:28755
756 def resolve_configuration_files(self):
757 self.resolve_composition_test_suites()
758 self.link_waterfalls_to_test_suites()
759
Nico Weberd18b8962018-05-16 19:39:38760 def unknown_bot(self, bot_name, waterfall_name):
761 return BBGenErr(
762 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
763
Kenneth Russelleb60cbd22017-12-05 07:54:28764 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
765 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38766 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28767 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
768
769 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
770 return BBGenErr(
771 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
772 ' on waterfall ' + waterfall_name)
773
Stephen Martinisb72f6d22018-10-04 23:29:01774 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:07775 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:32776
777 Checks in the waterfall, builder, and test objects for mixins.
778 """
779 def valid_mixin(mixin_name):
780 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:01781 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:32782 raise BBGenErr("bad mixin %s" % mixin_name)
783 def must_be_list(mixins, typ, name):
784 """Asserts that given mixins are a list."""
785 if not isinstance(mixins, list):
786 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
787
Stephen Martinisb72f6d22018-10-04 23:29:01788 if 'mixins' in waterfall:
789 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
790 for mixin in waterfall['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32791 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01792 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32793
Stephen Martinisb72f6d22018-10-04 23:29:01794 if 'mixins' in builder:
795 must_be_list(builder['mixins'], 'builder', builder_name)
796 for mixin in builder['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32797 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01798 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32799
Stephen Martinisb72f6d22018-10-04 23:29:01800 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:07801 return test
802
Stephen Martinis2a0667022018-09-25 22:31:14803 test_name = test.get('name')
804 if not test_name:
805 test_name = test.get('test')
806 if not test_name: # pragma: no cover
807 # Not the best name, but we should say something.
808 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:01809 must_be_list(test['mixins'], 'test', test_name)
810 for mixin in test['mixins']:
Stephen Martinis0382bc12018-09-17 22:29:07811 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01812 test = self.apply_mixin(self.mixins[mixin], test)
813 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:07814 return test
Stephen Martinisb6a50492018-09-12 23:59:32815
Stephen Martinisb72f6d22018-10-04 23:29:01816 def apply_mixin(self, mixin, test):
817 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:32818
Stephen Martinis0382bc12018-09-17 22:29:07819 Mixins will not override an existing key. This is to ensure exceptions can
820 override a setting a mixin applies.
821
Stephen Martinisb72f6d22018-10-04 23:29:01822 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:32823 'dimension_sets', which is how normal test suites specify their dimensions,
824 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
825 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:01826
Stephen Martinisb6a50492018-09-12 23:59:32827 """
828 new_test = copy.deepcopy(test)
829 mixin = copy.deepcopy(mixin)
830
Stephen Martinisb72f6d22018-10-04 23:29:01831 if 'swarming' in mixin:
832 swarming_mixin = mixin['swarming']
833 new_test.setdefault('swarming', {})
834 if 'dimensions' in swarming_mixin:
835 new_test['swarming'].setdefault('dimension_sets', [{}])
836 for dimension_set in new_test['swarming']['dimension_sets']:
837 dimension_set.update(swarming_mixin['dimensions'])
838 del swarming_mixin['dimensions']
Stephen Martinisb6a50492018-09-12 23:59:32839
Stephen Martinisb72f6d22018-10-04 23:29:01840 # python dict update doesn't do recursion at all. Just hard code the
841 # nested update we need (mixin['swarming'] shouldn't clobber
842 # test['swarming'], but should update it).
843 new_test['swarming'].update(swarming_mixin)
844 del mixin['swarming']
845
Wezc0e835b702018-10-30 00:38:41846 if '$mixin_append' in mixin:
847 # Values specified under $mixin_append should be appended to existing
848 # lists, rather than replacing them.
849 mixin_append = mixin['$mixin_append']
850 for key in mixin_append:
851 new_test.setdefault(key, [])
852 if not isinstance(mixin_append[key], list):
853 raise BBGenErr(
854 'Key "' + key + '" in $mixin_append must be a list.')
855 if not isinstance(new_test[key], list):
856 raise BBGenErr(
857 'Cannot apply $mixin_append to non-list "' + key + '".')
858 new_test[key].extend(mixin_append[key])
859 if 'args' in mixin_append:
860 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
861 del mixin['$mixin_append']
862
Stephen Martinisb72f6d22018-10-04 23:29:01863 new_test.update(mixin)
Stephen Martinis0382bc12018-09-17 22:29:07864
Stephen Martinisb6a50492018-09-12 23:59:32865 return new_test
866
Kenneth Russelleb60cbd22017-12-05 07:54:28867 def generate_waterfall_json(self, waterfall):
868 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28869 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:01870 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:43871 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28872 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43873 # Copy only well-understood entries in the machine's configuration
874 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28875 if 'additional_compile_targets' in config:
876 tests['additional_compile_targets'] = config[
877 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43878 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28879 if test_type not in generator_map:
880 raise self.unknown_test_suite_type(
881 test_type, name, waterfall['name']) # pragma: no cover
882 test_generator = generator_map[test_type]
Nico Weber79dc5f6852018-07-13 19:38:49883 # Let multiple kinds of generators generate the same kinds
884 # of tests. For example, gpu_telemetry_tests are a
885 # specialization of isolated_scripts.
886 new_tests = test_generator.generate(
887 waterfall, name, config, input_tests)
888 remapped_test_type = test_type_remapper.get(test_type, test_type)
889 tests[remapped_test_type] = test_generator.sort(
890 tests.get(remapped_test_type, []) + new_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28891 all_tests[name] = tests
892 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
893 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
894 return json.dumps(all_tests, indent=2, separators=(',', ': '),
895 sort_keys=True) + '\n'
896
897 def generate_waterfalls(self): # pragma: no cover
898 self.load_configuration_files()
899 self.resolve_configuration_files()
900 filters = self.args.waterfall_filters
901 suffix = '.json'
902 if self.args.new_files:
903 suffix = '.new' + suffix
904 for waterfall in self.waterfalls:
905 should_gen = not filters or waterfall['name'] in filters
906 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:11907 file_path = waterfall['name'] + suffix
908 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:28909 self.generate_waterfall_json(waterfall))
910
Nico Weberd18b8962018-05-16 19:39:38911 def get_valid_bot_names(self):
John Budorick699282e2019-02-13 01:27:33912 # Extract bot names from infra/config/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:42913 # NOTE: This reference can cause issues; if a file changes there, the
914 # presubmit here won't be run by default. A manually maintained list there
915 # tries to run presubmit here when luci-milo.cfg is changed. If any other
916 # references to configs outside of this directory are added, please change
917 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
918 # never ends up in an invalid state.
Nico Weberd18b8962018-05-16 19:39:38919 bot_names = set()
John Budorickc12abd12018-08-14 19:37:43920 infra_config_dir = os.path.abspath(
921 os.path.join(os.path.dirname(__file__),
John Budorick699282e2019-02-13 01:27:33922 '..', '..', 'infra', 'config'))
John Budorickc12abd12018-08-14 19:37:43923 milo_configs = [
924 os.path.join(infra_config_dir, 'luci-milo.cfg'),
925 os.path.join(infra_config_dir, 'luci-milo-dev.cfg'),
926 ]
927 for c in milo_configs:
928 for l in self.read_file(c).splitlines():
929 if (not 'name: "buildbucket/luci.chromium.' in l and
Hans Wennborg98ffd7d92019-02-06 14:14:34930 not 'name: "buildbucket/luci.chrome.' in l and
John Budorickb1833612018-12-07 04:36:41931 not 'name: "buildbot/chromium.' in l and
932 not 'name: "buildbot/tryserver.chromium.' in l):
John Budorickc12abd12018-08-14 19:37:43933 continue
934 # l looks like
935 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
936 # Extract win_chromium_dbg_ng part.
937 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:38938 return bot_names
939
Kenneth Russell8a386d42018-06-02 09:48:01940 def get_bots_that_do_not_actually_exist(self):
941 # Some of the bots on the chromium.gpu.fyi waterfall in particular
942 # are defined only to be mirrored into trybots, and don't actually
943 # exist on any of the waterfalls or consoles.
944 return [
Jamie Madillda894ce2019-04-08 17:19:17945 'ANGLE GPU Linux Release (Intel HD 630)',
946 'ANGLE GPU Linux Release (NVIDIA)',
947 'ANGLE GPU Mac Release (Intel)',
948 'ANGLE GPU Mac Retina Release (AMD)',
949 'ANGLE GPU Mac Retina Release (NVIDIA)',
Jamie Madilldc7feeb82018-11-14 04:54:56950 'ANGLE GPU Win10 Release (Intel HD 630)',
951 'ANGLE GPU Win10 Release (NVIDIA)',
Corentin Wallez7d3f4fa22018-11-19 23:35:44952 'Dawn GPU Linux Release (Intel HD 630)',
953 'Dawn GPU Linux Release (NVIDIA)',
954 'Dawn GPU Mac Release (Intel)',
955 'Dawn GPU Mac Retina Release (AMD)',
956 'Dawn GPU Mac Retina Release (NVIDIA)',
Jamie Madilldc7feeb82018-11-14 04:54:56957 'Dawn GPU Win10 Release (Intel HD 630)',
958 'Dawn GPU Win10 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:01959 'Optional Android Release (Nexus 5X)',
960 'Optional Linux Release (Intel HD 630)',
961 'Optional Linux Release (NVIDIA)',
962 'Optional Mac Release (Intel)',
963 'Optional Mac Retina Release (AMD)',
964 'Optional Mac Retina Release (NVIDIA)',
965 'Optional Win10 Release (Intel HD 630)',
966 'Optional Win10 Release (NVIDIA)',
967 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:08968 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:29969 'linux-blink-rel-dummy',
970 'mac10.10-blink-rel-dummy',
971 'mac10.11-blink-rel-dummy',
972 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d2018-07-17 21:39:20973 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:29974 'mac10.13-blink-rel-dummy',
975 'win7-blink-rel-dummy',
976 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:08977 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:33978 'WebKit Linux composite_after_paint Dummy Builder',
Nico Weber7fc8b9da2018-06-08 19:22:08979 'WebKit Linux layout_ng Dummy Builder',
980 'WebKit Linux root_layer_scrolls Dummy Builder',
Stephen Martinis769b25112018-08-30 18:52:06981 # chromium, due to https://crbug.com/878915
982 'win-dbg',
983 'win32-dbg',
Stephen Martinis47d77132019-04-24 23:51:33984 'win-archive-dbg',
985 'win32-archive-dbg',
Stephen Martinis07a9f742019-03-20 19:16:56986 # chromium.mac, see https://crbug.com/943804
987 'mac-dummy-rel',
Ben Pastene7687c0112019-03-05 22:43:14988 # Defined in internal configs.
989 'chromeos-amd64-generic-google-rel',
Anushruth9420fddf2019-04-04 00:24:59990 'chromeos-betty-google-rel',
Stephen Martinis47d77132019-04-24 23:51:33991 # chromium, https://crbug.com/888810
992 'android-archive-dbg',
993 'android-archive-rel',
994 'linux-archive-dbg',
995 'linux-archive-rel',
996 'mac-archive-dbg',
997 'mac-archive-rel',
998 'win-archive-rel',
999 'win32-archive-rel',
Yuke Liaobc9ff982019-04-30 06:56:161000 # code coverage, see see https://crbug.com/930364
1001 'Linux Builder Code Coverage',
1002 'Linux Tests Code Coverage',
1003 'GPU Linux Builder Code Coverage',
1004 'Linux Release Code Coverage (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011005 ]
1006
Stephen Martinisf83893722018-09-19 00:02:181007 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201008 self.check_input_files_sorting(verbose)
1009
Kenneth Russelleb60cbd22017-12-05 07:54:281010 self.load_configuration_files()
Stephen Martinis54d64ad2018-09-21 22:16:201011 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:281012 self.check_composition_test_suites()
Nico Weberd18b8962018-05-16 19:39:381013
1014 # All bots should exist.
1015 bot_names = self.get_valid_bot_names()
Kenneth Russell8a386d42018-06-02 09:48:011016 bots_that_dont_exist = self.get_bots_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:381017 for waterfall in self.waterfalls:
1018 for bot_name in waterfall['machines']:
Kenneth Russell8a386d42018-06-02 09:48:011019 if bot_name in bots_that_dont_exist:
1020 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381021 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:081022 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:381023 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:521024 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:321025 if waterfall['name'] in ['tryserver.webrtc',
1026 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:381027 # These waterfalls have their bot configs in a different repo.
1028 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:521029 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381030 raise self.unknown_bot(bot_name, waterfall['name'])
1031
Kenneth Russelleb60cbd22017-12-05 07:54:281032 # All test suites must be referenced.
1033 suites_seen = set()
1034 generator_map = self.get_test_generator_map()
1035 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431036 for bot_name, tester in waterfall['machines'].iteritems():
1037 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281038 if suite_type not in generator_map:
1039 raise self.unknown_test_suite_type(suite_type, bot_name,
1040 waterfall['name'])
1041 if suite not in self.test_suites:
1042 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1043 suites_seen.add(suite)
1044 # Since we didn't resolve the configuration files, this set
1045 # includes both composition test suites and regular ones.
1046 resolved_suites = set()
1047 for suite_name in suites_seen:
1048 suite = self.test_suites[suite_name]
1049 if isinstance(suite, list):
1050 for sub_suite in suite:
1051 resolved_suites.add(sub_suite)
1052 resolved_suites.add(suite_name)
1053 # At this point, every key in test_suites.pyl should be referenced.
1054 missing_suites = set(self.test_suites.keys()) - resolved_suites
1055 if missing_suites:
1056 raise BBGenErr('The following test suites were unreferenced by bots on '
1057 'the waterfalls: ' + str(missing_suites))
1058
1059 # All test suite exceptions must refer to bots on the waterfall.
1060 all_bots = set()
1061 missing_bots = set()
1062 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431063 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281064 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281065 # In order to disambiguate between bots with the same name on
1066 # different waterfalls, support has been added to various
1067 # exceptions for concatenating the waterfall name after the bot
1068 # name.
1069 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281070 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381071 removals = (exception.get('remove_from', []) +
1072 exception.get('remove_gtest_from', []) +
1073 exception.get('modifications', {}).keys())
1074 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281075 if removal not in all_bots:
1076 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411077
1078 missing_bots = missing_bots - set(bots_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281079 if missing_bots:
1080 raise BBGenErr('The following nonexistent machines were referenced in '
1081 'the test suite exceptions: ' + str(missing_bots))
1082
Stephen Martinis0382bc12018-09-17 22:29:071083 # All mixins must be referenced
1084 seen_mixins = set()
1085 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011086 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071087 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011088 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071089 for suite in self.test_suites.values():
1090 if isinstance(suite, list):
1091 # Don't care about this, it's a composition, which shouldn't include a
1092 # swarming mixin.
1093 continue
1094
1095 for test in suite.values():
1096 if not isinstance(test, dict):
1097 # Some test suites have top level keys, which currently can't be
1098 # swarming mixin entries. Ignore them
1099 continue
1100
Stephen Martinisb72f6d22018-10-04 23:29:011101 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071102
Stephen Martinisb72f6d22018-10-04 23:29:011103 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071104 if missing_mixins:
1105 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1106 ' referenced in a waterfall, machine, or test suite.' % (
1107 str(missing_mixins)))
1108
Stephen Martinis54d64ad2018-09-21 22:16:201109
1110 def type_assert(self, node, typ, filename, verbose=False):
1111 """Asserts that the Python AST node |node| is of type |typ|.
1112
1113 If verbose is set, it prints out some helpful context lines, showing where
1114 exactly the error occurred in the file.
1115 """
1116 if not isinstance(node, typ):
1117 if verbose:
1118 lines = [""] + self.read_file(filename).splitlines()
1119
1120 context = 2
1121 lines_start = max(node.lineno - context, 0)
1122 # Add one to include the last line
1123 lines_end = min(node.lineno + context, len(lines)) + 1
1124 lines = (
1125 ['== %s ==\n' % filename] +
1126 ["<snip>\n"] +
1127 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1128 lines[lines_start:lines_start + context])] +
1129 ['-' * 80 + '\n'] +
1130 ['%d %s' % (node.lineno, lines[node.lineno])] +
1131 ['-' * (node.col_offset + 3) + '^' + '-' * (
1132 80 - node.col_offset - 4) + '\n'] +
1133 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1134 lines[node.lineno + 1:lines_end])] +
1135 ["<snip>\n"]
1136 )
1137 # Print out a useful message when a type assertion fails.
1138 for l in lines:
1139 self.print_line(l.strip())
1140
1141 node_dumped = ast.dump(node, annotate_fields=False)
1142 # If the node is huge, truncate it so everything fits in a terminal
1143 # window.
1144 if len(node_dumped) > 60: # pragma: no cover
1145 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1146 raise BBGenErr(
1147 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1148 ' be %s, is %s' % (
1149 filename, node_dumped,
1150 node.lineno, typ, type(node)))
1151
1152 def ensure_ast_dict_keys_sorted(self, node, filename, verbose):
1153 is_valid = True
1154
1155 keys = []
1156 # The keys of this dict are ordered as ordered in the file; normal python
1157 # dictionary keys are given an arbitrary order, but since we parsed the
1158 # file itself, the order as given in the file is preserved.
1159 for key in node.keys:
1160 self.type_assert(key, ast.Str, filename, verbose)
1161 keys.append(key.s)
1162
1163 keys_sorted = sorted(keys)
1164 if keys_sorted != keys:
1165 is_valid = False
1166 if verbose:
1167 for line in difflib.unified_diff(
1168 keys,
1169 keys_sorted, fromfile='current (%r)' % filename, tofile='sorted'):
1170 self.print_line(line)
1171
1172 if len(set(keys)) != len(keys):
1173 for i in range(len(keys_sorted)-1):
1174 if keys_sorted[i] == keys_sorted[i+1]:
1175 self.print_line('Key %s is duplicated' % keys_sorted[i])
1176 is_valid = False
1177 return is_valid
Stephen Martinisf83893722018-09-19 00:02:181178
1179 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201180 # TODO(https://crbug.com/886993): Add the ability for this script to
1181 # actually format the files, rather than just complain if they're
1182 # incorrectly formatted.
1183 bad_files = set()
1184
1185 for filename in (
Stephen Martinisb72f6d22018-10-04 23:29:011186 'mixins.pyl',
Stephen Martinis54d64ad2018-09-21 22:16:201187 'test_suites.pyl',
1188 'test_suite_exceptions.pyl',
1189 ):
Stephen Martinisf83893722018-09-19 00:02:181190 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1191
Stephen Martinisf83893722018-09-19 00:02:181192 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201193 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181194 module = parsed.body
1195
1196 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201197 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181198 if len(module) != 1: # pragma: no cover
1199 raise BBGenErr('Invalid .pyl file %s' % filename)
1200 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201201 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181202
1203 # Value should be a dictionary.
1204 value = expr.value
Stephen Martinis54d64ad2018-09-21 22:16:201205 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181206
Stephen Martinis54d64ad2018-09-21 22:16:201207 if filename == 'test_suites.pyl':
1208 expected_keys = ['basic_suites', 'compound_suites']
1209 actual_keys = [node.s for node in value.keys]
1210 assert all(key in expected_keys for key in actual_keys), (
1211 'Invalid %r file; expected keys %r, got %r' % (
1212 filename, expected_keys, actual_keys))
1213 suite_dicts = [node for node in value.values]
1214 # Only two keys should mean only 1 or 2 values
1215 assert len(suite_dicts) <= 2
1216 for suite_group in suite_dicts:
1217 if not self.ensure_ast_dict_keys_sorted(
1218 suite_group, filename, verbose):
1219 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181220
Stephen Martinis54d64ad2018-09-21 22:16:201221 else:
1222 if not self.ensure_ast_dict_keys_sorted(
1223 value, filename, verbose):
1224 bad_files.add(filename)
1225
1226 # waterfalls.pyl is slightly different, just do it manually here
1227 filename = 'waterfalls.pyl'
1228 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1229
1230 # Must be a module.
1231 self.type_assert(parsed, ast.Module, filename, verbose)
1232 module = parsed.body
1233
1234 # Only one expression in the module.
1235 self.type_assert(module, list, filename, verbose)
1236 if len(module) != 1: # pragma: no cover
1237 raise BBGenErr('Invalid .pyl file %s' % filename)
1238 expr = module[0]
1239 self.type_assert(expr, ast.Expr, filename, verbose)
1240
1241 # Value should be a list.
1242 value = expr.value
1243 self.type_assert(value, ast.List, filename, verbose)
1244
1245 keys = []
1246 for val in value.elts:
1247 self.type_assert(val, ast.Dict, filename, verbose)
1248 waterfall_name = None
1249 for key, val in zip(val.keys, val.values):
1250 self.type_assert(key, ast.Str, filename, verbose)
1251 if key.s == 'machines':
1252 if not self.ensure_ast_dict_keys_sorted(val, filename, verbose):
1253 bad_files.add(filename)
1254
1255 if key.s == "name":
1256 self.type_assert(val, ast.Str, filename, verbose)
1257 waterfall_name = val.s
1258 assert waterfall_name
1259 keys.append(waterfall_name)
1260
1261 if sorted(keys) != keys:
1262 bad_files.add(filename)
1263 if verbose: # pragma: no cover
1264 for line in difflib.unified_diff(
1265 keys,
1266 sorted(keys), fromfile='current', tofile='sorted'):
1267 self.print_line(line)
Stephen Martinisf83893722018-09-19 00:02:181268
1269 if bad_files:
1270 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201271 'The following files have invalid keys: %s\n. They are either '
1272 'unsorted, or have duplicates.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181273
Kenneth Russelleb60cbd22017-12-05 07:54:281274 def check_output_file_consistency(self, verbose=False):
1275 self.load_configuration_files()
1276 # All waterfalls must have been written by this script already.
1277 self.resolve_configuration_files()
1278 ungenerated_waterfalls = set()
1279 for waterfall in self.waterfalls:
1280 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111281 file_path = waterfall['name'] + '.json'
1282 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281283 if expected != current:
1284 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321285 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501286 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281287 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321288 'contents:')
1289 for line in difflib.unified_diff(
1290 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501291 current.splitlines(),
1292 fromfile='expected', tofile='current'):
1293 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281294 if ungenerated_waterfalls:
1295 raise BBGenErr('The following waterfalls have not been properly '
1296 'autogenerated by generate_buildbot_json.py: ' +
1297 str(ungenerated_waterfalls))
1298
1299 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501300 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281301 self.check_output_file_consistency(verbose) # pragma: no cover
1302
1303 def parse_args(self, argv): # pragma: no cover
Karen Qiane24b7ee2019-02-12 23:37:061304
1305 # RawTextHelpFormatter allows for styling of help statement
1306 parser = argparse.ArgumentParser(formatter_class=
1307 argparse.RawTextHelpFormatter)
1308
1309 group = parser.add_mutually_exclusive_group()
1310 group.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281311 '-c', '--check', action='store_true', help=
1312 'Do consistency checks of configuration and generated files and then '
1313 'exit. Used during presubmit. Causes the tool to not generate any files.')
Karen Qiane24b7ee2019-02-12 23:37:061314 group.add_argument(
1315 '--query', type=str, help=
1316 ("Returns raw JSON information of buildbots and tests.\n" +
1317 "Examples:\n" +
1318 " List all bots (all info):\n" +
1319 " --query bots\n\n" +
1320 " List all bots and only their associated tests:\n" +
1321 " --query bots/tests\n\n" +
1322 " List all information about 'bot1' " +
1323 "(make sure you have quotes):\n" +
1324 " --query bot/'bot1'\n\n" +
1325 " List tests running for 'bot1' (make sure you have quotes):\n" +
1326 " --query bot/'bot1'/tests\n\n" +
1327 " List all tests:\n" +
1328 " --query tests\n\n" +
1329 " List all tests and the bots running them:\n" +
1330 " --query tests/bots\n\n"+
1331 " List all tests that satisfy multiple parameters\n" +
1332 " (separation of parameters by '&' symbol):\n" +
1333 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
1334 " List all tests that run with a specific flag:\n" +
1335 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
1336 " List specific test (make sure you have quotes):\n"
1337 " --query test/'test1'\n\n"
1338 " List all bots running 'test1' " +
1339 "(make sure you have quotes):\n" +
1340 " --query test/'test1'/bots" ))
Kenneth Russelleb60cbd22017-12-05 07:54:281341 parser.add_argument(
1342 '-n', '--new-files', action='store_true', help=
1343 'Write output files as .new.json. Useful during development so old and '
1344 'new files can be looked at side-by-side.')
1345 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501346 '-v', '--verbose', action='store_true', help=
1347 'Increases verbosity. Affects consistency checks.')
1348 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281349 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1350 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111351 parser.add_argument(
1352 '--pyl-files-dir', type=os.path.realpath,
1353 help='Path to the directory containing the input .pyl files.')
Karen Qiane24b7ee2019-02-12 23:37:061354 parser.add_argument(
1355 '--json', help=
1356 ("Outputs results into a json file. Only works with query function.\n" +
1357 "Examples:\n" +
1358 " Outputs file into specified json file: \n" +
1359 " --json <file-name-here.json>"))
Kenneth Russelleb60cbd22017-12-05 07:54:281360 self.args = parser.parse_args(argv)
Karen Qiane24b7ee2019-02-12 23:37:061361 if self.args.json and not self.args.query:
1362 parser.error("The --json flag can only be used with --query.")
1363
1364 def does_test_match(self, test_info, params_dict):
1365 """Checks to see if the test matches the parameters given.
1366
1367 Compares the provided test_info with the params_dict to see
1368 if the bot matches the parameters given. If so, returns True.
1369 Else, returns false.
1370
1371 Args:
1372 test_info (dict): Information about a specific bot provided
1373 in the format shown in waterfalls.pyl
1374 params_dict (dict): Dictionary of parameters and their values
1375 to look for in the bot
1376 Ex: {
1377 'device_os':'android',
1378 '--flag':True,
1379 'mixins': ['mixin1', 'mixin2'],
1380 'ex_key':'ex_value'
1381 }
1382
1383 """
1384 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
1385 'kvm', 'pool', 'integrity'] # dimension parameters
1386 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
1387 'can_use_on_swarming_builders']
1388 for param in params_dict:
1389 # if dimension parameter
1390 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
1391 if not 'swarming' in test_info:
1392 return False
1393 swarming = test_info['swarming']
1394 if param in SWARMING_PARAMS:
1395 if not param in swarming:
1396 return False
1397 if not str(swarming[param]) == params_dict[param]:
1398 return False
1399 else:
1400 if not 'dimension_sets' in swarming:
1401 return False
1402 d_set = swarming['dimension_sets']
1403 # only looking at the first dimension set
1404 if not param in d_set[0]:
1405 return False
1406 if not d_set[0][param] == params_dict[param]:
1407 return False
1408
1409 # if flag
1410 elif param.startswith('--'):
1411 if not 'args' in test_info:
1412 return False
1413 if not param in test_info['args']:
1414 return False
1415
1416 # not dimension parameter/flag/mixin
1417 else:
1418 if not param in test_info:
1419 return False
1420 if not test_info[param] == params_dict[param]:
1421 return False
1422 return True
1423 def error_msg(self, msg):
1424 """Prints an error message.
1425
1426 In addition to a catered error message, also prints
1427 out where the user can find more help. Then, program exits.
1428 """
1429 self.print_line(msg + (' If you need more information, ' +
1430 'please run with -h or --help to see valid commands.'))
1431 sys.exit(1)
1432
1433 def find_bots_that_run_test(self, test, bots):
1434 matching_bots = []
1435 for bot in bots:
1436 bot_info = bots[bot]
1437 tests = self.flatten_tests_for_bot(bot_info)
1438 for test_info in tests:
1439 test_name = ""
1440 if 'name' in test_info:
1441 test_name = test_info['name']
1442 elif 'test' in test_info:
1443 test_name = test_info['test']
1444 if not test_name == test:
1445 continue
1446 matching_bots.append(bot)
1447 return matching_bots
1448
1449 def find_tests_with_params(self, tests, params_dict):
1450 matching_tests = []
1451 for test_name in tests:
1452 test_info = tests[test_name]
1453 if not self.does_test_match(test_info, params_dict):
1454 continue
1455 if not test_name in matching_tests:
1456 matching_tests.append(test_name)
1457 return matching_tests
1458
1459 def flatten_waterfalls_for_query(self, waterfalls):
1460 bots = {}
1461 for waterfall in waterfalls:
1462 waterfall_json = json.loads(self.generate_waterfall_json(waterfall))
1463 for bot in waterfall_json:
1464 bot_info = waterfall_json[bot]
1465 if 'AAAAA' not in bot:
1466 bots[bot] = bot_info
1467 return bots
1468
1469 def flatten_tests_for_bot(self, bot_info):
1470 """Returns a list of flattened tests.
1471
1472 Returns a list of tests not grouped by test category
1473 for a specific bot.
1474 """
1475 TEST_CATS = self.get_test_generator_map().keys()
1476 tests = []
1477 for test_cat in TEST_CATS:
1478 if not test_cat in bot_info:
1479 continue
1480 test_cat_tests = bot_info[test_cat]
1481 tests = tests + test_cat_tests
1482 return tests
1483
1484 def flatten_tests_for_query(self, test_suites):
1485 """Returns a flattened dictionary of tests.
1486
1487 Returns a dictionary of tests associate with their
1488 configuration, not grouped by their test suite.
1489 """
1490 tests = {}
1491 for test_suite in test_suites.itervalues():
1492 for test in test_suite:
1493 test_info = test_suite[test]
1494 test_name = test
1495 if 'name' in test_info:
1496 test_name = test_info['name']
1497 tests[test_name] = test_info
1498 return tests
1499
1500 def parse_query_filter_params(self, params):
1501 """Parses the filter parameters.
1502
1503 Creates a dictionary from the parameters provided
1504 to filter the bot array.
1505 """
1506 params_dict = {}
1507 for p in params:
1508 # flag
1509 if p.startswith("--"):
1510 params_dict[p] = True
1511 else:
1512 pair = p.split(":")
1513 if len(pair) != 2:
1514 self.error_msg('Invalid command.')
1515 # regular parameters
1516 if pair[1].lower() == "true":
1517 params_dict[pair[0]] = True
1518 elif pair[1].lower() == "false":
1519 params_dict[pair[0]] = False
1520 else:
1521 params_dict[pair[0]] = pair[1]
1522 return params_dict
1523
1524 def get_test_suites_dict(self, bots):
1525 """Returns a dictionary of bots and their tests.
1526
1527 Returns a dictionary of bots and a list of their associated tests.
1528 """
1529 test_suite_dict = dict()
1530 for bot in bots:
1531 bot_info = bots[bot]
1532 tests = self.flatten_tests_for_bot(bot_info)
1533 test_suite_dict[bot] = tests
1534 return test_suite_dict
1535
1536 def output_query_result(self, result, json_file=None):
1537 """Outputs the result of the query.
1538
1539 If a json file parameter name is provided, then
1540 the result is output into the json file. If not,
1541 then the result is printed to the console.
1542 """
1543 output = json.dumps(result, indent=2)
1544 if json_file:
1545 self.write_file(json_file, output)
1546 else:
1547 self.print_line(output)
1548 return
1549
1550 def query(self, args):
1551 """Queries tests or bots.
1552
1553 Depending on the arguments provided, outputs a json of
1554 tests or bots matching the appropriate optional parameters provided.
1555 """
1556 # split up query statement
1557 query = args.query.split('/')
1558 self.load_configuration_files()
1559 self.resolve_configuration_files()
1560
1561 # flatten bots json
1562 tests = self.test_suites
1563 bots = self.flatten_waterfalls_for_query(self.waterfalls)
1564
1565 cmd_class = query[0]
1566
1567 # For queries starting with 'bots'
1568 if cmd_class == "bots":
1569 if len(query) == 1:
1570 return self.output_query_result(bots, args.json)
1571 # query with specific parameters
1572 elif len(query) == 2:
1573 if query[1] == 'tests':
1574 test_suites_dict = self.get_test_suites_dict(bots)
1575 return self.output_query_result(test_suites_dict, args.json)
1576 else:
1577 self.error_msg("This query should be in the format: bots/tests.")
1578
1579 else:
1580 self.error_msg("This query should have 0 or 1 '/', found %s instead."
1581 % str(len(query)-1))
1582
1583 # For queries starting with 'bot'
1584 elif cmd_class == "bot":
1585 if not len(query) == 2 and not len(query) == 3:
1586 self.error_msg("Command should have 1 or 2 '/', found %s instead."
1587 % str(len(query)-1))
1588 bot_id = query[1]
1589 if not bot_id in bots:
1590 self.error_msg("No bot named '" + bot_id + "' found.")
1591 bot_info = bots[bot_id]
1592 if len(query) == 2:
1593 return self.output_query_result(bot_info, args.json)
1594 if not query[2] == 'tests':
1595 self.error_msg("The query should be in the format:" +
1596 "bot/<bot-name>/tests.")
1597
1598 bot_tests = self.flatten_tests_for_bot(bot_info)
1599 return self.output_query_result(bot_tests, args.json)
1600
1601 # For queries starting with 'tests'
1602 elif cmd_class == "tests":
1603 if not len(query) == 1 and not len(query) == 2:
1604 self.error_msg("The query should have 0 or 1 '/', found %s instead."
1605 % str(len(query)-1))
1606 flattened_tests = self.flatten_tests_for_query(tests)
1607 if len(query) == 1:
1608 return self.output_query_result(flattened_tests, args.json)
1609
1610 # create params dict
1611 params = query[1].split('&')
1612 params_dict = self.parse_query_filter_params(params)
1613 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
1614 return self.output_query_result(matching_bots)
1615
1616 # For queries starting with 'test'
1617 elif cmd_class == "test":
1618 if not len(query) == 2 and not len(query) == 3:
1619 self.error_msg("The query should have 1 or 2 '/', found %s instead."
1620 % str(len(query)-1))
1621 test_id = query[1]
1622 if len(query) == 2:
1623 flattened_tests = self.flatten_tests_for_query(tests)
1624 for test in flattened_tests:
1625 if test == test_id:
1626 return self.output_query_result(flattened_tests[test], args.json)
1627 self.error_msg("There is no test named %s." % test_id)
1628 if not query[2] == 'bots':
1629 self.error_msg("The query should be in the format: " +
1630 "test/<test-name>/bots")
1631 bots_for_test = self.find_bots_that_run_test(test_id, bots)
1632 return self.output_query_result(bots_for_test)
1633
1634 else:
1635 self.error_msg("Your command did not match any valid commands." +
1636 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:281637
1638 def main(self, argv): # pragma: no cover
1639 self.parse_args(argv)
1640 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:501641 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:061642 elif self.args.query:
1643 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:281644 else:
1645 self.generate_waterfalls()
1646 return 0
1647
1648if __name__ == "__main__": # pragma: no cover
1649 generator = BBJSONGenerator()
John Budorick699282e2019-02-13 01:27:331650 sys.exit(generator.main(sys.argv[1:]))