9aa0946f89bea6165b33ce5543f54235324bb2b4
[WebKit.git] / Tools / 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 errno
36 import logging
37 import os
38 import re
39 import shutil
40 import signal
41 import subprocess
42 import sys
43 import tempfile
44 import time
45 import webbrowser
46
47 from webkitpy.common.system import executive
48 from webkitpy.common.system.path import cygpath
49 from webkitpy.layout_tests.layout_package import test_expectations
50 from webkitpy.layout_tests.layout_package import test_output
51
52 import base
53 import http_server
54
55 # Chromium DRT on OSX uses WebKitDriver.
56 if sys.platform == 'darwin':
57     import webkit
58
59 import websocket_server
60
61 _log = logging.getLogger("webkitpy.layout_tests.port.chromium")
62
63
64 # FIXME: This function doesn't belong in this package.
65 def check_file_exists(path_to_file, file_description, override_step=None,
66                       logging=True):
67     """Verify the file is present where expected or log an error.
68
69     Args:
70         file_name: The (human friendly) name or description of the file
71             you're looking for (e.g., "HTTP Server"). Used for error logging.
72         override_step: An optional string to be logged if the check fails.
73         logging: Whether or not log the error messages."""
74     if not os.path.exists(path_to_file):
75         if logging:
76             _log.error('Unable to find %s' % file_description)
77             _log.error('    at %s' % path_to_file)
78             if override_step:
79                 _log.error('    %s' % override_step)
80                 _log.error('')
81         return False
82     return True
83
84
85 class ChromiumPort(base.Port):
86     """Abstract base class for Chromium implementations of the Port class."""
87
88     def __init__(self, **kwargs):
89         base.Port.__init__(self, **kwargs)
90         self._chromium_base_dir = None
91
92     def baseline_path(self):
93         return self._webkit_baseline_path(self._name)
94
95     def check_build(self, needs_http):
96         result = True
97
98         dump_render_tree_binary_path = self._path_to_driver()
99         result = check_file_exists(dump_render_tree_binary_path,
100                                     'test driver') and result
101         if result and self.get_option('build'):
102             result = self._check_driver_build_up_to_date(
103                 self.get_option('configuration'))
104         else:
105             _log.error('')
106
107         helper_path = self._path_to_helper()
108         if helper_path:
109             result = check_file_exists(helper_path,
110                                        'layout test helper') and result
111
112         if self.get_option('pixel_tests'):
113             result = self.check_image_diff(
114                 'To override, invoke with --no-pixel-tests') and result
115
116         # It's okay if pretty patch isn't available, but we will at
117         # least log a message.
118         self.check_pretty_patch()
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
125         local_error = executive.ScriptError()
126
127         def error_handler(script_error):
128             local_error.exit_code = script_error.exit_code
129
130         output = self._executive.run_command(cmd, error_handler=error_handler)
131         if local_error.exit_code:
132             _log.error('System dependencies check failed.')
133             _log.error('To override, invoke with --nocheck-sys-deps')
134             _log.error('')
135             _log.error(output)
136             return False
137         return True
138
139     def check_image_diff(self, override_step=None, logging=True):
140         image_diff_path = self._path_to_image_diff()
141         return check_file_exists(image_diff_path, 'image diff exe',
142                                  override_step, logging)
143
144     def diff_image(self, expected_contents, actual_contents,
145                    diff_filename=None):
146         executable = self._path_to_image_diff()
147
148         tempdir = tempfile.mkdtemp()
149         expected_filename = os.path.join(tempdir, "expected.png")
150         with open(expected_filename, 'w+b') as file:
151             file.write(expected_contents)
152         actual_filename = os.path.join(tempdir, "actual.png")
153         with open(actual_filename, 'w+b') as file:
154             file.write(actual_contents)
155
156         if diff_filename:
157             cmd = [executable, '--diff', expected_filename,
158                    actual_filename, diff_filename]
159         else:
160             cmd = [executable, expected_filename, actual_filename]
161
162         result = True
163         try:
164             exit_code = self._executive.run_command(cmd, return_exit_code=True)
165             if exit_code == 0:
166                 # The images are the same.
167                 result = False
168             elif exit_code != 1:
169                 _log.error("image diff returned an exit code of "
170                            + str(exit_code))
171                 # Returning False here causes the script to think that we
172                 # successfully created the diff even though we didn't.  If
173                 # we return True, we think that the images match but the hashes
174                 # don't match.
175                 # FIXME: Figure out why image_diff returns other values.
176                 result = False
177         except OSError, e:
178             if e.errno == errno.ENOENT or e.errno == errno.EACCES:
179                 _compare_available = False
180             else:
181                 raise e
182         finally:
183             shutil.rmtree(tempdir, ignore_errors=True)
184         return result
185
186     def driver_name(self):
187         if self._options.use_test_shell:
188             return "test_shell"
189         return "DumpRenderTree"
190
191     def path_from_chromium_base(self, *comps):
192         """Returns the full path to path made by joining the top of the
193         Chromium source tree and the list of path components in |*comps|."""
194         if not self._chromium_base_dir:
195             abspath = os.path.abspath(__file__)
196             offset = abspath.find('third_party')
197             if offset == -1:
198                 self._chromium_base_dir = os.path.join(
199                     abspath[0:abspath.find('Tools')],
200                     'WebKit', 'chromium')
201             else:
202                 self._chromium_base_dir = abspath[0:offset]
203         return os.path.join(self._chromium_base_dir, *comps)
204
205     def path_to_test_expectations_file(self):
206         return self.path_from_webkit_base('LayoutTests', 'platform',
207             'chromium', 'test_expectations.txt')
208
209     def results_directory(self):
210         try:
211             return self.path_from_chromium_base('webkit',
212                 self.get_option('configuration'),
213                 self.get_option('results_directory'))
214         except AssertionError:
215             return self._build_path(self.get_option('configuration'),
216                                     self.get_option('results_directory'))
217
218     def setup_test_run(self):
219         # Delete the disk cache if any to ensure a clean test run.
220         dump_render_tree_binary_path = self._path_to_driver()
221         cachedir = os.path.split(dump_render_tree_binary_path)[0]
222         cachedir = os.path.join(cachedir, "cache")
223         if os.path.exists(cachedir):
224             shutil.rmtree(cachedir)
225
226     def create_driver(self, worker_number):
227         """Starts a new Driver and returns a handle to it."""
228         if not self.get_option('use_test_shell') and sys.platform == 'darwin':
229             return webkit.WebKitDriver(self, worker_number)
230         return ChromiumDriver(self, worker_number)
231
232     def start_helper(self):
233         helper_path = self._path_to_helper()
234         if helper_path:
235             _log.debug("Starting layout helper %s" % helper_path)
236             # Note: Not thread safe: http://bugs.python.org/issue2320
237             self._helper = subprocess.Popen([helper_path],
238                 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None)
239             is_ready = self._helper.stdout.readline()
240             if not is_ready.startswith('ready'):
241                 _log.error("layout_test_helper failed to be ready")
242
243     def stop_helper(self):
244         if self._helper:
245             _log.debug("Stopping layout test helper")
246             self._helper.stdin.write("x\n")
247             self._helper.stdin.close()
248             # wait() is not threadsafe and can throw OSError due to:
249             # http://bugs.python.org/issue1731717
250             self._helper.wait()
251
252     def test_base_platform_names(self):
253         return ('linux', 'mac', 'win')
254
255     def test_expectations(self):
256         """Returns the test expectations for this port.
257
258         Basically this string should contain the equivalent of a
259         test_expectations file. See test_expectations.py for more details."""
260         expectations_path = self.path_to_test_expectations_file()
261         with codecs.open(expectations_path, "r", "utf-8") as file:
262             return file.read()
263
264     def test_expectations_overrides(self):
265         try:
266             overrides_path = self.path_from_chromium_base('webkit', 'tools',
267                 'layout_tests', 'test_expectations.txt')
268         except AssertionError:
269             return None
270         if not os.path.exists(overrides_path):
271             return None
272         with codecs.open(overrides_path, "r", "utf-8") as file:
273             return file.read()
274
275     def skipped_layout_tests(self, extra_test_files=None):
276         expectations_str = self.test_expectations()
277         overrides_str = self.test_expectations_overrides()
278         test_platform_name = self.test_platform_name()
279         is_debug_mode = False
280
281         all_test_files = self.tests([])
282         if extra_test_files:
283             all_test_files.update(extra_test_files)
284
285         expectations = test_expectations.TestExpectations(
286             self, all_test_files, expectations_str, test_platform_name,
287             is_debug_mode, is_lint_mode=False, overrides=overrides_str)
288         tests_dir = self.layout_tests_dir()
289         return [self.relative_test_filename(test)
290                 for test in expectations.get_tests_with_result_type(test_expectations.SKIP)]
291
292     def test_platform_names(self):
293         return self.test_base_platform_names() + ('win-xp',
294             'win-vista', 'win-7')
295
296     def test_platform_name_to_name(self, test_platform_name):
297         if test_platform_name in self.test_platform_names():
298             return 'chromium-' + test_platform_name
299         raise ValueError('Unsupported test_platform_name: %s' %
300                          test_platform_name)
301
302     def test_repository_paths(self):
303         # Note: for JSON file's backward-compatibility we use 'chrome' rather
304         # than 'chromium' here.
305         repos = super(ChromiumPort, self).test_repository_paths()
306         repos.append(('chrome', self.path_from_chromium_base()))
307         return repos
308
309     #
310     # PROTECTED METHODS
311     #
312     # These routines should only be called by other methods in this file
313     # or any subclasses.
314     #
315
316     def _check_driver_build_up_to_date(self, configuration):
317         if configuration in ('Debug', 'Release'):
318             try:
319                 debug_path = self._path_to_driver('Debug')
320                 release_path = self._path_to_driver('Release')
321
322                 debug_mtime = os.stat(debug_path).st_mtime
323                 release_mtime = os.stat(release_path).st_mtime
324
325                 if (debug_mtime > release_mtime and configuration == 'Release' or
326                     release_mtime > debug_mtime and configuration == 'Debug'):
327                     _log.warning('You are not running the most '
328                                  'recent DumpRenderTree binary. You need to '
329                                  'pass --debug or not to select between '
330                                  'Debug and Release.')
331                     _log.warning('')
332             # This will fail if we don't have both a debug and release binary.
333             # That's fine because, in this case, we must already be running the
334             # most up-to-date one.
335             except OSError:
336                 pass
337         return True
338
339     def _chromium_baseline_path(self, platform):
340         if platform is None:
341             platform = self.name()
342         return self.path_from_webkit_base('LayoutTests', 'platform', platform)
343
344     def _convert_path(self, path):
345         """Handles filename conversion for subprocess command line args."""
346         # See note above in diff_image() for why we need this.
347         if sys.platform == 'cygwin':
348             return cygpath(path)
349         return path
350
351     def _path_to_image_diff(self):
352         binary_name = 'ImageDiff'
353         if self.get_option('use_test_shell'):
354             binary_name = 'image_diff'
355         return self._build_path(self.get_option('configuration'), binary_name)
356
357
358 class ChromiumDriver(base.Driver):
359     """Abstract interface for test_shell."""
360
361     def __init__(self, port, worker_number):
362         self._port = port
363         self._worker_number = worker_number
364         self._image_path = None
365         if self._port.get_option('pixel_tests'):
366             self._image_path = os.path.join(
367                 self._port.get_option('results_directory'),
368                 'png_result%s.png' % self._worker_number)
369
370     def cmd_line(self):
371         cmd = self._command_wrapper(self._port.get_option('wrapper'))
372         cmd.append(self._port._path_to_driver())
373         if self._port.get_option('pixel_tests'):
374             # See note above in diff_image() for why we need _convert_path().
375             cmd.append("--pixel-tests=" +
376                        self._port._convert_path(self._image_path))
377
378         if self._port.get_option('use_test_shell'):
379             cmd.append('--layout-tests')
380         else:
381             cmd.append('--test-shell')
382
383         if self._port.get_option('startup_dialog'):
384             cmd.append('--testshell-startup-dialog')
385
386         if self._port.get_option('gp_fault_error_box'):
387             cmd.append('--gp-fault-error-box')
388
389         if self._port.get_option('js_flags') is not None:
390             cmd.append('--js-flags="' + self._port.get_option('js_flags') + '"')
391
392         if self._port.get_option('stress_opt'):
393             cmd.append('--stress-opt')
394
395         if self._port.get_option('stress_deopt'):
396             cmd.append('--stress-deopt')
397
398         # test_shell does not support accelerated compositing.
399         if not self._port.get_option("use_test_shell"):
400             if self._port.get_option('accelerated_compositing'):
401                 cmd.append('--enable-accelerated-compositing')
402             if self._port.get_option('accelerated_2d_canvas'):
403                 cmd.append('--enable-accelerated-2d-canvas')
404             if self._port.get_option('enable_hardware_gpu'):
405                 cmd.append('--enable-hardware-gpu')
406         return cmd
407
408     def start(self):
409         # FIXME: Should be an error to call this method twice.
410         cmd = self.cmd_line()
411
412         # We need to pass close_fds=True to work around Python bug #2320
413         # (otherwise we can hang when we kill DumpRenderTree when we are running
414         # multiple threads). See http://bugs.python.org/issue2320 .
415         # Note that close_fds isn't supported on Windows, but this bug only
416         # shows up on Mac and Linux.
417         close_flag = sys.platform not in ('win32', 'cygwin')
418         self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
419                                       stdout=subprocess.PIPE,
420                                       stderr=subprocess.STDOUT,
421                                       close_fds=close_flag)
422
423     def poll(self):
424         # poll() is not threadsafe and can throw OSError due to:
425         # http://bugs.python.org/issue1731717
426         return self._proc.poll()
427
428     def _write_command_and_read_line(self, input=None):
429         """Returns a tuple: (line, did_crash)"""
430         try:
431             if input:
432                 if isinstance(input, unicode):
433                     # TestShell expects utf-8
434                     input = input.encode("utf-8")
435                 self._proc.stdin.write(input)
436             # DumpRenderTree text output is always UTF-8.  However some tests
437             # (e.g. webarchive) may spit out binary data instead of text so we
438             # don't bother to decode the output (for either DRT or test_shell).
439             line = self._proc.stdout.readline()
440             # We could assert() here that line correctly decodes as UTF-8.
441             return (line, False)
442         except IOError, e:
443             _log.error("IOError communicating w/ test_shell: " + str(e))
444             return (None, True)
445
446     def _test_shell_command(self, uri, timeoutms, checksum):
447         cmd = uri
448         if timeoutms:
449             cmd += ' ' + str(timeoutms)
450         if checksum:
451             cmd += ' ' + checksum
452         cmd += "\n"
453         return cmd
454
455     def _output_image(self):
456         """Returns the image output which driver generated."""
457         png_path = self._image_path
458         if png_path and os.path.isfile(png_path):
459             with open(png_path, 'rb') as image_file:
460                 return image_file.read()
461         else:
462             return None
463
464     def _output_image_with_retry(self):
465         # Retry a few more times because open() sometimes fails on Windows,
466         # raising "IOError: [Errno 13] Permission denied:"
467         retry_num = 50
468         timeout_seconds = 5.0
469         for i in range(retry_num):
470             try:
471                 return self._output_image()
472             except IOError, e:
473                 if e.errno == errno.EACCES:
474                     time.sleep(timeout_seconds / retry_num)
475                 else:
476                     raise e
477         return self._output_image()
478
479     def run_test(self, test_input):
480         output = []
481         error = []
482         crash = False
483         timeout = False
484         actual_uri = None
485         actual_checksum = None
486
487         start_time = time.time()
488
489         uri = self._port.filename_to_uri(test_input.filename)
490         cmd = self._test_shell_command(uri, test_input.timeout,
491                                        test_input.image_hash)
492         (line, crash) = self._write_command_and_read_line(input=cmd)
493
494         while not crash and line.rstrip() != "#EOF":
495             # Make sure we haven't crashed.
496             if line == '' and self.poll() is not None:
497                 # This is hex code 0xc000001d, which is used for abrupt
498                 # termination. This happens if we hit ctrl+c from the prompt
499                 # and we happen to be waiting on test_shell.
500                 # sdoyon: Not sure for which OS and in what circumstances the
501                 # above code is valid. What works for me under Linux to detect
502                 # ctrl+c is for the subprocess returncode to be negative
503                 # SIGINT. And that agrees with the subprocess documentation.
504                 if (-1073741510 == self._proc.returncode or
505                     - signal.SIGINT == self._proc.returncode):
506                     raise KeyboardInterrupt
507                 crash = True
508                 break
509
510             # Don't include #URL lines in our output
511             if line.startswith("#URL:"):
512                 actual_uri = line.rstrip()[5:]
513                 if uri != actual_uri:
514                     # GURL capitalizes the drive letter of a file URL.
515                     if (not re.search("^file:///[a-z]:", uri) or
516                         uri.lower() != actual_uri.lower()):
517                         _log.fatal("Test got out of sync:\n|%s|\n|%s|" %
518                                    (uri, actual_uri))
519                         raise AssertionError("test out of sync")
520             elif line.startswith("#MD5:"):
521                 actual_checksum = line.rstrip()[5:]
522             elif line.startswith("#TEST_TIMED_OUT"):
523                 timeout = True
524                 # Test timed out, but we still need to read until #EOF.
525             elif actual_uri:
526                 output.append(line)
527             else:
528                 error.append(line)
529
530             (line, crash) = self._write_command_and_read_line(input=None)
531
532         run_time = time.time() - start_time
533         return test_output.TestOutput(
534             ''.join(output), self._output_image_with_retry(), actual_checksum,
535             crash, run_time, timeout, ''.join(error))
536
537     def stop(self):
538         if self._proc:
539             self._proc.stdin.close()
540             self._proc.stdout.close()
541             if self._proc.stderr:
542                 self._proc.stderr.close()
543             if sys.platform not in ('win32', 'cygwin'):
544                 # Closing stdin/stdout/stderr hangs sometimes on OS X,
545                 # (see __init__(), above), and anyway we don't want to hang
546                 # the harness if test_shell is buggy, so we wait a couple
547                 # seconds to give test_shell a chance to clean up, but then
548                 # force-kill the process if necessary.
549                 KILL_TIMEOUT = 3.0
550                 timeout = time.time() + KILL_TIMEOUT
551                 # poll() is not threadsafe and can throw OSError due to:
552                 # http://bugs.python.org/issue1731717
553                 while self._proc.poll() is None and time.time() < timeout:
554                     time.sleep(0.1)
555                 # poll() is not threadsafe and can throw OSError due to:
556                 # http://bugs.python.org/issue1731717
557                 if self._proc.poll() is None:
558                     _log.warning('stopping test driver timed out, '
559                                  'killing it')
560                     self._port._executive.kill_process(self._proc.pid)