1 # Copyright (C) 2011 Google Inc. All rights reserved.
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are
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
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.
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.
29 """Handle messages from the Manager and executes actual tests."""
36 from webkitpy.layout_tests.controllers import manager_worker_broker
37 from webkitpy.layout_tests.controllers import single_test_runner
38 from webkitpy.layout_tests.models import test_expectations
39 from webkitpy.layout_tests.models import test_results
42 _log = logging.getLogger(__name__)
45 class Worker(manager_worker_broker.AbstractWorker):
46 def __init__(self, worker_connection, worker_number, options):
47 manager_worker_broker.AbstractWorker.__init__(self, worker_connection, worker_number, options)
49 self._canceled = False
51 self._batch_size = None
52 self._batch_count = None
53 self._filesystem = None
55 self._tests_run_file = None
56 self._tests_run_filename = None
61 def safe_init(self, port):
62 """This method should only be called when it is is safe for the mixin
63 to create state that can't be Pickled.
65 This routine exists so that the mixin can be created and then marshaled
66 across into a child process."""
68 self._filesystem = port.filesystem
70 self._batch_size = self._options.batch_size or 0
71 tests_run_filename = self._filesystem.join(port.results_directory(), "tests_run%d.txt" % self._worker_number)
72 self._tests_run_file = self._filesystem.open_text_file_for_writing(tests_run_filename)
75 """Attempt to abort processing (best effort)."""
79 return self._done or self._canceled
88 _log.debug("%s starting" % self._name)
91 self._worker_connection.run_message_loop()
92 if not self.is_done():
93 raise AssertionError("%s: ran out of messages in worker queue."
95 except KeyboardInterrupt:
96 exception_msg = ", interrupted"
97 self._worker_connection.raise_exception(sys.exc_info())
99 exception_msg = ", exception raised"
100 self._worker_connection.raise_exception(sys.exc_info())
102 _log.debug("%s done with message loop%s" % (self._name, exception_msg))
103 self._worker_connection.post_message('done')
105 _log.debug("%s exiting" % self._name)
107 def handle_test_list(self, src, list_name, test_list):
108 start_time = time.time()
110 for test_input in test_list:
111 self._run_test(test_input)
113 self._worker_connection.yield_to_broker()
115 elapsed_time = time.time() - start_time
116 self._worker_connection.post_message('finished_list', list_name, num_tests, elapsed_time)
118 def handle_stop(self, src):
121 def _run_test(self, test_input):
122 test_timeout_sec = self.timeout(test_input)
124 self._worker_connection.post_message('started_test', test_input, test_timeout_sec)
126 result = self.run_test_with_timeout(test_input, test_timeout_sec)
128 elapsed_time = time.time() - start
129 self._worker_connection.post_message('finished_test', result, elapsed_time)
131 self.clean_up_after_test(test_input, result)
134 _log.debug("%s cleaning up" % self._name)
136 if self._tests_run_file:
137 self._tests_run_file.close()
138 self._tests_run_file = None
140 def timeout(self, test_input):
141 """Compute the appropriate timeout value for a test."""
142 # The DumpRenderTree watchdog uses 2.5x the timeout; we want to be
143 # larger than that. We also add a little more padding if we're
144 # running tests in a separate thread.
146 # Note that we need to convert the test timeout from a
147 # string value in milliseconds to a float for Python.
148 driver_timeout_sec = 3.0 * float(test_input.timeout) / 1000.0
149 if not self._options.run_singly:
150 return driver_timeout_sec
152 thread_padding_sec = 1.0
153 thread_timeout_sec = driver_timeout_sec + thread_padding_sec
154 return thread_timeout_sec
156 def kill_driver(self):
158 _log.debug("%s killing driver" % self._name)
162 def run_test_with_timeout(self, test_input, timeout):
163 if self._options.run_singly:
164 return self._run_test_in_another_thread(test_input, timeout)
165 return self._run_test_in_this_thread(test_input)
167 def clean_up_after_test(self, test_input, result):
168 self._batch_count += 1
169 test_name = test_input.test_name
170 self._tests_run_file.write(test_name + "\n")
173 # Check and kill DumpRenderTree if we need to.
174 if any([f.driver_needs_restart() for f in result.failures]):
176 # Reset the batch count since the shell just bounced.
177 self._batch_count = 0
179 # Print the error message(s).
180 _log.debug("%s %s failed:" % (self._name, test_name))
181 for f in result.failures:
182 _log.debug("%s %s" % (self._name, f.message()))
183 elif result.type == test_expectations.SKIP:
184 _log.debug("%s %s skipped" % (self._name, test_name))
186 _log.debug("%s %s passed" % (self._name, test_name))
188 if self._batch_size > 0 and self._batch_count >= self._batch_size:
190 self._batch_count = 0
192 def _run_test_in_another_thread(self, test_input, thread_timeout_sec):
193 """Run a test in a separate thread, enforcing a hard time limit.
195 Since we can only detect the termination of a thread, not any internal
196 state or progress, we can only run per-test timeouts when running test
200 test_input: Object containing the test filename and timeout
201 thread_timeout_sec: time to wait before killing the driver process.
207 driver = self._port.create_driver(self._worker_number)
210 class SingleTestThread(threading.Thread):
212 threading.Thread.__init__(self)
216 self.result = worker.run_single_test(driver, test_input)
218 thread = SingleTestThread()
220 thread.join(thread_timeout_sec)
221 result = thread.result
223 # If join() returned with the thread still running, the
224 # DumpRenderTree is completely hung and there's nothing
225 # more we can do with it. We have to kill all the
226 # DumpRenderTrees to free it up. If we're running more than
227 # one DumpRenderTree thread, we'll end up killing the other
228 # DumpRenderTrees too, introducing spurious crashes. We accept
229 # that tradeoff in order to avoid losing the rest of this
231 _log.error('Test thread hung: killing all DumpRenderTrees')
236 result = test_results.TestResult(test_input.test_name, failures=[], test_run_time=0)
239 def _run_test_in_this_thread(self, test_input):
240 """Run a single test file using a shared DumpRenderTree process.
243 test_input: Object containing the test filename, uri and timeout
245 Returns: a TestResult object.
247 if not self._driver or self._driver.poll() is not None:
248 self._driver = self._port.create_driver(self._worker_number)
250 return self.run_single_test(self._driver, test_input)
252 def run_single_test(self, driver, test_input):
253 return single_test_runner.run_single_test(self._port, self._options,
254 test_input, driver, self._name)