| #!/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()) |