OLD | NEW |
1 #!/usr/bin/env python2 | 1 #!/usr/bin/env python2 |
2 # Copyright 2013 Google Inc. All rights reserved. | 2 # Copyright 2013 Google Inc. All rights reserved. |
3 # | 3 # |
4 # Licensed under the Apache License, Version 2.0 (the "License"); | 4 # Licensed under the Apache License, Version 2.0 (the "License"); |
5 # you may not use this file except in compliance with the License. | 5 # you may not use this file except in compliance with the License. |
6 # You may obtain a copy of the License at | 6 # You may obtain a copy of the License at |
7 # | 7 # |
8 # http://www.apache.org/licenses/LICENSE-2.0 | 8 # http://www.apache.org/licenses/LICENSE-2.0 |
9 # | 9 # |
10 # Unless required by applicable law or agreed to in writing, software | 10 # Unless required by applicable law or agreed to in writing, software |
(...skipping 29 matching lines...) Expand all Loading... |
40 # wait(p) will call p.terminate() and raise ProcessWasInterrupted. | 40 # wait(p) will call p.terminate() and raise ProcessWasInterrupted. |
41 class SigintHandler(object): | 41 class SigintHandler(object): |
42 class ProcessWasInterrupted(Exception): pass | 42 class ProcessWasInterrupted(Exception): pass |
43 sigint_returncodes = {-signal.SIGINT, # Unix | 43 sigint_returncodes = {-signal.SIGINT, # Unix |
44 -1073741510, # Windows | 44 -1073741510, # Windows |
45 } | 45 } |
46 def __init__(self): | 46 def __init__(self): |
47 self.__lock = threading.Lock() | 47 self.__lock = threading.Lock() |
48 self.__processes = set() | 48 self.__processes = set() |
49 self.__got_sigint = False | 49 self.__got_sigint = False |
50 signal.signal(signal.SIGINT, self.__sigint_handler) | 50 signal.signal(signal.SIGINT, lambda signal_num, frame: self.interrupt()) |
51 def __on_sigint(self): | 51 def __on_sigint(self): |
52 self.__got_sigint = True | 52 self.__got_sigint = True |
53 while self.__processes: | 53 while self.__processes: |
54 try: | 54 try: |
55 self.__processes.pop().terminate() | 55 self.__processes.pop().terminate() |
56 except OSError: | 56 except OSError: |
57 pass | 57 pass |
58 def __sigint_handler(self, signal_num, frame): | 58 def interrupt(self): |
59 with self.__lock: | 59 with self.__lock: |
60 self.__on_sigint() | 60 self.__on_sigint() |
61 def got_sigint(self): | 61 def got_sigint(self): |
62 with self.__lock: | 62 with self.__lock: |
63 return self.__got_sigint | 63 return self.__got_sigint |
64 def wait(self, p): | 64 def wait(self, p): |
65 with self.__lock: | 65 with self.__lock: |
66 if self.__got_sigint: | 66 if self.__got_sigint: |
67 p.terminate() | 67 p.terminate() |
68 self.__processes.add(p) | 68 self.__processes.add(p) |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
120 if sys.stdout.isatty(): | 120 if sys.stdout.isatty(): |
121 # stdout needs to be unbuffered since the output is interactive. | 121 # stdout needs to be unbuffered since the output is interactive. |
122 sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) | 122 sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) |
123 | 123 |
124 out = Outputter(sys.stdout) | 124 out = Outputter(sys.stdout) |
125 total_tests = 0 | 125 total_tests = 0 |
126 finished_tests = 0 | 126 finished_tests = 0 |
127 | 127 |
128 tests = {} | 128 tests = {} |
129 outputs = {} | 129 outputs = {} |
| 130 started = [] |
| 131 finished = [] |
130 failures = [] | 132 failures = [] |
131 | 133 |
| 134 def print_tests(self, message, test_ids): |
| 135 if test_ids: |
| 136 self.out.permanent_line("%s (%s/%s):" % |
| 137 (message, len(test_ids), self.total_tests)) |
| 138 test_ids = sorted(test_ids, key=lambda test_id: self.tests[test_id]) |
| 139 for test_id in test_ids: |
| 140 self.out.permanent_line(" %s: %s" % self.tests[test_id]) |
| 141 |
132 def print_test_status(self, last_finished_test, time_ms): | 142 def print_test_status(self, last_finished_test, time_ms): |
133 self.out.transient_line("[%d/%d] %s (%d ms)" | 143 self.out.transient_line("[%d/%d] %s (%d ms)" |
134 % (self.finished_tests, self.total_tests, | 144 % (self.finished_tests, self.total_tests, |
135 last_finished_test, time_ms)) | 145 last_finished_test, time_ms)) |
136 | 146 |
137 def handle_meta(self, job_id, args): | 147 def handle_meta(self, job_id, args): |
138 (command, arg) = args.split(' ', 1) | 148 (command, arg) = args.split(' ', 1) |
139 if command == "TEST": | 149 if command == "TEST": |
140 (binary, test) = arg.split(' ', 1) | 150 (binary, test) = arg.split(' ', 1) |
141 self.tests[job_id] = (binary, test.strip()) | 151 self.tests[job_id] = (binary, test.strip()) |
| 152 elif command == "START": |
| 153 self.started.append(job_id) |
142 elif command == "EXIT": | 154 elif command == "EXIT": |
143 (exit_code, time_ms) = [int(x) for x in arg.split(' ', 1)] | 155 (exit_code, time_ms) = [int(x) for x in arg.split(' ', 1)] |
144 self.finished_tests += 1 | 156 self.finished_tests += 1 |
| 157 self.finished.append(job_id) |
145 (binary, test) = self.tests[job_id] | 158 (binary, test) = self.tests[job_id] |
146 self.print_test_status(test, time_ms) | 159 self.print_test_status(test, time_ms) |
147 if exit_code != 0: | 160 if exit_code != 0: |
148 self.failures.append(self.tests[job_id]) | 161 self.failures.append(job_id) |
149 with open(self.outputs[job_id]) as f: | 162 with open(self.outputs[job_id]) as f: |
150 for line in f.readlines(): | 163 for line in f.readlines(): |
151 self.out.permanent_line(line.rstrip()) | 164 self.out.permanent_line(line.rstrip()) |
152 self.out.permanent_line( | 165 self.out.permanent_line( |
153 "[%d/%d] %s returned/aborted with exit code %d (%d ms)" | 166 "[%d/%d] %s returned/aborted with exit code %d (%d ms)" |
154 % (self.finished_tests, self.total_tests, test, exit_code, time_ms)) | 167 % (self.finished_tests, self.total_tests, test, exit_code, time_ms)) |
155 elif command == "TESTCNT": | 168 elif command == "TESTCNT": |
156 self.total_tests = int(arg.split(' ', 1)[1]) | 169 self.total_tests = int(arg.split(' ', 1)[1]) |
157 self.out.transient_line("[0/%d] Running tests..." % self.total_tests) | 170 self.out.transient_line("[0/%d] Running tests..." % self.total_tests) |
158 | 171 |
159 def logfile(self, job_id, name): | 172 def logfile(self, job_id, name): |
160 self.outputs[job_id] = name | 173 self.outputs[job_id] = name |
161 | 174 |
162 def log(self, line): | 175 def log(self, line): |
163 stdout_lock.acquire() | 176 stdout_lock.acquire() |
164 (prefix, output) = line.split(' ', 1) | 177 (prefix, output) = line.split(' ', 1) |
165 | 178 |
166 assert prefix[-1] == ':' | 179 assert prefix[-1] == ':' |
167 self.handle_meta(int(prefix[:-1]), output) | 180 self.handle_meta(int(prefix[:-1]), output) |
168 stdout_lock.release() | 181 stdout_lock.release() |
169 | 182 |
170 def end(self): | 183 def end(self): |
171 if self.failures: | 184 self.print_tests("FAILED TESTS", self.failures) |
172 self.out.permanent_line("FAILED TESTS (%d/%d):" | 185 interruptions = set(self.started) - set(self.finished) |
173 % (len(self.failures), self.total_tests)) | 186 self.print_tests("INTERRUPTED TESTS", interruptions) |
174 for (binary, test) in self.failures: | |
175 self.out.permanent_line(" " + binary + ": " + test) | |
176 self.out.flush_transient_output() | 187 self.out.flush_transient_output() |
177 | 188 |
178 class RawFormat: | 189 class RawFormat: |
179 def log(self, line): | 190 def log(self, line): |
180 stdout_lock.acquire() | 191 stdout_lock.acquire() |
181 sys.stdout.write(line + "\n") | 192 sys.stdout.write(line + "\n") |
182 sys.stdout.flush() | 193 sys.stdout.flush() |
183 stdout_lock.release() | 194 stdout_lock.release() |
184 def logfile(self, job_id, name): | 195 def logfile(self, job_id, name): |
185 with open(name) as f: | 196 with open(name) as f: |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
217 def dump_to_file_and_close(self): | 228 def dump_to_file_and_close(self): |
218 json.dump(self.test_results, self.json_dump_file) | 229 json.dump(self.test_results, self.json_dump_file) |
219 self.json_dump_file.close() | 230 self.json_dump_file.close() |
220 | 231 |
221 class IgnoreTestResults(object): | 232 class IgnoreTestResults(object): |
222 def log(self, test, result): | 233 def log(self, test, result): |
223 pass | 234 pass |
224 def dump_to_file_and_close(self): | 235 def dump_to_file_and_close(self): |
225 pass | 236 pass |
226 | 237 |
| 238 class DummyTimer(object): |
| 239 def start(self): |
| 240 pass |
| 241 def cancel(self): |
| 242 pass |
| 243 |
227 # Record of test runtimes. Has built-in locking. | 244 # Record of test runtimes. Has built-in locking. |
228 class TestTimes(object): | 245 class TestTimes(object): |
229 def __init__(self, save_file): | 246 def __init__(self, save_file): |
230 "Create new object seeded with saved test times from the given file." | 247 "Create new object seeded with saved test times from the given file." |
231 self.__times = {} # (test binary, test name) -> runtime in ms | 248 self.__times = {} # (test binary, test name) -> runtime in ms |
232 | 249 |
233 # Protects calls to record_test_time(); other calls are not | 250 # Protects calls to record_test_time(); other calls are not |
234 # expected to be made concurrently. | 251 # expected to be made concurrently. |
235 self.__lock = threading.Lock() | 252 self.__lock = threading.Lock() |
236 | 253 |
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
307 parser.add_option('--shard_count', type='int', default=1, | 324 parser.add_option('--shard_count', type='int', default=1, |
308 help='total number of shards (for sharding test execution ' | 325 help='total number of shards (for sharding test execution ' |
309 'between multiple machines)') | 326 'between multiple machines)') |
310 parser.add_option('--shard_index', type='int', default=0, | 327 parser.add_option('--shard_index', type='int', default=0, |
311 help='zero-indexed number identifying this shard (for ' | 328 help='zero-indexed number identifying this shard (for ' |
312 'sharding test execution between multiple machines)') | 329 'sharding test execution between multiple machines)') |
313 parser.add_option('--dump_json_test_results', type='string', default=None, | 330 parser.add_option('--dump_json_test_results', type='string', default=None, |
314 help='Saves the results of the tests as a JSON machine-' | 331 help='Saves the results of the tests as a JSON machine-' |
315 'readable file. The format of the file is specified at ' | 332 'readable file. The format of the file is specified at ' |
316 'https://www.chromium.org/developers/the-json-test-result
s-format') | 333 'https://www.chromium.org/developers/the-json-test-result
s-format') |
| 334 parser.add_option('--timeout', type='int', default=None, |
| 335 help='Interrupt all remaining processes after the given ' |
| 336 'time (in seconds).') |
317 | 337 |
318 (options, binaries) = parser.parse_args() | 338 (options, binaries) = parser.parse_args() |
319 | 339 |
320 if binaries == []: | 340 if binaries == []: |
321 parser.print_usage() | 341 parser.print_usage() |
322 sys.exit(1) | 342 sys.exit(1) |
323 | 343 |
324 logger = RawFormat() | 344 logger = RawFormat() |
325 if options.format == 'raw': | 345 if options.format == 'raw': |
326 pass | 346 pass |
327 elif options.format == 'filter': | 347 elif options.format == 'filter': |
328 logger = FilterFormat() | 348 logger = FilterFormat() |
329 else: | 349 else: |
330 parser.error("Unknown output format: " + options.format) | 350 parser.error("Unknown output format: " + options.format) |
331 | 351 |
332 if options.shard_count < 1: | 352 if options.shard_count < 1: |
333 parser.error("Invalid number of shards: %d. Must be at least 1." % | 353 parser.error("Invalid number of shards: %d. Must be at least 1." % |
334 options.shard_count) | 354 options.shard_count) |
335 if not (0 <= options.shard_index < options.shard_count): | 355 if not (0 <= options.shard_index < options.shard_count): |
336 parser.error("Invalid shard index: %d. Must be between 0 and %d " | 356 parser.error("Invalid shard index: %d. Must be between 0 and %d " |
337 "(less than the number of shards)." % | 357 "(less than the number of shards)." % |
338 (options.shard_index, options.shard_count - 1)) | 358 (options.shard_index, options.shard_count - 1)) |
339 | 359 |
| 360 timeout = (DummyTimer() if options.timeout is None |
| 361 else threading.Timer(options.timeout, sigint_handler.interrupt)) |
| 362 |
340 test_results = (IgnoreTestResults() if options.dump_json_test_results is None | 363 test_results = (IgnoreTestResults() if options.dump_json_test_results is None |
341 else CollectTestResults(options.dump_json_test_results)) | 364 else CollectTestResults(options.dump_json_test_results)) |
342 | 365 |
343 # Find tests. | 366 # Find tests. |
344 save_file = os.path.join(os.path.expanduser("~"), ".gtest-parallel-times") | 367 save_file = os.path.join(os.path.expanduser("~"), ".gtest-parallel-times") |
345 times = TestTimes(save_file) | 368 times = TestTimes(save_file) |
346 tests = [] | 369 tests = [] |
347 for test_binary in binaries: | 370 for test_binary in binaries: |
348 command = [test_binary] | 371 command = [test_binary] |
349 if options.gtest_also_run_disabled_tests: | 372 if options.gtest_also_run_disabled_tests: |
(...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
411 # Remove files from old test runs. | 434 # Remove files from old test runs. |
412 for logfile in os.listdir(options.output_dir): | 435 for logfile in os.listdir(options.output_dir): |
413 os.remove(os.path.join(options.output_dir, logfile)) | 436 os.remove(os.path.join(options.output_dir, logfile)) |
414 | 437 |
415 # Run the specified job. Return the elapsed time in milliseconds if | 438 # Run the specified job. Return the elapsed time in milliseconds if |
416 # the job succeeds, or None if the job fails. (This ensures that | 439 # the job succeeds, or None if the job fails. (This ensures that |
417 # failing tests will run first the next time.) | 440 # failing tests will run first the next time.) |
418 def run_job((command, job_id, test, test_index)): | 441 def run_job((command, job_id, test, test_index)): |
419 begin = time.time() | 442 begin = time.time() |
420 | 443 |
| 444 logger.log("%s: START " % job_id) |
421 test_name = re.sub('[^A-Za-z0-9]', '_', test) + '-' + str(test_index) + '.log' | 445 test_name = re.sub('[^A-Za-z0-9]', '_', test) + '-' + str(test_index) + '.log' |
422 with open(os.path.join(options.output_dir, test_name), 'w') as log: | 446 with open(os.path.join(options.output_dir, test_name), 'w') as log: |
423 sub = subprocess.Popen(command + ['--gtest_filter=' + test] + | 447 sub = subprocess.Popen(command + ['--gtest_filter=' + test] + |
424 ['--gtest_color=' + options.gtest_color], | 448 ['--gtest_color=' + options.gtest_color], |
425 stdout=log, stderr=log) | 449 stdout=log, stderr=log) |
426 try: | 450 try: |
427 code = sigint_handler.wait(sub) | 451 code = sigint_handler.wait(sub) |
428 except sigint_handler.ProcessWasInterrupted: | 452 except sigint_handler.ProcessWasInterrupted: |
429 thread.exit() | 453 thread.exit() |
430 runtime_ms = int(1000 * (time.time() - begin)) | 454 runtime_ms = int(1000 * (time.time() - begin)) |
(...skipping 25 matching lines...) Expand all Loading... |
456 if job is None: | 480 if job is None: |
457 return | 481 return |
458 times.record_test_time(test_binary, test, run_job(job)) | 482 times.record_test_time(test_binary, test, run_job(job)) |
459 | 483 |
460 def start_daemon(func): | 484 def start_daemon(func): |
461 t = threading.Thread(target=func) | 485 t = threading.Thread(target=func) |
462 t.daemon = True | 486 t.daemon = True |
463 t.start() | 487 t.start() |
464 return t | 488 return t |
465 | 489 |
466 workers = [start_daemon(worker) for i in range(options.workers)] | 490 try: |
| 491 timeout.start() |
| 492 workers = [start_daemon(worker) for i in range(options.workers)] |
| 493 [t.join() for t in workers] |
| 494 finally: |
| 495 timeout.cancel() |
467 | 496 |
468 [t.join() for t in workers] | |
469 logger.end() | 497 logger.end() |
470 times.write_to_file(save_file) | 498 times.write_to_file(save_file) |
471 if options.print_test_times: | 499 if options.print_test_times: |
472 ts = sorted((times.get_test_time(test_binary, test), test_binary, test) | 500 ts = sorted((times.get_test_time(test_binary, test), test_binary, test) |
473 for (_, test_binary, test, _) in tests | 501 for (_, test_binary, test, _) in tests |
474 if times.get_test_time(test_binary, test) is not None) | 502 if times.get_test_time(test_binary, test) is not None) |
475 for (time_ms, test_binary, test) in ts: | 503 for (time_ms, test_binary, test) in ts: |
476 print "%8s %s" % ("%dms" % time_ms, test) | 504 print "%8s %s" % ("%dms" % time_ms, test) |
477 | 505 |
478 test_results.dump_to_file_and_close() | 506 test_results.dump_to_file_and_close() |
479 sys.exit(-signal.SIGINT if sigint_handler.got_sigint() else exit_code) | 507 sys.exit(-signal.SIGINT if sigint_handler.got_sigint() else exit_code) |
OLD | NEW |