2010-04-28 Eric Seidel <eric@webkit.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             # wait() is not threadsafe and can throw OSError due to:
205             # http://bugs.python.org/issue1731717
206             self._helper.wait()
207
208     def test_base_platform_names(self):
209         return ('linux', 'mac', 'win')
210
211     def test_expectations(self):
212         """Returns the test expectations for this port.
213
214         Basically this string should contain the equivalent of a
215         test_expectations file. See test_expectations.py for more details."""
216         expectations_path = self.path_to_test_expectations_file()
217         with codecs.open(expectations_path, "r", "utf-8") as file:
218             return file.read()
219
220     def test_expectations_overrides(self):
221         try:
222             overrides_path = self.path_from_chromium_base('webkit', 'tools',
223                 'layout_tests', 'test_expectations.txt')
224         except AssertionError:
225             return None
226         if not os.path.exists(overrides_path):
227             return None
228         with codecs.open(overrides_path, "r", "utf-8") as file:
229             return file.read()
230
231     def test_platform_names(self):
232         return self.test_base_platform_names() + ('win-xp',
233             'win-vista', 'win-7')
234
235     def test_platform_name_to_name(self, test_platform_name):
236         if test_platform_name in self.test_platform_names():
237             return 'chromium-' + test_platform_name
238         raise ValueError('Unsupported test_platform_name: %s' %
239                          test_platform_name)
240
241     #
242     # PROTECTED METHODS
243     #
244     # These routines should only be called by other methods in this file
245     # or any subclasses.
246     #
247
248     def _check_driver_build_up_to_date(self, configuration):
249         if configuration in ('Debug', 'Release'):
250             try:
251                 debug_path = self._path_to_driver('Debug')
252                 release_path = self._path_to_driver('Release')
253
254                 debug_mtime = os.stat(debug_path).st_mtime
255                 release_mtime = os.stat(release_path).st_mtime
256
257                 if (debug_mtime > release_mtime and configuration == 'Release' or
258                     release_mtime > debug_mtime and configuration == 'Debug'):
259                     _log.warning('You are not running the most '
260                                  'recent DumpRenderTree binary. You need to '
261                                  'pass --debug or not to select between '
262                                  'Debug and Release.')
263                     _log.warning('')
264             # This will fail if we don't have both a debug and release binary.
265             # That's fine because, in this case, we must already be running the
266             # most up-to-date one.
267             except OSError:
268                 pass
269         return True
270
271     def _chromium_baseline_path(self, platform):
272         if platform is None:
273             platform = self.name()
274         return self.path_from_webkit_base('LayoutTests', 'platform', platform)
275
276
277 class ChromiumDriver(base.Driver):
278     """Abstract interface for test_shell."""
279
280     def __init__(self, port, image_path, options, executive=Executive()):
281         self._port = port
282         self._configuration = port._options.configuration
283         # FIXME: _options is very confusing, because it's not an Options() element.
284         # FIXME: These don't need to be passed into the constructor, but could rather
285         # be passed into .start()
286         self._options = options
287         self._image_path = image_path
288         self._executive = executive
289
290     def start(self):
291         # FIXME: Should be an error to call this method twice.
292         cmd = []
293         # FIXME: We should not be grabbing at self._port._options.wrapper directly.
294         cmd += self._command_wrapper(self._port._options.wrapper)
295         cmd += [self._port._path_to_driver(), '--layout-tests']
296         if self._options:
297             cmd += self._options
298
299         # We need to pass close_fds=True to work around Python bug #2320
300         # (otherwise we can hang when we kill DumpRenderTree when we are running
301         # multiple threads). See http://bugs.python.org/issue2320 .
302         # Note that close_fds isn't supported on Windows, but this bug only
303         # shows up on Mac and Linux.
304         close_flag = sys.platform not in ('win32', 'cygwin')
305         self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
306                                       stdout=subprocess.PIPE,
307                                       stderr=subprocess.STDOUT,
308                                       close_fds=close_flag)
309
310     def poll(self):
311         # poll() is not threadsafe and can throw OSError due to:
312         # http://bugs.python.org/issue1731717
313         return self._proc.poll()
314
315     def returncode(self):
316         return self._proc.returncode
317
318     def _write_command_and_read_line(self, input=None):
319         """Returns a tuple: (line, did_crash)"""
320         try:
321             if input:
322                 self._proc.stdin.write(input)
323             # DumpRenderTree text output is always UTF-8.  However some tests
324             # (e.g. webarchive) may spit out binary data instead of text so we
325             # don't bother to decode the output (for either DRT or test_shell).
326             line = self._proc.stdout.readline()
327             # We could assert() here that line correctly decodes as UTF-8.
328             return (line, False)
329         except IOError, e:
330             _log.error("IOError communicating w/ test_shell: " + str(e))
331             return (None, True)
332
333     def _test_shell_command(self, uri, timeoutms, checksum):
334         cmd = uri
335         if timeoutms:
336             cmd += ' ' + str(timeoutms)
337         if checksum:
338             cmd += ' ' + checksum
339         cmd += "\n"
340         return cmd
341
342     def run_test(self, uri, timeoutms, checksum):
343         output = []
344         error = []
345         crash = False
346         timeout = False
347         actual_uri = None
348         actual_checksum = None
349
350         start_time = time.time()
351
352         cmd = self._test_shell_command(uri, timeoutms, checksum)
353         (line, crash) = self._write_command_and_read_line(input=cmd)
354
355         while not crash and line.rstrip() != "#EOF":
356             # Make sure we haven't crashed.
357             if line == '' and self.poll() is not None:
358                 # This is hex code 0xc000001d, which is used for abrupt
359                 # termination. This happens if we hit ctrl+c from the prompt
360                 # and we happen to be waiting on test_shell.
361                 # sdoyon: Not sure for which OS and in what circumstances the
362                 # above code is valid. What works for me under Linux to detect
363                 # ctrl+c is for the subprocess returncode to be negative
364                 # SIGINT. And that agrees with the subprocess documentation.
365                 if (-1073741510 == self._proc.returncode or
366                     - signal.SIGINT == self._proc.returncode):
367                     raise KeyboardInterrupt
368                 crash = True
369                 break
370
371             # Don't include #URL lines in our output
372             if line.startswith("#URL:"):
373                 actual_uri = line.rstrip()[5:]
374                 if uri != actual_uri:
375                     _log.fatal("Test got out of sync:\n|%s|\n|%s|" %
376                                (uri, actual_uri))
377                     raise AssertionError("test out of sync")
378             elif line.startswith("#MD5:"):
379                 actual_checksum = line.rstrip()[5:]
380             elif line.startswith("#TEST_TIMED_OUT"):
381                 timeout = True
382                 # Test timed out, but we still need to read until #EOF.
383             elif actual_uri:
384                 output.append(line)
385             else:
386                 error.append(line)
387
388             (line, crash) = self._write_command_and_read_line(input=None)
389
390         return (crash, timeout, actual_checksum, ''.join(output),
391                 ''.join(error))
392
393     def stop(self):
394         if self._proc:
395             self._proc.stdin.close()
396             self._proc.stdout.close()
397             if self._proc.stderr:
398                 self._proc.stderr.close()
399             if sys.platform not in ('win32', 'cygwin'):
400                 # Closing stdin/stdout/stderr hangs sometimes on OS X,
401                 # (see __init__(), above), and anyway we don't want to hang
402                 # the harness if test_shell is buggy, so we wait a couple
403                 # seconds to give test_shell a chance to clean up, but then
404                 # force-kill the process if necessary.
405                 KILL_TIMEOUT = 3.0
406                 timeout = time.time() + KILL_TIMEOUT
407                 # poll() is not threadsafe and can throw OSError due to:
408                 # http://bugs.python.org/issue1731717
409                 while self._proc.poll() is None and time.time() < timeout:
410                     time.sleep(0.1)
411                 # poll() is not threadsafe and can throw OSError due to:
412                 # http://bugs.python.org/issue1731717
413                 if self._proc.poll() is None:
414                     _log.warning('stopping test driver timed out, '
415                                  'killing it')
416                     self._executive.kill_process(self._proc.pid)