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