1# Copyright (C) 2011 Google Inc. All rights reserved. 2# 3# Redistribution and use in source and binary forms, with or without 4# modification, are permitted provided that the following conditions are 5# met: 6# 7# * Redistributions of source code must retain the above copyright 8# notice, this list of conditions and the following disclaimer. 9# * Redistributions in binary form must reproduce the above 10# copyright notice, this list of conditions and the following disclaimer 11# in the documentation and/or other materials provided with the 12# distribution. 13# * Neither the name of Google Inc. nor the names of its 14# contributors may be used to endorse or promote products derived from 15# this software without specific prior written permission. 16# 17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29import logging 30import sys 31import threading 32import time 33 34from webkitpy.layout_tests.layout_package import single_test_runner 35from webkitpy.layout_tests.layout_package import test_results 36 37_log = logging.getLogger(__name__) 38 39 40class WorkerMixin(object): 41 """This class holds logic common to Worker and TestShellThread that 42 doesn't directly have to do with running the tests (which is 43 SingleTestRunner's responsibility. This class cannot stand on its own.""" 44 def __init__(self): 45 assert False, "WorkerMixin can't be directly instantiated" 46 47 def safe_init(self, port): 48 """This method should only be called when it is is safe for the mixin 49 to create state that can't be Pickled. 50 51 This routine exists so that the mixin can be created and then marshaled 52 across into a child process.""" 53 self._port = port 54 self._filesystem = port._filesystem 55 self._batch_count = 0 56 self._batch_size = self._options.batch_size 57 self._driver = None 58 tests_run_filename = self._filesystem.join(port.results_directory(), 59 "tests_run%d.txt" % self._worker_number) 60 self._tests_run_file = self._filesystem.open_text_file_for_writing(tests_run_filename) 61 62 # FIXME: it's goofy that we have to track this at all, but it's due to 63 # the awkward logic in TestShellThread._run(). When we remove that 64 # file, we should rewrite this code so that caller keeps track of whether 65 # the lock is held. 66 self._has_http_lock = False 67 68 def cleanup(self): 69 if self._driver: 70 self.kill_driver() 71 if self._has_http_lock: 72 self.stop_servers_with_lock() 73 if self._tests_run_file: 74 self._tests_run_file.close() 75 self._tests_run_file = None 76 77 def timeout(self, test_input): 78 """Compute the appropriate timeout value for a test.""" 79 # The DumpRenderTree watchdog uses 2.5x the timeout; we want to be 80 # larger than that. We also add a little more padding if we're 81 # running tests in a separate thread. 82 # 83 # Note that we need to convert the test timeout from a 84 # string value in milliseconds to a float for Python. 85 driver_timeout_sec = 3.0 * float(test_input.timeout) / 1000.0 86 if not self._options.run_singly: 87 return driver_timeout_sec 88 89 thread_padding_sec = 1.0 90 thread_timeout_sec = driver_timeout_sec + thread_padding_sec 91 return thread_timeout_sec 92 93 def start_servers_with_lock(self): 94 _log.debug('Acquiring http lock ...') 95 self._port.acquire_http_lock() 96 _log.debug('Starting HTTP server ...') 97 self._port.start_http_server() 98 _log.debug('Starting WebSocket server ...') 99 self._port.start_websocket_server() 100 self._has_http_lock = True 101 102 def stop_servers_with_lock(self): 103 if self._has_http_lock: 104 _log.debug('Stopping HTTP server ...') 105 self._port.stop_http_server() 106 _log.debug('Stopping WebSocket server ...') 107 self._port.stop_websocket_server() 108 _log.debug('Releasing server lock ...') 109 self._port.release_http_lock() 110 self._has_http_lock = False 111 112 def kill_driver(self): 113 if self._driver: 114 self._driver.stop() 115 self._driver = None 116 117 def run_test_with_timeout(self, test_input, timeout): 118 if self._options.run_singly: 119 return self._run_test_in_another_thread(test_input, timeout) 120 else: 121 return self._run_test_in_this_thread(test_input) 122 return result 123 124 def clean_up_after_test(self, test_input, result): 125 self._batch_count += 1 126 self._tests_run_file.write(test_input.filename + "\n") 127 test_name = self._port.relative_test_filename(test_input.filename) 128 129 if result.failures: 130 # Check and kill DumpRenderTree if we need to. 131 if any([f.should_kill_dump_render_tree() for f in result.failures]): 132 self.kill_driver() 133 # Reset the batch count since the shell just bounced. 134 self._batch_count = 0 135 136 # Print the error message(s). 137 _log.debug("%s %s failed:" % (self._name, test_name)) 138 for f in result.failures: 139 _log.debug("%s %s" % (self._name, f.message())) 140 else: 141 _log.debug("%s %s passed" % (self._name, test_name)) 142 143 if self._batch_size > 0 and self._batch_count >= self._batch_size: 144 # Bounce the shell and reset count. 145 self.kill_driver() 146 self._batch_count = 0 147 148 def _run_test_in_another_thread(self, test_input, thread_timeout_sec): 149 """Run a test in a separate thread, enforcing a hard time limit. 150 151 Since we can only detect the termination of a thread, not any internal 152 state or progress, we can only run per-test timeouts when running test 153 files singly. 154 155 Args: 156 test_input: Object containing the test filename and timeout 157 thread_timeout_sec: time to wait before killing the driver process. 158 Returns: 159 A TestResult 160 """ 161 worker = self 162 163 driver = worker._port.create_driver(worker._worker_number) 164 driver.start() 165 166 class SingleTestThread(threading.Thread): 167 def run(self): 168 self.result = worker._run_single_test(driver, test_input) 169 170 thread = SingleTestThread() 171 thread.start() 172 thread.join(thread_timeout_sec) 173 result = getattr(thread, 'result', None) 174 if thread.isAlive(): 175 # If join() returned with the thread still running, the 176 # DumpRenderTree is completely hung and there's nothing 177 # more we can do with it. We have to kill all the 178 # DumpRenderTrees to free it up. If we're running more than 179 # one DumpRenderTree thread, we'll end up killing the other 180 # DumpRenderTrees too, introducing spurious crashes. We accept 181 # that tradeoff in order to avoid losing the rest of this 182 # thread's results. 183 _log.error('Test thread hung: killing all DumpRenderTrees') 184 185 driver.stop() 186 187 if not result: 188 result = test_results.TestResult(test_input.filename, failures=[], test_run_time=0) 189 return result 190 191 def _run_test_in_this_thread(self, test_input): 192 """Run a single test file using a shared DumpRenderTree process. 193 194 Args: 195 test_input: Object containing the test filename, uri and timeout 196 197 Returns: a TestResult object. 198 """ 199 # poll() is not threadsafe and can throw OSError due to: 200 # http://bugs.python.org/issue1731717 201 if not self._driver or self._driver.poll() is not None: 202 self._driver = self._port.create_driver(self._worker_number) 203 self._driver.start() 204 return self._run_single_test(self._driver, test_input) 205 206 def _run_single_test(self, driver, test_input): 207 return single_test_runner.run_single_test(self._port, self._options, 208 test_input, driver, self._name) 209