2010-02-22 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, str):
46     """Verify the executable is present where expected or log an error."""
47     if not os.path.exists(path_to_file):
48         logging.error('Unable to find %s at %s' % (str, path_to_file))
49         return False
50     return True
51
52
53 class ChromiumPort(base.Port):
54     """Abstract base class for Chromium implementations of the Port class."""
55
56     def __init__(self, port_name=None, options=None):
57         base.Port.__init__(self, port_name, options)
58         self._chromium_base_dir = None
59
60     def baseline_path(self):
61         return self._chromium_baseline_path(self._name)
62
63     def check_sys_deps(self, needs_http):
64         result = True
65         test_shell_binary_path = self._path_to_driver()
66         result = check_file_exists(test_shell_binary_path,
67                                    'test driver')
68         if result:
69             result = (self._check_build_up_to_date(self._options.target)
70                       and result)
71
72             proc = subprocess.Popen([test_shell_binary_path,
73                                      '--check-layout-test-sys-deps'])
74             if proc.wait() != 0:
75                 logging.error('System dependencies check failed.')
76                 logging.error('To override, invoke with --nocheck-sys-deps')
77                 logging.error('')
78                 result = False
79
80         else:
81             logging.error('')
82
83
84         if not self._options.no_pixel_tests:
85             image_diff_path = self._path_to_image_diff()
86             if not check_file_exists(image_diff_path, 'image diff exe'):
87                 logging.error('To override, invoke with --no-pixel-tests')
88                 logging.error('')
89                 result = False
90
91         return result
92
93     def compare_text(self, actual_text, expected_text):
94         return actual_text != expected_text
95
96     def path_from_chromium_base(self, *comps):
97         """Returns the full path to path made by joining the top of the
98         Chromium source tree and the list of path components in |*comps|."""
99         if not self._chromium_base_dir:
100             abspath = os.path.abspath(__file__)
101             self._chromium_base_dir = abspath[0:abspath.find('third_party')]
102         return os.path.join(self._chromium_base_dir, *comps)
103
104     def path_to_test_expectations_file(self):
105         return self.path_from_chromium_base('webkit', 'tools', 'layout_tests',
106                                             'test_expectations.txt')
107
108     def results_directory(self):
109         return self.path_from_chromium_base('webkit', self._options.target,
110                                             self._options.results_directory)
111
112     def setup_test_run(self):
113         # Delete the disk cache if any to ensure a clean test run.
114         test_shell_binary_path = self._path_to_driver()
115         cachedir = os.path.split(test_shell_binary_path)[0]
116         cachedir = os.path.join(cachedir, "cache")
117         if os.path.exists(cachedir):
118             shutil.rmtree(cachedir)
119
120     def show_results_html_file(self, results_filename):
121         subprocess.Popen([self._path_to_driver(),
122                           self.filename_to_uri(results_filename)])
123
124     def start_driver(self, image_path, options):
125         """Starts a new Driver and returns a handle to it."""
126         return ChromiumDriver(self, image_path, options)
127
128     def start_helper(self):
129         helper_path = self._path_to_helper()
130         if helper_path:
131             logging.debug("Starting layout helper %s" % helper_path)
132             self._helper = subprocess.Popen([helper_path],
133                 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None)
134             is_ready = self._helper.stdout.readline()
135             if not is_ready.startswith('ready'):
136                 logging.error("layout_test_helper failed to be ready")
137
138     def stop_helper(self):
139         if self._helper:
140             logging.debug("Stopping layout test helper")
141             self._helper.stdin.write("x\n")
142             self._helper.stdin.close()
143             self._helper.wait()
144
145     def test_base_platform_names(self):
146         return ('linux', 'mac', 'win')
147
148     def test_expectations(self):
149         """Returns the test expectations for this port.
150
151         Basically this string should contain the equivalent of a
152         test_expectations file. See test_expectations.py for more details."""
153         expectations_file = self.path_to_test_expectations_file()
154         return file(expectations_file, "r").read()
155
156     def test_platform_names(self):
157         return self.test_base_platform_names() + ('win-xp',
158             'win-vista', 'win-7')
159
160     #
161     # PROTECTED METHODS
162     #
163     # These routines should only be called by other methods in this file
164     # or any subclasses.
165     #
166
167     def _check_build_up_to_date(self, target):
168         if target in ('Debug', 'Release'):
169             try:
170                 debug_path = self._path_to_driver('Debug')
171                 release_path = self._path_to_driver('Release')
172
173                 debug_mtime = os.stat(debug_path).st_mtime
174                 release_mtime = os.stat(release_path).st_mtime
175
176                 if (debug_mtime > release_mtime and target == 'Release' or
177                     release_mtime > debug_mtime and target == 'Debug'):
178                     logging.warning('You are not running the most '
179                                     'recent test_shell binary. You need to '
180                                     'pass --debug or not to select between '
181                                     'Debug and Release.')
182                     logging.warning('')
183             # This will fail if we don't have both a debug and release binary.
184             # That's fine because, in this case, we must already be running the
185             # most up-to-date one.
186             except OSError:
187                 pass
188         return True
189
190     def _chromium_baseline_path(self, platform):
191         if platform is None:
192             platform = self.name()
193         return self.path_from_chromium_base('webkit', 'data', 'layout_tests',
194             'platform', platform, 'LayoutTests')
195
196
197 class ChromiumDriver(base.Driver):
198     """Abstract interface for the DumpRenderTree interface."""
199
200     def __init__(self, port, image_path, options):
201         self._port = port
202         self._options = options
203         self._target = port._options.target
204         self._image_path = image_path
205
206         cmd = []
207         # Hook for injecting valgrind or other runtime instrumentation,
208         # used by e.g. tools/valgrind/valgrind_tests.py.
209         wrapper = os.environ.get("BROWSER_WRAPPER", None)
210         if wrapper != None:
211             cmd += [wrapper]
212         if self._port._options.wrapper:
213             # This split() isn't really what we want -- it incorrectly will
214             # split quoted strings within the wrapper argument -- but in
215             # practice it shouldn't come up and the --help output warns
216             # about it anyway.
217             cmd += self._options.wrapper.split()
218         cmd += [port._path_to_driver(), '--layout-tests']
219         if options:
220             cmd += options
221         self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
222                                       stdout=subprocess.PIPE,
223                                       stderr=subprocess.STDOUT)
224
225     def poll(self):
226         return self._proc.poll()
227
228     def returncode(self):
229         return self._proc.returncode
230
231     def run_test(self, uri, timeoutms, checksum):
232         output = []
233         error = []
234         crash = False
235         timeout = False
236         actual_uri = None
237         actual_checksum = None
238
239         start_time = time.time()
240         cmd = uri
241         if timeoutms:
242             cmd += ' ' + str(timeoutms)
243         if checksum:
244             cmd += ' ' + checksum
245         cmd += "\n"
246
247         self._proc.stdin.write(cmd)
248         line = self._proc.stdout.readline()
249         while line.rstrip() != "#EOF":
250             # Make sure we haven't crashed.
251             if line == '' and self.poll() is not None:
252                 # This is hex code 0xc000001d, which is used for abrupt
253                 # termination. This happens if we hit ctrl+c from the prompt
254                 # and we happen to be waiting on the test_shell.
255                 # sdoyon: Not sure for which OS and in what circumstances the
256                 # above code is valid. What works for me under Linux to detect
257                 # ctrl+c is for the subprocess returncode to be negative
258                 # SIGINT. And that agrees with the subprocess documentation.
259                 if (-1073741510 == self._proc.returncode or
260                     - signal.SIGINT == self._proc.returncode):
261                     raise KeyboardInterrupt
262                 crash = True
263                 break
264
265             # Don't include #URL lines in our output
266             if line.startswith("#URL:"):
267                 actual_uri = line.rstrip()[5:]
268                 if uri != actual_uri:
269                     logging.fatal("Test got out of sync:\n|%s|\n|%s|" %
270                                 (uri, actual_uri))
271                     raise AssertionError("test out of sync")
272             elif line.startswith("#MD5:"):
273                 actual_checksum = line.rstrip()[5:]
274             elif line.startswith("#TEST_TIMED_OUT"):
275                 timeout = True
276                 # Test timed out, but we still need to read until #EOF.
277             elif actual_uri:
278                 output.append(line)
279             else:
280                 error.append(line)
281
282             line = self._proc.stdout.readline()
283
284         return (crash, timeout, actual_checksum, ''.join(output),
285                 ''.join(error))
286
287     def stop(self):
288         if self._proc:
289             self._proc.stdin.close()
290             self._proc.stdout.close()
291             if self._proc.stderr:
292                 self._proc.stderr.close()
293             if (sys.platform not in ('win32', 'cygwin') and
294                 not self._proc.poll()):
295                 # Closing stdin/stdout/stderr hangs sometimes on OS X.
296                 null = open(os.devnull, "w")
297                 subprocess.Popen(["kill", "-9",
298                                  str(self._proc.pid)], stderr=null)
299                 null.close()