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