blob: 6dfec5e499165296b88947e135e3c9e92790bd58 [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
Nodir Turakulovfce34292019-12-18 17:05:41199 self.gn_isolate_map = None
Kenneth Russelleb60cbd22017-12-05 07:54:28200
201 def generate_abs_file_path(self, relative_path):
202 return os.path.join(self.this_dir, relative_path) # pragma: no cover
203
Stephen Martinis7eb8b612018-09-21 00:17:50204 def print_line(self, line):
205 # Exists so that tests can mock
206 print line # pragma: no cover
207
Kenneth Russelleb60cbd22017-12-05 07:54:28208 def read_file(self, relative_path):
209 with open(self.generate_abs_file_path(
210 relative_path)) as fp: # pragma: no cover
211 return fp.read() # pragma: no cover
212
213 def write_file(self, relative_path, contents):
214 with open(self.generate_abs_file_path(
215 relative_path), 'wb') as fp: # pragma: no cover
216 fp.write(contents) # pragma: no cover
217
Zhiling Huangbe008172018-03-08 19:13:11218 def pyl_file_path(self, filename):
219 if self.args and self.args.pyl_files_dir:
220 return os.path.join(self.args.pyl_files_dir, filename)
221 return filename
222
Kenneth Russelleb60cbd22017-12-05 07:54:28223 def load_pyl_file(self, filename):
224 try:
Zhiling Huangbe008172018-03-08 19:13:11225 return ast.literal_eval(self.read_file(
226 self.pyl_file_path(filename)))
Kenneth Russelleb60cbd22017-12-05 07:54:28227 except (SyntaxError, ValueError) as e: # pragma: no cover
228 raise BBGenErr('Failed to parse pyl file "%s": %s' %
229 (filename, e)) # pragma: no cover
230
Kenneth Russell8a386d42018-06-02 09:48:01231 # TOOD(kbr): require that os_type be specified for all bots in waterfalls.pyl.
232 # Currently it is only mandatory for bots which run GPU tests. Change these to
233 # use [] instead of .get().
Kenneth Russelleb60cbd22017-12-05 07:54:28234 def is_android(self, tester_config):
235 return tester_config.get('os_type') == 'android'
236
Ben Pastenea9e583b2019-01-16 02:57:26237 def is_chromeos(self, tester_config):
238 return tester_config.get('os_type') == 'chromeos'
239
Kenneth Russell8a386d42018-06-02 09:48:01240 def is_linux(self, tester_config):
241 return tester_config.get('os_type') == 'linux'
242
Kai Ninomiya40de9f52019-10-18 21:38:49243 def is_mac(self, tester_config):
244 return tester_config.get('os_type') == 'mac'
245
246 def is_win(self, tester_config):
247 return tester_config.get('os_type') == 'win'
248
249 def is_win64(self, tester_config):
250 return (tester_config.get('os_type') == 'win' and
251 tester_config.get('browser_config') == 'release_x64')
252
Kenneth Russelleb60cbd22017-12-05 07:54:28253 def get_exception_for_test(self, test_name, test_config):
254 # gtests may have both "test" and "name" fields, and usually, if the "name"
255 # field is specified, it means that the same test is being repurposed
256 # multiple times with different command line arguments. To handle this case,
257 # prefer to lookup per the "name" field of the test itself, as opposed to
258 # the "test_name", which is actually the "test" field.
259 if 'name' in test_config:
260 return self.exceptions.get(test_config['name'])
261 else:
262 return self.exceptions.get(test_name)
263
Nico Weberb0b3f5862018-07-13 18:45:15264 def should_run_on_tester(self, waterfall, tester_name,test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28265 # Currently, the only reason a test should not run on a given tester is that
266 # it's in the exceptions. (Once the GPU waterfall generation script is
267 # incorporated here, the rules will become more complex.)
268 exception = self.get_exception_for_test(test_name, test_config)
269 if not exception:
270 return True
Kenneth Russell8ceeabf2017-12-11 17:53:28271 remove_from = None
Kenneth Russelleb60cbd22017-12-05 07:54:28272 remove_from = exception.get('remove_from')
Kenneth Russell8ceeabf2017-12-11 17:53:28273 if remove_from:
274 if tester_name in remove_from:
275 return False
276 # TODO(kbr): this code path was added for some tests (including
277 # android_webview_unittests) on one machine (Nougat Phone
278 # Tester) which exists with the same name on two waterfalls,
279 # chromium.android and chromium.fyi; the tests are run on one
280 # but not the other. Once the bots are all uniquely named (a
281 # different ongoing project) this code should be removed.
282 # TODO(kbr): add coverage.
283 return (tester_name + ' ' + waterfall['name']
284 not in remove_from) # pragma: no cover
285 return True
Kenneth Russelleb60cbd22017-12-05 07:54:28286
Nico Weber79dc5f6852018-07-13 19:38:49287 def get_test_modifications(self, test, test_name, tester_name):
Kenneth Russelleb60cbd22017-12-05 07:54:28288 exception = self.get_exception_for_test(test_name, test)
289 if not exception:
290 return None
Nico Weber79dc5f6852018-07-13 19:38:49291 return exception.get('modifications', {}).get(tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28292
Brian Sheedye6ea0ee2019-07-11 02:54:37293 def get_test_replacements(self, test, test_name, tester_name):
294 exception = self.get_exception_for_test(test_name, test)
295 if not exception:
296 return None
297 return exception.get('replacements', {}).get(tester_name)
298
Kenneth Russell8a386d42018-06-02 09:48:01299 def merge_command_line_args(self, arr, prefix, splitter):
300 prefix_len = len(prefix)
Kenneth Russell650995a2018-05-03 21:17:01301 idx = 0
302 first_idx = -1
Kenneth Russell8a386d42018-06-02 09:48:01303 accumulated_args = []
Kenneth Russell650995a2018-05-03 21:17:01304 while idx < len(arr):
305 flag = arr[idx]
306 delete_current_entry = False
Kenneth Russell8a386d42018-06-02 09:48:01307 if flag.startswith(prefix):
308 arg = flag[prefix_len:]
309 accumulated_args.extend(arg.split(splitter))
Kenneth Russell650995a2018-05-03 21:17:01310 if first_idx < 0:
311 first_idx = idx
312 else:
313 delete_current_entry = True
314 if delete_current_entry:
315 del arr[idx]
316 else:
317 idx += 1
318 if first_idx >= 0:
Kenneth Russell8a386d42018-06-02 09:48:01319 arr[first_idx] = prefix + splitter.join(accumulated_args)
320 return arr
321
322 def maybe_fixup_args_array(self, arr):
323 # The incoming array of strings may be an array of command line
324 # arguments. To make it easier to turn on certain features per-bot or
325 # per-test-suite, look specifically for certain flags and merge them
326 # appropriately.
327 # --enable-features=Feature1 --enable-features=Feature2
328 # are merged to:
329 # --enable-features=Feature1,Feature2
330 # and:
331 # --extra-browser-args=arg1 --extra-browser-args=arg2
332 # are merged to:
333 # --extra-browser-args=arg1 arg2
334 arr = self.merge_command_line_args(arr, '--enable-features=', ',')
335 arr = self.merge_command_line_args(arr, '--extra-browser-args=', ' ')
Kenneth Russell650995a2018-05-03 21:17:01336 return arr
337
Kenneth Russelleb60cbd22017-12-05 07:54:28338 def dictionary_merge(self, a, b, path=None, update=True):
339 """http://stackoverflow.com/questions/7204805/
340 python-dictionaries-of-dictionaries-merge
341 merges b into a
342 """
343 if path is None:
344 path = []
345 for key in b:
346 if key in a:
347 if isinstance(a[key], dict) and isinstance(b[key], dict):
348 self.dictionary_merge(a[key], b[key], path + [str(key)])
349 elif a[key] == b[key]:
350 pass # same leaf value
351 elif isinstance(a[key], list) and isinstance(b[key], list):
Stephen Martinis3bed2ab2018-04-23 19:42:06352 # Args arrays are lists of strings. Just concatenate them,
353 # and don't sort them, in order to keep some needed
354 # arguments adjacent (like --time-out-ms [arg], etc.)
Kenneth Russell8ceeabf2017-12-11 17:53:28355 if all(isinstance(x, str)
356 for x in itertools.chain(a[key], b[key])):
Kenneth Russell650995a2018-05-03 21:17:01357 a[key] = self.maybe_fixup_args_array(a[key] + b[key])
Kenneth Russell8ceeabf2017-12-11 17:53:28358 else:
359 # TODO(kbr): this only works properly if the two arrays are
360 # the same length, which is currently always the case in the
361 # swarming dimension_sets that we have to merge. It will fail
362 # to merge / override 'args' arrays which are different
363 # length.
364 for idx in xrange(len(b[key])):
365 try:
366 a[key][idx] = self.dictionary_merge(a[key][idx], b[key][idx],
367 path + [str(key), str(idx)],
368 update=update)
Jeff Yoon8154e582019-12-03 23:30:01369 except (IndexError, TypeError):
370 raise BBGenErr('Error merging lists by key "%s" from source %s '
371 'into target %s at index %s. Verify target list '
372 'length is equal or greater than source'
373 % (str(key), str(b), str(a), str(idx)))
John Budorick5bc387fe2019-05-09 20:02:53374 elif update:
375 if b[key] is None:
376 del a[key]
377 else:
378 a[key] = b[key]
Kenneth Russelleb60cbd22017-12-05 07:54:28379 else:
380 raise BBGenErr('Conflict at %s' % '.'.join(
381 path + [str(key)])) # pragma: no cover
John Budorick5bc387fe2019-05-09 20:02:53382 elif b[key] is not None:
Kenneth Russelleb60cbd22017-12-05 07:54:28383 a[key] = b[key]
384 return a
385
John Budorickab108712018-09-01 00:12:21386 def initialize_args_for_test(
387 self, generated_test, tester_config, additional_arg_keys=None):
388
389 args = []
390 args.extend(generated_test.get('args', []))
391 args.extend(tester_config.get('args', []))
John Budorickedfe7f872018-01-23 15:27:22392
Kenneth Russell8a386d42018-06-02 09:48:01393 def add_conditional_args(key, fn):
John Budorickab108712018-09-01 00:12:21394 val = generated_test.pop(key, [])
395 if fn(tester_config):
396 args.extend(val)
Kenneth Russell8a386d42018-06-02 09:48:01397
398 add_conditional_args('desktop_args', lambda cfg: not self.is_android(cfg))
399 add_conditional_args('linux_args', self.is_linux)
400 add_conditional_args('android_args', self.is_android)
Ben Pastene52890ace2019-05-24 20:03:36401 add_conditional_args('chromeos_args', self.is_chromeos)
Kai Ninomiya40de9f52019-10-18 21:38:49402 add_conditional_args('mac_args', self.is_mac)
403 add_conditional_args('win_args', self.is_win)
404 add_conditional_args('win64_args', self.is_win64)
Kenneth Russell8a386d42018-06-02 09:48:01405
John Budorickab108712018-09-01 00:12:21406 for key in additional_arg_keys or []:
407 args.extend(generated_test.pop(key, []))
408 args.extend(tester_config.get(key, []))
409
410 if args:
411 generated_test['args'] = self.maybe_fixup_args_array(args)
Kenneth Russell8a386d42018-06-02 09:48:01412
Kenneth Russelleb60cbd22017-12-05 07:54:28413 def initialize_swarming_dictionary_for_test(self, generated_test,
414 tester_config):
415 if 'swarming' not in generated_test:
416 generated_test['swarming'] = {}
Dirk Pranke81ff51c2017-12-09 19:24:28417 if not 'can_use_on_swarming_builders' in generated_test['swarming']:
418 generated_test['swarming'].update({
419 'can_use_on_swarming_builders': tester_config.get('use_swarming', True)
420 })
Kenneth Russelleb60cbd22017-12-05 07:54:28421 if 'swarming' in tester_config:
Ben Pastene796c62862018-06-13 02:40:03422 if ('dimension_sets' not in generated_test['swarming'] and
423 'dimension_sets' in tester_config['swarming']):
Kenneth Russelleb60cbd22017-12-05 07:54:28424 generated_test['swarming']['dimension_sets'] = copy.deepcopy(
425 tester_config['swarming']['dimension_sets'])
426 self.dictionary_merge(generated_test['swarming'],
427 tester_config['swarming'])
428 # Apply any Android-specific Swarming dimensions after the generic ones.
429 if 'android_swarming' in generated_test:
430 if self.is_android(tester_config): # pragma: no cover
431 self.dictionary_merge(
432 generated_test['swarming'],
433 generated_test['android_swarming']) # pragma: no cover
434 del generated_test['android_swarming'] # pragma: no cover
435
436 def clean_swarming_dictionary(self, swarming_dict):
437 # Clean out redundant entries from a test's "swarming" dictionary.
438 # This is really only needed to retain 100% parity with the
439 # handwritten JSON files, and can be removed once all the files are
440 # autogenerated.
441 if 'shards' in swarming_dict:
442 if swarming_dict['shards'] == 1: # pragma: no cover
443 del swarming_dict['shards'] # pragma: no cover
Kenneth Russellfbda3c532017-12-08 23:57:24444 if 'hard_timeout' in swarming_dict:
445 if swarming_dict['hard_timeout'] == 0: # pragma: no cover
446 del swarming_dict['hard_timeout'] # pragma: no cover
Stephen Martinisf5f4ea22018-09-20 01:07:43447 if not swarming_dict.get('can_use_on_swarming_builders', False):
Kenneth Russelleb60cbd22017-12-05 07:54:28448 # Remove all other keys.
449 for k in swarming_dict.keys(): # pragma: no cover
450 if k != 'can_use_on_swarming_builders': # pragma: no cover
451 del swarming_dict[k] # pragma: no cover
452
Stephen Martinis0382bc12018-09-17 22:29:07453 def update_and_cleanup_test(self, test, test_name, tester_name, tester_config,
454 waterfall):
455 # Apply swarming mixins.
Stephen Martinisb72f6d22018-10-04 23:29:01456 test = self.apply_all_mixins(
Stephen Martinis0382bc12018-09-17 22:29:07457 test, waterfall, tester_name, tester_config)
Kenneth Russelleb60cbd22017-12-05 07:54:28458 # See if there are any exceptions that need to be merged into this
459 # test's specification.
Nico Weber79dc5f6852018-07-13 19:38:49460 modifications = self.get_test_modifications(test, test_name, tester_name)
Kenneth Russelleb60cbd22017-12-05 07:54:28461 if modifications:
462 test = self.dictionary_merge(test, modifications)
Dirk Pranke1b767092017-12-07 04:44:23463 if 'swarming' in test:
464 self.clean_swarming_dictionary(test['swarming'])
Ben Pastenee012aea42019-05-14 22:32:28465 # Ensure all Android Swarming tests run only on userdebug builds if another
466 # build type was not specified.
467 if 'swarming' in test and self.is_android(tester_config):
468 for d in test['swarming'].get('dimension_sets', []):
Ben Pastened15aa8a2019-05-16 16:59:22469 if d.get('os') == 'Android' and not d.get('device_os_type'):
Ben Pastenee012aea42019-05-14 22:32:28470 d['device_os_type'] = 'userdebug'
Brian Sheedye6ea0ee2019-07-11 02:54:37471 self.replace_test_args(test, test_name, tester_name)
Ben Pastenee012aea42019-05-14 22:32:28472
Kenneth Russelleb60cbd22017-12-05 07:54:28473 return test
474
Brian Sheedye6ea0ee2019-07-11 02:54:37475 def replace_test_args(self, test, test_name, tester_name):
476 replacements = self.get_test_replacements(
477 test, test_name, tester_name) or {}
478 valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
479 for key, replacement_dict in replacements.iteritems():
480 if key not in valid_replacement_keys:
481 raise BBGenErr(
482 'Given replacement key %s for %s on %s is not in the list of valid '
483 'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
484 for replacement_key, replacement_val in replacement_dict.iteritems():
485 found_key = False
486 for i, test_key in enumerate(test.get(key, [])):
487 # Handle both the key/value being replaced being defined as two
488 # separate items or as key=value.
489 if test_key == replacement_key:
490 found_key = True
491 # Handle flags without values.
492 if replacement_val == None:
493 del test[key][i]
494 else:
495 test[key][i+1] = replacement_val
496 break
497 elif test_key.startswith(replacement_key + '='):
498 found_key = True
499 if replacement_val == None:
500 del test[key][i]
501 else:
502 test[key][i] = '%s=%s' % (replacement_key, replacement_val)
503 break
504 if not found_key:
505 raise BBGenErr('Could not find %s in existing list of values for key '
506 '%s in %s on %s' % (replacement_key, key, test_name,
507 tester_name))
508
Shenghua Zhangaba8bad2018-02-07 02:12:09509 def add_common_test_properties(self, test, tester_config):
510 if tester_config.get('use_multi_dimension_trigger_script'):
Kenneth Russell73c3bd8b2018-10-19 22:30:19511 # Assumes update_and_cleanup_test has already been called, so the
512 # builder's mixins have been flattened into the test.
Shenghua Zhangaba8bad2018-02-07 02:12:09513 test['trigger_script'] = {
514 'script': '//testing/trigger_scripts/trigger_multiple_dimensions.py',
515 'args': [
516 '--multiple-trigger-configs',
Kenneth Russell73c3bd8b2018-10-19 22:30:19517 json.dumps(test['swarming']['dimension_sets'] +
Shenghua Zhangaba8bad2018-02-07 02:12:09518 tester_config.get('alternate_swarming_dimensions', [])),
519 '--multiple-dimension-script-verbose',
520 'True'
521 ],
522 }
Ben Pastenea9e583b2019-01-16 02:57:26523 elif self.is_chromeos(tester_config) and tester_config.get('use_swarming',
524 True):
525 # The presence of the "device_type" dimension indicates that the tests
526 # are targetting CrOS hardware and so need the special trigger script.
527 dimension_sets = tester_config['swarming']['dimension_sets']
528 if all('device_type' in ds for ds in dimension_sets):
529 test['trigger_script'] = {
530 'script': '//testing/trigger_scripts/chromeos_device_trigger.py',
531 }
Shenghua Zhangaba8bad2018-02-07 02:12:09532
Ben Pastene858f4be2019-01-09 23:52:09533 def add_android_presentation_args(self, tester_config, test_name, result):
534 args = result.get('args', [])
John Budorick262ae112019-07-12 19:24:38535 bucket = tester_config.get('results_bucket', 'chromium-result-details')
536 args.append('--gs-results-bucket=%s' % bucket)
Ben Pastene858f4be2019-01-09 23:52:09537 if (result['swarming']['can_use_on_swarming_builders'] and not
538 tester_config.get('skip_merge_script', False)):
539 result['merge'] = {
540 'args': [
541 '--bucket',
John Budorick262ae112019-07-12 19:24:38542 bucket,
Ben Pastene858f4be2019-01-09 23:52:09543 '--test-name',
544 test_name
545 ],
546 'script': '//build/android/pylib/results/presentation/'
547 'test_results_presentation.py',
548 }
549 if not tester_config.get('skip_cipd_packages', False):
Ben Pastenee5949ea82019-01-10 21:45:26550 cipd_packages = result['swarming'].get('cipd_packages', [])
551 cipd_packages.append(
Ben Pastene858f4be2019-01-09 23:52:09552 {
553 'cipd_package': 'infra/tools/luci/logdog/butler/${platform}',
554 'location': 'bin',
555 'revision': 'git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c',
556 }
Ben Pastenee5949ea82019-01-10 21:45:26557 )
558 result['swarming']['cipd_packages'] = cipd_packages
Ben Pastene858f4be2019-01-09 23:52:09559 if not tester_config.get('skip_output_links', False):
560 result['swarming']['output_links'] = [
561 {
562 'link': [
563 'https://luci-logdog.appspot.com/v/?s',
564 '=android%2Fswarming%2Flogcats%2F',
565 '${TASK_ID}%2F%2B%2Funified_logcats',
566 ],
567 'name': 'shard #${SHARD_INDEX} logcats',
568 },
569 ]
570 if args:
571 result['args'] = args
572
Kenneth Russelleb60cbd22017-12-05 07:54:28573 def generate_gtest(self, waterfall, tester_name, tester_config, test_name,
574 test_config):
575 if not self.should_run_on_tester(
Nico Weberb0b3f5862018-07-13 18:45:15576 waterfall, tester_name, test_name, test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28577 return None
578 result = copy.deepcopy(test_config)
579 if 'test' in result:
580 result['name'] = test_name
581 else:
582 result['test'] = test_name
583 self.initialize_swarming_dictionary_for_test(result, tester_config)
John Budorickab108712018-09-01 00:12:21584
585 self.initialize_args_for_test(
586 result, tester_config, additional_arg_keys=['gtest_args'])
Kenneth Russelleb60cbd22017-12-05 07:54:28587 if self.is_android(tester_config) and tester_config.get('use_swarming',
588 True):
Ben Pastene858f4be2019-01-09 23:52:09589 self.add_android_presentation_args(tester_config, test_name, result)
590 result['args'] = result.get('args', []) + ['--recover-devices']
Benjamin Pastene766d48f52017-12-18 21:47:42591
Stephen Martinis0382bc12018-09-17 22:29:07592 result = self.update_and_cleanup_test(
593 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09594 self.add_common_test_properties(result, tester_config)
Stephen Martinisbc7b7772019-05-01 22:01:43595
596 if not result.get('merge'):
597 # TODO(https://crbug.com/958376): Consider adding the ability to not have
598 # this default.
599 result['merge'] = {
600 'script': '//testing/merge_scripts/standard_gtest_merge.py',
601 'args': [],
602 }
Kenneth Russelleb60cbd22017-12-05 07:54:28603 return result
604
605 def generate_isolated_script_test(self, waterfall, tester_name, tester_config,
606 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01607 if not self.should_run_on_tester(waterfall, tester_name, test_name,
608 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28609 return None
610 result = copy.deepcopy(test_config)
611 result['isolate_name'] = result.get('isolate_name', test_name)
612 result['name'] = test_name
613 self.initialize_swarming_dictionary_for_test(result, tester_config)
Kenneth Russell8a386d42018-06-02 09:48:01614 self.initialize_args_for_test(result, tester_config)
Ben Pastene858f4be2019-01-09 23:52:09615 if tester_config.get('use_android_presentation', False):
616 self.add_android_presentation_args(tester_config, test_name, result)
Stephen Martinis0382bc12018-09-17 22:29:07617 result = self.update_and_cleanup_test(
618 result, test_name, tester_name, tester_config, waterfall)
Shenghua Zhangaba8bad2018-02-07 02:12:09619 self.add_common_test_properties(result, tester_config)
Stephen Martinisf50047062019-05-06 22:26:17620
621 if not result.get('merge'):
622 # TODO(https://crbug.com/958376): Consider adding the ability to not have
623 # this default.
624 result['merge'] = {
625 'script': '//testing/merge_scripts/standard_isolated_script_merge.py',
626 'args': [],
627 }
Kenneth Russelleb60cbd22017-12-05 07:54:28628 return result
629
630 def generate_script_test(self, waterfall, tester_name, tester_config,
631 test_name, test_config):
Brian Sheedy158cd0f2019-04-26 01:12:44632 # TODO(https://crbug.com/953072): Remove this check whenever a better
633 # long-term solution is implemented.
634 if (waterfall.get('forbid_script_tests', False) or
635 waterfall['machines'][tester_name].get('forbid_script_tests', False)):
636 raise BBGenErr('Attempted to generate a script test on tester ' +
637 tester_name + ', which explicitly forbids script tests')
Kenneth Russell8a386d42018-06-02 09:48:01638 if not self.should_run_on_tester(waterfall, tester_name, test_name,
639 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28640 return None
641 result = {
642 'name': test_name,
643 'script': test_config['script']
644 }
Stephen Martinis0382bc12018-09-17 22:29:07645 result = self.update_and_cleanup_test(
646 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28647 return result
648
649 def generate_junit_test(self, waterfall, tester_name, tester_config,
650 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01651 if not self.should_run_on_tester(waterfall, tester_name, test_name,
652 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28653 return None
John Budorickdef6acb2019-09-17 22:51:09654 result = copy.deepcopy(test_config)
655 result.update({
John Budorickcadc4952019-09-16 23:51:37656 'name': test_name,
657 'test': test_config.get('test', test_name),
John Budorickdef6acb2019-09-17 22:51:09658 })
659 self.initialize_args_for_test(result, tester_config)
660 result = self.update_and_cleanup_test(
661 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28662 return result
663
664 def generate_instrumentation_test(self, waterfall, tester_name, tester_config,
665 test_name, test_config):
Kenneth Russell8a386d42018-06-02 09:48:01666 if not self.should_run_on_tester(waterfall, tester_name, test_name,
667 test_config):
Kenneth Russelleb60cbd22017-12-05 07:54:28668 return None
669 result = copy.deepcopy(test_config)
Kenneth Russell8ceeabf2017-12-11 17:53:28670 if 'test' in result and result['test'] != test_name:
671 result['name'] = test_name
672 else:
673 result['test'] = test_name
Stephen Martinis0382bc12018-09-17 22:29:07674 result = self.update_and_cleanup_test(
675 result, test_name, tester_name, tester_config, waterfall)
Kenneth Russelleb60cbd22017-12-05 07:54:28676 return result
677
Stephen Martinis2a0667022018-09-25 22:31:14678 def substitute_gpu_args(self, tester_config, swarming_config, args):
Kenneth Russell8a386d42018-06-02 09:48:01679 substitutions = {
680 # Any machine in waterfalls.pyl which desires to run GPU tests
681 # must provide the os_type key.
682 'os_type': tester_config['os_type'],
683 'gpu_vendor_id': '0',
684 'gpu_device_id': '0',
685 }
Stephen Martinis2a0667022018-09-25 22:31:14686 dimension_set = swarming_config['dimension_sets'][0]
Kenneth Russell8a386d42018-06-02 09:48:01687 if 'gpu' in dimension_set:
688 # First remove the driver version, then split into vendor and device.
689 gpu = dimension_set['gpu']
Kenneth Russell384a1732019-03-16 02:36:02690 # Handle certain specialized named GPUs.
691 if gpu.startswith('nvidia-quadro-p400'):
692 gpu = ['10de', '1cb3']
693 elif gpu.startswith('intel-hd-630'):
694 gpu = ['8086', '5912']
Brian Sheedyf9387db7b2019-08-05 19:26:10695 elif gpu.startswith('intel-uhd-630'):
696 gpu = ['8086', '3e92']
Kenneth Russell384a1732019-03-16 02:36:02697 else:
698 gpu = gpu.split('-')[0].split(':')
Kenneth Russell8a386d42018-06-02 09:48:01699 substitutions['gpu_vendor_id'] = gpu[0]
700 substitutions['gpu_device_id'] = gpu[1]
701 return [string.Template(arg).safe_substitute(substitutions) for arg in args]
702
703 def generate_gpu_telemetry_test(self, waterfall, tester_name, tester_config,
Bo Liu555a0f92019-03-29 12:11:56704 test_name, test_config, is_android_webview):
Kenneth Russell8a386d42018-06-02 09:48:01705 # These are all just specializations of isolated script tests with
706 # a bunch of boilerplate command line arguments added.
707
708 # The step name must end in 'test' or 'tests' in order for the
709 # results to automatically show up on the flakiness dashboard.
710 # (At least, this was true some time ago.) Continue to use this
711 # naming convention for the time being to minimize changes.
712 step_name = test_config.get('name', test_name)
713 if not (step_name.endswith('test') or step_name.endswith('tests')):
714 step_name = '%s_tests' % step_name
715 result = self.generate_isolated_script_test(
716 waterfall, tester_name, tester_config, step_name, test_config)
717 if not result:
718 return None
719 result['isolate_name'] = 'telemetry_gpu_integration_test'
720 args = result.get('args', [])
721 test_to_run = result.pop('telemetry_test_name', test_name)
erikchen6da2d9b2018-08-03 23:01:14722
723 # These tests upload and download results from cloud storage and therefore
724 # aren't idempotent yet. https://crbug.com/549140.
725 result['swarming']['idempotent'] = False
726
Kenneth Russell44910c32018-12-03 23:35:11727 # The GPU tests act much like integration tests for the entire browser, and
728 # tend to uncover flakiness bugs more readily than other test suites. In
729 # order to surface any flakiness more readily to the developer of the CL
730 # which is introducing it, we disable retries with patch on the commit
731 # queue.
732 result['should_retry_with_patch'] = False
733
Bo Liu555a0f92019-03-29 12:11:56734 browser = ('android-webview-instrumentation'
735 if is_android_webview else tester_config['browser_config'])
Kenneth Russell8a386d42018-06-02 09:48:01736 args = [
Bo Liu555a0f92019-03-29 12:11:56737 test_to_run,
738 '--show-stdout',
739 '--browser=%s' % browser,
740 # --passthrough displays more of the logging in Telemetry when
741 # run via typ, in particular some of the warnings about tests
742 # being expected to fail, but passing.
743 '--passthrough',
744 '-v',
745 '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
Kenneth Russell8a386d42018-06-02 09:48:01746 ] + args
747 result['args'] = self.maybe_fixup_args_array(self.substitute_gpu_args(
Stephen Martinis2a0667022018-09-25 22:31:14748 tester_config, result['swarming'], args))
Kenneth Russell8a386d42018-06-02 09:48:01749 return result
750
Kenneth Russelleb60cbd22017-12-05 07:54:28751 def get_test_generator_map(self):
752 return {
Bo Liu555a0f92019-03-29 12:11:56753 'android_webview_gpu_telemetry_tests':
754 GPUTelemetryTestGenerator(self, is_android_webview=True),
755 'cts_tests':
756 CTSGenerator(self),
757 'gpu_telemetry_tests':
758 GPUTelemetryTestGenerator(self),
759 'gtest_tests':
760 GTestGenerator(self),
761 'instrumentation_tests':
762 InstrumentationTestGenerator(self),
763 'isolated_scripts':
764 IsolatedScriptTestGenerator(self),
765 'junit_tests':
766 JUnitGenerator(self),
767 'scripts':
768 ScriptGenerator(self),
Kenneth Russelleb60cbd22017-12-05 07:54:28769 }
770
Kenneth Russell8a386d42018-06-02 09:48:01771 def get_test_type_remapper(self):
772 return {
773 # These are a specialization of isolated_scripts with a bunch of
774 # boilerplate command line arguments added to each one.
Bo Liu555a0f92019-03-29 12:11:56775 'android_webview_gpu_telemetry_tests': 'isolated_scripts',
Kenneth Russell8a386d42018-06-02 09:48:01776 'gpu_telemetry_tests': 'isolated_scripts',
777 }
778
Jeff Yoon8154e582019-12-03 23:30:01779 def check_composition_type_test_suites(self, test_type):
Kenneth Russelleb60cbd22017-12-05 07:54:28780 # Pre-pass to catch errors reliably.
Jeff Yoon8154e582019-12-03 23:30:01781 target_test_suites = self.test_suites.get(test_type, {})
782 # This check is used by both matrix and composition test suites.
783 # Neither can reference each other nor themselves, so we switch depending
784 # on the type that's being checked.
785 if test_type == 'matrix_compound_suites':
786 other_type = 'compound_suites'
787 else:
788 other_type = 'matrix_compound_suites'
789 other_test_suites = self.test_suites.get(other_type, {})
790 basic_suites = self.test_suites.get('basic_suites', {})
791
792 for suite, suite_def in target_test_suites.iteritems():
793 if suite in basic_suites:
794 raise BBGenErr('%s names may not duplicate basic test suite names '
795 '(error found while processsing %s)'
796 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:01797 seen_tests = {} # Maps a test to a test definition.
Jeff Yoon8154e582019-12-03 23:30:01798 for sub_suite in suite_def:
799 if sub_suite in other_test_suites or sub_suite in target_test_suites:
800 raise BBGenErr('%s may not refer to other composition type test '
801 'suites (error found while processing %s)'
802 % (test_type, suite))
Nodir Turakulov28232afd2019-12-17 18:02:01803 if sub_suite not in basic_suites:
Jeff Yoon8154e582019-12-03 23:30:01804 raise BBGenErr('Unable to find reference to %s while processing %s'
805 % (sub_suite, suite))
Nodir Turakulov28232afd2019-12-17 18:02:01806
807 # Ensure that if a test is reachable via multiple basic suites,
808 # all of them have an identical definition of the test.
809 for test_name in basic_suites[sub_suite]:
810 if (test_name in seen_tests and
811 basic_suites[sub_suite][test_name] !=
812 basic_suites[seen_tests[test_name]][test_name]):
813 raise BBGenErr('Conflicting test definitions for %s from %s '
814 'and %s in %s (error found while processing %s)'
815 % (test_name, seen_tests[test_name], sub_suite,
816 test_type, suite))
817 seen_tests[test_name] = sub_suite
Kenneth Russelleb60cbd22017-12-05 07:54:28818
Stephen Martinis54d64ad2018-09-21 22:16:20819 def flatten_test_suites(self):
820 new_test_suites = {}
Jeff Yoon8154e582019-12-03 23:30:01821 test_types = ['basic_suites', 'compound_suites', 'matrix_compound_suites']
822 for category in test_types:
823 for name, value in self.test_suites.get(category, {}).iteritems():
824 new_test_suites[name] = value
Stephen Martinis54d64ad2018-09-21 22:16:20825 self.test_suites = new_test_suites
826
Nodir Turakulovfce34292019-12-18 17:05:41827 def resolve_full_test_targets(self):
828 for suite in self.test_suites['basic_suites'].itervalues():
829 for key, test in suite.iteritems():
830 if not isinstance(test, dict):
831 # Some test definitions are just strings, such as CTS.
832 # Skip them.
833 continue
834
835 # This assumes the recipe logic which prefers 'test' to 'isolate_name'
836 # https://source.chromium.org/chromium/chromium/tools/build/+/master:scripts/slave/recipe_modules/chromium_tests/generators.py;l=89;drc=14c062ba0eb418d3c4623dde41a753241b9df06b
837 # TODO(crbug.com/1035124): clean this up.
838 isolate_name = test.get('test') or test.get('isolate_name') or key
839 gn_entry = self.gn_isolate_map.get(isolate_name)
840 if gn_entry:
841 test['test_target'] = gn_entry['label']
842 else: # pragma: no cover
843 # Some tests do not have an entry gn_isolate_map.pyl, such as
844 # telemetry tests.
845 # TODO(crbug.com/1035304): require an entry in gn_isolate_map.
846 pass
847
Kenneth Russelleb60cbd22017-12-05 07:54:28848 def resolve_composition_test_suites(self):
Jeff Yoon8154e582019-12-03 23:30:01849 self.check_composition_type_test_suites('compound_suites')
Stephen Martinis54d64ad2018-09-21 22:16:20850
Jeff Yoon8154e582019-12-03 23:30:01851 compound_suites = self.test_suites.get('compound_suites', {})
852 # check_composition_type_test_suites() checks that all basic suites
853 # referenced by compound suites exist.
854 basic_suites = self.test_suites.get('basic_suites')
855
856 for name, value in compound_suites.iteritems():
857 # Resolve this to a dictionary.
858 full_suite = {}
859 for entry in value:
860 suite = basic_suites[entry]
861 full_suite.update(suite)
862 compound_suites[name] = full_suite
863
864 def resolve_matrix_compound_test_suites(self):
865 self.check_composition_type_test_suites('matrix_compound_suites')
866
867 matrix_compound_suites = self.test_suites.get('matrix_compound_suites', {})
868 # check_composition_type_test_suites() checks that all basic suites
869 # referenced by matrix suites exist.
870 basic_suites = self.test_suites.get('basic_suites')
871
872 for name, value in matrix_compound_suites.iteritems():
873 # Resolve this to a dictionary.
874 full_suite = {}
875 for test_suite_name, test_suite_config in value.iteritems():
876 swarming_defined = bool(
877 'swarming' in test_suite_config
878 and 'dimension_sets' in test_suite_config['swarming'])
879 test = copy.deepcopy(basic_suites[test_suite_name])
880 for test_config in test.values():
881 if (swarming_defined
882 and 'swarming' in test_config
883 and 'dimension_sets' in test_config['swarming']):
884 self.dictionary_merge(test_config['swarming'],
885 test_suite_config['swarming'])
886 else:
887 test_config.update(test_suite_config)
888 full_suite.update(test)
889 matrix_compound_suites[name] = full_suite
Kenneth Russelleb60cbd22017-12-05 07:54:28890
891 def link_waterfalls_to_test_suites(self):
892 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:43893 for tester_name, tester in waterfall['machines'].iteritems():
894 for suite, value in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:28895 if not value in self.test_suites:
896 # Hard / impossible to cover this in the unit test.
897 raise self.unknown_test_suite(
898 value, tester_name, waterfall['name']) # pragma: no cover
899 tester['test_suites'][suite] = self.test_suites[value]
900
901 def load_configuration_files(self):
902 self.waterfalls = self.load_pyl_file('waterfalls.pyl')
903 self.test_suites = self.load_pyl_file('test_suites.pyl')
904 self.exceptions = self.load_pyl_file('test_suite_exceptions.pyl')
Stephen Martinisb72f6d22018-10-04 23:29:01905 self.mixins = self.load_pyl_file('mixins.pyl')
Nodir Turakulovfce34292019-12-18 17:05:41906 self.gn_isolate_map = self.load_pyl_file('gn_isolate_map.pyl')
Kenneth Russelleb60cbd22017-12-05 07:54:28907
908 def resolve_configuration_files(self):
Nodir Turakulovfce34292019-12-18 17:05:41909 self.resolve_full_test_targets()
Kenneth Russelleb60cbd22017-12-05 07:54:28910 self.resolve_composition_test_suites()
Jeff Yoon8154e582019-12-03 23:30:01911 self.resolve_matrix_compound_test_suites()
912 self.flatten_test_suites()
Kenneth Russelleb60cbd22017-12-05 07:54:28913 self.link_waterfalls_to_test_suites()
914
Nico Weberd18b8962018-05-16 19:39:38915 def unknown_bot(self, bot_name, waterfall_name):
916 return BBGenErr(
917 'Unknown bot name "%s" on waterfall "%s"' % (bot_name, waterfall_name))
918
Kenneth Russelleb60cbd22017-12-05 07:54:28919 def unknown_test_suite(self, suite_name, bot_name, waterfall_name):
920 return BBGenErr(
Nico Weberd18b8962018-05-16 19:39:38921 'Test suite %s from machine %s on waterfall %s not present in '
Kenneth Russelleb60cbd22017-12-05 07:54:28922 'test_suites.pyl' % (suite_name, bot_name, waterfall_name))
923
924 def unknown_test_suite_type(self, suite_type, bot_name, waterfall_name):
925 return BBGenErr(
926 'Unknown test suite type ' + suite_type + ' in bot ' + bot_name +
927 ' on waterfall ' + waterfall_name)
928
Stephen Martinisb72f6d22018-10-04 23:29:01929 def apply_all_mixins(self, test, waterfall, builder_name, builder):
Stephen Martinis0382bc12018-09-17 22:29:07930 """Applies all present swarming mixins to the test for a given builder.
Stephen Martinisb6a50492018-09-12 23:59:32931
932 Checks in the waterfall, builder, and test objects for mixins.
933 """
934 def valid_mixin(mixin_name):
935 """Asserts that the mixin is valid."""
Stephen Martinisb72f6d22018-10-04 23:29:01936 if mixin_name not in self.mixins:
Stephen Martinisb6a50492018-09-12 23:59:32937 raise BBGenErr("bad mixin %s" % mixin_name)
938 def must_be_list(mixins, typ, name):
939 """Asserts that given mixins are a list."""
940 if not isinstance(mixins, list):
941 raise BBGenErr("'%s' in %s '%s' must be a list" % (mixins, typ, name))
942
Stephen Martinisb72f6d22018-10-04 23:29:01943 if 'mixins' in waterfall:
944 must_be_list(waterfall['mixins'], 'waterfall', waterfall['name'])
945 for mixin in waterfall['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32946 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01947 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32948
Stephen Martinisb72f6d22018-10-04 23:29:01949 if 'mixins' in builder:
950 must_be_list(builder['mixins'], 'builder', builder_name)
951 for mixin in builder['mixins']:
Stephen Martinisb6a50492018-09-12 23:59:32952 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01953 test = self.apply_mixin(self.mixins[mixin], test)
Stephen Martinisb6a50492018-09-12 23:59:32954
Stephen Martinisb72f6d22018-10-04 23:29:01955 if not 'mixins' in test:
Stephen Martinis0382bc12018-09-17 22:29:07956 return test
957
Stephen Martinis2a0667022018-09-25 22:31:14958 test_name = test.get('name')
959 if not test_name:
960 test_name = test.get('test')
961 if not test_name: # pragma: no cover
962 # Not the best name, but we should say something.
963 test_name = str(test)
Stephen Martinisb72f6d22018-10-04 23:29:01964 must_be_list(test['mixins'], 'test', test_name)
965 for mixin in test['mixins']:
Stephen Martinis0382bc12018-09-17 22:29:07966 valid_mixin(mixin)
Stephen Martinisb72f6d22018-10-04 23:29:01967 test = self.apply_mixin(self.mixins[mixin], test)
968 del test['mixins']
Stephen Martinis0382bc12018-09-17 22:29:07969 return test
Stephen Martinisb6a50492018-09-12 23:59:32970
Stephen Martinisb72f6d22018-10-04 23:29:01971 def apply_mixin(self, mixin, test):
972 """Applies a mixin to a test.
Stephen Martinisb6a50492018-09-12 23:59:32973
Stephen Martinis0382bc12018-09-17 22:29:07974 Mixins will not override an existing key. This is to ensure exceptions can
975 override a setting a mixin applies.
976
Stephen Martinisb72f6d22018-10-04 23:29:01977 Swarming dimensions are handled in a special way. Instead of specifying
Stephen Martinisb6a50492018-09-12 23:59:32978 'dimension_sets', which is how normal test suites specify their dimensions,
979 you specify a 'dimensions' key, which maps to a dictionary. This dictionary
980 is then applied to every dimension set in the test.
Stephen Martinisb72f6d22018-10-04 23:29:01981
Stephen Martinisb6a50492018-09-12 23:59:32982 """
983 new_test = copy.deepcopy(test)
984 mixin = copy.deepcopy(mixin)
985
Stephen Martinisb72f6d22018-10-04 23:29:01986 if 'swarming' in mixin:
987 swarming_mixin = mixin['swarming']
988 new_test.setdefault('swarming', {})
989 if 'dimensions' in swarming_mixin:
990 new_test['swarming'].setdefault('dimension_sets', [{}])
991 for dimension_set in new_test['swarming']['dimension_sets']:
992 dimension_set.update(swarming_mixin['dimensions'])
993 del swarming_mixin['dimensions']
Stephen Martinisb6a50492018-09-12 23:59:32994
Stephen Martinisb72f6d22018-10-04 23:29:01995 # python dict update doesn't do recursion at all. Just hard code the
996 # nested update we need (mixin['swarming'] shouldn't clobber
997 # test['swarming'], but should update it).
998 new_test['swarming'].update(swarming_mixin)
999 del mixin['swarming']
1000
Wezc0e835b702018-10-30 00:38:411001 if '$mixin_append' in mixin:
1002 # Values specified under $mixin_append should be appended to existing
1003 # lists, rather than replacing them.
1004 mixin_append = mixin['$mixin_append']
1005 for key in mixin_append:
1006 new_test.setdefault(key, [])
1007 if not isinstance(mixin_append[key], list):
1008 raise BBGenErr(
1009 'Key "' + key + '" in $mixin_append must be a list.')
1010 if not isinstance(new_test[key], list):
1011 raise BBGenErr(
1012 'Cannot apply $mixin_append to non-list "' + key + '".')
1013 new_test[key].extend(mixin_append[key])
1014 if 'args' in mixin_append:
1015 new_test['args'] = self.maybe_fixup_args_array(new_test['args'])
1016 del mixin['$mixin_append']
1017
Stephen Martinisb72f6d22018-10-04 23:29:011018 new_test.update(mixin)
Stephen Martinis0382bc12018-09-17 22:29:071019
Stephen Martinisb6a50492018-09-12 23:59:321020 return new_test
1021
Kenneth Russelleb60cbd22017-12-05 07:54:281022 def generate_waterfall_json(self, waterfall):
1023 all_tests = {}
Kenneth Russelleb60cbd22017-12-05 07:54:281024 generator_map = self.get_test_generator_map()
Kenneth Russell8a386d42018-06-02 09:48:011025 test_type_remapper = self.get_test_type_remapper()
Kenneth Russell139f8642017-12-05 08:51:431026 for name, config in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281027 tests = {}
Kenneth Russell139f8642017-12-05 08:51:431028 # Copy only well-understood entries in the machine's configuration
1029 # verbatim into the generated JSON.
Kenneth Russelleb60cbd22017-12-05 07:54:281030 if 'additional_compile_targets' in config:
1031 tests['additional_compile_targets'] = config[
1032 'additional_compile_targets']
Kenneth Russell139f8642017-12-05 08:51:431033 for test_type, input_tests in config.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281034 if test_type not in generator_map:
1035 raise self.unknown_test_suite_type(
1036 test_type, name, waterfall['name']) # pragma: no cover
1037 test_generator = generator_map[test_type]
Nico Weber79dc5f6852018-07-13 19:38:491038 # Let multiple kinds of generators generate the same kinds
1039 # of tests. For example, gpu_telemetry_tests are a
1040 # specialization of isolated_scripts.
1041 new_tests = test_generator.generate(
1042 waterfall, name, config, input_tests)
1043 remapped_test_type = test_type_remapper.get(test_type, test_type)
1044 tests[remapped_test_type] = test_generator.sort(
1045 tests.get(remapped_test_type, []) + new_tests)
Kenneth Russelleb60cbd22017-12-05 07:54:281046 all_tests[name] = tests
1047 all_tests['AAAAA1 AUTOGENERATED FILE DO NOT EDIT'] = {}
1048 all_tests['AAAAA2 See generate_buildbot_json.py to make changes'] = {}
1049 return json.dumps(all_tests, indent=2, separators=(',', ': '),
1050 sort_keys=True) + '\n'
1051
1052 def generate_waterfalls(self): # pragma: no cover
1053 self.load_configuration_files()
1054 self.resolve_configuration_files()
1055 filters = self.args.waterfall_filters
1056 suffix = '.json'
1057 if self.args.new_files:
1058 suffix = '.new' + suffix
1059 for waterfall in self.waterfalls:
1060 should_gen = not filters or waterfall['name'] in filters
1061 if should_gen:
Zhiling Huangbe008172018-03-08 19:13:111062 file_path = waterfall['name'] + suffix
1063 self.write_file(self.pyl_file_path(file_path),
Kenneth Russelleb60cbd22017-12-05 07:54:281064 self.generate_waterfall_json(waterfall))
1065
Nico Weberd18b8962018-05-16 19:39:381066 def get_valid_bot_names(self):
John Budorick699282e2019-02-13 01:27:331067 # Extract bot names from infra/config/luci-milo.cfg.
Stephen Martinis26627cf2018-12-19 01:51:421068 # NOTE: This reference can cause issues; if a file changes there, the
1069 # presubmit here won't be run by default. A manually maintained list there
1070 # tries to run presubmit here when luci-milo.cfg is changed. If any other
1071 # references to configs outside of this directory are added, please change
1072 # their presubmit to run `generate_buildbot_json.py -c`, so that the tree
1073 # never ends up in an invalid state.
Nico Weberd18b8962018-05-16 19:39:381074 bot_names = set()
John Budorickc12abd12018-08-14 19:37:431075 infra_config_dir = os.path.abspath(
1076 os.path.join(os.path.dirname(__file__),
John Budorick699282e2019-02-13 01:27:331077 '..', '..', 'infra', 'config'))
John Budorickc12abd12018-08-14 19:37:431078 milo_configs = [
Garrett Beatybb8322bf2019-10-17 20:53:051079 os.path.join(infra_config_dir, 'generated', 'luci-milo.cfg'),
Garrett Beatye95b81722019-10-24 17:12:181080 os.path.join(infra_config_dir, 'generated', 'luci-milo-dev.cfg'),
John Budorickc12abd12018-08-14 19:37:431081 ]
1082 for c in milo_configs:
1083 for l in self.read_file(c).splitlines():
1084 if (not 'name: "buildbucket/luci.chromium.' in l and
Hans Wennborg98ffd7d92019-02-06 14:14:341085 not 'name: "buildbucket/luci.chrome.' in l and
John Budorickb1833612018-12-07 04:36:411086 not 'name: "buildbot/chromium.' in l and
1087 not 'name: "buildbot/tryserver.chromium.' in l):
John Budorickc12abd12018-08-14 19:37:431088 continue
1089 # l looks like
1090 # `name: "buildbucket/luci.chromium.try/win_chromium_dbg_ng"`
1091 # Extract win_chromium_dbg_ng part.
1092 bot_names.add(l[l.rindex('/') + 1:l.rindex('"')])
Nico Weberd18b8962018-05-16 19:39:381093 return bot_names
1094
Ben Pastene9a010082019-09-25 20:41:371095 def get_builders_that_do_not_actually_exist(self):
Kenneth Russell8a386d42018-06-02 09:48:011096 # Some of the bots on the chromium.gpu.fyi waterfall in particular
1097 # are defined only to be mirrored into trybots, and don't actually
1098 # exist on any of the waterfalls or consoles.
1099 return [
Michael Spangeb07eba62019-05-14 22:22:581100 'GPU FYI Fuchsia Builder',
Yuly Novikoveb26b812019-07-26 02:08:191101 'ANGLE GPU Android Release (Nexus 5X)',
Jamie Madillda894ce2019-04-08 17:19:171102 'ANGLE GPU Linux Release (Intel HD 630)',
1103 'ANGLE GPU Linux Release (NVIDIA)',
1104 'ANGLE GPU Mac Release (Intel)',
1105 'ANGLE GPU Mac Retina Release (AMD)',
1106 'ANGLE GPU Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491107 'ANGLE GPU Win10 x64 Release (Intel HD 630)',
1108 'ANGLE GPU Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011109 'Optional Android Release (Nexus 5X)',
1110 'Optional Linux Release (Intel HD 630)',
1111 'Optional Linux Release (NVIDIA)',
1112 'Optional Mac Release (Intel)',
1113 'Optional Mac Retina Release (AMD)',
1114 'Optional Mac Retina Release (NVIDIA)',
Yuly Novikovbc1ccff2019-08-03 00:05:491115 'Optional Win10 x64 Release (Intel HD 630)',
1116 'Optional Win10 x64 Release (NVIDIA)',
Kenneth Russell8a386d42018-06-02 09:48:011117 'Win7 ANGLE Tryserver (AMD)',
Nico Weber7fc8b9da2018-06-08 19:22:081118 # chromium.fyi
Dirk Pranke85369442018-06-16 02:01:291119 'linux-blink-rel-dummy',
1120 'mac10.10-blink-rel-dummy',
1121 'mac10.11-blink-rel-dummy',
1122 'mac10.12-blink-rel-dummy',
Kenneth Russell911da0d32018-07-17 21:39:201123 'mac10.13_retina-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291124 'mac10.13-blink-rel-dummy',
John Chenad978322019-12-16 18:07:211125 'mac10.14-blink-rel-dummy',
Dirk Pranke85369442018-06-16 02:01:291126 'win7-blink-rel-dummy',
1127 'win10-blink-rel-dummy',
Nico Weber7fc8b9da2018-06-08 19:22:081128 'Dummy WebKit Mac10.13',
Philip Rogers639990262018-12-08 00:13:331129 'WebKit Linux composite_after_paint Dummy Builder',
Scott Violet744e04662019-08-19 23:51:531130 'WebKit Linux layout_ng_disabled Builder',
Stephen Martinis769b25112018-08-30 18:52:061131 # chromium, due to https://crbug.com/878915
1132 'win-dbg',
1133 'win32-dbg',
Stephen Martinis47d77132019-04-24 23:51:331134 'win-archive-dbg',
1135 'win32-archive-dbg',
Kenneth Russell8a386d42018-06-02 09:48:011136 ]
1137
Ben Pastene9a010082019-09-25 20:41:371138 def get_internal_waterfalls(self):
1139 # Similar to get_builders_that_do_not_actually_exist above, but for
1140 # waterfalls defined in internal configs.
1141 return ['chrome']
1142
Stephen Martinisf83893722018-09-19 00:02:181143 def check_input_file_consistency(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201144 self.check_input_files_sorting(verbose)
1145
Kenneth Russelleb60cbd22017-12-05 07:54:281146 self.load_configuration_files()
Jeff Yoon8154e582019-12-03 23:30:011147 self.check_composition_type_test_suites('compound_suites')
1148 self.check_composition_type_test_suites('matrix_compound_suites')
Nodir Turakulovfce34292019-12-18 17:05:411149 self.resolve_full_test_targets()
Stephen Martinis54d64ad2018-09-21 22:16:201150 self.flatten_test_suites()
Nico Weberd18b8962018-05-16 19:39:381151
1152 # All bots should exist.
1153 bot_names = self.get_valid_bot_names()
Ben Pastene9a010082019-09-25 20:41:371154 internal_waterfalls = self.get_internal_waterfalls()
1155 builders_that_dont_exist = self.get_builders_that_do_not_actually_exist()
Nico Weberd18b8962018-05-16 19:39:381156 for waterfall in self.waterfalls:
Ben Pastene9a010082019-09-25 20:41:371157 # TODO(crbug.com/991417): Remove the need for this exception.
1158 if waterfall['name'] in internal_waterfalls:
1159 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381160 for bot_name in waterfall['machines']:
Ben Pastene9a010082019-09-25 20:41:371161 if bot_name in builders_that_dont_exist:
Kenneth Russell8a386d42018-06-02 09:48:011162 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381163 if bot_name not in bot_names:
Nico Weber7fc8b9da2018-06-08 19:22:081164 if waterfall['name'] in ['client.v8.chromium', 'client.v8.fyi']:
Nico Weberd18b8962018-05-16 19:39:381165 # TODO(thakis): Remove this once these bots move to luci.
Kenneth Russell78fd8702018-05-17 01:15:521166 continue # pragma: no cover
Patrik Höglunda1e04892018-09-12 12:49:321167 if waterfall['name'] in ['tryserver.webrtc',
1168 'webrtc.chromium.fyi.experimental']:
Nico Weberd18b8962018-05-16 19:39:381169 # These waterfalls have their bot configs in a different repo.
1170 # so we don't know about their bot names.
Kenneth Russell78fd8702018-05-17 01:15:521171 continue # pragma: no cover
Jeff Yoon8154e582019-12-03 23:30:011172 if waterfall['name'] in ['client.devtools-frontend.integration',
1173 'tryserver.devtools-frontend']:
Tamer Tas2c506412019-08-20 07:44:411174 continue # pragma: no cover
Nico Weberd18b8962018-05-16 19:39:381175 raise self.unknown_bot(bot_name, waterfall['name'])
1176
Kenneth Russelleb60cbd22017-12-05 07:54:281177 # All test suites must be referenced.
1178 suites_seen = set()
1179 generator_map = self.get_test_generator_map()
1180 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431181 for bot_name, tester in waterfall['machines'].iteritems():
1182 for suite_type, suite in tester.get('test_suites', {}).iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281183 if suite_type not in generator_map:
1184 raise self.unknown_test_suite_type(suite_type, bot_name,
1185 waterfall['name'])
1186 if suite not in self.test_suites:
1187 raise self.unknown_test_suite(suite, bot_name, waterfall['name'])
1188 suites_seen.add(suite)
1189 # Since we didn't resolve the configuration files, this set
1190 # includes both composition test suites and regular ones.
1191 resolved_suites = set()
1192 for suite_name in suites_seen:
1193 suite = self.test_suites[suite_name]
Jeff Yoon8154e582019-12-03 23:30:011194 for sub_suite in suite:
1195 resolved_suites.add(sub_suite)
Kenneth Russelleb60cbd22017-12-05 07:54:281196 resolved_suites.add(suite_name)
1197 # At this point, every key in test_suites.pyl should be referenced.
1198 missing_suites = set(self.test_suites.keys()) - resolved_suites
1199 if missing_suites:
1200 raise BBGenErr('The following test suites were unreferenced by bots on '
1201 'the waterfalls: ' + str(missing_suites))
1202
1203 # All test suite exceptions must refer to bots on the waterfall.
1204 all_bots = set()
1205 missing_bots = set()
1206 for waterfall in self.waterfalls:
Kenneth Russell139f8642017-12-05 08:51:431207 for bot_name, tester in waterfall['machines'].iteritems():
Kenneth Russelleb60cbd22017-12-05 07:54:281208 all_bots.add(bot_name)
Kenneth Russell8ceeabf2017-12-11 17:53:281209 # In order to disambiguate between bots with the same name on
1210 # different waterfalls, support has been added to various
1211 # exceptions for concatenating the waterfall name after the bot
1212 # name.
1213 all_bots.add(bot_name + ' ' + waterfall['name'])
Kenneth Russelleb60cbd22017-12-05 07:54:281214 for exception in self.exceptions.itervalues():
Nico Weberd18b8962018-05-16 19:39:381215 removals = (exception.get('remove_from', []) +
1216 exception.get('remove_gtest_from', []) +
1217 exception.get('modifications', {}).keys())
1218 for removal in removals:
Kenneth Russelleb60cbd22017-12-05 07:54:281219 if removal not in all_bots:
1220 missing_bots.add(removal)
Stephen Martiniscc70c962018-07-31 21:22:411221
Ben Pastene9a010082019-09-25 20:41:371222 missing_bots = missing_bots - set(builders_that_dont_exist)
Kenneth Russelleb60cbd22017-12-05 07:54:281223 if missing_bots:
1224 raise BBGenErr('The following nonexistent machines were referenced in '
1225 'the test suite exceptions: ' + str(missing_bots))
1226
Stephen Martinis0382bc12018-09-17 22:29:071227 # All mixins must be referenced
1228 seen_mixins = set()
1229 for waterfall in self.waterfalls:
Stephen Martinisb72f6d22018-10-04 23:29:011230 seen_mixins = seen_mixins.union(waterfall.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071231 for bot_name, tester in waterfall['machines'].iteritems():
Stephen Martinisb72f6d22018-10-04 23:29:011232 seen_mixins = seen_mixins.union(tester.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071233 for suite in self.test_suites.values():
1234 if isinstance(suite, list):
1235 # Don't care about this, it's a composition, which shouldn't include a
1236 # swarming mixin.
1237 continue
1238
1239 for test in suite.values():
1240 if not isinstance(test, dict):
1241 # Some test suites have top level keys, which currently can't be
1242 # swarming mixin entries. Ignore them
1243 continue
1244
Stephen Martinisb72f6d22018-10-04 23:29:011245 seen_mixins = seen_mixins.union(test.get('mixins', set()))
Stephen Martinis0382bc12018-09-17 22:29:071246
Stephen Martinisb72f6d22018-10-04 23:29:011247 missing_mixins = set(self.mixins.keys()) - seen_mixins
Stephen Martinis0382bc12018-09-17 22:29:071248 if missing_mixins:
1249 raise BBGenErr('The following mixins are unreferenced: %s. They must be'
1250 ' referenced in a waterfall, machine, or test suite.' % (
1251 str(missing_mixins)))
1252
Stephen Martinis54d64ad2018-09-21 22:16:201253
1254 def type_assert(self, node, typ, filename, verbose=False):
1255 """Asserts that the Python AST node |node| is of type |typ|.
1256
1257 If verbose is set, it prints out some helpful context lines, showing where
1258 exactly the error occurred in the file.
1259 """
1260 if not isinstance(node, typ):
1261 if verbose:
1262 lines = [""] + self.read_file(filename).splitlines()
1263
1264 context = 2
1265 lines_start = max(node.lineno - context, 0)
1266 # Add one to include the last line
1267 lines_end = min(node.lineno + context, len(lines)) + 1
1268 lines = (
1269 ['== %s ==\n' % filename] +
1270 ["<snip>\n"] +
1271 ['%d %s' % (lines_start + i, line) for i, line in enumerate(
1272 lines[lines_start:lines_start + context])] +
1273 ['-' * 80 + '\n'] +
1274 ['%d %s' % (node.lineno, lines[node.lineno])] +
1275 ['-' * (node.col_offset + 3) + '^' + '-' * (
1276 80 - node.col_offset - 4) + '\n'] +
1277 ['%d %s' % (node.lineno + 1 + i, line) for i, line in enumerate(
1278 lines[node.lineno + 1:lines_end])] +
1279 ["<snip>\n"]
1280 )
1281 # Print out a useful message when a type assertion fails.
1282 for l in lines:
1283 self.print_line(l.strip())
1284
1285 node_dumped = ast.dump(node, annotate_fields=False)
1286 # If the node is huge, truncate it so everything fits in a terminal
1287 # window.
1288 if len(node_dumped) > 60: # pragma: no cover
1289 node_dumped = node_dumped[:30] + ' <SNIP> ' + node_dumped[-30:]
1290 raise BBGenErr(
1291 'Invalid .pyl file %r. Python AST node %r on line %s expected to'
1292 ' be %s, is %s' % (
1293 filename, node_dumped,
1294 node.lineno, typ, type(node)))
1295
1296 def ensure_ast_dict_keys_sorted(self, node, filename, verbose):
1297 is_valid = True
1298
1299 keys = []
1300 # The keys of this dict are ordered as ordered in the file; normal python
1301 # dictionary keys are given an arbitrary order, but since we parsed the
1302 # file itself, the order as given in the file is preserved.
1303 for key in node.keys:
1304 self.type_assert(key, ast.Str, filename, verbose)
1305 keys.append(key.s)
1306
1307 keys_sorted = sorted(keys)
1308 if keys_sorted != keys:
1309 is_valid = False
1310 if verbose:
1311 for line in difflib.unified_diff(
1312 keys,
1313 keys_sorted, fromfile='current (%r)' % filename, tofile='sorted'):
1314 self.print_line(line)
1315
1316 if len(set(keys)) != len(keys):
1317 for i in range(len(keys_sorted)-1):
1318 if keys_sorted[i] == keys_sorted[i+1]:
1319 self.print_line('Key %s is duplicated' % keys_sorted[i])
1320 is_valid = False
1321 return is_valid
Stephen Martinisf83893722018-09-19 00:02:181322
1323 def check_input_files_sorting(self, verbose=False):
Stephen Martinis54d64ad2018-09-21 22:16:201324 # TODO(https://crbug.com/886993): Add the ability for this script to
1325 # actually format the files, rather than just complain if they're
1326 # incorrectly formatted.
1327 bad_files = set()
1328
1329 for filename in (
Stephen Martinisb72f6d22018-10-04 23:29:011330 'mixins.pyl',
Stephen Martinis54d64ad2018-09-21 22:16:201331 'test_suites.pyl',
1332 'test_suite_exceptions.pyl',
1333 ):
Stephen Martinisf83893722018-09-19 00:02:181334 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1335
Stephen Martinisf83893722018-09-19 00:02:181336 # Must be a module.
Stephen Martinis54d64ad2018-09-21 22:16:201337 self.type_assert(parsed, ast.Module, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181338 module = parsed.body
1339
1340 # Only one expression in the module.
Stephen Martinis54d64ad2018-09-21 22:16:201341 self.type_assert(module, list, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181342 if len(module) != 1: # pragma: no cover
1343 raise BBGenErr('Invalid .pyl file %s' % filename)
1344 expr = module[0]
Stephen Martinis54d64ad2018-09-21 22:16:201345 self.type_assert(expr, ast.Expr, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181346
1347 # Value should be a dictionary.
1348 value = expr.value
Stephen Martinis54d64ad2018-09-21 22:16:201349 self.type_assert(value, ast.Dict, filename, verbose)
Stephen Martinisf83893722018-09-19 00:02:181350
Stephen Martinis54d64ad2018-09-21 22:16:201351 if filename == 'test_suites.pyl':
Jeff Yoon8154e582019-12-03 23:30:011352 expected_keys = ['basic_suites',
1353 'compound_suites',
1354 'matrix_compound_suites']
Stephen Martinis54d64ad2018-09-21 22:16:201355 actual_keys = [node.s for node in value.keys]
1356 assert all(key in expected_keys for key in actual_keys), (
1357 'Invalid %r file; expected keys %r, got %r' % (
1358 filename, expected_keys, actual_keys))
1359 suite_dicts = [node for node in value.values]
1360 # Only two keys should mean only 1 or 2 values
Jeff Yoon8154e582019-12-03 23:30:011361 assert len(suite_dicts) <= 3
Stephen Martinis54d64ad2018-09-21 22:16:201362 for suite_group in suite_dicts:
1363 if not self.ensure_ast_dict_keys_sorted(
1364 suite_group, filename, verbose):
1365 bad_files.add(filename)
Stephen Martinisf83893722018-09-19 00:02:181366
Stephen Martinis54d64ad2018-09-21 22:16:201367 else:
1368 if not self.ensure_ast_dict_keys_sorted(
1369 value, filename, verbose):
1370 bad_files.add(filename)
1371
1372 # waterfalls.pyl is slightly different, just do it manually here
1373 filename = 'waterfalls.pyl'
1374 parsed = ast.parse(self.read_file(self.pyl_file_path(filename)))
1375
1376 # Must be a module.
1377 self.type_assert(parsed, ast.Module, filename, verbose)
1378 module = parsed.body
1379
1380 # Only one expression in the module.
1381 self.type_assert(module, list, filename, verbose)
1382 if len(module) != 1: # pragma: no cover
1383 raise BBGenErr('Invalid .pyl file %s' % filename)
1384 expr = module[0]
1385 self.type_assert(expr, ast.Expr, filename, verbose)
1386
1387 # Value should be a list.
1388 value = expr.value
1389 self.type_assert(value, ast.List, filename, verbose)
1390
1391 keys = []
1392 for val in value.elts:
1393 self.type_assert(val, ast.Dict, filename, verbose)
1394 waterfall_name = None
1395 for key, val in zip(val.keys, val.values):
1396 self.type_assert(key, ast.Str, filename, verbose)
1397 if key.s == 'machines':
1398 if not self.ensure_ast_dict_keys_sorted(val, filename, verbose):
1399 bad_files.add(filename)
1400
1401 if key.s == "name":
1402 self.type_assert(val, ast.Str, filename, verbose)
1403 waterfall_name = val.s
1404 assert waterfall_name
1405 keys.append(waterfall_name)
1406
1407 if sorted(keys) != keys:
1408 bad_files.add(filename)
1409 if verbose: # pragma: no cover
1410 for line in difflib.unified_diff(
1411 keys,
1412 sorted(keys), fromfile='current', tofile='sorted'):
1413 self.print_line(line)
Stephen Martinisf83893722018-09-19 00:02:181414
1415 if bad_files:
1416 raise BBGenErr(
Stephen Martinis54d64ad2018-09-21 22:16:201417 'The following files have invalid keys: %s\n. They are either '
1418 'unsorted, or have duplicates.' % ', '.join(bad_files))
Stephen Martinisf83893722018-09-19 00:02:181419
Kenneth Russelleb60cbd22017-12-05 07:54:281420 def check_output_file_consistency(self, verbose=False):
1421 self.load_configuration_files()
1422 # All waterfalls must have been written by this script already.
1423 self.resolve_configuration_files()
1424 ungenerated_waterfalls = set()
1425 for waterfall in self.waterfalls:
1426 expected = self.generate_waterfall_json(waterfall)
Zhiling Huangbe008172018-03-08 19:13:111427 file_path = waterfall['name'] + '.json'
1428 current = self.read_file(self.pyl_file_path(file_path))
Kenneth Russelleb60cbd22017-12-05 07:54:281429 if expected != current:
1430 ungenerated_waterfalls.add(waterfall['name'])
John Budorick826d5ed2017-12-28 19:27:321431 if verbose: # pragma: no cover
Stephen Martinis7eb8b612018-09-21 00:17:501432 self.print_line('Waterfall ' + waterfall['name'] +
Kenneth Russelleb60cbd22017-12-05 07:54:281433 ' did not have the following expected '
John Budorick826d5ed2017-12-28 19:27:321434 'contents:')
1435 for line in difflib.unified_diff(
1436 expected.splitlines(),
Stephen Martinis7eb8b612018-09-21 00:17:501437 current.splitlines(),
1438 fromfile='expected', tofile='current'):
1439 self.print_line(line)
Kenneth Russelleb60cbd22017-12-05 07:54:281440 if ungenerated_waterfalls:
1441 raise BBGenErr('The following waterfalls have not been properly '
1442 'autogenerated by generate_buildbot_json.py: ' +
1443 str(ungenerated_waterfalls))
1444
1445 def check_consistency(self, verbose=False):
Stephen Martinis7eb8b612018-09-21 00:17:501446 self.check_input_file_consistency(verbose) # pragma: no cover
Kenneth Russelleb60cbd22017-12-05 07:54:281447 self.check_output_file_consistency(verbose) # pragma: no cover
1448
1449 def parse_args(self, argv): # pragma: no cover
Karen Qiane24b7ee2019-02-12 23:37:061450
1451 # RawTextHelpFormatter allows for styling of help statement
1452 parser = argparse.ArgumentParser(formatter_class=
1453 argparse.RawTextHelpFormatter)
1454
1455 group = parser.add_mutually_exclusive_group()
1456 group.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281457 '-c', '--check', action='store_true', help=
1458 'Do consistency checks of configuration and generated files and then '
1459 'exit. Used during presubmit. Causes the tool to not generate any files.')
Karen Qiane24b7ee2019-02-12 23:37:061460 group.add_argument(
1461 '--query', type=str, help=
1462 ("Returns raw JSON information of buildbots and tests.\n" +
1463 "Examples:\n" +
1464 " List all bots (all info):\n" +
1465 " --query bots\n\n" +
1466 " List all bots and only their associated tests:\n" +
1467 " --query bots/tests\n\n" +
1468 " List all information about 'bot1' " +
1469 "(make sure you have quotes):\n" +
1470 " --query bot/'bot1'\n\n" +
1471 " List tests running for 'bot1' (make sure you have quotes):\n" +
1472 " --query bot/'bot1'/tests\n\n" +
1473 " List all tests:\n" +
1474 " --query tests\n\n" +
1475 " List all tests and the bots running them:\n" +
1476 " --query tests/bots\n\n"+
1477 " List all tests that satisfy multiple parameters\n" +
1478 " (separation of parameters by '&' symbol):\n" +
1479 " --query tests/'device_os:Android&device_type:hammerhead'\n\n" +
1480 " List all tests that run with a specific flag:\n" +
1481 " --query bots/'--test-launcher-print-test-studio=always'\n\n" +
1482 " List specific test (make sure you have quotes):\n"
1483 " --query test/'test1'\n\n"
1484 " List all bots running 'test1' " +
1485 "(make sure you have quotes):\n" +
1486 " --query test/'test1'/bots" ))
Kenneth Russelleb60cbd22017-12-05 07:54:281487 parser.add_argument(
1488 '-n', '--new-files', action='store_true', help=
1489 'Write output files as .new.json. Useful during development so old and '
1490 'new files can be looked at side-by-side.')
1491 parser.add_argument(
Stephen Martinis7eb8b612018-09-21 00:17:501492 '-v', '--verbose', action='store_true', help=
1493 'Increases verbosity. Affects consistency checks.')
1494 parser.add_argument(
Kenneth Russelleb60cbd22017-12-05 07:54:281495 'waterfall_filters', metavar='waterfalls', type=str, nargs='*',
1496 help='Optional list of waterfalls to generate.')
Zhiling Huangbe008172018-03-08 19:13:111497 parser.add_argument(
1498 '--pyl-files-dir', type=os.path.realpath,
1499 help='Path to the directory containing the input .pyl files.')
Karen Qiane24b7ee2019-02-12 23:37:061500 parser.add_argument(
1501 '--json', help=
1502 ("Outputs results into a json file. Only works with query function.\n" +
1503 "Examples:\n" +
1504 " Outputs file into specified json file: \n" +
1505 " --json <file-name-here.json>"))
Kenneth Russelleb60cbd22017-12-05 07:54:281506 self.args = parser.parse_args(argv)
Karen Qiane24b7ee2019-02-12 23:37:061507 if self.args.json and not self.args.query:
1508 parser.error("The --json flag can only be used with --query.")
1509
1510 def does_test_match(self, test_info, params_dict):
1511 """Checks to see if the test matches the parameters given.
1512
1513 Compares the provided test_info with the params_dict to see
1514 if the bot matches the parameters given. If so, returns True.
1515 Else, returns false.
1516
1517 Args:
1518 test_info (dict): Information about a specific bot provided
1519 in the format shown in waterfalls.pyl
1520 params_dict (dict): Dictionary of parameters and their values
1521 to look for in the bot
1522 Ex: {
1523 'device_os':'android',
1524 '--flag':True,
1525 'mixins': ['mixin1', 'mixin2'],
1526 'ex_key':'ex_value'
1527 }
1528
1529 """
1530 DIMENSION_PARAMS = ['device_os', 'device_type', 'os',
1531 'kvm', 'pool', 'integrity'] # dimension parameters
1532 SWARMING_PARAMS = ['shards', 'hard_timeout', 'idempotent',
1533 'can_use_on_swarming_builders']
1534 for param in params_dict:
1535 # if dimension parameter
1536 if param in DIMENSION_PARAMS or param in SWARMING_PARAMS:
1537 if not 'swarming' in test_info:
1538 return False
1539 swarming = test_info['swarming']
1540 if param in SWARMING_PARAMS:
1541 if not param in swarming:
1542 return False
1543 if not str(swarming[param]) == params_dict[param]:
1544 return False
1545 else:
1546 if not 'dimension_sets' in swarming:
1547 return False
1548 d_set = swarming['dimension_sets']
1549 # only looking at the first dimension set
1550 if not param in d_set[0]:
1551 return False
1552 if not d_set[0][param] == params_dict[param]:
1553 return False
1554
1555 # if flag
1556 elif param.startswith('--'):
1557 if not 'args' in test_info:
1558 return False
1559 if not param in test_info['args']:
1560 return False
1561
1562 # not dimension parameter/flag/mixin
1563 else:
1564 if not param in test_info:
1565 return False
1566 if not test_info[param] == params_dict[param]:
1567 return False
1568 return True
1569 def error_msg(self, msg):
1570 """Prints an error message.
1571
1572 In addition to a catered error message, also prints
1573 out where the user can find more help. Then, program exits.
1574 """
1575 self.print_line(msg + (' If you need more information, ' +
1576 'please run with -h or --help to see valid commands.'))
1577 sys.exit(1)
1578
1579 def find_bots_that_run_test(self, test, bots):
1580 matching_bots = []
1581 for bot in bots:
1582 bot_info = bots[bot]
1583 tests = self.flatten_tests_for_bot(bot_info)
1584 for test_info in tests:
1585 test_name = ""
1586 if 'name' in test_info:
1587 test_name = test_info['name']
1588 elif 'test' in test_info:
1589 test_name = test_info['test']
1590 if not test_name == test:
1591 continue
1592 matching_bots.append(bot)
1593 return matching_bots
1594
1595 def find_tests_with_params(self, tests, params_dict):
1596 matching_tests = []
1597 for test_name in tests:
1598 test_info = tests[test_name]
1599 if not self.does_test_match(test_info, params_dict):
1600 continue
1601 if not test_name in matching_tests:
1602 matching_tests.append(test_name)
1603 return matching_tests
1604
1605 def flatten_waterfalls_for_query(self, waterfalls):
1606 bots = {}
1607 for waterfall in waterfalls:
1608 waterfall_json = json.loads(self.generate_waterfall_json(waterfall))
1609 for bot in waterfall_json:
1610 bot_info = waterfall_json[bot]
1611 if 'AAAAA' not in bot:
1612 bots[bot] = bot_info
1613 return bots
1614
1615 def flatten_tests_for_bot(self, bot_info):
1616 """Returns a list of flattened tests.
1617
1618 Returns a list of tests not grouped by test category
1619 for a specific bot.
1620 """
1621 TEST_CATS = self.get_test_generator_map().keys()
1622 tests = []
1623 for test_cat in TEST_CATS:
1624 if not test_cat in bot_info:
1625 continue
1626 test_cat_tests = bot_info[test_cat]
1627 tests = tests + test_cat_tests
1628 return tests
1629
1630 def flatten_tests_for_query(self, test_suites):
1631 """Returns a flattened dictionary of tests.
1632
1633 Returns a dictionary of tests associate with their
1634 configuration, not grouped by their test suite.
1635 """
1636 tests = {}
1637 for test_suite in test_suites.itervalues():
1638 for test in test_suite:
1639 test_info = test_suite[test]
1640 test_name = test
1641 if 'name' in test_info:
1642 test_name = test_info['name']
1643 tests[test_name] = test_info
1644 return tests
1645
1646 def parse_query_filter_params(self, params):
1647 """Parses the filter parameters.
1648
1649 Creates a dictionary from the parameters provided
1650 to filter the bot array.
1651 """
1652 params_dict = {}
1653 for p in params:
1654 # flag
1655 if p.startswith("--"):
1656 params_dict[p] = True
1657 else:
1658 pair = p.split(":")
1659 if len(pair) != 2:
1660 self.error_msg('Invalid command.')
1661 # regular parameters
1662 if pair[1].lower() == "true":
1663 params_dict[pair[0]] = True
1664 elif pair[1].lower() == "false":
1665 params_dict[pair[0]] = False
1666 else:
1667 params_dict[pair[0]] = pair[1]
1668 return params_dict
1669
1670 def get_test_suites_dict(self, bots):
1671 """Returns a dictionary of bots and their tests.
1672
1673 Returns a dictionary of bots and a list of their associated tests.
1674 """
1675 test_suite_dict = dict()
1676 for bot in bots:
1677 bot_info = bots[bot]
1678 tests = self.flatten_tests_for_bot(bot_info)
1679 test_suite_dict[bot] = tests
1680 return test_suite_dict
1681
1682 def output_query_result(self, result, json_file=None):
1683 """Outputs the result of the query.
1684
1685 If a json file parameter name is provided, then
1686 the result is output into the json file. If not,
1687 then the result is printed to the console.
1688 """
1689 output = json.dumps(result, indent=2)
1690 if json_file:
1691 self.write_file(json_file, output)
1692 else:
1693 self.print_line(output)
1694 return
1695
1696 def query(self, args):
1697 """Queries tests or bots.
1698
1699 Depending on the arguments provided, outputs a json of
1700 tests or bots matching the appropriate optional parameters provided.
1701 """
1702 # split up query statement
1703 query = args.query.split('/')
1704 self.load_configuration_files()
1705 self.resolve_configuration_files()
1706
1707 # flatten bots json
1708 tests = self.test_suites
1709 bots = self.flatten_waterfalls_for_query(self.waterfalls)
1710
1711 cmd_class = query[0]
1712
1713 # For queries starting with 'bots'
1714 if cmd_class == "bots":
1715 if len(query) == 1:
1716 return self.output_query_result(bots, args.json)
1717 # query with specific parameters
1718 elif len(query) == 2:
1719 if query[1] == 'tests':
1720 test_suites_dict = self.get_test_suites_dict(bots)
1721 return self.output_query_result(test_suites_dict, args.json)
1722 else:
1723 self.error_msg("This query should be in the format: bots/tests.")
1724
1725 else:
1726 self.error_msg("This query should have 0 or 1 '/', found %s instead."
1727 % str(len(query)-1))
1728
1729 # For queries starting with 'bot'
1730 elif cmd_class == "bot":
1731 if not len(query) == 2 and not len(query) == 3:
1732 self.error_msg("Command should have 1 or 2 '/', found %s instead."
1733 % str(len(query)-1))
1734 bot_id = query[1]
1735 if not bot_id in bots:
1736 self.error_msg("No bot named '" + bot_id + "' found.")
1737 bot_info = bots[bot_id]
1738 if len(query) == 2:
1739 return self.output_query_result(bot_info, args.json)
1740 if not query[2] == 'tests':
1741 self.error_msg("The query should be in the format:" +
1742 "bot/<bot-name>/tests.")
1743
1744 bot_tests = self.flatten_tests_for_bot(bot_info)
1745 return self.output_query_result(bot_tests, args.json)
1746
1747 # For queries starting with 'tests'
1748 elif cmd_class == "tests":
1749 if not len(query) == 1 and not len(query) == 2:
1750 self.error_msg("The query should have 0 or 1 '/', found %s instead."
1751 % str(len(query)-1))
1752 flattened_tests = self.flatten_tests_for_query(tests)
1753 if len(query) == 1:
1754 return self.output_query_result(flattened_tests, args.json)
1755
1756 # create params dict
1757 params = query[1].split('&')
1758 params_dict = self.parse_query_filter_params(params)
1759 matching_bots = self.find_tests_with_params(flattened_tests, params_dict)
1760 return self.output_query_result(matching_bots)
1761
1762 # For queries starting with 'test'
1763 elif cmd_class == "test":
1764 if not len(query) == 2 and not len(query) == 3:
1765 self.error_msg("The query should have 1 or 2 '/', found %s instead."
1766 % str(len(query)-1))
1767 test_id = query[1]
1768 if len(query) == 2:
1769 flattened_tests = self.flatten_tests_for_query(tests)
1770 for test in flattened_tests:
1771 if test == test_id:
1772 return self.output_query_result(flattened_tests[test], args.json)
1773 self.error_msg("There is no test named %s." % test_id)
1774 if not query[2] == 'bots':
1775 self.error_msg("The query should be in the format: " +
1776 "test/<test-name>/bots")
1777 bots_for_test = self.find_bots_that_run_test(test_id, bots)
1778 return self.output_query_result(bots_for_test)
1779
1780 else:
1781 self.error_msg("Your command did not match any valid commands." +
1782 "Try starting with 'bots', 'bot', 'tests', or 'test'.")
Kenneth Russelleb60cbd22017-12-05 07:54:281783
1784 def main(self, argv): # pragma: no cover
1785 self.parse_args(argv)
1786 if self.args.check:
Stephen Martinis7eb8b612018-09-21 00:17:501787 self.check_consistency(verbose=self.args.verbose)
Karen Qiane24b7ee2019-02-12 23:37:061788 elif self.args.query:
1789 self.query(self.args)
Kenneth Russelleb60cbd22017-12-05 07:54:281790 else:
1791 self.generate_waterfalls()
1792 return 0
1793
1794if __name__ == "__main__": # pragma: no cover
1795 generator = BBJSONGenerator()
John Budorick699282e2019-02-13 01:27:331796 sys.exit(generator.main(sys.argv[1:]))