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