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 BLINK_DIR
= os
.path
.join(SRC_DIR
, 'third_party', 'WebKit')
25 sys
.path
.insert(0, os
.path
.join(SRC_DIR
, 'third_party', 'colorama', 'src'))
31 # These are not 'builders'.
32 'compile_targets', 'gtest_tests', 'filter_compile_builders',
33 'non_filter_builders', 'non_filter_tests_builders',
35 # These are not supported on Swarming yet.
36 # http://crbug.com/472205
38 # http://crbug.com/441429
39 'Linux Trusty (32)', 'Linux Trusty (dbg)(32)',
41 # http://crbug.com/480053
45 # Unmaintained builders on chromium.fyi
49 # This builder is fine, but win8_chromium_ng uses GN and this configuration,
50 # which breaks everything.
53 # One off builders. Note that Swarming does support ARM.
54 'Linux ARM Cross-Compile',
55 'Site Isolation Linux',
60 SKIP_GN_ISOLATE_MAP_TARGETS
= {
61 # TODO(GYP): These targets have not been ported to GN yet.
62 'cast_media_unittests',
63 'cast_shell_browser_test',
65 'nacl_helper_nonsfi_unittests',
67 # These targets are run on the bots but not listed in the
68 # buildbot JSON files.
69 'angle_end2end_tests',
73 'tab_capture_end2end_tests',
78 class Error(Exception):
79 """Processing error."""
83 """Returns the list of all isolate files."""
85 def git_ls_files(cwd
):
86 return subprocess
.check_output(['git', 'ls-files'], cwd
=cwd
).splitlines()
88 files
= git_ls_files(SRC_DIR
) + git_ls_files(BLINK_DIR
)
89 return [os
.path
.basename(f
) for f
in files
if f
.endswith('.isolate')]
92 def process_builder_convert(data
, test_name
):
93 """Converts 'test_name' to run on Swarming in 'data'.
95 Returns True if 'test_name' was found.
98 for test
in data
['gtest_tests']:
99 if test
['test'] != test_name
:
101 test
.setdefault('swarming', {})
102 if not test
['swarming'].get('can_use_on_swarming_builders'):
103 test
['swarming']['can_use_on_swarming_builders'] = True
108 def process_builder_remaining(data
, filename
, builder
, tests_location
):
109 """Calculates tests_location when mode is --remaining."""
110 for test
in data
['gtest_tests']:
112 if test
.get('swarming', {}).get('can_use_on_swarming_builders'):
113 tests_location
[name
]['count_run_on_swarming'] += 1
115 tests_location
[name
]['count_run_local'] += 1
116 tests_location
[name
]['local_configs'].setdefault(
117 filename
, []).append(builder
)
120 def process_file(mode
, test_name
, tests_location
, filepath
, ninja_targets
,
122 """Processes a json file describing what tests should be run for each recipe.
124 The action depends on mode. Updates tests_location.
126 Return False if the process exit code should be 1.
128 filename
= os
.path
.basename(filepath
)
129 with
open(filepath
) as f
:
132 config
= json
.loads(content
)
133 except ValueError as e
:
134 raise Error('Exception raised while checking %s: %s' % (filepath
, e
))
136 for builder
, data
in sorted(config
.iteritems()):
140 if not isinstance(data
, dict):
141 raise Error('%s: %s is broken: %s' % (filename
, builder
, data
))
142 if 'gtest_tests' not in data
:
144 if not isinstance(data
['gtest_tests'], list):
146 '%s: %s is broken: %s' % (filename
, builder
, data
['gtest_tests']))
147 if not all(isinstance(g
, dict) for g
in data
['gtest_tests']):
149 '%s: %s is broken: %s' % (filename
, builder
, data
['gtest_tests']))
151 for d
in data
['gtest_tests']:
152 if (d
['test'] not in ninja_targets
and
153 d
['test'] not in SKIP_GN_ISOLATE_MAP_TARGETS
):
154 raise Error('%s: %s / %s is not listed in gn_isolate_map.pyl.' %
155 (filename
, builder
, d
['test']))
156 elif d
['test'] in ninja_targets
:
157 ninja_targets_seen
.add(d
['test'])
159 config
[builder
]['gtest_tests'] = sorted(
160 data
['gtest_tests'], key
=lambda x
: x
['test'])
162 # The trick here is that process_builder_remaining() is called before
163 # process_builder_convert() so tests_location can be used to know how many
164 # tests were converted.
165 if mode
in ('convert', 'remaining'):
166 process_builder_remaining(data
, filename
, builder
, tests_location
)
167 if mode
== 'convert':
168 process_builder_convert(data
, test_name
)
170 expected
= json
.dumps(
171 config
, sort_keys
=True, indent
=2, separators
=(',', ': ')) + '\n'
172 if content
!= expected
:
173 if mode
in ('convert', 'write'):
174 with
open(filepath
, 'wb') as f
:
177 print('Updated %s' % filename
)
179 print('%s is not in canonical format' % filename
)
180 print('run `testing/buildbot/manage.py -w` to fix')
181 return mode
!= 'check'
185 def print_convert(test_name
, tests_location
):
186 """Prints statistics for a test being converted for use in a CL description.
188 data
= tests_location
[test_name
]
189 print('Convert %s to run exclusively on Swarming' % test_name
)
191 print('%d configs already ran on Swarming' % data
['count_run_on_swarming'])
192 print('%d used to run locally and were converted:' % data
['count_run_local'])
193 for master
, builders
in sorted(data
['local_configs'].iteritems()):
194 for builder
in builders
:
195 print('- %s: %s' % (master
, builder
))
198 print(' ./manage.py --convert %s' % test_name
)
204 def print_remaining(test_name
, tests_location
):
205 """Prints a visual summary of what tests are yet to be converted to run on
209 if test_name
not in tests_location
:
210 raise Error('Unknown test %s' % test_name
)
211 for config
, builders
in sorted(
212 tests_location
[test_name
]['local_configs'].iteritems()):
213 print('%s:' % config
)
214 for builder
in sorted(builders
):
215 print(' %s' % builder
)
218 isolates
= get_isolates()
219 l
= max(map(len, tests_location
))
220 print('%-*s%sLocal %sSwarming %sMissing isolate' %
221 (l
, 'Test', colorama
.Fore
.RED
, colorama
.Fore
.GREEN
,
222 colorama
.Fore
.MAGENTA
))
225 for name
, location
in sorted(tests_location
.iteritems()):
226 if not location
['count_run_on_swarming']:
227 c
= colorama
.Fore
.RED
228 elif location
['count_run_local']:
229 c
= colorama
.Fore
.YELLOW
231 c
= colorama
.Fore
.GREEN
232 total_local
+= location
['count_run_local']
233 total_swarming
+= location
['count_run_on_swarming']
235 if name
+ '.isolate' not in isolates
:
236 missing_isolate
= colorama
.Fore
.MAGENTA
+ '*'
237 print('%s%-*s %4d %4d %s' %
238 (c
, l
, name
, location
['count_run_local'],
239 location
['count_run_on_swarming'], missing_isolate
))
241 total
= total_local
+ total_swarming
242 p_local
= 100. * total_local
/ total
243 p_swarming
= 100. * total_swarming
/ total
244 print('%s%-*s %4d (%4.1f%%) %4d (%4.1f%%)' %
245 (colorama
.Fore
.WHITE
, l
, 'Total:', total_local
, p_local
,
246 total_swarming
, p_swarming
))
247 print('%-*s %4d' % (l
, 'Total executions:', total
))
252 parser
= argparse
.ArgumentParser(description
=sys
.modules
[__name__
].__doc
__)
253 group
= parser
.add_mutually_exclusive_group(required
=True)
255 '-c', '--check', dest
='mode', action
='store_const', const
='check',
256 default
='check', help='Only check the files')
258 '--convert', dest
='mode', action
='store_const', const
='convert',
259 help='Convert a test to run on Swarming everywhere')
261 '--remaining', dest
='mode', action
='store_const', const
='remaining',
262 help='Count the number of tests not yet running on Swarming')
264 '-w', '--write', dest
='mode', action
='store_const', const
='write',
265 help='Rewrite the files')
267 'test_name', nargs
='?',
268 help='The test name to print which configs to update; only to be used '
270 args
= parser
.parse_args()
272 if args
.mode
== 'convert':
273 if not args
.test_name
:
274 parser
.error('A test name is required with --convert')
275 if args
.test_name
+ '.isolate' not in get_isolates():
276 parser
.error('Create %s.isolate first' % args
.test_name
)
278 # Stats when running in --remaining mode;
279 tests_location
= collections
.defaultdict(
281 'count_run_local': 0, 'count_run_on_swarming': 0, 'local_configs': {}
284 with
open(os
.path
.join(THIS_DIR
, "gn_isolate_map.pyl")) as fp
:
285 gn_isolate_map
= ast
.literal_eval(fp
.read())
286 ninja_targets
= {k
: v
['label'] for k
, v
in gn_isolate_map
.items()}
290 ninja_targets_seen
= set()
291 for filepath
in glob
.glob(os
.path
.join(THIS_DIR
, '*.json')):
292 if not process_file(args
.mode
, args
.test_name
, tests_location
, filepath
,
293 ninja_targets
, ninja_targets_seen
):
296 extra_targets
= (set(ninja_targets
) - ninja_targets_seen
-
297 SKIP_GN_ISOLATE_MAP_TARGETS
)
299 if len(extra_targets
) > 1:
300 extra_targets_str
= ', '.join(extra_targets
) + ' are'
302 extra_targets_str
= list(extra_targets
)[0] + ' is'
303 raise Error('%s listed in gn_isolate_map.pyl but not in any .json '
304 'files' % extra_targets_str
)
306 if args
.mode
== 'convert':
307 print_convert(args
.test_name
, tests_location
)
308 elif args
.mode
== 'remaining':
309 print_remaining(args
.test_name
, tests_location
)
312 sys
.stderr
.write('%s\n' % e
)
316 if __name__
== "__main__":