initial import
[vuplus_webkit] / Tools / Scripts / webkitpy / layout_tests / controllers / worker.py
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
29 """Handle messages from the Manager and executes actual tests."""
30
31 import logging
32 import sys
33 import threading
34 import time
35
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
40
41
42 _log = logging.getLogger(__name__)
43
44
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)
48         self._done = False
49         self._canceled = False
50         self._port = None
51         self._batch_size = None
52         self._batch_count = None
53         self._filesystem = None
54         self._driver = None
55         self._tests_run_file = None
56         self._tests_run_filename = None
57
58     def __del__(self):
59         self.cleanup()
60
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.
64
65         This routine exists so that the mixin can be created and then marshaled
66         across into a child process."""
67         self._port = port
68         self._filesystem = port.filesystem
69         self._batch_count = 0
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)
73
74     def cancel(self):
75         """Attempt to abort processing (best effort)."""
76         self._canceled = True
77
78     def is_done(self):
79         return self._done or self._canceled
80
81     def name(self):
82         return self._name
83
84     def run(self, port):
85         self.safe_init(port)
86
87         exception_msg = ""
88         _log.debug("%s starting" % self._name)
89
90         try:
91             self._worker_connection.run_message_loop()
92             if not self.is_done():
93                 raise AssertionError("%s: ran out of messages in worker queue."
94                                      % self._name)
95         except KeyboardInterrupt:
96             exception_msg = ", interrupted"
97             self._worker_connection.raise_exception(sys.exc_info())
98         except:
99             exception_msg = ", exception raised"
100             self._worker_connection.raise_exception(sys.exc_info())
101         finally:
102             _log.debug("%s done with message loop%s" % (self._name, exception_msg))
103             self._worker_connection.post_message('done')
104             self.cleanup()
105             _log.debug("%s exiting" % self._name)
106
107     def handle_test_list(self, src, list_name, test_list):
108         start_time = time.time()
109         num_tests = 0
110         for test_input in test_list:
111             self._run_test(test_input)
112             num_tests += 1
113             self._worker_connection.yield_to_broker()
114
115         elapsed_time = time.time() - start_time
116         self._worker_connection.post_message('finished_list', list_name, num_tests, elapsed_time)
117
118     def handle_stop(self, src):
119         self._done = True
120
121     def _run_test(self, test_input):
122         test_timeout_sec = self.timeout(test_input)
123         start = time.time()
124         self._worker_connection.post_message('started_test', test_input, test_timeout_sec)
125
126         result = self.run_test_with_timeout(test_input, test_timeout_sec)
127
128         elapsed_time = time.time() - start
129         self._worker_connection.post_message('finished_test', result, elapsed_time)
130
131         self.clean_up_after_test(test_input, result)
132
133     def cleanup(self):
134         _log.debug("%s cleaning up" % self._name)
135         self.kill_driver()
136         if self._tests_run_file:
137             self._tests_run_file.close()
138             self._tests_run_file = None
139
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.
145         #
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
151
152         thread_padding_sec = 1.0
153         thread_timeout_sec = driver_timeout_sec + thread_padding_sec
154         return thread_timeout_sec
155
156     def kill_driver(self):
157         if self._driver:
158             _log.debug("%s killing driver" % self._name)
159             self._driver.stop()
160             self._driver = None
161
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)
166
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")
171
172         if result.failures:
173             # Check and kill DumpRenderTree if we need to.
174             if any([f.driver_needs_restart() for f in result.failures]):
175                 self.kill_driver()
176                 # Reset the batch count since the shell just bounced.
177                 self._batch_count = 0
178
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))
185         else:
186             _log.debug("%s %s passed" % (self._name, test_name))
187
188         if self._batch_size > 0 and self._batch_count >= self._batch_size:
189             self.kill_driver()
190             self._batch_count = 0
191
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.
194
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
197         files singly.
198
199         Args:
200           test_input: Object containing the test filename and timeout
201           thread_timeout_sec: time to wait before killing the driver process.
202         Returns:
203           A TestResult
204         """
205         worker = self
206
207         driver = self._port.create_driver(self._worker_number)
208         driver.start()
209
210         class SingleTestThread(threading.Thread):
211             def __init__(self):
212                 threading.Thread.__init__(self)
213                 self.result = None
214
215             def run(self):
216                 self.result = worker.run_single_test(driver, test_input)
217
218         thread = SingleTestThread()
219         thread.start()
220         thread.join(thread_timeout_sec)
221         result = thread.result
222         if thread.isAlive():
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
230             # thread's results.
231             _log.error('Test thread hung: killing all DumpRenderTrees')
232
233         driver.stop()
234
235         if not result:
236             result = test_results.TestResult(test_input.test_name, failures=[], test_run_time=0)
237         return result
238
239     def _run_test_in_this_thread(self, test_input):
240         """Run a single test file using a shared DumpRenderTree process.
241
242         Args:
243           test_input: Object containing the test filename, uri and timeout
244
245         Returns: a TestResult object.
246         """
247         if not self._driver or self._driver.poll() is not None:
248             self._driver = self._port.create_driver(self._worker_number)
249             self._driver.start()
250         return self.run_single_test(self._driver, test_input)
251
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)