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