| #!/usr/bin/env python |
| # Copyright (C) 2010 Google Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # * Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # * Redistributions in binary form must reproduce the above |
| # copyright notice, this list of conditions and the following disclaimer |
| # in the documentation and/or other materials provided with the |
| # distribution. |
| # * Neither the name of Google Inc. nor the names of its |
| # contributors may be used to endorse or promote products derived from |
| # this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| """Chromium implementations of the Port interface.""" |
| |
| import errno |
| import logging |
| import re |
| import signal |
| import subprocess |
| import sys |
| import time |
| import webbrowser |
| |
| from webkitpy.common.system import executive |
| from webkitpy.common.system.path import cygpath |
| from webkitpy.layout_tests.layout_package import test_expectations |
| from webkitpy.layout_tests.port import base |
| from webkitpy.layout_tests.port import http_server |
| from webkitpy.layout_tests.port import websocket_server |
| |
| _log = logging.getLogger("webkitpy.layout_tests.port.chromium") |
| |
| |
| # FIXME: This function doesn't belong in this package. |
| class ChromiumPort(base.Port): |
| """Abstract base class for Chromium implementations of the Port class.""" |
| ALL_BASELINE_VARIANTS = [ |
| 'chromium-mac-snowleopard', 'chromium-mac-leopard', |
| 'chromium-win-win7', 'chromium-win-vista', 'chromium-win-xp', |
| 'chromium-linux-x86', 'chromium-linux-x86_64', |
| 'chromium-gpu-mac-snowleopard', 'chromium-gpu-win-win7', 'chromium-gpu-linux-x86_64', |
| ] |
| |
| def __init__(self, **kwargs): |
| base.Port.__init__(self, **kwargs) |
| self._chromium_base_dir = None |
| |
| def _check_file_exists(self, path_to_file, file_description, |
| override_step=None, logging=True): |
| """Verify the file is present where expected or log an error. |
| |
| Args: |
| file_name: The (human friendly) name or description of the file |
| you're looking for (e.g., "HTTP Server"). Used for error logging. |
| override_step: An optional string to be logged if the check fails. |
| logging: Whether or not log the error messages.""" |
| if not self._filesystem.exists(path_to_file): |
| if logging: |
| _log.error('Unable to find %s' % file_description) |
| _log.error(' at %s' % path_to_file) |
| if override_step: |
| _log.error(' %s' % override_step) |
| _log.error('') |
| return False |
| return True |
| |
| |
| def baseline_path(self): |
| return self._webkit_baseline_path(self._name) |
| |
| def check_build(self, needs_http): |
| result = True |
| |
| dump_render_tree_binary_path = self._path_to_driver() |
| result = self._check_file_exists(dump_render_tree_binary_path, |
| 'test driver') and result |
| if result and self.get_option('build'): |
| result = self._check_driver_build_up_to_date( |
| self.get_option('configuration')) |
| else: |
| _log.error('') |
| |
| helper_path = self._path_to_helper() |
| if helper_path: |
| result = self._check_file_exists(helper_path, |
| 'layout test helper') and result |
| |
| if self.get_option('pixel_tests'): |
| result = self.check_image_diff( |
| 'To override, invoke with --no-pixel-tests') and result |
| |
| # It's okay if pretty patch isn't available, but we will at |
| # least log a message. |
| self._pretty_patch_available = self.check_pretty_patch() |
| |
| return result |
| |
| def check_sys_deps(self, needs_http): |
| cmd = [self._path_to_driver(), '--check-layout-test-sys-deps'] |
| |
| local_error = executive.ScriptError() |
| |
| def error_handler(script_error): |
| local_error.exit_code = script_error.exit_code |
| |
| output = self._executive.run_command(cmd, error_handler=error_handler) |
| if local_error.exit_code: |
| _log.error('System dependencies check failed.') |
| _log.error('To override, invoke with --nocheck-sys-deps') |
| _log.error('') |
| _log.error(output) |
| return False |
| return True |
| |
| def check_image_diff(self, override_step=None, logging=True): |
| image_diff_path = self._path_to_image_diff() |
| return self._check_file_exists(image_diff_path, 'image diff exe', |
| override_step, logging) |
| |
| def diff_image(self, expected_contents, actual_contents, |
| diff_filename=None): |
| # FIXME: need unit tests for this. |
| if not actual_contents and not expected_contents: |
| return False |
| if not actual_contents or not expected_contents: |
| return True |
| |
| tempdir = self._filesystem.mkdtemp() |
| expected_filename = self._filesystem.join(str(tempdir), "expected.png") |
| self._filesystem.write_binary_file(expected_filename, expected_contents) |
| actual_filename = self._filesystem.join(str(tempdir), "actual.png") |
| self._filesystem.write_binary_file(actual_filename, actual_contents) |
| |
| executable = self._path_to_image_diff() |
| if diff_filename: |
| cmd = [executable, '--diff', expected_filename, |
| actual_filename, diff_filename] |
| else: |
| cmd = [executable, expected_filename, actual_filename] |
| |
| result = True |
| try: |
| exit_code = self._executive.run_command(cmd, return_exit_code=True) |
| if exit_code == 0: |
| # The images are the same. |
| result = False |
| elif exit_code != 1: |
| _log.error("image diff returned an exit code of " |
| + str(exit_code)) |
| # Returning False here causes the script to think that we |
| # successfully created the diff even though we didn't. If |
| # we return True, we think that the images match but the hashes |
| # don't match. |
| # FIXME: Figure out why image_diff returns other values. |
| result = False |
| except OSError, e: |
| if e.errno == errno.ENOENT or e.errno == errno.EACCES: |
| _compare_available = False |
| else: |
| raise e |
| finally: |
| self._filesystem.rmtree(str(tempdir)) |
| return result |
| |
| def driver_name(self): |
| return "DumpRenderTree" |
| |
| def path_from_chromium_base(self, *comps): |
| """Returns the full path to path made by joining the top of the |
| Chromium source tree and the list of path components in |*comps|.""" |
| if not self._chromium_base_dir: |
| abspath = self._filesystem.abspath(__file__) |
| offset = abspath.find('third_party') |
| if offset == -1: |
| self._chromium_base_dir = self._filesystem.join( |
| abspath[0:abspath.find('Tools')], |
| 'Source', 'WebKit', 'chromium') |
| else: |
| self._chromium_base_dir = abspath[0:offset] |
| return self._filesystem.join(self._chromium_base_dir, *comps) |
| |
| def path_to_test_expectations_file(self): |
| return self.path_from_webkit_base('LayoutTests', 'platform', |
| 'chromium', 'test_expectations.txt') |
| |
| def default_results_directory(self): |
| try: |
| return self.path_from_chromium_base('webkit', |
| self.get_option('configuration'), |
| 'layout-test-results') |
| except AssertionError: |
| return self._build_path(self.get_option('configuration'), |
| 'layout-test-results') |
| |
| def setup_test_run(self): |
| # Delete the disk cache if any to ensure a clean test run. |
| dump_render_tree_binary_path = self._path_to_driver() |
| cachedir = self._filesystem.dirname(dump_render_tree_binary_path) |
| cachedir = self._filesystem.join(cachedir, "cache") |
| if self._filesystem.exists(cachedir): |
| self._filesystem.rmtree(cachedir) |
| |
| def create_driver(self, worker_number): |
| """Starts a new Driver and returns a handle to it.""" |
| return ChromiumDriver(self, worker_number) |
| |
| def start_helper(self): |
| helper_path = self._path_to_helper() |
| if helper_path: |
| _log.debug("Starting layout helper %s" % helper_path) |
| # Note: Not thread safe: http://bugs.python.org/issue2320 |
| self._helper = subprocess.Popen([helper_path], |
| stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None) |
| is_ready = self._helper.stdout.readline() |
| if not is_ready.startswith('ready'): |
| _log.error("layout_test_helper failed to be ready") |
| |
| def stop_helper(self): |
| if self._helper: |
| _log.debug("Stopping layout test helper") |
| self._helper.stdin.write("x\n") |
| self._helper.stdin.close() |
| # wait() is not threadsafe and can throw OSError due to: |
| # http://bugs.python.org/issue1731717 |
| self._helper.wait() |
| |
| def all_baseline_variants(self): |
| return self.ALL_BASELINE_VARIANTS |
| |
| def test_expectations(self): |
| """Returns the test expectations for this port. |
| |
| Basically this string should contain the equivalent of a |
| test_expectations file. See test_expectations.py for more details.""" |
| expectations_path = self.path_to_test_expectations_file() |
| return self._filesystem.read_text_file(expectations_path) |
| |
| def test_expectations_overrides(self): |
| try: |
| overrides_path = self.path_from_chromium_base('webkit', 'tools', |
| 'layout_tests', 'test_expectations.txt') |
| except AssertionError: |
| return None |
| if not self._filesystem.exists(overrides_path): |
| return None |
| return self._filesystem.read_text_file(overrides_path) |
| |
| def skipped_layout_tests(self, extra_test_files=None): |
| expectations_str = self.test_expectations() |
| overrides_str = self.test_expectations_overrides() |
| is_debug_mode = False |
| |
| all_test_files = self.tests([]) |
| if extra_test_files: |
| all_test_files.update(extra_test_files) |
| |
| expectations = test_expectations.TestExpectations( |
| self, all_test_files, expectations_str, self.test_configuration(), |
| is_lint_mode=False, overrides=overrides_str) |
| tests_dir = self.layout_tests_dir() |
| return [self.relative_test_filename(test) |
| for test in expectations.get_tests_with_result_type(test_expectations.SKIP)] |
| |
| def test_repository_paths(self): |
| # Note: for JSON file's backward-compatibility we use 'chrome' rather |
| # than 'chromium' here. |
| repos = super(ChromiumPort, self).test_repository_paths() |
| repos.append(('chrome', self.path_from_chromium_base())) |
| return repos |
| |
| # |
| # PROTECTED METHODS |
| # |
| # These routines should only be called by other methods in this file |
| # or any subclasses. |
| # |
| |
| def _check_driver_build_up_to_date(self, configuration): |
| if configuration in ('Debug', 'Release'): |
| try: |
| debug_path = self._path_to_driver('Debug') |
| release_path = self._path_to_driver('Release') |
| |
| debug_mtime = self._filesystem.mtime(debug_path) |
| release_mtime = self._filesystem.mtime(release_path) |
| |
| if (debug_mtime > release_mtime and configuration == 'Release' or |
| release_mtime > debug_mtime and configuration == 'Debug'): |
| _log.warning('You are not running the most ' |
| 'recent DumpRenderTree binary. You need to ' |
| 'pass --debug or not to select between ' |
| 'Debug and Release.') |
| _log.warning('') |
| # This will fail if we don't have both a debug and release binary. |
| # That's fine because, in this case, we must already be running the |
| # most up-to-date one. |
| except OSError: |
| pass |
| return True |
| |
| def _chromium_baseline_path(self, platform): |
| if platform is None: |
| platform = self.name() |
| return self.path_from_webkit_base('LayoutTests', 'platform', platform) |
| |
| def _convert_path(self, path): |
| """Handles filename conversion for subprocess command line args.""" |
| # See note above in diff_image() for why we need this. |
| if sys.platform == 'cygwin': |
| return cygpath(path) |
| return path |
| |
| def _path_to_image_diff(self): |
| binary_name = 'ImageDiff' |
| return self._build_path(self.get_option('configuration'), binary_name) |
| |
| |
| class ChromiumDriver(base.Driver): |
| """Abstract interface for DRT.""" |
| |
| def __init__(self, port, worker_number): |
| self._port = port |
| self._worker_number = worker_number |
| self._image_path = None |
| self.KILL_TIMEOUT = 3.0 |
| if self._port.get_option('pixel_tests'): |
| self._image_path = self._port._filesystem.join(self._port.results_directory(), |
| 'png_result%s.png' % self._worker_number) |
| |
| def cmd_line(self): |
| cmd = self._command_wrapper(self._port.get_option('wrapper')) |
| cmd.append(self._port._path_to_driver()) |
| if self._port.get_option('pixel_tests'): |
| # See note above in diff_image() for why we need _convert_path(). |
| cmd.append("--pixel-tests=" + |
| self._port._convert_path(self._image_path)) |
| |
| cmd.append('--test-shell') |
| |
| if self._port.get_option('startup_dialog'): |
| cmd.append('--testshell-startup-dialog') |
| |
| if self._port.get_option('gp_fault_error_box'): |
| cmd.append('--gp-fault-error-box') |
| |
| if self._port.get_option('js_flags') is not None: |
| cmd.append('--js-flags="' + self._port.get_option('js_flags') + '"') |
| |
| if self._port.get_option('stress_opt'): |
| cmd.append('--stress-opt') |
| |
| if self._port.get_option('stress_deopt'): |
| cmd.append('--stress-deopt') |
| |
| if self._port.get_option('accelerated_compositing'): |
| cmd.append('--enable-accelerated-compositing') |
| if self._port.get_option('accelerated_2d_canvas'): |
| cmd.append('--enable-accelerated-2d-canvas') |
| if self._port.get_option('enable_hardware_gpu'): |
| cmd.append('--enable-hardware-gpu') |
| |
| cmd.extend(self._port.get_option('additional_drt_flag', [])) |
| return cmd |
| |
| def start(self): |
| # FIXME: Should be an error to call this method twice. |
| cmd = self.cmd_line() |
| |
| # We need to pass close_fds=True to work around Python bug #2320 |
| # (otherwise we can hang when we kill DumpRenderTree when we are running |
| # multiple threads). See http://bugs.python.org/issue2320 . |
| # Note that close_fds isn't supported on Windows, but this bug only |
| # shows up on Mac and Linux. |
| close_flag = sys.platform not in ('win32', 'cygwin') |
| self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| close_fds=close_flag) |
| |
| def poll(self): |
| # poll() is not threadsafe and can throw OSError due to: |
| # http://bugs.python.org/issue1731717 |
| return self._proc.poll() |
| |
| def _write_command_and_read_line(self, input=None): |
| """Returns a tuple: (line, did_crash)""" |
| try: |
| if input: |
| if isinstance(input, unicode): |
| # DRT expects utf-8 |
| input = input.encode("utf-8") |
| self._proc.stdin.write(input) |
| # DumpRenderTree text output is always UTF-8. However some tests |
| # (e.g. webarchive) may spit out binary data instead of text so we |
| # don't bother to decode the output. |
| line = self._proc.stdout.readline() |
| # We could assert() here that line correctly decodes as UTF-8. |
| return (line, False) |
| except IOError, e: |
| _log.error("IOError communicating w/ DRT: " + str(e)) |
| return (None, True) |
| |
| def _test_shell_command(self, uri, timeoutms, checksum): |
| cmd = uri |
| if timeoutms: |
| cmd += ' ' + str(timeoutms) |
| if checksum: |
| cmd += ' ' + checksum |
| cmd += "\n" |
| return cmd |
| |
| def _output_image(self): |
| """Returns the image output which driver generated.""" |
| png_path = self._image_path |
| if png_path and self._port._filesystem.exists(png_path): |
| return self._port._filesystem.read_binary_file(png_path) |
| else: |
| return None |
| |
| def _output_image_with_retry(self): |
| # Retry a few more times because open() sometimes fails on Windows, |
| # raising "IOError: [Errno 13] Permission denied:" |
| retry_num = 50 |
| timeout_seconds = 5.0 |
| for i in range(retry_num): |
| try: |
| return self._output_image() |
| except IOError, e: |
| if e.errno == errno.EACCES: |
| time.sleep(timeout_seconds / retry_num) |
| else: |
| raise e |
| return self._output_image() |
| |
| def _clear_output_image(self): |
| png_path = self._image_path |
| if png_path and self._port._filesystem.exists(png_path): |
| self._port._filesystem.remove(png_path) |
| |
| def run_test(self, driver_input): |
| output = [] |
| error = [] |
| crash = False |
| timeout = False |
| actual_uri = None |
| actual_checksum = None |
| self._clear_output_image() |
| start_time = time.time() |
| |
| uri = self._port.filename_to_uri(driver_input.filename) |
| cmd = self._test_shell_command(uri, driver_input.timeout, |
| driver_input.image_hash) |
| (line, crash) = self._write_command_and_read_line(input=cmd) |
| |
| while not crash and line.rstrip() != "#EOF": |
| # Make sure we haven't crashed. |
| if line == '' and self.poll() is not None: |
| # This is hex code 0xc000001d, which is used for abrupt |
| # termination. This happens if we hit ctrl+c from the prompt |
| # and we happen to be waiting on DRT. |
| # sdoyon: Not sure for which OS and in what circumstances the |
| # above code is valid. What works for me under Linux to detect |
| # ctrl+c is for the subprocess returncode to be negative |
| # SIGINT. And that agrees with the subprocess documentation. |
| if (-1073741510 == self._proc.returncode or |
| - signal.SIGINT == self._proc.returncode): |
| raise KeyboardInterrupt |
| crash = True |
| break |
| |
| # Don't include #URL lines in our output |
| if line.startswith("#URL:"): |
| actual_uri = line.rstrip()[5:] |
| if uri != actual_uri: |
| # GURL capitalizes the drive letter of a file URL. |
| if (not re.search("^file:///[a-z]:", uri) or |
| uri.lower() != actual_uri.lower()): |
| _log.fatal("Test got out of sync:\n|%s|\n|%s|" % |
| (uri, actual_uri)) |
| raise AssertionError("test out of sync") |
| elif line.startswith("#MD5:"): |
| actual_checksum = line.rstrip()[5:] |
| elif line.startswith("#TEST_TIMED_OUT"): |
| timeout = True |
| # Test timed out, but we still need to read until #EOF. |
| elif actual_uri: |
| output.append(line) |
| else: |
| error.append(line) |
| |
| (line, crash) = self._write_command_and_read_line(input=None) |
| |
| # FIXME: Add support for audio when we're ready. |
| |
| run_time = time.time() - start_time |
| output_image = self._output_image_with_retry() |
| text = ''.join(output) |
| if not text: |
| text = None |
| |
| return base.DriverOutput(text, output_image, actual_checksum, audio=None, |
| crash=crash, test_time=run_time, timeout=timeout, error=''.join(error)) |
| |
| def stop(self): |
| if self._proc: |
| self._proc.stdin.close() |
| self._proc.stdout.close() |
| if self._proc.stderr: |
| self._proc.stderr.close() |
| # Closing stdin/stdout/stderr hangs sometimes on OS X, |
| # (see __init__(), above), and anyway we don't want to hang |
| # the harness if DRT is buggy, so we wait a couple |
| # seconds to give DRT a chance to clean up, but then |
| # force-kill the process if necessary. |
| timeout = time.time() + self.KILL_TIMEOUT |
| while self._proc.poll() is None and time.time() < timeout: |
| time.sleep(0.1) |
| if self._proc.poll() is None: |
| _log.warning('stopping test driver timed out, ' |
| 'killing it') |
| self._port._executive.kill_process(self._proc.pid) |
| # FIXME: This is sometime none. What is wrong? assert self._proc.poll() is not None |
| if self._proc.poll() is not None: |
| self._proc.wait() |
| self._proc = None |