2010-04-19 Kent Tamura <tkent@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 import webbrowser
40
41 import base
42 import http_server
43 import webkit
44 import websocket_server
45
46 _log = logging.getLogger("webkitpy.layout_tests.port.chromium")
47
48
49 # FIXME: This function doesn't belong in this package.
50 def check_file_exists(path_to_file, file_description, override_step=None,
51                       logging=True):
52     """Verify the file is present where expected or log an error.
53
54     Args:
55         file_name: The (human friendly) name or description of the file
56             you're looking for (e.g., "HTTP Server"). Used for error logging.
57         override_step: An optional string to be logged if the check fails.
58         logging: Whether or not log the error messages."""
59     if not os.path.exists(path_to_file):
60         if logging:
61             _log.error('Unable to find %s' % file_description)
62             _log.error('    at %s' % path_to_file)
63             if override_step:
64                 _log.error('    %s' % override_step)
65                 _log.error('')
66         return False
67     return True
68
69
70 class ChromiumPort(base.Port):
71     """Abstract base class for Chromium implementations of the Port class."""
72
73     def __init__(self, port_name=None, options=None):
74         base.Port.__init__(self, port_name, options)
75         self._chromium_base_dir = None
76
77     def baseline_path(self):
78         return self._webkit_baseline_path(self._name)
79
80     def check_build(self, needs_http):
81         result = True
82         dump_render_tree_binary_path = self._path_to_driver()
83         result = check_file_exists(dump_render_tree_binary_path,
84                                    'test driver')
85         if result:
86             result = (self._check_driver_build_up_to_date(self._options.configuration)
87                       and result)
88         else:
89             _log.error('')
90
91         helper_path = self._path_to_helper()
92         if helper_path:
93             result = check_file_exists(helper_path,
94                                        'layout test helper') and result
95
96         if self._options.pixel_tests:
97             result = self.check_image_diff(
98                 'To override, invoke with --no-pixel-tests') and result
99
100         return result
101
102     def check_sys_deps(self, needs_http):
103         dump_render_tree_binary_path = self._path_to_driver()
104         proc = subprocess.Popen([dump_render_tree_binary_path,
105                                 '--check-layout-test-sys-deps'])
106         if proc.wait():
107             _log.error('System dependencies check failed.')
108             _log.error('To override, invoke with --nocheck-sys-deps')
109             _log.error('')
110             return False
111         return True
112
113     def check_image_diff(self, override_step=None, logging=True):
114         image_diff_path = self._path_to_image_diff()
115         return check_file_exists(image_diff_path, 'image diff exe',
116                                  override_step, logging)
117
118     def driver_name(self):
119         return "test_shell"
120
121     def path_from_chromium_base(self, *comps):
122         """Returns the full path to path made by joining the top of the
123         Chromium source tree and the list of path components in |*comps|."""
124         if not self._chromium_base_dir:
125             abspath = os.path.abspath(__file__)
126             offset = abspath.find('third_party')
127             if offset == -1:
128                 raise AssertionError('could not find Chromium base dir from ' +
129                                      abspath)
130             self._chromium_base_dir = abspath[0:offset]
131         return os.path.join(self._chromium_base_dir, *comps)
132
133     def path_to_test_expectations_file(self):
134         return self.path_from_webkit_base('LayoutTests', 'platform',
135             'chromium', 'test_expectations.txt')
136
137     def results_directory(self):
138         try:
139             return self.path_from_chromium_base('webkit',
140                 self._options.configuration, self._options.results_directory)
141         except AssertionError:
142             return self.path_from_webkit_base('WebKit', 'chromium',
143                 'xcodebuild', self._options.configuration,
144                 self._options.results_directory)
145
146     def setup_test_run(self):
147         # Delete the disk cache if any to ensure a clean test run.
148         dump_render_tree_binary_path = self._path_to_driver()
149         cachedir = os.path.split(dump_render_tree_binary_path)[0]
150         cachedir = os.path.join(cachedir, "cache")
151         if os.path.exists(cachedir):
152             shutil.rmtree(cachedir)
153
154     def show_results_html_file(self, results_filename):
155         uri = self.filename_to_uri(results_filename)
156         if self._options.use_drt:
157             webbrowser.open(uri, new=1)
158         else:
159             subprocess.Popen([self._path_to_driver(), uri])
160
161     def start_driver(self, image_path, options):
162         """Starts a new Driver and returns a handle to it."""
163         if self._options.use_drt:
164             return webkit.WebKitDriver(self, image_path, options)
165         return ChromiumDriver(self, image_path, options)
166
167     def start_helper(self):
168         helper_path = self._path_to_helper()
169         if helper_path:
170             _log.debug("Starting layout helper %s" % helper_path)
171             self._helper = subprocess.Popen([helper_path],
172                 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None)
173             is_ready = self._helper.stdout.readline()
174             if not is_ready.startswith('ready'):
175                 _log.error("layout_test_helper failed to be ready")
176
177     def stop_helper(self):
178         if self._helper:
179             _log.debug("Stopping layout test helper")
180             self._helper.stdin.write("x\n")
181             self._helper.stdin.close()
182             self._helper.wait()
183
184     def test_base_platform_names(self):
185         return ('linux', 'mac', 'win')
186
187     def test_expectations(self):
188         """Returns the test expectations for this port.
189
190         Basically this string should contain the equivalent of a
191         test_expectations file. See test_expectations.py for more details."""
192         expectations_file = self.path_to_test_expectations_file()
193         return file(expectations_file, "r").read()
194
195     def test_expectations_overrides(self):
196         try:
197             overrides_file = self.path_from_chromium_base('webkit', 'tools',
198                 'layout_tests', 'test_expectations.txt')
199         except AssertionError:
200             return None
201         if os.path.exists(overrides_file):
202             return file(overrides_file, "r").read()
203         else:
204             return None
205
206     def test_platform_names(self):
207         return self.test_base_platform_names() + ('win-xp',
208             'win-vista', 'win-7')
209
210     def test_platform_name_to_name(self, test_platform_name):
211         if test_platform_name in self.test_platform_names():
212             return 'chromium-' + test_platform_name
213         raise ValueError('Unsupported test_platform_name: %s' %
214                          test_platform_name)
215
216     #
217     # PROTECTED METHODS
218     #
219     # These routines should only be called by other methods in this file
220     # or any subclasses.
221     #
222
223     def _check_driver_build_up_to_date(self, configuration):
224         if configuration in ('Debug', 'Release'):
225             try:
226                 debug_path = self._path_to_driver('Debug')
227                 release_path = self._path_to_driver('Release')
228
229                 debug_mtime = os.stat(debug_path).st_mtime
230                 release_mtime = os.stat(release_path).st_mtime
231
232                 if (debug_mtime > release_mtime and configuration == 'Release' or
233                     release_mtime > debug_mtime and configuration == 'Debug'):
234                     _log.warning('You are not running the most '
235                                  'recent DumpRenderTree binary. You need to '
236                                  'pass --debug or not to select between '
237                                  'Debug and Release.')
238                     _log.warning('')
239             # This will fail if we don't have both a debug and release binary.
240             # That's fine because, in this case, we must already be running the
241             # most up-to-date one.
242             except OSError:
243                 pass
244         return True
245
246     def _chromium_baseline_path(self, platform):
247         if platform is None:
248             platform = self.name()
249         return self.path_from_webkit_base('LayoutTests', 'platform', platform)
250
251
252 class ChromiumDriver(base.Driver):
253     """Abstract interface for the DumpRenderTree interface."""
254
255     def __init__(self, port, image_path, options):
256         self._port = port
257         self._options = options
258         self._configuration = port._options.configuration
259         self._image_path = image_path
260
261         cmd = []
262         # Hook for injecting valgrind or other runtime instrumentation,
263         # used by e.g. tools/valgrind/valgrind_tests.py.
264         wrapper = os.environ.get("BROWSER_WRAPPER", None)
265         if wrapper != None:
266             cmd += [wrapper]
267         if self._port._options.wrapper:
268             # This split() isn't really what we want -- it incorrectly will
269             # split quoted strings within the wrapper argument -- but in
270             # practice it shouldn't come up and the --help output warns
271             # about it anyway.
272             cmd += self._options.wrapper.split()
273         cmd += [port._path_to_driver(), '--layout-tests']
274         if options:
275             cmd += options
276
277         # We need to pass close_fds=True to work around Python bug #2320
278         # (otherwise we can hang when we kill DumpRenderTree when we are running
279         # multiple threads). See http://bugs.python.org/issue2320 .
280         # Note that close_fds isn't supported on Windows, but this bug only
281         # shows up on Mac and Linux.
282         close_flag = sys.platform not in ('win32', 'cygwin')
283         self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
284                                       stdout=subprocess.PIPE,
285                                       stderr=subprocess.STDOUT,
286                                       close_fds=close_flag)
287     def poll(self):
288         return self._proc.poll()
289
290     def returncode(self):
291         return self._proc.returncode
292
293     def run_test(self, uri, timeoutms, checksum):
294         output = []
295         error = []
296         crash = False
297         timeout = False
298         actual_uri = None
299         actual_checksum = None
300
301         start_time = time.time()
302         cmd = uri
303         if timeoutms:
304             cmd += ' ' + str(timeoutms)
305         if checksum:
306             cmd += ' ' + checksum
307         cmd += "\n"
308
309         self._proc.stdin.write(cmd)
310         line = self._proc.stdout.readline()
311         while line.rstrip() != "#EOF":
312             # Make sure we haven't crashed.
313             if line == '' and self.poll() is not None:
314                 # This is hex code 0xc000001d, which is used for abrupt
315                 # termination. This happens if we hit ctrl+c from the prompt
316                 # and we happen to be waiting on the DumpRenderTree.
317                 # sdoyon: Not sure for which OS and in what circumstances the
318                 # above code is valid. What works for me under Linux to detect
319                 # ctrl+c is for the subprocess returncode to be negative
320                 # SIGINT. And that agrees with the subprocess documentation.
321                 if (-1073741510 == self._proc.returncode or
322                     - signal.SIGINT == self._proc.returncode):
323                     raise KeyboardInterrupt
324                 crash = True
325                 break
326
327             # Don't include #URL lines in our output
328             if line.startswith("#URL:"):
329                 actual_uri = line.rstrip()[5:]
330                 if uri != actual_uri:
331                     _log.fatal("Test got out of sync:\n|%s|\n|%s|" %
332                                (uri, actual_uri))
333                     raise AssertionError("test out of sync")
334             elif line.startswith("#MD5:"):
335                 actual_checksum = line.rstrip()[5:]
336             elif line.startswith("#TEST_TIMED_OUT"):
337                 timeout = True
338                 # Test timed out, but we still need to read until #EOF.
339             elif actual_uri:
340                 output.append(line)
341             else:
342                 error.append(line)
343
344             line = self._proc.stdout.readline()
345
346         return (crash, timeout, actual_checksum, ''.join(output),
347                 ''.join(error))
348
349     def stop(self):
350         if self._proc:
351             self._proc.stdin.close()
352             self._proc.stdout.close()
353             if self._proc.stderr:
354                 self._proc.stderr.close()
355             if sys.platform not in ('win32', 'cygwin'):
356                 # Closing stdin/stdout/stderr hangs sometimes on OS X,
357                 # (see __init__(), above), and anyway we don't want to hang
358                 # the harness if DumpRenderTree is buggy, so we wait a couple
359                 # seconds to give DumpRenderTree a chance to clean up, but then
360                 # force-kill the process if necessary.
361                 KILL_TIMEOUT = 3.0
362                 timeout = time.time() + KILL_TIMEOUT
363                 while self._proc.poll() is None and time.time() < timeout:
364                     time.sleep(0.1)
365                 if self._proc.poll() is None:
366                     _log.warning('stopping test driver timed out, '
367                                  'killing it')
368                     null = open(os.devnull, "w")
369                     subprocess.Popen(["kill", "-9",
370                                      str(self._proc.pid)], stderr=null)
371                     null.close()