Home | History | Annotate | Download | only in port
      1 # Copyright (C) 2010 Google Inc. All rights reserved.
      2 #
      3 # Redistribution and use in source and binary forms, with or without
      4 # modification, are permitted provided that the following conditions are
      5 # met:
      6 #
      7 #     * Redistributions of source code must retain the above copyright
      8 # notice, this list of conditions and the following disclaimer.
      9 #     * Redistributions in binary form must reproduce the above
     10 # copyright notice, this list of conditions and the following disclaimer
     11 # in the documentation and/or other materials provided with the
     12 # distribution.
     13 #     * Neither the Google name nor the names of its
     14 # contributors may be used to endorse or promote products derived from
     15 # this software without specific prior written permission.
     16 #
     17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     28 
     29 """Abstract base class of Port-specific entry points for the layout tests
     30 test infrastructure (the Port and Driver classes)."""
     31 
     32 import cgi
     33 import difflib
     34 import errno
     35 import itertools
     36 import json
     37 import logging
     38 import os
     39 import operator
     40 import optparse
     41 import re
     42 import sys
     43 
     44 try:
     45     from collections import OrderedDict
     46 except ImportError:
     47     # Needed for Python < 2.7
     48     from webkitpy.thirdparty.ordered_dict import OrderedDict
     49 
     50 
     51 from webkitpy.common import find_files
     52 from webkitpy.common import read_checksum_from_png
     53 from webkitpy.common.memoized import memoized
     54 from webkitpy.common.system import path
     55 from webkitpy.common.system.executive import ScriptError
     56 from webkitpy.common.system.path import cygpath
     57 from webkitpy.common.system.systemhost import SystemHost
     58 from webkitpy.common.webkit_finder import WebKitFinder
     59 from webkitpy.layout_tests.layout_package.bot_test_expectations import BotTestExpectationsFactory
     60 from webkitpy.layout_tests.models import test_run_results
     61 from webkitpy.layout_tests.models.test_configuration import TestConfiguration
     62 from webkitpy.layout_tests.port import config as port_config
     63 from webkitpy.layout_tests.port import driver
     64 from webkitpy.layout_tests.port import server_process
     65 from webkitpy.layout_tests.port.factory import PortFactory
     66 from webkitpy.layout_tests.servers import apache_http
     67 from webkitpy.layout_tests.servers import pywebsocket
     68 
     69 _log = logging.getLogger(__name__)
     70 
     71 
     72 # FIXME: This class should merge with WebKitPort now that Chromium behaves mostly like other webkit ports.
     73 class Port(object):
     74     """Abstract class for Port-specific hooks for the layout_test package."""
     75 
     76     # Subclasses override this. This should indicate the basic implementation
     77     # part of the port name, e.g., 'mac', 'win', 'gtk'; there is probably (?)
     78     # one unique value per class.
     79 
     80     # FIXME: We should probably rename this to something like 'implementation_name'.
     81     port_name = None
     82 
     83     # Test names resemble unix relative paths, and use '/' as a directory separator.
     84     TEST_PATH_SEPARATOR = '/'
     85 
     86     ALL_BUILD_TYPES = ('debug', 'release')
     87 
     88     CONTENT_SHELL_NAME = 'content_shell'
     89 
     90     # True if the port as aac and mp3 codecs built in.
     91     PORT_HAS_AUDIO_CODECS_BUILT_IN = False
     92 
     93     ALL_SYSTEMS = (
     94         ('snowleopard', 'x86'),
     95         ('lion', 'x86'),
     96 
     97         # FIXME: We treat Retina (High-DPI) devices as if they are running
     98         # a different operating system version. This isn't accurate, but will work until
     99         # we need to test and support baselines across multiple O/S versions.
    100         ('retina', 'x86'),
    101 
    102         ('mountainlion', 'x86'),
    103         ('mavericks', 'x86'),
    104         ('xp', 'x86'),
    105         ('win7', 'x86'),
    106         ('lucid', 'x86'),
    107         ('lucid', 'x86_64'),
    108         # FIXME: Technically this should be 'arm', but adding a third architecture type breaks TestConfigurationConverter.
    109         # If we need this to be 'arm' in the future, then we first have to fix TestConfigurationConverter.
    110         ('icecreamsandwich', 'x86'),
    111         )
    112 
    113     ALL_BASELINE_VARIANTS = [
    114         'mac-mavericks', 'mac-mountainlion', 'mac-retina', 'mac-lion', 'mac-snowleopard',
    115         'win-win7', 'win-xp',
    116         'linux-x86_64', 'linux-x86',
    117     ]
    118 
    119     CONFIGURATION_SPECIFIER_MACROS = {
    120         'mac': ['snowleopard', 'lion', 'retina', 'mountainlion', 'mavericks'],
    121         'win': ['xp', 'win7'],
    122         'linux': ['lucid'],
    123         'android': ['icecreamsandwich'],
    124     }
    125 
    126     DEFAULT_BUILD_DIRECTORIES = ('out',)
    127 
    128     # overridden in subclasses.
    129     FALLBACK_PATHS = {}
    130 
    131     SUPPORTED_VERSIONS = []
    132 
    133     # URL to the build requirements page.
    134     BUILD_REQUIREMENTS_URL = ''
    135 
    136     @classmethod
    137     def latest_platform_fallback_path(cls):
    138         return cls.FALLBACK_PATHS[cls.SUPPORTED_VERSIONS[-1]]
    139 
    140     @classmethod
    141     def _static_build_path(cls, filesystem, build_directory, chromium_base, configuration, comps):
    142         if build_directory:
    143             return filesystem.join(build_directory, configuration, *comps)
    144 
    145         hits = []
    146         for directory in cls.DEFAULT_BUILD_DIRECTORIES:
    147             base_dir = filesystem.join(chromium_base, directory, configuration)
    148             path = filesystem.join(base_dir, *comps)
    149             if filesystem.exists(path):
    150                 hits.append((filesystem.mtime(path), path))
    151 
    152         if hits:
    153             hits.sort(reverse=True)
    154             return hits[0][1]  # Return the newest file found.
    155 
    156         # We have to default to something, so pick the last one.
    157         return filesystem.join(base_dir, *comps)
    158 
    159     @classmethod
    160     def determine_full_port_name(cls, host, options, port_name):
    161         """Return a fully-specified port name that can be used to construct objects."""
    162         # Subclasses will usually override this.
    163         assert port_name.startswith(cls.port_name)
    164         return port_name
    165 
    166     def __init__(self, host, port_name, options=None, **kwargs):
    167 
    168         # This value may be different from cls.port_name by having version modifiers
    169         # and other fields appended to it (for example, 'qt-arm' or 'mac-wk2').
    170         self._name = port_name
    171 
    172         # These are default values that should be overridden in a subclasses.
    173         self._version = ''
    174         self._architecture = 'x86'
    175 
    176         # FIXME: Ideally we'd have a package-wide way to get a
    177         # well-formed options object that had all of the necessary
    178         # options defined on it.
    179         self._options = options or optparse.Values()
    180 
    181         self.host = host
    182         self._executive = host.executive
    183         self._filesystem = host.filesystem
    184         self._webkit_finder = WebKitFinder(host.filesystem)
    185         self._config = port_config.Config(self._executive, self._filesystem, self.port_name)
    186 
    187         self._helper = None
    188         self._http_server = None
    189         self._websocket_server = None
    190         self._image_differ = None
    191         self._server_process_constructor = server_process.ServerProcess  # overridable for testing
    192         self._http_lock = None  # FIXME: Why does this live on the port object?
    193         self._dump_reader = None
    194 
    195         # Python's Popen has a bug that causes any pipes opened to a
    196         # process that can't be executed to be leaked.  Since this
    197         # code is specifically designed to tolerate exec failures
    198         # to gracefully handle cases where wdiff is not installed,
    199         # the bug results in a massive file descriptor leak. As a
    200         # workaround, if an exec failure is ever experienced for
    201         # wdiff, assume it's not available.  This will leak one
    202         # file descriptor but that's better than leaking each time
    203         # wdiff would be run.
    204         #
    205         # http://mail.python.org/pipermail/python-list/
    206         #    2008-August/505753.html
    207         # http://bugs.python.org/issue3210
    208         self._wdiff_available = None
    209 
    210         # FIXME: prettypatch.py knows this path, why is it copied here?
    211         self._pretty_patch_path = self.path_from_webkit_base("Tools", "Scripts", "webkitruby", "PrettyPatch", "prettify.rb")
    212         self._pretty_patch_available = None
    213 
    214         if not hasattr(options, 'configuration') or not options.configuration:
    215             self.set_option_default('configuration', self.default_configuration())
    216         self._test_configuration = None
    217         self._reftest_list = {}
    218         self._results_directory = None
    219         self._virtual_test_suites = None
    220 
    221     def buildbot_archives_baselines(self):
    222         return True
    223 
    224     def additional_drt_flag(self):
    225         if self.driver_name() == self.CONTENT_SHELL_NAME:
    226             return ['--dump-render-tree']
    227         return []
    228 
    229     def supports_per_test_timeout(self):
    230         return False
    231 
    232     def default_pixel_tests(self):
    233         return True
    234 
    235     def default_smoke_test_only(self):
    236         return False
    237 
    238     def default_timeout_ms(self):
    239         timeout_ms = 6 * 1000
    240         if self.get_option('configuration') == 'Debug':
    241             # Debug is usually 2x-3x slower than Release.
    242             return 3 * timeout_ms
    243         return timeout_ms
    244 
    245     def driver_stop_timeout(self):
    246         """ Returns the amount of time in seconds to wait before killing the process in driver.stop()."""
    247         # We want to wait for at least 3 seconds, but if we are really slow, we want to be slow on cleanup as
    248         # well (for things like ASAN, Valgrind, etc.)
    249         return 3.0 * float(self.get_option('time_out_ms', '0')) / self.default_timeout_ms()
    250 
    251     def wdiff_available(self):
    252         if self._wdiff_available is None:
    253             self._wdiff_available = self.check_wdiff(logging=False)
    254         return self._wdiff_available
    255 
    256     def pretty_patch_available(self):
    257         if self._pretty_patch_available is None:
    258             self._pretty_patch_available = self.check_pretty_patch(logging=False)
    259         return self._pretty_patch_available
    260 
    261     def default_child_processes(self):
    262         """Return the number of drivers to use for this port."""
    263         return self._executive.cpu_count()
    264 
    265     def default_max_locked_shards(self):
    266         """Return the number of "locked" shards to run in parallel (like the http tests)."""
    267         max_locked_shards = int(self.default_child_processes()) / 4
    268         if not max_locked_shards:
    269             return 1
    270         return max_locked_shards
    271 
    272     def baseline_path(self):
    273         """Return the absolute path to the directory to store new baselines in for this port."""
    274         # FIXME: remove once all callers are calling either baseline_version_dir() or baseline_platform_dir()
    275         return self.baseline_version_dir()
    276 
    277     def baseline_platform_dir(self):
    278         """Return the absolute path to the default (version-independent) platform-specific results."""
    279         return self._filesystem.join(self.layout_tests_dir(), 'platform', self.port_name)
    280 
    281     def baseline_version_dir(self):
    282         """Return the absolute path to the platform-and-version-specific results."""
    283         baseline_search_paths = self.baseline_search_path()
    284         return baseline_search_paths[0]
    285 
    286     def virtual_baseline_search_path(self, test_name):
    287         suite = self.lookup_virtual_suite(test_name)
    288         if not suite:
    289             return None
    290         return [self._filesystem.join(path, suite.name) for path in self.default_baseline_search_path()]
    291 
    292     def baseline_search_path(self):
    293         return self.get_option('additional_platform_directory', []) + self._compare_baseline() + self.default_baseline_search_path()
    294 
    295     def default_baseline_search_path(self):
    296         """Return a list of absolute paths to directories to search under for
    297         baselines. The directories are searched in order."""
    298         return map(self._webkit_baseline_path, self.FALLBACK_PATHS[self.version()])
    299 
    300     @memoized
    301     def _compare_baseline(self):
    302         factory = PortFactory(self.host)
    303         target_port = self.get_option('compare_port')
    304         if target_port:
    305             return factory.get(target_port).default_baseline_search_path()
    306         return []
    307 
    308     def _check_file_exists(self, path_to_file, file_description,
    309                            override_step=None, logging=True):
    310         """Verify the file is present where expected or log an error.
    311 
    312         Args:
    313             file_name: The (human friendly) name or description of the file
    314                 you're looking for (e.g., "HTTP Server"). Used for error logging.
    315             override_step: An optional string to be logged if the check fails.
    316             logging: Whether or not log the error messages."""
    317         if not self._filesystem.exists(path_to_file):
    318             if logging:
    319                 _log.error('Unable to find %s' % file_description)
    320                 _log.error('    at %s' % path_to_file)
    321                 if override_step:
    322                     _log.error('    %s' % override_step)
    323                     _log.error('')
    324             return False
    325         return True
    326 
    327     def check_build(self, needs_http, printer):
    328         result = True
    329 
    330         dump_render_tree_binary_path = self._path_to_driver()
    331         result = self._check_file_exists(dump_render_tree_binary_path,
    332                                          'test driver') and result
    333         if not result and self.get_option('build'):
    334             result = self._check_driver_build_up_to_date(
    335                 self.get_option('configuration'))
    336         else:
    337             _log.error('')
    338 
    339         helper_path = self._path_to_helper()
    340         if helper_path:
    341             result = self._check_file_exists(helper_path,
    342                                              'layout test helper') and result
    343 
    344         if self.get_option('pixel_tests'):
    345             result = self.check_image_diff(
    346                 'To override, invoke with --no-pixel-tests') and result
    347 
    348         # It's okay if pretty patch and wdiff aren't available, but we will at least log messages.
    349         self._pretty_patch_available = self.check_pretty_patch()
    350         self._wdiff_available = self.check_wdiff()
    351 
    352         if self._dump_reader:
    353             result = self._dump_reader.check_is_functional() and result
    354 
    355         if needs_http:
    356             result = self.check_httpd() and result
    357 
    358         return test_run_results.OK_EXIT_STATUS if result else test_run_results.UNEXPECTED_ERROR_EXIT_STATUS
    359 
    360     def _check_driver(self):
    361         driver_path = self._path_to_driver()
    362         if not self._filesystem.exists(driver_path):
    363             _log.error("%s was not found at %s" % (self.driver_name(), driver_path))
    364             return False
    365         return True
    366 
    367     def _check_port_build(self):
    368         # Ports can override this method to do additional checks.
    369         return True
    370 
    371     def check_sys_deps(self, needs_http):
    372         """If the port needs to do some runtime checks to ensure that the
    373         tests can be run successfully, it should override this routine.
    374         This step can be skipped with --nocheck-sys-deps.
    375 
    376         Returns whether the system is properly configured."""
    377         cmd = [self._path_to_driver(), '--check-layout-test-sys-deps']
    378 
    379         local_error = ScriptError()
    380 
    381         def error_handler(script_error):
    382             local_error.exit_code = script_error.exit_code
    383 
    384         output = self._executive.run_command(cmd, error_handler=error_handler)
    385         if local_error.exit_code:
    386             _log.error('System dependencies check failed.')
    387             _log.error('To override, invoke with --nocheck-sys-deps')
    388             _log.error('')
    389             _log.error(output)
    390             if self.BUILD_REQUIREMENTS_URL is not '':
    391                 _log.error('')
    392                 _log.error('For complete build requirements, please see:')
    393                 _log.error(self.BUILD_REQUIREMENTS_URL)
    394             return test_run_results.SYS_DEPS_EXIT_STATUS
    395         return test_run_results.OK_EXIT_STATUS
    396 
    397     def check_image_diff(self, override_step=None, logging=True):
    398         """This routine is used to check whether image_diff binary exists."""
    399         image_diff_path = self._path_to_image_diff()
    400         if not self._filesystem.exists(image_diff_path):
    401             _log.error("image_diff was not found at %s" % image_diff_path)
    402             return False
    403         return True
    404 
    405     def check_pretty_patch(self, logging=True):
    406         """Checks whether we can use the PrettyPatch ruby script."""
    407         try:
    408             _ = self._executive.run_command(['ruby', '--version'])
    409         except OSError, e:
    410             if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]:
    411                 if logging:
    412                     _log.warning("Ruby is not installed; can't generate pretty patches.")
    413                     _log.warning('')
    414                 return False
    415 
    416         if not self._filesystem.exists(self._pretty_patch_path):
    417             if logging:
    418                 _log.warning("Unable to find %s; can't generate pretty patches." % self._pretty_patch_path)
    419                 _log.warning('')
    420             return False
    421 
    422         return True
    423 
    424     def check_wdiff(self, logging=True):
    425         if not self._path_to_wdiff():
    426             # Don't need to log here since this is the port choosing not to use wdiff.
    427             return False
    428 
    429         try:
    430             _ = self._executive.run_command([self._path_to_wdiff(), '--help'])
    431         except OSError:
    432             if logging:
    433                 message = self._wdiff_missing_message()
    434                 if message:
    435                     for line in message.splitlines():
    436                         _log.warning('    ' + line)
    437                         _log.warning('')
    438             return False
    439 
    440         return True
    441 
    442     def _wdiff_missing_message(self):
    443         return 'wdiff is not installed; please install it to generate word-by-word diffs.'
    444 
    445     def check_httpd(self):
    446         httpd_path = self.path_to_apache()
    447         try:
    448             server_name = self._filesystem.basename(httpd_path)
    449             env = self.setup_environ_for_server(server_name)
    450             if self._executive.run_command([httpd_path, "-v"], env=env, return_exit_code=True) != 0:
    451                 _log.error("httpd seems broken. Cannot run http tests.")
    452                 return False
    453             return True
    454         except OSError:
    455             _log.error("No httpd found. Cannot run http tests.")
    456             return False
    457 
    458     def do_text_results_differ(self, expected_text, actual_text):
    459         return expected_text != actual_text
    460 
    461     def do_audio_results_differ(self, expected_audio, actual_audio):
    462         return expected_audio != actual_audio
    463 
    464     def diff_image(self, expected_contents, actual_contents):
    465         """Compare two images and return a tuple of an image diff, and an error string.
    466 
    467         If an error occurs (like image_diff isn't found, or crashes, we log an error and return True (for a diff).
    468         """
    469         # If only one of them exists, return that one.
    470         if not actual_contents and not expected_contents:
    471             return (None, None)
    472         if not actual_contents:
    473             return (expected_contents, None)
    474         if not expected_contents:
    475             return (actual_contents, None)
    476 
    477         tempdir = self._filesystem.mkdtemp()
    478 
    479         expected_filename = self._filesystem.join(str(tempdir), "expected.png")
    480         self._filesystem.write_binary_file(expected_filename, expected_contents)
    481 
    482         actual_filename = self._filesystem.join(str(tempdir), "actual.png")
    483         self._filesystem.write_binary_file(actual_filename, actual_contents)
    484 
    485         diff_filename = self._filesystem.join(str(tempdir), "diff.png")
    486 
    487         # image_diff needs native win paths as arguments, so we need to convert them if running under cygwin.
    488         native_expected_filename = self._convert_path(expected_filename)
    489         native_actual_filename = self._convert_path(actual_filename)
    490         native_diff_filename = self._convert_path(diff_filename)
    491 
    492         executable = self._path_to_image_diff()
    493         # Note that although we are handed 'old', 'new', image_diff wants 'new', 'old'.
    494         comand = [executable, '--diff', native_actual_filename, native_expected_filename, native_diff_filename]
    495 
    496         result = None
    497         err_str = None
    498         try:
    499             exit_code = self._executive.run_command(comand, return_exit_code=True)
    500             if exit_code == 0:
    501                 # The images are the same.
    502                 result = None
    503             elif exit_code == 1:
    504                 result = self._filesystem.read_binary_file(native_diff_filename)
    505             else:
    506                 err_str = "Image diff returned an exit code of %s. See http://crbug.com/278596" % exit_code
    507         except OSError, e:
    508             err_str = 'error running image diff: %s' % str(e)
    509         finally:
    510             self._filesystem.rmtree(str(tempdir))
    511 
    512         return (result, err_str or None)
    513 
    514     def diff_text(self, expected_text, actual_text, expected_filename, actual_filename):
    515         """Returns a string containing the diff of the two text strings
    516         in 'unified diff' format."""
    517 
    518         # The filenames show up in the diff output, make sure they're
    519         # raw bytes and not unicode, so that they don't trigger join()
    520         # trying to decode the input.
    521         def to_raw_bytes(string_value):
    522             if isinstance(string_value, unicode):
    523                 return string_value.encode('utf-8')
    524             return string_value
    525         expected_filename = to_raw_bytes(expected_filename)
    526         actual_filename = to_raw_bytes(actual_filename)
    527         diff = difflib.unified_diff(expected_text.splitlines(True),
    528                                     actual_text.splitlines(True),
    529                                     expected_filename,
    530                                     actual_filename)
    531 
    532         # The diff generated by the difflib is incorrect if one of the files
    533         # does not have a newline at the end of the file and it is present in
    534         # the diff. Relevant Python issue: http://bugs.python.org/issue2142
    535         def diff_fixup(diff):
    536             for line in diff:
    537                 yield line
    538                 if not line.endswith('\n'):
    539                     yield '\n\ No newline at end of file\n'
    540 
    541         return ''.join(diff_fixup(diff))
    542 
    543     def driver_name(self):
    544         if self.get_option('driver_name'):
    545             return self.get_option('driver_name')
    546         return self.CONTENT_SHELL_NAME
    547 
    548     def expected_baselines_by_extension(self, test_name):
    549         """Returns a dict mapping baseline suffix to relative path for each baseline in
    550         a test. For reftests, it returns ".==" or ".!=" instead of the suffix."""
    551         # FIXME: The name similarity between this and expected_baselines() below, is unfortunate.
    552         # We should probably rename them both.
    553         baseline_dict = {}
    554         reference_files = self.reference_files(test_name)
    555         if reference_files:
    556             # FIXME: How should this handle more than one type of reftest?
    557             baseline_dict['.' + reference_files[0][0]] = self.relative_test_filename(reference_files[0][1])
    558 
    559         for extension in self.baseline_extensions():
    560             path = self.expected_filename(test_name, extension, return_default=False)
    561             baseline_dict[extension] = self.relative_test_filename(path) if path else path
    562 
    563         return baseline_dict
    564 
    565     def baseline_extensions(self):
    566         """Returns a tuple of all of the non-reftest baseline extensions we use. The extensions include the leading '.'."""
    567         return ('.wav', '.txt', '.png')
    568 
    569     def expected_baselines(self, test_name, suffix, all_baselines=False):
    570         """Given a test name, finds where the baseline results are located.
    571 
    572         Args:
    573         test_name: name of test file (usually a relative path under LayoutTests/)
    574         suffix: file suffix of the expected results, including dot; e.g.
    575             '.txt' or '.png'.  This should not be None, but may be an empty
    576             string.
    577         all_baselines: If True, return an ordered list of all baseline paths
    578             for the given platform. If False, return only the first one.
    579         Returns
    580         a list of ( platform_dir, results_filename ), where
    581             platform_dir - abs path to the top of the results tree (or test
    582                 tree)
    583             results_filename - relative path from top of tree to the results
    584                 file
    585             (port.join() of the two gives you the full path to the file,
    586                 unless None was returned.)
    587         Return values will be in the format appropriate for the current
    588         platform (e.g., "\\" for path separators on Windows). If the results
    589         file is not found, then None will be returned for the directory,
    590         but the expected relative pathname will still be returned.
    591 
    592         This routine is generic but lives here since it is used in
    593         conjunction with the other baseline and filename routines that are
    594         platform specific.
    595         """
    596         baseline_filename = self._filesystem.splitext(test_name)[0] + '-expected' + suffix
    597         baseline_search_path = self.baseline_search_path()
    598 
    599         baselines = []
    600         for platform_dir in baseline_search_path:
    601             if self._filesystem.exists(self._filesystem.join(platform_dir, baseline_filename)):
    602                 baselines.append((platform_dir, baseline_filename))
    603 
    604             if not all_baselines and baselines:
    605                 return baselines
    606 
    607         # If it wasn't found in a platform directory, return the expected
    608         # result in the test directory, even if no such file actually exists.
    609         platform_dir = self.layout_tests_dir()
    610         if self._filesystem.exists(self._filesystem.join(platform_dir, baseline_filename)):
    611             baselines.append((platform_dir, baseline_filename))
    612 
    613         if baselines:
    614             return baselines
    615 
    616         return [(None, baseline_filename)]
    617 
    618     def expected_filename(self, test_name, suffix, return_default=True):
    619         """Given a test name, returns an absolute path to its expected results.
    620 
    621         If no expected results are found in any of the searched directories,
    622         the directory in which the test itself is located will be returned.
    623         The return value is in the format appropriate for the platform
    624         (e.g., "\\" for path separators on windows).
    625 
    626         Args:
    627         test_name: name of test file (usually a relative path under LayoutTests/)
    628         suffix: file suffix of the expected results, including dot; e.g. '.txt'
    629             or '.png'.  This should not be None, but may be an empty string.
    630         platform: the most-specific directory name to use to build the
    631             search list of directories, e.g., 'win', or
    632             'chromium-cg-mac-leopard' (we follow the WebKit format)
    633         return_default: if True, returns the path to the generic expectation if nothing
    634             else is found; if False, returns None.
    635 
    636         This routine is generic but is implemented here to live alongside
    637         the other baseline and filename manipulation routines.
    638         """
    639         # FIXME: The [0] here is very mysterious, as is the destructured return.
    640         platform_dir, baseline_filename = self.expected_baselines(test_name, suffix)[0]
    641         if platform_dir:
    642             return self._filesystem.join(platform_dir, baseline_filename)
    643 
    644         actual_test_name = self.lookup_virtual_test_base(test_name)
    645         if actual_test_name:
    646             return self.expected_filename(actual_test_name, suffix)
    647 
    648         if return_default:
    649             return self._filesystem.join(self.layout_tests_dir(), baseline_filename)
    650         return None
    651 
    652     def expected_checksum(self, test_name):
    653         """Returns the checksum of the image we expect the test to produce, or None if it is a text-only test."""
    654         png_path = self.expected_filename(test_name, '.png')
    655 
    656         if self._filesystem.exists(png_path):
    657             with self._filesystem.open_binary_file_for_reading(png_path) as filehandle:
    658                 return read_checksum_from_png.read_checksum(filehandle)
    659 
    660         return None
    661 
    662     def expected_image(self, test_name):
    663         """Returns the image we expect the test to produce."""
    664         baseline_path = self.expected_filename(test_name, '.png')
    665         if not self._filesystem.exists(baseline_path):
    666             return None
    667         return self._filesystem.read_binary_file(baseline_path)
    668 
    669     def expected_audio(self, test_name):
    670         baseline_path = self.expected_filename(test_name, '.wav')
    671         if not self._filesystem.exists(baseline_path):
    672             return None
    673         return self._filesystem.read_binary_file(baseline_path)
    674 
    675     def expected_text(self, test_name):
    676         """Returns the text output we expect the test to produce, or None
    677         if we don't expect there to be any text output.
    678         End-of-line characters are normalized to '\n'."""
    679         # FIXME: DRT output is actually utf-8, but since we don't decode the
    680         # output from DRT (instead treating it as a binary string), we read the
    681         # baselines as a binary string, too.
    682         baseline_path = self.expected_filename(test_name, '.txt')
    683         if not self._filesystem.exists(baseline_path):
    684             return None
    685         text = self._filesystem.read_binary_file(baseline_path)
    686         return text.replace("\r\n", "\n")
    687 
    688     def _get_reftest_list(self, test_name):
    689         dirname = self._filesystem.join(self.layout_tests_dir(), self._filesystem.dirname(test_name))
    690         if dirname not in self._reftest_list:
    691             self._reftest_list[dirname] = Port._parse_reftest_list(self._filesystem, dirname)
    692         return self._reftest_list[dirname]
    693 
    694     @staticmethod
    695     def _parse_reftest_list(filesystem, test_dirpath):
    696         reftest_list_path = filesystem.join(test_dirpath, 'reftest.list')
    697         if not filesystem.isfile(reftest_list_path):
    698             return None
    699         reftest_list_file = filesystem.read_text_file(reftest_list_path)
    700 
    701         parsed_list = {}
    702         for line in reftest_list_file.split('\n'):
    703             line = re.sub('#.+$', '', line)
    704             split_line = line.split()
    705             if len(split_line) == 4:
    706                 # FIXME: Probably one of mozilla's extensions in the reftest.list format. Do we need to support this?
    707                 _log.warning("unsupported reftest.list line '%s' in %s" % (line, reftest_list_path))
    708                 continue
    709             if len(split_line) < 3:
    710                 continue
    711             expectation_type, test_file, ref_file = split_line
    712             parsed_list.setdefault(filesystem.join(test_dirpath, test_file), []).append((expectation_type, filesystem.join(test_dirpath, ref_file)))
    713         return parsed_list
    714 
    715     def reference_files(self, test_name):
    716         """Return a list of expectation (== or !=) and filename pairs"""
    717 
    718         reftest_list = self._get_reftest_list(test_name)
    719         if not reftest_list:
    720             reftest_list = []
    721             for expectation, prefix in (('==', ''), ('!=', '-mismatch')):
    722                 for extention in Port._supported_file_extensions:
    723                     path = self.expected_filename(test_name, prefix + extention)
    724                     if self._filesystem.exists(path):
    725                         reftest_list.append((expectation, path))
    726             return reftest_list
    727 
    728         return reftest_list.get(self._filesystem.join(self.layout_tests_dir(), test_name), [])  # pylint: disable=E1103
    729 
    730     def tests(self, paths):
    731         """Return the list of tests found matching paths."""
    732         tests = self._real_tests(paths)
    733 
    734         suites = self.virtual_test_suites()
    735         if paths:
    736             tests.extend(self._virtual_tests_matching_paths(paths, suites))
    737         else:
    738             tests.extend(self._all_virtual_tests(suites))
    739         return tests
    740 
    741     def _real_tests(self, paths):
    742         # When collecting test cases, skip these directories
    743         skipped_directories = set(['.svn', '_svn', 'platform', 'resources', 'support', 'script-tests', 'reference', 'reftest'])
    744         files = find_files.find(self._filesystem, self.layout_tests_dir(), paths, skipped_directories, Port.is_test_file, self.test_key)
    745         return [self.relative_test_filename(f) for f in files]
    746 
    747     # When collecting test cases, we include any file with these extensions.
    748     _supported_file_extensions = set(['.html', '.xml', '.xhtml', '.xht', '.pl',
    749                                       '.htm', '.php', '.svg', '.mht', '.pdf'])
    750 
    751     @staticmethod
    752     # If any changes are made here be sure to update the isUsedInReftest method in old-run-webkit-tests as well.
    753     def is_reference_html_file(filesystem, dirname, filename):
    754         if filename.startswith('ref-') or filename.startswith('notref-'):
    755             return True
    756         filename_wihout_ext, unused = filesystem.splitext(filename)
    757         for suffix in ['-expected', '-expected-mismatch', '-ref', '-notref']:
    758             if filename_wihout_ext.endswith(suffix):
    759                 return True
    760         return False
    761 
    762     @staticmethod
    763     def _has_supported_extension(filesystem, filename):
    764         """Return true if filename is one of the file extensions we want to run a test on."""
    765         extension = filesystem.splitext(filename)[1]
    766         return extension in Port._supported_file_extensions
    767 
    768     @staticmethod
    769     def is_test_file(filesystem, dirname, filename):
    770         return Port._has_supported_extension(filesystem, filename) and not Port.is_reference_html_file(filesystem, dirname, filename)
    771 
    772     ALL_TEST_TYPES = ['audio', 'harness', 'pixel', 'ref', 'text', 'unknown']
    773 
    774     def test_type(self, test_name):
    775         fs = self._filesystem
    776         if fs.exists(self.expected_filename(test_name, '.png')):
    777             return 'pixel'
    778         if fs.exists(self.expected_filename(test_name, '.wav')):
    779             return 'audio'
    780         if self.reference_files(test_name):
    781             return 'ref'
    782         txt = self.expected_text(test_name)
    783         if txt:
    784             if 'layer at (0,0) size 800x600' in txt:
    785                 return 'pixel'
    786             for line in txt.splitlines():
    787                 if line.startswith('FAIL') or line.startswith('TIMEOUT') or line.startswith('PASS'):
    788                     return 'harness'
    789             return 'text'
    790         return 'unknown'
    791 
    792     def test_key(self, test_name):
    793         """Turns a test name into a list with two sublists, the natural key of the
    794         dirname, and the natural key of the basename.
    795 
    796         This can be used when sorting paths so that files in a directory.
    797         directory are kept together rather than being mixed in with files in
    798         subdirectories."""
    799         dirname, basename = self.split_test(test_name)
    800         return (self._natural_sort_key(dirname + self.TEST_PATH_SEPARATOR), self._natural_sort_key(basename))
    801 
    802     def _natural_sort_key(self, string_to_split):
    803         """ Turns a string into a list of string and number chunks, i.e. "z23a" -> ["z", 23, "a"]
    804 
    805         This can be used to implement "natural sort" order. See:
    806         http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html
    807         http://nedbatchelder.com/blog/200712.html#e20071211T054956
    808         """
    809         def tryint(val):
    810             try:
    811                 return int(val)
    812             except ValueError:
    813                 return val
    814 
    815         return [tryint(chunk) for chunk in re.split('(\d+)', string_to_split)]
    816 
    817     def test_dirs(self):
    818         """Returns the list of top-level test directories."""
    819         layout_tests_dir = self.layout_tests_dir()
    820         return filter(lambda x: self._filesystem.isdir(self._filesystem.join(layout_tests_dir, x)),
    821                       self._filesystem.listdir(layout_tests_dir))
    822 
    823     @memoized
    824     def test_isfile(self, test_name):
    825         """Return True if the test name refers to a directory of tests."""
    826         # Used by test_expectations.py to apply rules to whole directories.
    827         if self._filesystem.isfile(self.abspath_for_test(test_name)):
    828             return True
    829         base = self.lookup_virtual_test_base(test_name)
    830         return base and self._filesystem.isfile(self.abspath_for_test(base))
    831 
    832     @memoized
    833     def test_isdir(self, test_name):
    834         """Return True if the test name refers to a directory of tests."""
    835         # Used by test_expectations.py to apply rules to whole directories.
    836         if self._filesystem.isdir(self.abspath_for_test(test_name)):
    837             return True
    838         base = self.lookup_virtual_test_base(test_name)
    839         return base and self._filesystem.isdir(self.abspath_for_test(base))
    840 
    841     @memoized
    842     def test_exists(self, test_name):
    843         """Return True if the test name refers to an existing test or baseline."""
    844         # Used by test_expectations.py to determine if an entry refers to a
    845         # valid test and by printing.py to determine if baselines exist.
    846         return self.test_isfile(test_name) or self.test_isdir(test_name)
    847 
    848     def split_test(self, test_name):
    849         """Splits a test name into the 'directory' part and the 'basename' part."""
    850         index = test_name.rfind(self.TEST_PATH_SEPARATOR)
    851         if index < 1:
    852             return ('', test_name)
    853         return (test_name[0:index], test_name[index:])
    854 
    855     def normalize_test_name(self, test_name):
    856         """Returns a normalized version of the test name or test directory."""
    857         if test_name.endswith('/'):
    858             return test_name
    859         if self.test_isdir(test_name):
    860             return test_name + '/'
    861         return test_name
    862 
    863     def driver_cmd_line(self):
    864         """Prints the DRT command line that will be used."""
    865         driver = self.create_driver(0)
    866         return driver.cmd_line(self.get_option('pixel_tests'), [])
    867 
    868     def update_baseline(self, baseline_path, data):
    869         """Updates the baseline for a test.
    870 
    871         Args:
    872             baseline_path: the actual path to use for baseline, not the path to
    873               the test. This function is used to update either generic or
    874               platform-specific baselines, but we can't infer which here.
    875             data: contents of the baseline.
    876         """
    877         self._filesystem.write_binary_file(baseline_path, data)
    878 
    879     # FIXME: update callers to create a finder and call it instead of these next five routines (which should be protected).
    880     def webkit_base(self):
    881         return self._webkit_finder.webkit_base()
    882 
    883     def path_from_webkit_base(self, *comps):
    884         return self._webkit_finder.path_from_webkit_base(*comps)
    885 
    886     def path_from_chromium_base(self, *comps):
    887         return self._webkit_finder.path_from_chromium_base(*comps)
    888 
    889     def path_to_script(self, script_name):
    890         return self._webkit_finder.path_to_script(script_name)
    891 
    892     def layout_tests_dir(self):
    893         return self._webkit_finder.layout_tests_dir()
    894 
    895     def perf_tests_dir(self):
    896         return self._webkit_finder.perf_tests_dir()
    897 
    898     def skipped_layout_tests(self, test_list):
    899         """Returns tests skipped outside of the TestExpectations files."""
    900         tests = set(self._skipped_tests_for_unsupported_features(test_list))
    901 
    902         # We explicitly skip any tests in LayoutTests/w3c if need be to avoid running any tests
    903         # left over from the old DEPS-pulled repos.
    904         # We also will warn at the end of the test run if these directories still exist.
    905         #
    906         # TODO(dpranke): Remove this check after 1/1/2015 and let people deal with the warnings.
    907         # Remove the check in controllers/manager.py as well.
    908         if self._filesystem.isdir(self._filesystem.join(self.layout_tests_dir(), 'w3c')):
    909             tests.add('w3c')
    910 
    911         return tests
    912 
    913     def _tests_from_skipped_file_contents(self, skipped_file_contents):
    914         tests_to_skip = []
    915         for line in skipped_file_contents.split('\n'):
    916             line = line.strip()
    917             line = line.rstrip('/')  # Best to normalize directory names to not include the trailing slash.
    918             if line.startswith('#') or not len(line):
    919                 continue
    920             tests_to_skip.append(line)
    921         return tests_to_skip
    922 
    923     def _expectations_from_skipped_files(self, skipped_file_paths):
    924         tests_to_skip = []
    925         for search_path in skipped_file_paths:
    926             filename = self._filesystem.join(self._webkit_baseline_path(search_path), "Skipped")
    927             if not self._filesystem.exists(filename):
    928                 _log.debug("Skipped does not exist: %s" % filename)
    929                 continue
    930             _log.debug("Using Skipped file: %s" % filename)
    931             skipped_file_contents = self._filesystem.read_text_file(filename)
    932             tests_to_skip.extend(self._tests_from_skipped_file_contents(skipped_file_contents))
    933         return tests_to_skip
    934 
    935     @memoized
    936     def skipped_perf_tests(self):
    937         return self._expectations_from_skipped_files([self.perf_tests_dir()])
    938 
    939     def skips_perf_test(self, test_name):
    940         for test_or_category in self.skipped_perf_tests():
    941             if test_or_category == test_name:
    942                 return True
    943             category = self._filesystem.join(self.perf_tests_dir(), test_or_category)
    944             if self._filesystem.isdir(category) and test_name.startswith(test_or_category):
    945                 return True
    946         return False
    947 
    948     def is_chromium(self):
    949         return True
    950 
    951     def name(self):
    952         """Returns a name that uniquely identifies this particular type of port
    953         (e.g., "mac-snowleopard" or "linux-x86_x64" and can be passed
    954         to factory.get() to instantiate the port."""
    955         return self._name
    956 
    957     def operating_system(self):
    958         # Subclasses should override this default implementation.
    959         return 'mac'
    960 
    961     def version(self):
    962         """Returns a string indicating the version of a given platform, e.g.
    963         'leopard' or 'xp'.
    964 
    965         This is used to help identify the exact port when parsing test
    966         expectations, determining search paths, and logging information."""
    967         return self._version
    968 
    969     def architecture(self):
    970         return self._architecture
    971 
    972     def get_option(self, name, default_value=None):
    973         return getattr(self._options, name, default_value)
    974 
    975     def set_option_default(self, name, default_value):
    976         return self._options.ensure_value(name, default_value)
    977 
    978     @memoized
    979     def path_to_generic_test_expectations_file(self):
    980         return self._filesystem.join(self.layout_tests_dir(), 'TestExpectations')
    981 
    982     def relative_test_filename(self, filename):
    983         """Returns a test_name a relative unix-style path for a filename under the LayoutTests
    984         directory. Ports may legitimately return abspaths here if no relpath makes sense."""
    985         # Ports that run on windows need to override this method to deal with
    986         # filenames with backslashes in them.
    987         if filename.startswith(self.layout_tests_dir()):
    988             return self.host.filesystem.relpath(filename, self.layout_tests_dir())
    989         else:
    990             return self.host.filesystem.abspath(filename)
    991 
    992     @memoized
    993     def abspath_for_test(self, test_name):
    994         """Returns the full path to the file for a given test name. This is the
    995         inverse of relative_test_filename()."""
    996         return self._filesystem.join(self.layout_tests_dir(), test_name)
    997 
    998     def results_directory(self):
    999         """Absolute path to the place to store the test results (uses --results-directory)."""
   1000         if not self._results_directory:
   1001             option_val = self.get_option('results_directory') or self.default_results_directory()
   1002             self._results_directory = self._filesystem.abspath(option_val)
   1003         return self._results_directory
   1004 
   1005     def perf_results_directory(self):
   1006         return self._build_path()
   1007 
   1008     def default_results_directory(self):
   1009         """Absolute path to the default place to store the test results."""
   1010         try:
   1011             return self.path_from_chromium_base('webkit', self.get_option('configuration'), 'layout-test-results')
   1012         except AssertionError:
   1013             return self._build_path('layout-test-results')
   1014 
   1015     def setup_test_run(self):
   1016         """Perform port-specific work at the beginning of a test run."""
   1017         # Delete the disk cache if any to ensure a clean test run.
   1018         dump_render_tree_binary_path = self._path_to_driver()
   1019         cachedir = self._filesystem.dirname(dump_render_tree_binary_path)
   1020         cachedir = self._filesystem.join(cachedir, "cache")
   1021         if self._filesystem.exists(cachedir):
   1022             self._filesystem.rmtree(cachedir)
   1023 
   1024         if self._dump_reader:
   1025             self._filesystem.maybe_make_directory(self._dump_reader.crash_dumps_directory())
   1026 
   1027     def num_workers(self, requested_num_workers):
   1028         """Returns the number of available workers (possibly less than the number requested)."""
   1029         return requested_num_workers
   1030 
   1031     def clean_up_test_run(self):
   1032         """Perform port-specific work at the end of a test run."""
   1033         if self._image_differ:
   1034             self._image_differ.stop()
   1035             self._image_differ = None
   1036 
   1037     # FIXME: os.environ access should be moved to onto a common/system class to be more easily mockable.
   1038     def _value_or_default_from_environ(self, name, default=None):
   1039         if name in os.environ:
   1040             return os.environ[name]
   1041         return default
   1042 
   1043     def _copy_value_from_environ_if_set(self, clean_env, name):
   1044         if name in os.environ:
   1045             clean_env[name] = os.environ[name]
   1046 
   1047     def setup_environ_for_server(self, server_name=None):
   1048         # We intentionally copy only a subset of os.environ when
   1049         # launching subprocesses to ensure consistent test results.
   1050         clean_env = {
   1051             'LOCAL_RESOURCE_ROOT': self.layout_tests_dir(),  # FIXME: Is this used?
   1052         }
   1053         variables_to_copy = [
   1054             'WEBKIT_TESTFONTS',  # FIXME: Is this still used?
   1055             'WEBKITOUTPUTDIR',   # FIXME: Is this still used?
   1056             'CHROME_DEVEL_SANDBOX',
   1057             'CHROME_IPC_LOGGING',
   1058             'ASAN_OPTIONS',
   1059             'VALGRIND_LIB',
   1060             'VALGRIND_LIB_INNER',
   1061         ]
   1062         if self.host.platform.is_linux() or self.host.platform.is_freebsd():
   1063             variables_to_copy += [
   1064                 'XAUTHORITY',
   1065                 'HOME',
   1066                 'LANG',
   1067                 'LD_LIBRARY_PATH',
   1068                 'DBUS_SESSION_BUS_ADDRESS',
   1069                 'XDG_DATA_DIRS',
   1070             ]
   1071             clean_env['DISPLAY'] = self._value_or_default_from_environ('DISPLAY', ':1')
   1072         if self.host.platform.is_mac():
   1073             clean_env['DYLD_LIBRARY_PATH'] = self._build_path()
   1074             clean_env['DYLD_FRAMEWORK_PATH'] = self._build_path()
   1075             variables_to_copy += [
   1076                 'HOME',
   1077             ]
   1078         if self.host.platform.is_win():
   1079             variables_to_copy += [
   1080                 'PATH',
   1081                 'GYP_DEFINES',  # Required to locate win sdk.
   1082             ]
   1083         if self.host.platform.is_cygwin():
   1084             variables_to_copy += [
   1085                 'HOMEDRIVE',
   1086                 'HOMEPATH',
   1087                 '_NT_SYMBOL_PATH',
   1088             ]
   1089 
   1090         for variable in variables_to_copy:
   1091             self._copy_value_from_environ_if_set(clean_env, variable)
   1092 
   1093         for string_variable in self.get_option('additional_env_var', []):
   1094             [name, value] = string_variable.split('=', 1)
   1095             clean_env[name] = value
   1096 
   1097         return clean_env
   1098 
   1099     def show_results_html_file(self, results_filename):
   1100         """This routine should display the HTML file pointed at by
   1101         results_filename in a users' browser."""
   1102         return self.host.user.open_url(path.abspath_to_uri(self.host.platform, results_filename))
   1103 
   1104     def create_driver(self, worker_number, no_timeout=False):
   1105         """Return a newly created Driver subclass for starting/stopping the test driver."""
   1106         return self._driver_class()(self, worker_number, pixel_tests=self.get_option('pixel_tests'), no_timeout=no_timeout)
   1107 
   1108     def start_helper(self):
   1109         """If a port needs to reconfigure graphics settings or do other
   1110         things to ensure a known test configuration, it should override this
   1111         method."""
   1112         helper_path = self._path_to_helper()
   1113         if helper_path:
   1114             _log.debug("Starting layout helper %s" % helper_path)
   1115             # Note: Not thread safe: http://bugs.python.org/issue2320
   1116             self._helper = self._executive.popen([helper_path],
   1117                 stdin=self._executive.PIPE, stdout=self._executive.PIPE, stderr=None)
   1118             is_ready = self._helper.stdout.readline()
   1119             if not is_ready.startswith('ready'):
   1120                 _log.error("layout_test_helper failed to be ready")
   1121 
   1122     def requires_http_server(self):
   1123         """Does the port require an HTTP server for running tests? This could
   1124         be the case when the tests aren't run on the host platform."""
   1125         return False
   1126 
   1127     def start_http_server(self, additional_dirs, number_of_drivers):
   1128         """Start a web server. Raise an error if it can't start or is already running.
   1129 
   1130         Ports can stub this out if they don't need a web server to be running."""
   1131         assert not self._http_server, 'Already running an http server.'
   1132 
   1133         server = apache_http.ApacheHTTP(self, self.results_directory(),
   1134                                         additional_dirs=additional_dirs,
   1135                                         number_of_servers=(number_of_drivers * 4))
   1136         server.start()
   1137         self._http_server = server
   1138 
   1139     def start_websocket_server(self):
   1140         """Start a web server. Raise an error if it can't start or is already running.
   1141 
   1142         Ports can stub this out if they don't need a websocket server to be running."""
   1143         assert not self._websocket_server, 'Already running a websocket server.'
   1144 
   1145         server = pywebsocket.PyWebSocket(self, self.results_directory())
   1146         server.start()
   1147         self._websocket_server = server
   1148 
   1149     def http_server_supports_ipv6(self):
   1150         # Apache < 2.4 on win32 does not support IPv6, nor does cygwin apache.
   1151         if self.host.platform.is_cygwin() or self.host.platform.is_win():
   1152             return False
   1153         return True
   1154 
   1155     def stop_helper(self):
   1156         """Shut down the test helper if it is running. Do nothing if
   1157         it isn't, or it isn't available. If a port overrides start_helper()
   1158         it must override this routine as well."""
   1159         if self._helper:
   1160             _log.debug("Stopping layout test helper")
   1161             try:
   1162                 self._helper.stdin.write("x\n")
   1163                 self._helper.stdin.close()
   1164                 self._helper.wait()
   1165             except IOError, e:
   1166                 pass
   1167             finally:
   1168                 self._helper = None
   1169 
   1170     def stop_http_server(self):
   1171         """Shut down the http server if it is running. Do nothing if it isn't."""
   1172         if self._http_server:
   1173             self._http_server.stop()
   1174             self._http_server = None
   1175 
   1176     def stop_websocket_server(self):
   1177         """Shut down the websocket server if it is running. Do nothing if it isn't."""
   1178         if self._websocket_server:
   1179             self._websocket_server.stop()
   1180             self._websocket_server = None
   1181 
   1182     #
   1183     # TEST EXPECTATION-RELATED METHODS
   1184     #
   1185 
   1186     def test_configuration(self):
   1187         """Returns the current TestConfiguration for the port."""
   1188         if not self._test_configuration:
   1189             self._test_configuration = TestConfiguration(self._version, self._architecture, self._options.configuration.lower())
   1190         return self._test_configuration
   1191 
   1192     # FIXME: Belongs on a Platform object.
   1193     @memoized
   1194     def all_test_configurations(self):
   1195         """Returns a list of TestConfiguration instances, representing all available
   1196         test configurations for this port."""
   1197         return self._generate_all_test_configurations()
   1198 
   1199     # FIXME: Belongs on a Platform object.
   1200     def configuration_specifier_macros(self):
   1201         """Ports may provide a way to abbreviate configuration specifiers to conveniently
   1202         refer to them as one term or alias specific values to more generic ones. For example:
   1203 
   1204         (xp, vista, win7) -> win # Abbreviate all Windows versions into one namesake.
   1205         (lucid) -> linux  # Change specific name of the Linux distro to a more generic term.
   1206 
   1207         Returns a dictionary, each key representing a macro term ('win', for example),
   1208         and value being a list of valid configuration specifiers (such as ['xp', 'vista', 'win7'])."""
   1209         return self.CONFIGURATION_SPECIFIER_MACROS
   1210 
   1211     def all_baseline_variants(self):
   1212         """Returns a list of platform names sufficient to cover all the baselines.
   1213 
   1214         The list should be sorted so that a later platform  will reuse
   1215         an earlier platform's baselines if they are the same (e.g.,
   1216         'snowleopard' should precede 'leopard')."""
   1217         return self.ALL_BASELINE_VARIANTS
   1218 
   1219     def _generate_all_test_configurations(self):
   1220         """Returns a sequence of the TestConfigurations the port supports."""
   1221         # By default, we assume we want to test every graphics type in
   1222         # every configuration on every system.
   1223         test_configurations = []
   1224         for version, architecture in self.ALL_SYSTEMS:
   1225             for build_type in self.ALL_BUILD_TYPES:
   1226                 test_configurations.append(TestConfiguration(version, architecture, build_type))
   1227         return test_configurations
   1228 
   1229     try_builder_names = frozenset([
   1230         'linux_layout',
   1231         'mac_layout',
   1232         'win_layout',
   1233         'linux_layout_rel',
   1234         'mac_layout_rel',
   1235         'win_layout_rel',
   1236     ])
   1237 
   1238     def warn_if_bug_missing_in_test_expectations(self):
   1239         return True
   1240 
   1241     def _port_specific_expectations_files(self):
   1242         paths = []
   1243         paths.append(self.path_from_chromium_base('skia', 'skia_test_expectations.txt'))
   1244         paths.append(self._filesystem.join(self.layout_tests_dir(), 'NeverFixTests'))
   1245         paths.append(self._filesystem.join(self.layout_tests_dir(), 'StaleTestExpectations'))
   1246         paths.append(self._filesystem.join(self.layout_tests_dir(), 'SlowTests'))
   1247         paths.append(self._filesystem.join(self.layout_tests_dir(), 'FlakyTests'))
   1248 
   1249         return paths
   1250 
   1251     def expectations_dict(self):
   1252         """Returns an OrderedDict of name -> expectations strings.
   1253         The names are expected to be (but not required to be) paths in the filesystem.
   1254         If the name is a path, the file can be considered updatable for things like rebaselining,
   1255         so don't use names that are paths if they're not paths.
   1256         Generally speaking the ordering should be files in the filesystem in cascade order
   1257         (TestExpectations followed by Skipped, if the port honors both formats),
   1258         then any built-in expectations (e.g., from compile-time exclusions), then --additional-expectations options."""
   1259         # FIXME: rename this to test_expectations() once all the callers are updated to know about the ordered dict.
   1260         expectations = OrderedDict()
   1261 
   1262         for path in self.expectations_files():
   1263             if self._filesystem.exists(path):
   1264                 expectations[path] = self._filesystem.read_text_file(path)
   1265 
   1266         for path in self.get_option('additional_expectations', []):
   1267             expanded_path = self._filesystem.expanduser(path)
   1268             if self._filesystem.exists(expanded_path):
   1269                 _log.debug("reading additional_expectations from path '%s'" % path)
   1270                 expectations[path] = self._filesystem.read_text_file(expanded_path)
   1271             else:
   1272                 _log.warning("additional_expectations path '%s' does not exist" % path)
   1273         return expectations
   1274 
   1275     def bot_expectations(self):
   1276         if not self.get_option('ignore_flaky_tests'):
   1277             return {}
   1278 
   1279         full_port_name = self.determine_full_port_name(self.host, self._options, self.port_name)
   1280         builder_category = self.get_option('ignore_builder_category', 'layout')
   1281         factory = BotTestExpectationsFactory()
   1282         # FIXME: This only grabs release builder's flakiness data. If we're running debug,
   1283         # when we should grab the debug builder's data.
   1284         expectations = factory.expectations_for_port(full_port_name, builder_category)
   1285 
   1286         if not expectations:
   1287             return {}
   1288 
   1289         ignore_mode = self.get_option('ignore_flaky_tests')
   1290         if ignore_mode == 'very-flaky' or ignore_mode == 'maybe-flaky':
   1291             return expectations.flakes_by_path(ignore_mode == 'very-flaky')
   1292         if ignore_mode == 'unexpected':
   1293             return expectations.unexpected_results_by_path()
   1294         _log.warning("Unexpected ignore mode: '%s'." % ignore_mode)
   1295         return {}
   1296 
   1297     def expectations_files(self):
   1298         return [self.path_to_generic_test_expectations_file()] + self._port_specific_expectations_files()
   1299 
   1300     def repository_paths(self):
   1301         """Returns a list of (repository_name, repository_path) tuples of its depending code base."""
   1302         return [('blink', self.layout_tests_dir()),
   1303                 ('chromium', self.path_from_chromium_base('build'))]
   1304 
   1305     _WDIFF_DEL = '##WDIFF_DEL##'
   1306     _WDIFF_ADD = '##WDIFF_ADD##'
   1307     _WDIFF_END = '##WDIFF_END##'
   1308 
   1309     def _format_wdiff_output_as_html(self, wdiff):
   1310         wdiff = cgi.escape(wdiff)
   1311         wdiff = wdiff.replace(self._WDIFF_DEL, "<span class=del>")
   1312         wdiff = wdiff.replace(self._WDIFF_ADD, "<span class=add>")
   1313         wdiff = wdiff.replace(self._WDIFF_END, "</span>")
   1314         html = "<head><style>.del { background: #faa; } "
   1315         html += ".add { background: #afa; }</style></head>"
   1316         html += "<pre>%s</pre>" % wdiff
   1317         return html
   1318 
   1319     def _wdiff_command(self, actual_filename, expected_filename):
   1320         executable = self._path_to_wdiff()
   1321         return [executable,
   1322                 "--start-delete=%s" % self._WDIFF_DEL,
   1323                 "--end-delete=%s" % self._WDIFF_END,
   1324                 "--start-insert=%s" % self._WDIFF_ADD,
   1325                 "--end-insert=%s" % self._WDIFF_END,
   1326                 actual_filename,
   1327                 expected_filename]
   1328 
   1329     @staticmethod
   1330     def _handle_wdiff_error(script_error):
   1331         # Exit 1 means the files differed, any other exit code is an error.
   1332         if script_error.exit_code != 1:
   1333             raise script_error
   1334 
   1335     def _run_wdiff(self, actual_filename, expected_filename):
   1336         """Runs wdiff and may throw exceptions.
   1337         This is mostly a hook for unit testing."""
   1338         # Diffs are treated as binary as they may include multiple files
   1339         # with conflicting encodings.  Thus we do not decode the output.
   1340         command = self._wdiff_command(actual_filename, expected_filename)
   1341         wdiff = self._executive.run_command(command, decode_output=False,
   1342             error_handler=self._handle_wdiff_error)
   1343         return self._format_wdiff_output_as_html(wdiff)
   1344 
   1345     _wdiff_error_html = "Failed to run wdiff, see error log."
   1346 
   1347     def wdiff_text(self, actual_filename, expected_filename):
   1348         """Returns a string of HTML indicating the word-level diff of the
   1349         contents of the two filenames. Returns an empty string if word-level
   1350         diffing isn't available."""
   1351         if not self.wdiff_available():
   1352             return ""
   1353         try:
   1354             # It's possible to raise a ScriptError we pass wdiff invalid paths.
   1355             return self._run_wdiff(actual_filename, expected_filename)
   1356         except OSError as e:
   1357             if e.errno in [errno.ENOENT, errno.EACCES, errno.ECHILD]:
   1358                 # Silently ignore cases where wdiff is missing.
   1359                 self._wdiff_available = False
   1360                 return ""
   1361             raise
   1362         except ScriptError as e:
   1363             _log.error("Failed to run wdiff: %s" % e)
   1364             self._wdiff_available = False
   1365             return self._wdiff_error_html
   1366 
   1367     # This is a class variable so we can test error output easily.
   1368     _pretty_patch_error_html = "Failed to run PrettyPatch, see error log."
   1369 
   1370     def pretty_patch_text(self, diff_path):
   1371         if self._pretty_patch_available is None:
   1372             self._pretty_patch_available = self.check_pretty_patch(logging=False)
   1373         if not self._pretty_patch_available:
   1374             return self._pretty_patch_error_html
   1375         command = ("ruby", "-I", self._filesystem.dirname(self._pretty_patch_path),
   1376                    self._pretty_patch_path, diff_path)
   1377         try:
   1378             # Diffs are treated as binary (we pass decode_output=False) as they
   1379             # may contain multiple files of conflicting encodings.
   1380             return self._executive.run_command(command, decode_output=False)
   1381         except OSError, e:
   1382             # If the system is missing ruby log the error and stop trying.
   1383             self._pretty_patch_available = False
   1384             _log.error("Failed to run PrettyPatch (%s): %s" % (command, e))
   1385             return self._pretty_patch_error_html
   1386         except ScriptError, e:
   1387             # If ruby failed to run for some reason, log the command
   1388             # output and stop trying.
   1389             self._pretty_patch_available = False
   1390             _log.error("Failed to run PrettyPatch (%s):\n%s" % (command, e.message_with_output()))
   1391             return self._pretty_patch_error_html
   1392 
   1393     def default_configuration(self):
   1394         return self._config.default_configuration()
   1395 
   1396     def clobber_old_port_specific_results(self):
   1397         pass
   1398 
   1399     # FIXME: This does not belong on the port object.
   1400     @memoized
   1401     def path_to_apache(self):
   1402         """Returns the full path to the apache binary.
   1403 
   1404         This is needed only by ports that use the apache_http_server module."""
   1405         raise NotImplementedError('Port.path_to_apache')
   1406 
   1407     def path_to_apache_config_file(self):
   1408         """Returns the full path to the apache configuration file.
   1409 
   1410         If the WEBKIT_HTTP_SERVER_CONF_PATH environment variable is set, its
   1411         contents will be used instead.
   1412 
   1413         This is needed only by ports that use the apache_http_server module."""
   1414         config_file_from_env = os.environ.get('WEBKIT_HTTP_SERVER_CONF_PATH')
   1415         if config_file_from_env:
   1416             if not self._filesystem.exists(config_file_from_env):
   1417                 raise IOError('%s was not found on the system' % config_file_from_env)
   1418             return config_file_from_env
   1419 
   1420         config_file_name = self._apache_config_file_name_for_platform(sys.platform)
   1421         return self._filesystem.join(self.layout_tests_dir(), 'http', 'conf', config_file_name)
   1422 
   1423     #
   1424     # PROTECTED ROUTINES
   1425     #
   1426     # The routines below should only be called by routines in this class
   1427     # or any of its subclasses.
   1428     #
   1429 
   1430     # FIXME: This belongs on some platform abstraction instead of Port.
   1431     def _is_redhat_based(self):
   1432         return self._filesystem.exists('/etc/redhat-release')
   1433 
   1434     def _is_debian_based(self):
   1435         return self._filesystem.exists('/etc/debian_version')
   1436 
   1437     def _apache_version(self):
   1438         config = self._executive.run_command([self.path_to_apache(), '-v'])
   1439         return re.sub(r'(?:.|\n)*Server version: Apache/(\d+\.\d+)(?:.|\n)*', r'\1', config)
   1440 
   1441     # We pass sys_platform into this method to make it easy to unit test.
   1442     def _apache_config_file_name_for_platform(self, sys_platform):
   1443         if sys_platform == 'cygwin':
   1444             return 'cygwin-httpd.conf'  # CYGWIN is the only platform to still use Apache 1.3.
   1445         if sys_platform.startswith('linux'):
   1446             if self._is_redhat_based():
   1447                 return 'fedora-httpd-' + self._apache_version() + '.conf'
   1448             if self._is_debian_based():
   1449                 return 'debian-httpd-' + self._apache_version() + '.conf'
   1450         # All platforms use apache2 except for CYGWIN (and Mac OS X Tiger and prior, which we no longer support).
   1451         return "apache2-httpd.conf"
   1452 
   1453     def _path_to_driver(self, configuration=None):
   1454         """Returns the full path to the test driver."""
   1455         return self._build_path(self.driver_name())
   1456 
   1457     def _path_to_webcore_library(self):
   1458         """Returns the full path to a built copy of WebCore."""
   1459         return None
   1460 
   1461     def _path_to_helper(self):
   1462         """Returns the full path to the layout_test_helper binary, which
   1463         is used to help configure the system for the test run, or None
   1464         if no helper is needed.
   1465 
   1466         This is likely only used by start/stop_helper()."""
   1467         return None
   1468 
   1469     def _path_to_image_diff(self):
   1470         """Returns the full path to the image_diff binary, or None if it is not available.
   1471 
   1472         This is likely used only by diff_image()"""
   1473         return self._build_path('image_diff')
   1474 
   1475     @memoized
   1476     def _path_to_wdiff(self):
   1477         """Returns the full path to the wdiff binary, or None if it is not available.
   1478 
   1479         This is likely used only by wdiff_text()"""
   1480         for path in ("/usr/bin/wdiff", "/usr/bin/dwdiff"):
   1481             if self._filesystem.exists(path):
   1482                 return path
   1483         return None
   1484 
   1485     def _webkit_baseline_path(self, platform):
   1486         """Return the  full path to the top of the baseline tree for a
   1487         given platform."""
   1488         return self._filesystem.join(self.layout_tests_dir(), 'platform', platform)
   1489 
   1490     def _driver_class(self):
   1491         """Returns the port's driver implementation."""
   1492         return driver.Driver
   1493 
   1494     def _output_contains_sanitizer_messages(self, output):
   1495         if not output:
   1496             return None
   1497         if 'AddressSanitizer' in output:
   1498             return 'AddressSanitizer'
   1499         if 'MemorySanitizer' in output:
   1500             return 'MemorySanitizer'
   1501         return None
   1502 
   1503     def _get_crash_log(self, name, pid, stdout, stderr, newer_than):
   1504         if self._output_contains_sanitizer_messages(stderr):
   1505             # Running the symbolizer script can take a lot of memory, so we need to
   1506             # serialize access to it across all the concurrently running drivers.
   1507 
   1508             llvm_symbolizer_path = self.path_from_chromium_base('third_party', 'llvm-build', 'Release+Asserts', 'bin', 'llvm-symbolizer')
   1509             if self._filesystem.exists(llvm_symbolizer_path):
   1510                 env = os.environ.copy()
   1511                 env['LLVM_SYMBOLIZER_PATH'] = llvm_symbolizer_path
   1512             else:
   1513                 env = None
   1514             sanitizer_filter_path = self.path_from_chromium_base('tools', 'valgrind', 'asan', 'asan_symbolize.py')
   1515             sanitizer_strip_path_prefix = 'Release/../../'
   1516             if self._filesystem.exists(sanitizer_filter_path):
   1517                 stderr = self._executive.run_command(['flock', sys.executable, sanitizer_filter_path, sanitizer_strip_path_prefix], input=stderr, decode_output=False, env=env)
   1518 
   1519         name_str = name or '<unknown process name>'
   1520         pid_str = str(pid or '<unknown>')
   1521         stdout_lines = (stdout or '<empty>').decode('utf8', 'replace').splitlines()
   1522         stderr_lines = (stderr or '<empty>').decode('utf8', 'replace').splitlines()
   1523         return (stderr, 'crash log for %s (pid %s):\n%s\n%s\n' % (name_str, pid_str,
   1524             '\n'.join(('STDOUT: ' + l) for l in stdout_lines),
   1525             '\n'.join(('STDERR: ' + l) for l in stderr_lines)))
   1526 
   1527     def look_for_new_crash_logs(self, crashed_processes, start_time):
   1528         pass
   1529 
   1530     def look_for_new_samples(self, unresponsive_processes, start_time):
   1531         pass
   1532 
   1533     def sample_process(self, name, pid):
   1534         pass
   1535 
   1536     def physical_test_suites(self):
   1537         return [
   1538             # For example, to turn on force-compositing-mode in the svg/ directory:
   1539             # PhysicalTestSuite('svg',
   1540             #                   ['--force-compositing-mode']),
   1541             ]
   1542 
   1543     def virtual_test_suites(self):
   1544         if self._virtual_test_suites is None:
   1545             path_to_virtual_test_suites = self._filesystem.join(self.layout_tests_dir(), 'VirtualTestSuites')
   1546             assert self._filesystem.exists(path_to_virtual_test_suites), 'LayoutTests/VirtualTestSuites not found'
   1547             try:
   1548                 test_suite_json = json.loads(self._filesystem.read_text_file(path_to_virtual_test_suites))
   1549                 self._virtual_test_suites = [VirtualTestSuite(**d) for d in test_suite_json]
   1550             except ValueError as e:
   1551                 raise ValueError("LayoutTests/VirtualTestSuites is not a valid JSON file: %s" % str(e))
   1552         return self._virtual_test_suites
   1553 
   1554     def _all_virtual_tests(self, suites):
   1555         tests = []
   1556         for suite in suites:
   1557             self._populate_virtual_suite(suite)
   1558             tests.extend(suite.tests.keys())
   1559         return tests
   1560 
   1561     def _virtual_tests_matching_paths(self, paths, suites):
   1562         tests = []
   1563         for suite in suites:
   1564             if any(p.startswith(suite.name) for p in paths):
   1565                 self._populate_virtual_suite(suite)
   1566             for test in suite.tests:
   1567                 if any(test.startswith(p) for p in paths):
   1568                     tests.append(test)
   1569         return tests
   1570 
   1571     def _populate_virtual_suite(self, suite):
   1572         if not suite.tests:
   1573             base_tests = self._real_tests([suite.base])
   1574             suite.tests = {}
   1575             for test in base_tests:
   1576                 suite.tests[test.replace(suite.base, suite.name, 1)] = test
   1577 
   1578     def is_virtual_test(self, test_name):
   1579         return bool(self.lookup_virtual_suite(test_name))
   1580 
   1581     def lookup_virtual_suite(self, test_name):
   1582         for suite in self.virtual_test_suites():
   1583             if test_name.startswith(suite.name):
   1584                 return suite
   1585         return None
   1586 
   1587     def lookup_virtual_test_base(self, test_name):
   1588         suite = self.lookup_virtual_suite(test_name)
   1589         if not suite:
   1590             return None
   1591         return test_name.replace(suite.name, suite.base, 1)
   1592 
   1593     def lookup_virtual_test_args(self, test_name):
   1594         for suite in self.virtual_test_suites():
   1595             if test_name.startswith(suite.name):
   1596                 return suite.args
   1597         return []
   1598 
   1599     def lookup_physical_test_args(self, test_name):
   1600         for suite in self.physical_test_suites():
   1601             if test_name.startswith(suite.name):
   1602                 return suite.args
   1603         return []
   1604 
   1605     def should_run_as_pixel_test(self, test_input):
   1606         if not self._options.pixel_tests:
   1607             return False
   1608         if self._options.pixel_test_directories:
   1609             return any(test_input.test_name.startswith(directory) for directory in self._options.pixel_test_directories)
   1610         return True
   1611 
   1612     def _modules_to_search_for_symbols(self):
   1613         path = self._path_to_webcore_library()
   1614         if path:
   1615             return [path]
   1616         return []
   1617 
   1618     def _symbols_string(self):
   1619         symbols = ''
   1620         for path_to_module in self._modules_to_search_for_symbols():
   1621             try:
   1622                 symbols += self._executive.run_command(['nm', path_to_module], error_handler=self._executive.ignore_error)
   1623             except OSError, e:
   1624                 _log.warn("Failed to run nm: %s.  Can't determine supported features correctly." % e)
   1625         return symbols
   1626 
   1627     # Ports which use compile-time feature detection should define this method and return
   1628     # a dictionary mapping from symbol substrings to possibly disabled test directories.
   1629     # When the symbol substrings are not matched, the directories will be skipped.
   1630     # If ports don't ever enable certain features, then those directories can just be
   1631     # in the Skipped list instead of compile-time-checked here.
   1632     def _missing_symbol_to_skipped_tests(self):
   1633         if self.PORT_HAS_AUDIO_CODECS_BUILT_IN:
   1634             return {}
   1635         else:
   1636             return {
   1637                 "ff_mp3_decoder": ["webaudio/codec-tests/mp3"],
   1638                 "ff_aac_decoder": ["webaudio/codec-tests/aac"],
   1639             }
   1640 
   1641     def _has_test_in_directories(self, directory_lists, test_list):
   1642         if not test_list:
   1643             return False
   1644 
   1645         directories = itertools.chain.from_iterable(directory_lists)
   1646         for directory, test in itertools.product(directories, test_list):
   1647             if test.startswith(directory):
   1648                 return True
   1649         return False
   1650 
   1651     def _skipped_tests_for_unsupported_features(self, test_list):
   1652         # Only check the symbols of there are tests in the test_list that might get skipped.
   1653         # This is a performance optimization to avoid the calling nm.
   1654         # Runtime feature detection not supported, fallback to static detection:
   1655         # Disable any tests for symbols missing from the executable or libraries.
   1656         if self._has_test_in_directories(self._missing_symbol_to_skipped_tests().values(), test_list):
   1657             symbols_string = self._symbols_string()
   1658             if symbols_string is not None:
   1659                 return reduce(operator.add, [directories for symbol_substring, directories in self._missing_symbol_to_skipped_tests().items() if symbol_substring not in symbols_string], [])
   1660         return []
   1661 
   1662     def _convert_path(self, path):
   1663         """Handles filename conversion for subprocess command line args."""
   1664         # See note above in diff_image() for why we need this.
   1665         if sys.platform == 'cygwin':
   1666             return cygpath(path)
   1667         return path
   1668 
   1669     def _build_path(self, *comps):
   1670         return self._build_path_with_configuration(None, *comps)
   1671 
   1672     def _build_path_with_configuration(self, configuration, *comps):
   1673         # Note that we don't do the option caching that the
   1674         # base class does, because finding the right directory is relatively
   1675         # fast.
   1676         configuration = configuration or self.get_option('configuration')
   1677         return self._static_build_path(self._filesystem, self.get_option('build_directory'),
   1678             self.path_from_chromium_base(), configuration, comps)
   1679 
   1680     def _check_driver_build_up_to_date(self, configuration):
   1681         if configuration in ('Debug', 'Release'):
   1682             try:
   1683                 debug_path = self._path_to_driver('Debug')
   1684                 release_path = self._path_to_driver('Release')
   1685 
   1686                 debug_mtime = self._filesystem.mtime(debug_path)
   1687                 release_mtime = self._filesystem.mtime(release_path)
   1688 
   1689                 if (debug_mtime > release_mtime and configuration == 'Release' or
   1690                     release_mtime > debug_mtime and configuration == 'Debug'):
   1691                     most_recent_binary = 'Release' if configuration == 'Debug' else 'Debug'
   1692                     _log.warning('You are running the %s binary. However the %s binary appears to be more recent. '
   1693                                  'Please pass --%s.', configuration, most_recent_binary, most_recent_binary.lower())
   1694                     _log.warning('')
   1695             # This will fail if we don't have both a debug and release binary.
   1696             # That's fine because, in this case, we must already be running the
   1697             # most up-to-date one.
   1698             except OSError:
   1699                 pass
   1700         return True
   1701 
   1702     def _chromium_baseline_path(self, platform):
   1703         if platform is None:
   1704             platform = self.name()
   1705         return self.path_from_webkit_base('LayoutTests', 'platform', platform)
   1706 
   1707 class VirtualTestSuite(object):
   1708     def __init__(self, prefix=None, base=None, args=None):
   1709         assert base
   1710         assert args
   1711         assert prefix.find('/') == -1, "Virtual test suites prefixes cannot contain /'s: %s" % prefix
   1712         self.name = 'virtual/' + prefix + '/' + base
   1713         self.base = base
   1714         self.args = args
   1715         self.tests = {}
   1716 
   1717     def __repr__(self):
   1718         return "VirtualTestSuite('%s', '%s', %s)" % (self.name, self.base, self.args)
   1719 
   1720 
   1721 class PhysicalTestSuite(object):
   1722     def __init__(self, base, args):
   1723         self.name = base
   1724         self.base = base
   1725         self.args = args
   1726         self.tests = set()
   1727 
   1728     def __repr__(self):
   1729         return "PhysicalTestSuite('%s', '%s', %s)" % (self.name, self.base, self.args)
   1730