Home | History | Annotate | Download | only in port
      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 
     45 class ChromiumPort(base.Port):
     46     """Abstract base class for Chromium implementations of the Port class."""
     47 
     48     def __init__(self, port_name=None, options=None):
     49         base.Port.__init__(self, port_name, options)
     50         self._chromium_base_dir = None
     51 
     52     def baseline_path(self):
     53         return self._chromium_baseline_path(self._name)
     54 
     55     def check_sys_deps(self):
     56         result = True
     57         test_shell_binary_path = self._path_to_driver()
     58         if os.path.exists(test_shell_binary_path):
     59             proc = subprocess.Popen([test_shell_binary_path,
     60                                      '--check-layout-test-sys-deps'])
     61             if proc.wait() != 0:
     62                 logging.error("Aborting because system dependencies check "
     63                               "failed.")
     64                 logging.error("To override, invoke with --nocheck-sys-deps")
     65                 result = False
     66         else:
     67             logging.error('test driver is not found at %s' %
     68                           test_shell_binary_path)
     69             result = False
     70 
     71         image_diff_path = self._path_to_image_diff()
     72         if (not os.path.exists(image_diff_path) and not
     73             self._options.no_pixel_tests):
     74             logging.error('image diff not found at %s' % image_diff_path)
     75             logging.error("To override, invoke with --no-pixel-tests")
     76             result = False
     77 
     78         return result
     79 
     80     def compare_text(self, actual_text, expected_text):
     81         return actual_text != expected_text
     82 
     83     def path_from_chromium_base(self, *comps):
     84         """Returns the full path to path made by joining the top of the
     85         Chromium source tree and the list of path components in |*comps|."""
     86         if not self._chromium_base_dir:
     87             abspath = os.path.abspath(__file__)
     88             self._chromium_base_dir = abspath[0:abspath.find('third_party')]
     89         return os.path.join(self._chromium_base_dir, *comps)
     90 
     91     def results_directory(self):
     92         return self.path_from_chromium_base('webkit', self._options.target,
     93                                             self._options.results_directory)
     94 
     95     def setup_test_run(self):
     96         # Delete the disk cache if any to ensure a clean test run.
     97         test_shell_binary_path = self._path_to_driver()
     98         cachedir = os.path.split(test_shell_binary_path)[0]
     99         cachedir = os.path.join(cachedir, "cache")
    100         if os.path.exists(cachedir):
    101             shutil.rmtree(cachedir)
    102 
    103     def show_results_html_file(self, results_filename):
    104         subprocess.Popen([self._path_to_driver(),
    105                           self.filename_to_uri(results_filename)])
    106 
    107     def start_driver(self, image_path, options):
    108         """Starts a new Driver and returns a handle to it."""
    109         return ChromiumDriver(self, image_path, options)
    110 
    111     def start_helper(self):
    112         helper_path = self._path_to_helper()
    113         if helper_path:
    114             logging.debug("Starting layout helper %s" % helper_path)
    115             self._helper = subprocess.Popen([helper_path],
    116                 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None)
    117             is_ready = self._helper.stdout.readline()
    118             if not is_ready.startswith('ready'):
    119                 logging.error("layout_test_helper failed to be ready")
    120 
    121     def stop_helper(self):
    122         if self._helper:
    123             logging.debug("Stopping layout test helper")
    124             self._helper.stdin.write("x\n")
    125             self._helper.stdin.close()
    126             self._helper.wait()
    127 
    128     def test_base_platform_names(self):
    129         return ('linux', 'mac', 'win')
    130 
    131     def test_expectations(self, options=None):
    132         """Returns the test expectations for this port.
    133 
    134         Basically this string should contain the equivalent of a
    135         test_expectations file. See test_expectations.py for more details."""
    136         expectations_file = self.path_from_chromium_base('webkit', 'tools',
    137             'layout_tests', 'test_expectations.txt')
    138         return file(expectations_file, "r").read()
    139 
    140     def test_platform_names(self):
    141         return self.test_base_platform_names() + ('win-xp',
    142             'win-vista', 'win-7')
    143 
    144     #
    145     # PROTECTED METHODS
    146     #
    147     # These routines should only be called by other methods in this file
    148     # or any subclasses.
    149     #
    150 
    151     def _chromium_baseline_path(self, platform):
    152         if platform is None:
    153             platform = self.name()
    154         return self.path_from_chromium_base('webkit', 'data', 'layout_tests',
    155             'platform', platform, 'LayoutTests')
    156 
    157 
    158 class ChromiumDriver(base.Driver):
    159     """Abstract interface for the DumpRenderTree interface."""
    160 
    161     def __init__(self, port, image_path, options):
    162         self._port = port
    163         self._options = options
    164         self._target = port._options.target
    165         self._image_path = image_path
    166 
    167         cmd = []
    168         # Hook for injecting valgrind or other runtime instrumentation,
    169         # used by e.g. tools/valgrind/valgrind_tests.py.
    170         wrapper = os.environ.get("BROWSER_WRAPPER", None)
    171         if wrapper != None:
    172             cmd += [wrapper]
    173         if self._port._options.wrapper:
    174             # This split() isn't really what we want -- it incorrectly will
    175             # split quoted strings within the wrapper argument -- but in
    176             # practice it shouldn't come up and the --help output warns
    177             # about it anyway.
    178             cmd += self._options.wrapper.split()
    179         cmd += [port._path_to_driver(), '--layout-tests']
    180         if options:
    181             cmd += options
    182         self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
    183                                       stdout=subprocess.PIPE,
    184                                       stderr=subprocess.STDOUT)
    185 
    186     def poll(self):
    187         return self._proc.poll()
    188 
    189     def returncode(self):
    190         return self._proc.returncode
    191 
    192     def run_test(self, uri, timeoutms, checksum):
    193         output = []
    194         error = []
    195         crash = False
    196         timeout = False
    197         actual_uri = None
    198         actual_checksum = None
    199 
    200         start_time = time.time()
    201         cmd = uri
    202         if timeoutms:
    203             cmd += ' ' + str(timeoutms)
    204         if checksum:
    205             cmd += ' ' + checksum
    206         cmd += "\n"
    207 
    208         self._proc.stdin.write(cmd)
    209         line = self._proc.stdout.readline()
    210         while line.rstrip() != "#EOF":
    211             # Make sure we haven't crashed.
    212             if line == '' and self.poll() is not None:
    213                 # This is hex code 0xc000001d, which is used for abrupt
    214                 # termination. This happens if we hit ctrl+c from the prompt
    215                 # and we happen to be waiting on the test_shell.
    216                 # sdoyon: Not sure for which OS and in what circumstances the
    217                 # above code is valid. What works for me under Linux to detect
    218                 # ctrl+c is for the subprocess returncode to be negative
    219                 # SIGINT. And that agrees with the subprocess documentation.
    220                 if (-1073741510 == self._proc.returncode or
    221                     - signal.SIGINT == self._proc.returncode):
    222                     raise KeyboardInterrupt
    223                 crash = True
    224                 break
    225 
    226             # Don't include #URL lines in our output
    227             if line.startswith("#URL:"):
    228                 actual_uri = line.rstrip()[5:]
    229                 if uri != actual_uri:
    230                     logging.fatal("Test got out of sync:\n|%s|\n|%s|" %
    231                                 (uri, actual_uri))
    232                     raise AssertionError("test out of sync")
    233             elif line.startswith("#MD5:"):
    234                 actual_checksum = line.rstrip()[5:]
    235             elif line.startswith("#TEST_TIMED_OUT"):
    236                 timeout = True
    237                 # Test timed out, but we still need to read until #EOF.
    238             elif actual_uri:
    239                 output.append(line)
    240             else:
    241                 error.append(line)
    242 
    243             line = self._proc.stdout.readline()
    244 
    245         return (crash, timeout, actual_checksum, ''.join(output),
    246                 ''.join(error))
    247 
    248     def stop(self):
    249         if self._proc:
    250             self._proc.stdin.close()
    251             self._proc.stdout.close()
    252             if self._proc.stderr:
    253                 self._proc.stderr.close()
    254             if (sys.platform not in ('win32', 'cygwin') and
    255                 not self._proc.poll()):
    256                 # Closing stdin/stdout/stderr hangs sometimes on OS X.
    257                 null = open(os.devnull, "w")
    258                 subprocess.Popen(["kill", "-9",
    259                                  str(self._proc.pid)], stderr=null)
    260                 null.close()
    261