| #!/usr/bin/env python |
| # Copyright 2015 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Toolbox to manage all the json files in this directory. |
| |
| It can reformat them in their canonical format or ensures they are well |
| formatted. |
| """ |
| |
| import argparse |
| import ast |
| import collections |
| import glob |
| import json |
| import os |
| import subprocess |
| import sys |
| |
| |
| THIS_DIR = os.path.dirname(os.path.abspath(__file__)) |
| SRC_DIR = os.path.dirname(os.path.dirname(THIS_DIR)) |
| BLINK_DIR = os.path.join(SRC_DIR, 'third_party', 'WebKit') |
| sys.path.insert(0, os.path.join(SRC_DIR, 'third_party', 'colorama', 'src')) |
| |
| import colorama |
| |
| |
| SKIP = { |
| # These are not 'builders'. |
| 'compile_targets', 'gtest_tests', 'filter_compile_builders', |
| 'non_filter_builders', 'non_filter_tests_builders', |
| |
| # These are not supported on Swarming yet. |
| |
| # Android Cloud is still experimental and involves spinning up an Android |
| # instance on GCE. Swarming doesn't work in that environment yet. |
| 'Android Cloud Tests', |
| |
| # Recipes don't promise execution on jelly bean. This could work if the |
| # OS dimensions go into the recipe, they're set in the json file, and |
| # jelly bean devices are in the pool. For now, just blacklist. |
| 'Jelly Bean Tester', |
| 'Lollipop Consumer Tester', |
| 'Lollipop Low-end Tester', |
| |
| # Android bots need custom dimension_sets entries for swarming, and capacity |
| # is not there yet -- so don't let manage.py add swarming automatically there. |
| 'Android User Builder Tests', |
| 'Android GN', |
| |
| # http://crbug.com/441429 |
| 'Linux Trusty (32)', 'Linux Trusty (dbg)(32)', |
| |
| # Swarming may not work on Mac10.10,11,12; need to |
| # re-investigate and confirm. |
| 'WebKit Mac10.10', |
| 'WebKit Mac10.11', |
| 'WebKit Mac10.12', |
| 'WebKit Mac10.11 (dbg)', |
| 'WebKit Mac10.13 (retina)', |
| 'Chromium Mac10.10 Tests', |
| 'Chromium Mac10.11 Tests', |
| |
| # One off builders. Note that Swarming does support ARM. |
| 'Linux ARM Cross-Compile', |
| 'Site Isolation Android', |
| 'Site Isolation Linux', |
| 'Site Isolation Win', |
| } |
| |
| |
| SKIP_GN_ISOLATE_MAP_TARGETS = { |
| # This target is magic and not present in gn_isolate_map.pyl. |
| 'all', |
| 'remoting/client:client', |
| 'remoting/host:host', |
| |
| # These targets are listed only in build-side recipes. |
| 'All_syzygy', |
| 'blink_tests', |
| 'cast_shell', |
| 'cast_shell_apk', |
| 'chrome_official_builder', |
| 'chrome_official_builder_no_unittests', |
| 'chromium_builder_asan', |
| 'chromium_builder_perf', |
| 'chromiumos_preflight', |
| 'mini_installer', |
| 'previous_version_mini_installer', |
| |
| # iOS tests are listed in //ios/build/bots. |
| 'cronet_test', |
| 'cronet_unittests_ios', |
| 'ios_chrome_bookmarks_egtests', |
| 'ios_chrome_integration_egtests', |
| 'ios_chrome_manual_fill_egtests', |
| 'ios_chrome_payments_egtests', |
| 'ios_chrome_reading_list_egtests', |
| 'ios_chrome_settings_egtests', |
| 'ios_chrome_smoke_egtests', |
| 'ios_chrome_ui_egtests', |
| 'ios_chrome_unittests', |
| 'ios_chrome_web_egtests', |
| 'ios_components_unittests', |
| 'ios_net_unittests', |
| "ios_remoting_unittests", |
| 'ios_showcase_egtests', |
| 'ios_web_inttests', |
| 'ios_web_shell_egtests', |
| 'ios_web_unittests', |
| 'ios_web_view_inttests', |
| 'ios_web_view_unittests', |
| 'ocmock_support_unittests', |
| |
| # These are listed in Builders that are skipped for other reasons. |
| 'chrome_junit_tests', |
| 'components_background_task_scheduler_junit_tests', |
| 'components_gcm_driver_junit_tests', |
| 'components_instance_id_junit_tests', |
| 'components_invalidation_impl_junit_tests', |
| 'components_policy_junit_tests', |
| 'components_variations_junit_tests', |
| 'components_web_restrictions_junit_tests', |
| 'content_junit_tests', |
| 'content_junit_tests', |
| 'device_junit_tests', |
| 'junit_unit_tests', |
| 'media_router_e2e_tests', |
| 'media_router_perf_tests', |
| 'motopho_latency_test', |
| 'net_junit_tests', |
| 'net_junit_tests', |
| 'service_junit_tests', |
| 'system_webview_apk', |
| 'ui_junit_tests', |
| 'vrcore_fps_test', |
| 'vr_common_perftests', |
| 'vr_perf_tests', |
| 'webapk_client_junit_tests', |
| 'webapk_shell_apk_junit_tests', |
| |
| # These tests are only run on WebRTC CI. |
| 'AppRTCMobileTest', |
| 'android_junit_tests', |
| 'audio_decoder_unittests', |
| 'common_audio_unittests', |
| 'common_video_unittests', |
| 'frame_analyzer', |
| 'libjingle_peerconnection_android_unittest', |
| 'modules_tests', |
| 'modules_unittests', |
| 'peerconnection_unittests', |
| 'rtc_media_unittests', |
| 'rtc_pc_unittests', |
| 'rtc_stats_unittests', |
| 'rtc_unittests', |
| 'system_wrappers_unittests', |
| 'test_support_unittests', |
| 'tools_unittests', |
| 'video_engine_tests', |
| 'voice_engine_unittests', |
| 'webrtc_nonparallel_tests', |
| 'xmllite_xmpp_unittests', |
| |
| # isolate is currently too slow for this target. |
| # http://crbug.com/524758 |
| 'webkit_layout_tests', |
| 'webkit_layout_tests_exparchive', |
| |
| # These are only run on V8 CI. |
| 'pdfium_test', |
| 'postmortem-metadata', |
| |
| # These are only for developer convenience and not on any bots. |
| 'telemetry_gpu_integration_test_scripts_only', |
| } |
| |
| |
| class Error(Exception): |
| """Processing error.""" |
| |
| |
| def get_isolates(): |
| """Returns the list of all isolate files.""" |
| |
| def git_ls_files(cwd): |
| return subprocess.check_output(['git', 'ls-files'], cwd=cwd).splitlines() |
| |
| files = git_ls_files(SRC_DIR) + git_ls_files(BLINK_DIR) |
| return [os.path.basename(f) for f in files if f.endswith('.isolate')] |
| |
| |
| def process_builder_convert(data, test_name): |
| """Converts 'test_name' to run on Swarming in 'data'. |
| |
| Returns True if 'test_name' was found. |
| """ |
| result = False |
| for test in data['gtest_tests']: |
| if test['test'] != test_name: |
| continue |
| test.setdefault('swarming', {}) |
| if not test['swarming'].get('can_use_on_swarming_builders'): |
| test['swarming']['can_use_on_swarming_builders'] = True |
| result = True |
| return result |
| |
| |
| def process_builder_remaining(data, filename, builder, tests_location): |
| """Calculates tests_location when mode is --remaining.""" |
| for test in data['gtest_tests']: |
| name = test['test'] |
| if test.get('swarming', {}).get('can_use_on_swarming_builders'): |
| tests_location[name]['count_run_on_swarming'] += 1 |
| else: |
| tests_location[name]['count_run_local'] += 1 |
| tests_location[name]['local_configs'].setdefault( |
| filename, []).append(builder) |
| |
| |
| def process_file(mode, test_name, tests_location, filepath, ninja_targets, |
| ninja_targets_seen): |
| """Processes a json file describing what tests should be run for each recipe. |
| |
| The action depends on mode. Updates tests_location. |
| |
| Return False if the process exit code should be 1. |
| """ |
| filename = os.path.basename(filepath) |
| with open(filepath) as f: |
| content = f.read() |
| try: |
| config = json.loads(content) |
| except ValueError as e: |
| raise Error('Exception raised while checking %s: %s' % (filepath, e)) |
| |
| for builder, data in sorted(config.iteritems()): |
| if builder in SKIP: |
| # Oddities. |
| continue |
| if not isinstance(data, dict): |
| raise Error('%s: %s is broken: %s' % (filename, builder, data)) |
| if ('gtest_tests' not in data and |
| 'isolated_scripts' not in data and |
| 'additional_compile_targets' not in data and |
| 'instrumentation_tests' not in data): |
| continue |
| |
| for d in data.get('junit_tests', []): |
| test = d['test'] |
| if (test not in ninja_targets and |
| test not in SKIP_GN_ISOLATE_MAP_TARGETS): |
| raise Error('%s: %s / %s is not listed in gn_isolate_map.pyl' % |
| (filename, builder, test)) |
| elif test in ninja_targets: |
| ninja_targets_seen.add(test) |
| |
| for target in data.get('additional_compile_targets', []): |
| if (target not in ninja_targets and |
| target not in SKIP_GN_ISOLATE_MAP_TARGETS): |
| raise Error('%s: %s / %s is not listed in gn_isolate_map.pyl' % |
| (filename, builder, target)) |
| elif target in ninja_targets: |
| ninja_targets_seen.add(target) |
| |
| gtest_tests = data.get('gtest_tests', []) |
| if not isinstance(gtest_tests, list): |
| raise Error( |
| '%s: %s is broken: %s' % (filename, builder, gtest_tests)) |
| if not all(isinstance(g, dict) for g in gtest_tests): |
| raise Error( |
| '%s: %s is broken: %s' % (filename, builder, gtest_tests)) |
| |
| seen = set() |
| for d in gtest_tests: |
| test = d['test'] |
| if (test not in ninja_targets and |
| test not in SKIP_GN_ISOLATE_MAP_TARGETS): |
| raise Error('%s: %s / %s is not listed in gn_isolate_map.pyl.' % |
| (filename, builder, test)) |
| elif test in ninja_targets: |
| ninja_targets_seen.add(test) |
| |
| name = d.get('name', d['test']) |
| if name in seen: |
| raise Error('%s: %s / %s is listed multiple times.' % |
| (filename, builder, name)) |
| seen.add(name) |
| d.setdefault('swarming', {}).setdefault( |
| 'can_use_on_swarming_builders', False) |
| |
| if gtest_tests: |
| config[builder]['gtest_tests'] = sorted( |
| gtest_tests, key=lambda x: x['test']) |
| |
| for d in data.get('isolated_scripts', []): |
| name = d['isolate_name'] |
| if (name not in ninja_targets and |
| name not in SKIP_GN_ISOLATE_MAP_TARGETS): |
| raise Error('%s: %s / %s is not listed in gn_isolate_map.pyl.' % |
| (filename, builder, name)) |
| elif name in ninja_targets: |
| ninja_targets_seen.add(name) |
| |
| for d in data.get('instrumentation_tests', []): |
| name = d['test'] |
| if (name not in ninja_targets and |
| name not in SKIP_GN_ISOLATE_MAP_TARGETS): |
| raise Error('%s: %s / %s is not listed in gn_isolate_map.pyl.' % |
| (filename, builder, name)) |
| elif name in ninja_targets: |
| ninja_targets_seen.add(name) |
| |
| # The trick here is that process_builder_remaining() is called before |
| # process_builder_convert() so tests_location can be used to know how many |
| # tests were converted. |
| if mode in ('convert', 'remaining'): |
| process_builder_remaining(data, filename, builder, tests_location) |
| if mode == 'convert': |
| process_builder_convert(data, test_name) |
| |
| expected = json.dumps( |
| config, sort_keys=True, indent=2, separators=(',', ': ')) + '\n' |
| if content != expected: |
| if mode in ('convert', 'write'): |
| with open(filepath, 'wb') as f: |
| f.write(expected) |
| if mode == 'write': |
| print('Updated %s' % filename) |
| else: |
| print('%s is not in canonical format' % filename) |
| print('run `testing/buildbot/manage.py -w` to fix') |
| return mode != 'check' |
| return True |
| |
| |
| def print_convert(test_name, tests_location): |
| """Prints statistics for a test being converted for use in a CL description. |
| """ |
| data = tests_location[test_name] |
| print('Convert %s to run exclusively on Swarming' % test_name) |
| print('') |
| print('%d configs already ran on Swarming' % data['count_run_on_swarming']) |
| print('%d used to run locally and were converted:' % data['count_run_local']) |
| for master, builders in sorted(data['local_configs'].iteritems()): |
| for builder in builders: |
| print('- %s: %s' % (master, builder)) |
| print('') |
| print('Ran:') |
| print(' ./manage.py --convert %s' % test_name) |
| print('') |
| print('R=') |
| print('BUG=98637') |
| |
| |
| def print_remaining(test_name, tests_location): |
| """Prints a visual summary of what tests are yet to be converted to run on |
| Swarming. |
| """ |
| if test_name: |
| if test_name not in tests_location: |
| raise Error('Unknown test %s' % test_name) |
| for config, builders in sorted( |
| tests_location[test_name]['local_configs'].iteritems()): |
| print('%s:' % config) |
| for builder in sorted(builders): |
| print(' %s' % builder) |
| return |
| |
| isolates = get_isolates() |
| l = max(map(len, tests_location)) |
| print('%-*s%sLocal %sSwarming %sMissing isolate' % |
| (l, 'Test', colorama.Fore.RED, colorama.Fore.GREEN, |
| colorama.Fore.MAGENTA)) |
| total_local = 0 |
| total_swarming = 0 |
| for name, location in sorted(tests_location.iteritems()): |
| if not location['count_run_on_swarming']: |
| c = colorama.Fore.RED |
| elif location['count_run_local']: |
| c = colorama.Fore.YELLOW |
| else: |
| c = colorama.Fore.GREEN |
| total_local += location['count_run_local'] |
| total_swarming += location['count_run_on_swarming'] |
| missing_isolate = '' |
| if name + '.isolate' not in isolates: |
| missing_isolate = colorama.Fore.MAGENTA + '*' |
| print('%s%-*s %4d %4d %s' % |
| (c, l, name, location['count_run_local'], |
| location['count_run_on_swarming'], missing_isolate)) |
| |
| total = total_local + total_swarming |
| p_local = 100. * total_local / total |
| p_swarming = 100. * total_swarming / total |
| print('%s%-*s %4d (%4.1f%%) %4d (%4.1f%%)' % |
| (colorama.Fore.WHITE, l, 'Total:', total_local, p_local, |
| total_swarming, p_swarming)) |
| print('%-*s %4d' % (l, 'Total executions:', total)) |
| |
| |
| def main(): |
| colorama.init() |
| parser = argparse.ArgumentParser(description=sys.modules[__name__].__doc__) |
| group = parser.add_mutually_exclusive_group(required=True) |
| group.add_argument( |
| '-c', '--check', dest='mode', action='store_const', const='check', |
| default='check', help='Only check the files') |
| group.add_argument( |
| '--convert', dest='mode', action='store_const', const='convert', |
| help='Convert a test to run on Swarming everywhere') |
| group.add_argument( |
| '--remaining', dest='mode', action='store_const', const='remaining', |
| help='Count the number of tests not yet running on Swarming') |
| group.add_argument( |
| '-w', '--write', dest='mode', action='store_const', const='write', |
| help='Rewrite the files') |
| parser.add_argument( |
| 'test_name', nargs='?', |
| help='The test name to print which configs to update; only to be used ' |
| 'with --remaining') |
| args = parser.parse_args() |
| |
| if args.mode == 'convert': |
| if not args.test_name: |
| parser.error('A test name is required with --convert') |
| if args.test_name + '.isolate' not in get_isolates(): |
| parser.error('Create %s.isolate first' % args.test_name) |
| |
| # Stats when running in --remaining mode; |
| tests_location = collections.defaultdict( |
| lambda: { |
| 'count_run_local': 0, 'count_run_on_swarming': 0, 'local_configs': {} |
| }) |
| |
| with open(os.path.join(THIS_DIR, "gn_isolate_map.pyl")) as fp: |
| gn_isolate_map = ast.literal_eval(fp.read()) |
| ninja_targets = {k: v['label'] for k, v in gn_isolate_map.items()} |
| |
| try: |
| result = 0 |
| ninja_targets_seen = set() |
| for filepath in glob.glob(os.path.join(THIS_DIR, '*.json')): |
| if not process_file(args.mode, args.test_name, tests_location, filepath, |
| ninja_targets, ninja_targets_seen): |
| result = 1 |
| |
| extra_targets = (set(ninja_targets) - ninja_targets_seen - |
| SKIP_GN_ISOLATE_MAP_TARGETS) |
| if extra_targets: |
| if len(extra_targets) > 1: |
| extra_targets_str = ', '.join(extra_targets) + ' are' |
| else: |
| extra_targets_str = list(extra_targets)[0] + ' is' |
| raise Error('%s listed in gn_isolate_map.pyl but not in any .json ' |
| 'files' % extra_targets_str) |
| |
| if args.mode == 'convert': |
| print_convert(args.test_name, tests_location) |
| elif args.mode == 'remaining': |
| print_remaining(args.test_name, tests_location) |
| return result |
| except Error as e: |
| sys.stderr.write('%s\n' % e) |
| return 1 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |