Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """A Thread object for running the test shell and processing URLs from a | 5 """A Thread object for running the test shell and processing URLs from a |
| 6 shared queue. | 6 shared queue. |
| 7 | 7 |
| 8 Each thread runs a separate instance of the test_shell binary and validates | 8 Each thread runs a separate instance of the test_shell binary and validates |
| 9 the output. When there are no more URLs to process in the shared queue, the | 9 the output. When there are no more URLs to process in the shared queue, the |
| 10 thread exits. | 10 thread exits. |
| 11 """ | 11 """ |
| 12 | 12 |
| 13 import copy | 13 import copy |
| 14 import logging | 14 import logging |
| 15 import os | 15 import os |
| 16 import Queue | 16 import Queue |
| 17 import signal | 17 import signal |
| 18 import subprocess | 18 import subprocess |
| 19 import sys | 19 import sys |
| 20 import thread | 20 import thread |
| 21 import threading | 21 import threading |
| 22 import time | 22 import time |
| 23 | 23 |
| 24 import path_utils | 24 import path_utils |
| 25 import test_failures | 25 import test_failures |
| 26 | 26 |
| 27 def ProcessOutput(proc, test_info, test_types, test_args, target): | 27 def ProcessOutput(proc, test_info, test_types, test_args, target, output_dir): |
| 28 """Receives the output from a test_shell process, subjects it to a number | 28 """Receives the output from a test_shell process, subjects it to a number |
| 29 of tests, and returns a list of failure types the test produced. | 29 of tests, and returns a list of failure types the test produced. |
| 30 | 30 |
| 31 Args: | 31 Args: |
| 32 proc: an active test_shell process | 32 proc: an active test_shell process |
| 33 test_info: Object containing the test filename, uri and timeout | 33 test_info: Object containing the test filename, uri and timeout |
| 34 test_types: list of test types to subject the output to | 34 test_types: list of test types to subject the output to |
| 35 test_args: arguments to be passed to each test | 35 test_args: arguments to be passed to each test |
| 36 target: Debug or Release | 36 target: Debug or Release |
| 37 output_dir: directory to put crash stack traces into | |
| 37 | 38 |
| 38 Returns: a list of failure objects and times for the test being processed | 39 Returns: a list of failure objects and times for the test being processed |
| 39 """ | 40 """ |
| 40 outlines = [] | 41 outlines = [] |
| 41 extra_lines = [] | 42 extra_lines = [] |
| 42 failures = [] | 43 failures = [] |
| 43 crash = False | 44 crash = False |
| 44 | 45 |
| 45 # Some test args, such as the image hash, may be added or changed on a | 46 # Some test args, such as the image hash, may be added or changed on a |
| 46 # test-by-test basis. | 47 # test-by-test basis. |
| 47 local_test_args = copy.copy(test_args) | 48 local_test_args = copy.copy(test_args) |
| 48 | 49 |
| 49 start_time = time.time() | 50 start_time = time.time() |
| 50 | 51 |
| 51 line = proc.stdout.readline() | 52 line = proc.stdout.readline() |
| 52 | 53 |
| 53 # Only start saving output lines once we've loaded the URL for the test. | 54 # Only start saving output lines once we've loaded the URL for the test. |
| 54 hit_load_url = False | 55 url = None |
| 56 test_string = test_info.uri.strip() | |
| 55 | 57 |
| 56 while line.rstrip() != "#EOF": | 58 while line.rstrip() != "#EOF": |
| 57 # Make sure we haven't crashed. | 59 # Make sure we haven't crashed. |
| 58 if line == '' and proc.poll() is not None: | 60 if line == '' and proc.poll() is not None: |
| 59 failures.append(test_failures.FailureCrash()) | 61 failures.append(test_failures.FailureCrash()) |
| 60 | 62 |
| 61 # This is hex code 0xc000001d, which is used for abrupt termination. | 63 # This is hex code 0xc000001d, which is used for abrupt termination. |
| 62 # This happens if we hit ctrl+c from the prompt and we happen to | 64 # This happens if we hit ctrl+c from the prompt and we happen to |
| 63 # be waiting on the test_shell. | 65 # be waiting on the test_shell. |
| 64 # sdoyon: Not sure for which OS and in what circumstances the | 66 # sdoyon: Not sure for which OS and in what circumstances the |
| 65 # above code is valid. What works for me under Linux to detect | 67 # above code is valid. What works for me under Linux to detect |
| 66 # ctrl+c is for the subprocess returncode to be negative SIGINT. And | 68 # ctrl+c is for the subprocess returncode to be negative SIGINT. And |
| 67 # that agrees with the subprocess documentation. | 69 # that agrees with the subprocess documentation. |
| 68 if (-1073741510 == proc.returncode or | 70 if (-1073741510 == proc.returncode or |
| 69 -signal.SIGINT == proc.returncode): | 71 -signal.SIGINT == proc.returncode): |
| 70 raise KeyboardInterrupt | 72 raise KeyboardInterrupt |
| 71 crash = True | 73 crash = True |
| 72 break | 74 break |
| 73 | 75 |
| 74 # Don't include #URL lines in our output | 76 # Don't include #URL lines in our output |
| 75 if line.startswith("#URL:"): | 77 if line.startswith("#URL:"): |
| 76 hit_load_url = True | |
| 77 test_string = test_info.uri.strip() | |
| 78 url = line.rstrip()[5:] | 78 url = line.rstrip()[5:] |
| 79 if url != test_string: | 79 if url != test_string: |
| 80 logging.fatal("Test got out of sync:\n|%s|\n|%s|" % | 80 logging.fatal("Test got out of sync:\n|%s|\n|%s|" % |
| 81 (url, test_string)) | 81 (url, test_string)) |
| 82 raise AssertionError("test out of sync") | 82 raise AssertionError("test out of sync") |
| 83 elif line.startswith("#MD5:"): | 83 elif line.startswith("#MD5:"): |
| 84 local_test_args.hash = line.rstrip()[5:] | 84 local_test_args.hash = line.rstrip()[5:] |
| 85 elif line.startswith("#TEST_TIMED_OUT"): | 85 elif line.startswith("#TEST_TIMED_OUT"): |
| 86 # Test timed out, but we still need to read until #EOF. | 86 # Test timed out, but we still need to read until #EOF. |
| 87 failures.append(test_failures.FailureTimeout()) | 87 failures.append(test_failures.FailureTimeout()) |
| 88 elif hit_load_url: | 88 elif url: |
| 89 outlines.append(line) | 89 outlines.append(line) |
| 90 else: | 90 else: |
| 91 extra_lines.append(line) | 91 extra_lines.append(line) |
| 92 | 92 |
| 93 line = proc.stdout.readline() | 93 line = proc.stdout.readline() |
| 94 | 94 |
| 95 end_test_time = time.time() | 95 end_test_time = time.time() |
| 96 | 96 |
| 97 if len(extra_lines): | 97 if len(extra_lines): |
| 98 logging.warning("Previous test output extra lines after dump:\n%s" % ( | 98 extra = "".join(extra_lines) |
| 99 "".join(extra_lines))) | 99 if crash: |
| 100 logging.info("Stacktrace for %s:\n%s" % (test_string, extra)) | |
| 101 # Strip off "file://" since RelativeTestFilename expects filesystem paths. | |
| 102 filename = os.path.join(output_dir, | |
| 103 path_utils.RelativeTestFilename(test_string[7:])) | |
| 104 filename = os.path.splitext(filename)[0] + "-stack.txt" | |
| 105 path_utils.MaybeMakeDirectory(os.path.split(filename)[0]) | |
| 106 open(filename, "wb").write(extra) | |
| 107 else: | |
| 108 logging.warning("Previous test output extra lines after dump:\n%s" % | |
| 109 extra) | |
| 100 | 110 |
| 101 # Check the output and save the results. | 111 # Check the output and save the results. |
| 102 time_for_diffs = {} | 112 time_for_diffs = {} |
| 103 for test_type in test_types: | 113 for test_type in test_types: |
| 104 start_diff_time = time.time() | 114 start_diff_time = time.time() |
| 105 new_failures = test_type.CompareOutput(test_info.filename, | 115 new_failures = test_type.CompareOutput(test_info.filename, |
| 106 proc, | 116 proc, |
| 107 ''.join(outlines), | 117 ''.join(outlines), |
| 108 local_test_args, | 118 local_test_args, |
| 109 target) | 119 target) |
| (...skipping 30 matching lines...) Expand all Loading... | |
| 140 total_time_for_all_diffs, time_for_diffs): | 150 total_time_for_all_diffs, time_for_diffs): |
| 141 self.filename = filename | 151 self.filename = filename |
| 142 self.failures = failures | 152 self.failures = failures |
| 143 self.test_run_time = test_run_time | 153 self.test_run_time = test_run_time |
| 144 self.total_time_for_all_diffs = total_time_for_all_diffs | 154 self.total_time_for_all_diffs = total_time_for_all_diffs |
| 145 self.time_for_diffs = time_for_diffs | 155 self.time_for_diffs = time_for_diffs |
| 146 | 156 |
| 147 class SingleTestThread(threading.Thread): | 157 class SingleTestThread(threading.Thread): |
| 148 """Thread wrapper for running a single test file.""" | 158 """Thread wrapper for running a single test file.""" |
| 149 def __init__(self, test_shell_command, shell_args, test_info, test_types, | 159 def __init__(self, test_shell_command, shell_args, test_info, test_types, |
| 150 test_args, target): | 160 test_args, target, output_dir): |
| 151 """ | 161 """ |
| 152 Args: | 162 Args: |
| 153 test_info: Object containing the test filename, uri and timeout | 163 test_info: Object containing the test filename, uri and timeout |
| 154 See TestShellThread for documentation of the remaining arguments. | 164 See TestShellThread for documentation of the remaining arguments. |
| 155 """ | 165 """ |
| 156 | 166 |
| 157 threading.Thread.__init__(self) | 167 threading.Thread.__init__(self) |
| 158 self._command = test_shell_command | 168 self._command = test_shell_command |
| 159 self._shell_args = shell_args | 169 self._shell_args = shell_args |
| 160 self._test_info = test_info | 170 self._test_info = test_info |
| 161 self._test_types = test_types | 171 self._test_types = test_types |
| 162 self._test_args = test_args | 172 self._test_args = test_args |
| 163 self._target = target | 173 self._target = target |
| 174 self._output_dir = output_dir | |
| 164 | 175 |
| 165 def run(self): | 176 def run(self): |
| 166 proc = StartTestShell(self._command, self._shell_args + | 177 proc = StartTestShell(self._command, self._shell_args + |
| 167 ["--time-out-ms=" + self._test_info.timeout, self._test_info.uri]) | 178 ["--time-out-ms=" + self._test_info.timeout, self._test_info.uri]) |
| 168 self._test_stats = ProcessOutput(proc, self._test_info, self._test_types, | 179 self._test_stats = ProcessOutput(proc, self._test_info, self._test_types, |
| 169 self._test_args, self._target) | 180 self._test_args, self._target, self._output_dir) |
| 170 | 181 |
| 171 def GetTestStats(self): | 182 def GetTestStats(self): |
| 172 return self._test_stats | 183 return self._test_stats |
| 173 | 184 |
| 174 class TestShellThread(threading.Thread): | 185 class TestShellThread(threading.Thread): |
| 175 | 186 |
| 176 def __init__(self, filename_list_queue, test_shell_command, test_types, | 187 def __init__(self, filename_list_queue, test_shell_command, test_types, |
| 177 test_args, shell_args, options): | 188 test_args, shell_args, output_dir, options): |
| 178 """Initialize all the local state for this test shell thread. | 189 """Initialize all the local state for this test shell thread. |
| 179 | 190 |
| 180 Args: | 191 Args: |
| 181 filename_list_queue: A thread safe Queue class that contains lists of | 192 filename_list_queue: A thread safe Queue class that contains lists of |
| 182 tuples of (filename, uri) pairs. | 193 tuples of (filename, uri) pairs. |
| 183 test_shell_command: A list specifying the command+args for test_shell | 194 test_shell_command: A list specifying the command+args for test_shell |
| 184 test_types: A list of TestType objects to run the test output against. | 195 test_types: A list of TestType objects to run the test output against. |
| 185 test_args: A TestArguments object to pass to each TestType. | 196 test_args: A TestArguments object to pass to each TestType. |
| 186 shell_args: Any extra arguments to be passed to test_shell.exe. | 197 shell_args: Any extra arguments to be passed to test_shell.exe. |
| 198 output_dir: Directory to put crash stacks into. | |
| 187 options: A property dictionary as produced by optparse. The command-line | 199 options: A property dictionary as produced by optparse. The command-line |
| 188 options should match those expected by run_webkit_tests; they | 200 options should match those expected by run_webkit_tests; they |
| 189 are typically passed via the run_webkit_tests.TestRunner class. | 201 are typically passed via the run_webkit_tests.TestRunner class. |
| 190 """ | 202 """ |
| 191 threading.Thread.__init__(self) | 203 threading.Thread.__init__(self) |
| 192 self._filename_list_queue = filename_list_queue | 204 self._filename_list_queue = filename_list_queue |
| 193 self._filename_list = [] | 205 self._filename_list = [] |
| 194 self._test_shell_command = test_shell_command | 206 self._test_shell_command = test_shell_command |
| 195 self._test_types = test_types | 207 self._test_types = test_types |
| 196 self._test_args = test_args | 208 self._test_args = test_args |
| 197 self._test_shell_proc = None | 209 self._test_shell_proc = None |
| 198 self._shell_args = shell_args | 210 self._shell_args = shell_args |
| 211 self._output_dir = output_dir | |
|
tony
2009/10/29 23:13:51
Nit: Maybe just use options.results_directory rath
| |
| 199 self._options = options | 212 self._options = options |
| 200 self._failures = {} | 213 self._failures = {} |
| 201 self._canceled = False | 214 self._canceled = False |
| 202 self._exception_info = None | 215 self._exception_info = None |
| 203 self._directory_timing_stats = {} | 216 self._directory_timing_stats = {} |
| 204 self._test_stats = [] | 217 self._test_stats = [] |
| 205 self._num_tests = 0 | 218 self._num_tests = 0 |
| 206 self._start_time = 0 | 219 self._start_time = 0 |
| 207 self._stop_time = 0 | 220 self._stop_time = 0 |
| 208 | 221 |
| (...skipping 141 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 350 test_info: Object containing the test filename, uri and timeout | 363 test_info: Object containing the test filename, uri and timeout |
| 351 | 364 |
| 352 Return: | 365 Return: |
| 353 A list of TestFailure objects describing the error. | 366 A list of TestFailure objects describing the error. |
| 354 """ | 367 """ |
| 355 worker = SingleTestThread(self._test_shell_command, | 368 worker = SingleTestThread(self._test_shell_command, |
| 356 self._shell_args, | 369 self._shell_args, |
| 357 test_info, | 370 test_info, |
| 358 self._test_types, | 371 self._test_types, |
| 359 self._test_args, | 372 self._test_args, |
| 360 self._options.target) | 373 self._options.target, |
| 374 self._output_dir) | |
| 361 | 375 |
| 362 worker.start() | 376 worker.start() |
| 363 | 377 |
| 364 # When we're running one test per test_shell process, we can enforce | 378 # When we're running one test per test_shell process, we can enforce |
| 365 # a hard timeout. the test_shell watchdog uses 2.5x the timeout | 379 # a hard timeout. the test_shell watchdog uses 2.5x the timeout |
| 366 # We want to be larger than that. | 380 # We want to be larger than that. |
| 367 worker.join(int(test_info.timeout) * 3.0 / 1000.0) | 381 worker.join(int(test_info.timeout) * 3.0 / 1000.0) |
| 368 if worker.isAlive(): | 382 if worker.isAlive(): |
| 369 # If join() returned with the thread still running, the test_shell.exe is | 383 # If join() returned with the thread still running, the test_shell.exe is |
| 370 # completely hung and there's nothing more we can do with it. We have | 384 # completely hung and there's nothing more we can do with it. We have |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 409 | 423 |
| 410 # If the test shell is dead, the above may cause an IOError as we | 424 # If the test shell is dead, the above may cause an IOError as we |
| 411 # try to write onto the broken pipe. If this is the first test for | 425 # try to write onto the broken pipe. If this is the first test for |
| 412 # this test shell process, than the test shell did not | 426 # this test shell process, than the test shell did not |
| 413 # successfully start. If this is not the first test, then the | 427 # successfully start. If this is not the first test, then the |
| 414 # previous tests have caused some kind of delayed crash. We don't | 428 # previous tests have caused some kind of delayed crash. We don't |
| 415 # try to recover here. | 429 # try to recover here. |
| 416 self._test_shell_proc.stdin.flush() | 430 self._test_shell_proc.stdin.flush() |
| 417 | 431 |
| 418 stats = ProcessOutput(self._test_shell_proc, test_info, self._test_types, | 432 stats = ProcessOutput(self._test_shell_proc, test_info, self._test_types, |
| 419 self._test_args, self._options.target) | 433 self._test_args, self._options.target, self._output_dir) |
| 420 | 434 |
| 421 self._test_stats.append(stats) | 435 self._test_stats.append(stats) |
| 422 return stats.failures | 436 return stats.failures |
| 423 | 437 |
| 424 | 438 |
| 425 def _EnsureTestShellIsRunning(self): | 439 def _EnsureTestShellIsRunning(self): |
| 426 """Start the shared test shell, if it's not running. Not for use when | 440 """Start the shared test shell, if it's not running. Not for use when |
| 427 running tests singly, since those each start a separate test shell in | 441 running tests singly, since those each start a separate test shell in |
| 428 their own thread. | 442 their own thread. |
| 429 """ | 443 """ |
| 430 if (not self._test_shell_proc or | 444 if (not self._test_shell_proc or |
| 431 self._test_shell_proc.poll() is not None): | 445 self._test_shell_proc.poll() is not None): |
| 432 self._test_shell_proc = StartTestShell(self._test_shell_command, | 446 self._test_shell_proc = StartTestShell(self._test_shell_command, |
| 433 self._shell_args) | 447 self._shell_args) |
| 434 | 448 |
| 435 def _KillTestShell(self): | 449 def _KillTestShell(self): |
| 436 """Kill the test shell process if it's running.""" | 450 """Kill the test shell process if it's running.""" |
| 437 if self._test_shell_proc: | 451 if self._test_shell_proc: |
| 438 self._test_shell_proc.stdin.close() | 452 self._test_shell_proc.stdin.close() |
| 439 self._test_shell_proc.stdout.close() | 453 self._test_shell_proc.stdout.close() |
| 440 if self._test_shell_proc.stderr: | 454 if self._test_shell_proc.stderr: |
| 441 self._test_shell_proc.stderr.close() | 455 self._test_shell_proc.stderr.close() |
| 442 if sys.platform not in ('win32', 'cygwin'): | 456 if sys.platform not in ('win32', 'cygwin'): |
| 443 # Closing stdin/stdout/stderr hangs sometimes on OS X. | 457 # Closing stdin/stdout/stderr hangs sometimes on OS X. |
| 444 subprocess.Popen(["kill", "-9", str(self._test_shell_proc.pid)]) | 458 subprocess.Popen(["kill", "-9", str(self._test_shell_proc.pid)]) |
| 445 self._test_shell_proc = None | 459 self._test_shell_proc = None |
| OLD | NEW |