2 # Copyright 2015 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.
6 """Toolbox to manage all the json files in this directory.
8 It can reformat them in their canonical format or ensures they are well
22 THIS_DIR
= os
.path
.dirname(os
.path
.abspath(__file__
))
23 SRC_DIR
= os
.path
.dirname(os
.path
.dirname(THIS_DIR
))
24 sys
.path
.insert(0, os
.path
.join(SRC_DIR
, 'third_party', 'colorama', 'src'))
30 # These are not 'builders'.
31 'compile_targets', 'gtest_tests', 'filter_compile_builders',
32 'non_filter_builders', 'non_filter_tests_builders',
34 # These are not supported on Swarming yet.
35 # http://crbug.com/472205
37 # http://crbug.com/441429
38 'Linux Trusty (32)', 'Linux Trusty (dbg)(32)',
40 # http://crbug.com/480053
44 # Unmaintained builders on chromium.fyi
48 # This builder is fine, but win8_chromium_ng uses GN and this configuration,
49 # which breaks everything.
52 # One off builders. Note that Swarming does support ARM.
53 'Linux ARM Cross-Compile',
54 'Site Isolation Linux',
59 SKIP_GN_ISOLATE_MAP_TARGETS
= {
60 # TODO(GYP): These targets have not been ported to GN yet.
61 'cast_media_unittests',
62 'cast_shell_browser_test',
64 'nacl_helper_nonsfi_unittests',
66 # These targets are run on the bots but not listed in the
67 # buildbot JSON files.
68 'angle_end2end_tests',
72 'tab_capture_end2end_tests',
77 class Error(Exception):
78 """Processing error."""
82 """Returns the list of all isolate files."""
83 files
= subprocess
.check_output(['git', 'ls-files'], cwd
=SRC_DIR
).splitlines()
84 return [os
.path
.basename(f
) for f
in files
if f
.endswith('.isolate')]
87 def process_builder_convert(data
, test_name
):
88 """Converts 'test_name' to run on Swarming in 'data'.
90 Returns True if 'test_name' was found.
93 for test
in data
['gtest_tests']:
94 if test
['test'] != test_name
:
96 test
.setdefault('swarming', {})
97 if not test
['swarming'].get('can_use_on_swarming_builders'):
98 test
['swarming']['can_use_on_swarming_builders'] = True
103 def process_builder_remaining(data
, filename
, builder
, tests_location
):
104 """Calculates tests_location when mode is --remaining."""
105 for test
in data
['gtest_tests']:
107 if test
.get('swarming', {}).get('can_use_on_swarming_builders'):
108 tests_location
[name
]['count_run_on_swarming'] += 1
110 tests_location
[name
]['count_run_local'] += 1
111 tests_location
[name
]['local_configs'].setdefault(
112 filename
, []).append(builder
)
115 def process_file(mode
, test_name
, tests_location
, filepath
, ninja_targets
,
117 """Processes a json file describing what tests should be run for each recipe.
119 The action depends on mode. Updates tests_location.
121 Return False if the process exit code should be 1.
123 filename
= os
.path
.basename(filepath
)
124 with
open(filepath
) as f
:
127 config
= json
.loads(content
)
128 except ValueError as e
:
129 raise Error('Exception raised while checking %s: %s' % (filepath
, e
))
131 for builder
, data
in sorted(config
.iteritems()):
135 if not isinstance(data
, dict):
136 raise Error('%s: %s is broken: %s' % (filename
, builder
, data
))
137 if 'gtest_tests' not in data
:
139 if not isinstance(data
['gtest_tests'], list):
141 '%s: %s is broken: %s' % (filename
, builder
, data
['gtest_tests']))
142 if not all(isinstance(g
, dict) for g
in data
['gtest_tests']):
144 '%s: %s is broken: %s' % (filename
, builder
, data
['gtest_tests']))
146 for d
in data
['gtest_tests']:
147 if (d
['test'] not in ninja_targets
and
148 d
['test'] not in SKIP_GN_ISOLATE_MAP_TARGETS
):
149 raise Error('%s: %s / %s is not listed in gn_isolate_map.pyl.' %
150 (filename
, builder
, d
['test']))
151 elif d
['test'] in ninja_targets
:
152 ninja_targets_seen
.add(d
['test'])
154 config
[builder
]['gtest_tests'] = sorted(
155 data
['gtest_tests'], key
=lambda x
: x
['test'])
157 # The trick here is that process_builder_remaining() is called before
158 # process_builder_convert() so tests_location can be used to know how many
159 # tests were converted.
160 if mode
in ('convert', 'remaining'):
161 process_builder_remaining(data
, filename
, builder
, tests_location
)
162 if mode
== 'convert':
163 process_builder_convert(data
, test_name
)
165 expected
= json
.dumps(
166 config
, sort_keys
=True, indent
=2, separators
=(',', ': ')) + '\n'
167 if content
!= expected
:
168 if mode
in ('convert', 'write'):
169 with
open(filepath
, 'wb') as f
:
172 print('Updated %s' % filename
)
174 print('%s is not in canonical format' % filename
)
175 print('run `testing/buildbot/manage.py -w` to fix')
176 return mode
!= 'check'
180 def print_convert(test_name
, tests_location
):
181 """Prints statistics for a test being converted for use in a CL description.
183 data
= tests_location
[test_name
]
184 print('Convert %s to run exclusively on Swarming' % test_name
)
186 print('%d configs already ran on Swarming' % data
['count_run_on_swarming'])
187 print('%d used to run locally and were converted:' % data
['count_run_local'])
188 for master
, builders
in sorted(data
['local_configs'].iteritems()):
189 for builder
in builders
:
190 print('- %s: %s' % (master
, builder
))
193 print(' ./manage.py --convert %s' % test_name
)
199 def print_remaining(test_name
, tests_location
):
200 """Prints a visual summary of what tests are yet to be converted to run on
204 if test_name
not in tests_location
:
205 raise Error('Unknown test %s' % test_name
)
206 for config
, builders
in sorted(
207 tests_location
[test_name
]['local_configs'].iteritems()):
208 print('%s:' % config
)
209 for builder
in sorted(builders
):
210 print(' %s' % builder
)
213 isolates
= get_isolates()
214 l
= max(map(len, tests_location
))
215 print('%-*s%sLocal %sSwarming %sMissing isolate' %
216 (l
, 'Test', colorama
.Fore
.RED
, colorama
.Fore
.GREEN
,
217 colorama
.Fore
.MAGENTA
))
220 for name
, location
in sorted(tests_location
.iteritems()):
221 if not location
['count_run_on_swarming']:
222 c
= colorama
.Fore
.RED
223 elif location
['count_run_local']:
224 c
= colorama
.Fore
.YELLOW
226 c
= colorama
.Fore
.GREEN
227 total_local
+= location
['count_run_local']
228 total_swarming
+= location
['count_run_on_swarming']
230 if name
+ '.isolate' not in isolates
:
231 missing_isolate
= colorama
.Fore
.MAGENTA
+ '*'
232 print('%s%-*s %4d %4d %s' %
233 (c
, l
, name
, location
['count_run_local'],
234 location
['count_run_on_swarming'], missing_isolate
))
236 total
= total_local
+ total_swarming
237 p_local
= 100. * total_local
/ total
238 p_swarming
= 100. * total_swarming
/ total
239 print('%s%-*s %4d (%4.1f%%) %4d (%4.1f%%)' %
240 (colorama
.Fore
.WHITE
, l
, 'Total:', total_local
, p_local
,
241 total_swarming
, p_swarming
))
242 print('%-*s %4d' % (l
, 'Total executions:', total
))
247 parser
= argparse
.ArgumentParser(description
=sys
.modules
[__name__
].__doc
__)
248 group
= parser
.add_mutually_exclusive_group(required
=True)
250 '-c', '--check', dest
='mode', action
='store_const', const
='check',
251 default
='check', help='Only check the files')
253 '--convert', dest
='mode', action
='store_const', const
='convert',
254 help='Convert a test to run on Swarming everywhere')
256 '--remaining', dest
='mode', action
='store_const', const
='remaining',
257 help='Count the number of tests not yet running on Swarming')
259 '-w', '--write', dest
='mode', action
='store_const', const
='write',
260 help='Rewrite the files')
262 'test_name', nargs
='?',
263 help='The test name to print which configs to update; only to be used '
265 args
= parser
.parse_args()
267 if args
.mode
== 'convert':
268 if not args
.test_name
:
269 parser
.error('A test name is required with --convert')
270 if args
.test_name
+ '.isolate' not in get_isolates():
271 parser
.error('Create %s.isolate first' % args
.test_name
)
273 # Stats when running in --remaining mode;
274 tests_location
= collections
.defaultdict(
276 'count_run_local': 0, 'count_run_on_swarming': 0, 'local_configs': {}
279 with
open(os
.path
.join(THIS_DIR
, "gn_isolate_map.pyl")) as fp
:
280 gn_isolate_map
= ast
.literal_eval(fp
.read())
281 ninja_targets
= {k
: v
['label'] for k
, v
in gn_isolate_map
.items()}
285 ninja_targets_seen
= set()
286 for filepath
in glob
.glob(os
.path
.join(THIS_DIR
, '*.json')):
287 if not process_file(args
.mode
, args
.test_name
, tests_location
, filepath
,
288 ninja_targets
, ninja_targets_seen
):
291 extra_targets
= (set(ninja_targets
) - ninja_targets_seen
-
292 SKIP_GN_ISOLATE_MAP_TARGETS
)
294 if len(extra_targets
) > 1:
295 extra_targets_str
= ', '.join(extra_targets
) + ' are'
297 extra_targets_str
= list(extra_targets
)[0] + ' is'
298 raise Error('%s listed in gn_isolate_map.pyl but not in any .json '
299 'files' % extra_targets_str
)
301 if args
.mode
== 'convert':
302 print_convert(args
.test_name
, tests_location
)
303 elif args
.mode
== 'remaining':
304 print_remaining(args
.test_name
, tests_location
)
307 sys
.stderr
.write('%s\n' % e
)
311 if __name__
== "__main__":