2011-02-03 Hayato Ito <hayato@chromium.org>
[WebKit-https.git] / Tools / Scripts / webkitpy / layout_tests / layout_package / dump_render_tree_thread.py
1 #!/usr/bin/env python
2 # Copyright (C) 2010 Google Inc. All rights reserved.
3 # Copyright (C) 2010 Gabor Rapcsanyi (rgabor@inf.u-szeged.hu), University of Szeged
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are
7 # met:
8 #
9 #     * Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 #     * Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following disclaimer
13 # in the documentation and/or other materials provided with the
14 # distribution.
15 #     * Neither the name of Google Inc. nor the names of its
16 # contributors may be used to endorse or promote products derived from
17 # this software without specific prior written permission.
18 #
19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31 """A Thread object for running DumpRenderTree and processing URLs from a
32 shared queue.
33
34 Each thread runs a separate instance of the DumpRenderTree binary and validates
35 the output.  When there are no more URLs to process in the shared queue, the
36 thread exits.
37 """
38
39 import copy
40 import logging
41 import os
42 import Queue
43 import signal
44 import sys
45 import thread
46 import threading
47 import time
48
49
50 from webkitpy.layout_tests.test_types import image_diff
51 from webkitpy.layout_tests.test_types import test_type_base
52 from webkitpy.layout_tests.test_types import text_diff
53
54 import single_test_runner
55 import test_failures
56 import test_results
57
58 _log = logging.getLogger("webkitpy.layout_tests.layout_package."
59                          "dump_render_tree_thread")
60
61
62 def _pad_timeout(timeout):
63     """Returns a safe multiple of the per-test timeout value to use
64     to detect hung test threads.
65
66     """
67     # When we're running one test per DumpRenderTree process, we can
68     # enforce a hard timeout.  The DumpRenderTree watchdog uses 2.5x
69     # the timeout; we want to be larger than that.
70     return timeout * 3
71
72
73 def _milliseconds_to_seconds(msecs):
74     return float(msecs) / 1000.0
75
76
77 def _should_fetch_expected_checksum(options):
78     return options.pixel_tests and not (options.new_baseline or options.reset_results)
79
80
81 class SingleTestThread(threading.Thread):
82     """Thread wrapper for running a single test file."""
83
84     def __init__(self, port, options, worker_number, worker_name,
85                  test_input, test_types, test_args):
86         """
87         Args:
88           port: object implementing port-specific hooks
89           options: command line argument object from optparse
90           worker_number: worker number for tests
91           worker_name: for logging
92           test_input: Object containing the test filename and timeout
93           test_types: A list of TestType objects to run the test output
94               against.
95           test_args: A TestArguments object to pass to each TestType.
96         """
97
98         threading.Thread.__init__(self)
99         self._port = port
100         self._options = options
101         self._test_input = test_input
102         self._test_types = test_types
103         self._test_args = test_args
104         self._driver = None
105         self._worker_number = worker_number
106         self._name = worker_name
107
108     def run(self):
109         self._covered_run()
110
111     def _covered_run(self):
112         # FIXME: this is a separate routine to work around a bug
113         # in coverage: see http://bitbucket.org/ned/coveragepy/issue/85.
114         self._driver = self._port.create_driver(self._worker_number)
115         self._driver.start()
116         self._test_result = single_test_runner.run_single_test(
117             self._port, self._options, self._test_input, self._driver,
118             self._name, self._test_types, self._test_args)
119         self._driver.stop()
120
121     def get_test_result(self):
122         return self._test_result
123
124
125 class WatchableThread(threading.Thread):
126     """This class abstracts an interface used by
127     run_webkit_tests.TestRunner._wait_for_threads_to_finish for thread
128     management."""
129     def __init__(self):
130         threading.Thread.__init__(self)
131         self._canceled = False
132         self._exception_info = None
133         self._next_timeout = None
134         self._thread_id = None
135
136     def cancel(self):
137         """Set a flag telling this thread to quit."""
138         self._canceled = True
139
140     def clear_next_timeout(self):
141         """Mark a flag telling this thread to stop setting timeouts."""
142         self._timeout = 0
143
144     def exception_info(self):
145         """If run() terminated on an uncaught exception, return it here
146         ((type, value, traceback) tuple).
147         Returns None if run() terminated normally. Meant to be called after
148         joining this thread."""
149         return self._exception_info
150
151     def id(self):
152         """Return a thread identifier."""
153         return self._thread_id
154
155     def next_timeout(self):
156         """Return the time the test is supposed to finish by."""
157         return self._next_timeout
158
159
160 class TestShellThread(WatchableThread):
161     def __init__(self, port, options, worker_number, worker_name,
162                  filename_list_queue, result_queue):
163         """Initialize all the local state for this DumpRenderTree thread.
164
165         Args:
166           port: interface to port-specific hooks
167           options: command line options argument from optparse
168           worker_number: identifier for a particular worker thread.
169           worker_name: for logging.
170           filename_list_queue: A thread safe Queue class that contains lists
171               of tuples of (filename, uri) pairs.
172           result_queue: A thread safe Queue class that will contain
173               serialized TestResult objects.
174         """
175         WatchableThread.__init__(self)
176         self._port = port
177         self._options = options
178         self._worker_number = worker_number
179         self._name = worker_name
180         self._filename_list_queue = filename_list_queue
181         self._result_queue = result_queue
182         self._filename_list = []
183         self._driver = None
184         self._test_group_timing_stats = {}
185         self._test_results = []
186         self._num_tests = 0
187         self._start_time = 0
188         self._stop_time = 0
189         self._have_http_lock = False
190         self._http_lock_wait_begin = 0
191         self._http_lock_wait_end = 0
192
193         self._test_types = []
194         for cls in self._get_test_type_classes():
195             self._test_types.append(cls(self._port,
196                                         self._options.results_directory))
197         self._test_args = self._get_test_args(worker_number)
198
199         # Current group of tests we're running.
200         self._current_group = None
201         # Number of tests in self._current_group.
202         self._num_tests_in_current_group = None
203         # Time at which we started running tests from self._current_group.
204         self._current_group_start_time = None
205
206     def _get_test_args(self, worker_number):
207         """Returns the tuple of arguments for tests and for DumpRenderTree."""
208         test_args = test_type_base.TestArguments()
209         test_args.new_baseline = self._options.new_baseline
210         test_args.reset_results = self._options.reset_results
211
212         return test_args
213
214     def _get_test_type_classes(self):
215         classes = [text_diff.TestTextDiff]
216         if self._options.pixel_tests:
217             classes.append(image_diff.ImageDiff)
218         return classes
219
220     def get_test_group_timing_stats(self):
221         """Returns a dictionary mapping test group to a tuple of
222         (number of tests in that group, time to run the tests)"""
223         return self._test_group_timing_stats
224
225     def get_test_results(self):
226         """Return the list of all tests run on this thread.
227
228         This is used to calculate per-thread statistics.
229
230         """
231         return self._test_results
232
233     def get_total_time(self):
234         return max(self._stop_time - self._start_time -
235                    self._http_lock_wait_time(), 0.0)
236
237     def get_num_tests(self):
238         return self._num_tests
239
240     def run(self):
241         """Delegate main work to a helper method and watch for uncaught
242         exceptions."""
243         self._covered_run()
244
245     def _covered_run(self):
246         # FIXME: this is a separate routine to work around a bug
247         # in coverage: see http://bitbucket.org/ned/coveragepy/issue/85.
248         self._thread_id = thread.get_ident()
249         self._start_time = time.time()
250         self._num_tests = 0
251         try:
252             _log.debug('%s starting' % (self.getName()))
253             self._run(test_runner=None, result_summary=None)
254             _log.debug('%s done (%d tests)' % (self.getName(),
255                        self.get_num_tests()))
256         except KeyboardInterrupt:
257             self._exception_info = sys.exc_info()
258             _log.debug("%s interrupted" % self.getName())
259         except:
260             # Save the exception for our caller to see.
261             self._exception_info = sys.exc_info()
262             self._stop_time = time.time()
263             _log.error('%s dying, exception raised' % self.getName())
264
265         self._stop_time = time.time()
266
267     def run_in_main_thread(self, test_runner, result_summary):
268         """This hook allows us to run the tests from the main thread if
269         --num-test-shells==1, instead of having to always run two or more
270         threads. This allows us to debug the test harness without having to
271         do multi-threaded debugging."""
272         self._run(test_runner, result_summary)
273
274     def cancel(self):
275         """Clean up http lock and set a flag telling this thread to quit."""
276         self._stop_servers_with_lock()
277         WatchableThread.cancel(self)
278
279     def next_timeout(self):
280         """Return the time the test is supposed to finish by."""
281         if self._next_timeout:
282             return self._next_timeout + self._http_lock_wait_time()
283         return self._next_timeout
284
285     def _http_lock_wait_time(self):
286         """Return the time what http locking takes."""
287         if self._http_lock_wait_begin == 0:
288             return 0
289         if self._http_lock_wait_end == 0:
290             return time.time() - self._http_lock_wait_begin
291         return self._http_lock_wait_end - self._http_lock_wait_begin
292
293     def _run(self, test_runner, result_summary):
294         """Main work entry point of the thread. Basically we pull urls from the
295         filename queue and run the tests until we run out of urls.
296
297         If test_runner is not None, then we call test_runner.UpdateSummary()
298         with the results of each test."""
299         batch_size = self._options.batch_size
300         batch_count = 0
301
302         # Append tests we're running to the existing tests_run.txt file.
303         # This is created in run_webkit_tests.py:_PrepareListsAndPrintOutput.
304         tests_run_filename = self._port._filesystem.join(self._options.results_directory,
305                                           "tests_run.txt")
306         tests_run_file = self._port._filesystem.open_text_file_for_writing(tests_run_filename, append=False)
307         while True:
308             if self._canceled:
309                 _log.debug('Testing cancelled')
310                 tests_run_file.close()
311                 return
312
313             if len(self._filename_list) is 0:
314                 if self._current_group is not None:
315                     self._test_group_timing_stats[self._current_group] = \
316                         (self._num_tests_in_current_group,
317                          time.time() - self._current_group_start_time)
318
319                 try:
320                     self._current_group, self._filename_list = \
321                         self._filename_list_queue.get_nowait()
322                 except Queue.Empty:
323                     self._stop_servers_with_lock()
324                     self._kill_dump_render_tree()
325                     tests_run_file.close()
326                     return
327
328                 if self._current_group == "tests_to_http_lock":
329                     self._start_servers_with_lock()
330                 elif self._have_http_lock:
331                     self._stop_servers_with_lock()
332
333                 self._num_tests_in_current_group = len(self._filename_list)
334                 self._current_group_start_time = time.time()
335
336             test_input = self._filename_list.pop()
337
338             # We have a url, run tests.
339             batch_count += 1
340             self._num_tests += 1
341             if self._options.run_singly:
342                 result = self._run_test_in_another_thread(test_input)
343             else:
344                 result = self._run_test_in_this_thread(test_input)
345
346             filename = test_input.filename
347             tests_run_file.write(filename + "\n")
348             if result.failures:
349                 # Check and kill DumpRenderTree if we need to.
350                 if len([1 for f in result.failures
351                         if f.should_kill_dump_render_tree()]):
352                     self._kill_dump_render_tree()
353                     # Reset the batch count since the shell just bounced.
354                     batch_count = 0
355                 # Print the error message(s).
356                 error_str = '\n'.join(['  ' + f.message() for
357                                        f in result.failures])
358                 _log.debug("%s %s failed:\n%s" % (self.getName(),
359                            self._port.relative_test_filename(filename),
360                            error_str))
361             else:
362                 _log.debug("%s %s passed" % (self.getName(),
363                            self._port.relative_test_filename(filename)))
364             self._result_queue.put(result.dumps())
365
366             if batch_size > 0 and batch_count >= batch_size:
367                 # Bounce the shell and reset count.
368                 self._kill_dump_render_tree()
369                 batch_count = 0
370
371             if test_runner:
372                 test_runner.update_summary(result_summary)
373
374     def _run_test_in_another_thread(self, test_input):
375         """Run a test in a separate thread, enforcing a hard time limit.
376
377         Since we can only detect the termination of a thread, not any internal
378         state or progress, we can only run per-test timeouts when running test
379         files singly.
380
381         Args:
382           test_input: Object containing the test filename and timeout
383
384         Returns:
385           A TestResult
386         """
387         worker = SingleTestThread(self._port,
388                                   self._options,
389                                   self._worker_number,
390                                   self._name,
391                                   test_input,
392                                   self._test_types,
393                                   self._test_args)
394
395         worker.start()
396
397         thread_timeout = _milliseconds_to_seconds(
398             _pad_timeout(int(test_input.timeout)))
399         thread._next_timeout = time.time() + thread_timeout
400         worker.join(thread_timeout)
401         if worker.isAlive():
402             # If join() returned with the thread still running, the
403             # DumpRenderTree is completely hung and there's nothing
404             # more we can do with it.  We have to kill all the
405             # DumpRenderTrees to free it up. If we're running more than
406             # one DumpRenderTree thread, we'll end up killing the other
407             # DumpRenderTrees too, introducing spurious crashes. We accept
408             # that tradeoff in order to avoid losing the rest of this
409             # thread's results.
410             _log.error('Test thread hung: killing all DumpRenderTrees')
411             if worker._driver:
412                 worker._driver.stop()
413
414         try:
415             result = worker.get_test_result()
416         except AttributeError, e:
417             # This gets raised if the worker thread has already exited.
418             _log.error('Cannot get results of test: %s' % test_input.filename)
419             # FIXME: Seems we want a unique failure type here.
420             result = test_results.TestResult(test_input.filename)
421
422         return result
423
424     def _run_test_in_this_thread(self, test_input):
425         """Run a single test file using a shared DumpRenderTree process.
426
427         Args:
428           test_input: Object containing the test filename, uri and timeout
429
430         Returns: a TestResult object.
431         """
432         self._ensure_dump_render_tree_is_running()
433         thread_timeout = _milliseconds_to_seconds(
434              _pad_timeout(int(test_input.timeout)))
435         self._next_timeout = time.time() + thread_timeout
436         test_result = single_test_runner.run_single_test(
437             self._port, self._options, test_input, self._driver, self._name,
438             self._test_types, self._test_args)
439         self._test_results.append(test_result)
440         return test_result
441
442     def _ensure_dump_render_tree_is_running(self):
443         """Start the shared DumpRenderTree, if it's not running.
444
445         This is not for use when running tests singly, since those each start
446         a separate DumpRenderTree in their own thread.
447
448         """
449         # poll() is not threadsafe and can throw OSError due to:
450         # http://bugs.python.org/issue1731717
451         if not self._driver or self._driver.poll() is not None:
452             self._driver = self._port.create_driver(self._worker_number)
453             self._driver.start()
454
455     def _start_servers_with_lock(self):
456         """Acquire http lock and start the servers."""
457         self._http_lock_wait_begin = time.time()
458         _log.debug('Acquire http lock ...')
459         self._port.acquire_http_lock()
460         _log.debug('Starting HTTP server ...')
461         self._port.start_http_server()
462         _log.debug('Starting WebSocket server ...')
463         self._port.start_websocket_server()
464         self._http_lock_wait_end = time.time()
465         self._have_http_lock = True
466
467     def _stop_servers_with_lock(self):
468         """Stop the servers and release http lock."""
469         if self._have_http_lock:
470             _log.debug('Stopping HTTP server ...')
471             self._port.stop_http_server()
472             _log.debug('Stopping WebSocket server ...')
473             self._port.stop_websocket_server()
474             _log.debug('Release http lock ...')
475             self._port.release_http_lock()
476             self._have_http_lock = False
477
478     def _kill_dump_render_tree(self):
479         """Kill the DumpRenderTree process if it's running."""
480         if self._driver:
481             self._driver.stop()
482             self._driver = None