|  | #!/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.12 (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_adaptive_toolbar_egtests', | 
|  | 'ios_chrome_bookmarks_egtests', | 
|  | 'ios_chrome_integration_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_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', | 
|  | } | 
|  |  | 
|  |  | 
|  | 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()) |