2 # Copyright (C) 2010 Google Inc. All rights reserved.
3 # Copyright (C) 2010 Gabor Rapcsanyi (rgabor@inf.u-szeged.hu), University of Szeged
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are
9 # * Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 # * Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following disclaimer
13 # in the documentation and/or other materials provided with the
15 # * Neither the name of Google Inc. nor the names of its
16 # contributors may be used to endorse or promote products derived from
17 # this software without specific prior written permission.
19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 """A Thread object for running DumpRenderTree and processing URLs from a
34 Each thread runs a separate instance of the DumpRenderTree binary and validates
35 the output. When there are no more URLs to process in the shared queue, the
39 from __future__ import with_statement
56 _log = logging.getLogger("webkitpy.layout_tests.layout_package."
57 "dump_render_tree_thread")
60 def find_thread_stack(id):
61 """Returns a stack object that can be used to dump a stack trace for
62 the given thread id (or None if the id is not found)."""
63 for thread_id, stack in sys._current_frames().items():
70 """Log a stack trace to log.error()."""
71 for filename, lineno, name, line in traceback.extract_stack(stack):
72 _log.error('File: "%s", line %d, in %s' % (filename, lineno, name))
74 _log.error(' %s' % line.strip())
77 def _process_output(port, options, test_info, test_types, test_args,
78 crash, timeout, test_run_time, actual_checksum,
80 """Receives the output from a DumpRenderTree process, subjects it to a
81 number of tests, and returns a list of failure types the test produced.
84 port: port-specific hooks
85 options: command line options argument from optparse
86 proc: an active DumpRenderTree process
87 test_info: Object containing the test filename, uri and timeout
88 test_types: list of test types to subject the output to
89 test_args: arguments to be passed to each test
91 Returns: a TestResult object
95 # Some test args, such as the image hash, may be added or changed on a
97 local_test_args = copy.copy(test_args)
99 local_test_args.hash = actual_checksum
102 failures.append(test_failures.FailureCrash())
104 failures.append(test_failures.FailureTimeout())
107 _log.debug("Stacktrace for %s:\n%s" % (test_info.filename, error))
108 # Strip off "file://" since RelativeTestFilename expects
110 filename = os.path.join(options.results_directory,
111 port.relative_test_filename(
113 filename = os.path.splitext(filename)[0] + "-stack.txt"
114 port.maybe_make_directory(os.path.split(filename)[0])
115 with codecs.open(filename, "wb", "utf-8") as file:
118 _log.debug("Previous test output stderr lines:\n%s" % error)
120 # Check the output and save the results.
121 start_time = time.time()
123 for test_type in test_types:
124 start_diff_time = time.time()
125 new_failures = test_type.compare_output(port, test_info.filename,
126 output, local_test_args,
127 options.configuration)
128 # Don't add any more failures if we already have a crash, so we don't
129 # double-report those tests. We do double-report for timeouts since
130 # we still want to see the text and image output.
132 failures.extend(new_failures)
133 time_for_diffs[test_type.__class__.__name__] = (
134 time.time() - start_diff_time)
136 total_time_for_all_diffs = time.time() - start_diff_time
137 return test_results.TestResult(test_info.filename, failures, test_run_time,
138 total_time_for_all_diffs, time_for_diffs)
141 def _pad_timeout(timeout):
142 """Returns a safe multiple of the per-test timeout value to use
143 to detect hung test threads.
146 # When we're running one test per DumpRenderTree process, we can
147 # enforce a hard timeout. The DumpRenderTree watchdog uses 2.5x
148 # the timeout; we want to be larger than that.
152 def _milliseconds_to_seconds(msecs):
153 return float(msecs) / 1000.0
156 def _image_hash(test_info, test_args, options):
157 """Returns the image hash of the test if it's needed, otherwise None."""
158 if (test_args.new_baseline or test_args.reset_results or not options.pixel_tests):
160 return test_info.image_hash()
163 class SingleTestThread(threading.Thread):
164 """Thread wrapper for running a single test file."""
166 def __init__(self, port, options, test_info, test_types, test_args):
169 port: object implementing port-specific hooks
170 options: command line argument object from optparse
171 test_info: Object containing the test filename, uri and timeout
172 test_types: A list of TestType objects to run the test output
174 test_args: A TestArguments object to pass to each TestType.
177 threading.Thread.__init__(self)
179 self._options = options
180 self._test_info = test_info
181 self._test_types = test_types
182 self._test_args = test_args
188 def _covered_run(self):
189 # FIXME: this is a separate routine to work around a bug
190 # in coverage: see http://bitbucket.org/ned/coveragepy/issue/85.
191 test_info = self._test_info
192 self._driver = self._port.create_driver(self._test_args.png_path,
195 image_hash = _image_hash(test_info, self._test_args, self._options)
197 crash, timeout, actual_checksum, output, error = \
198 self._driver.run_test(test_info.uri.strip(), test_info.timeout,
201 self._test_result = _process_output(self._port, self._options,
202 test_info, self._test_types, self._test_args,
203 crash, timeout, end - start,
204 actual_checksum, output, error)
207 def get_test_result(self):
208 return self._test_result
211 class WatchableThread(threading.Thread):
212 """This class abstracts an interface used by
213 run_webkit_tests.TestRunner._wait_for_threads_to_finish for thread
216 threading.Thread.__init__(self)
217 self._canceled = False
218 self._exception_info = None
219 self._next_timeout = None
220 self._thread_id = None
223 """Set a flag telling this thread to quit."""
224 self._canceled = True
226 def clear_next_timeout(self):
227 """Mark a flag telling this thread to stop setting timeouts."""
230 def exception_info(self):
231 """If run() terminated on an uncaught exception, return it here
232 ((type, value, traceback) tuple).
233 Returns None if run() terminated normally. Meant to be called after
234 joining this thread."""
235 return self._exception_info
238 """Return a thread identifier."""
239 return self._thread_id
241 def next_timeout(self):
242 """Return the time the test is supposed to finish by."""
243 return self._next_timeout
246 class TestShellThread(WatchableThread):
247 def __init__(self, port, options, filename_list_queue, result_queue,
248 test_types, test_args):
249 """Initialize all the local state for this DumpRenderTree thread.
252 port: interface to port-specific hooks
253 options: command line options argument from optparse
254 filename_list_queue: A thread safe Queue class that contains lists
255 of tuples of (filename, uri) pairs.
256 result_queue: A thread safe Queue class that will contain
257 serialized TestResult objects.
258 test_types: A list of TestType objects to run the test output
260 test_args: A TestArguments object to pass to each TestType.
263 WatchableThread.__init__(self)
265 self._options = options
266 self._filename_list_queue = filename_list_queue
267 self._result_queue = result_queue
268 self._filename_list = []
269 self._test_types = test_types
270 self._test_args = test_args
272 self._test_group_timing_stats = {}
273 self._test_results = []
277 self._have_http_lock = False
278 self._http_lock_wait_begin = 0
279 self._http_lock_wait_end = 0
281 # Current group of tests we're running.
282 self._current_group = None
283 # Number of tests in self._current_group.
284 self._num_tests_in_current_group = None
285 # Time at which we started running tests from self._current_group.
286 self._current_group_start_time = None
288 def get_test_group_timing_stats(self):
289 """Returns a dictionary mapping test group to a tuple of
290 (number of tests in that group, time to run the tests)"""
291 return self._test_group_timing_stats
293 def get_test_results(self):
294 """Return the list of all tests run on this thread.
296 This is used to calculate per-thread statistics.
299 return self._test_results
301 def get_total_time(self):
302 return max(self._stop_time - self._start_time -
303 self._http_lock_wait_time(), 0.0)
305 def get_num_tests(self):
306 return self._num_tests
309 """Delegate main work to a helper method and watch for uncaught
313 def _covered_run(self):
314 # FIXME: this is a separate routine to work around a bug
315 # in coverage: see http://bitbucket.org/ned/coveragepy/issue/85.
316 self._thread_id = thread.get_ident()
317 self._start_time = time.time()
320 _log.debug('%s starting' % (self.getName()))
321 self._run(test_runner=None, result_summary=None)
322 _log.debug('%s done (%d tests)' % (self.getName(),
323 self.get_num_tests()))
324 except KeyboardInterrupt:
325 self._exception_info = sys.exc_info()
326 _log.debug("%s interrupted" % self.getName())
328 # Save the exception for our caller to see.
329 self._exception_info = sys.exc_info()
330 self._stop_time = time.time()
331 _log.error('%s dying, exception raised' % self.getName())
333 self._stop_time = time.time()
335 def run_in_main_thread(self, test_runner, result_summary):
336 """This hook allows us to run the tests from the main thread if
337 --num-test-shells==1, instead of having to always run two or more
338 threads. This allows us to debug the test harness without having to
339 do multi-threaded debugging."""
340 self._run(test_runner, result_summary)
343 """Clean up http lock and set a flag telling this thread to quit."""
344 self._stop_servers_with_lock()
345 WatchableThread.cancel(self)
347 def next_timeout(self):
348 """Return the time the test is supposed to finish by."""
349 if self._next_timeout:
350 return self._next_timeout + self._http_lock_wait_time()
351 return self._next_timeout
353 def _http_lock_wait_time(self):
354 """Return the time what http locking takes."""
355 if self._http_lock_wait_begin == 0:
357 if self._http_lock_wait_end == 0:
358 return time.time() - self._http_lock_wait_begin
359 return self._http_lock_wait_end - self._http_lock_wait_begin
361 def _run(self, test_runner, result_summary):
362 """Main work entry point of the thread. Basically we pull urls from the
363 filename queue and run the tests until we run out of urls.
365 If test_runner is not None, then we call test_runner.UpdateSummary()
366 with the results of each test."""
367 batch_size = self._options.batch_size
370 # Append tests we're running to the existing tests_run.txt file.
371 # This is created in run_webkit_tests.py:_PrepareListsAndPrintOutput.
372 tests_run_filename = os.path.join(self._options.results_directory,
374 tests_run_file = codecs.open(tests_run_filename, "a", "utf-8")
378 _log.debug('Testing cancelled')
379 tests_run_file.close()
382 if len(self._filename_list) is 0:
383 if self._current_group is not None:
384 self._test_group_timing_stats[self._current_group] = \
385 (self._num_tests_in_current_group,
386 time.time() - self._current_group_start_time)
389 self._current_group, self._filename_list = \
390 self._filename_list_queue.get_nowait()
392 self._stop_servers_with_lock()
393 self._kill_dump_render_tree()
394 tests_run_file.close()
397 if self._current_group == "tests_to_http_lock":
398 self._start_servers_with_lock()
399 elif self._have_http_lock:
400 self._stop_servers_with_lock()
402 self._num_tests_in_current_group = len(self._filename_list)
403 self._current_group_start_time = time.time()
405 test_info = self._filename_list.pop()
407 # We have a url, run tests.
410 if self._options.run_singly:
411 result = self._run_test_singly(test_info)
413 result = self._run_test(test_info)
415 filename = test_info.filename
416 tests_run_file.write(filename + "\n")
418 # Check and kill DumpRenderTree if we need to.
419 if len([1 for f in result.failures
420 if f.should_kill_dump_render_tree()]):
421 self._kill_dump_render_tree()
422 # Reset the batch count since the shell just bounced.
424 # Print the error message(s).
425 error_str = '\n'.join([' ' + f.message() for
426 f in result.failures])
427 _log.debug("%s %s failed:\n%s" % (self.getName(),
428 self._port.relative_test_filename(filename),
431 _log.debug("%s %s passed" % (self.getName(),
432 self._port.relative_test_filename(filename)))
433 self._result_queue.put(result.dumps())
435 if batch_size > 0 and batch_count >= batch_size:
436 # Bounce the shell and reset count.
437 self._kill_dump_render_tree()
441 test_runner.update_summary(result_summary)
443 def _run_test_singly(self, test_info):
444 """Run a test in a separate thread, enforcing a hard time limit.
446 Since we can only detect the termination of a thread, not any internal
447 state or progress, we can only run per-test timeouts when running test
451 test_info: Object containing the test filename, uri and timeout
457 worker = SingleTestThread(self._port,
465 thread_timeout = _milliseconds_to_seconds(
466 _pad_timeout(int(test_info.timeout)))
467 thread._next_timeout = time.time() + thread_timeout
468 worker.join(thread_timeout)
470 # If join() returned with the thread still running, the
471 # DumpRenderTree is completely hung and there's nothing
472 # more we can do with it. We have to kill all the
473 # DumpRenderTrees to free it up. If we're running more than
474 # one DumpRenderTree thread, we'll end up killing the other
475 # DumpRenderTrees too, introducing spurious crashes. We accept
476 # that tradeoff in order to avoid losing the rest of this
478 _log.error('Test thread hung: killing all DumpRenderTrees')
480 worker._driver.stop()
483 result = worker.get_test_result()
484 except AttributeError, e:
485 # This gets raised if the worker thread has already exited.
487 _log.error('Cannot get results of test: %s' %
489 result = test_results.TestResult(test_info.filename, failures=[],
490 test_run_time=0, total_time_for_all_diffs=0, time_for_diffs=0)
494 def _run_test(self, test_info):
495 """Run a single test file using a shared DumpRenderTree process.
498 test_info: Object containing the test filename, uri and timeout
500 Returns: a TestResult object.
502 self._ensure_dump_render_tree_is_running()
503 # The pixel_hash is used to avoid doing an image dump if the
504 # checksums match, so it should be set to a blank value if we
505 # are generating a new baseline. (Otherwise, an image from a
506 # previous run will be copied into the baseline.)
507 image_hash = _image_hash(test_info, self._test_args, self._options)
510 thread_timeout = _milliseconds_to_seconds(
511 _pad_timeout(int(test_info.timeout)))
512 self._next_timeout = start + thread_timeout
514 crash, timeout, actual_checksum, output, error = \
515 self._driver.run_test(test_info.uri, test_info.timeout, image_hash)
518 result = _process_output(self._port, self._options,
519 test_info, self._test_types,
520 self._test_args, crash,
521 timeout, end - start, actual_checksum,
523 self._test_results.append(result)
526 def _ensure_dump_render_tree_is_running(self):
527 """Start the shared DumpRenderTree, if it's not running.
529 This is not for use when running tests singly, since those each start
530 a separate DumpRenderTree in their own thread.
533 # poll() is not threadsafe and can throw OSError due to:
534 # http://bugs.python.org/issue1731717
535 if (not self._driver or self._driver.poll() is not None):
536 self._driver = self._port.create_driver(self._test_args.png_path,
540 def _start_servers_with_lock(self):
541 """Acquire http lock and start the servers."""
542 self._http_lock_wait_begin = time.time()
543 _log.debug('Acquire http lock ...')
544 self._port.acquire_http_lock()
545 _log.debug('Starting HTTP server ...')
546 self._port.start_http_server()
547 _log.debug('Starting WebSocket server ...')
548 self._port.start_websocket_server()
549 self._http_lock_wait_end = time.time()
550 self._have_http_lock = True
552 def _stop_servers_with_lock(self):
553 """Stop the servers and release http lock."""
554 if self._have_http_lock:
555 _log.debug('Stopping HTTP server ...')
556 self._port.stop_http_server()
557 _log.debug('Stopping WebSocket server ...')
558 self._port.stop_websocket_server()
559 _log.debug('Release http lock ...')
560 self._port.release_http_lock()
561 self._have_http_lock = False
563 def _kill_dump_render_tree(self):
564 """Kill the DumpRenderTree process if it's running."""