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
30test infrastructure (the Port and Driver classes)."""
31
32import cgi
33import difflib
34import errno
35import itertools
36import json
37import logging
38import os
39import operator
40import optparse
41import re
42import sys
43
44try:
45    from collections import OrderedDict
46except ImportError:
47    # Needed for Python < 2.7
48    from webkitpy.thirdparty.ordered_dict import OrderedDict
49
50
51from webkitpy.common import find_files
52from webkitpy.common import read_checksum_from_png
53from webkitpy.common.memoized import memoized
54from webkitpy.common.system import path
55from webkitpy.common.system.executive import ScriptError
56from webkitpy.common.system.path import cygpath
57from webkitpy.common.system.systemhost import SystemHost
58from webkitpy.common.webkit_finder import WebKitFinder
59from webkitpy.layout_tests.layout_package.bot_test_expectations import BotTestExpectationsFactory
60from webkitpy.layout_tests.models import test_run_results
61from webkitpy.layout_tests.models.test_configuration import TestConfiguration
62from webkitpy.layout_tests.port import config as port_config
63from webkitpy.layout_tests.port import driver
64from webkitpy.layout_tests.port import server_process
65from webkitpy.layout_tests.port.factory import PortFactory
66from webkitpy.layout_tests.servers import apache_http
67from 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.
73class 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
1707class 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
1721class 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