| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 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 # run_slavelastic.py: Runs a test based off of a slavelastic manifest file. | 5 # run_slavelastic.py: Runs a test based off of a slavelastic manifest file. |
| 6 | 6 |
| 7 from __future__ import with_statement | 7 from __future__ import with_statement |
| 8 import json # pylint: disable=F0401 | 8 import json |
| 9 import optparse | 9 import optparse |
| 10 import os | 10 import os |
| 11 import platform | 11 import platform |
| 12 import random | 12 import random |
| 13 import socket | 13 import socket |
| 14 import sys | 14 import sys |
| 15 import time | 15 import time |
| 16 import urllib2 | 16 import urllib2 |
| 17 import zipfile | 17 import zipfile |
| 18 | 18 |
| (...skipping 21 matching lines...) Expand all Loading... |
| 40 } | 40 } |
| 41 | 41 |
| 42 # This can cause problems when | 42 # This can cause problems when |
| 43 # |current_platform| != |switches_dict['os_image']| | 43 # |current_platform| != |switches_dict['os_image']| |
| 44 # crbug.com/117442 | 44 # crbug.com/117442 |
| 45 current_platform = platform_mapping[sys.platform] | 45 current_platform = platform_mapping[sys.platform] |
| 46 switches_dict = { | 46 switches_dict = { |
| 47 'num_shards': switches.num_shards, | 47 'num_shards': switches.num_shards, |
| 48 'os_image': current_platform, | 48 'os_image': current_platform, |
| 49 } | 49 } |
| 50 self.name = filename | 50 self.manifest_name = filename |
| 51 | 51 |
| 52 self.g_shards = switches.num_shards | 52 self.g_shards = switches.num_shards |
| 53 # Random name for the output zip file | 53 # Random name for the output zip file |
| 54 self.zipfile_name = 'swarm_tempfile_%s.zip' % ''.join(random.choice( | 54 self.zipfile_name = 'swarm_tempfile_%s.zip' % ''.join(random.choice( |
| 55 'abcdefghijklmnopqrstuvwxyz0123456789') for x in range(10)) | 55 'abcdefghijklmnopqrstuvwxyz0123456789') for x in range(10)) |
| 56 self.tasks = [] | 56 self.tasks = [] |
| 57 self.current_platform = current_platform | 57 self.current_platform = current_platform |
| 58 self.target_platform = switches_dict['os_image'] | 58 self.target_platform = switches_dict['os_image'] |
| 59 self.working_dir = switches.working_dir | 59 self.working_dir = switches.working_dir |
| 60 self.test_name = switches.test_name |
| 60 | 61 |
| 61 def add_task(self, task_name, actions): | 62 def add_task(self, task_name, actions): |
| 62 """Appends a new task to the swarm manifest file.""" | 63 """Appends a new task to the swarm manifest file.""" |
| 63 self.tasks.append({ | 64 self.tasks.append({ |
| 64 'test_name': task_name, | 65 'test_name': task_name, |
| 65 'action': actions, | 66 'action': actions, |
| 66 }) | 67 }) |
| 67 | 68 |
| 68 def zip(self): | 69 def zip(self): |
| 69 """Zip up all the files in self.files""" | 70 """Zip up all the files in self.files""" |
| 70 start_time = time.time() | 71 start_time = time.time() |
| 71 | 72 |
| 72 zip_file = zipfile.ZipFile(self.zipfile_name, 'w') | 73 zip_file = zipfile.ZipFile(self.zipfile_name, 'w') |
| 73 zip_file.write(self.name) | 74 zip_file.write(self.manifest_name) |
| 74 zip_file.write(self.run_test_path) | 75 zip_file.write(self.run_test_path) |
| 75 zip_file.close() | 76 zip_file.close() |
| 76 | 77 |
| 77 print 'Zipping completed, time elapsed: %f' % (time.time() - start_time) | 78 print 'Zipping completed, time elapsed: %f' % (time.time() - start_time) |
| 78 | 79 |
| 79 def cleanup(self): | 80 def cleanup(self): |
| 80 os.remove(self.zipfile_name) | 81 os.remove(self.zipfile_name) |
| 81 | 82 |
| 82 def to_json(self): | 83 def to_json(self): |
| 83 """Export the current configuration into a swarm-readable manifest file""" | 84 """Export the current configuration into a swarm-readable manifest file""" |
| 84 hostname = socket.gethostbyname(socket.gethostname()) | 85 hostname = socket.gethostbyname(socket.gethostname()) |
| 85 # pylint: disable=E1103 | 86 # pylint: disable=E1103 |
| 86 filepath = os.path.relpath(self.zipfile_name, '../..').replace('\\', '/') | 87 filepath = os.path.relpath(self.zipfile_name, '../..').replace('\\', '/') |
| 87 | 88 |
| 88 url = 'http://%s/hashtable/' % hostname | 89 url = 'http://%s/hashtable/' % hostname |
| 89 self.add_task( | 90 self.add_task( |
| 90 'Run Test', | 91 'Run Test', |
| 91 ['python', self.run_test_path, '-m', self.name, '-r', url]) | 92 ['python', self.run_test_path, '-m', self.manifest_name, '-r', url]) |
| 92 | 93 |
| 93 # Clean up | 94 # Clean up |
| 94 if self.current_platform == 'Linux' or self.current_platform == 'Mac': | 95 if self.current_platform == 'Linux' or self.current_platform == 'Mac': |
| 95 cleanup_commands = ['rm', '-rf'] | 96 cleanup_commands = ['rm', '-rf'] |
| 96 elif self.current_platform == 'Windows': | 97 elif self.current_platform == 'Windows': |
| 97 cleanup_commands = ['del'] | 98 cleanup_commands = ['del'] |
| 98 self.add_task('Clean Up', cleanup_commands + [self.zipfile_name]) | 99 self.add_task('Clean Up', cleanup_commands + [self.zipfile_name]) |
| 99 | 100 |
| 100 # Call kill_processes.py if on windows | 101 # Call kill_processes.py if on windows |
| 101 if self.target_platform == 'Windows': | 102 if self.target_platform == 'Windows': |
| 102 self.add_task('Kill Processes', | 103 self.add_task('Kill Processes', |
| 103 [sys.executable, '..\\b\\build\\scripts\\slave\\kill_processes.py']) | 104 [sys.executable, '..\\b\\build\\scripts\\slave\\kill_processes.py']) |
| 104 | 105 |
| 105 # Construct test case | 106 # Construct test case |
| 106 test_case = { | 107 test_case = { |
| 107 'test_case_name': self.name, | 108 'test_case_name': self.test_name, |
| 108 'data': [ | 109 'data': [ |
| 109 'http://%s/%s' % (hostname, filepath), | 110 'http://%s/%s' % (hostname, filepath), |
| 110 ], | 111 ], |
| 111 'tests': self.tasks, | 112 'tests': self.tasks, |
| 112 'env_vars': { | 113 'env_vars': { |
| 113 'GTEST_TOTAL_SHARDS': '%(num_instances)s', | 114 'GTEST_TOTAL_SHARDS': '%(num_instances)s', |
| 114 'GTEST_SHARD_INDEX': '%(instance_index)s', | 115 'GTEST_SHARD_INDEX': '%(instance_index)s', |
| 115 }, | 116 }, |
| 116 'configurations': [ | 117 'configurations': [ |
| 117 { | 118 { |
| 118 'min_instances': self.g_shards, | 119 'min_instances': self.g_shards, |
| 119 'max_instances': self.g_shards, | 120 'max_instances': self.g_shards, |
| 120 'config_name': self.target_platform, | 121 'config_name': self.target_platform, |
| 121 'dimensions': { | 122 'dimensions': { |
| 122 'os': self.target_platform, | 123 'os': self.target_platform, |
| 123 }, | 124 }, |
| 124 }, | 125 }, |
| 125 ], | 126 ], |
| 126 'working_dir': self.working_dir, | 127 'working_dir': self.working_dir, |
| 127 'cleanup': 'data', | 128 'cleanup': 'data', |
| 128 } | 129 } |
| 129 | 130 |
| 130 return json.dumps(test_case) | 131 return json.dumps(test_case) |
| 131 | 132 |
| 132 | 133 |
| 133 def _get_first_number(line): | |
| 134 for part in line.split(): | |
| 135 if part.isdigit(): | |
| 136 return int(part) | |
| 137 | |
| 138 print 'No number in :' | |
| 139 print line | |
| 140 return 0 | |
| 141 | |
| 142 | |
| 143 class TestSummary(object): | |
| 144 def __init__(self): | |
| 145 self.test_passed_count = 0 | |
| 146 self.failed_tests = [] | |
| 147 self.disabled_test_count = 0 | |
| 148 self.ignored_test_count = 0 | |
| 149 | |
| 150 def AddSummaryData(self, buf): | |
| 151 lines = buf.splitlines() | |
| 152 | |
| 153 for line in lines: | |
| 154 if '[ PASSED ]' in line: | |
| 155 self.test_passed_count += _get_first_number(line) | |
| 156 elif '[ FAILED ]' in line: | |
| 157 if ', listed below' not in line: | |
| 158 self.failed_tests.append(line) | |
| 159 elif 'DISABLED' in line: | |
| 160 self.disabled_test_count += _get_first_number(line) | |
| 161 elif 'failures' in line: | |
| 162 self.ignored_test_count += _get_first_number(line) | |
| 163 | |
| 164 def Output(self): | |
| 165 output = [] | |
| 166 | |
| 167 output.append('[ PASSED ] %i tests.' % self.test_passed_count) | |
| 168 if self.failed_tests: | |
| 169 output.append('[ FAILED ] failed tests listed below:') | |
| 170 output.extend(self.failed_tests) | |
| 171 output.append('%i FAILED TESTS' % len(self.failed_tests)) | |
| 172 | |
| 173 if self.disabled_test_count: | |
| 174 output.append('%i DISABLED TESTS' % self.disabled_test_count) | |
| 175 | |
| 176 if self.ignored_test_count: | |
| 177 output.append('%i tests with ignored failures (FAILS prefix)' % | |
| 178 self.ignored_test_count) | |
| 179 | |
| 180 return output | |
| 181 | |
| 182 def exit_code(self): | |
| 183 return int(bool(self.failed_tests)) | |
| 184 | |
| 185 | |
| 186 # TODO(csharp) The sharing_supervisor.py also has test parsing code, they should | |
| 187 # be shared. | |
| 188 def TestRunOutput(output): | |
| 189 """Go through the given output and only return the output from the Test Run | |
| 190 Step. | |
| 191 """ | |
| 192 test_run_output = [] | |
| 193 | |
| 194 in_step = False | |
| 195 step_name = '' | |
| 196 for line in output.splitlines(): | |
| 197 if in_step: | |
| 198 if '[ OK ] ' + step_name in line: | |
| 199 break | |
| 200 else: | |
| 201 test_run_output.append(line) | |
| 202 elif '[ RUN ] ' in line and 'Run Test' in line: | |
| 203 in_step = True | |
| 204 i = len('[ RUN ] ') | |
| 205 step_name = line[i:] | |
| 206 | |
| 207 return '\n'.join(test_run_output) | |
| 208 | |
| 209 | |
| 210 def main(): | 134 def main(): |
| 211 """Packages up a Slavelastic test and send it to swarm. Receive output from | 135 """Packages up a Slavelastic test and send it to swarm. Receive output from |
| 212 all shards and print it to stdout. | 136 all shards and print it to stdout. |
| 213 | 137 |
| 214 Args | 138 Args |
| 215 slavelastic manifest file | 139 slavelastic manifest file |
| 216 number of shards | 140 number of shards |
| 217 ... | 141 ... |
| 218 """ | 142 """ |
| 219 # Parses arguments | 143 # Parses arguments |
| (...skipping 10 matching lines...) Expand all Loading... |
| 230 'greater than or equal to min_shards.') | 154 'greater than or equal to min_shards.') |
| 231 parser.add_option('-o', '--os_image', | 155 parser.add_option('-o', '--os_image', |
| 232 help='Swarm OS image to request. Defaults to the ' | 156 help='Swarm OS image to request. Defaults to the ' |
| 233 'current platform.') | 157 'current platform.') |
| 234 parser.add_option('-n', '--hostname', default='localhost', | 158 parser.add_option('-n', '--hostname', default='localhost', |
| 235 help='Specify the hostname of the Swarm server. ' | 159 help='Specify the hostname of the Swarm server. ' |
| 236 'Defaults to %default') | 160 'Defaults to %default') |
| 237 parser.add_option('-p', '--port', type='int', default=8080, | 161 parser.add_option('-p', '--port', type='int', default=8080, |
| 238 help='Specify the port of the Swarm server. ' | 162 help='Specify the port of the Swarm server. ' |
| 239 'Defaults to %default') | 163 'Defaults to %default') |
| 164 parser.add_option('-t', '--test_name', |
| 165 help='Specify the name to give the swarm test request. ' |
| 166 'Defaults to the given filename') |
| 240 parser.add_option('-v', '--verbose', action='store_true', | 167 parser.add_option('-v', '--verbose', action='store_true', |
| 241 help='Print verbose logging') | 168 help='Print verbose logging') |
| 242 (options, args) = parser.parse_args() | 169 (options, args) = parser.parse_args() |
| 243 if not args: | 170 if not args: |
| 244 parser.error('Must specify one filename.') | 171 parser.error('Must specify one filename.') |
| 245 elif len(args) > 1: | 172 elif len(args) > 1: |
| 246 parser.error('Must specify only one filename.') | 173 parser.error('Must specify only one filename.') |
| 247 filename = args[0] | 174 filename = args[0] |
| 248 if not options.os_image: | 175 if not options.os_image: |
| 249 options.os_image = '%s %d' % (platform.uname()[0], 32) | 176 options.os_image = '%s %d' % (platform.uname()[0], 32) |
| 177 if not options.test_name: |
| 178 options.test_name = filename |
| 250 | 179 |
| 251 # Parses manifest file | 180 # Parses manifest file |
| 252 print "Parsing file %s..." % filename | 181 print "Parsing file %s..." % filename |
| 253 manifest = Manifest(filename, options) | 182 manifest = Manifest(filename, options) |
| 254 | 183 |
| 255 # Zip up relevent files | 184 # Zip up relevent files |
| 256 print "Zipping up files..." | 185 print "Zipping up files..." |
| 257 manifest.zip() | 186 manifest.zip() |
| 258 | 187 |
| 259 # Send test requests off to swarm. | 188 # Send test requests off to swarm. |
| 260 print 'Sending test requests to swarm' | 189 print 'Sending test requests to swarm' |
| 261 base_url = 'http://%s:%d' % (options.hostname, options.port) | 190 base_url = 'http://%s:%d' % (options.hostname, options.port) |
| 262 test_url = base_url + '/test' | 191 test_url = base_url + '/test' |
| 263 manifest_text = manifest.to_json() | 192 manifest_text = manifest.to_json() |
| 264 result = urllib2.urlopen(test_url, manifest_text).read() | 193 result = urllib2.urlopen(test_url, manifest_text).read() |
| 265 | 194 |
| 266 # Check that we can read the output as a JSON string | 195 # Check that we can read the output as a JSON string |
| 267 try: | 196 try: |
| 268 test_keys = json.loads(result) | 197 json.loads(result) |
| 269 except (ValueError, TypeError), e: | 198 except (ValueError, TypeError), e: |
| 270 print 'Request failed:' | 199 print 'Request failed:' |
| 271 print result | 200 print result |
| 201 print e |
| 272 return 1 | 202 return 1 |
| 273 | 203 |
| 274 running_test_keys = test_keys['test_keys'] | 204 return 0 |
| 275 | |
| 276 # TODO(csharp) Get hostnames from key through swarm | |
| 277 hostnames = ['localhost' for i in range(options.num_shards)] | |
| 278 | |
| 279 # TODO(csharp) Get exit codes from key through swarm | |
| 280 exit_codes = [0 for i in range(options.num_shards)] | |
| 281 | |
| 282 # Listen to output_destination | |
| 283 summary_total = TestSummary() | |
| 284 for index in range(options.num_shards): | |
| 285 print | |
| 286 print '====================================================================' | |
| 287 print 'Begin output from shard index %d (%s)' % (index, hostnames[i]) | |
| 288 print '====================================================================' | |
| 289 print | |
| 290 while True: | |
| 291 try: | |
| 292 key_url = '%s/get_result?r=%s' % (base_url, | |
| 293 running_test_keys[index]['test_key']) | |
| 294 output = urllib2.urlopen(key_url).read() | |
| 295 | |
| 296 if output: | |
| 297 cleaned_output = TestRunOutput(output) | |
| 298 summary_index = cleaned_output.rfind('[ PASSED ]') | |
| 299 summary_total.AddSummaryData(cleaned_output[summary_index:]) | |
| 300 sys.stdout.write(cleaned_output[:summary_index - 1]) | |
| 301 break | |
| 302 else: | |
| 303 # Test is not yet done, wait a bit before checking again. | |
| 304 time.sleep(0.5) | |
| 305 except urllib2.HTTPError, e: | |
| 306 print 'Calling %s threw %s' % (key_url, e) | |
| 307 print | |
| 308 print '====================================================================' | |
| 309 print 'End output from shard index %d (%s). Return %d' % (index, | |
| 310 hostnames[i], exit_codes[i]) | |
| 311 print '====================================================================' | |
| 312 print | |
| 313 manifest.cleanup() # Delete temp zip file | |
| 314 | |
| 315 print '\n'.join(summary_total.Output()) | |
| 316 print | |
| 317 | |
| 318 if options.verbose: | |
| 319 print 'All tests completed:' | |
| 320 for i in range(options.num_shards): | |
| 321 print 'Shard index %d (%s): Exit code: %d' % (i, | |
| 322 hostnames[i], exit_codes[i]) | |
| 323 | |
| 324 # TODO(csharp) replace with max exit code once exit_codes gets real values | |
| 325 return summary_total.exit_code() | |
| 326 | 205 |
| 327 if __name__ == '__main__': | 206 if __name__ == '__main__': |
| 328 sys.exit(main()) | 207 sys.exit(main()) |
| OLD | NEW |