Unreviewed, rolling out r112014.
[WebKit-https.git] / 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.common.host import Host
37 from webkitpy.layout_tests.controllers import manager_worker_broker
38 from webkitpy.layout_tests.controllers import single_test_runner
39 from webkitpy.layout_tests.models import test_expectations
40 from webkitpy.layout_tests.models import test_results
41 from webkitpy.layout_tests.views import printing
42
43
44 _log = logging.getLogger(__name__)
45
46
47 class WorkerArguments(object):
48     def __init__(self, worker_number, results_directory, options):
49         self.worker_number = worker_number
50         self.results_directory = results_directory
51         self.options = options
52
53
54 class Worker(manager_worker_broker.AbstractWorker):
55     def __init__(self, worker_connection, worker_arguments):
56         super(Worker, self).__init__(worker_connection, worker_arguments)
57         self._worker_number = worker_arguments.worker_number
58         self._name = 'worker/%d' % self._worker_number
59         self._results_directory = worker_arguments.results_directory
60         self._options = worker_arguments.options
61         self._port = None
62         self._batch_size = None
63         self._batch_count = None
64         self._filesystem = None
65         self._driver = None
66         self._tests_run_file = None
67         self._tests_run_filename = None
68         self._printer = None
69
70     def __del__(self):
71         self.cleanup()
72
73     def safe_init(self):
74         """This method should only be called when it is is safe for the mixin
75         to create state that can't be Pickled.
76
77         This routine exists so that the mixin can be created and then marshaled
78         across into a child process."""
79         self._filesystem = self._port.host.filesystem
80         self._batch_count = 0
81         self._batch_size = self._options.batch_size or 0
82         tests_run_filename = self._filesystem.join(self._results_directory, "tests_run%d.txt" % self._worker_number)
83         self._tests_run_file = self._filesystem.open_text_file_for_writing(tests_run_filename)
84
85     def set_inline_arguments(self, port):
86         self._port = port
87
88     def run(self):
89         if not self._port:
90             # We are running in a child process and need to create a new Host.
91             if self._options.platform and 'test' in self._options.platform:
92                 # It is lame to import mocks into real code, but this allows us to use the test port in multi-process tests as well.
93                 from webkitpy.common.host_mock import MockHost
94                 host = MockHost()
95             else:
96                 host = Host()
97
98             options = self._options
99             self._port = host.port_factory.get(options.platform, options)
100
101             # The unix multiprocessing implementation clones the
102             # log handler configuration into the child processes,
103             # but the win implementation doesn't.
104             configure_logging = (sys.platform == 'win32')
105
106             # FIXME: This won't work if the calling process is logging
107             # somewhere other than sys.stderr and sys.stdout, but I'm not sure
108             # if this will be an issue in practice.
109             self._printer = printing.Printer(self._port, options, sys.stderr, sys.stdout, configure_logging)
110
111         self.safe_init()
112
113         try:
114             _log.debug("%s starting" % self._name)
115             super(Worker, self).run()
116         finally:
117             self._worker_connection.post_message('done')
118             self.cleanup()
119             _log.debug("%s exiting" % self._name)
120
121     def handle_test_list(self, src, list_name, test_list):
122         start_time = time.time()
123         num_tests = 0
124         for test_input in test_list:
125             #FIXME: When the DRT support also this function, that would be useful
126             if self._port.driver_name() == "WebKitTestRunner" and self._port.get_option('skip_pixel_test_if_no_baseline') and self._port.get_option('pixel_tests'):
127                 test_input.should_run_pixel_test = (self._port.expected_image(test_input.test_name) != None)
128             self._run_test(test_input)
129             num_tests += 1
130             self._worker_connection.yield_to_broker()
131
132         elapsed_time = time.time() - start_time
133         self._worker_connection.post_message('finished_list', list_name, num_tests, elapsed_time)
134
135     def handle_stop(self, src):
136         self.stop_handling_messages()
137
138     def _run_test(self, test_input):
139         test_timeout_sec = self.timeout(test_input)
140         start = time.time()
141         self._worker_connection.post_message('started_test', test_input, test_timeout_sec)
142
143         result = self.run_test_with_timeout(test_input, test_timeout_sec)
144
145         elapsed_time = time.time() - start
146         self._worker_connection.post_message('finished_test', result, elapsed_time)
147
148         self.clean_up_after_test(test_input, result)
149
150     def cleanup(self):
151         _log.debug("%s cleaning up" % self._name)
152         self.kill_driver()
153         if self._tests_run_file:
154             self._tests_run_file.close()
155             self._tests_run_file = None
156         if self._printer:
157             self._printer.cleanup()
158             self._printer = None
159
160     def timeout(self, test_input):
161         """Compute the appropriate timeout value for a test."""
162         # The DumpRenderTree watchdog uses 2.5x the timeout; we want to be
163         # larger than that. We also add a little more padding if we're
164         # running tests in a separate thread.
165         #
166         # Note that we need to convert the test timeout from a
167         # string value in milliseconds to a float for Python.
168         driver_timeout_sec = 3.0 * float(test_input.timeout) / 1000.0
169         if not self._options.run_singly:
170             return driver_timeout_sec
171
172         thread_padding_sec = 1.0
173         thread_timeout_sec = driver_timeout_sec + thread_padding_sec
174         return thread_timeout_sec
175
176     def kill_driver(self):
177         if self._driver:
178             _log.debug("%s killing driver" % self._name)
179             self._driver.stop()
180             self._driver = None
181
182     def run_test_with_timeout(self, test_input, timeout):
183         if self._options.run_singly:
184             return self._run_test_in_another_thread(test_input, timeout)
185         return self._run_test_in_this_thread(test_input)
186
187     def clean_up_after_test(self, test_input, result):
188         self._batch_count += 1
189         test_name = test_input.test_name
190         self._tests_run_file.write(test_name + "\n")
191
192         if result.failures:
193             # Check and kill DumpRenderTree if we need to.
194             if any([f.driver_needs_restart() for f in result.failures]):
195                 self.kill_driver()
196                 # Reset the batch count since the shell just bounced.
197                 self._batch_count = 0
198
199             # Print the error message(s).
200             _log.debug("%s %s failed:" % (self._name, test_name))
201             for f in result.failures:
202                 _log.debug("%s  %s" % (self._name, f.message()))
203         elif result.type == test_expectations.SKIP:
204             _log.debug("%s %s skipped" % (self._name, test_name))
205         else:
206             _log.debug("%s %s passed" % (self._name, test_name))
207
208         if self._batch_size > 0 and self._batch_count >= self._batch_size:
209             self.kill_driver()
210             self._batch_count = 0
211
212     def _run_test_in_another_thread(self, test_input, thread_timeout_sec):
213         """Run a test in a separate thread, enforcing a hard time limit.
214
215         Since we can only detect the termination of a thread, not any internal
216         state or progress, we can only run per-test timeouts when running test
217         files singly.
218
219         Args:
220           test_input: Object containing the test filename and timeout
221           thread_timeout_sec: time to wait before killing the driver process.
222         Returns:
223           A TestResult
224         """
225         worker = self
226
227         driver = self._port.create_driver(self._worker_number)
228
229         class SingleTestThread(threading.Thread):
230             def __init__(self):
231                 threading.Thread.__init__(self)
232                 self.result = None
233
234             def run(self):
235                 self.result = worker.run_single_test(driver, test_input)
236
237         thread = SingleTestThread()
238         thread.start()
239         thread.join(thread_timeout_sec)
240         result = thread.result
241         if thread.isAlive():
242             # If join() returned with the thread still running, the
243             # DumpRenderTree is completely hung and there's nothing
244             # more we can do with it.  We have to kill all the
245             # DumpRenderTrees to free it up. If we're running more than
246             # one DumpRenderTree thread, we'll end up killing the other
247             # DumpRenderTrees too, introducing spurious crashes. We accept
248             # that tradeoff in order to avoid losing the rest of this
249             # thread's results.
250             _log.error('Test thread hung: killing all DumpRenderTrees')
251
252         driver.stop()
253
254         if not result:
255             result = test_results.TestResult(test_input.test_name, failures=[], test_run_time=0)
256         return result
257
258     def _run_test_in_this_thread(self, test_input):
259         """Run a single test file using a shared DumpRenderTree process.
260
261         Args:
262           test_input: Object containing the test filename, uri and timeout
263
264         Returns: a TestResult object.
265         """
266         if self._driver and self._driver.has_crashed():
267             self.kill_driver()
268         if not self._driver:
269             self._driver = self._port.create_driver(self._worker_number)
270         return self.run_single_test(self._driver, test_input)
271
272     def run_single_test(self, driver, test_input):
273         return single_test_runner.run_single_test(self._port, self._options,
274             test_input, driver, self._name)