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