2010-03-02 Dirk Pranke <dpranke@chromium.org>
[WebKit-https.git] / WebKitTools / Scripts / webkitpy / layout_tests / port / chromium.py
1 #!/usr/bin/env python
2 # Copyright (C) 2010 Google Inc. All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 #     * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 #     * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 #     * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 """Chromium implementations of the Port interface."""
31
32 import logging
33 import os
34 import shutil
35 import signal
36 import subprocess
37 import sys
38 import time
39
40 import base
41 import http_server
42 import websocket_server
43
44
45 def check_file_exists(path_to_file, file_description, override_step=None):
46     """Verify the file is present where expected or log an error.
47
48     Args:
49         file_name: The (human friendly) name or description of the file
50             you're looking for (e.g., "HTTP Server"). Used for error logging.
51         override_step: An optional string to be logged if the check fails."""
52     if not os.path.exists(path_to_file):
53         logging.error('Unable to find %s' % file_description)
54         logging.error('    at %s' % path_to_file)
55         if override_step:
56             logging.error('    %s' % override_step)
57             logging.error('')
58         return False
59     return True
60
61
62 class ChromiumPort(base.Port):
63     """Abstract base class for Chromium implementations of the Port class."""
64
65     def __init__(self, port_name=None, options=None):
66         base.Port.__init__(self, port_name, options)
67         self._chromium_base_dir = None
68
69     def baseline_path(self):
70         return self._chromium_baseline_path(self._name)
71
72     def check_build(self, needs_http):
73         result = True
74         test_shell_binary_path = self._path_to_driver()
75         result = check_file_exists(test_shell_binary_path,
76                                    'test driver')
77         if result:
78             result = (self._check_driver_build_up_to_date(self._options.target)
79                       and result)
80         else:
81             logging.error('')
82
83         helper_path = self._path_to_helper()
84         result = check_file_exists(helper_path,
85                                    'layout test helper') and result
86
87         if not self._options.no_pixel_tests:
88             image_diff_path = self._path_to_image_diff()
89             result = check_file_exists(image_diff_path, 'image diff exe',
90                 'To override, invoke with --no-pixel-tests') and result
91
92         return result
93
94     def check_sys_deps(self, needs_http):
95         test_shell_binary_path = self._path_to_driver()
96         proc = subprocess.Popen([test_shell_binary_path,
97                                 '--check-layout-test-sys-deps'])
98         if proc.wait():
99             logging.error('System dependencies check failed.')
100             logging.error('To override, invoke with --nocheck-sys-deps')
101             logging.error('')
102             return False
103         return True
104
105     def path_from_chromium_base(self, *comps):
106         """Returns the full path to path made by joining the top of the
107         Chromium source tree and the list of path components in |*comps|."""
108         if not self._chromium_base_dir:
109             abspath = os.path.abspath(__file__)
110             self._chromium_base_dir = abspath[0:abspath.find('third_party')]
111         return os.path.join(self._chromium_base_dir, *comps)
112
113     def path_to_test_expectations_file(self):
114         return self.path_from_chromium_base('webkit', 'tools', 'layout_tests',
115                                             'test_expectations.txt')
116
117     def results_directory(self):
118         return self.path_from_chromium_base('webkit', self._options.target,
119                                             self._options.results_directory)
120
121     def setup_test_run(self):
122         # Delete the disk cache if any to ensure a clean test run.
123         test_shell_binary_path = self._path_to_driver()
124         cachedir = os.path.split(test_shell_binary_path)[0]
125         cachedir = os.path.join(cachedir, "cache")
126         if os.path.exists(cachedir):
127             shutil.rmtree(cachedir)
128
129     def show_results_html_file(self, results_filename):
130         subprocess.Popen([self._path_to_driver(),
131                           self.filename_to_uri(results_filename)])
132
133     def start_driver(self, image_path, options):
134         """Starts a new Driver and returns a handle to it."""
135         return ChromiumDriver(self, image_path, options)
136
137     def start_helper(self):
138         helper_path = self._path_to_helper()
139         if helper_path:
140             logging.debug("Starting layout helper %s" % helper_path)
141             self._helper = subprocess.Popen([helper_path],
142                 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None)
143             is_ready = self._helper.stdout.readline()
144             if not is_ready.startswith('ready'):
145                 logging.error("layout_test_helper failed to be ready")
146
147     def stop_helper(self):
148         if self._helper:
149             logging.debug("Stopping layout test helper")
150             self._helper.stdin.write("x\n")
151             self._helper.stdin.close()
152             self._helper.wait()
153
154     def test_base_platform_names(self):
155         return ('linux', 'mac', 'win')
156
157     def test_expectations(self):
158         """Returns the test expectations for this port.
159
160         Basically this string should contain the equivalent of a
161         test_expectations file. See test_expectations.py for more details."""
162         expectations_file = self.path_to_test_expectations_file()
163         return file(expectations_file, "r").read()
164
165     def test_platform_names(self):
166         return self.test_base_platform_names() + ('win-xp',
167             'win-vista', 'win-7')
168
169     #
170     # PROTECTED METHODS
171     #
172     # These routines should only be called by other methods in this file
173     # or any subclasses.
174     #
175
176     def _check_driver_build_up_to_date(self, target):
177         if target in ('Debug', 'Release'):
178             try:
179                 debug_path = self._path_to_driver('Debug')
180                 release_path = self._path_to_driver('Release')
181
182                 debug_mtime = os.stat(debug_path).st_mtime
183                 release_mtime = os.stat(release_path).st_mtime
184
185                 if (debug_mtime > release_mtime and target == 'Release' or
186                     release_mtime > debug_mtime and target == 'Debug'):
187                     logging.warning('You are not running the most '
188                                     'recent test_shell binary. You need to '
189                                     'pass --debug or not to select between '
190                                     'Debug and Release.')
191                     logging.warning('')
192             # This will fail if we don't have both a debug and release binary.
193             # That's fine because, in this case, we must already be running the
194             # most up-to-date one.
195             except OSError:
196                 pass
197         return True
198
199     def _chromium_baseline_path(self, platform):
200         if platform is None:
201             platform = self.name()
202         return self.path_from_chromium_base('webkit', 'data', 'layout_tests',
203             'platform', platform, 'LayoutTests')
204
205
206 class ChromiumDriver(base.Driver):
207     """Abstract interface for the DumpRenderTree interface."""
208
209     def __init__(self, port, image_path, options):
210         self._port = port
211         self._options = options
212         self._target = port._options.target
213         self._image_path = image_path
214
215         cmd = []
216         # Hook for injecting valgrind or other runtime instrumentation,
217         # used by e.g. tools/valgrind/valgrind_tests.py.
218         wrapper = os.environ.get("BROWSER_WRAPPER", None)
219         if wrapper != None:
220             cmd += [wrapper]
221         if self._port._options.wrapper:
222             # This split() isn't really what we want -- it incorrectly will
223             # split quoted strings within the wrapper argument -- but in
224             # practice it shouldn't come up and the --help output warns
225             # about it anyway.
226             cmd += self._options.wrapper.split()
227         cmd += [port._path_to_driver(), '--layout-tests']
228         if options:
229             cmd += options
230
231         # We need to pass close_fds=True to work around Python bug #2320
232         # (otherwise we can hang when we kill test_shell when we are running
233         # multiple threads). See http://bugs.python.org/issue2320 .
234         # Note that close_fds isn't supported on Windows, but this bug only
235         # shows up on Mac and Linux.
236         close_flag = sys.platform not in ('win32', 'cygwin')
237         self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
238                                       stdout=subprocess.PIPE,
239                                       stderr=subprocess.STDOUT,
240                                       close_fds=close_flag)
241     def poll(self):
242         return self._proc.poll()
243
244     def returncode(self):
245         return self._proc.returncode
246
247     def run_test(self, uri, timeoutms, checksum):
248         output = []
249         error = []
250         crash = False
251         timeout = False
252         actual_uri = None
253         actual_checksum = None
254
255         start_time = time.time()
256         cmd = uri
257         if timeoutms:
258             cmd += ' ' + str(timeoutms)
259         if checksum:
260             cmd += ' ' + checksum
261         cmd += "\n"
262
263         self._proc.stdin.write(cmd)
264         line = self._proc.stdout.readline()
265         while line.rstrip() != "#EOF":
266             # Make sure we haven't crashed.
267             if line == '' and self.poll() is not None:
268                 # This is hex code 0xc000001d, which is used for abrupt
269                 # termination. This happens if we hit ctrl+c from the prompt
270                 # and we happen to be waiting on the test_shell.
271                 # sdoyon: Not sure for which OS and in what circumstances the
272                 # above code is valid. What works for me under Linux to detect
273                 # ctrl+c is for the subprocess returncode to be negative
274                 # SIGINT. And that agrees with the subprocess documentation.
275                 if (-1073741510 == self._proc.returncode or
276                     - signal.SIGINT == self._proc.returncode):
277                     raise KeyboardInterrupt
278                 crash = True
279                 break
280
281             # Don't include #URL lines in our output
282             if line.startswith("#URL:"):
283                 actual_uri = line.rstrip()[5:]
284                 if uri != actual_uri:
285                     logging.fatal("Test got out of sync:\n|%s|\n|%s|" %
286                                 (uri, actual_uri))
287                     raise AssertionError("test out of sync")
288             elif line.startswith("#MD5:"):
289                 actual_checksum = line.rstrip()[5:]
290             elif line.startswith("#TEST_TIMED_OUT"):
291                 timeout = True
292                 # Test timed out, but we still need to read until #EOF.
293             elif actual_uri:
294                 output.append(line)
295             else:
296                 error.append(line)
297
298             line = self._proc.stdout.readline()
299
300         return (crash, timeout, actual_checksum, ''.join(output),
301                 ''.join(error))
302
303     def stop(self):
304         if self._proc:
305             self._proc.stdin.close()
306             self._proc.stdout.close()
307             if self._proc.stderr:
308                 self._proc.stderr.close()
309             if sys.platform not in ('win32', 'cygwin'):
310                 # Closing stdin/stdout/stderr hangs sometimes on OS X,
311                 # (see __init__(), above), and anyway we don't want to hang
312                 # the harness if test_shell is buggy, so we wait a couple
313                 # seconds to give test_shell a chance to clean up, but then
314                 # force-kill the process if necessary.
315                 KILL_TIMEOUT = 3.0
316                 timeout = time.time() + KILL_TIMEOUT
317                 while self._proc.poll() is None and time.time() < timeout:
318                     time.sleep(0.1)
319                 if self._proc.poll() is None:
320                     logging.warning('stopping test driver timed out, '
321                                     'killing it')
322                     null = open(os.devnull, "w")
323                     subprocess.Popen(["kill", "-9",
324                                      str(self._proc.pid)], stderr=null)
325                     null.close()