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 Google name 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 """Abstract base class of Port-specific entrypoints for the layout tests
     31 test infrastructure (the Port and Driver classes)."""
     32 
     33 from __future__ import with_statement
     34 
     35 import cgi
     36 import difflib
     37 import errno
     38 import os
     39 import shlex
     40 import sys
     41 import time
     42 
     43 # Handle Python < 2.6 where multiprocessing isn't available.
     44 try:
     45     import multiprocessing
     46 except ImportError:
     47     multiprocessing = None
     48 
     49 from webkitpy.common import system
     50 from webkitpy.common.system import filesystem
     51 from webkitpy.common.system import logutils
     52 from webkitpy.common.system import path
     53 from webkitpy.common.system.executive import Executive, ScriptError
     54 from webkitpy.common.system.user import User
     55 from webkitpy.layout_tests import read_checksum_from_png
     56 from webkitpy.layout_tests.port import apache_http_server
     57 from webkitpy.layout_tests.port import config as port_config
     58 from webkitpy.layout_tests.port import http_lock
     59 from webkitpy.layout_tests.port import http_server
     60 from webkitpy.layout_tests.port import test_files
     61 from webkitpy.layout_tests.port import websocket_server
     62 
     63 _log = logutils.get_logger(__file__)
     64 
     65 
     66 class DummyOptions(object):
     67     """Fake implementation of optparse.Values. Cloned from
     68     webkitpy.tool.mocktool.MockOptions.
     69 
     70     """
     71 
     72     def __init__(self, **kwargs):
     73         # The caller can set option values using keyword arguments. We don't
     74         # set any values by default because we don't know how this
     75         # object will be used. Generally speaking unit tests should
     76         # subclass this or provider wrapper functions that set a common
     77         # set of options.
     78         for key, value in kwargs.items():
     79             self.__dict__[key] = value
     80 
     81 
     82 # FIXME: This class should merge with webkitpy.webkit_port at some point.
     83 class Port(object):
     84     """Abstract class for Port-specific hooks for the layout_test package."""
     85 
     86     def __init__(self, port_name=None, options=None,
     87                  executive=None,
     88                  user=None,
     89                  filesystem=None,
     90                  config=None,
     91                  **kwargs):
     92         self._name = port_name
     93 
     94         # These are default values that should be overridden in a subclasses.
     95         # FIXME: These should really be passed in.
     96         self._operating_system = 'mac'
     97         self._version = ''
     98         self._architecture = 'x86'
     99         self._graphics_type = 'cpu'
    100 
    101         # FIXME: Ideally we'd have a package-wide way to get a
    102         # well-formed options object that had all of the necessary
    103         # options defined on it.
    104         self._options = options or DummyOptions()
    105 
    106         self._executive = executive or Executive()
    107         self._user = user or User()
    108         self._filesystem = filesystem or system.filesystem.FileSystem()
    109         self._config = config or port_config.Config(self._executive, self._filesystem)
    110 
    111         self._helper = None
    112         self._http_server = None
    113         self._webkit_base_dir = None
    114         self._websocket_server = None
    115         self._http_lock = None
    116 
    117         # Python's Popen has a bug that causes any pipes opened to a
    118         # process that can't be executed to be leaked.  Since this
    119         # code is specifically designed to tolerate exec failures
    120         # to gracefully handle cases where wdiff is not installed,
    121         # the bug results in a massive file descriptor leak. As a
    122         # workaround, if an exec failure is ever experienced for
    123         # wdiff, assume it's not available.  This will leak one
    124         # file descriptor but that's better than leaking each time
    125         # wdiff would be run.
    126         #
    127         # http://mail.python.org/pipermail/python-list/
    128         #    2008-August/505753.html
    129         # http://bugs.python.org/issue3210
    130         self._wdiff_available = True
    131 
    132         # FIXME: prettypatch.py knows this path, why is it copied here?
    133         self._pretty_patch_path = self.path_from_webkit_base("Websites",
    134             "bugs.webkit.org", "PrettyPatch", "prettify.rb")
    135         self._pretty_patch_available = None
    136 
    137         if not hasattr(self._options, 'configuration') or self._options.configuration is None:
    138             self._options.configuration = self.default_configuration()
    139         self._test_configuration = None
    140         self._multiprocessing_is_available = (multiprocessing is not None)
    141         self._results_directory = None
    142 
    143     def wdiff_available(self):
    144         return bool(self._wdiff_available)
    145 
    146     def pretty_patch_available(self):
    147         return bool(self._pretty_patch_available)
    148 
    149     def default_child_processes(self):
    150         """Return the number of DumpRenderTree instances to use for this
    151         port."""
    152         return self._executive.cpu_count()
    153 
    154     def default_worker_model(self):
    155         if self._multiprocessing_is_available:
    156             return 'processes'
    157         return 'threads'
    158 
    159     def baseline_path(self):
    160         """Return the absolute path to the directory to store new baselines
    161         in for this port."""
    162         raise NotImplementedError('Port.baseline_path')
    163 
    164     def baseline_search_path(self):
    165         """Return a list of absolute paths to directories to search under for
    166         baselines. The directories are searched in order."""
    167         raise NotImplementedError('Port.baseline_search_path')
    168 
    169     def check_build(self, needs_http):
    170         """This routine is used to ensure that the build is up to date
    171         and all the needed binaries are present."""
    172         raise NotImplementedError('Port.check_build')
    173 
    174     def check_sys_deps(self, needs_http):
    175         """If the port needs to do some runtime checks to ensure that the
    176         tests can be run successfully, it should override this routine.
    177         This step can be skipped with --nocheck-sys-deps.
    178 
    179         Returns whether the system is properly configured."""
    180         return True
    181 
    182     def check_image_diff(self, override_step=None, logging=True):
    183         """This routine is used to check whether image_diff binary exists."""
    184         raise NotImplementedError('Port.check_image_diff')
    185 
    186     def check_pretty_patch(self, logging=True):
    187         """Checks whether we can use the PrettyPatch ruby script."""
    188         # check if Ruby is installed
    189         try:
    190             result = self._executive.run_command(['ruby', '--version'])
    191         except OSError, e:
    192             if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]:
    193                 if logging:
    194                     _log.error("Ruby is not installed; can't generate pretty patches.")
    195                     _log.error('')
    196                 return False
    197 
    198         if not self.path_exists(self._pretty_patch_path):
    199             if logging:
    200                 _log.error("Unable to find %s; can't generate pretty patches." % self._pretty_patch_path)
    201                 _log.error('')
    202             return False
    203 
    204         return True
    205 
    206     def compare_text(self, expected_text, actual_text):
    207         """Return whether or not the two strings are *not* equal. This
    208         routine is used to diff text output.
    209 
    210         While this is a generic routine, we include it in the Port
    211         interface so that it can be overriden for testing purposes."""
    212         return expected_text != actual_text
    213 
    214     def compare_audio(self, expected_audio, actual_audio):
    215         """Return whether the two audio files are *not* equal."""
    216         return expected_audio != actual_audio
    217 
    218     def diff_image(self, expected_contents, actual_contents,
    219                    diff_filename=None, tolerance=0):
    220         """Compare two images and produce a delta image file.
    221 
    222         Return True if the two images are different, False if they are the same.
    223         Also produce a delta image of the two images and write that into
    224         |diff_filename| if it is not None.
    225 
    226         |tolerance| should be a percentage value (0.0 - 100.0).
    227         If it is omitted, the port default tolerance value is used.
    228 
    229         """
    230         raise NotImplementedError('Port.diff_image')
    231 
    232 
    233     def diff_text(self, expected_text, actual_text,
    234                   expected_filename, actual_filename):
    235         """Returns a string containing the diff of the two text strings
    236         in 'unified diff' format.
    237 
    238         While this is a generic routine, we include it in the Port
    239         interface so that it can be overriden for testing purposes."""
    240 
    241         # The filenames show up in the diff output, make sure they're
    242         # raw bytes and not unicode, so that they don't trigger join()
    243         # trying to decode the input.
    244         def to_raw_bytes(str):
    245             if isinstance(str, unicode):
    246                 return str.encode('utf-8')
    247             return str
    248         expected_filename = to_raw_bytes(expected_filename)
    249         actual_filename = to_raw_bytes(actual_filename)
    250         diff = difflib.unified_diff(expected_text.splitlines(True),
    251                                     actual_text.splitlines(True),
    252                                     expected_filename,
    253                                     actual_filename)
    254         return ''.join(diff)
    255 
    256     def driver_name(self):
    257         """Returns the name of the actual binary that is performing the test,
    258         so that it can be referred to in log messages. In most cases this
    259         will be DumpRenderTree, but if a port uses a binary with a different
    260         name, it can be overridden here."""
    261         return "DumpRenderTree"
    262 
    263     def expected_baselines(self, filename, suffix, all_baselines=False):
    264         """Given a test name, finds where the baseline results are located.
    265 
    266         Args:
    267         filename: absolute filename to test file
    268         suffix: file suffix of the expected results, including dot; e.g.
    269             '.txt' or '.png'.  This should not be None, but may be an empty
    270             string.
    271         all_baselines: If True, return an ordered list of all baseline paths
    272             for the given platform. If False, return only the first one.
    273         Returns
    274         a list of ( platform_dir, results_filename ), where
    275             platform_dir - abs path to the top of the results tree (or test
    276                 tree)
    277             results_filename - relative path from top of tree to the results
    278                 file
    279             (port.join() of the two gives you the full path to the file,
    280                 unless None was returned.)
    281         Return values will be in the format appropriate for the current
    282         platform (e.g., "\\" for path separators on Windows). If the results
    283         file is not found, then None will be returned for the directory,
    284         but the expected relative pathname will still be returned.
    285 
    286         This routine is generic but lives here since it is used in
    287         conjunction with the other baseline and filename routines that are
    288         platform specific.
    289         """
    290         testname = self._filesystem.splitext(self.relative_test_filename(filename))[0]
    291 
    292         baseline_filename = testname + '-expected' + suffix
    293 
    294         baseline_search_path = self.get_option('additional_platform_directory', []) + self.baseline_search_path()
    295 
    296         baselines = []
    297         for platform_dir in baseline_search_path:
    298             if self.path_exists(self._filesystem.join(platform_dir,
    299                                                       baseline_filename)):
    300                 baselines.append((platform_dir, baseline_filename))
    301 
    302             if not all_baselines and baselines:
    303                 return baselines
    304 
    305         # If it wasn't found in a platform directory, return the expected
    306         # result in the test directory, even if no such file actually exists.
    307         platform_dir = self.layout_tests_dir()
    308         if self.path_exists(self._filesystem.join(platform_dir,
    309                                                   baseline_filename)):
    310             baselines.append((platform_dir, baseline_filename))
    311 
    312         if baselines:
    313             return baselines
    314 
    315         return [(None, baseline_filename)]
    316 
    317     def expected_filename(self, filename, suffix):
    318         """Given a test name, returns an absolute path to its expected results.
    319 
    320         If no expected results are found in any of the searched directories,
    321         the directory in which the test itself is located will be returned.
    322         The return value is in the format appropriate for the platform
    323         (e.g., "\\" for path separators on windows).
    324 
    325         Args:
    326         filename: absolute filename to test file
    327         suffix: file suffix of the expected results, including dot; e.g. '.txt'
    328             or '.png'.  This should not be None, but may be an empty string.
    329         platform: the most-specific directory name to use to build the
    330             search list of directories, e.g., 'chromium-win', or
    331             'chromium-mac-leopard' (we follow the WebKit format)
    332 
    333         This routine is generic but is implemented here to live alongside
    334         the other baseline and filename manipulation routines.
    335         """
    336         platform_dir, baseline_filename = self.expected_baselines(
    337             filename, suffix)[0]
    338         if platform_dir:
    339             return self._filesystem.join(platform_dir, baseline_filename)
    340         return self._filesystem.join(self.layout_tests_dir(), baseline_filename)
    341 
    342     def expected_checksum(self, test):
    343         """Returns the checksum of the image we expect the test to produce, or None if it is a text-only test."""
    344         png_path = self.expected_filename(test, '.png')
    345         checksum_path = self._filesystem.splitext(png_path)[0] + '.checksum'
    346 
    347         if self.path_exists(checksum_path):
    348             return self._filesystem.read_binary_file(checksum_path)
    349 
    350         if self.path_exists(png_path):
    351             with self._filesystem.open_binary_file_for_reading(png_path) as filehandle:
    352                 return read_checksum_from_png.read_checksum(filehandle)
    353 
    354         return None
    355 
    356     def expected_image(self, test):
    357         """Returns the image we expect the test to produce."""
    358         path = self.expected_filename(test, '.png')
    359         if not self.path_exists(path):
    360             return None
    361         return self._filesystem.read_binary_file(path)
    362 
    363     def expected_audio(self, test):
    364         path = self.expected_filename(test, '.wav')
    365         if not self.path_exists(path):
    366             return None
    367         return self._filesystem.read_binary_file(path)
    368 
    369     def expected_text(self, test):
    370         """Returns the text output we expect the test to produce, or None
    371         if we don't expect there to be any text output.
    372         End-of-line characters are normalized to '\n'."""
    373         # FIXME: DRT output is actually utf-8, but since we don't decode the
    374         # output from DRT (instead treating it as a binary string), we read the
    375         # baselines as a binary string, too.
    376         path = self.expected_filename(test, '.txt')
    377         if not self.path_exists(path):
    378             return None
    379         text = self._filesystem.read_binary_file(path)
    380         return text.replace("\r\n", "\n")
    381 
    382     def reftest_expected_filename(self, filename):
    383         """Return the filename of reference we expect the test matches."""
    384         return self.expected_filename(filename, '.html')
    385 
    386     def reftest_expected_mismatch_filename(self, filename):
    387         """Return the filename of reference we don't expect the test matches."""
    388         return self.expected_filename(filename, '-mismatch.html')
    389 
    390     def filename_to_uri(self, filename):
    391         """Convert a test file (which is an absolute path) to a URI."""
    392         LAYOUTTEST_HTTP_DIR = "http/tests/"
    393         LAYOUTTEST_WEBSOCKET_DIR = "http/tests/websocket/tests/"
    394 
    395         relative_path = self.relative_test_filename(filename)
    396         port = None
    397         use_ssl = False
    398 
    399         if (relative_path.startswith(LAYOUTTEST_WEBSOCKET_DIR)
    400             or relative_path.startswith(LAYOUTTEST_HTTP_DIR)):
    401             relative_path = relative_path[len(LAYOUTTEST_HTTP_DIR):]
    402             port = 8000
    403 
    404         # Make http/tests/local run as local files. This is to mimic the
    405         # logic in run-webkit-tests.
    406         #
    407         # TODO(dpranke): remove the SSL reference?
    408         if (port and not relative_path.startswith("local/")):
    409             if relative_path.startswith("ssl/"):
    410                 port += 443
    411                 protocol = "https"
    412             else:
    413                 protocol = "http"
    414             return "%s://127.0.0.1:%u/%s" % (protocol, port, relative_path)
    415 
    416         return path.abspath_to_uri(self._filesystem.abspath(filename))
    417 
    418     def tests(self, paths):
    419         """Return the list of tests found (relative to layout_tests_dir()."""
    420         return test_files.find(self, paths)
    421 
    422     def test_dirs(self):
    423         """Returns the list of top-level test directories.
    424 
    425         Used by --clobber-old-results."""
    426         layout_tests_dir = self.layout_tests_dir()
    427         return filter(lambda x: self._filesystem.isdir(self._filesystem.join(layout_tests_dir, x)),
    428                       self._filesystem.listdir(layout_tests_dir))
    429 
    430     def path_isdir(self, path):
    431         """Return True if the path refers to a directory of tests."""
    432         # Used by test_expectations.py to apply rules to whole directories.
    433         return self._filesystem.isdir(path)
    434 
    435     def path_exists(self, path):
    436         """Return True if the path refers to an existing test or baseline."""
    437         # Used by test_expectations.py to determine if an entry refers to a
    438         # valid test and by printing.py to determine if baselines exist.
    439         return self._filesystem.exists(path)
    440 
    441     def driver_cmd_line(self):
    442         """Prints the DRT command line that will be used."""
    443         driver = self.create_driver(0)
    444         return driver.cmd_line()
    445 
    446     def update_baseline(self, path, data):
    447         """Updates the baseline for a test.
    448 
    449         Args:
    450             path: the actual path to use for baseline, not the path to
    451               the test. This function is used to update either generic or
    452               platform-specific baselines, but we can't infer which here.
    453             data: contents of the baseline.
    454         """
    455         self._filesystem.write_binary_file(path, data)
    456 
    457     def uri_to_test_name(self, uri):
    458         """Return the base layout test name for a given URI.
    459 
    460         This returns the test name for a given URI, e.g., if you passed in
    461         "file:///src/LayoutTests/fast/html/keygen.html" it would return
    462         "fast/html/keygen.html".
    463 
    464         """
    465         test = uri
    466         if uri.startswith("file:///"):
    467             prefix = path.abspath_to_uri(self.layout_tests_dir()) + "/"
    468             return test[len(prefix):]
    469 
    470         if uri.startswith("http://127.0.0.1:8880/"):
    471             # websocket tests
    472             return test.replace('http://127.0.0.1:8880/', '')
    473 
    474         if uri.startswith("http://"):
    475             # regular HTTP test
    476             return test.replace('http://127.0.0.1:8000/', 'http/tests/')
    477 
    478         if uri.startswith("https://"):
    479             return test.replace('https://127.0.0.1:8443/', 'http/tests/')
    480 
    481         raise NotImplementedError('unknown url type: %s' % uri)
    482 
    483     def layout_tests_dir(self):
    484         """Return the absolute path to the top of the LayoutTests directory."""
    485         return self.path_from_webkit_base('LayoutTests')
    486 
    487     def skips_layout_test(self, test_name):
    488         """Figures out if the givent test is being skipped or not.
    489 
    490         Test categories are handled as well."""
    491         for test_or_category in self.skipped_layout_tests():
    492             if test_or_category == test_name:
    493                 return True
    494             category = self._filesystem.join(self.layout_tests_dir(),
    495                                              test_or_category)
    496             if (self._filesystem.isdir(category) and
    497                 test_name.startswith(test_or_category)):
    498                 return True
    499         return False
    500 
    501     def maybe_make_directory(self, *path):
    502         """Creates the specified directory if it doesn't already exist."""
    503         self._filesystem.maybe_make_directory(*path)
    504 
    505     def name(self):
    506         """Return the name of the port (e.g., 'mac', 'chromium-win-xp')."""
    507         return self._name
    508 
    509     def operating_system(self):
    510         return self._operating_system
    511 
    512     def version(self):
    513         """Returns a string indicating the version of a given platform, e.g.
    514         'leopard' or 'xp'.
    515 
    516         This is used to help identify the exact port when parsing test
    517         expectations, determining search paths, and logging information."""
    518         return self._version
    519 
    520     def graphics_type(self):
    521         """Returns whether the port uses accelerated graphics ('gpu') or not
    522         ('cpu')."""
    523         return self._graphics_type
    524 
    525     def architecture(self):
    526         return self._architecture
    527 
    528     def real_name(self):
    529         """Returns the actual name of the port, not the delegate's."""
    530         return self.name()
    531 
    532     def get_option(self, name, default_value=None):
    533         # FIXME: Eventually we should not have to do a test for
    534         # hasattr(), and we should be able to just do
    535         # self.options.value. See additional FIXME in the constructor.
    536         if hasattr(self._options, name):
    537             return getattr(self._options, name)
    538         return default_value
    539 
    540     def set_option_default(self, name, default_value):
    541         if not hasattr(self._options, name):
    542             return setattr(self._options, name, default_value)
    543 
    544     def path_from_webkit_base(self, *comps):
    545         """Returns the full path to path made by joining the top of the
    546         WebKit source tree and the list of path components in |*comps|."""
    547         return self._config.path_from_webkit_base(*comps)
    548 
    549     def script_path(self, script_name):
    550         return self._config.script_path(script_name)
    551 
    552     def script_shell_command(self, script_name):
    553         return self._config.script_shell_command(script_name)
    554 
    555     def path_to_test_expectations_file(self):
    556         """Update the test expectations to the passed-in string.
    557 
    558         This is used by the rebaselining tool. Raises NotImplementedError
    559         if the port does not use expectations files."""
    560         raise NotImplementedError('Port.path_to_test_expectations_file')
    561 
    562     def relative_test_filename(self, filename):
    563         """Relative unix-style path for a filename under the LayoutTests
    564         directory. Filenames outside the LayoutTests directory should raise
    565         an error."""
    566         # FIXME: On Windows, does this return test_names with forward slashes,
    567         # or windows-style relative paths?
    568         assert filename.startswith(self.layout_tests_dir()), "%s did not start with %s" % (filename, self.layout_tests_dir())
    569         return filename[len(self.layout_tests_dir()) + 1:]
    570 
    571     def abspath_for_test(self, test_name):
    572         """Returns the full path to the file for a given test name. This is the
    573         inverse of relative_test_filename()."""
    574         return self._filesystem.normpath(self._filesystem.join(self.layout_tests_dir(), test_name))
    575 
    576     def results_directory(self):
    577         """Absolute path to the place to store the test results (uses --results-directory)."""
    578         if not self._results_directory:
    579             option_val = self.get_option('results_directory') or self.default_results_directory()
    580             self._results_directory = self._filesystem.abspath(option_val)
    581         return self._results_directory
    582 
    583     def default_results_directory(self):
    584         """Absolute path to the default place to store the test results."""
    585         raise NotImplementedError()
    586 
    587     def setup_test_run(self):
    588         """Perform port-specific work at the beginning of a test run."""
    589         pass
    590 
    591     def setup_environ_for_server(self):
    592         """Perform port-specific work at the beginning of a server launch.
    593 
    594         Returns:
    595            Operating-system's environment.
    596         """
    597         return os.environ.copy()
    598 
    599     def show_results_html_file(self, results_filename):
    600         """This routine should display the HTML file pointed at by
    601         results_filename in a users' browser."""
    602         return self._user.open_url(results_filename)
    603 
    604     def create_driver(self, worker_number):
    605         """Return a newly created base.Driver subclass for starting/stopping
    606         the test driver."""
    607         raise NotImplementedError('Port.create_driver')
    608 
    609     def start_helper(self):
    610         """If a port needs to reconfigure graphics settings or do other
    611         things to ensure a known test configuration, it should override this
    612         method."""
    613         pass
    614 
    615     def start_http_server(self):
    616         """Start a web server if it is available. Do nothing if
    617         it isn't. This routine is allowed to (and may) fail if a server
    618         is already running."""
    619         if self.get_option('use_apache'):
    620             self._http_server = apache_http_server.LayoutTestApacheHttpd(self,
    621                 self.results_directory())
    622         else:
    623             self._http_server = http_server.Lighttpd(self, self.results_directory())
    624         self._http_server.start()
    625 
    626     def start_websocket_server(self):
    627         """Start a websocket server if it is available. Do nothing if
    628         it isn't. This routine is allowed to (and may) fail if a server
    629         is already running."""
    630         self._websocket_server = websocket_server.PyWebSocket(self, self.results_directory())
    631         self._websocket_server.start()
    632 
    633     def acquire_http_lock(self):
    634         self._http_lock = http_lock.HttpLock(None)
    635         self._http_lock.wait_for_httpd_lock()
    636 
    637     def stop_helper(self):
    638         """Shut down the test helper if it is running. Do nothing if
    639         it isn't, or it isn't available. If a port overrides start_helper()
    640         it must override this routine as well."""
    641         pass
    642 
    643     def stop_http_server(self):
    644         """Shut down the http server if it is running. Do nothing if
    645         it isn't, or it isn't available."""
    646         if self._http_server:
    647             self._http_server.stop()
    648 
    649     def stop_websocket_server(self):
    650         """Shut down the websocket server if it is running. Do nothing if
    651         it isn't, or it isn't available."""
    652         if self._websocket_server:
    653             self._websocket_server.stop()
    654 
    655     def release_http_lock(self):
    656         if self._http_lock:
    657             self._http_lock.cleanup_http_lock()
    658 
    659     #
    660     # TEST EXPECTATION-RELATED METHODS
    661     #
    662 
    663     def test_configuration(self):
    664         """Returns the current TestConfiguration for the port."""
    665         if not self._test_configuration:
    666             self._test_configuration = TestConfiguration(self)
    667         return self._test_configuration
    668 
    669     def all_test_configurations(self):
    670         return self.test_configuration().all_test_configurations()
    671 
    672     def all_baseline_variants(self):
    673         """Returns a list of platform names sufficient to cover all the baselines.
    674 
    675         The list should be sorted so that a later platform  will reuse
    676         an earlier platform's baselines if they are the same (e.g.,
    677         'snowleopard' should precede 'leopard')."""
    678         raise NotImplementedError
    679 
    680     def test_expectations(self):
    681         """Returns the test expectations for this port.
    682 
    683         Basically this string should contain the equivalent of a
    684         test_expectations file. See test_expectations.py for more details."""
    685         return self._filesystem.read_text_file(self.path_to_test_expectations_file())
    686 
    687     def test_expectations_overrides(self):
    688         """Returns an optional set of overrides for the test_expectations.
    689 
    690         This is used by ports that have code in two repositories, and where
    691         it is possible that you might need "downstream" expectations that
    692         temporarily override the "upstream" expectations until the port can
    693         sync up the two repos."""
    694         return None
    695 
    696     def test_repository_paths(self):
    697         """Returns a list of (repository_name, repository_path) tuples
    698         of its depending code base.  By default it returns a list that only
    699         contains a ('webkit', <webkitRepossitoryPath>) tuple.
    700         """
    701         return [('webkit', self.layout_tests_dir())]
    702 
    703 
    704     _WDIFF_DEL = '##WDIFF_DEL##'
    705     _WDIFF_ADD = '##WDIFF_ADD##'
    706     _WDIFF_END = '##WDIFF_END##'
    707 
    708     def _format_wdiff_output_as_html(self, wdiff):
    709         wdiff = cgi.escape(wdiff)
    710         wdiff = wdiff.replace(self._WDIFF_DEL, "<span class=del>")
    711         wdiff = wdiff.replace(self._WDIFF_ADD, "<span class=add>")
    712         wdiff = wdiff.replace(self._WDIFF_END, "</span>")
    713         html = "<head><style>.del { background: #faa; } "
    714         html += ".add { background: #afa; }</style></head>"
    715         html += "<pre>%s</pre>" % wdiff
    716         return html
    717 
    718     def _wdiff_command(self, actual_filename, expected_filename):
    719         executable = self._path_to_wdiff()
    720         return [executable,
    721                 "--start-delete=%s" % self._WDIFF_DEL,
    722                 "--end-delete=%s" % self._WDIFF_END,
    723                 "--start-insert=%s" % self._WDIFF_ADD,
    724                 "--end-insert=%s" % self._WDIFF_END,
    725                 actual_filename,
    726                 expected_filename]
    727 
    728     @staticmethod
    729     def _handle_wdiff_error(script_error):
    730         # Exit 1 means the files differed, any other exit code is an error.
    731         if script_error.exit_code != 1:
    732             raise script_error
    733 
    734     def _run_wdiff(self, actual_filename, expected_filename):
    735         """Runs wdiff and may throw exceptions.
    736         This is mostly a hook for unit testing."""
    737         # Diffs are treated as binary as they may include multiple files
    738         # with conflicting encodings.  Thus we do not decode the output.
    739         command = self._wdiff_command(actual_filename, expected_filename)
    740         wdiff = self._executive.run_command(command, decode_output=False,
    741             error_handler=self._handle_wdiff_error)
    742         return self._format_wdiff_output_as_html(wdiff)
    743 
    744     def wdiff_text(self, actual_filename, expected_filename):
    745         """Returns a string of HTML indicating the word-level diff of the
    746         contents of the two filenames. Returns an empty string if word-level
    747         diffing isn't available."""
    748         if not self._wdiff_available:
    749             return ""
    750         try:
    751             # It's possible to raise a ScriptError we pass wdiff invalid paths.
    752             return self._run_wdiff(actual_filename, expected_filename)
    753         except OSError, e:
    754             if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]:
    755                 # Silently ignore cases where wdiff is missing.
    756                 self._wdiff_available = False
    757                 return ""
    758             raise
    759 
    760     # This is a class variable so we can test error output easily.
    761     _pretty_patch_error_html = "Failed to run PrettyPatch, see error log."
    762 
    763     def pretty_patch_text(self, diff_path):
    764         if self._pretty_patch_available is None:
    765             self._pretty_patch_available = self.check_pretty_patch(logging=False)
    766         if not self._pretty_patch_available:
    767             return self._pretty_patch_error_html
    768         command = ("ruby", "-I", self._filesystem.dirname(self._pretty_patch_path),
    769                    self._pretty_patch_path, diff_path)
    770         try:
    771             # Diffs are treated as binary (we pass decode_output=False) as they
    772             # may contain multiple files of conflicting encodings.
    773             return self._executive.run_command(command, decode_output=False)
    774         except OSError, e:
    775             # If the system is missing ruby log the error and stop trying.
    776             self._pretty_patch_available = False
    777             _log.error("Failed to run PrettyPatch (%s): %s" % (command, e))
    778             return self._pretty_patch_error_html
    779         except ScriptError, e:
    780             # If ruby failed to run for some reason, log the command
    781             # output and stop trying.
    782             self._pretty_patch_available = False
    783             _log.error("Failed to run PrettyPatch (%s):\n%s" % (command,
    784                        e.message_with_output()))
    785             return self._pretty_patch_error_html
    786 
    787     def default_configuration(self):
    788         return self._config.default_configuration()
    789 
    790     #
    791     # PROTECTED ROUTINES
    792     #
    793     # The routines below should only be called by routines in this class
    794     # or any of its subclasses.
    795     #
    796     def _webkit_build_directory(self, args):
    797         return self._config.build_directory(args[0])
    798 
    799     def _path_to_apache(self):
    800         """Returns the full path to the apache binary.
    801 
    802         This is needed only by ports that use the apache_http_server module."""
    803         raise NotImplementedError('Port.path_to_apache')
    804 
    805     def _path_to_apache_config_file(self):
    806         """Returns the full path to the apache binary.
    807 
    808         This is needed only by ports that use the apache_http_server module."""
    809         raise NotImplementedError('Port.path_to_apache_config_file')
    810 
    811     def _path_to_driver(self, configuration=None):
    812         """Returns the full path to the test driver (DumpRenderTree)."""
    813         raise NotImplementedError('Port._path_to_driver')
    814 
    815     def _path_to_webcore_library(self):
    816         """Returns the full path to a built copy of WebCore."""
    817         raise NotImplementedError('Port.path_to_webcore_library')
    818 
    819     def _path_to_helper(self):
    820         """Returns the full path to the layout_test_helper binary, which
    821         is used to help configure the system for the test run, or None
    822         if no helper is needed.
    823 
    824         This is likely only used by start/stop_helper()."""
    825         raise NotImplementedError('Port._path_to_helper')
    826 
    827     def _path_to_image_diff(self):
    828         """Returns the full path to the image_diff binary, or None if it
    829         is not available.
    830 
    831         This is likely used only by diff_image()"""
    832         raise NotImplementedError('Port.path_to_image_diff')
    833 
    834     def _path_to_lighttpd(self):
    835         """Returns the path to the LigHTTPd binary.
    836 
    837         This is needed only by ports that use the http_server.py module."""
    838         raise NotImplementedError('Port._path_to_lighttpd')
    839 
    840     def _path_to_lighttpd_modules(self):
    841         """Returns the path to the LigHTTPd modules directory.
    842 
    843         This is needed only by ports that use the http_server.py module."""
    844         raise NotImplementedError('Port._path_to_lighttpd_modules')
    845 
    846     def _path_to_lighttpd_php(self):
    847         """Returns the path to the LigHTTPd PHP executable.
    848 
    849         This is needed only by ports that use the http_server.py module."""
    850         raise NotImplementedError('Port._path_to_lighttpd_php')
    851 
    852     def _path_to_wdiff(self):
    853         """Returns the full path to the wdiff binary, or None if it is
    854         not available.
    855 
    856         This is likely used only by wdiff_text()"""
    857         raise NotImplementedError('Port._path_to_wdiff')
    858 
    859     def _shut_down_http_server(self, pid):
    860         """Forcefully and synchronously kills the web server.
    861 
    862         This routine should only be called from http_server.py or its
    863         subclasses."""
    864         raise NotImplementedError('Port._shut_down_http_server')
    865 
    866     def _webkit_baseline_path(self, platform):
    867         """Return the  full path to the top of the baseline tree for a
    868         given platform."""
    869         return self._filesystem.join(self.layout_tests_dir(), 'platform',
    870                                      platform)
    871 
    872 
    873 class DriverInput(object):
    874     """Holds the input parameters for a driver."""
    875 
    876     def __init__(self, filename, timeout, image_hash):
    877         """Initializes a DriverInput object.
    878 
    879         Args:
    880           filename: Full path to the test.
    881           timeout: Timeout in msecs the driver should use while running the test
    882           image_hash: A image checksum which is used to avoid doing an image dump if
    883                      the checksums match.
    884         """
    885         self.filename = filename
    886         self.timeout = timeout
    887         self.image_hash = image_hash
    888 
    889 
    890 class DriverOutput(object):
    891     """Groups information about a output from driver for easy passing of data."""
    892 
    893     def __init__(self, text, image, image_hash, audio,
    894                  crash=False, test_time=0, timeout=False, error=''):
    895         """Initializes a TestOutput object.
    896 
    897         Args:
    898           text: a text output
    899           image: an image output
    900           image_hash: a string containing the checksum of the image
    901           audio: contents of an audio stream, if any (in WAV format)
    902           crash: a boolean indicating whether the driver crashed on the test
    903           test_time: the time the test took to execute
    904           timeout: a boolean indicating whehter the test timed out
    905           error: any unexpected or additional (or error) text output
    906         """
    907         self.text = text
    908         self.image = image
    909         self.image_hash = image_hash
    910         self.audio = audio
    911         self.crash = crash
    912         self.test_time = test_time
    913         self.timeout = timeout
    914         self.error = error
    915 
    916 
    917 class Driver:
    918     """Abstract interface for the DumpRenderTree interface."""
    919 
    920     def __init__(self, port, worker_number):
    921         """Initialize a Driver to subsequently run tests.
    922 
    923         Typically this routine will spawn DumpRenderTree in a config
    924         ready for subsequent input.
    925 
    926         port - reference back to the port object.
    927         worker_number - identifier for a particular worker/driver instance
    928         """
    929         raise NotImplementedError('Driver.__init__')
    930 
    931     def run_test(self, driver_input):
    932         """Run a single test and return the results.
    933 
    934         Note that it is okay if a test times out or crashes and leaves
    935         the driver in an indeterminate state. The upper layers of the program
    936         are responsible for cleaning up and ensuring things are okay.
    937 
    938         Args:
    939           driver_input: a DriverInput object
    940 
    941         Returns a DriverOutput object.
    942           Note that DriverOutput.image will be '' (empty string) if a test crashes.
    943         """
    944         raise NotImplementedError('Driver.run_test')
    945 
    946     # FIXME: This is static so we can test it w/o creating a Base instance.
    947     @classmethod
    948     def _command_wrapper(cls, wrapper_option):
    949         # Hook for injecting valgrind or other runtime instrumentation,
    950         # used by e.g. tools/valgrind/valgrind_tests.py.
    951         wrapper = []
    952         browser_wrapper = os.environ.get("BROWSER_WRAPPER", None)
    953         if browser_wrapper:
    954             # FIXME: There seems to be no reason to use BROWSER_WRAPPER over --wrapper.
    955             # Remove this code any time after the date listed below.
    956             _log.error("BROWSER_WRAPPER is deprecated, please use --wrapper instead.")
    957             _log.error("BROWSER_WRAPPER will be removed any time after June 1st 2010 and your scripts will break.")
    958             wrapper += [browser_wrapper]
    959 
    960         if wrapper_option:
    961             wrapper += shlex.split(wrapper_option)
    962         return wrapper
    963 
    964     def poll(self):
    965         """Returns None if the Driver is still running. Returns the returncode
    966         if it has exited."""
    967         raise NotImplementedError('Driver.poll')
    968 
    969     def stop(self):
    970         raise NotImplementedError('Driver.stop')
    971 
    972 
    973 class TestConfiguration(object):
    974     def __init__(self, port=None, os=None, version=None, architecture=None,
    975                  build_type=None, graphics_type=None):
    976         self.os = os or port.operating_system()
    977         self.version = version or port.version()
    978         self.architecture = architecture or port.architecture()
    979         self.build_type = build_type or port._options.configuration.lower()
    980         self.graphics_type = graphics_type or port.graphics_type()
    981 
    982     def items(self):
    983         return self.__dict__.items()
    984 
    985     def keys(self):
    986         return self.__dict__.keys()
    987 
    988     def __str__(self):
    989         return ("<%(os)s, %(version)s, %(architecture)s, %(build_type)s, %(graphics_type)s>" %
    990                 self.__dict__)
    991 
    992     def __repr__(self):
    993         return "TestConfig(os='%(os)s', version='%(version)s', architecture='%(architecture)s', build_type='%(build_type)s', graphics_type='%(graphics_type)s')" % self.__dict__
    994 
    995     def values(self):
    996         """Returns the configuration values of this instance as a tuple."""
    997         return self.__dict__.values()
    998 
    999     def all_test_configurations(self):
   1000         """Returns a sequence of the TestConfigurations the port supports."""
   1001         # By default, we assume we want to test every graphics type in
   1002         # every configuration on every system.
   1003         test_configurations = []
   1004         for system in self.all_systems():
   1005             for build_type in self.all_build_types():
   1006                 for graphics_type in self.all_graphics_types():
   1007                     test_configurations.append(TestConfiguration(
   1008                         os=system[0],
   1009                         version=system[1],
   1010                         architecture=system[2],
   1011                         build_type=build_type,
   1012                         graphics_type=graphics_type))
   1013         return test_configurations
   1014 
   1015     def all_systems(self):
   1016         return (('mac', 'leopard', 'x86'),
   1017                 ('mac', 'snowleopard', 'x86'),
   1018                 ('win', 'xp', 'x86'),
   1019                 ('win', 'vista', 'x86'),
   1020                 ('win', 'win7', 'x86'),
   1021                 ('linux', 'hardy', 'x86'),
   1022                 ('linux', 'hardy', 'x86_64'))
   1023 
   1024     def all_build_types(self):
   1025         return ('debug', 'release')
   1026 
   1027     def all_graphics_types(self):
   1028         return ('cpu', 'gpu')
   1029