• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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