blob: 567882e6899447d9810a068ab7d1ebce1310281c [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):
66 def __init__(self, bb_gen):
67 super(GPUTelemetryTestGenerator, self).__init__(bb_gen)
68
69 def generate(self, waterfall, tester_name, tester_config, input_tests):
70 isolated_scripts = []
71 for test_name, test_config in sorted(input_tests.iteritems()):
72 test = self.bb_gen.generate_gpu_telemetry_test(
73 waterfall, tester_name, tester_config, test_name, test_config)
74 if test:
75 isolated_scripts.append(test)
76 return isolated_scripts
77
78 def sort(self, tests):
79 return sorted(tests, key=lambda x: x['name'])
80
81
Kenneth Russelleb60cbd22017-12-05 07:54:2882class GTestGenerator(BaseGenerator):
83 def __init__(self, bb_gen):
84 super(GTestGenerator, self).__init__(bb_gen)
85
Kenneth Russell8ceeabf2017-12-11 17:53:2886 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:2887 # The relative ordering of some of the tests is important to
88 # minimize differences compared to the handwritten JSON files, since
89 # Python's sorts are stable and there are some tests with the same
90 # key (see gles2_conform_d3d9_test and similar variants). Avoid
91 # losing the order by avoiding coalescing the dictionaries into one.
92 gtests = []
93 for test_name, test_config in sorted(input_tests.iteritems()):
Nico Weber79dc5f6852018-07-13 19:38:4994 test = self.bb_gen.generate_gtest(
95 waterfall, tester_name, tester_config, test_name, test_config)
96 if test:
97 # generate_gtest may veto the test generation on this tester.
98 gtests.append(test)
Kenneth Russelleb60cbd22017-12-05 07:54:2899 return gtests
100
101 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28102 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28103
104
105class IsolatedScriptTestGenerator(BaseGenerator):
106 def __init__(self, bb_gen):
107 super(IsolatedScriptTestGenerator, self).__init__(bb_gen)
108
Kenneth Russell8ceeabf2017-12-11 17:53:28109 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28110 isolated_scripts = []
111 for test_name, test_config in sorted(input_tests.iteritems()):
112 test = self.bb_gen.generate_isolated_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28113 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28114 if test:
115 isolated_scripts.append(test)
116 return isolated_scripts
117
118 def sort(self, tests):
119 return sorted(tests, key=lambda x: x['name'])
120
121
122class ScriptGenerator(BaseGenerator):
123 def __init__(self, bb_gen):
124 super(ScriptGenerator, self).__init__(bb_gen)
125
Kenneth Russell8ceeabf2017-12-11 17:53:28126 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28127 scripts = []
128 for test_name, test_config in sorted(input_tests.iteritems()):
129 test = self.bb_gen.generate_script_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28130 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28131 if test:
132 scripts.append(test)
133 return scripts
134
135 def sort(self, tests):
136 return sorted(tests, key=lambda x: x['name'])
137
138
139class JUnitGenerator(BaseGenerator):
140 def __init__(self, bb_gen):
141 super(JUnitGenerator, self).__init__(bb_gen)
142
Kenneth Russell8ceeabf2017-12-11 17:53:28143 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28144 scripts = []
145 for test_name, test_config in sorted(input_tests.iteritems()):
146 test = self.bb_gen.generate_junit_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28147 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28148 if test:
149 scripts.append(test)
150 return scripts
151
152 def sort(self, tests):
153 return sorted(tests, key=lambda x: x['test'])
154
155
156class CTSGenerator(BaseGenerator):
157 def __init__(self, bb_gen):
158 super(CTSGenerator, self).__init__(bb_gen)
159
Kenneth Russell8ceeabf2017-12-11 17:53:28160 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28161 # These only contain one entry and it's the contents of the input tests'
162 # dictionary, verbatim.
163 cts_tests = []
164 cts_tests.append(input_tests)
165 return cts_tests
166
167 def sort(self, tests):
168 return tests
169
170
171class InstrumentationTestGenerator(BaseGenerator):
172 def __init__(self, bb_gen):
173 super(InstrumentationTestGenerator, self).__init__(bb_gen)
174
Kenneth Russell8ceeabf2017-12-11 17:53:28175 def generate(self, waterfall, tester_name, tester_config, input_tests):
Kenneth Russelleb60cbd22017-12-05 07:54:28176 scripts = []
177 for test_name, test_config in sorted(input_tests.iteritems()):
178 test = self.bb_gen.generate_instrumentation_test(
Kenneth Russell8ceeabf2017-12-11 17:53:28179 waterfall, tester_name, tester_config, test_name, test_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28180 if test:
181 scripts.append(test)
182 return scripts
183
184 def sort(self, tests):
Kenneth Russell8ceeabf2017-12-11 17:53:28185 return sorted(tests, cmp=cmp_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28186
187
188class BBJSONGenerator(object):
189 def __init__(self):
190 self.this_dir = THIS_DIR
191 self.args = None
192 self.waterfalls = None
193 self.test_suites = None
194 self.exceptions = None
Stephen Martinisb72f6d22018-10-04 23:29:01195 self.mixins = None
Kenneth Russelleb60cbd22017-12-05 07:54:28196
197 def generate_abs_file_path(self, relative_path):
198 return os.path.join(self.this_dir, relative_path) # pragma: no cover
199
Stephen Martinis7eb8b612018-09-21 00:17:50200 def print_line(self, line):
201 # Exists so that tests can mock
202 print line # pragma: no cover
203
Kenneth Russelleb60cbd22017-12-05 07:54:28204 def read_file(self, relative_path):
205 with open(self.generate_abs_file_path(
206 relative_path)) as fp: # pragma: no cover
207 return fp.read() # pragma: no cover
208
209 def write_file(self, relative_path, contents):
210 with open(self.generate_abs_file_path(
211 relative_path), 'wb') as fp: # pragma: no cover
212 fp.write(contents) # pragma: no cover
213
Zhiling Huangbe008172018-03-08 19:13:11214 def pyl_file_path(self, filename):
215 if self.args and self.args.pyl_files_dir:
216 return os.path.join(self.args.pyl_files_dir, filename)
217 return filename
218
Kenneth Russelleb60cbd22017-12-05 07:54:28219 def load_pyl_file(self, filename):
220 try:
Zhiling Huangbe008172018-03-08 19:13:11221 return ast.literal_eval(self.read_file(
222 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28223 except (SyntaxError, ValueError) as e: # pragma: no cover
224 raise BBGenErr('Failed to parse pyl file "%s": %s' %
225 (filename, e)) # pragma: no cover
226
Kenneth Russell8a386d42018-06-02 09:48:01227 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
228 # Currently it is only mandatory for bots which run GPU tests. Change these to
229 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28230 def is_android(self, tester_config):
231 return tester_config.get('os_type') == 'android'
232
Ben Pastenea9e583b2019-01-16 02:57:26233 def is_chromeos(self, tester_config):
234 return tester_config.get('os_type') == 'chromeos'
235
Kenneth Russell8a386d42018-06-02 09:48:01236 def is_linux(self, tester_config):
237 return tester_config.get('os_type') == 'linux'
238
Kenneth Russelleb60cbd22017-12-05 07:54:28239 def get_exception_for_test(self, test_name, test_config):
240 # gtests may have both "test" and "name" fields, and usually, if the "name"
241 # field is specified, it means that the same test is being repurposed
242 # multiple times with different command line arguments. To handle this case,
243 # prefer to lookup per the "name" field of the test itself, as opposed to
244 # the "test_name", which is actually the "test" field.
245 if 'name' in test_config:
246 return self.exceptions.get(test_config['name'])
247 else:
248 return self.exceptions.get(test_name)
249
Nico Weberb0b3f5862018-07-13 18:45:15250 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28251 # Currently, the only reason a test should not run on a given tester is that
252 # it's in the exceptions. (Once the GPU waterfall generation script is
253 # incorporated here, the rules will become more complex.)
254 exception = self.get_exception_for_test(test_name, test_config)
255 if not exception:
256 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28257 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28258 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28259 if remove_from:
260 if tester_name in remove_from:
261 return False
262 # TODO(kbr): this code path was added for some tests (including
263 # android_webview_unittests) on one machine (Nougat Phone
264 # Tester) which exists with the same name on two waterfalls,
265 # chromium.android and chromium.fyi; the tests are run on one
266 # but not the other. Once the bots are all uniquely named (a
267 # different ongoing project) this code should be removed.
268 # TODO(kbr): add coverage.
269 return (tester_name + ' ' + waterfall['name']
270 not in remove_from) # pragma: no cover
271 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28272
Nico Weber79dc5f6852018-07-13 19:38:49273 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28274 exception = self.get_exception_for_test(test_name, test)
275 if not exception:
276 return None
Nico Weber79dc5f6852018-07-13 19:38:49277 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28278
Kenneth Russell8a386d42018-06-02 09:48:01279 def merge_command_line_args(self, arr, prefix, splitter):
280 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01281 idx = 0
282 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01283 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01284 while idx < len(arr):
285 flag = arr[idx]
286 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01287 if flag.startswith(prefix):
288 arg = flag[prefix_len:]
289 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01290 if first_idx < 0:
291 first_idx = idx
292 else:
293 delete_current_entry = True
294 if delete_current_entry:
295 del arr[idx]
296 else:
297 idx += 1
298 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01299 arr[first_idx] = prefix + splitter.join(accumulated_args)
300 return arr
301
302 def maybe_fixup_args_array(self, arr):
303 # The incoming array of strings may be an array of command line
304 # arguments. To make it easier to turn on certain features per-bot or
305 # per-test-suite, look specifically for certain flags and merge them
306 # appropriately.
307 # --enable-features=Feature1 --enable-features=Feature2
308 # are merged to:
309 # --enable-features=Feature1,Feature2
310 # and:
311 # --extra-browser-args=arg1 --extra-browser-args=arg2
312 # are merged to:
313 # --extra-browser-args=arg1 arg2
314 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
315 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01316 return arr
317
Kenneth Russelleb60cbd22017-12-05 07:54:28318 def dictionary_merge(self, a, b, path=None, update=True):
319 """http://stackoverflow.com/questions/7204805/
320 python-dictionaries-of-dictionaries-merge
321 merges b into a
322 """
323 if path is None:
324 path = []
325 for key in b:
326 if key in a:
327 if isinstance(a[key], dict) and isinstance(b[key], dict):
328 self.dictionary_merge(a[key], b[key], path + [str(key)])
329 elif a[key] == b[key]:
330 pass # same leaf value
331 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06332 # Args arrays are lists of strings. Just concatenate them,
333 # and don't sort them, in order to keep some needed
334 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28335 if all(isinstance(x, str)
336 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01337 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28338 else:
339 # TODO(kbr): this only works properly if the two arrays are
340 # the same length, which is currently always the case in the
341 # swarming dimension_sets that we have to merge. It will fail
342 # to merge / override 'args' arrays which are different
343 # length.
344 for idx in xrange(len(b[key])):
345 try:
346 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
347 path + [str(key), str(idx)],
348 update=update)
349 except (IndexError, TypeError): # pragma: no cover
350 raise BBGenErr('Error merging list keys ' + str(key) +
351 ' and indices ' + str(idx) + ' between ' +
352 str(a) + ' and ' + str(b)) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:28353 elif update: # pragma: no cover
354 a[key] = b[key] # pragma: no cover
355 else:
356 raise BBGenErr('Conflict at %s' % '.'.join(
357 path + [str(key)])) # pragma: no cover
358 else:
359 a[key] = b[key]
360 return a
361
John Budorickab108712018-09-01 00:12:21362 def initialize_args_for_test(
363 self, generated_test, tester_config, additional_arg_keys=None):
364
365 args = []
366 args.extend(generated_test.get('args', []))
367 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22368
Kenneth Russell8a386d42018-06-02 09:48:01369 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21370 val = generated_test.pop(key, [])
371 if fn(tester_config):
372 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01373
374 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
375 add_conditional_args('linux_args', self.is_linux)
376 add_conditional_args('android_args', self.is_android)
377
John Budorickab108712018-09-01 00:12:21378 for key in additional_arg_keys or []:
379 args.extend(generated_test.pop(key, []))
380 args.extend(tester_config.get(key, []))
381
382 if args:
383 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01384
Kenneth Russelleb60cbd22017-12-05 07:54:28385 def initialize_swarming_dictionary_for_test(self, generated_test,
386 tester_config):
387 if 'swarming' not in generated_test:
388 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28389 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
390 generated_test['swarming'].update({
391 'can_use_on_swarming_builders': tester_config.get('use_swarming', True)
392 })
Kenneth Russelleb60cbd22017-12-05 07:54:28393 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03394 if ('dimension_sets' not in generated_test['swarming'] and
395 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28396 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
397 tester_config['swarming']['dimension_sets'])
398 self.dictionary_merge(generated_test['swarming'],
399 tester_config['swarming'])
400 # Apply any Android-specific Swarming dimensions after the generic ones.
401 if 'android_swarming' in generated_test:
402 if self.is_android(tester_config): # pragma: no cover
403 self.dictionary_merge(
404 generated_test['swarming'],
405 generated_test['android_swarming']) # pragma: no cover
406 del generated_test['android_swarming'] # pragma: no cover
407
408 def clean_swarming_dictionary(self, swarming_dict):
409 # Clean out redundant entries from a test's "swarming" dictionary.
410 # This is really only needed to retain 100% parity with the
411 # handwritten JSON files, and can be removed once all the files are
412 # autogenerated.
413 if 'shards' in swarming_dict:
414 if swarming_dict['shards'] == 1: # pragma: no cover
415 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24416 if 'hard_timeout' in swarming_dict:
417 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
418 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43419 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28420 # Remove all other keys.
421 for k in swarming_dict.keys(): # pragma: no cover
422 if k != 'can_use_on_swarming_builders': # pragma: no cover
423 del swarming_dict[k] # pragma: no cover
424
Stephen Martinis0382bc12018-09-17 22:29:07425 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
426 waterfall):
427 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01428 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07429 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28430 # See if there are any exceptions that need to be merged into this
431 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49432 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28433 if modifications:
434 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23435 if 'swarming' in test:
436 self.clean_swarming_dictionary(test['swarming'])
Kenneth Russelleb60cbd22017-12-05 07:54:28437 return test
438
Shenghua Zhangaba8bad2018-02-07 02:12:09439 def add_common_test_properties(self, test, tester_config):
440 if tester_config.get('use_multi_dimension_trigger_script'):
Kenneth Russell73c3bd8b2018-10-19 22:30:19441 # Assumes update_and_cleanup_test has already been called, so the
442 # builder's mixins have been flattened into the test.
Shenghua Zhangaba8bad2018-02-07 02:12:09443 test['trigger_script'] = {
444 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
445 'args': [
446 '--multiple-trigger-configs',
Kenneth Russell73c3bd8b2018-10-19 22:30:19447 json.dumps(test['swarming']['dimension_sets'] +
Shenghua Zhangaba8bad2018-02-07 02:12:09448 tester_config.get('alternate_swarming_dimensions', [])),
449 '--multiple-dimension-script-verbose',
450 'True'
451 ],
452 }
Ben Pastenea9e583b2019-01-16 02:57:26453 elif self.is_chromeos(tester_config) and tester_config.get('use_swarming',
454 True):
455 # The presence of the "device_type" dimension indicates that the tests
456 # are targetting CrOS hardware and so need the special trigger script.
457 dimension_sets = tester_config['swarming']['dimension_sets']
458 if all('device_type' in ds for ds in dimension_sets):
459 test['trigger_script'] = {
460 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
461 }
Shenghua Zhangaba8bad2018-02-07 02:12:09462
Ben Pastene858f4be2019-01-09 23:52:09463 def add_android_presentation_args(self, tester_config, test_name, result):
464 args = result.get('args', [])
465 args.append('--gs-results-bucket=chromium-result-details')
466 if (result['swarming']['can_use_on_swarming_builders'] and not
467 tester_config.get('skip_merge_script', False)):
468 result['merge'] = {
469 'args': [
470 '--bucket',
471 'chromium-result-details',
472 '--test-name',
473 test_name
474 ],
475 'script': '//build/android/pylib/results/presentation/'
476 'test_results_presentation.py',
477 }
478 if not tester_config.get('skip_cipd_packages', False):
Ben Pastenee5949ea82019-01-10 21:45:26479 cipd_packages = result['swarming'].get('cipd_packages', [])
480 cipd_packages.append(
Ben Pastene858f4be2019-01-09 23:52:09481 {
482 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
483 'location': 'bin',
484 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
485 }
Ben Pastenee5949ea82019-01-10 21:45:26486 )
487 result['swarming']['cipd_packages'] = cipd_packages
Ben Pastene858f4be2019-01-09 23:52:09488 if not tester_config.get('skip_output_links', False):
489 result['swarming']['output_links'] = [
490 {
491 'link': [
492 'https://luci-logdog.appspot.com/v/?s',
493 '=android%2Fswarming%2Flogcats%2F',
494 '${TASK_ID}%2F%2B%2Funified_logcats',
495 ],
496 'name': 'shard #${SHARD_INDEX} logcats',
497 },
498 ]
499 if args:
500 result['args'] = args
501
Kenneth Russelleb60cbd22017-12-05 07:54:28502 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
503 test_config):
504 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15505 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28506 return None
507 result = copy.deepcopy(test_config)
508 if 'test' in result:
509 result['name'] = test_name
510 else:
511 result['test'] = test_name
512 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21513
514 self.initialize_args_for_test(
515 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28516 if self.is_android(tester_config) and tester_config.get('use_swarming',
517 True):
Ben Pastene858f4be2019-01-09 23:52:09518 self.add_android_presentation_args(tester_config, test_name, result)
519 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42520
Stephen Martinis0382bc12018-09-17 22:29:07521 result = self.update_and_cleanup_test(
522 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09523 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28524 return result
525
526 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
527 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01528 if not self.should_run_on_tester(waterfall, tester_name, test_name,
529 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28530 return None
531 result = copy.deepcopy(test_config)
532 result['isolate_name'] = result.get('isolate_name', test_name)
533 result['name'] = test_name
534 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01535 self.initialize_args_for_test(result, tester_config)
Ben Pastene858f4be2019-01-09 23:52:09536 if tester_config.get('use_android_presentation', False):
537 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07538 result = self.update_and_cleanup_test(
539 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09540 self.add_common_test_properties(result, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28541 return result
542
543 def generate_script_test(self, waterfall, tester_name, tester_config,
544 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01545 if not self.should_run_on_tester(waterfall, tester_name, test_name,
546 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28547 return None
548 result = {
549 'name': test_name,
550 'script': test_config['script']
551 }
Stephen Martinis0382bc12018-09-17 22:29:07552 result = self.update_and_cleanup_test(
553 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28554 return result
555
556 def generate_junit_test(self, waterfall, tester_name, tester_config,
557 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01558 del tester_config
559 if not self.should_run_on_tester(waterfall, tester_name, test_name,
560 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28561 return None
562 result = {
563 'test': test_name,
564 }
565 return result
566
567 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
568 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01569 if not self.should_run_on_tester(waterfall, tester_name, test_name,
570 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28571 return None
572 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28573 if 'test' in result and result['test'] != test_name:
574 result['name'] = test_name
575 else:
576 result['test'] = test_name
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
Stephen Martinis2a0667022018-09-25 22:31:14581 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01582 substitutions = {
583 # Any machine in waterfalls.pyl which desires to run GPU tests
584 # must provide the os_type key.
585 'os_type': tester_config['os_type'],
586 'gpu_vendor_id': '0',
587 'gpu_device_id': '0',
588 }
Stephen Martinis2a0667022018-09-25 22:31:14589 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01590 if 'gpu' in dimension_set:
591 # First remove the driver version, then split into vendor and device.
592 gpu = dimension_set['gpu']
Kenneth Russell384a1732019-03-16 02:36:02593 # Handle certain specialized named GPUs.
594 if gpu.startswith('nvidia-quadro-p400'):
595 gpu = ['10de', '1cb3']
596 elif gpu.startswith('intel-hd-630'):
597 gpu = ['8086', '5912']
598 else:
599 gpu = gpu.split('-')[0].split(':')
Kenneth Russell8a386d42018-06-02 09:48:01600 substitutions['gpu_vendor_id'] = gpu[0]
601 substitutions['gpu_device_id'] = gpu[1]
602 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
603
604 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
605 test_name, test_config):
606 # These are all just specializations of isolated script tests with
607 # a bunch of boilerplate command line arguments added.
608
609 # The step name must end in 'test' or 'tests' in order for the
610 # results to automatically show up on the flakiness dashboard.
611 # (At least, this was true some time ago.) Continue to use this
612 # naming convention for the time being to minimize changes.
613 step_name = test_config.get('name', test_name)
614 if not (step_name.endswith('test') or step_name.endswith('tests')):
615 step_name = '%s_tests' % step_name
616 result = self.generate_isolated_script_test(
617 waterfall, tester_name, tester_config, step_name, test_config)
618 if not result:
619 return None
620 result['isolate_name'] = 'telemetry_gpu_integration_test'
621 args = result.get('args', [])
622 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14623
624 # These tests upload and download results from cloud storage and therefore
625 # aren't idempotent yet. https://crbug.com/549140.
626 result['swarming']['idempotent'] = False
627
Kenneth Russell44910c32018-12-03 23:35:11628 # The GPU tests act much like integration tests for the entire browser, and
629 # tend to uncover flakiness bugs more readily than other test suites. In
630 # order to surface any flakiness more readily to the developer of the CL
631 # which is introducing it, we disable retries with patch on the commit
632 # queue.
633 result['should_retry_with_patch'] = False
634
Kenneth Russell8a386d42018-06-02 09:48:01635 args = [
636 test_to_run,
637 '--show-stdout',
638 '--browser=%s' % tester_config['browser_config'],
639 # --passthrough displays more of the logging in Telemetry when
640 # run via typ, in particular some of the warnings about tests
641 # being expected to fail, but passing.
642 '--passthrough',
643 '-v',
644 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
645 ] + args
646 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14647 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01648 return result
649
Kenneth Russelleb60cbd22017-12-05 07:54:28650 def get_test_generator_map(self):
651 return {
652 'cts_tests': CTSGenerator(self),
Kenneth Russell8a386d42018-06-02 09:48:01653 'gpu_telemetry_tests': GPUTelemetryTestGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28654 'gtest_tests': GTestGenerator(self),
655 'instrumentation_tests': InstrumentationTestGenerator(self),
656 'isolated_scripts': IsolatedScriptTestGenerator(self),
657 'junit_tests': JUnitGenerator(self),
658 'scripts': ScriptGenerator(self),
659 }
660
Kenneth Russell8a386d42018-06-02 09:48:01661 def get_test_type_remapper(self):
662 return {
663 # These are a specialization of isolated_scripts with a bunch of
664 # boilerplate command line arguments added to each one.
665 'gpu_telemetry_tests': 'isolated_scripts',
666 }
667
Kenneth Russelleb60cbd22017-12-05 07:54:28668 def check_composition_test_suites(self):
669 # Pre-pass to catch errors reliably.
670 for name, value in self.test_suites.iteritems():
671 if isinstance(value, list):
672 for entry in value:
673 if isinstance(self.test_suites[entry], list):
Nico Weberd18b8962018-05-16 19:39:38674 raise BBGenErr('Composition test suites may not refer to other '
675 'composition test suites (error found while '
Kenneth Russelleb60cbd22017-12-05 07:54:28676 'processing %s)' % name)
677
Stephen Martinis54d64ad2018-09-21 22:16:20678 def flatten_test_suites(self):
679 new_test_suites = {}
680 for name, value in self.test_suites.get('basic_suites', {}).iteritems():
681 new_test_suites[name] = value
682 for name, value in self.test_suites.get('compound_suites', {}).iteritems():
683 if name in new_test_suites:
684 raise BBGenErr('Composition test suite names may not duplicate basic '
685 'test suite names (error found while processsing %s' % (
686 name))
687 new_test_suites[name] = value
688 self.test_suites = new_test_suites
689
Kenneth Russelleb60cbd22017-12-05 07:54:28690 def resolve_composition_test_suites(self):
Stephen Martinis54d64ad2018-09-21 22:16:20691 self.flatten_test_suites()
692
Kenneth Russelleb60cbd22017-12-05 07:54:28693 self.check_composition_test_suites()
694 for name, value in self.test_suites.iteritems():
695 if isinstance(value, list):
696 # Resolve this to a dictionary.
697 full_suite = {}
698 for entry in value:
699 suite = self.test_suites[entry]
700 full_suite.update(suite)
701 self.test_suites[name] = full_suite
702
703 def link_waterfalls_to_test_suites(self):
704 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43705 for tester_name, tester in waterfall['machines'].iteritems():
706 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28707 if not value in self.test_suites:
708 # Hard / impossible to cover this in the unit test.
709 raise self.unknown_test_suite(
710 value, tester_name, waterfall['name']) # pragma: no cover
711 tester['test_suites'][suite] = self.test_suites[value]
712
713 def load_configuration_files(self):
714 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
715 self.test_suites = self.load_pyl_file('test_suites.pyl')
716 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:01717 self.mixins = self.load_pyl_file('mixins.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:28718
719 def resolve_configuration_files(self):
720 self.resolve_composition_test_suites()
721 self.link_waterfalls_to_test_suites()
722
Nico Weberd18b8962018-05-16 19:39:38723 def unknown_bot(self, bot_name, waterfall_name):
724 return BBGenErr(
725 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
726
Kenneth Russelleb60cbd22017-12-05 07:54:28727 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
728 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38729 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28730 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
731
732 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
733 return BBGenErr(
734 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
735 ' on waterfall ' + waterfall_name)
736
Stephen Martinisb72f6d22018-10-04 23:29:01737 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:07738 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:32739
740 Checks in the waterfall, builder, and test objects for mixins.
741 """
742 def valid_mixin(mixin_name):
743 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:01744 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:32745 raise BBGenErr("bad mixin %s" % mixin_name)
746 def must_be_list(mixins, typ, name):
747 """Asserts that given mixins are a list."""
748 if not isinstance(mixins, list):
749 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
750
Stephen Martinisb72f6d22018-10-04 23:29:01751 if 'mixins' in waterfall:
752 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
753 for mixin in waterfall['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32754 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01755 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32756
Stephen Martinisb72f6d22018-10-04 23:29:01757 if 'mixins' in builder:
758 must_be_list(builder['mixins'], 'builder', builder_name)
759 for mixin in builder['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32760 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01761 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32762
Stephen Martinisb72f6d22018-10-04 23:29:01763 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:07764 return test
765
Stephen Martinis2a0667022018-09-25 22:31:14766 test_name = test.get('name')
767 if not test_name:
768 test_name = test.get('test')
769 if not test_name: # pragma: no cover
770 # Not the best name, but we should say something.
771 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:01772 must_be_list(test['mixins'], 'test', test_name)
773 for mixin in test['mixins']:
Stephen Martinis0382bc12018-09-17 22:29:07774 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01775 test = self.apply_mixin(self.mixins[mixin], test)
776 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:07777 return test
Stephen Martinisb6a50492018-09-12 23:59:32778
Stephen Martinisb72f6d22018-10-04 23:29:01779 def apply_mixin(self, mixin, test):
780 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:32781
Stephen Martinis0382bc12018-09-17 22:29:07782 Mixins will not override an existing key. This is to ensure exceptions can
783 override a setting a mixin applies.
784
Stephen Martinisb72f6d22018-10-04 23:29:01785 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:32786 'dimension_sets', which is how normal test suites specify their dimensions,
787 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
788 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:01789
Stephen Martinisb6a50492018-09-12 23:59:32790 """
791 new_test = copy.deepcopy(test)
792 mixin = copy.deepcopy(mixin)
793
Stephen Martinisb72f6d22018-10-04 23:29:01794 if 'swarming' in mixin:
795 swarming_mixin = mixin['swarming']
796 new_test.setdefault('swarming', {})
797 if 'dimensions' in swarming_mixin:
798 new_test['swarming'].setdefault('dimension_sets', [{}])
799 for dimension_set in new_test['swarming']['dimension_sets']:
800 dimension_set.update(swarming_mixin['dimensions'])
801 del swarming_mixin['dimensions']
Stephen Martinisb6a50492018-09-12 23:59:32802
Stephen Martinisb72f6d22018-10-04 23:29:01803 # python dict update doesn't do recursion at all. Just hard code the
804 # nested update we need (mixin['swarming'] shouldn't clobber
805 # test['swarming'], but should update it).
806 new_test['swarming'].update(swarming_mixin)
807 del mixin['swarming']
808
Wezc0e835b702018-10-30 00:38:41809 if '$mixin_append' in mixin:
810 # Values specified under $mixin_append should be appended to existing
811 # lists, rather than replacing them.
812 mixin_append = mixin['$mixin_append']
813 for key in mixin_append:
814 new_test.setdefault(key, [])
815 if not isinstance(mixin_append[key], list):
816 raise BBGenErr(
817 'Key "' + key + '" in $mixin_append must be a list.')
818 if not isinstance(new_test[key], list):
819 raise BBGenErr(
820 'Cannot apply $mixin_append to non-list "' + key + '".')
821 new_test[key].extend(mixin_append[key])
822 if 'args' in mixin_append:
823 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
824 del mixin['$mixin_append']
825
Stephen Martinisb72f6d22018-10-04 23:29:01826 new_test.update(mixin)
Stephen Martinis0382bc12018-09-17 22:29:07827
Stephen Martinisb6a50492018-09-12 23:59:32828 return new_test
829
Kenneth Russelleb60cbd22017-12-05 07:54:28830 def generate_waterfall_json(self, waterfall):
831 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:28832 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:01833 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:43834 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28835 tests = {}
Kenneth Russell139f8642017-12-05 08:51:43836 # Copy only well-understood entries in the machine's configuration
837 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:28838 if 'additional_compile_targets' in config:
839 tests['additional_compile_targets'] = config[
840 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:43841 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28842 if test_type not in generator_map:
843 raise self.unknown_test_suite_type(
844 test_type, name, waterfall['name']) # pragma: no cover
845 test_generator = generator_map[test_type]
Nico Weber79dc5f6852018-07-13 19:38:49846 # Let multiple kinds of generators generate the same kinds
847 # of tests. For example, gpu_telemetry_tests are a
848 # specialization of isolated_scripts.
849 new_tests = test_generator.generate(
850 waterfall, name, config, input_tests)
851 remapped_test_type = test_type_remapper.get(test_type, test_type)
852 tests[remapped_test_type] = test_generator.sort(
853 tests.get(remapped_test_type, []) + new_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:28854 all_tests[name] = tests
855 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
856 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
857 return json.dumps(all_tests, indent=2, separators=(',', ': '),
858 sort_keys=True) + '\n'
859
860 def generate_waterfalls(self): # pragma: no cover
861 self.load_configuration_files()
862 self.resolve_configuration_files()
863 filters = self.args.waterfall_filters
864 suffix = '.json'
865 if self.args.new_files:
866 suffix = '.new' + suffix
867 for waterfall in self.waterfalls:
868 should_gen = not filters or waterfall['name'] in filters
869 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:11870 file_path = waterfall['name'] + suffix
871 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:28872 self.generate_waterfall_json(waterfall))
873
Nico Weberd18b8962018-05-16 19:39:38874 def get_valid_bot_names(self):
John Budorick699282e2019-02-13 01:27:33875 # Extract bot names from infra/config/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:42876 # NOTE: This reference can cause issues; if a file changes there, the
877 # presubmit here won't be run by default. A manually maintained list there
878 # tries to run presubmit here when luci-milo.cfg is changed. If any other
879 # references to configs outside of this directory are added, please change
880 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
881 # never ends up in an invalid state.
Nico Weberd18b8962018-05-16 19:39:38882 bot_names = set()
John Budorickc12abd12018-08-14 19:37:43883 infra_config_dir = os.path.abspath(
884 os.path.join(os.path.dirname(__file__),
John Budorick699282e2019-02-13 01:27:33885 '..', '..', 'infra', 'config'))
John Budorickc12abd12018-08-14 19:37:43886 milo_configs = [
887 os.path.join(infra_config_dir, 'luci-milo.cfg'),
888 os.path.join(infra_config_dir, 'luci-milo-dev.cfg'),
889 ]
890 for c in milo_configs:
891 for l in self.read_file(c).splitlines():
892 if (not 'name: "buildbucket/luci.chromium.' in l and
Hans Wennborg98ffd7d92019-02-06 14:14:34893 not 'name: "buildbucket/luci.chrome.' in l and
John Budorickb1833612018-12-07 04:36:41894 not 'name: "buildbot/chromium.' in l and
895 not 'name: "buildbot/tryserver.chromium.' in l):
John Budorickc12abd12018-08-14 19:37:43896 continue
897 # l looks like
898 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
899 # Extract win_chromium_dbg_ng part.
900 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:38901 return bot_names
902
Kenneth Russell8a386d42018-06-02 09:48:01903 def get_bots_that_do_not_actually_exist(self):
904 # Some of the bots on the chromium.gpu.fyi waterfall in particular
905 # are defined only to be mirrored into trybots, and don't actually
906 # exist on any of the waterfalls or consoles.
907 return [
Jamie Madilldc7feeb82018-11-14 04:54:56908 'ANGLE GPU Win10 Release (Intel HD 630)',
909 'ANGLE GPU Win10 Release (NVIDIA)',
Corentin Wallez7d3f4fa22018-11-19 23:35:44910 'Dawn GPU Linux Release (Intel HD 630)',
911 'Dawn GPU Linux Release (NVIDIA)',
912 'Dawn GPU Mac Release (Intel)',
913 'Dawn GPU Mac Retina Release (AMD)',
914 'Dawn GPU Mac Retina Release (NVIDIA)',
Jamie Madilldc7feeb82018-11-14 04:54:56915 'Dawn GPU Win10 Release (Intel HD 630)',
916 'Dawn GPU Win10 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:01917 'Optional Android Release (Nexus 5X)',
918 'Optional Linux Release (Intel HD 630)',
919 'Optional Linux Release (NVIDIA)',
920 'Optional Mac Release (Intel)',
921 'Optional Mac Retina Release (AMD)',
922 'Optional Mac Retina Release (NVIDIA)',
923 'Optional Win10 Release (Intel HD 630)',
924 'Optional Win10 Release (NVIDIA)',
925 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:08926 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:29927 'linux-blink-rel-dummy',
928 'mac10.10-blink-rel-dummy',
929 'mac10.11-blink-rel-dummy',
930 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:20931 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:29932 'mac10.13-blink-rel-dummy',
933 'win7-blink-rel-dummy',
934 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:08935 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:33936 'WebKit Linux composite_after_paint Dummy Builder',
Nico Weber7fc8b9da2018-06-08 19:22:08937 'WebKit Linux layout_ng Dummy Builder',
938 'WebKit Linux root_layer_scrolls Dummy Builder',
Stephen Martinis769b25112018-08-30 18:52:06939 # chromium, due to https://crbug.com/878915
940 'win-dbg',
941 'win32-dbg',
Ben Pastene7687c0112019-03-05 22:43:14942 # Defined in internal configs.
943 'chromeos-amd64-generic-google-rel',
Kenneth Russell8a386d42018-06-02 09:48:01944 ]
945
Stephen Martinisf83893722018-09-19 00:02:18946 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:20947 self.check_input_files_sorting(verbose)
948
Kenneth Russelleb60cbd22017-12-05 07:54:28949 self.load_configuration_files()
Stephen Martinis54d64ad2018-09-21 22:16:20950 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:28951 self.check_composition_test_suites()
Nico Weberd18b8962018-05-16 19:39:38952
953 # All bots should exist.
954 bot_names = self.get_valid_bot_names()
Kenneth Russell8a386d42018-06-02 09:48:01955 bots_that_dont_exist = self.get_bots_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:38956 for waterfall in self.waterfalls:
957 for bot_name in waterfall['machines']:
Kenneth Russell8a386d42018-06-02 09:48:01958 if bot_name in bots_that_dont_exist:
959 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38960 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:08961 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:38962 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:52963 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:32964 if waterfall['name'] in ['tryserver.webrtc',
965 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:38966 # These waterfalls have their bot configs in a different repo.
967 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:52968 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:38969 raise self.unknown_bot(bot_name, waterfall['name'])
970
Kenneth Russelleb60cbd22017-12-05 07:54:28971 # All test suites must be referenced.
972 suites_seen = set()
973 generator_map = self.get_test_generator_map()
974 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43975 for bot_name, tester in waterfall['machines'].iteritems():
976 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28977 if suite_type not in generator_map:
978 raise self.unknown_test_suite_type(suite_type, bot_name,
979 waterfall['name'])
980 if suite not in self.test_suites:
981 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
982 suites_seen.add(suite)
983 # Since we didn't resolve the configuration files, this set
984 # includes both composition test suites and regular ones.
985 resolved_suites = set()
986 for suite_name in suites_seen:
987 suite = self.test_suites[suite_name]
988 if isinstance(suite, list):
989 for sub_suite in suite:
990 resolved_suites.add(sub_suite)
991 resolved_suites.add(suite_name)
992 # At this point, every key in test_suites.pyl should be referenced.
993 missing_suites = set(self.test_suites.keys()) - resolved_suites
994 if missing_suites:
995 raise BBGenErr('The following test suites were unreferenced by bots on '
996 'the waterfalls: ' + str(missing_suites))
997
998 # All test suite exceptions must refer to bots on the waterfall.
999 all_bots = set()
1000 missing_bots = set()
1001 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431002 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281003 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281004 # In order to disambiguate between bots with the same name on
1005 # different waterfalls, support has been added to various
1006 # exceptions for concatenating the waterfall name after the bot
1007 # name.
1008 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281009 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381010 removals = (exception.get('remove_from', []) +
1011 exception.get('remove_gtest_from', []) +
1012 exception.get('modifications', {}).keys())
1013 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281014 if removal not in all_bots:
1015 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411016
1017 missing_bots = missing_bots - set(bots_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281018 if missing_bots:
1019 raise BBGenErr('The following nonexistent machines were referenced in '
1020 'the test suite exceptions: ' + str(missing_bots))
1021
Stephen Martinis0382bc12018-09-17 22:29:071022 # All mixins must be referenced
1023 seen_mixins = set()
1024 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011025 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071026 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011027 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071028 for suite in self.test_suites.values():
1029 if isinstance(suite, list):
1030 # Don't care about this, it's a composition, which shouldn't include a
1031 # swarming mixin.
1032 continue
1033
1034 for test in suite.values():
1035 if not isinstance(test, dict):
1036 # Some test suites have top level keys, which currently can't be
1037 # swarming mixin entries. Ignore them
1038 continue
1039
Stephen Martinisb72f6d22018-10-04 23:29:011040 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071041
Stephen Martinisb72f6d22018-10-04 23:29:011042 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071043 if missing_mixins:
1044 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1045 ' referenced in a waterfall, machine, or test suite.' % (
1046 str(missing_mixins)))
1047
Stephen Martinis54d64ad2018-09-21 22:16:201048
1049 def type_assert(self, node, typ, filename, verbose=False):
1050 """Asserts that the Python AST node |node| is of type |typ|.
1051
1052 If verbose is set, it prints out some helpful context lines, showing where
1053 exactly the error occurred in the file.
1054 """
1055 if not isinstance(node, typ):
1056 if verbose:
1057 lines = [""] + self.read_file(filename).splitlines()
1058
1059 context = 2
1060 lines_start = max(node.lineno - context, 0)
1061 # Add one to include the last line
1062 lines_end = min(node.lineno + context, len(lines)) + 1
1063 lines = (
1064 ['== %s ==\n' % filename] +
1065 ["<snip>\n"] +
1066 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1067 lines[lines_start:lines_start + context])] +
1068 ['-' * 80 + '\n'] +
1069 ['%d %s' % (node.lineno, lines[node.lineno])] +
1070 ['-' * (node.col_offset + 3) + '^' + '-' * (
1071 80 - node.col_offset - 4) + '\n'] +
1072 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1073 lines[node.lineno + 1:lines_end])] +
1074 ["<snip>\n"]
1075 )
1076 # Print out a useful message when a type assertion fails.
1077 for l in lines:
1078 self.print_line(l.strip())
1079
1080 node_dumped = ast.dump(node, annotate_fields=False)
1081 # If the node is huge, truncate it so everything fits in a terminal
1082 # window.
1083 if len(node_dumped) > 60: # pragma: no cover
1084 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1085 raise BBGenErr(
1086 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1087 ' be %s, is %s' % (
1088 filename, node_dumped,
1089 node.lineno, typ, type(node)))
1090
1091 def ensure_ast_dict_keys_sorted(self, node, filename, verbose):
1092 is_valid = True
1093
1094 keys = []
1095 # The keys of this dict are ordered as ordered in the file; normal python
1096 # dictionary keys are given an arbitrary order, but since we parsed the
1097 # file itself, the order as given in the file is preserved.
1098 for key in node.keys:
1099 self.type_assert(key, ast.Str, filename, verbose)
1100 keys.append(key.s)
1101
1102 keys_sorted = sorted(keys)
1103 if keys_sorted != keys:
1104 is_valid = False
1105 if verbose:
1106 for line in difflib.unified_diff(
1107 keys,
1108 keys_sorted, fromfile='current (%r)' % filename, tofile='sorted'):
1109 self.print_line(line)
1110
1111 if len(set(keys)) != len(keys):
1112 for i in range(len(keys_sorted)-1):
1113 if keys_sorted[i] == keys_sorted[i+1]:
1114 self.print_line('Key %s is duplicated' % keys_sorted[i])
1115 is_valid = False
1116 return is_valid
Stephen Martinisf83893722018-09-19 00:02:181117
1118 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201119 # TODO(https://crbug.com/886993): Add the ability for this script to
1120 # actually format the files, rather than just complain if they're
1121 # incorrectly formatted.
1122 bad_files = set()
1123
1124 for filename in (
Stephen Martinisb72f6d22018-10-04 23:29:011125 'mixins.pyl',
Stephen Martinis54d64ad2018-09-21 22:16:201126 'test_suites.pyl',
1127 'test_suite_exceptions.pyl',
1128 ):
Stephen Martinisf83893722018-09-19 00:02:181129 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1130
Stephen Martinisf83893722018-09-19 00:02:181131 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201132 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181133 module = parsed.body
1134
1135 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201136 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181137 if len(module) != 1: # pragma: no cover
1138 raise BBGenErr('Invalid .pyl file %s' % filename)
1139 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201140 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181141
1142 # Value should be a dictionary.
1143 value = expr.value
Stephen Martinis54d64ad2018-09-21 22:16:201144 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181145
Stephen Martinis54d64ad2018-09-21 22:16:201146 if filename == 'test_suites.pyl':
1147 expected_keys = ['basic_suites', 'compound_suites']
1148 actual_keys = [node.s for node in value.keys]
1149 assert all(key in expected_keys for key in actual_keys), (
1150 'Invalid %r file; expected keys %r, got %r' % (
1151 filename, expected_keys, actual_keys))
1152 suite_dicts = [node for node in value.values]
1153 # Only two keys should mean only 1 or 2 values
1154 assert len(suite_dicts) <= 2
1155 for suite_group in suite_dicts:
1156 if not self.ensure_ast_dict_keys_sorted(
1157 suite_group, filename, verbose):
1158 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181159
Stephen Martinis54d64ad2018-09-21 22:16:201160 else:
1161 if not self.ensure_ast_dict_keys_sorted(
1162 value, filename, verbose):
1163 bad_files.add(filename)
1164
1165 # waterfalls.pyl is slightly different, just do it manually here
1166 filename = 'waterfalls.pyl'
1167 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1168
1169 # Must be a module.
1170 self.type_assert(parsed, ast.Module, filename, verbose)
1171 module = parsed.body
1172
1173 # Only one expression in the module.
1174 self.type_assert(module, list, filename, verbose)
1175 if len(module) != 1: # pragma: no cover
1176 raise BBGenErr('Invalid .pyl file %s' % filename)
1177 expr = module[0]
1178 self.type_assert(expr, ast.Expr, filename, verbose)
1179
1180 # Value should be a list.
1181 value = expr.value
1182 self.type_assert(value, ast.List, filename, verbose)
1183
1184 keys = []
1185 for val in value.elts:
1186 self.type_assert(val, ast.Dict, filename, verbose)
1187 waterfall_name = None
1188 for key, val in zip(val.keys, val.values):
1189 self.type_assert(key, ast.Str, filename, verbose)
1190 if key.s == 'machines':
1191 if not self.ensure_ast_dict_keys_sorted(val, filename, verbose):
1192 bad_files.add(filename)
1193
1194 if key.s == "name":
1195 self.type_assert(val, ast.Str, filename, verbose)
1196 waterfall_name = val.s
1197 assert waterfall_name
1198 keys.append(waterfall_name)
1199
1200 if sorted(keys) != keys:
1201 bad_files.add(filename)
1202 if verbose: # pragma: no cover
1203 for line in difflib.unified_diff(
1204 keys,
1205 sorted(keys), fromfile='current', tofile='sorted'):
1206 self.print_line(line)
Stephen Martinisf83893722018-09-19 00:02:181207
1208 if bad_files:
1209 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201210 'The following files have invalid keys: %s\n. They are either '
1211 'unsorted, or have duplicates.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181212
Kenneth Russelleb60cbd22017-12-05 07:54:281213 def check_output_file_consistency(self, verbose=False):
1214 self.load_configuration_files()
1215 # All waterfalls must have been written by this script already.
1216 self.resolve_configuration_files()
1217 ungenerated_waterfalls = set()
1218 for waterfall in self.waterfalls:
1219 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111220 file_path = waterfall['name'] + '.json'
1221 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281222 if expected != current:
1223 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321224 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501225 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281226 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321227 'contents:')
1228 for line in difflib.unified_diff(
1229 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501230 current.splitlines(),
1231 fromfile='expected', tofile='current'):
1232 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281233 if ungenerated_waterfalls:
1234 raise BBGenErr('The following waterfalls have not been properly '
1235 'autogenerated by generate_buildbot_json.py: ' +
1236 str(ungenerated_waterfalls))
1237
1238 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501239 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281240 self.check_output_file_consistency(verbose) # pragma: no cover
1241
1242 def parse_args(self, argv): # pragma: no cover
Karen Qiane24b7ee2019-02-12 23:37:061243
1244 # RawTextHelpFormatter allows for styling of help statement
1245 parser = argparse.ArgumentParser(formatter_class=
1246 argparse.RawTextHelpFormatter)
1247
1248 group = parser.add_mutually_exclusive_group()
1249 group.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281250 '-c', '--check', action='store_true', help=
1251 'Do consistency checks of configuration and generated files and then '
1252 'exit. Used during presubmit. Causes the tool to not generate any files.')
Karen Qiane24b7ee2019-02-12 23:37:061253 group.add_argument(
1254 '--query', type=str, help=
1255 ("Returns raw JSON information of buildbots and tests.\n" +
1256 "Examples:\n" +
1257 " List all bots (all info):\n" +
1258 " --query bots\n\n" +
1259 " List all bots and only their associated tests:\n" +
1260 " --query bots/tests\n\n" +
1261 " List all information about 'bot1' " +
1262 "(make sure you have quotes):\n" +
1263 " --query bot/'bot1'\n\n" +
1264 " List tests running for 'bot1' (make sure you have quotes):\n" +
1265 " --query bot/'bot1'/tests\n\n" +
1266 " List all tests:\n" +
1267 " --query tests\n\n" +
1268 " List all tests and the bots running them:\n" +
1269 " --query tests/bots\n\n"+
1270 " List all tests that satisfy multiple parameters\n" +
1271 " (separation of parameters by '&' symbol):\n" +
1272 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
1273 " List all tests that run with a specific flag:\n" +
1274 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
1275 " List specific test (make sure you have quotes):\n"
1276 " --query test/'test1'\n\n"
1277 " List all bots running 'test1' " +
1278 "(make sure you have quotes):\n" +
1279 " --query test/'test1'/bots" ))
Kenneth Russelleb60cbd22017-12-05 07:54:281280 parser.add_argument(
1281 '-n', '--new-files', action='store_true', help=
1282 'Write output files as .new.json. Useful during development so old and '
1283 'new files can be looked at side-by-side.')
1284 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501285 '-v', '--verbose', action='store_true', help=
1286 'Increases verbosity. Affects consistency checks.')
1287 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281288 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1289 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111290 parser.add_argument(
1291 '--pyl-files-dir', type=os.path.realpath,
1292 help='Path to the directory containing the input .pyl files.')
Karen Qiane24b7ee2019-02-12 23:37:061293 parser.add_argument(
1294 '--json', help=
1295 ("Outputs results into a json file. Only works with query function.\n" +
1296 "Examples:\n" +
1297 " Outputs file into specified json file: \n" +
1298 " --json <file-name-here.json>"))
Kenneth Russelleb60cbd22017-12-05 07:54:281299 self.args = parser.parse_args(argv)
Karen Qiane24b7ee2019-02-12 23:37:061300 if self.args.json and not self.args.query:
1301 parser.error("The --json flag can only be used with --query.")
1302
1303 def does_test_match(self, test_info, params_dict):
1304 """Checks to see if the test matches the parameters given.
1305
1306 Compares the provided test_info with the params_dict to see
1307 if the bot matches the parameters given. If so, returns True.
1308 Else, returns false.
1309
1310 Args:
1311 test_info (dict): Information about a specific bot provided
1312 in the format shown in waterfalls.pyl
1313 params_dict (dict): Dictionary of parameters and their values
1314 to look for in the bot
1315 Ex: {
1316 'device_os':'android',
1317 '--flag':True,
1318 'mixins': ['mixin1', 'mixin2'],
1319 'ex_key':'ex_value'
1320 }
1321
1322 """
1323 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
1324 'kvm', 'pool', 'integrity'] # dimension parameters
1325 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
1326 'can_use_on_swarming_builders']
1327 for param in params_dict:
1328 # if dimension parameter
1329 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
1330 if not 'swarming' in test_info:
1331 return False
1332 swarming = test_info['swarming']
1333 if param in SWARMING_PARAMS:
1334 if not param in swarming:
1335 return False
1336 if not str(swarming[param]) == params_dict[param]:
1337 return False
1338 else:
1339 if not 'dimension_sets' in swarming:
1340 return False
1341 d_set = swarming['dimension_sets']
1342 # only looking at the first dimension set
1343 if not param in d_set[0]:
1344 return False
1345 if not d_set[0][param] == params_dict[param]:
1346 return False
1347
1348 # if flag
1349 elif param.startswith('--'):
1350 if not 'args' in test_info:
1351 return False
1352 if not param in test_info['args']:
1353 return False
1354
1355 # not dimension parameter/flag/mixin
1356 else:
1357 if not param in test_info:
1358 return False
1359 if not test_info[param] == params_dict[param]:
1360 return False
1361 return True
1362 def error_msg(self, msg):
1363 """Prints an error message.
1364
1365 In addition to a catered error message, also prints
1366 out where the user can find more help. Then, program exits.
1367 """
1368 self.print_line(msg + (' If you need more information, ' +
1369 'please run with -h or --help to see valid commands.'))
1370 sys.exit(1)
1371
1372 def find_bots_that_run_test(self, test, bots):
1373 matching_bots = []
1374 for bot in bots:
1375 bot_info = bots[bot]
1376 tests = self.flatten_tests_for_bot(bot_info)
1377 for test_info in tests:
1378 test_name = ""
1379 if 'name' in test_info:
1380 test_name = test_info['name']
1381 elif 'test' in test_info:
1382 test_name = test_info['test']
1383 if not test_name == test:
1384 continue
1385 matching_bots.append(bot)
1386 return matching_bots
1387
1388 def find_tests_with_params(self, tests, params_dict):
1389 matching_tests = []
1390 for test_name in tests:
1391 test_info = tests[test_name]
1392 if not self.does_test_match(test_info, params_dict):
1393 continue
1394 if not test_name in matching_tests:
1395 matching_tests.append(test_name)
1396 return matching_tests
1397
1398 def flatten_waterfalls_for_query(self, waterfalls):
1399 bots = {}
1400 for waterfall in waterfalls:
1401 waterfall_json = json.loads(self.generate_waterfall_json(waterfall))
1402 for bot in waterfall_json:
1403 bot_info = waterfall_json[bot]
1404 if 'AAAAA' not in bot:
1405 bots[bot] = bot_info
1406 return bots
1407
1408 def flatten_tests_for_bot(self, bot_info):
1409 """Returns a list of flattened tests.
1410
1411 Returns a list of tests not grouped by test category
1412 for a specific bot.
1413 """
1414 TEST_CATS = self.get_test_generator_map().keys()
1415 tests = []
1416 for test_cat in TEST_CATS:
1417 if not test_cat in bot_info:
1418 continue
1419 test_cat_tests = bot_info[test_cat]
1420 tests = tests + test_cat_tests
1421 return tests
1422
1423 def flatten_tests_for_query(self, test_suites):
1424 """Returns a flattened dictionary of tests.
1425
1426 Returns a dictionary of tests associate with their
1427 configuration, not grouped by their test suite.
1428 """
1429 tests = {}
1430 for test_suite in test_suites.itervalues():
1431 for test in test_suite:
1432 test_info = test_suite[test]
1433 test_name = test
1434 if 'name' in test_info:
1435 test_name = test_info['name']
1436 tests[test_name] = test_info
1437 return tests
1438
1439 def parse_query_filter_params(self, params):
1440 """Parses the filter parameters.
1441
1442 Creates a dictionary from the parameters provided
1443 to filter the bot array.
1444 """
1445 params_dict = {}
1446 for p in params:
1447 # flag
1448 if p.startswith("--"):
1449 params_dict[p] = True
1450 else:
1451 pair = p.split(":")
1452 if len(pair) != 2:
1453 self.error_msg('Invalid command.')
1454 # regular parameters
1455 if pair[1].lower() == "true":
1456 params_dict[pair[0]] = True
1457 elif pair[1].lower() == "false":
1458 params_dict[pair[0]] = False
1459 else:
1460 params_dict[pair[0]] = pair[1]
1461 return params_dict
1462
1463 def get_test_suites_dict(self, bots):
1464 """Returns a dictionary of bots and their tests.
1465
1466 Returns a dictionary of bots and a list of their associated tests.
1467 """
1468 test_suite_dict = dict()
1469 for bot in bots:
1470 bot_info = bots[bot]
1471 tests = self.flatten_tests_for_bot(bot_info)
1472 test_suite_dict[bot] = tests
1473 return test_suite_dict
1474
1475 def output_query_result(self, result, json_file=None):
1476 """Outputs the result of the query.
1477
1478 If a json file parameter name is provided, then
1479 the result is output into the json file. If not,
1480 then the result is printed to the console.
1481 """
1482 output = json.dumps(result, indent=2)
1483 if json_file:
1484 self.write_file(json_file, output)
1485 else:
1486 self.print_line(output)
1487 return
1488
1489 def query(self, args):
1490 """Queries tests or bots.
1491
1492 Depending on the arguments provided, outputs a json of
1493 tests or bots matching the appropriate optional parameters provided.
1494 """
1495 # split up query statement
1496 query = args.query.split('/')
1497 self.load_configuration_files()
1498 self.resolve_configuration_files()
1499
1500 # flatten bots json
1501 tests = self.test_suites
1502 bots = self.flatten_waterfalls_for_query(self.waterfalls)
1503
1504 cmd_class = query[0]
1505
1506 # For queries starting with 'bots'
1507 if cmd_class == "bots":
1508 if len(query) == 1:
1509 return self.output_query_result(bots, args.json)
1510 # query with specific parameters
1511 elif len(query) == 2:
1512 if query[1] == 'tests':
1513 test_suites_dict = self.get_test_suites_dict(bots)
1514 return self.output_query_result(test_suites_dict, args.json)
1515 else:
1516 self.error_msg("This query should be in the format: bots/tests.")
1517
1518 else:
1519 self.error_msg("This query should have 0 or 1 '/', found %s instead."
1520 % str(len(query)-1))
1521
1522 # For queries starting with 'bot'
1523 elif cmd_class == "bot":
1524 if not len(query) == 2 and not len(query) == 3:
1525 self.error_msg("Command should have 1 or 2 '/', found %s instead."
1526 % str(len(query)-1))
1527 bot_id = query[1]
1528 if not bot_id in bots:
1529 self.error_msg("No bot named '" + bot_id + "' found.")
1530 bot_info = bots[bot_id]
1531 if len(query) == 2:
1532 return self.output_query_result(bot_info, args.json)
1533 if not query[2] == 'tests':
1534 self.error_msg("The query should be in the format:" +
1535 "bot/<bot-name>/tests.")
1536
1537 bot_tests = self.flatten_tests_for_bot(bot_info)
1538 return self.output_query_result(bot_tests, args.json)
1539
1540 # For queries starting with 'tests'
1541 elif cmd_class == "tests":
1542 if not len(query) == 1 and not len(query) == 2:
1543 self.error_msg("The query should have 0 or 1 '/', found %s instead."
1544 % str(len(query)-1))
1545 flattened_tests = self.flatten_tests_for_query(tests)
1546 if len(query) == 1:
1547 return self.output_query_result(flattened_tests, args.json)
1548
1549 # create params dict
1550 params = query[1].split('&')
1551 params_dict = self.parse_query_filter_params(params)
1552 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
1553 return self.output_query_result(matching_bots)
1554
1555 # For queries starting with 'test'
1556 elif cmd_class == "test":
1557 if not len(query) == 2 and not len(query) == 3:
1558 self.error_msg("The query should have 1 or 2 '/', found %s instead."
1559 % str(len(query)-1))
1560 test_id = query[1]
1561 if len(query) == 2:
1562 flattened_tests = self.flatten_tests_for_query(tests)
1563 for test in flattened_tests:
1564 if test == test_id:
1565 return self.output_query_result(flattened_tests[test], args.json)
1566 self.error_msg("There is no test named %s." % test_id)
1567 if not query[2] == 'bots':
1568 self.error_msg("The query should be in the format: " +
1569 "test/<test-name>/bots")
1570 bots_for_test = self.find_bots_that_run_test(test_id, bots)
1571 return self.output_query_result(bots_for_test)
1572
1573 else:
1574 self.error_msg("Your command did not match any valid commands." +
1575 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:281576
1577 def main(self, argv): # pragma: no cover
1578 self.parse_args(argv)
1579 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:501580 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:061581 elif self.args.query:
1582 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:281583 else:
1584 self.generate_waterfalls()
1585 return 0
1586
1587if __name__ == "__main__": # pragma: no cover
1588 generator = BBJSONGenerator()
John Budorick699282e2019-02-13 01:27:331589 sys.exit(generator.main(sys.argv[1:]))