Index: webkit/tools/layout_tests/layout_package/test_shell_thread.py |
=================================================================== |
--- webkit/tools/layout_tests/layout_package/test_shell_thread.py (revision 36724) |
+++ webkit/tools/layout_tests/layout_package/test_shell_thread.py (working copy) |
@@ -1,488 +0,0 @@ |
-# Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. |
-# Use of this source code is governed by a BSD-style license that can be |
-# found in the LICENSE file. |
- |
-"""A Thread object for running the test shell and processing URLs from a |
-shared queue. |
- |
-Each thread runs a separate instance of the test_shell binary and validates |
-the output. When there are no more URLs to process in the shared queue, the |
-thread exits. |
-""" |
- |
-import copy |
-import logging |
-import os |
-import Queue |
-import signal |
-import subprocess |
-import sys |
-import thread |
-import threading |
-import time |
- |
-import path_utils |
-import test_failures |
- |
- |
-def ProcessOutput(proc, test_info, test_types, test_args, target, output_dir): |
- """Receives the output from a test_shell process, subjects it to a number |
- of tests, and returns a list of failure types the test produced. |
- |
- Args: |
- proc: an active test_shell process |
- test_info: Object containing the test filename, uri and timeout |
- test_types: list of test types to subject the output to |
- test_args: arguments to be passed to each test |
- target: Debug or Release |
- output_dir: directory to put crash stack traces into |
- |
- Returns: a list of failure objects and times for the test being processed |
- """ |
- outlines = [] |
- extra_lines = [] |
- failures = [] |
- crash = False |
- |
- # Some test args, such as the image hash, may be added or changed on a |
- # test-by-test basis. |
- local_test_args = copy.copy(test_args) |
- |
- start_time = time.time() |
- |
- line = proc.stdout.readline() |
- |
- # Only start saving output lines once we've loaded the URL for the test. |
- url = None |
- test_string = test_info.uri.strip() |
- |
- while line.rstrip() != "#EOF": |
- # Make sure we haven't crashed. |
- if line == '' and proc.poll() is not None: |
- failures.append(test_failures.FailureCrash()) |
- |
- # This is hex code 0xc000001d, which is used for abrupt |
- # termination. This happens if we hit ctrl+c from the prompt and |
- # we happen to be waiting on the test_shell. |
- # sdoyon: Not sure for which OS and in what circumstances the |
- # above code is valid. What works for me under Linux to detect |
- # ctrl+c is for the subprocess returncode to be negative SIGINT. |
- # And that agrees with the subprocess documentation. |
- if (-1073741510 == proc.returncode or |
- - signal.SIGINT == proc.returncode): |
- raise KeyboardInterrupt |
- crash = True |
- break |
- |
- # Don't include #URL lines in our output |
- if line.startswith("#URL:"): |
- url = line.rstrip()[5:] |
- if url != test_string: |
- logging.fatal("Test got out of sync:\n|%s|\n|%s|" % |
- (url, test_string)) |
- raise AssertionError("test out of sync") |
- elif line.startswith("#MD5:"): |
- local_test_args.hash = line.rstrip()[5:] |
- elif line.startswith("#TEST_TIMED_OUT"): |
- # Test timed out, but we still need to read until #EOF. |
- failures.append(test_failures.FailureTimeout()) |
- elif url: |
- outlines.append(line) |
- else: |
- extra_lines.append(line) |
- |
- line = proc.stdout.readline() |
- |
- end_test_time = time.time() |
- |
- if len(extra_lines): |
- extra = "".join(extra_lines) |
- if crash: |
- logging.debug("Stacktrace for %s:\n%s" % (test_string, extra)) |
- # Strip off "file://" since RelativeTestFilename expects |
- # filesystem paths. |
- filename = os.path.join(output_dir, |
- path_utils.RelativeTestFilename(test_string[7:])) |
- filename = os.path.splitext(filename)[0] + "-stack.txt" |
- path_utils.MaybeMakeDirectory(os.path.split(filename)[0]) |
- open(filename, "wb").write(extra) |
- else: |
- logging.debug("Previous test output extra lines after dump:\n%s" % |
- extra) |
- |
- # Check the output and save the results. |
- time_for_diffs = {} |
- for test_type in test_types: |
- start_diff_time = time.time() |
- new_failures = test_type.CompareOutput(test_info.filename, |
- proc, |
- ''.join(outlines), |
- local_test_args, |
- target) |
- # Don't add any more failures if we already have a crash, so we don't |
- # double-report those tests. We do double-report for timeouts since |
- # we still want to see the text and image output. |
- if not crash: |
- failures.extend(new_failures) |
- time_for_diffs[test_type.__class__.__name__] = ( |
- time.time() - start_diff_time) |
- |
- total_time_for_all_diffs = time.time() - end_test_time |
- test_run_time = end_test_time - start_time |
- return TestStats(test_info.filename, failures, test_run_time, |
- total_time_for_all_diffs, time_for_diffs) |
- |
- |
-def StartTestShell(command, args): |
- """Returns the process for a new test_shell started in layout-tests mode. |
- """ |
- cmd = [] |
- # Hook for injecting valgrind or other runtime instrumentation, |
- # used by e.g. tools/valgrind/valgrind_tests.py. |
- wrapper = os.environ.get("BROWSER_WRAPPER", None) |
- if wrapper != None: |
- cmd += [wrapper] |
- cmd += command + ['--layout-tests'] + args |
- return subprocess.Popen(cmd, |
- stdin=subprocess.PIPE, |
- stdout=subprocess.PIPE, |
- stderr=subprocess.STDOUT) |
- |
- |
-class TestStats: |
- |
- def __init__(self, filename, failures, test_run_time, |
- total_time_for_all_diffs, time_for_diffs): |
- self.filename = filename |
- self.failures = failures |
- self.test_run_time = test_run_time |
- self.total_time_for_all_diffs = total_time_for_all_diffs |
- self.time_for_diffs = time_for_diffs |
- |
- |
-class SingleTestThread(threading.Thread): |
- """Thread wrapper for running a single test file.""" |
- |
- def __init__(self, test_shell_command, shell_args, test_info, test_types, |
- test_args, target, output_dir): |
- """ |
- Args: |
- test_info: Object containing the test filename, uri and timeout |
- output_dir: Directory to put crash stacks into. |
- See TestShellThread for documentation of the remaining arguments. |
- """ |
- |
- threading.Thread.__init__(self) |
- self._command = test_shell_command |
- self._shell_args = shell_args |
- self._test_info = test_info |
- self._test_types = test_types |
- self._test_args = test_args |
- self._target = target |
- self._output_dir = output_dir |
- |
- def run(self): |
- proc = StartTestShell(self._command, self._shell_args + |
- ["--time-out-ms=" + self._test_info.timeout, self._test_info.uri]) |
- self._test_stats = ProcessOutput(proc, self._test_info, |
- self._test_types, self._test_args, self._target, self._output_dir) |
- |
- def GetTestStats(self): |
- return self._test_stats |
- |
- |
-class TestShellThread(threading.Thread): |
- |
- def __init__(self, filename_list_queue, result_queue, test_shell_command, |
- test_types, test_args, shell_args, options): |
- """Initialize all the local state for this test shell thread. |
- |
- Args: |
- filename_list_queue: A thread safe Queue class that contains lists |
- of tuples of (filename, uri) pairs. |
- result_queue: A thread safe Queue class that will contain tuples of |
- (test, failure lists) for the test results. |
- test_shell_command: A list specifying the command+args for |
- test_shell |
- test_types: A list of TestType objects to run the test output |
- against. |
- test_args: A TestArguments object to pass to each TestType. |
- shell_args: Any extra arguments to be passed to test_shell.exe. |
- options: A property dictionary as produced by optparse. The |
- command-line options should match those expected by |
- run_webkit_tests; they are typically passed via the |
- run_webkit_tests.TestRunner class.""" |
- threading.Thread.__init__(self) |
- self._filename_list_queue = filename_list_queue |
- self._result_queue = result_queue |
- self._filename_list = [] |
- self._test_shell_command = test_shell_command |
- self._test_types = test_types |
- self._test_args = test_args |
- self._test_shell_proc = None |
- self._shell_args = shell_args |
- self._options = options |
- self._canceled = False |
- self._exception_info = None |
- self._directory_timing_stats = {} |
- self._test_stats = [] |
- self._num_tests = 0 |
- self._start_time = 0 |
- self._stop_time = 0 |
- |
- # Current directory of tests we're running. |
- self._current_dir = None |
- # Number of tests in self._current_dir. |
- self._num_tests_in_current_dir = None |
- # Time at which we started running tests from self._current_dir. |
- self._current_dir_start_time = None |
- |
- def GetDirectoryTimingStats(self): |
- """Returns a dictionary mapping test directory to a tuple of |
- (number of tests in that directory, time to run the tests)""" |
- return self._directory_timing_stats |
- |
- def GetIndividualTestStats(self): |
- """Returns a list of (test_filename, time_to_run_test, |
- total_time_for_all_diffs, time_for_diffs) tuples.""" |
- return self._test_stats |
- |
- def Cancel(self): |
- """Set a flag telling this thread to quit.""" |
- self._canceled = True |
- |
- def GetExceptionInfo(self): |
- """If run() terminated on an uncaught exception, return it here |
- ((type, value, traceback) tuple). |
- Returns None if run() terminated normally. Meant to be called after |
- joining this thread.""" |
- return self._exception_info |
- |
- def GetTotalTime(self): |
- return max(self._stop_time - self._start_time, 0.0) |
- |
- def GetNumTests(self): |
- return self._num_tests |
- |
- def run(self): |
- """Delegate main work to a helper method and watch for uncaught |
- exceptions.""" |
- self._start_time = time.time() |
- self._num_tests = 0 |
- try: |
- logging.debug('%s starting' % (self.getName())) |
- self._Run(test_runner=None, result_summary=None) |
- logging.debug('%s done (%d tests)' % (self.getName(), |
- self.GetNumTests())) |
- except: |
- # Save the exception for our caller to see. |
- self._exception_info = sys.exc_info() |
- self._stop_time = time.time() |
- # Re-raise it and die. |
- logging.error('%s dying: %s' % (self.getName(), |
- self._exception_info)) |
- raise |
- self._stop_time = time.time() |
- |
- def RunInMainThread(self, test_runner, result_summary): |
- """This hook allows us to run the tests from the main thread if |
- --num-test-shells==1, instead of having to always run two or more |
- threads. This allows us to debug the test harness without having to |
- do multi-threaded debugging.""" |
- self._Run(test_runner, result_summary) |
- |
- def _Run(self, test_runner, result_summary): |
- """Main work entry point of the thread. Basically we pull urls from the |
- filename queue and run the tests until we run out of urls. |
- |
- If test_runner is not None, then we call test_runner.UpdateSummary() |
- with the results of each test.""" |
- batch_size = 0 |
- batch_count = 0 |
- if self._options.batch_size: |
- try: |
- batch_size = int(self._options.batch_size) |
- except: |
- logging.info("Ignoring invalid batch size '%s'" % |
- self._options.batch_size) |
- |
- # Append tests we're running to the existing tests_run.txt file. |
- # This is created in run_webkit_tests.py:_PrepareListsAndPrintOutput. |
- tests_run_filename = os.path.join(self._options.results_directory, |
- "tests_run.txt") |
- tests_run_file = open(tests_run_filename, "a") |
- |
- while True: |
- if self._canceled: |
- logging.info('Testing canceled') |
- tests_run_file.close() |
- return |
- |
- if len(self._filename_list) is 0: |
- if self._current_dir is not None: |
- self._directory_timing_stats[self._current_dir] = \ |
- (self._num_tests_in_current_dir, |
- time.time() - self._current_dir_start_time) |
- |
- try: |
- self._current_dir, self._filename_list = \ |
- self._filename_list_queue.get_nowait() |
- except Queue.Empty: |
- self._KillTestShell() |
- tests_run_file.close() |
- return |
- |
- self._num_tests_in_current_dir = len(self._filename_list) |
- self._current_dir_start_time = time.time() |
- |
- test_info = self._filename_list.pop() |
- |
- # We have a url, run tests. |
- batch_count += 1 |
- self._num_tests += 1 |
- if self._options.run_singly: |
- failures = self._RunTestSingly(test_info) |
- else: |
- failures = self._RunTest(test_info) |
- |
- filename = test_info.filename |
- tests_run_file.write(filename + "\n") |
- if failures: |
- # Check and kill test shell if we need too. |
- if len([1 for f in failures if f.ShouldKillTestShell()]): |
- self._KillTestShell() |
- # Reset the batch count since the shell just bounced. |
- batch_count = 0 |
- # Print the error message(s). |
- error_str = '\n'.join([' ' + f.Message() for f in failures]) |
- logging.debug("%s %s failed:\n%s" % (self.getName(), |
- path_utils.RelativeTestFilename(filename), |
- error_str)) |
- else: |
- logging.debug("%s %s passed" % (self.getName(), |
- path_utils.RelativeTestFilename(filename))) |
- self._result_queue.put((filename, failures)) |
- |
- if batch_size > 0 and batch_count > batch_size: |
- # Bounce the shell and reset count. |
- self._KillTestShell() |
- batch_count = 0 |
- |
- if test_runner: |
- test_runner.UpdateSummary(result_summary) |
- |
- def _RunTestSingly(self, test_info): |
- """Run a test in a separate thread, enforcing a hard time limit. |
- |
- Since we can only detect the termination of a thread, not any internal |
- state or progress, we can only run per-test timeouts when running test |
- files singly. |
- |
- Args: |
- test_info: Object containing the test filename, uri and timeout |
- |
- Return: |
- A list of TestFailure objects describing the error. |
- """ |
- worker = SingleTestThread(self._test_shell_command, |
- self._shell_args, |
- test_info, |
- self._test_types, |
- self._test_args, |
- self._options.target, |
- self._options.results_directory) |
- |
- worker.start() |
- |
- # When we're running one test per test_shell process, we can enforce |
- # a hard timeout. the test_shell watchdog uses 2.5x the timeout |
- # We want to be larger than that. |
- worker.join(int(test_info.timeout) * 3.0 / 1000.0) |
- if worker.isAlive(): |
- # If join() returned with the thread still running, the |
- # test_shell.exe is completely hung and there's nothing |
- # more we can do with it. We have to kill all the |
- # test_shells to free it up. If we're running more than |
- # one test_shell thread, we'll end up killing the other |
- # test_shells too, introducing spurious crashes. We accept that |
- # tradeoff in order to avoid losing the rest of this thread's |
- # results. |
- logging.error('Test thread hung: killing all test_shells') |
- path_utils.KillAllTestShells() |
- |
- try: |
- stats = worker.GetTestStats() |
- self._test_stats.append(stats) |
- failures = stats.failures |
- except AttributeError, e: |
- failures = [] |
- logging.error('Cannot get results of test: %s' % |
- test_info.filename) |
- |
- return failures |
- |
- def _RunTest(self, test_info): |
- """Run a single test file using a shared test_shell process. |
- |
- Args: |
- test_info: Object containing the test filename, uri and timeout |
- |
- Return: |
- A list of TestFailure objects describing the error. |
- """ |
- self._EnsureTestShellIsRunning() |
- # Args to test_shell is a space-separated list of |
- # "uri timeout pixel_hash" |
- # The timeout and pixel_hash are optional. The timeout is used if this |
- # test has a custom timeout. The pixel_hash is used to avoid doing an |
- # image dump if the checksums match, so it should be set to a blank |
- # value if we are generating a new baseline. |
- # (Otherwise, an image from a previous run will be copied into |
- # the baseline.) |
- image_hash = test_info.image_hash |
- if image_hash and self._test_args.new_baseline: |
- image_hash = "" |
- self._test_shell_proc.stdin.write(("%s %s %s\n" % |
- (test_info.uri, test_info.timeout, image_hash))) |
- |
- # If the test shell is dead, the above may cause an IOError as we |
- # try to write onto the broken pipe. If this is the first test for |
- # this test shell process, than the test shell did not |
- # successfully start. If this is not the first test, then the |
- # previous tests have caused some kind of delayed crash. We don't |
- # try to recover here. |
- self._test_shell_proc.stdin.flush() |
- |
- stats = ProcessOutput(self._test_shell_proc, test_info, |
- self._test_types, self._test_args, |
- self._options.target, |
- self._options.results_directory) |
- |
- self._test_stats.append(stats) |
- return stats.failures |
- |
- def _EnsureTestShellIsRunning(self): |
- """Start the shared test shell, if it's not running. Not for use when |
- running tests singly, since those each start a separate test shell in |
- their own thread. |
- """ |
- if (not self._test_shell_proc or |
- self._test_shell_proc.poll() is not None): |
- self._test_shell_proc = StartTestShell(self._test_shell_command, |
- self._shell_args) |
- |
- def _KillTestShell(self): |
- """Kill the test shell process if it's running.""" |
- if self._test_shell_proc: |
- self._test_shell_proc.stdin.close() |
- self._test_shell_proc.stdout.close() |
- if self._test_shell_proc.stderr: |
- self._test_shell_proc.stderr.close() |
- if (sys.platform not in ('win32', 'cygwin') and |
- not self._test_shell_proc.poll()): |
- # Closing stdin/stdout/stderr hangs sometimes on OS X. |
- null = open(os.devnull, "w") |
- subprocess.Popen(["kill", "-9", |
- str(self._test_shell_proc.pid)], stderr=null) |
- null.close() |
- self._test_shell_proc = None |