| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # Copyright (c) 2012 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. | |
| 5 | |
| 6 """Retrieves all the output that the Swarm server has produced for requests with | |
| 7 that name. | |
| 8 """ | |
| 9 | |
| 10 import json | |
| 11 import logging | |
| 12 import optparse | |
| 13 import sys | |
| 14 import time | |
| 15 import urllib | |
| 16 | |
| 17 from third_party.depot_tools import fix_encoding | |
| 18 | |
| 19 import run_isolated | |
| 20 | |
| 21 from utils import threading_utils | |
| 22 from utils import tools | |
| 23 | |
| 24 | |
| 25 # The default time to wait for a shard to finish running. | |
| 26 DEFAULT_SHARD_WAIT_TIME = 40 * 60. | |
| 27 | |
| 28 | |
| 29 class Failure(Exception): | |
| 30 """Generic failure.""" | |
| 31 pass | |
| 32 | |
| 33 | |
| 34 def get_test_keys(swarm_base_url, test_name, _=None): | |
| 35 """Returns the Swarm test key for each shards of test_name.""" | |
| 36 # TODO(maruel): Remove the parameter '_' once the | |
| 37 # build/scripts/slave/get_swarm_results.py stops passing it. | |
| 38 key_data = urllib.urlencode([('name', test_name)]) | |
| 39 url = '%s/get_matching_test_cases?%s' % (swarm_base_url, key_data) | |
| 40 | |
| 41 for i in range(run_isolated.URL_OPEN_MAX_ATTEMPTS): | |
| 42 response = run_isolated.url_open(url, retry_404=True) | |
| 43 if response is None: | |
| 44 raise Failure( | |
| 45 'Error: Unable to find any tests with the name, %s, on swarm server' | |
| 46 % test_name) | |
| 47 | |
| 48 result = response.read() | |
| 49 # TODO(maruel): Compare exact string. | |
| 50 if 'No matching' in result: | |
| 51 logging.warning('Unable to find any tests with the name, %s, on swarm ' | |
| 52 'server' % test_name) | |
| 53 if i != run_isolated.URL_OPEN_MAX_ATTEMPTS: | |
| 54 run_isolated.HttpService.sleep_before_retry(i, None) | |
| 55 continue | |
| 56 return json.loads(result) | |
| 57 | |
| 58 raise Failure( | |
| 59 'Error: Unable to find any tests with the name, %s, on swarm server' | |
| 60 % test_name) | |
| 61 | |
| 62 | |
| 63 def now(): | |
| 64 """Exists so it can be mocked easily.""" | |
| 65 return time.time() | |
| 66 | |
| 67 | |
| 68 def retrieve_results(base_url, test_key, timeout, should_stop): | |
| 69 """Retrieves results for a single test_key.""" | |
| 70 assert isinstance(timeout, float) | |
| 71 params = [('r', test_key)] | |
| 72 result_url = '%s/get_result?%s' % (base_url, urllib.urlencode(params)) | |
| 73 start = now() | |
| 74 while True: | |
| 75 if timeout and (now() - start) >= timeout: | |
| 76 logging.error('retrieve_results(%s) timed out', base_url) | |
| 77 return {} | |
| 78 # Do retries ourselves. | |
| 79 response = run_isolated.url_open( | |
| 80 result_url, retry_404=False, retry_50x=False) | |
| 81 if response is None: | |
| 82 # Aggressively poll for results. Do not use retry_404 so | |
| 83 # should_stop is polled more often. | |
| 84 remaining = min(5, timeout - (now() - start)) if timeout else 5 | |
| 85 if remaining > 0: | |
| 86 run_isolated.HttpService.sleep_before_retry(1, remaining) | |
| 87 else: | |
| 88 try: | |
| 89 data = json.load(response) or {} | |
| 90 except (ValueError, TypeError): | |
| 91 logging.warning( | |
| 92 'Received corrupted data for test_key %s. Retrying.', test_key) | |
| 93 else: | |
| 94 if data['output']: | |
| 95 return data | |
| 96 if should_stop.get(): | |
| 97 return {} | |
| 98 | |
| 99 | |
| 100 def yield_results(swarm_base_url, test_keys, timeout, max_threads): | |
| 101 """Yields swarm test results from the swarm server as (index, result). | |
| 102 | |
| 103 Duplicate shards are ignored, the first one to complete is returned. | |
| 104 | |
| 105 max_threads is optional and is used to limit the number of parallel fetches | |
| 106 done. Since in general the number of test_keys is in the range <=10, it's not | |
| 107 worth normally to limit the number threads. Mostly used for testing purposes. | |
| 108 """ | |
| 109 shards_remaining = range(len(test_keys)) | |
| 110 number_threads = ( | |
| 111 min(max_threads, len(test_keys)) if max_threads else len(test_keys)) | |
| 112 should_stop = threading_utils.Bit() | |
| 113 results_remaining = len(test_keys) | |
| 114 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool: | |
| 115 try: | |
| 116 for test_key in test_keys: | |
| 117 pool.add_task( | |
| 118 0, retrieve_results, swarm_base_url, test_key, timeout, should_stop) | |
| 119 while shards_remaining and results_remaining: | |
| 120 result = pool.get_one_result() | |
| 121 results_remaining -= 1 | |
| 122 if not result: | |
| 123 # Failed to retrieve one key. | |
| 124 logging.error('Failed to retrieve the results for a swarm key') | |
| 125 continue | |
| 126 shard_index = result['config_instance_index'] | |
| 127 if shard_index in shards_remaining: | |
| 128 shards_remaining.remove(shard_index) | |
| 129 yield shard_index, result | |
| 130 else: | |
| 131 logging.warning('Ignoring duplicate shard index %d', shard_index) | |
| 132 # Pop the last entry, there's no such shard. | |
| 133 shards_remaining.pop() | |
| 134 finally: | |
| 135 # Done, kill the remaining threads. | |
| 136 should_stop.set() | |
| 137 | |
| 138 | |
| 139 def parse_args(): | |
| 140 tools.disable_buffering() | |
| 141 parser = optparse.OptionParser( | |
| 142 usage='%prog [options] test_name', | |
| 143 description=sys.modules[__name__].__doc__) | |
| 144 parser.add_option( | |
| 145 '-u', '--url', default='http://localhost:8080', | |
| 146 help='Specify the url of the Swarm server, defaults: %default') | |
| 147 parser.add_option( | |
| 148 '-v', '--verbose', action='store_true', | |
| 149 help='Print verbose logging') | |
| 150 parser.add_option( | |
| 151 '-t', '--timeout', | |
| 152 type='float', | |
| 153 default=DEFAULT_SHARD_WAIT_TIME, | |
| 154 help='Timeout to wait for result, set to 0 for no timeout; default: ' | |
| 155 '%default s') | |
| 156 # TODO(maruel): Remove once the masters have been updated. | |
| 157 parser.add_option( | |
| 158 '-s', '--shards', | |
| 159 help=optparse.SUPPRESS_HELP) | |
| 160 | |
| 161 (options, args) = parser.parse_args() | |
| 162 if not args: | |
| 163 parser.error('Must specify one test name.') | |
| 164 elif len(args) > 1: | |
| 165 parser.error('Must specify only one test name.') | |
| 166 options.url = options.url.rstrip('/') | |
| 167 logging.basicConfig(level=logging.DEBUG if options.verbose else logging.ERROR) | |
| 168 return parser, options, args[0] | |
| 169 | |
| 170 | |
| 171 def main(): | |
| 172 parser, options, test_name = parse_args() | |
| 173 try: | |
| 174 test_keys = get_test_keys(options.url, test_name) | |
| 175 except Failure as e: | |
| 176 parser.error(e.args[0]) | |
| 177 if not test_keys: | |
| 178 parser.error('No test keys to get results with.') | |
| 179 | |
| 180 options.shards = len(test_keys) if options.shards == -1 else options.shards | |
| 181 exit_code = None | |
| 182 for _index, output in yield_results( | |
| 183 options.url, test_keys, options.timeout, None): | |
| 184 print( | |
| 185 '%s/%s: %s' % ( | |
| 186 output['machine_id'], output['machine_tag'], output['exit_codes'])) | |
| 187 print(''.join(' %s\n' % l for l in output['output'].splitlines())) | |
| 188 exit_code = max(exit_code, max(map(int, output['exit_codes'].split(',')))) | |
| 189 | |
| 190 return exit_code | |
| 191 | |
| 192 | |
| 193 if __name__ == '__main__': | |
| 194 fix_encoding.fix_encoding() | |
| 195 sys.exit(main()) | |
| OLD | NEW |