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 |