Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2013 The Chromium Authors. All rights reserved. | 2 # Copyright 2013 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Client tool to trigger tasks or retrieve results from a Swarming server.""" | 6 """Client tool to trigger tasks or retrieve results from a Swarming server.""" |
| 7 | 7 |
| 8 __version__ = '0.1' | 8 __version__ = '0.1' |
| 9 | 9 |
| 10 import binascii | |
| 10 import hashlib | 11 import hashlib |
| 11 import json | 12 import json |
| 12 import logging | 13 import logging |
| 13 import os | 14 import os |
| 14 import re | 15 import re |
| 15 import shutil | 16 import shutil |
| 16 import StringIO | |
| 17 import subprocess | 17 import subprocess |
| 18 import sys | 18 import sys |
| 19 import time | 19 import time |
| 20 import urllib | 20 import urllib |
| 21 import zipfile | |
| 22 | 21 |
| 23 from third_party import colorama | 22 from third_party import colorama |
| 24 from third_party.depot_tools import fix_encoding | 23 from third_party.depot_tools import fix_encoding |
| 25 from third_party.depot_tools import subcommand | 24 from third_party.depot_tools import subcommand |
| 25 | |
| 26 from utils import net | |
| 27 from utils import threading_utils | |
| 26 from utils import tools | 28 from utils import tools |
| 27 from utils import threading_utils | 29 from utils import zip_package |
| 28 | 30 |
| 29 import run_isolated | 31 import run_isolated |
| 30 | 32 |
| 31 | 33 |
| 32 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) | 34 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) |
| 33 TOOLS_PATH = os.path.join(ROOT_DIR, 'tools') | 35 TOOLS_PATH = os.path.join(ROOT_DIR, 'tools') |
| 34 | 36 |
| 35 | 37 |
| 36 # Default servers. | 38 # Default servers. |
| 37 # TODO(maruel): Chromium-specific. | 39 # TODO(maruel): Chromium-specific. |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 76 shards - The number of swarm shards to request. | 78 shards - The number of swarm shards to request. |
| 77 test_filter - The gtest filter to apply when running the test. | 79 test_filter - The gtest filter to apply when running the test. |
| 78 slave_os - OS to run on. | 80 slave_os - OS to run on. |
| 79 working_dir - Relative working directory to start the script. | 81 working_dir - Relative working directory to start the script. |
| 80 isolate_server - isolate server url. | 82 isolate_server - isolate server url. |
| 81 verbose - if True, have the slave print more details. | 83 verbose - if True, have the slave print more details. |
| 82 profile - if True, have the slave print more timing data. | 84 profile - if True, have the slave print more timing data. |
| 83 priority - int between 0 and 1000, lower the higher priority | 85 priority - int between 0 and 1000, lower the higher priority |
| 84 """ | 86 """ |
| 85 self.manifest_hash = manifest_hash | 87 self.manifest_hash = manifest_hash |
| 88 self.bundle = zip_package.ZipPackage(ROOT_DIR) | |
| 89 | |
| 86 self._test_name = test_name | 90 self._test_name = test_name |
| 87 self._shards = shards | 91 self._shards = shards |
| 88 self._test_filter = test_filter | 92 self._test_filter = test_filter |
| 89 self._target_platform = PLATFORM_MAPPING[slave_os] | 93 self._target_platform = PLATFORM_MAPPING[slave_os] |
| 90 self._working_dir = working_dir | 94 self._working_dir = working_dir |
| 91 | 95 |
| 92 self.data_server_retrieval = isolate_server + '/content/retrieve/default/' | 96 self.data_server_retrieval = isolate_server + '/content/retrieve/default/' |
| 93 self._data_server_storage = isolate_server + '/content/store/default/' | 97 self._data_server_storage = isolate_server + '/content/store/default/' |
| 94 self._data_server_has = isolate_server + '/content/contains/default' | 98 self._data_server_has = isolate_server + '/content/contains/default' |
| 95 self._data_server_get_token = isolate_server + '/content/get_token' | 99 self._data_server_get_token = isolate_server + '/content/get_token' |
| 96 | 100 |
| 97 self.verbose = bool(verbose) | 101 self.verbose = bool(verbose) |
| 98 self.profile = bool(profile) | 102 self.profile = bool(profile) |
| 99 self.priority = priority | 103 self.priority = priority |
| 100 | 104 |
| 101 self._zip_file_hash = '' | 105 self._zip_file_hash = '' |
| 102 self._tasks = [] | 106 self._tasks = [] |
| 103 self._files = {} | 107 self._files = {} |
| 104 self._token_cache = None | 108 self._token_cache = None |
| 105 | 109 |
| 106 def _token(self): | 110 def _token(self): |
| 107 if not self._token_cache: | 111 if not self._token_cache: |
| 108 result = run_isolated.url_open(self._data_server_get_token) | 112 result = net.url_open(self._data_server_get_token) |
| 109 if not result: | 113 if not result: |
| 110 # TODO(maruel): Implement authentication. | 114 # TODO(maruel): Implement authentication. |
| 111 raise Failure('Failed to get token, need authentication') | 115 raise Failure('Failed to get token, need authentication') |
| 112 # Quote it right away, so creating the urls is simpler. | 116 # Quote it right away, so creating the urls is simpler. |
| 113 self._token_cache = urllib.quote(result.read()) | 117 self._token_cache = urllib.quote(result.read()) |
| 114 return self._token_cache | 118 return self._token_cache |
| 115 | 119 |
| 116 def add_task(self, task_name, actions, time_out=600): | 120 def add_task(self, task_name, actions, time_out=600): |
| 117 """Appends a new task to the swarm manifest file.""" | 121 """Appends a new task to the swarm manifest file.""" |
| 118 # See swarming/src/common/test_request_message.py TestObject constructor for | 122 # See swarming/src/common/test_request_message.py TestObject constructor for |
| 119 # the valid flags. | 123 # the valid flags. |
| 120 self._tasks.append( | 124 self._tasks.append( |
| 121 { | 125 { |
| 122 'action': actions, | 126 'action': actions, |
| 123 'decorate_output': self.verbose, | 127 'decorate_output': self.verbose, |
| 124 'test_name': task_name, | 128 'test_name': task_name, |
| 125 'time_out': time_out, | 129 'time_out': time_out, |
| 126 }) | 130 }) |
| 127 | 131 |
| 128 def add_file(self, source_path, rel_path): | |
| 129 self._files[source_path] = rel_path | |
| 130 | |
| 131 def zip_and_upload(self): | 132 def zip_and_upload(self): |
| 132 """Zips up all the files necessary to run a shard and uploads to Swarming | 133 """Zips up all the files necessary to run a shard and uploads to Swarming |
| 133 master. | 134 master. |
| 134 """ | 135 """ |
| 135 assert not self._zip_file_hash | 136 assert not self._zip_file_hash |
| 137 | |
| 136 start_time = time.time() | 138 start_time = time.time() |
| 137 | 139 zip_contents = self.bundle.zip_into_buffer() |
| 138 zip_memory_file = StringIO.StringIO() | 140 self._zip_file_hash = hashlib.sha1(zip_contents).hexdigest() |
| 139 zip_file = zipfile.ZipFile(zip_memory_file, 'w') | |
| 140 | |
| 141 for source, relpath in self._files.iteritems(): | |
| 142 zip_file.write(source, relpath) | |
| 143 | |
| 144 zip_file.close() | |
| 145 print 'Zipping completed, time elapsed: %f' % (time.time() - start_time) | 141 print 'Zipping completed, time elapsed: %f' % (time.time() - start_time) |
| 146 | 142 |
| 147 zip_memory_file.flush() | 143 response = net.url_open( |
| 148 zip_contents = zip_memory_file.getvalue() | |
| 149 zip_memory_file.close() | |
| 150 | |
| 151 self._zip_file_hash = hashlib.sha1(zip_contents).hexdigest() | |
| 152 | |
| 153 response = run_isolated.url_open( | |
| 154 self._data_server_has + '?token=%s' % self._token(), | 144 self._data_server_has + '?token=%s' % self._token(), |
| 155 data=self._zip_file_hash, | 145 data=binascii.unhexlify(self._zip_file_hash), |
|
M-A Ruel
2013/08/28 15:01:48
You could use .digest() instead.
| |
| 156 content_type='application/octet-stream') | 146 content_type='application/octet-stream') |
| 157 if response is None: | 147 if response is None: |
| 158 print >> sys.stderr, ( | 148 print >> sys.stderr, ( |
| 159 'Unable to query server for zip file presence, aborting.') | 149 'Unable to query server for zip file presence, aborting.') |
| 160 return False | 150 return False |
| 161 | 151 |
| 162 if response.read(1) == chr(1): | 152 if response.read(1) == chr(1): |
| 163 print 'Zip file already on server, no need to reupload.' | 153 print 'Zip file already on server, no need to reupload.' |
| 164 return True | 154 return True |
| 165 | 155 |
| 166 print 'Zip file not on server, starting uploading.' | 156 print 'Zip file not on server, starting uploading.' |
| 167 | 157 |
| 168 url = '%s%s?priority=0&token=%s' % ( | 158 url = '%s%s?priority=0&token=%s' % ( |
| 169 self._data_server_storage, self._zip_file_hash, self._token()) | 159 self._data_server_storage, self._zip_file_hash, self._token()) |
| 170 response = run_isolated.url_open( | 160 response = net.url_open( |
| 171 url, data=zip_contents, content_type='application/octet-stream') | 161 url, data=zip_contents, content_type='application/octet-stream') |
| 172 if response is None: | 162 if response is None: |
| 173 print >> sys.stderr, 'Failed to upload the zip file: %s' % url | 163 print >> sys.stderr, 'Failed to upload the zip file: %s' % url |
| 174 return False | 164 return False |
| 175 | 165 |
| 176 return True | 166 return True |
| 177 | 167 |
| 178 def to_json(self): | 168 def to_json(self): |
| 179 """Exports the current configuration into a swarm-readable manifest file. | 169 """Exports the current configuration into a swarm-readable manifest file. |
| 180 | 170 |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 216 def now(): | 206 def now(): |
| 217 """Exists so it can be mocked easily.""" | 207 """Exists so it can be mocked easily.""" |
| 218 return time.time() | 208 return time.time() |
| 219 | 209 |
| 220 | 210 |
| 221 def get_test_keys(swarm_base_url, test_name): | 211 def get_test_keys(swarm_base_url, test_name): |
| 222 """Returns the Swarm test key for each shards of test_name.""" | 212 """Returns the Swarm test key for each shards of test_name.""" |
| 223 key_data = urllib.urlencode([('name', test_name)]) | 213 key_data = urllib.urlencode([('name', test_name)]) |
| 224 url = '%s/get_matching_test_cases?%s' % (swarm_base_url, key_data) | 214 url = '%s/get_matching_test_cases?%s' % (swarm_base_url, key_data) |
| 225 | 215 |
| 226 for i in range(run_isolated.URL_OPEN_MAX_ATTEMPTS): | 216 for i in range(net.URL_OPEN_MAX_ATTEMPTS): |
| 227 response = run_isolated.url_open(url, retry_404=True) | 217 response = net.url_open(url, retry_404=True) |
| 228 if response is None: | 218 if response is None: |
| 229 raise Failure( | 219 raise Failure( |
| 230 'Error: Unable to find any tests with the name, %s, on swarm server' | 220 'Error: Unable to find any tests with the name, %s, on swarm server' |
| 231 % test_name) | 221 % test_name) |
| 232 | 222 |
| 233 result = response.read() | 223 result = response.read() |
| 234 # TODO(maruel): Compare exact string. | 224 # TODO(maruel): Compare exact string. |
| 235 if 'No matching' in result: | 225 if 'No matching' in result: |
| 236 logging.warning('Unable to find any tests with the name, %s, on swarm ' | 226 logging.warning('Unable to find any tests with the name, %s, on swarm ' |
| 237 'server' % test_name) | 227 'server' % test_name) |
| 238 if i != run_isolated.URL_OPEN_MAX_ATTEMPTS: | 228 if i != net.URL_OPEN_MAX_ATTEMPTS: |
| 239 run_isolated.HttpService.sleep_before_retry(i, None) | 229 net.HttpService.sleep_before_retry(i, None) |
| 240 continue | 230 continue |
| 241 return json.loads(result) | 231 return json.loads(result) |
| 242 | 232 |
| 243 raise Failure( | 233 raise Failure( |
| 244 'Error: Unable to find any tests with the name, %s, on swarm server' | 234 'Error: Unable to find any tests with the name, %s, on swarm server' |
| 245 % test_name) | 235 % test_name) |
| 246 | 236 |
| 247 | 237 |
| 248 def retrieve_results(base_url, test_key, timeout, should_stop): | 238 def retrieve_results(base_url, test_key, timeout, should_stop): |
| 249 """Retrieves results for a single test_key.""" | 239 """Retrieves results for a single test_key.""" |
| 250 assert isinstance(timeout, float) | 240 assert isinstance(timeout, float) |
| 251 params = [('r', test_key)] | 241 params = [('r', test_key)] |
| 252 result_url = '%s/get_result?%s' % (base_url, urllib.urlencode(params)) | 242 result_url = '%s/get_result?%s' % (base_url, urllib.urlencode(params)) |
| 253 start = now() | 243 start = now() |
| 254 while True: | 244 while True: |
| 255 if timeout and (now() - start) >= timeout: | 245 if timeout and (now() - start) >= timeout: |
| 256 logging.error('retrieve_results(%s) timed out', base_url) | 246 logging.error('retrieve_results(%s) timed out', base_url) |
| 257 return {} | 247 return {} |
| 258 # Do retries ourselves. | 248 # Do retries ourselves. |
| 259 response = run_isolated.url_open( | 249 response = net.url_open(result_url, retry_404=False, retry_50x=False) |
| 260 result_url, retry_404=False, retry_50x=False) | |
| 261 if response is None: | 250 if response is None: |
| 262 # Aggressively poll for results. Do not use retry_404 so | 251 # Aggressively poll for results. Do not use retry_404 so |
| 263 # should_stop is polled more often. | 252 # should_stop is polled more often. |
| 264 remaining = min(5, timeout - (now() - start)) if timeout else 5 | 253 remaining = min(5, timeout - (now() - start)) if timeout else 5 |
| 265 if remaining > 0: | 254 if remaining > 0: |
| 266 run_isolated.HttpService.sleep_before_retry(1, remaining) | 255 net.HttpService.sleep_before_retry(1, remaining) |
| 267 else: | 256 else: |
| 268 try: | 257 try: |
| 269 data = json.load(response) or {} | 258 data = json.load(response) or {} |
| 270 except (ValueError, TypeError): | 259 except (ValueError, TypeError): |
| 271 logging.warning( | 260 logging.warning( |
| 272 'Received corrupted data for test_key %s. Retrying.', test_key) | 261 'Received corrupted data for test_key %s. Retrying.', test_key) |
| 273 else: | 262 else: |
| 274 if data['output']: | 263 if data['output']: |
| 275 return data | 264 return data |
| 276 if should_stop.get(): | 265 if should_stop.get(): |
| (...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 314 finally: | 303 finally: |
| 315 # Done, kill the remaining threads. | 304 # Done, kill the remaining threads. |
| 316 should_stop.set() | 305 should_stop.set() |
| 317 | 306 |
| 318 | 307 |
| 319 def chromium_setup(manifest): | 308 def chromium_setup(manifest): |
| 320 """Sets up the commands to run. | 309 """Sets up the commands to run. |
| 321 | 310 |
| 322 Highly chromium specific. | 311 Highly chromium specific. |
| 323 """ | 312 """ |
| 313 # Add uncompressed zip here. It'll be compressed as part of the package sent | |
| 314 # to Swarming server. | |
| 315 run_test_name = 'run_isolated.zip' | |
| 316 manifest.bundle.add_buffer(run_test_name, | |
| 317 run_isolated.get_as_zip_package().zip_into_buffer(compress=False)) | |
| 318 | |
| 324 cleanup_script_name = 'swarm_cleanup.py' | 319 cleanup_script_name = 'swarm_cleanup.py' |
| 325 cleanup_script_path = os.path.join(TOOLS_PATH, cleanup_script_name) | 320 manifest.bundle.add_file(os.path.join(TOOLS_PATH, cleanup_script_name), |
| 326 run_test_name = 'run_isolated.py' | 321 cleanup_script_name) |
| 327 run_test_path = os.path.join(ROOT_DIR, run_test_name) | |
| 328 | 322 |
| 329 manifest.add_file(run_test_path, run_test_name) | |
| 330 manifest.add_file(cleanup_script_path, cleanup_script_name) | |
| 331 run_cmd = [ | 323 run_cmd = [ |
| 332 'python', run_test_name, | 324 'python', run_test_name, |
| 333 '--hash', manifest.manifest_hash, | 325 '--hash', manifest.manifest_hash, |
| 334 '--remote', manifest.data_server_retrieval.rstrip('/') + '-gzip/', | 326 '--remote', manifest.data_server_retrieval.rstrip('/') + '-gzip/', |
| 335 ] | 327 ] |
| 336 if manifest.verbose or manifest.profile: | 328 if manifest.verbose or manifest.profile: |
| 337 # Have it print the profiling section. | 329 # Have it print the profiling section. |
| 338 run_cmd.append('--verbose') | 330 run_cmd.append('--verbose') |
| 339 manifest.add_task('Run Test', run_cmd) | 331 manifest.add_task('Run Test', run_cmd) |
| 340 | 332 |
| (...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 397 print('Zipping up files...') | 389 print('Zipping up files...') |
| 398 if not manifest.zip_and_upload(): | 390 if not manifest.zip_and_upload(): |
| 399 return 1 | 391 return 1 |
| 400 | 392 |
| 401 # Send test requests off to swarm. | 393 # Send test requests off to swarm. |
| 402 print('Sending test requests to swarm.') | 394 print('Sending test requests to swarm.') |
| 403 print('Server: %s' % swarming) | 395 print('Server: %s' % swarming) |
| 404 print('Job name: %s' % test_name) | 396 print('Job name: %s' % test_name) |
| 405 test_url = swarming + '/test' | 397 test_url = swarming + '/test' |
| 406 manifest_text = manifest.to_json() | 398 manifest_text = manifest.to_json() |
| 407 result = run_isolated.url_open(test_url, data={'request': manifest_text}) | 399 result = net.url_open(test_url, data={'request': manifest_text}) |
| 408 if not result: | 400 if not result: |
| 409 print >> sys.stderr, 'Failed to send test for %s\n%s' % ( | 401 print >> sys.stderr, 'Failed to send test for %s\n%s' % ( |
| 410 test_name, test_url) | 402 test_name, test_url) |
| 411 return 1 | 403 return 1 |
| 412 try: | 404 try: |
| 413 json.load(result) | 405 json.load(result) |
| 414 except (ValueError, TypeError) as e: | 406 except (ValueError, TypeError) as e: |
| 415 print >> sys.stderr, 'Failed to send test for %s' % test_name | 407 print >> sys.stderr, 'Failed to send test for %s' % test_name |
| 416 print >> sys.stderr, 'Manifest: %s' % manifest_text | 408 print >> sys.stderr, 'Manifest: %s' % manifest_text |
| 417 print >> sys.stderr, str(e) | 409 print >> sys.stderr, str(e) |
| (...skipping 261 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 679 sys.stderr.write(str(e)) | 671 sys.stderr.write(str(e)) |
| 680 sys.stderr.write('\n') | 672 sys.stderr.write('\n') |
| 681 return 1 | 673 return 1 |
| 682 | 674 |
| 683 | 675 |
| 684 if __name__ == '__main__': | 676 if __name__ == '__main__': |
| 685 fix_encoding.fix_encoding() | 677 fix_encoding.fix_encoding() |
| 686 tools.disable_buffering() | 678 tools.disable_buffering() |
| 687 colorama.init() | 679 colorama.init() |
| 688 sys.exit(main(sys.argv[1:])) | 680 sys.exit(main(sys.argv[1:])) |
| OLD | NEW |