2010-02-03 Dirk Pranke <dpranke@chromium.org>
authordpranke@chromium.org <dpranke@chromium.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 6 Feb 2010 00:01:04 +0000 (00:01 +0000)
committerdpranke@chromium.org <dpranke@chromium.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 6 Feb 2010 00:01:04 +0000 (00:01 +0000)
        Reviewed by Eric Siedel.

        Refactor the port package into an object-oriented style and merge
        path_utils into it. We add a 'base' and a 'chromium' object to the
        port package; this will allow us to easily add new ports (like
        WebKit Mac).

        https://bugs.webkit.org/show_bug.cgi?id=34511

        * Scripts/rebaseline-chromium-webkit-tests:
        * Scripts/run-chromium-webkit-tests:
        * Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py:
        * Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py:
        * Scripts/webkitpy/layout_tests/layout_package/test_expectations.py:
        * Scripts/webkitpy/layout_tests/layout_package/test_files.py:
        * Scripts/webkitpy/layout_tests/layout_package/test_shell_thread.py:
        * Scripts/webkitpy/layout_tests/port/__init__.py:
        * Scripts/webkitpy/layout_tests/port/apache_http_server.py:
        * Scripts/webkitpy/layout_tests/port/base.py: Added.
        * Scripts/webkitpy/layout_tests/port/chromium.py: Added.
        * Scripts/webkitpy/layout_tests/port/chromium_linux.py:
        * Scripts/webkitpy/layout_tests/port/chromium_mac.py:
        * Scripts/webkitpy/layout_tests/port/chromium_win.py:
        * Scripts/webkitpy/layout_tests/port/http_server.py:
        * Scripts/webkitpy/layout_tests/port/http_server_base.py:
        * Scripts/webkitpy/layout_tests/port/path_utils.py: Removed.
        * Scripts/webkitpy/layout_tests/port/websocket_server.py:
        * Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py:
        * Scripts/webkitpy/layout_tests/run_chromium_webkit_tests.py:
        * Scripts/webkitpy/layout_tests/test_types/fuzzy_image_diff.py:
        * Scripts/webkitpy/layout_tests/test_types/image_diff.py:
        * Scripts/webkitpy/layout_tests/test_types/test_type_base.py:
        * Scripts/webkitpy/layout_tests/test_types/text_diff.py:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@54449 268f45cc-cd09-0410-ab3c-d52691b4dbfc

25 files changed:
WebKitTools/ChangeLog
WebKitTools/Scripts/rebaseline-chromium-webkit-tests
WebKitTools/Scripts/run-chromium-webkit-tests
WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py
WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py
WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations.py
WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_files.py
WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_shell_thread.py
WebKitTools/Scripts/webkitpy/layout_tests/port/__init__.py
WebKitTools/Scripts/webkitpy/layout_tests/port/apache_http_server.py
WebKitTools/Scripts/webkitpy/layout_tests/port/base.py [new file with mode: 0644]
WebKitTools/Scripts/webkitpy/layout_tests/port/chromium.py [new file with mode: 0644]
WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_linux.py
WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_mac.py
WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py
WebKitTools/Scripts/webkitpy/layout_tests/port/http_server.py
WebKitTools/Scripts/webkitpy/layout_tests/port/http_server_base.py
WebKitTools/Scripts/webkitpy/layout_tests/port/path_utils.py [deleted file]
WebKitTools/Scripts/webkitpy/layout_tests/port/websocket_server.py
WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py
WebKitTools/Scripts/webkitpy/layout_tests/run_chromium_webkit_tests.py
WebKitTools/Scripts/webkitpy/layout_tests/test_types/fuzzy_image_diff.py
WebKitTools/Scripts/webkitpy/layout_tests/test_types/image_diff.py
WebKitTools/Scripts/webkitpy/layout_tests/test_types/test_type_base.py
WebKitTools/Scripts/webkitpy/layout_tests/test_types/text_diff.py

index cfc1611..59cff32 100644 (file)
@@ -1,3 +1,39 @@
+2010-02-03  Dirk Pranke  <dpranke@chromium.org>
+
+        Reviewed by Eric Siedel.
+
+        Refactor the port package into an object-oriented style and merge
+        path_utils into it. We add a 'base' and a 'chromium' object to the
+        port package; this will allow us to easily add new ports (like
+        WebKit Mac).
+
+        https://bugs.webkit.org/show_bug.cgi?id=34511
+
+        * Scripts/rebaseline-chromium-webkit-tests:
+        * Scripts/run-chromium-webkit-tests:
+        * Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py:
+        * Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py:
+        * Scripts/webkitpy/layout_tests/layout_package/test_expectations.py:
+        * Scripts/webkitpy/layout_tests/layout_package/test_files.py:
+        * Scripts/webkitpy/layout_tests/layout_package/test_shell_thread.py:
+        * Scripts/webkitpy/layout_tests/port/__init__.py:
+        * Scripts/webkitpy/layout_tests/port/apache_http_server.py:
+        * Scripts/webkitpy/layout_tests/port/base.py: Added.
+        * Scripts/webkitpy/layout_tests/port/chromium.py: Added.
+        * Scripts/webkitpy/layout_tests/port/chromium_linux.py:
+        * Scripts/webkitpy/layout_tests/port/chromium_mac.py:
+        * Scripts/webkitpy/layout_tests/port/chromium_win.py:
+        * Scripts/webkitpy/layout_tests/port/http_server.py:
+        * Scripts/webkitpy/layout_tests/port/http_server_base.py:
+        * Scripts/webkitpy/layout_tests/port/path_utils.py: Removed.
+        * Scripts/webkitpy/layout_tests/port/websocket_server.py:
+        * Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py:
+        * Scripts/webkitpy/layout_tests/run_chromium_webkit_tests.py:
+        * Scripts/webkitpy/layout_tests/test_types/fuzzy_image_diff.py:
+        * Scripts/webkitpy/layout_tests/test_types/image_diff.py:
+        * Scripts/webkitpy/layout_tests/test_types/test_type_base.py:
+        * Scripts/webkitpy/layout_tests/test_types/text_diff.py:
+
 2010-01-19  Kenneth Rohde Christiansen  <kenneth@webkit.org>
 
         Reviewed by Dave Hyatt.
index 9a8a156..aea0edc 100755 (executable)
@@ -11,7 +11,7 @@
 # copyright notice, this list of conditions and the following disclaimer
 # in the documentation and/or other materials provided with the
 # distribution.
-#     * Neither the Chromium name nor the names of its
+#     * Neither the name of Google Inc. nor the names of its
 # contributors may be used to endorse or promote products derived from
 # this software without specific prior written permission.
 #
index 9c027b8..221b5aa 100755 (executable)
@@ -11,7 +11,7 @@
 # copyright notice, this list of conditions and the following disclaimer
 # in the documentation and/or other materials provided with the
 # distribution.
-#     * Neither the Chromium name nor the names of its
+#     * Neither the name of Google Inc. nor the names of its
 # contributors may be used to endorse or promote products derived from
 # this software without specific prior written permission.
 #
@@ -33,6 +33,7 @@ import sys
 
 sys.path.append(os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])),
                              "webkitpy", "layout_tests"))
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0]))))
 import run_chromium_webkit_tests
 
 if __name__ == '__main__':
index f38a7ab..520ab1f 100644 (file)
@@ -29,9 +29,9 @@
 
 import logging
 import os
+import simplejson
 
 from layout_package import json_results_generator
-from port import path_utils
 from layout_package import test_expectations
 from layout_package import test_failures
 
@@ -45,7 +45,7 @@ class JSONLayoutResultsGenerator(json_results_generator.JSONResultsGenerator):
     WONTFIX = "wontfixCounts"
     DEFERRED = "deferredCounts"
 
-    def __init__(self, builder_name, build_name, build_number,
+    def __init__(self, port, builder_name, build_name, build_number,
         results_file_base_path, builder_base_url,
         test_timings, expectations, result_summary, all_tests):
         """Modifies the results.json file. Grabs it off the archive directory
@@ -56,7 +56,7 @@ class JSONLayoutResultsGenerator(json_results_generator.JSONResultsGenerator):
               results.
           (see the comment of JSONResultsGenerator.__init__ for other Args)
         """
-
+        self._port = port
         self._builder_name = builder_name
         self._build_name = build_name
         self._build_number = build_number
@@ -153,7 +153,7 @@ class JSONLayoutResultsGenerator(json_results_generator.JSONResultsGenerator):
             test, test_name, tests)
 
         # Remove tests that don't exist anymore.
-        full_path = os.path.join(path_utils.layout_tests_dir(), test_name)
+        full_path = os.path.join(self._port.layout_tests_dir(), test_name)
         full_path = os.path.normpath(full_path)
         if not os.path.exists(full_path):
             del tests[test_name]
index dc24ade..84be0e1 100644 (file)
 
 import logging
 import os
+import simplejson
 import subprocess
 import sys
 import time
 import urllib2
 import xml.dom.minidom
 
-from port import path_utils
 from layout_package import test_expectations
 
-sys.path.append(path_utils.path_from_base('third_party', 'WebKit', 
-                                          'WebKitTools')) 
-import simplejson
-
 
 class JSONResultsGenerator(object):
 
@@ -80,7 +76,7 @@ class JSONResultsGenerator(object):
 
     RESULTS_FILENAME = "results.json"
 
-    def __init__(self, builder_name, build_name, build_number,
+    def __init__(self, port, builder_name, build_name, build_number,
         results_file_base_path, builder_base_url,
         test_timings, failures, passed_tests, skipped_tests, all_tests):
         """Modifies the results.json file. Grabs it off the archive directory
@@ -100,6 +96,7 @@ class JSONResultsGenerator(object):
           all_tests: List of all the tests that were run.  This should not
               include skipped tests.
         """
+        self._port = port
         self._builder_name = builder_name
         self._build_name = build_name
         self._build_number = build_number
@@ -122,22 +119,24 @@ class JSONResultsGenerator(object):
             results_file.write(json)
             results_file.close()
 
-    def _get_svn_revision(self, in_directory=None):
+    def _get_svn_revision(self, in_directory):
         """Returns the svn revision for the given directory.
 
         Args:
           in_directory: The directory where svn is to be run.
         """
-        output = subprocess.Popen(["svn", "info", "--xml"],
-                                  cwd=in_directory,
-                                  shell=(sys.platform == 'win32'),
-                                  stdout=subprocess.PIPE).communicate()[0]
-        try:
-            dom = xml.dom.minidom.parseString(output)
-            return dom.getElementsByTagName('entry')[0].getAttribute(
-                'revision')
-        except xml.parsers.expat.ExpatError:
-            return ""
+        if os.path.exists(os.path.join(in_directory, '.svn')):
+            output = subprocess.Popen(["svn", "info", "--xml"],
+                                      cwd=in_directory,
+                                      shell=(sys.platform == 'win32'),
+                                      stdout=subprocess.PIPE).communicate()[0]
+            try:
+                dom = xml.dom.minidom.parseString(output)
+                return dom.getElementsByTagName('entry')[0].getAttribute(
+                    'revision')
+            except xml.parsers.expat.ExpatError:
+                return ""
+        return ""
 
     def _get_archived_json_results(self):
         """Reads old results JSON file if it exists.
@@ -305,16 +304,19 @@ class JSONResultsGenerator(object):
         self._insert_item_into_raw_list(results_for_builder,
             self._build_number, self.BUILD_NUMBERS)
 
-        path_to_webkit = path_utils.path_from_base('third_party', 'WebKit',
-                                                   'WebCore')
-        self._insert_item_into_raw_list(results_for_builder,
-            self._get_svn_revision(path_to_webkit),
-            self.WEBKIT_SVN)
-
-        path_to_chrome_base = path_utils.path_from_base()
-        self._insert_item_into_raw_list(results_for_builder,
-            self._get_svn_revision(path_to_chrome_base),
-            self.CHROME_SVN)
+        # These next two branches test to see which source repos we can
+        # pull revisions from.
+        if hasattr(self._port, 'path_from_webkit_base'):
+            path_to_webkit = self._port.path_from_webkit_base()
+            self._insert_item_into_raw_list(results_for_builder,
+                self._get_svn_revision(path_to_webkit),
+                self.WEBKIT_SVN)
+
+        if hasattr(self._port, 'path_from_chromium_base'):
+            path_to_chrome = self._port.path_from_chromium_base()
+            self._insert_item_into_raw_list(results_for_builder,
+                self._get_svn_revision(path_to_chrome),
+                self.CHROME_SVN)
 
         self._insert_item_into_raw_list(results_for_builder,
             int(time.time()),
index 5b0d186..a3650ed 100644 (file)
@@ -36,10 +36,7 @@ import os
 import re
 import sys
 import time
-from port import path_utils
 
-sys.path.append(path_utils.path_from_base('third_party', 'WebKit', 
-                                          'WebKitTools')) 
 import simplejson
 
 # Test expectation and modifier constants.
@@ -53,12 +50,28 @@ import simplejson
 class TestExpectations:
     TEST_LIST = "test_expectations.txt"
 
-    def __init__(self, tests, directory, platform, is_debug_mode, is_lint_mode,
-        tests_are_present=True):
-        """Reads the test expectations files from the given directory."""
-        path = os.path.join(directory, self.TEST_LIST)
-        self._expected_failures = TestExpectationsFile(path, tests, platform,
-            is_debug_mode, is_lint_mode, tests_are_present=tests_are_present)
+    def __init__(self, port, tests, expectations, test_platform_name,
+                 is_debug_mode, is_lint_mode, tests_are_present=True):
+        """Loads and parses the test expectations given in the string.
+        Args:
+            port: handle to object containing platform-specific functionality
+            test: list of all of the test files
+            expectations: test expectations as a string
+            test_platform_name: name of the platform to match expectations
+                against. Note that this may be different than
+                port.test_platform_name() when is_lint_mode is True.
+            is_debug_mode: whether to use the DEBUG or RELEASE modifiers
+                in the expectations
+            is_lint_mode: If True, just parse the expectations string
+                looking for errors.
+            tests_are_present: whether the test files exist in the file
+                system and can be probed for. This is useful for distinguishing
+                test files from directories, and is needed by the LTTF
+                dashboard, where the files aren't actually locally present.
+        """
+        self._expected_failures = TestExpectationsFile(port, expectations,
+            tests, test_platform_name, is_debug_mode, is_lint_mode,
+            tests_are_present=tests_are_present)
 
     # TODO(ojan): Allow for removing skipped tests when getting the list of
     # tests to run, but not when getting metrics.
@@ -230,9 +243,6 @@ class TestExpectationsFile:
     EXPECTATION_ORDER = (PASS, CRASH, TIMEOUT, MISSING, IMAGE_PLUS_TEXT,
        TEXT, IMAGE, FAIL, SKIP)
 
-    BASE_PLATFORMS = ('linux', 'mac', 'win')
-    PLATFORMS = BASE_PLATFORMS + ('win-xp', 'win-vista', 'win-7')
-
     BUILD_TYPES = ('debug', 'release')
 
     MODIFIERS = {'skip': SKIP,
@@ -251,37 +261,34 @@ class TestExpectationsFile:
                     'fail': FAIL,
                     'flaky': FLAKY}
 
-    def __init__(self, path, full_test_list, platform, is_debug_mode,
-        is_lint_mode, expectations_as_str=None, suppress_errors=False,
+    def __init__(self, port, expectations, full_test_list, test_platform_name,
+        is_debug_mode, is_lint_mode, suppress_errors=False,
         tests_are_present=True):
         """
-        path: The path to the expectation file. An error is thrown if a test is
-            listed more than once.
+        expectations: Contents of the expectations file
         full_test_list: The list of all tests to be run pending processing of
             the expections for those tests.
-        platform: Which platform from self.PLATFORMS to filter tests for.
+        test_platform_name: name of the platform to match expectations
+            against. Note that this may be different than
+            port.test_platform_name() when is_lint_mode is True.
         is_debug_mode: Whether we testing a test_shell built debug mode.
         is_lint_mode: Whether this is just linting test_expecatations.txt.
-        expectations_as_str: Contents of the expectations file. Used instead of
-            the path. This makes unittesting sane.
         suppress_errors: Whether to suppress lint errors.
         tests_are_present: Whether the test files are present in the local
             filesystem. The LTTF Dashboard uses False here to avoid having to
             keep a local copy of the tree.
         """
 
-        self._path = path
-        self._expectations_as_str = expectations_as_str
+        self._port = port
+        self._expectations = expectations
+        self._full_test_list = full_test_list
+        self._test_platform_name = test_platform_name
+        self._is_debug_mode = is_debug_mode
         self._is_lint_mode = is_lint_mode
         self._tests_are_present = tests_are_present
-        self._full_test_list = full_test_list
         self._suppress_errors = suppress_errors
         self._errors = []
         self._non_fatal_errors = []
-        self._platform = self.to_test_platform_name(platform)
-        if self._platform is None:
-            raise Exception("Unknown platform '%s'" % (platform))
-        self._is_debug_mode = is_debug_mode
 
         # Maps relative test paths as listed in the expectations file to a
         # list of maps containing modifiers and expectations for each time
@@ -320,27 +327,13 @@ class TestExpectationsFile:
         """Returns an object that can be iterated over. Allows for not caring
         about whether we're iterating over a file or a new-line separated
         string."""
-        if self._expectations_as_str:
-            iterable = [x + "\n" for x in
-                self._expectations_as_str.split("\n")]
-            # Strip final entry if it's empty to avoid added in an extra
-            # newline.
-            if iterable[len(iterable) - 1] == "\n":
-                return iterable[:len(iterable) - 1]
-            return iterable
-        else:
-            return open(self._path)
-
-    def to_test_platform_name(self, name):
-        """Returns the test expectation platform that will be used for a
-        given platform name, or None if there is no match."""
-        chromium_prefix = 'chromium-'
-        name = name.lower()
-        if name.startswith(chromium_prefix):
-            name = name[len(chromium_prefix):]
-        if name in self.PLATFORMS:
-            return name
-        return None
+        iterable = [x + "\n" for x in
+            self._expectations.split("\n")]
+        # Strip final entry if it's empty to avoid added in an extra
+        # newline.
+        if iterable[-1] == "\n":
+            return iterable[:-1]
+        return iterable
 
     def get_test_set(self, modifier, expectation=None, include_skips=True):
         if expectation is None:
@@ -398,6 +391,12 @@ class TestExpectationsFile:
           no
         """
 
+        # FIXME - remove_platform_from file worked by writing a new
+        # test_expectations.txt file over the old one. Now that we're just
+        # parsing strings, we need to change this to return the new
+        # expectations string.
+        raise NotImplementedException('remove_platform_from_file')
+
         new_file = self._path + '.new'
         logging.debug('Original file: "%s"', self._path)
         logging.debug('New file: "%s"', new_file)
@@ -430,11 +429,12 @@ class TestExpectationsFile:
             elif action == ADD_PLATFORMS_EXCEPT_THIS:
                 parts = line.split(':')
                 new_options = parts[0]
-                for p in self.PLATFORMS:
-                    p = p.upper();
+                for p in self._port.test_platform_names():
+                    p = p.upper()
                     # This is a temp solution for rebaselining tool.
                     # Do not add tags WIN-7 and WIN-VISTA to test expectations
-                    # if the original line does not specify the platform option.
+                    # if the original line does not specify the platform
+                    # option.
                     # TODO(victorw): Remove WIN-VISTA and WIN-7 once we have
                     # reliable Win 7 and Win Vista buildbots setup.
                     if not p in (platform.upper(), 'WIN-VISTA', 'WIN-7'):
@@ -517,7 +517,7 @@ class TestExpectationsFile:
 
         has_any_platform = False
         for option in options:
-            if option in self.PLATFORMS:
+            if option in self._port.test_platform_names():
                 has_any_platform = True
                 if not option == platform:
                     return REMOVE_PLATFORM
@@ -547,7 +547,7 @@ class TestExpectationsFile:
         for option in options:
             if option in self.MODIFIERS:
                 modifiers.add(option)
-            elif option in self.PLATFORMS:
+            elif option in self._port.test_platform_names():
                 has_any_platform = True
             elif option.startswith('bug'):
                 has_bug_id = True
@@ -590,7 +590,7 @@ class TestExpectationsFile:
           options: list of options
         """
         for opt in options:
-            if self._platform.startswith(opt):
+            if self._test_platform_name.startswith(opt):
                 return True
         return False
 
@@ -632,7 +632,7 @@ class TestExpectationsFile:
                     'indefinitely, then it should be just timeout.',
                     test_list_path)
 
-            full_path = os.path.join(path_utils.layout_tests_dir(),
+            full_path = os.path.join(self._port.layout_tests_dir(),
                                      test_list_path)
             full_path = os.path.normpath(full_path)
             # WebKit's way of skipping tests is to add a -disabled suffix.
@@ -662,7 +662,7 @@ class TestExpectationsFile:
             else:
                 build_type = 'RELEASE'
             print "\nFAILURES FOR PLATFORM: %s, BUILD_TYPE: %s" \
-                % (self._platform.upper(), build_type)
+                % (self._test_platform_name.upper(), build_type)
 
             for error in self._non_fatal_errors:
                 logging.error(error)
@@ -695,7 +695,7 @@ class TestExpectationsFile:
     def _expand_tests(self, test_list_path):
         """Convert the test specification to an absolute, normalized
         path and make sure directories end with the OS path separator."""
-        path = os.path.join(path_utils.layout_tests_dir(), test_list_path)
+        path = os.path.join(self._port.layout_tests_dir(), test_list_path)
         path = os.path.normpath(path)
         path = self._fix_dir(path)
 
index b7e620d..3c087c0 100644 (file)
@@ -36,7 +36,6 @@ under that directory."""
 
 import glob
 import os
-from port import path_utils
 
 # When collecting test cases, we include any file with these extensions.
 _supported_file_extensions = set(['.html', '.shtml', '.xml', '.xhtml', '.pl',
@@ -45,7 +44,7 @@ _supported_file_extensions = set(['.html', '.shtml', '.xml', '.xhtml', '.pl',
 _skipped_directories = set(['.svn', '_svn', 'resources', 'script-tests'])
 
 
-def gather_test_files(paths):
+def gather_test_files(port, paths):
     """Generate a set of test files and return them.
 
     Args:
@@ -57,14 +56,14 @@ def gather_test_files(paths):
     if paths:
         for path in paths:
             # If there's an * in the name, assume it's a glob pattern.
-            path = os.path.join(path_utils.layout_tests_dir(), path)
+            path = os.path.join(port.layout_tests_dir(), path)
             if path.find('*') > -1:
                 filenames = glob.glob(path)
                 paths_to_walk.update(filenames)
             else:
                 paths_to_walk.add(path)
     else:
-        paths_to_walk.add(path_utils.layout_tests_dir())
+        paths_to_walk.add(port.layout_tests_dir())
 
     # Now walk all the paths passed in on the command line and get filenames
     test_files = set()
index 9f52686..f0e5252 100644 (file)
@@ -40,21 +40,22 @@ import logging
 import os
 import Queue
 import signal
-import subprocess
 import sys
 import thread
 import threading
 import time
 
-from port import path_utils
 import test_failures
 
 
-def process_output(proc, test_info, test_types, test_args, target, output_dir):
+def process_output(port, test_info, test_types, test_args, target, output_dir,
+                   crash, timeout, test_run_time, actual_checksum,
+                   output, error):
     """Receives the output from a test_shell process, subjects it to a number
     of tests, and returns a list of failure types the test produced.
 
     Args:
+      port: port-specific hooks
       proc: an active test_shell process
       test_info: Object containing the test filename, uri and timeout
       test_types: list of test types to subject the output to
@@ -64,84 +65,39 @@ def process_output(proc, test_info, test_types, test_args, target, output_dir):
 
     Returns: a list of failure objects and times for the test being processed
     """
-    outlines = []
-    extra_lines = []
     failures = []
-    crash = False
 
     # Some test args, such as the image hash, may be added or changed on a
     # test-by-test basis.
     local_test_args = copy.copy(test_args)
 
-    start_time = time.time()
-
-    line = proc.stdout.readline()
-
-    # Only start saving output lines once we've loaded the URL for the test.
-    url = None
-    test_string = test_info.uri.strip()
-
-    while line.rstrip() != "#EOF":
-        # Make sure we haven't crashed.
-        if line == '' and proc.poll() is not None:
-            failures.append(test_failures.FailureCrash())
-
-            # This is hex code 0xc000001d, which is used for abrupt
-            # termination. This happens if we hit ctrl+c from the prompt and
-            # we happen to be waiting on the test_shell.
-            # sdoyon: Not sure for which OS and in what circumstances the
-            # above code is valid. What works for me under Linux to detect
-            # ctrl+c is for the subprocess returncode to be negative SIGINT.
-            # And that agrees with the subprocess documentation.
-            if (-1073741510 == proc.returncode or
-                - signal.SIGINT == proc.returncode):
-                raise KeyboardInterrupt
-            crash = True
-            break
-
-        # Don't include #URL lines in our output
-        if line.startswith("#URL:"):
-            url = line.rstrip()[5:]
-            if url != test_string:
-                logging.fatal("Test got out of sync:\n|%s|\n|%s|" %
-                              (url, test_string))
-                raise AssertionError("test out of sync")
-        elif line.startswith("#MD5:"):
-            local_test_args.hash = line.rstrip()[5:]
-        elif line.startswith("#TEST_TIMED_OUT"):
-            # Test timed out, but we still need to read until #EOF.
-            failures.append(test_failures.FailureTimeout())
-        elif url:
-            outlines.append(line)
-        else:
-            extra_lines.append(line)
-
-        line = proc.stdout.readline()
-
-    end_test_time = time.time()
-
-    if len(extra_lines):
-        extra = "".join(extra_lines)
-        if crash:
-            logging.debug("Stacktrace for %s:\n%s" % (test_string, extra))
-            # Strip off "file://" since RelativeTestFilename expects
-            # filesystem paths.
-            filename = os.path.join(output_dir,
-                path_utils.relative_test_filename(test_string[7:]))
-            filename = os.path.splitext(filename)[0] + "-stack.txt"
-            path_utils.maybe_make_directory(os.path.split(filename)[0])
-            open(filename, "wb").write(extra)
-        else:
-            logging.debug("Previous test output extra lines after dump:\n%s" %
-                extra)
+    local_test_args.hash = actual_checksum
+
+    if crash:
+        failures.append(test_failures.FailureCrash())
+    if timeout:
+        failures.append(test_failures.FailureTimeout())
+
+    if crash:
+        logging.debug("Stacktrace for %s:\n%s" % (test_info.filename, error))
+        # Strip off "file://" since RelativeTestFilename expects
+        # filesystem paths.
+        filename = os.path.join(output_dir, test_info.filename)
+        filename = os.path.splitext(filename)[0] + "-stack.txt"
+        port.maybe_make_directory(os.path.split(filename)[0])
+        open(filename, "wb").write(error)
+    else:
+        logging.debug("Previous test output extra lines after dump:\n%s" %
+            error)
 
     # Check the output and save the results.
+    start_time = time.time()
     time_for_diffs = {}
     for test_type in test_types:
         start_diff_time = time.time()
-        new_failures = test_type.compare_output(test_info.filename,
-                                                proc, ''.join(outlines),
-                                                local_test_args, target)
+        new_failures = test_type.compare_output(port, test_info.filename,
+                                                output, local_test_args,
+                                                target)
         # Don't add any more failures if we already have a crash, so we don't
         # double-report those tests. We do double-report for timeouts since
         # we still want to see the text and image output.
@@ -150,28 +106,11 @@ def process_output(proc, test_info, test_types, test_args, target, output_dir):
         time_for_diffs[test_type.__class__.__name__] = (
             time.time() - start_diff_time)
 
-    total_time_for_all_diffs = time.time() - end_test_time
-    test_run_time = end_test_time - start_time
+    total_time_for_all_diffs = time.time() - start_diff_time
     return TestStats(test_info.filename, failures, test_run_time,
         total_time_for_all_diffs, time_for_diffs)
 
 
-def start_test_shell(command, args):
-    """Returns the process for a new test_shell started in layout-tests mode.
-    """
-    cmd = []
-    # Hook for injecting valgrind or other runtime instrumentation,
-    # used by e.g. tools/valgrind/valgrind_tests.py.
-    wrapper = os.environ.get("BROWSER_WRAPPER", None)
-    if wrapper != None:
-        cmd += [wrapper]
-    cmd += command + ['--layout-tests'] + args
-    return subprocess.Popen(cmd,
-                            stdin=subprocess.PIPE,
-                            stdout=subprocess.PIPE,
-                            stderr=subprocess.STDOUT)
-
-
 class TestStats:
 
     def __init__(self, filename, failures, test_run_time,
@@ -186,17 +125,19 @@ class TestStats:
 class SingleTestThread(threading.Thread):
     """Thread wrapper for running a single test file."""
 
-    def __init__(self, test_shell_command, shell_args, test_info, test_types,
-        test_args, target, output_dir):
+    def __init__(self, port, image_path, shell_args, test_info,
+        test_types, test_args, target, output_dir):
         """
         Args:
+          port: object implementing port-specific hooks
           test_info: Object containing the test filename, uri and timeout
           output_dir: Directory to put crash stacks into.
           See TestShellThread for documentation of the remaining arguments.
         """
 
         threading.Thread.__init__(self)
-        self._command = test_shell_command
+        self._port = port
+        self._image_path = image_path
         self._shell_args = shell_args
         self._test_info = test_info
         self._test_types = test_types
@@ -205,10 +146,18 @@ class SingleTestThread(threading.Thread):
         self._output_dir = output_dir
 
     def run(self):
-        proc = start_test_shell(self._command, self._shell_args +
-            ["--time-out-ms=" + self._test_info.timeout, self._test_info.uri])
-        self._test_stats = process_output(proc, self._test_info,
-            self._test_types, self._test_args, self._target, self._output_dir)
+        driver = self._port.start_test_driver(self._image_path,
+            self._shell_args)
+        start = time.time()
+        crash, timeout, actual_checksum, output, error = \
+            driver.run_test(test_info.uri.strip(), test_info.timeout,
+                            test_info.image_hash)
+        end = time.time()
+        self._test_stats = process_output(self._port,
+            self._test_info, self._test_types, self._test_args,
+            self._target, self._output_dir, crash, timeout, end - start,
+            actual_checksum, output, error)
+        driver.stop()
 
     def get_test_stats(self):
         return self._test_stats
@@ -216,17 +165,16 @@ class SingleTestThread(threading.Thread):
 
 class TestShellThread(threading.Thread):
 
-    def __init__(self, filename_list_queue, result_queue, test_shell_command,
-                 test_types, test_args, shell_args, options):
+    def __init__(self, port, filename_list_queue, result_queue,
+                 test_types, test_args, image_path, shell_args, options):
         """Initialize all the local state for this test shell thread.
 
         Args:
+          port: interface to port-specific hooks
           filename_list_queue: A thread safe Queue class that contains lists
               of tuples of (filename, uri) pairs.
           result_queue: A thread safe Queue class that will contain tuples of
               (test, failure lists) for the test results.
-          test_shell_command: A list specifying the command+args for
-              test_shell
           test_types: A list of TestType objects to run the test output
               against.
           test_args: A TestArguments object to pass to each TestType.
@@ -236,13 +184,14 @@ class TestShellThread(threading.Thread):
               run_webkit_tests; they are typically passed via the
               run_webkit_tests.TestRunner class."""
         threading.Thread.__init__(self)
+        self._port = port
         self._filename_list_queue = filename_list_queue
         self._result_queue = result_queue
         self._filename_list = []
-        self._test_shell_command = test_shell_command
         self._test_types = test_types
         self._test_args = test_args
-        self._test_shell_proc = None
+        self._driver = None
+        self._image_path = image_path
         self._shell_args = shell_args
         self._options = options
         self._canceled = False
@@ -379,11 +328,11 @@ class TestShellThread(threading.Thread):
                 # Print the error message(s).
                 error_str = '\n'.join(['  ' + f.message() for f in failures])
                 logging.debug("%s %s failed:\n%s" % (self.getName(),
-                              path_utils.relative_test_filename(filename),
+                              self._port.relative_test_filename(filename),
                               error_str))
             else:
                 logging.debug("%s %s passed" % (self.getName(),
-                              path_utils.relative_test_filename(filename)))
+                              self._port.relative_test_filename(filename)))
             self._result_queue.put((filename, failures))
 
             if batch_size > 0 and batch_count > batch_size:
@@ -407,7 +356,7 @@ class TestShellThread(threading.Thread):
         Return:
           A list of TestFailure objects describing the error.
         """
-        worker = SingleTestThread(self._test_shell_command,
+        worker = SingleTestThread(self._port, self._image_path,
                                   self._shell_args,
                                   test_info,
                                   self._test_types,
@@ -431,7 +380,7 @@ class TestShellThread(threading.Thread):
             # tradeoff in order to avoid losing the rest of this thread's
             # results.
             logging.error('Test thread hung: killing all test_shells')
-            path_utils.kill_all_test_shells()
+            worker._driver.stop()
 
         try:
             stats = worker.get_test_stats()
@@ -454,32 +403,23 @@ class TestShellThread(threading.Thread):
           A list of TestFailure objects describing the error.
         """
         self._ensure_test_shell_is_running()
-        # Args to test_shell is a space-separated list of
-        # "uri timeout pixel_hash"
-        # The timeout and pixel_hash are optional.  The timeout is used if this
-        # test has a custom timeout. The pixel_hash is used to avoid doing an
-        # image dump if the checksums match, so it should be set to a blank
-        # value if we are generating a new baseline.
-        # (Otherwise, an image from a previous run will be copied into
-        # the baseline.)
+        # The pixel_hash is used to avoid doing an image dump if the
+        # checksums match, so it should be set to a blank value if we
+        # are generating a new baseline.  (Otherwise, an image from a
+        # previous run will be copied into the baseline.)
         image_hash = test_info.image_hash
         if image_hash and self._test_args.new_baseline:
             image_hash = ""
-        self._test_shell_proc.stdin.write(("%s %s %s\n" %
-            (test_info.uri, test_info.timeout, image_hash)))
-
-        # If the test shell is dead, the above may cause an IOError as we
-        # try to write onto the broken pipe. If this is the first test for
-        # this test shell process, than the test shell did not
-        # successfully start. If this is not the first test, then the
-        # previous tests have caused some kind of delayed crash. We don't
-        # try to recover here.
-        self._test_shell_proc.stdin.flush()
-
-        stats = process_output(self._test_shell_proc, test_info,
-                               self._test_types, self._test_args,
-                               self._options.target,
-                               self._options.results_directory)
+        start = time.time()
+        crash, timeout, actual_checksum, output, error = \
+           self._driver.run_test(test_info.uri, test_info.timeout, image_hash)
+        end = time.time()
+
+        stats = process_output(self._port, test_info, self._test_types,
+                               self._test_args, self._options.target,
+                               self._options.results_directory, crash,
+                               timeout, end - start, actual_checksum,
+                               output, error)
 
         self._test_stats.append(stats)
         return stats.failures
@@ -489,23 +429,12 @@ class TestShellThread(threading.Thread):
         running tests singly, since those each start a separate test shell in
         their own thread.
         """
-        if (not self._test_shell_proc or
-            self._test_shell_proc.poll() is not None):
-            self._test_shell_proc = start_test_shell(self._test_shell_command,
-                                                     self._shell_args)
+        if (not self._driver or self._driver.poll() is not None):
+            self._driver = self._port.start_driver(
+                self._image_path, self._shell_args)
 
     def _kill_test_shell(self):
         """Kill the test shell process if it's running."""
-        if self._test_shell_proc:
-            self._test_shell_proc.stdin.close()
-            self._test_shell_proc.stdout.close()
-            if self._test_shell_proc.stderr:
-                self._test_shell_proc.stderr.close()
-            if (sys.platform not in ('win32', 'cygwin') and
-                not self._test_shell_proc.poll()):
-                # Closing stdin/stdout/stderr hangs sometimes on OS X.
-                null = open(os.devnull, "w")
-                subprocess.Popen(["kill", "-9",
-                                 str(self._test_shell_proc.pid)], stderr=null)
-                null.close()
-            self._test_shell_proc = None
+        if self._driver:
+            self._driver.stop()
+            self._driver = None
index 1730085..b4fa2ad 100644 (file)
 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-"""Platform-specific utilities and pseudo-constants
+"""Port-specific entrypoints for the layout tests test infrastructure."""
 
-Any functions whose implementations or values differ from one platform to
-another should be defined in their respective <platform>.py
-modules. The appropriate one of those will be imported into this module to
-provide callers with a common, platform-independent interface.
 
-This file should only ever be imported by layout_package.path_utils.
-"""
+def get(port_name=None, options=None):
+    """Returns an object implementing the Port interface. If
+    port_name is None, this routine attempts to guess at the most
+    appropriate port on this platform."""
+    port_to_use = port_name
+    if port_to_use is None:
+        port_to_use = 'chromium-mac'
 
-import sys
+    if port_to_use.startswith('chromium-mac'):
+        import chromium_mac
+        return chromium_mac.ChromiumMacPort(port_name, options)
+    elif port_to_use.startswith('chromium-linux'):
+        import chromium_linux
+        return chromium_linux.ChromiumLinuxPort(port_name, options)
+    elif port_to_use.startwith('chromium-win'):
+        import chromium_win
+        return chromium_win.ChromiumWinPort(port_name, options)
 
-# We may not support the version of Python that a user has installed (Cygwin
-# especially has had problems), but we'll allow the platform utils to be
-# included in any case so we don't get an import error.
-if sys.platform in ('cygwin', 'win32'):
-    from chromium_win import *
-elif sys.platform == 'darwin':
-    from chromium_mac import *
-elif sys.platform in ('linux', 'linux2', 'freebsd7', 'openbsd4'):
-    from chromium_linux import *
+    raise NotImplementedError('unsupported port: %s' % port_name)
index 1fb0367..9ff3671 100644 (file)
@@ -37,19 +37,19 @@ import subprocess
 import sys
 
 import http_server_base
-import path_utils
-import port
 
 
 class LayoutTestApacheHttpd(http_server_base.HttpServerBase):
 
-    def __init__(self, output_dir):
+    def __init__(self, port_obj, output_dir):
         """Args:
+          port_obj: handle to the platform-specific routines
           output_dir: the absolute path to the layout test result directory
         """
+        http_server_base.HttpServerBase.__init__(self, port_obj)
         self._output_dir = output_dir
         self._httpd_proc = None
-        path_utils.maybe_make_directory(output_dir)
+        port_obj.maybe_make_directory(output_dir)
 
         self.mappings = [{'port': 8000},
                          {'port': 8080},
@@ -59,15 +59,14 @@ class LayoutTestApacheHttpd(http_server_base.HttpServerBase):
         # The upstream .conf file assumed the existence of /tmp/WebKit for
         # placing apache files like the lock file there.
         self._runtime_path = os.path.join("/tmp", "WebKit")
-        path_utils.maybe_make_directory(self._runtime_path)
+        port_obj.maybe_make_directory(self._runtime_path)
 
         # The PID returned when Apache is started goes away (due to dropping
         # privileges?). The proper controlling PID is written to a file in the
         # apache runtime directory.
         self._pid_file = os.path.join(self._runtime_path, 'httpd.pid')
 
-        test_dir = path_utils.path_from_base('third_party', 'WebKit',
-            'LayoutTests')
+        test_dir = self._port_obj.layout_tests_dir()
         js_test_resources_dir = self._cygwin_safe_join(test_dir, "fast", "js",
             "resources")
         mime_types_path = self._cygwin_safe_join(test_dir, "http", "conf",
@@ -78,7 +77,7 @@ class LayoutTestApacheHttpd(http_server_base.HttpServerBase):
         error_log = self._cygwin_safe_join(output_dir, "error_log.txt")
         document_root = self._cygwin_safe_join(test_dir, "http", "tests")
 
-        executable = port.apache_executable_path()
+        executable = self._port_obj._path_to_apache()
         if self._is_cygwin():
             executable = self._get_cygwin_path(executable)
 
@@ -95,7 +94,8 @@ class LayoutTestApacheHttpd(http_server_base.HttpServerBase):
                 os.environ.get("USER", ""))]
 
         if self._is_cygwin():
-            cygbin = path_utils.path_from_base('third_party', 'cygwin', 'bin')
+            cygbin = self._port_obj._path_from_base('third_party', 'cygwin',
+                'bin')
             # Not entirely sure why, but from cygwin we need to run the
             # httpd command through bash.
             self._start_cmd = [
@@ -146,7 +146,7 @@ class LayoutTestApacheHttpd(http_server_base.HttpServerBase):
           test_dir: absolute path to the LayoutTests directory.
           output_dir: absolute path to the layout test results directory.
         """
-        httpd_config = port.apache_config_file_path()
+        httpd_config = self._port_obj._path_to_apache_config_file()
         httpd_config_copy = os.path.join(output_dir, "httpd.conf")
         httpd_conf = open(httpd_config).read()
         if self._is_cygwin():
@@ -156,22 +156,11 @@ class LayoutTestApacheHttpd(http_server_base.HttpServerBase):
             # plus the relative paths to the .so files listed in the .conf
             # file. We have apache/cygwin checked into our tree so
             # people don't have to install it into their cygwin.
-            cygusr = path_utils.path_from_base('third_party', 'cygwin', 'usr')
+            cygusr = self._port_obj._path_from_base('third_party', 'cygwin',
+                'usr')
             httpd_conf = httpd_conf.replace('ServerRoot "/usr"',
                 'ServerRoot "%s"' % self._get_cygwin_path(cygusr))
 
-        # TODO(ojan): Instead of writing an extra file, checkin a conf file
-        # upstream. Or, even better, upstream/delete all our chrome http
-        # tests so we don't need this special-cased DocumentRoot and then
-        # just use the upstream
-        # conf file.
-        chrome_document_root = path_utils.path_from_base('webkit', 'data',
-            'layout_tests')
-        if self._is_cygwin():
-            chrome_document_root = self._get_cygwin_path(chrome_document_root)
-        httpd_conf = (httpd_conf +
-            self._get_virtual_host_config(chrome_document_root, 8081))
-
         f = open(httpd_config_copy, 'wb')
         f.write(httpd_conf)
         f.close()
@@ -226,4 +215,4 @@ class LayoutTestApacheHttpd(http_server_base.HttpServerBase):
         httpd_pid = None
         if os.path.exists(self._pid_file):
             httpd_pid = int(open(self._pid_file).readline())
-        path_utils.shut_down_http_server(httpd_pid)
+        self._port_obj._shut_down_http_server(httpd_pid)
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py
new file mode 100644 (file)
index 0000000..f9fc3cb
--- /dev/null
@@ -0,0 +1,639 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the Google name nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Abstract base class of Port-specific entrypoints for the layout tests
+test infrastructure (the Port and Driver classes)."""
+
+import cgi
+import difflib
+import errno
+import os
+import subprocess
+import sys
+
+import apache_http_server
+import http_server
+import websocket_server
+
+# Python bug workaround.  See Port.wdiff_text() for an explanation.
+_wdiff_available = True
+
+
+class Port(object):
+    """Abstract class for Port-specific hooks for the layout_test package.
+    """
+
+    def __init__(self, port_name=None, options=None):
+        self._name = port_name
+        self._options = options
+        self._helper = None
+        self._http_server = None
+        self._webkit_base_dir = None
+        self._websocket_server = None
+
+    def baseline_path(self):
+        """Return the absolute path to the directory to store new baselines
+        in for this port."""
+        raise NotImplementedError('Port.baseline_path')
+
+    def baseline_search_path(self):
+        """Return a list of absolute paths to directories to search under for
+        baselines. The directories are searched in order."""
+        raise NotImplementedError('Port.baseline_search_path')
+
+    def check_sys_deps(self):
+        """If the port needs to do some runtime checks to ensure that the
+        tests can be run successfully, they should be done here.
+
+        Returns whether the system is properly configured."""
+        raise NotImplementedError('Port.check_sys_deps')
+
+    def compare_text(self, actual_text, expected_text):
+        """Return whether or not the two strings are *not* equal. This
+        routine is used to diff text output.
+
+        While this is a generic routine, we include it in the Port
+        interface so that it can be overriden for testing purposes."""
+        return actual_text != expected_text
+
+    def diff_image(self, actual_filename, expected_filename, diff_filename):
+        """Compare two image files and produce a delta image file.
+
+        Return 1 if the two files are different, 0 if they are the same.
+        Also produce a delta image of the two images and write that into
+        |diff_filename|.
+
+        While this is a generic routine, we include it in the Port
+        interface so that it can be overriden for testing purposes."""
+        executable = self._path_to_image_diff()
+        cmd = [executable, '--diff', actual_filename, expected_filename,
+               diff_filename]
+        result = 1
+        try:
+            result = subprocess.call(cmd)
+        except OSError, e:
+            if e.errno == errno.ENOENT or e.errno == errno.EACCES:
+                _compare_available = False
+            else:
+                raise e
+        except ValueError:
+            # work around a race condition in Python 2.4's implementation
+            # of subprocess.Popen
+            pass
+        return result
+
+    def diff_text(self, actual_text, expected_text,
+                  actual_filename, expected_filename):
+        """Returns a string containing the diff of the two text strings
+        in 'unified diff' format.
+
+        While this is a generic routine, we include it in the Port
+        interface so that it can be overriden for testing purposes."""
+        diff = difflib.unified_diff(expected_text.splitlines(True),
+                                    actual_text.splitlines(True),
+                                    expected_filename,
+                                    actual_filename)
+        return ''.join(diff)
+
+    def expected_baselines(self, filename, suffix, all_baselines=False):
+        """Given a test name, finds where the baseline results are located.
+
+        Args:
+        filename: absolute filename to test file
+        suffix: file suffix of the expected results, including dot; e.g.
+            '.txt' or '.png'.  This should not be None, but may be an empty
+            string.
+        all_baselines: If True, return an ordered list of all baseline paths
+            for the given platform. If False, return only the first one.
+        Returns
+        a list of ( platform_dir, results_filename ), where
+            platform_dir - abs path to the top of the results tree (or test
+                tree)
+            results_filename - relative path from top of tree to the results
+                file
+            (os.path.join of the two gives you the full path to the file,
+                unless None was returned.)
+        Return values will be in the format appropriate for the current
+        platform (e.g., "\\" for path separators on Windows). If the results
+        file is not found, then None will be returned for the directory,
+        but the expected relative pathname will still be returned.
+
+        This routine is generic but lives here since it is used in
+        conjunction with the other baseline and filename routines that are
+        platform specific.
+        """
+        testname = os.path.splitext(self.relative_test_filename(filename))[0]
+
+        baseline_filename = testname + '-expected' + suffix
+
+        baseline_search_path = self.baseline_search_path()
+
+        baselines = []
+        for platform_dir in baseline_search_path:
+            if os.path.exists(os.path.join(platform_dir, baseline_filename)):
+                baselines.append((platform_dir, baseline_filename))
+
+            if not all_baselines and baselines:
+                return baselines
+
+        # If it wasn't found in a platform directory, return the expected
+        # result in the test directory, even if no such file actually exists.
+        platform_dir = self.layout_tests_dir()
+        if os.path.exists(os.path.join(platform_dir, baseline_filename)):
+            baselines.append((platform_dir, baseline_filename))
+
+        if baselines:
+            return baselines
+
+        return [(None, baseline_filename)]
+
+    def expected_filename(self, filename, suffix):
+        """Given a test name, returns an absolute path to its expected results.
+
+        If no expected results are found in any of the searched directories,
+        the directory in which the test itself is located will be returned.
+        The return value is in the format appropriate for the platform
+        (e.g., "\\" for path separators on windows).
+
+        Args:
+        filename: absolute filename to test file
+        suffix: file suffix of the expected results, including dot; e.g. '.txt'
+            or '.png'.  This should not be None, but may be an empty string.
+        platform: the most-specific directory name to use to build the
+            search list of directories, e.g., 'chromium-win', or
+            'chromium-mac-leopard' (we follow the WebKit format)
+
+        This routine is generic but is implemented here to live alongside
+        the other baseline and filename manipulation routines.
+        """
+        platform_dir, baseline_filename = self.expected_baselines(
+            filename, suffix)[0]
+        if platform_dir:
+            return os.path.join(platform_dir, baseline_filename)
+        return os.path.join(self.layout_tests_dir(), baseline_filename)
+
+    def filename_to_uri(self, filename):
+        """Convert a test file to a URI."""
+        LAYOUTTEST_HTTP_DIR = "http/tests/"
+        LAYOUTTEST_WEBSOCKET_DIR = "websocket/tests/"
+
+        relative_path = self.relative_test_filename(filename)
+        port = None
+        use_ssl = False
+
+        if relative_path.startswith(LAYOUTTEST_HTTP_DIR):
+            # http/tests/ run off port 8000 and ssl/ off 8443
+            relative_path = relative_path[len(LAYOUTTEST_HTTP_DIR):]
+            port = 8000
+        elif relative_path.startswith(LAYOUTTEST_WEBSOCKET_DIR):
+            # websocket/tests/ run off port 8880 and 9323
+            # Note: the root is /, not websocket/tests/
+            port = 8880
+
+        # Make http/tests/local run as local files. This is to mimic the
+        # logic in run-webkit-tests.
+        #
+        # TODO(dpranke): remove the media reference and the SSL reference?
+        if (port and not relative_path.startswith("local/") and
+            not relative_path.startswith("media/")):
+            if relative_path.startswith("ssl/"):
+                port += 443
+                protocol = "https"
+            else:
+                protocol = "http"
+            return "%s://127.0.0.1:%u/%s" % (protocol, port, relative_path)
+
+        if sys.platform in ('cygwin', 'win32'):
+            return "file:///" + self.get_absolute_path(filename)
+        return "file://" + self.get_absolute_path(filename)
+
+    def get_absolute_path(self, filename):
+        """Return the absolute path in unix format for the given filename.
+
+        This routine exists so that platforms that don't use unix filenames
+        can convert accordingly."""
+        return os.path.abspath(filename)
+
+    def layout_tests_dir(self):
+        """Return the absolute path to the top of the LayoutTests directory."""
+        return self.path_from_webkit_base('LayoutTests')
+
+    def maybe_make_directory(self, *path):
+        """Creates the specified directory if it doesn't already exist."""
+        try:
+            os.makedirs(os.path.join(*path))
+        except OSError, e:
+            if e.errno != errno.EEXIST:
+                raise
+
+    def name(self):
+        """Return the name of the port (e.g., 'mac', 'chromium-win-xp').
+
+        Note that this is different from the test_platform_name(), which
+        may be different (e.g., 'win-xp' instead of 'chromium-win-xp'."""
+        return self._name
+
+    def num_cores(self):
+        """Return the number of cores/cpus available on this machine.
+
+        This routine is used to determine the default amount of parallelism
+        used by run-chromium-webkit-tests."""
+        raise NotImplementedError('Port.num_cores')
+
+    def path_from_webkit_base(self, *comps):
+        """Returns the full path to path made by joining the top of the
+        WebKit source tree and the list of path components in |*comps|."""
+        if not self._webkit_base_dir:
+            abspath = os.path.abspath(__file__)
+            self._webkit_base_dir = abspath[0:abspath.find('WebKitTools')]
+        return os.path.join(self._webkit_base_dir, *comps)
+
+    def remove_directory(self, *path):
+        """Recursively removes a directory, even if it's marked read-only.
+
+        Remove the directory located at *path, if it exists.
+
+        shutil.rmtree() doesn't work on Windows if any of the files
+        or directories are read-only, which svn repositories and
+        some .svn files are.  We need to be able to force the files
+        to be writable (i.e., deletable) as we traverse the tree.
+
+        Even with all this, Windows still sometimes fails to delete a file,
+        citing a permission error (maybe something to do with antivirus
+        scans or disk indexing).  The best suggestion any of the user
+        forums had was to wait a bit and try again, so we do that too.
+        It's hand-waving, but sometimes it works. :/
+        """
+        file_path = os.path.join(*path)
+        if not os.path.exists(file_path):
+            return
+
+        win32 = False
+        if sys.platform == 'win32':
+            win32 = True
+            # Some people don't have the APIs installed. In that case we'll do
+            # without.
+            try:
+                win32api = __import__('win32api')
+                win32con = __import__('win32con')
+            except ImportError:
+                win32 = False
+
+            def remove_with_retry(rmfunc, path):
+                os.chmod(path, stat.S_IWRITE)
+                if win32:
+                    win32api.SetFileAttributes(path,
+                                              win32con.FILE_ATTRIBUTE_NORMAL)
+                try:
+                    return rmfunc(path)
+                except EnvironmentError, e:
+                    if e.errno != errno.EACCES:
+                        raise
+                    print 'Failed to delete %s: trying again' % repr(path)
+                    time.sleep(0.1)
+                    return rmfunc(path)
+        else:
+
+            def remove_with_retry(rmfunc, path):
+                if os.path.islink(path):
+                    return os.remove(path)
+                else:
+                    return rmfunc(path)
+
+        for root, dirs, files in os.walk(file_path, topdown=False):
+            # For POSIX:  making the directory writable guarantees
+            # removability. Windows will ignore the non-read-only
+            # bits in the chmod value.
+            os.chmod(root, 0770)
+            for name in files:
+                remove_with_retry(os.remove, os.path.join(root, name))
+            for name in dirs:
+                remove_with_retry(os.rmdir, os.path.join(root, name))
+
+        remove_with_retry(os.rmdir, file_path)
+
+    def test_platform_name(self):
+        return self._name
+
+    def relative_test_filename(self, filename):
+        """Relative unix-style path for a filename under the LayoutTests
+        directory. Filenames outside the LayoutTests directory should raise
+        an error."""
+        return filename[len(self.layout_tests_dir()) + 1:]
+
+    def results_directory(self):
+        """Absolute path to the place to store the test results."""
+        raise NotImplemented('Port.results_directory')
+
+    def setup_test_run(self):
+        """This routine can be overridden to perform any port-specific
+        work that shouuld be done at the beginning of a test run."""
+        pass
+
+    def show_html_results_file(self, results_filename):
+        """This routine should display the HTML file pointed at by
+        results_filename in a users' browser."""
+        raise NotImplementedError('Port.show_html_results_file')
+
+    def start_driver(self, png_path, options):
+        """Starts a new test Driver and returns a handle to the object."""
+        raise NotImplementedError('Port.start_driver')
+
+    def start_helper(self):
+        """Start a layout test helper if needed on this port. The test helper
+        is used to reconfigure graphics settings and do other things that
+        may be necessary to ensure a known test configuration."""
+        raise NotImplementedError('Port.start_helper')
+
+    def start_http_server(self):
+        """Start a web server if it is available. Do nothing if
+        it isn't. This routine is allowed to (and may) fail if a server
+        is already running."""
+        if self._options.use_apache:
+            self._http_server = apache_http_server.LayoutTestApacheHttpd(self,
+                self._options.results_directory)
+        else:
+            self._http_server = http_server.Lighttpd(self,
+                self._options.results_directory)
+        self._http_server.start()
+
+    def start_websocket_server(self):
+        """Start a websocket server if it is available. Do nothing if
+        it isn't. This routine is allowed to (and may) fail if a server
+        is already running."""
+        self._websocket_server = websocket_server.PyWebSocket(self,
+            self._options.results_directory)
+        self._websocket_server.start()
+
+    def stop_helper(self):
+        """Shut down the test helper if it is running. Do nothing if
+        it isn't, or it isn't available."""
+        raise NotImplementedError('Port.stop_helper')
+
+    def stop_http_server(self):
+        """Shut down the http server if it is running. Do nothing if
+        it isn't, or it isn't available."""
+        if self._http_server:
+            self._http_server.stop()
+
+    def stop_websocket_server(self):
+        """Shut down the websocket server if it is running. Do nothing if
+        it isn't, or it isn't available."""
+        if self._websocket_server:
+            self._websocket_server.stop()
+
+    def test_expectations(self):
+        """Returns the test expectations for this port.
+
+        Basically this string should contain the equivalent of a
+        test_expectations file. See test_expectations.py for more details."""
+        raise NotImplementedError('Port.test_expectations')
+
+    def test_base_platform_names(self):
+        """Return a list of the 'base' platforms on your port. The base
+        platforms represent different architectures, operating systems,
+        or implementations (as opposed to different versions of a single
+        platform). For example, 'mac' and 'win' might be different base
+        platforms, wherease 'mac-tiger' and 'mac-leopard' might be
+        different platforms. This routine is used by the rebaselining tool
+        and the dashboards, and the strings correspond to the identifiers
+        in your test expectations (*not* necessarily the platform names
+        themselves)."""
+        raise NotImplementedError('Port.base_test_platforms')
+
+    def test_platform_name(self):
+        """Returns the string that corresponds to the given platform name
+        in the test expectations. This may be the same as name(), or it
+        may be different. For example, chromium returns 'mac' for
+        'chromium-mac'."""
+        raise NotImplementedError('Port.test_platform_name')
+
+    def test_platforms(self):
+        """Returns the list of test platform identifiers as used in the
+        test_expectations and on dashboards, the rebaselining tool, etc.
+
+        Note that this is not necessarily the same as the list of ports,
+        which must be globally unique (e.g., both 'chromium-mac' and 'mac'
+        might return 'mac' as a test_platform name'."""
+        raise NotImplementedError('Port.platforms')
+
+    def version(self):
+        """Returns a string indicating the version of a given platform, e.g.
+        '-leopard' or '-xp'.
+
+        This is used to help identify the exact port when parsing test
+        expectations, determining search paths, and logging information."""
+        raise NotImplementedError('Port.version')
+
+    def wdiff_text(self, actual_filename, expected_filename):
+        """Returns a string of HTML indicating the word-level diff of the
+        contents of the two filenames. Returns an empty string if word-level
+        diffing isn't available."""
+        executable = self._path_to_wdiff()
+        cmd = [executable,
+               '--start-delete=##WDIFF_DEL##',
+               '--end-delete=##WDIFF_END##',
+               '--start-insert=##WDIFF_ADD##',
+               '--end-insert=##WDIFF_END##',
+               expected_filename,
+               actual_filename]
+        global _wdiff_available
+        result = ''
+        try:
+            # Python's Popen has a bug that causes any pipes opened to a
+            # process that can't be executed to be leaked.  Since this
+            # code is specifically designed to tolerate exec failures
+            # to gracefully handle cases where wdiff is not installed,
+            # the bug results in a massive file descriptor leak. As a
+            # workaround, if an exec failure is ever experienced for
+            # wdiff, assume it's not available.  This will leak one
+            # file descriptor but that's better than leaking each time
+            # wdiff would be run.
+            #
+            # http://mail.python.org/pipermail/python-list/
+            #    2008-August/505753.html
+            # http://bugs.python.org/issue3210
+            #
+            # It also has a threading bug, so we don't output wdiff if
+            # the Popen raises a ValueError.
+            # http://bugs.python.org/issue1236
+            if _wdiff_available:
+                wdiff = subprocess.Popen(cmd,
+                    stdout=subprocess.PIPE).communicate()[0]
+                wdiff = cgi.escape(wdiff)
+                wdiff = wdiff.replace('##WDIFF_DEL##', '<span class=del>')
+                wdiff = wdiff.replace('##WDIFF_ADD##', '<span class=add>')
+                wdiff = wdiff.replace('##WDIFF_END##', '</span>')
+                result = '<head><style>.del { background: #faa; } '
+                result += '.add { background: #afa; }</style></head>'
+                result += '<pre>' + wdiff + '</pre>'
+        except OSError, e:
+            if (e.errno == errno.ENOENT or e.errno == errno.EACCES or
+                e.errno == errno.ECHILD):
+                _wdiff_available = False
+            else:
+                raise e
+        return result
+
+    #
+    # PROTECTED ROUTINES
+    #
+    # The routines below should only be called by routines in this class
+    # or any of its subclasses.
+    #
+
+    def _kill_process(self, pid):
+        """Forcefully kill a process.
+
+        This routine should not be used or needed generically, but can be
+        used in helper files like http_server.py."""
+        raise NotImplementedError('Port.kill_process')
+
+    def _path_to_apache(self):
+        """Returns the full path to the apache binary.
+
+        This is needed only by ports that use the apache_http_server module."""
+        raise NotImplementedError('Port.path_to_apache')
+
+    def _path_to_apache_config_file(self):
+        """Returns the full path to the apache binary.
+
+        This is needed only by ports that use the apache_http_server module."""
+        raise NotImplementedError('Port.path_to_apache_config_file')
+
+    def _path_to_driver(self):
+        """Returns the full path to the test driver (DumpRenderTree)."""
+        raise NotImplementedError('Port.path_to_driver')
+
+    def _path_to_helper(self):
+        """Returns the full path to the layout_test_helper binary, which
+        is used to help configure the system for the test run, or None
+        if no helper is needed.
+
+        This is likely only used by start/stop_helper()."""
+        raise NotImplementedError('Port._path_to_helper')
+
+    def _path_to_image_diff(self):
+        """Returns the full path to the image_diff binary, or None if it
+        is not available.
+
+        This is likely used only by diff_image()"""
+        raise NotImplementedError('Port.path_to_image_diff')
+
+    def _path_to_lighttpd(self):
+        """Returns the path to the LigHTTPd binary.
+
+        This is needed only by ports that use the http_server.py module."""
+        raise NotImplementedError('Port._path_to_lighttpd')
+
+    def _path_to_lighttpd_modules(self):
+        """Returns the path to the LigHTTPd modules directory.
+
+        This is needed only by ports that use the http_server.py module."""
+        raise NotImplementedError('Port._path_to_lighttpd_modules')
+
+    def _path_to_lighttpd_php(self):
+        """Returns the path to the LigHTTPd PHP executable.
+
+        This is needed only by ports that use the http_server.py module."""
+        raise NotImplementedError('Port._path_to_lighttpd_php')
+
+    def _path_to_wdiff(self):
+        """Returns the full path to the wdiff binary, or None if it is
+        not available.
+
+        This is likely used only by wdiff_text()"""
+        raise NotImplementedError('Port._path_to_wdiff')
+
+    def _shut_down_http_server(self, pid):
+        """Forcefully and synchronously kills the web server.
+
+        This routine should only be called from http_server.py or its
+        subclasses."""
+        raise NotImplementedError('Port._shut_down_http_server')
+
+    def _webkit_baseline_path(self, platform):
+        """Return the  full path to the top of the baseline tree for a
+        given platform."""
+        return os.path.join(self.layout_tests_dir(), 'platform',
+                            platform)
+
+
+class Driver:
+    """Abstract interface for the DumpRenderTree interface."""
+
+    def __init__(self, port, png_path, options):
+        """Initialize a Driver to subsequently run tests.
+
+        Typically this routine will spawn DumpRenderTree in a config
+        ready for subsequent input.
+
+        port - reference back to the port object.
+        png_path - an absolute path for the driver to write any image
+            data for a test (as a PNG). If no path is provided, that
+            indicates that pixel test results will not be checked.
+        options - any port-specific driver options."""
+        raise NotImplementedError('Driver.__init__')
+
+    def run_test(self, uri, timeout, checksum):
+        """Run a single test and return the results.
+
+        Note that it is okay if a test times out or crashes and leaves
+        the driver in an indeterminate state. The upper layers of the program
+        are responsible for cleaning up and ensuring things are okay.
+
+        uri - a full URI for the given test
+        timeout - number of milliseconds to wait before aborting this test.
+        checksum - if present, the expected checksum for the image for this
+            test
+
+        Returns a tuple of the following:
+            crash - a boolean indicating whether the driver crashed on the test
+            timeout - a boolean indicating whehter the test timed out
+            checksum - a string containing the checksum of the image, if
+                present
+            output - any text output
+            error - any unexpected or additional (or error) text output
+
+        Note that the image itself should be written to the path that was
+        specified in the __init__() call."""
+        raise NotImplementedError('Driver.run_test')
+
+    def poll(self):
+        """Returns None if the Driver is still running. Returns the returncode
+        if it has exited."""
+        raise NotImplementedError('Driver.poll')
+
+    def returncode(self):
+        """Returns the system-specific returncode if the Driver has stopped or
+        exited."""
+        raise NotImplementedError('Driver.returncode')
+
+    def stop(self):
+        raise NotImplementedError('Driver.stop')
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium.py
new file mode 100644 (file)
index 0000000..00390d0
--- /dev/null
@@ -0,0 +1,259 @@
+#!/usr/bin/env python
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Chromium implementations of the Port interface."""
+
+import logging
+import os
+import signal
+import subprocess
+import sys
+import time
+
+import base
+import http_server
+import websocket_server
+
+
+class ChromiumPort(base.Port):
+    """Abstract base class for Chromium implementations of the Port class."""
+
+    def __init__(self, port_name=None, options=None):
+        base.Port.__init__(self, port_name, options)
+        self._chromium_base_dir = None
+
+    def baseline_path(self):
+        return self._chromium_baseline_path(self._name)
+
+    def check_sys_deps(self):
+        result = True
+        test_shell_binary_path = self._path_to_driver()
+        if os.path.exists(test_shell_binary_path):
+            proc = subprocess.Popen([test_shell_binary_path,
+                                     '--check-layout-test-sys-deps'])
+            if proc.wait() != 0:
+                logging.error("Aborting because system dependencies check "
+                              "failed.")
+                logging.error("To override, invoke with --nocheck-sys-deps")
+                result = False
+        else:
+            logging.error('test driver is not found at %s' %
+                          test_shell_binary_path)
+            result = False
+
+        image_diff_path = self._path_to_image_diff()
+        if (not os.path.exists(image_diff_path) and not
+            self._options.no_pixel_tests):
+            logging.error('image diff not found at %s' % image_diff_path)
+            logging.error("To override, invoke with --no-pixel-tests")
+            result = False
+
+        return result
+
+    def compare_text(self, actual_text, expected_text):
+        return actual_text != expected_text
+
+    def path_from_chromium_base(self, *comps):
+        """Returns the full path to path made by joining the top of the
+        Chromium source tree and the list of path components in |*comps|."""
+        if not self._chromium_base_dir:
+            abspath = os.path.abspath(__file__)
+            self._chromium_base_dir = abspath[0:abspath.find('third_party')]
+        return os.path.join(self._chromium_base_dir, *comps)
+
+    def results_directory(self):
+        return self.path_from_chromium_base('webkit', self._options.target,
+                                            self._options.results_directory)
+
+    def setup_test_run(self):
+        # Delete the disk cache if any to ensure a clean test run.
+        test_shell_binary_path = self._path_to_driver()
+        cachedir = os.path.split(test_shell_binary_path)[0]
+        cachedir = os.path.join(cachedir, "cache")
+        if os.path.exists(cachedir):
+            shutil.rmtree(cachedir)
+
+    def show_results_html_file(self, results_filename):
+        subprocess.Popen([self._path_to_driver(),
+                          self.filename_to_uri(results_filename)])
+
+    def start_driver(self, image_path, options):
+        """Starts a new Driver and returns a handle to it."""
+        return ChromiumDriver(self, image_path, options)
+
+    def start_helper(self):
+        helper_path = self._path_to_helper()
+        if helper_path:
+            logging.debug("Starting layout helper %s" % helper_path)
+            self._helper = subprocess.Popen([helper_path],
+                stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None)
+            is_ready = self._helper.stdout.readline()
+            if not is_ready.startswith('ready'):
+                logging.error("layout_test_helper failed to be ready")
+
+    def stop_helper(self):
+        if self._helper:
+            logging.debug("Stopping layout test helper")
+            self._helper.stdin.write("x\n")
+            self._helper.stdin.close()
+            self._helper.wait()
+
+    def test_base_platform_names(self):
+        return ('linux', 'mac', 'win')
+
+    def test_expectations(self, options=None):
+        """Returns the test expectations for this port.
+
+        Basically this string should contain the equivalent of a
+        test_expectations file. See test_expectations.py for more details."""
+        expectations_file = self.path_from_chromium_base('webkit', 'tools',
+            'layout_tests', 'test_expectations.txt')
+        return file(expectations_file, "r").read()
+
+    def test_platform_names(self):
+        return self.test_base_platform_names() + ('win-xp',
+            'win-vista', 'win-7')
+
+    #
+    # PROTECTED METHODS
+    #
+    # These routines should only be called by other methods in this file
+    # or any subclasses.
+    #
+
+    def _chromium_baseline_path(self, platform):
+        if platform is None:
+            platform = self.name()
+        return self.path_from_chromium_base('webkit', 'data', 'layout_tests',
+            'platform', platform, 'LayoutTests')
+
+
+class ChromiumDriver(base.Driver):
+    """Abstract interface for the DumpRenderTree interface."""
+
+    def __init__(self, port, image_path, options):
+        self._port = port
+        self._options = options
+        self._target = port._options.target
+        self._image_path = image_path
+
+        cmd = []
+        # Hook for injecting valgrind or other runtime instrumentation,
+        # used by e.g. tools/valgrind/valgrind_tests.py.
+        wrapper = os.environ.get("BROWSER_WRAPPER", None)
+        if wrapper != None:
+            cmd += [wrapper]
+        if self._port._options.wrapper:
+            # This split() isn't really what we want -- it incorrectly will
+            # split quoted strings within the wrapper argument -- but in
+            # practice it shouldn't come up and the --help output warns
+            # about it anyway.
+            cmd += self._options.wrapper.split()
+        cmd += [port._path_to_driver(), '--layout-tests']
+        if options:
+            cmd += options
+        self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
+                                      stdout=subprocess.PIPE,
+                                      stderr=subprocess.STDOUT)
+
+    def poll(self):
+        return self._proc.poll()
+
+    def returncode(self):
+        return self._proc.returncode
+
+    def run_test(self, uri, timeoutms, checksum):
+        output = []
+        error = []
+        crash = False
+        timeout = False
+        actual_uri = None
+        actual_checksum = None
+
+        start_time = time.time()
+        cmd = uri
+        if timeoutms:
+            cmd += ' ' + str(timeoutms)
+        if checksum:
+            cmd += ' ' + checksum
+        cmd += "\n"
+
+        self._proc.stdin.write(cmd)
+        line = self._proc.stdout.readline()
+        while line.rstrip() != "#EOF":
+            # Make sure we haven't crashed.
+            if line == '' and self.poll() is not None:
+                # This is hex code 0xc000001d, which is used for abrupt
+                # termination. This happens if we hit ctrl+c from the prompt
+                # and we happen to be waiting on the test_shell.
+                # sdoyon: Not sure for which OS and in what circumstances the
+                # above code is valid. What works for me under Linux to detect
+                # ctrl+c is for the subprocess returncode to be negative
+                # SIGINT. And that agrees with the subprocess documentation.
+                if (-1073741510 == self._proc.returncode or
+                    - signal.SIGINT == self._proc.returncode):
+                    raise KeyboardInterrupt
+                crash = True
+                break
+
+            # Don't include #URL lines in our output
+            if line.startswith("#URL:"):
+                actual_uri = line.rstrip()[5:]
+                if uri != actual_uri:
+                    logging.fatal("Test got out of sync:\n|%s|\n|%s|" %
+                                (uri, actual_uri))
+                    raise AssertionError("test out of sync")
+            elif line.startswith("#MD5:"):
+                actual_checksum = line.rstrip()[5:]
+            elif line.startswith("#TEST_TIMED_OUT"):
+                timeout = True
+                # Test timed out, but we still need to read until #EOF.
+            elif actual_uri:
+                output.append(line)
+            else:
+                error.append(line)
+
+            line = self._proc.stdout.readline()
+
+        return (crash, timeout, actual_checksum, ''.join(output),
+                ''.join(error))
+
+    def stop(self):
+        if self._proc:
+            self._proc.stdin.close()
+            self._proc.stdout.close()
+            if self._proc.stderr:
+                self._proc.stderr.close()
+            if (sys.platform not in ('win32', 'cygwin') and
+                not self._proc.poll()):
+                # Closing stdin/stdout/stderr hangs sometimes on OS X.
+                null = open(os.devnull, "w")
+                subprocess.Popen(["kill", "-9",
+                                 str(self._proc.pid)], stderr=null)
+                null.close()
index 9ffc401..64d1412 100644 (file)
 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-"""This is the Linux implementation of the port
-   package. This file should only be imported by that package."""
+"""Chromium Mac implementation of the Port interface."""
 
 import os
+import platform
 import signal
 import subprocess
-import sys
-import logging
 
-import chromium_win
-import path_utils
+import chromium
 
-
-def platform_name():
-    """Returns the name of the platform we're currently running on."""
-    return 'chromium-linux' + platform_version()
-
-
-def platform_version():
-    """Returns the version string for the platform, e.g. '-vista' or
-    '-snowleopard'. If the platform does not distinguish between
-    minor versions, it returns ''."""
-    return ''
-
-
-def get_num_cores():
-    """Returns the number of cores on the machine. For hyperthreaded machines,
-    this will be double the number of actual processors."""
-    num_cores = os.sysconf("SC_NPROCESSORS_ONLN")
-    if isinstance(num_cores, int) and num_cores > 0:
-        return num_cores
-    return 1
-
-
-def baseline_path(platform=None):
-    """Returns the path relative to the top of the source tree for the
-    baselines for the specified platform version. If |platform| is None,
-    then the version currently in use is used."""
-    if platform is None:
-        platform = platform_name()
-    return path_utils.path_from_base('webkit', 'data', 'layout_tests',
-                                     'platform', platform, 'LayoutTests')
-
-
-def baseline_search_path(platform=None):
-    """Returns the list of directories to search for baselines/results, in
-    order of preference. Paths are relative to the top of the source tree."""
-    return [baseline_path(platform),
-            chromium_win.baseline_path('chromium-win'),
-            path_utils.webkit_baseline_path('win'),
-            path_utils.webkit_baseline_path('mac')]
-
-
-def apache_executable_path():
-    """Returns the executable path to start Apache"""
-    path = os.path.join("/usr", "sbin", "apache2")
-    if os.path.exists(path):
-        return path
-    print "Unable to fine Apache executable %s" % path
-    _missing_apache()
-
-
-def apache_config_file_path():
-    """Returns the path to Apache config file"""
-    return path_utils.path_from_base("third_party", "WebKit", "LayoutTests",
-        "http", "conf", "apache2-debian-httpd.conf")
-
-
-def lighttpd_executable_path():
-    """Returns the executable path to start LigHTTPd"""
-    binpath = "/usr/sbin/lighttpd"
-    if os.path.exists(binpath):
-        return binpath
-    print "Unable to find LigHTTPd executable %s" % binpath
-    _missing_lighttpd()
-
-
-def lighttpd_module_path():
-    """Returns the library module path for LigHTTPd"""
-    modpath = "/usr/lib/lighttpd"
-    if os.path.exists(modpath):
-        return modpath
-    print "Unable to find LigHTTPd modules %s" % modpath
-    _missing_lighttpd()
-
-
-def lighttpd_php_path():
-    """Returns the PHP executable path for LigHTTPd"""
-    binpath = "/usr/bin/php-cgi"
-    if os.path.exists(binpath):
-        return binpath
-    print "Unable to find PHP CGI executable %s" % binpath
-    _missing_lighttpd()
-
-
-def wdiff_path():
-    """Path to the WDiff executable, which we assume is already installed and
-    in the user's $PATH."""
-    return 'wdiff'
-
-
-def image_diff_path(target):
-    """Path to the image_diff binary.
-
-    Args:
-      target: Build target mode (debug or release)"""
-    return _path_from_build_results(target, 'image_diff')
-
-
-def layout_test_helper_path(target):
-    """Path to the layout_test helper binary, if needed, empty otherwise"""
-    return ''
-
-
-def test_shell_path(target):
-    """Return the platform-specific binary path for our TestShell.
-
-    Args:
-      target: Build target mode (debug or release) """
-    if target in ('Debug', 'Release'):
-        try:
-            debug_path = _path_from_build_results('Debug', 'test_shell')
-            release_path = _path_from_build_results('Release', 'test_shell')
-
-            debug_mtime = os.stat(debug_path).st_mtime
-            release_mtime = os.stat(release_path).st_mtime
-
-            if debug_mtime > release_mtime and target == 'Release' or \
-               release_mtime > debug_mtime and target == 'Debug':
-                logging.info('\x1b[31mWarning: you are not running the most '
-                             'recent test_shell binary. You need to pass '
-                             '--debug or not to select between Debug and '
-                             'Release.\x1b[0m')
-        # This will fail if we don't have both a debug and release binary.
-        # That's fine because, in this case, we must already be running the
-        # most up-to-date one.
-        except path_utils.PathNotFound:
-            pass
-
-    return _path_from_build_results(target, 'test_shell')
-
-
-def fuzzy_match_path():
-    """Return the path to the fuzzy matcher binary."""
-    return path_utils.path_from_base('third_party', 'fuzzymatch', 'fuzzymatch')
-
-
-def shut_down_http_server(server_pid):
-    """Shut down the lighttpd web server. Blocks until it's fully shut down.
-
-    Args:
-      server_pid: The process ID of the running server.
-    """
-    # server_pid is not set when "http_server.py stop" is run manually.
-    if server_pid is None:
-        # This isn't ideal, since it could conflict with web server processes
-        # not started by http_server.py, but good enough for now.
-        kill_all_process('lighttpd')
-        kill_all_process('apache2')
-    else:
-        try:
-            os.kill(server_pid, signal.SIGTERM)
-            #TODO(mmoss) Maybe throw in a SIGKILL just to be sure?
-        except OSError:
-            # Sometimes we get a bad PID (e.g. from a stale httpd.pid file),
-            # so if kill fails on the given PID, just try to 'killall' web
-            # servers.
-            shut_down_http_server(None)
-
-
-def kill_process(pid):
-    """Forcefully kill the process.
-
-    Args:
-      pid: The id of the process to be killed.
-    """
-    os.kill(pid, signal.SIGKILL)
-
-
-def kill_all_process(process_name):
-    null = open(os.devnull)
-    subprocess.call(['killall', '-TERM', '-u', os.getenv('USER'),
-                    process_name], stderr=null)
-    null.close()
-
-
-def kill_all_test_shells():
-    """Kills all instances of the test_shell binary currently running."""
-    kill_all_process('test_shell')
-
-#
-# Private helper functions
-#
-
-
-def _missing_lighttpd():
-    print 'Please install using: "sudo apt-get install lighttpd php5-cgi"'
-    print 'For complete Linux build requirements, please see:'
-    print 'http://code.google.com/p/chromium/wiki/LinuxBuildInstructions'
-    sys.exit(1)
-
-
-def _missing_apache():
-    print ('Please install using: "sudo apt-get install apache2 '
-        'libapache2-mod-php5"')
-    print 'For complete Linux build requirements, please see:'
-    print 'http://code.google.com/p/chromium/wiki/LinuxBuildInstructions'
-    sys.exit(1)
-
-
-def _path_from_build_results(*pathies):
-    # FIXME(dkegel): use latest or warn if more than one found?
-    for dir in ["sconsbuild", "out", "xcodebuild"]:
-        try:
-            return path_utils.path_from_base(dir, *pathies)
-        except:
-            pass
-    raise path_utils.PathNotFound("Unable to find %s in build tree" %
-        (os.path.join(*pathies)))
+
+class ChromiumLinuxPort(chromium.ChromiumPort):
+    """Chromium Linux implementation of the Port class."""
+
+    def __init__(self, port_name=None, options=None):
+        if port_name is None:
+            port_name = 'chromium-linux'
+        chromium.ChromiumPort.__init__(self, port_name, options)
+
+    def baseline_search_path(self):
+        return [self.baseline_path(),
+                self.chromium_baseline_path('chromium-win'),
+                self.webkit_baseline_path('win'),
+                self.webkit_baseline_path('mac')]
+
+    def check_sys_deps(self):
+        # We have no platform-specific dependencies to check.
+        return True
+
+    def num_cores(self):
+        num_cores = os.sysconf("SC_NPROCESSORS_ONLN")
+        if isinstance(num_cores, int) and num_cores > 0:
+            return num_cores
+        return 1
+
+    def test_platform_name(self):
+        # We use 'linux' instead of 'chromium-linux' in test_expectations.txt.
+        return 'linux'
+
+    def version(self):
+        # We don't have different versions on linux.
+        return ''
+
+    #
+    # PROTECTED METHODS
+    #
+
+    def _build_path(self, *comps):
+        return self.path_from_chromium_base('sconsbuild', self._options.target,
+                                            *comps)
+
+    def _kill_process(self, pid):
+        """Forcefully kill the process.
+
+        Args:
+        pid: The id of the process to be killed.
+        """
+        os.kill(pid, signal.SIGKILL)
+
+    def _kill_all_process(self, process_name):
+        null = open(os.devnull)
+        subprocess.call(['killall', '-TERM', '-u', os.getenv('USER'),
+                        process_name], stderr=null)
+        null.close()
+
+    def _path_to_apache(self):
+        return '/usr/sbin/apache2'
+
+    def _path_to_apache_config_file(self):
+        return os.path.join(self.layout_tests_dir(), 'http', 'conf',
+                            'apache2-debian-httpd.conf')
+
+    def _path_to_lighttpd(self):
+        return "/usr/sbin/lighttpd"
+
+    def _path_to_lighttpd_modules(self):
+        return "/usr/lib/lighttpd"
+
+    def _path_to_lighttpd_php(self):
+        return "/usr/bin/php-cgi"
+
+    def _path_to_driver(self):
+        return self._build_path('test_shell')
+
+    def _path_to_helper(self):
+        return None
+
+    def _path_to_image_diff(self):
+        return self._build_path('image_diff')
+
+    def _path_to_wdiff(self):
+        return 'wdiff'
+
+    def _shut_down_http_server(self, server_pid):
+        """Shut down the lighttpd web server. Blocks until it's fully
+        shut down.
+
+        Args:
+            server_pid: The process ID of the running server.
+        """
+        # server_pid is not set when "http_server.py stop" is run manually.
+        if server_pid is None:
+            # TODO(mmoss) This isn't ideal, since it could conflict with
+            # lighttpd processes not started by http_server.py,
+            # but good enough for now.
+            self._kill_all_process('lighttpd')
+            self._kill_all_process('apache2')
+        else:
+            try:
+                os.kill(server_pid, signal.SIGTERM)
+                # TODO(mmoss) Maybe throw in a SIGKILL just to be sure?
+            except OSError:
+                # Sometimes we get a bad PID (e.g. from a stale httpd.pid
+                # file), so if kill fails on the given PID, just try to
+                # 'killall' web servers.
+                self._shut_down_http_server(None)
index d0fbc01..7e7b4ca 100644 (file)
 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-"""This is the Mac implementation of the port interface
-   package. This file should only be imported by that package."""
+"""Chromium Mac implementation of the Port interface."""
 
 import os
 import platform
 import signal
 import subprocess
 
-import path_utils
-
-
-def platform_name():
-    """Returns the name of the platform we're currently running on."""
-    # At the moment all chromium mac results are version-independent. At some
-    # point we may need to return 'chromium-mac' + PlatformVersion()
-    return 'chromium-mac'
-
-
-def platform_version():
-    """Returns the version string for the platform, e.g. '-vista' or
-    '-snowleopard'. If the platform does not distinguish between
-    minor versions, it returns ''."""
-    os_version_string = platform.mac_ver()[0]  # e.g. "10.5.6"
-    if not os_version_string:
-        return '-leopard'
-
-    release_version = int(os_version_string.split('.')[1])
-
-    # we don't support 'tiger' or earlier releases
-    if release_version == 5:
-        return '-leopard'
-    elif release_version == 6:
-        return '-snowleopard'
-
-    return ''
-
-
-def get_num_cores():
-    """Returns the number of cores on the machine. For hyperthreaded machines,
-    this will be double the number of actual processors."""
-    return int(os.popen2("sysctl -n hw.ncpu")[1].read())
-
-
-def baseline_path(platform=None):
-    """Returns the path relative to the top of the source tree for the
-    baselines for the specified platform version. If |platform| is None,
-    then the version currently in use is used."""
-    if platform is None:
-        platform = platform_name()
-    return path_utils.path_from_base('webkit', 'data', 'layout_tests',
-                                     'platform', platform, 'LayoutTests')
-
-# TODO: We should add leopard and snowleopard to the list of paths to check
-# once we start running the tests from snowleopard.
-
-
-def baseline_search_path(platform=None):
-    """Returns the list of directories to search for baselines/results, in
-    order of preference. Paths are relative to the top of the source tree."""
-    return [baseline_path(platform),
-            path_utils.webkit_baseline_path('mac' + platform_version()),
-            path_utils.webkit_baseline_path('mac')]
-
-
-def wdiff_path():
-    """Path to the WDiff executable, which we assume is already installed and
-    in the user's $PATH."""
-    return 'wdiff'
-
-
-def image_diff_path(target):
-    """Path to the image_diff executable
-
-    Args:
-      target: build type - 'Debug','Release',etc."""
-    return path_utils.path_from_base('xcodebuild', target, 'image_diff')
-
-
-def layout_test_helper_path(target):
-    """Path to the layout_test_helper executable, if needed, empty otherwise
-
-    Args:
-      target: build type - 'Debug','Release',etc."""
-    return path_utils.path_from_base('xcodebuild', target,
-                                     'layout_test_helper')
-
-
-def test_shell_path(target):
-    """Path to the test_shell executable.
-
-    Args:
-      target: build type - 'Debug','Release',etc."""
-    # TODO(pinkerton): make |target| happy with case-sensitive file systems.
-    return path_utils.path_from_base('xcodebuild', target, 'TestShell.app',
-                                     'Contents', 'MacOS', 'TestShell')
-
-
-def apache_executable_path():
-    """Returns the executable path to start Apache"""
-    return os.path.join("/usr", "sbin", "httpd")
-
-
-def apache_config_file_path():
-    """Returns the path to Apache config file"""
-    return path_utils.path_from_base("third_party", "WebKit", "LayoutTests",
-        "http", "conf", "apache2-httpd.conf")
-
-
-def lighttpd_executable_path():
-    """Returns the executable path to start LigHTTPd"""
-    return path_utils.path_from_base('third_party', 'lighttpd', 'mac',
-                                     'bin', 'lighttpd')
-
-
-def lighttpd_module_path():
-    """Returns the library module path for LigHTTPd"""
-    return path_utils.path_from_base('third_party', 'lighttpd', 'mac', 'lib')
-
-
-def lighttpd_php_path():
-    """Returns the PHP executable path for LigHTTPd"""
-    return path_utils.path_from_base('third_party', 'lighttpd', 'mac', 'bin',
-                                     'php-cgi')
-
-
-def shut_down_http_server(server_pid):
-    """Shut down the lighttpd web server. Blocks until it's fully shut down.
-
-      Args:
-        server_pid: The process ID of the running server.
-    """
-    # server_pid is not set when "http_server.py stop" is run manually.
-    if server_pid is None:
-        # TODO(mmoss) This isn't ideal, since it could conflict with lighttpd
-        # processes not started by http_server.py, but good enough for now.
-        kill_all_process('lighttpd')
-        kill_all_process('httpd')
-    else:
-        try:
-            os.kill(server_pid, signal.SIGTERM)
-            # TODO(mmoss) Maybe throw in a SIGKILL just to be sure?
-        except OSError:
-            # Sometimes we get a bad PID (e.g. from a stale httpd.pid file),
-            # so if kill fails on the given PID, just try to 'killall' web
-            # servers.
-            shut_down_http_server(None)
-
-
-def kill_process(pid):
-    """Forcefully kill the process.
-
-    Args:
-      pid: The id of the process to be killed.
-    """
-    os.kill(pid, signal.SIGKILL)
-
-
-def kill_all_process(process_name):
-    # On Mac OS X 10.6, killall has a new constraint: -SIGNALNAME or
-    # -SIGNALNUMBER must come first.  Example problem:
-    #   $ killall -u $USER -TERM lighttpd
-    #   killall: illegal option -- T
-    # Use of the earlier -TERM placement is just fine on 10.5.
-    null = open(os.devnull)
-    subprocess.call(['killall', '-TERM', '-u', os.getenv('USER'),
-                     process_name], stderr=null)
-    null.close()
-
-
-def kill_all_test_shells():
-    """Kills all instances of the test_shell binary currently running."""
-    kill_all_process('TestShell')
+import chromium
+
+
+class ChromiumMacPort(chromium.ChromiumPort):
+    """Chromium Mac implementation of the Port class."""
+
+    def __init__(self, port_name=None, options=None):
+        if port_name is None:
+            port_name = 'chromium-mac'
+        chromium.ChromiumPort.__init__(self, port_name, options)
+
+    def baseline_search_path(self):
+        return [self.baseline_path(),
+                self._webkit_baseline_path('mac' + self.version()),
+                self._webkit_baseline_path('mac')]
+
+    def check_sys_deps(self):
+        # We have no specific platform dependencies.
+        return True
+
+    def num_cores(self):
+        return int(subprocess.Popen(['sysctl','-n','hw.ncpu'],
+                                    stdout=subprocess.PIPE).stdout.read())
+
+    def test_platform_name(self):
+        # We use 'mac' instead of 'chromium-mac'
+        return 'mac'
+
+    def version(self):
+        os_version_string = platform.mac_ver()[0]  # e.g. "10.5.6"
+        if not os_version_string:
+            return '-leopard'
+        release_version = int(os_version_string.split('.')[1])
+        # we don't support 'tiger' or earlier releases
+        if release_version == 5:
+            return '-leopard'
+        elif release_version == 6:
+            return '-snowleopard'
+        return ''
+
+    #
+    # PROTECTED METHODS
+    #
+
+    def _build_path(self, *comps):
+        return self.path_from_chromium_base('xcodebuild', self._options.target,
+                                            *comps)
+
+    def _lighttpd_path(self, *comps):
+        return self.path_from_chromium_base('third_party', 'lighttpd',
+                                            'mac', *comps)
+
+    def _kill_process(self, pid):
+        """Forcefully kill the process.
+
+        Args:
+            pid: The id of the process to be killed.
+        """
+        os.kill(pid, signal.SIGKILL)
+
+    def _kill_all_process(self, process_name):
+        """Kill any processes running under this name."""
+        # On Mac OS X 10.6, killall has a new constraint: -SIGNALNAME or
+        # -SIGNALNUMBER must come first.  Example problem:
+        #   $ killall -u $USER -TERM lighttpd
+        #   killall: illegal option -- T
+        # Use of the earlier -TERM placement is just fine on 10.5.
+        null = open(os.devnull)
+        subprocess.call(['killall', '-TERM', '-u', os.getenv('USER'),
+                        process_name], stderr=null)
+        null.close()
+
+    def _path_to_apache(self):
+        return '/usr/sbin/httpd'
+
+    def _path_to_apache_config_file(self):
+        return os.path.join(self.layout_tests_dir(), 'http', 'conf',
+                            'apache2-httpd.conf')
+
+    def _path_to_lighttpd(self):
+        return self._lighttp_path('bin', 'lighttp')
+
+    def _path_to_lighttpd_modules(self):
+        return self._lighttp_path('lib')
+
+    def _path_to_lighttpd_php(self):
+        return self._lighttpd_path('bin', 'php-cgi')
+
+    def _path_to_driver(self):
+        # TODO(pinkerton): make |target| happy with case-sensitive file
+        # systems.
+        return self._build_path('TestShell.app', 'Contents', 'MacOS', 
+                                'TestShell')
+
+    def _path_to_helper(self):
+        return self._build_path('layout_test_helper')
+
+    def _path_to_image_diff(self):
+        return self._build_path('image_diff')
+
+    def _path_to_wdiff(self):
+        return 'wdiff'
+
+    def _shut_down_http_server(self, server_pid):
+        """Shut down the lighttpd web server. Blocks until it's fully
+        shut down.
+
+        Args:
+            server_pid: The process ID of the running server.
+        """
+        # server_pid is not set when "http_server.py stop" is run manually.
+        if server_pid is None:
+            # TODO(mmoss) This isn't ideal, since it could conflict with
+            # lighttpd processes not started by http_server.py,
+            # but good enough for now.
+            self._kill_all_process('lighttpd')
+            self._kill_all_process('httpd')
+        else:
+            try:
+                os.kill(server_pid, signal.SIGTERM)
+                # TODO(mmoss) Maybe throw in a SIGKILL just to be sure?
+            except OSError:
+                # Sometimes we get a bad PID (e.g. from a stale httpd.pid
+                # file), so if kill fails on the given PID, just try to
+                # 'killall' web servers.
+                self._shut_down_http_server(None)
index 1e0b212..f79932d 100644 (file)
 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-"""This is the Linux implementation of the port
-   package. This file should only be imported by that package."""
+"""Chromium Win implementation of the Port interface."""
 
 import os
+import platform
+import signal
 import subprocess
-import sys
-
-import path_utils
 
-def platform_name():
-    """Returns the name of the platform we're currently running on."""
-    # We're not ready for version-specific results yet. When we uncomment
-    # this, we also need to add it to the BaselineSearchPath()
-    return 'chromium-win' + platform_version()
+import chromium
 
 
-def platform_version():
-    """Returns the version string for the platform, e.g. '-vista' or
-    '-snowleopard'. If the platform does not distinguish between
-    minor versions, it returns ''."""
-    winver = sys.getwindowsversion()
-    if winver[0] == 6 and (winver[1] == 1):
-        return '-7'
-    if winver[0] == 6 and (winver[1] == 0):
-        return '-vista'
-    if winver[0] == 5 and (winver[1] == 1 or winver[1] == 2):
-        return '-xp'
-    return ''
-
-
-def get_num_cores():
-    """Returns the number of cores on the machine. For hyperthreaded machines,
-    this will be double the number of actual processors."""
-    return int(os.environ.get('NUMBER_OF_PROCESSORS', 1))
-
-
-def baseline_path(platform=None):
-    """Returns the path relative to the top of the source tree for the
-    baselines for the specified platform version. If |platform| is None,
-    then the version currently in use is used."""
-    if platform is None:
-        platform = platform_name()
-    return path_utils.path_from_base('webkit', 'data', 'layout_tests',
-                                     'platform', platform, 'LayoutTests')
-
-
-def baseline_search_path(platform=None):
-    """Returns the list of directories to search for baselines/results, in
-    order of preference. Paths are relative to the top of the source tree."""
-    dirs = []
-    if platform is None:
-        platform = platform_name()
-
-    if platform == 'chromium-win-xp':
-        dirs.append(baseline_path(platform))
-    if platform in ('chromium-win-xp', 'chromium-win-vista'):
-        dirs.append(baseline_path('chromium-win-vista'))
-    dirs.append(baseline_path('chromium-win'))
-    dirs.append(path_utils.webkit_baseline_path('win'))
-    dirs.append(path_utils.webkit_baseline_path('mac'))
-    return dirs
-
-
-def wdiff_path():
-    """Path to the WDiff executable, whose binary is checked in on Win"""
-    return path_utils.path_from_base('third_party', 'cygwin', 'bin',
-                                     'wdiff.exe')
-
-
-def image_diff_path(target):
-    """Return the platform-specific binary path for the image compare util.
-         We use this if we can't find the binary in the default location
-         in path_utils.
-
-    Args:
-      target: Build target mode (debug or release)
-    """
-    return _find_binary(target, 'image_diff.exe')
-
-
-def layout_test_helper_path(target):
-    """Return the platform-specific binary path for the layout test helper.
-    We use this if we can't find the binary in the default location
-    in path_utils.
-
-    Args:
-      target: Build target mode (debug or release)
-    """
-    return _find_binary(target, 'layout_test_helper.exe')
-
-
-def test_shell_path(target):
-    """Return the platform-specific binary path for our TestShell.
-       We use this if we can't find the binary in the default location
-       in path_utils.
-
-    Args:
-      target: Build target mode (debug or release)
-    """
-    return _find_binary(target, 'test_shell.exe')
-
-
-def apache_executable_path():
-    """Returns the executable path to start Apache"""
-    path = path_utils.path_from_base('third_party', 'cygwin', "usr", "sbin")
-    # Don't return httpd.exe since we want to use this from cygwin.
-    return os.path.join(path, "httpd")
-
-
-def apache_config_file_path():
-    """Returns the path to Apache config file"""
-    return path_utils.path_from_base("third_party", "WebKit", "LayoutTests",
-        "http", "conf", "cygwin-httpd.conf")
-
-
-def lighttpd_executable_path():
-    """Returns the executable path to start LigHTTPd"""
-    return path_utils.path_from_base('third_party', 'lighttpd', 'win',
-                                     'LightTPD.exe')
-
-
-def lighttpd_module_path():
-    """Returns the library module path for LigHTTPd"""
-    return path_utils.path_from_base('third_party', 'lighttpd', 'win', 'lib')
-
-
-def lighttpd_php_path():
-    """Returns the PHP executable path for LigHTTPd"""
-    return path_utils.path_from_base('third_party', 'lighttpd', 'win', 'php5',
-                                     'php-cgi.exe')
-
-
-def shut_down_http_server(server_pid):
-    """Shut down the lighttpd web server. Blocks until it's fully shut down.
-
-    Args:
-      server_pid: The process ID of the running server.
-          Unused in this implementation of the method.
-    """
-    subprocess.Popen(('taskkill.exe', '/f', '/im', 'LightTPD.exe'),
-                     stdout=subprocess.PIPE,
-                     stderr=subprocess.PIPE).wait()
-    subprocess.Popen(('taskkill.exe', '/f', '/im', 'httpd.exe'),
-                     stdout=subprocess.PIPE,
-                     stderr=subprocess.PIPE).wait()
-
-
-def kill_process(pid):
-    """Forcefully kill the process.
-
-    Args:
-      pid: The id of the process to be killed.
-    """
-    subprocess.call(('taskkill.exe', '/f', '/pid', str(pid)),
-                    stdout=subprocess.PIPE,
-                    stderr=subprocess.PIPE)
-
-
-def kill_all_test_shells(self):
-    """Kills all instances of the test_shell binary currently running."""
-    subprocess.Popen(('taskkill.exe', '/f', '/im', 'test_shell.exe'),
-                     stdout=subprocess.PIPE,
-                     stderr=subprocess.PIPE).wait()
-
-#
-# Private helper functions.
-#
-
-
-def _find_binary(target, binary):
-    """On Windows, we look for binaries that we compile in potentially
-    two places: src/webkit/$target (preferably, which we get if we
-    built using webkit_glue.gyp), or src/chrome/$target (if compiled some
-    other way)."""
-    try:
-        return path_utils.path_from_base('webkit', target, binary)
-    except path_utils.PathNotFound:
-        try:
-            return path_utils.path_from_base('chrome', target, binary)
-        except path_utils.PathNotFound:
-            return path_utils.path_from_base('build', target, binary)
+class ChromiumWinPort(chromium.ChromiumPort):
+    """Chromium Win implementation of the Port class."""
+
+    def __init__(self, port_name=None, options=None):
+        if port_name is None:
+            port_name = 'chromium-win' + self.version()
+        chromium.ChromiumPort.__init__(self, port_name, options)
+
+    def baseline_search_path(self):
+        if self._name == 'chromium-win-xp':
+            dirs.append(self.baseline_path(self._name))
+        if self._name in ('chromium-win-xp', 'chromium-win-vista'):
+            dirs.append(self.baseline_path('chromium-win-vista'))
+        dirs.append(self.baseline_path('chromium-win'))
+        dirs.append(self.webkit_baseline_path('win'))
+        dirs.append(self.webkit_baseline_path('mac'))
+        return dirs
+
+    def check_sys_deps(self):
+        # TODO(dpranke): implement this
+        return True
+
+    def get_absolute_path(self, filename):
+        """Return the absolute path in unix format for the given filename."""
+        abspath = os.path.abspath(filename)
+        return path.replace('\\', '/')
+
+    def num_cores(self):
+        return int(os.environ.get('NUMBER_OF_PROCESSORS', 1))
+
+    def test_platform_name(self):
+        # We return 'win-xp', not 'chromium-win-xp' here, for convenience.
+        return 'win' + self.version()
+
+    def version(self):
+        winver = sys.getwindowsversion()
+        if winver[0] == 6 and (winver[1] == 1):
+            return '-7'
+        if winver[0] == 6 and (winver[1] == 0):
+            return '-vista'
+        if winver[0] == 5 and (winver[1] == 1 or winver[1] == 2):
+            return '-xp'
+        return ''
+
+    #
+    # PROTECTED ROUTINES
+    #
+
+    def _build_path(self, *comps):
+        # FIXME(dpranke): allow for builds under 'chrome' as well.
+        return self.path_from_chromium_base('webkit', self._options.target,
+                                            *comps)
+
+    def _lighttpd_path(self, *comps):
+        return self.path_from_chromium_base('third_party', 'lighttpd', 'win',
+                                            *comps)
+
+    def _kill_process(self, pid):
+        """Forcefully kill the process.
+
+        Args:
+        pid: The id of the process to be killed.
+        """
+        subprocess.call(('taskkill.exe', '/f', '/pid', str(pid)),
+                        stdout=subprocess.PIPE,
+                        stderr=subprocess.PIPE)
+
+    def _path_to_apache(self):
+        return self.path_from_chromium_base('third_party', 'cygwin', 'usr',
+                                            'sbin', 'httpd')
+
+    def _path_to_apache_config_file(self):
+        return os.path.join(self.layout_tests_dir(), 'http', 'conf',
+                            'cygwin-httpd.conf')
+
+    def _path_to_lighttpd(self):
+        return self._lighttp_path('LightTPD.exe')
+
+    def _path_to_lighttpd_modules(self):
+        return self._lighttp_path('lib')
+
+    def _path_to_lighttpd_php(self):
+        return self._lighttp_path('php5', 'php-cgi.exe')
+
+    def _path_to_driver(self):
+        return self._build_path('test_shell.exe')
+
+    def _path_to_helper(self):
+        return self._build_path('layout_test_helper.exe')
+
+    def _path_to_image_diff(self):
+        return self._build_path('layout_test_helper.exe')
+
+    def _path_to_wdiff(self):
+        return self.path_from_chromium_base('third_party', 'cygwin', 'bin',
+                                            'wdiff.exe')
+
+    def _shut_down_http_server(self, server_pid):
+        """Shut down the lighttpd web server. Blocks until it's fully
+        shut down.
+
+        Args:
+            server_pid: The process ID of the running server.
+        """
+        subprocess.Popen(('taskkill.exe', '/f', '/im', 'LightTPD.exe'),
+                        stdout=subprocess.PIPE,
+                        stderr=subprocess.PIPE).wait()
+        subprocess.Popen(('taskkill.exe', '/f', '/im', 'httpd.exe'),
+                        stdout=subprocess.PIPE,
+                        stderr=subprocess.PIPE).wait()
index 99e2ea1..b24b9c0 100755 (executable)
@@ -41,52 +41,21 @@ import time
 import urllib
 
 import http_server_base
-import path_utils
 
-class HttpdNotStarted(Exception): pass
 
-def remove_log_files(folder, starts_with):
-    files = os.listdir(folder)
-    for file in files:
-        if file.startswith(starts_with):
-            full_path = os.path.join(folder, file)
-            os.remove(full_path)
+class HttpdNotStarted(Exception):
+    pass
 
 
 class Lighttpd(http_server_base.HttpServerBase):
-    # Webkit tests
-    try:
-        _webkit_tests = path_utils.path_from_base('third_party', 'WebKit',
-                                                  'LayoutTests', 'http',
-                                                  'tests')
-        _js_test_resource = path_utils.path_from_base('third_party', 'WebKit',
-                                                      'LayoutTests', 'fast',
-                                                      'js', 'resources')
-    except path_utils.PathNotFound:
-        _webkit_tests = None
-        _js_test_resource = None
-
-    # Path where we can access all of the tests
-    _all_tests = path_utils.path_from_base('webkit', 'data', 'layout_tests')
-    # Self generated certificate for SSL server (for client cert get
-    # <base-path>\chrome\test\data\ssl\certs\root_ca_cert.crt)
-    _pem_file = path_utils.path_from_base(
-        os.path.dirname(os.path.abspath(__file__)), 'httpd2.pem')
-    # One mapping where we can get to everything
-    VIRTUALCONFIG = [{'port': 8081, 'docroot': _all_tests}]
-
-    if _webkit_tests:
-        VIRTUALCONFIG.extend(
-          # Three mappings (one with SSL enabled) for LayoutTests http tests
-          [{'port': 8000, 'docroot': _webkit_tests},
-           {'port': 8080, 'docroot': _webkit_tests},
-           {'port': 8443, 'docroot': _webkit_tests, 'sslcert': _pem_file}])
-
-    def __init__(self, output_dir, background=False, port=None,
+
+    def __init__(self, port_obj, output_dir, background=False, port=None,
                  root=None, register_cygwin=None, run_background=None):
         """Args:
           output_dir: the absolute path to the layout test result directory
         """
+        # Webkit tests
+        http_server_base.HttpServerBase.__init__(self, port_obj)
         self._output_dir = output_dir
         self._process = None
         self._port = port
@@ -96,6 +65,31 @@ class Lighttpd(http_server_base.HttpServerBase):
         if self._port:
             self._port = int(self._port)
 
+        try:
+            _webkit_tests = os.path.join(self._port_obj.layout_tests_dir(),
+                 'http', 'tests')
+            _webkit_tests = os.path.join(self._port_obj.layout_tests_dir(),
+                 'fast', 'js', 'resources')
+        except:
+            _webkit_tests = None
+            _js_test_resource = None
+
+        # Self generated certificate for SSL server (for client cert get
+        # <base-path>\chrome\test\data\ssl\certs\root_ca_cert.crt)
+        self._pem_file = os.path.join(
+            os.path.dirname(os.path.abspath(__file__)), 'httpd2.pem')
+
+        # One mapping where we can get to everything
+        VIRTUALCONFIG = []
+
+        if _webkit_tests:
+            VIRTUALCONFIG.extend(
+               # Three mappings (one with SSL) for LayoutTests http tests
+               [{'port': 8000, 'docroot': _webkit_tests},
+                {'port': 8080, 'docroot': _webkit_tests},
+                {'port': 8443, 'docroot': _webkit_tests,
+                 'sslcert': self._pem_file}])
+
     def is_running(self):
         return self._process != None
 
@@ -103,9 +97,9 @@ class Lighttpd(http_server_base.HttpServerBase):
         if self.is_running():
             raise 'Lighttpd already running'
 
-        base_conf_file = path_utils.path_from_base('third_party',
+        base_conf_file = self._port_obj.path_from_base('third_party',
             'WebKitTools', 'Scripts', 'webkitpy', 'layout_tests',
-            'layout_package', 'lighttpd.conf')
+            'port', 'lighttpd.conf')
         out_conf_file = os.path.join(self._output_dir, 'lighttpd.conf')
         time_str = time.strftime("%d%b%Y-%H%M%S")
         access_file_name = "access.log-" + time_str + ".txt"
@@ -114,8 +108,8 @@ class Lighttpd(http_server_base.HttpServerBase):
         error_log = os.path.join(self._output_dir, log_file_name)
 
         # Remove old log files. We only need to keep the last ones.
-        remove_log_files(self._output_dir, "access.log-")
-        remove_log_files(self._output_dir, "error.log-")
+        self.remove_log_files(self._output_dir, "access.log-")
+        self.remove_log_files(self._output_dir, "error.log-")
 
         # Write out the config
         f = file(base_conf_file, 'rb')
@@ -132,7 +126,7 @@ class Lighttpd(http_server_base.HttpServerBase):
                  '               ".pl"   => "/usr/bin/env",\n'
                  '               ".asis" => "/bin/cat",\n'
                  '               ".php"  => "%s" )\n\n') %
-                                     path_utils.lighttpd_php_path())
+                                     self._port_obj._path_to_lighttpd_php())
 
         # Setup log files
         f.write(('server.errorlog = "%s"\n'
@@ -161,7 +155,7 @@ class Lighttpd(http_server_base.HttpServerBase):
                 mappings = [{'port': 8000, 'docroot': self._root},
                             {'port': 8080, 'docroot': self._root},
                             {'port': 8443, 'docroot': self._root,
-                             'sslcert': Lighttpd._pem_file}]
+                             'sslcert': self._pem_file}]
         else:
             mappings = self.VIRTUALCONFIG
         for mapping in mappings:
@@ -176,12 +170,12 @@ class Lighttpd(http_server_base.HttpServerBase):
                      '}\n\n') % (mapping['port'], mapping['docroot']))
         f.close()
 
-        executable = path_utils.lighttpd_executable_path()
-        module_path = path_utils.lighttpd_module_path()
+        executable = self._port_obj._path_to_lighttpd()
+        module_path = self._port_obj._path_to_lighttpd_modules()
         start_cmd = [executable,
                      # Newly written config file
-                     '-f', path_utils.path_from_base(self._output_dir,
-                                                     'lighttpd.conf'),
+                     '-f', self._port_obj._path_from_base(self._output_dir,
+                                                         'lighttpd.conf'),
                      # Where it can find its module dynamic libraries
                      '-m', module_path]
 
@@ -203,11 +197,11 @@ class Lighttpd(http_server_base.HttpServerBase):
         env = os.environ
         if sys.platform in ('cygwin', 'win32'):
             env['PATH'] = '%s;%s' % (
-                path_utils.path_from_base('third_party', 'cygwin', 'bin'),
+                port.path_from_base('third_party', 'cygwin', 'bin'),
                 env['PATH'])
 
         if sys.platform == 'win32' and self._register_cygwin:
-            setup_mount = path_utils.path_from_base('third_party', 'cygwin',
+            setup_mount = port.path_from_base('third_party', 'cygwin',
                                                     'setup_mount.bat')
             subprocess.Popen(setup_mount).wait()
 
@@ -235,7 +229,7 @@ class Lighttpd(http_server_base.HttpServerBase):
         httpd_pid = None
         if self._process:
             httpd_pid = self._process.pid
-        path_utils.shut_down_http_server(httpd_pid)
+        port._shut_down_http_server(httpd_pid)
 
         if self._process:
             self._process.wait()
index 026e070..e82943e 100644 (file)
 """Base class with common routines between the Apache and Lighttpd servers."""
 
 import logging
+import os
 import time
 import urllib
 
 
 class HttpServerBase(object):
 
+    def __init__(self, port_obj):
+        self._port_obj = port_obj
+
     def wait_for_action(self, action):
         """Repeat the action for 20 seconds or until it succeeds. Returns
         whether it succeeded."""
@@ -65,3 +69,10 @@ class HttpServerBase(object):
                 return False
 
         return True
+
+    def remove_log_files(self, folder, starts_with):
+        files = os.listdir(folder)
+        for file in files:
+            if file.startswith(starts_with):
+                full_path = os.path.join(folder, file)
+                os.remove(full_path)
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/path_utils.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/path_utils.py
deleted file mode 100644 (file)
index 70b8c03..0000000
+++ /dev/null
@@ -1,395 +0,0 @@
-#!/usr/bin/env python
-# Copyright (C) 2010 Google Inc. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-#     * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#     * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following disclaimer
-# in the documentation and/or other materials provided with the
-# distribution.
-#     * Neither the name of Google Inc. nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-"""This package contains utility methods for manipulating paths and
-filenames for test results and baselines. It also contains wrappers
-of a few routines in port/ so that the port package can
-be considered a 'protected' package - i.e., this file should be
-the only file that ever includes port. This leads to
-us including a few things that don't really have anything to do
- with paths, unfortunately."""
-
-import errno
-import os
-import stat
-import sys
-
-import port
-import chromium_win
-import chromium_mac
-import chromium_linux
-
-# Cache some values so we don't have to recalculate them. _basedir is
-# used by PathFromBase() and caches the full (native) path to the top
-# of the source tree (/src). _baseline_search_path is used by
-# ExpectedBaselines() and caches the list of native paths to search
-# for baseline results.
-_basedir = None
-_baseline_search_path = None
-
-
-class PathNotFound(Exception):
-    pass
-
-
-def layout_tests_dir():
-    """Returns the fully-qualified path to the directory containing the input
-    data for the specified layout test."""
-    return path_from_base('third_party', 'WebKit', 'LayoutTests')
-
-
-def chromium_baseline_path(platform=None):
-    """Returns the full path to the directory containing expected
-    baseline results from chromium ports. If |platform| is None, the
-    currently executing platform is used.
-
-    Note: although directly referencing individual port/* files is
-    usually discouraged, we allow it here so that the rebaselining tool can
-    pull baselines for platforms other than the host platform."""
-
-    # Normalize the platform string.
-    platform = platform_name(platform)
-    if platform.startswith('chromium-mac'):
-        return chromium_mac.baseline_path(platform)
-    elif platform.startswith('chromium-win'):
-        return chromium_win.baseline_path(platform)
-    elif platform.startswith('chromium-linux'):
-        return chromium_linux.baseline_path(platform)
-
-    return port.baseline_path()
-
-
-def webkit_baseline_path(platform):
-    """Returns the full path to the directory containing expected
-    baseline results from WebKit ports."""
-    return path_from_base('third_party', 'WebKit', 'LayoutTests',
-                          'platform', platform)
-
-
-def baseline_search_path(platform=None):
-    """Returns the list of directories to search for baselines/results for a
-    given platform, in order of preference. Paths are relative to the top of
-    the source tree. If parameter platform is None, returns the list for the
-    current platform that the script is running on.
-
-    Note: although directly referencing individual port/* files is
-    usually discouraged, we allow it here so that the rebaselining tool can
-    pull baselines for platforms other than the host platform."""
-
-    # Normalize the platform name.
-    platform = platform_name(platform)
-    if platform.startswith('chromium-mac'):
-        return chromium_mac.baseline_search_path(platform)
-    elif platform.startswith('chromium-win'):
-        return chromium_win.baseline_search_path(platform)
-    elif platform.startswith('chromium-linux'):
-        return chromium_linux.baseline_search_path(platform)
-    return port.baseline_search_path()
-
-
-def expected_baselines(filename, suffix, platform=None, all_baselines=False):
-    """Given a test name, finds where the baseline results are located.
-
-    Args:
-       filename: absolute filename to test file
-       suffix: file suffix of the expected results, including dot; e.g. '.txt'
-           or '.png'.  This should not be None, but may be an empty string.
-       platform: layout test platform: 'win', 'linux' or 'mac'. Defaults to
-                 the current platform.
-       all_baselines: If True, return an ordered list of all baseline paths
-                      for the given platform. If False, return only the first
-                      one.
-    Returns
-       a list of ( platform_dir, results_filename ), where
-         platform_dir - abs path to the top of the results tree (or test tree)
-         results_filename - relative path from top of tree to the results file
-           (os.path.join of the two gives you the full path to the file,
-            unless None was returned.)
-      Return values will be in the format appropriate for the current platform
-      (e.g., "\\" for path separators on Windows). If the results file is not
-      found, then None will be returned for the directory, but the expected
-      relative pathname will still be returned.
-    """
-    global _baseline_search_path
-    global _search_path_platform
-    testname = os.path.splitext(relative_test_filename(filename))[0]
-
-    baseline_filename = testname + '-expected' + suffix
-
-    if (_baseline_search_path is None) or (_search_path_platform != platform):
-        _baseline_search_path = baseline_search_path(platform)
-        _search_path_platform = platform
-
-    baselines = []
-    for platform_dir in _baseline_search_path:
-        if os.path.exists(os.path.join(platform_dir, baseline_filename)):
-            baselines.append((platform_dir, baseline_filename))
-
-        if not all_baselines and baselines:
-            return baselines
-
-    # If it wasn't found in a platform directory, return the expected result
-    # in the test directory, even if no such file actually exists.
-    platform_dir = layout_tests_dir()
-    if os.path.exists(os.path.join(platform_dir, baseline_filename)):
-        baselines.append((platform_dir, baseline_filename))
-
-    if baselines:
-        return baselines
-
-    return [(None, baseline_filename)]
-
-
-def expected_filename(filename, suffix):
-    """Given a test name, returns an absolute path to its expected results.
-
-    If no expected results are found in any of the searched directories, the
-    directory in which the test itself is located will be returned. The return
-    value is in the format appropriate for the platform (e.g., "\\" for
-    path separators on windows).
-
-    Args:
-       filename: absolute filename to test file
-       suffix: file suffix of the expected results, including dot; e.g. '.txt'
-           or '.png'.  This should not be None, but may be an empty string.
-       platform: the most-specific directory name to use to build the
-           search list of directories, e.g., 'chromium-win', or
-           'chromium-mac-leopard' (we follow the WebKit format)
-    """
-    platform_dir, baseline_filename = expected_baselines(filename, suffix)[0]
-    if platform_dir:
-        return os.path.join(platform_dir, baseline_filename)
-    return os.path.join(layout_tests_dir(), baseline_filename)
-
-
-def relative_test_filename(filename):
-    """Provide the filename of the test relative to the layout tests
-    directory as a unix style path (a/b/c)."""
-    return _win_path_to_unix(filename[len(layout_tests_dir()) + 1:])
-
-
-def _win_path_to_unix(path):
-    """Convert a windows path to use unix-style path separators (a/b/c)."""
-    return path.replace('\\', '/')
-
-#
-# Routines that are arguably platform-specific but have been made
-# generic for now
-#
-
-
-def filename_to_uri(full_path):
-    """Convert a test file to a URI."""
-    LAYOUTTEST_HTTP_DIR = "http/tests/"
-    LAYOUTTEST_WEBSOCKET_DIR = "websocket/tests/"
-
-    relative_path = _win_path_to_unix(relative_test_filename(full_path))
-    port = None
-    use_ssl = False
-
-    if relative_path.startswith(LAYOUTTEST_HTTP_DIR):
-        # http/tests/ run off port 8000 and ssl/ off 8443
-        relative_path = relative_path[len(LAYOUTTEST_HTTP_DIR):]
-        port = 8000
-    elif relative_path.startswith(LAYOUTTEST_WEBSOCKET_DIR):
-        # websocket/tests/ run off port 8880 and 9323
-        # Note: the root is /, not websocket/tests/
-        port = 8880
-
-    # Make http/tests/local run as local files. This is to mimic the
-    # logic in run-webkit-tests.
-    # TODO(jianli): Consider extending this to "media/".
-    if port and not relative_path.startswith("local/"):
-        if relative_path.startswith("ssl/"):
-            port += 443
-            protocol = "https"
-        else:
-            protocol = "http"
-        return "%s://127.0.0.1:%u/%s" % (protocol, port, relative_path)
-
-    if sys.platform in ('cygwin', 'win32'):
-        return "file:///" + get_absolute_path(full_path)
-    return "file://" + get_absolute_path(full_path)
-
-
-def get_absolute_path(path):
-    """Returns an absolute UNIX path."""
-    return _win_path_to_unix(os.path.abspath(path))
-
-
-def maybe_make_directory(*path):
-    """Creates the specified directory if it doesn't already exist."""
-    try:
-        os.makedirs(os.path.join(*path))
-    except OSError, e:
-        if e.errno != errno.EEXIST:
-            raise
-
-
-def path_from_base(*comps):
-    """Returns an absolute filename from a set of components specified
-    relative to the top of the source tree. If the path does not exist,
-    the exception PathNotFound is raised."""
-    global _basedir
-    if _basedir == None:
-        # We compute the top of the source tree by finding the absolute
-        # path of this source file, and then climbing up three directories
-        # as given in subpath. If we move this file, subpath needs to be
-        # updated.
-        path = os.path.abspath(__file__)
-        subpath = os.path.join('third_party', 'WebKit')
-        _basedir = path[:path.index(subpath)]
-    path = os.path.join(_basedir, *comps)
-    if not os.path.exists(path):
-        raise PathNotFound('could not find %s' % (path))
-    return path
-
-
-def remove_directory(*path):
-    """Recursively removes a directory, even if it's marked read-only.
-
-    Remove the directory located at *path, if it exists.
-
-    shutil.rmtree() doesn't work on Windows if any of the files or directories
-    are read-only, which svn repositories and some .svn files are.  We need to
-    be able to force the files to be writable (i.e., deletable) as we traverse
-    the tree.
-
-    Even with all this, Windows still sometimes fails to delete a file, citing
-    a permission error (maybe something to do with antivirus scans or disk
-    indexing).  The best suggestion any of the user forums had was to wait a
-    bit and try again, so we do that too.  It's hand-waving, but sometimes it
-    works. :/
-    """
-    file_path = os.path.join(*path)
-    if not os.path.exists(file_path):
-        return
-
-    win32 = False
-    if sys.platform == 'win32':
-        win32 = True
-        # Some people don't have the APIs installed. In that case we'll do
-        # without.
-        try:
-            win32api = __import__('win32api')
-            win32con = __import__('win32con')
-        except ImportError:
-            win32 = False
-
-        def remove_with_retry(rmfunc, path):
-            os.chmod(path, stat.S_IWRITE)
-            if win32:
-                win32api.SetFileAttributes(path,
-                                           win32con.FILE_ATTRIBUTE_NORMAL)
-            try:
-                return rmfunc(path)
-            except EnvironmentError, e:
-                if e.errno != errno.EACCES:
-                    raise
-                print 'Failed to delete %s: trying again' % repr(path)
-                time.sleep(0.1)
-                return rmfunc(path)
-    else:
-
-        def remove_with_retry(rmfunc, path):
-            if os.path.islink(path):
-                return os.remove(path)
-            else:
-                return rmfunc(path)
-
-    for root, dirs, files in os.walk(file_path, topdown=False):
-        # For POSIX:  making the directory writable guarantees removability.
-        # Windows will ignore the non-read-only bits in the chmod value.
-        os.chmod(root, 0770)
-        for name in files:
-            remove_with_retry(os.remove, os.path.join(root, name))
-        for name in dirs:
-            remove_with_retry(os.rmdir, os.path.join(root, name))
-
-    remove_with_retry(os.rmdir, file_path)
-
-#
-# Wrappers around port/
-#
-
-
-def platform_name(platform=None):
-    """Returns the appropriate chromium platform name for |platform|. If
-       |platform| is None, returns the name of the chromium platform on the
-       currently running system. If |platform| is of the form 'chromium-*',
-       it is returned unchanged, otherwise 'chromium-' is prepended."""
-    if platform == None:
-        return port.platform_name()
-    if not platform.startswith('chromium-'):
-        platform = "chromium-" + platform
-    return platform
-
-
-def platform_version():
-    return port.platform_version()
-
-
-def lighttpd_executable_path():
-    return port.lighttpd_executable_path()
-
-
-def lighttpd_module_path():
-    return port.lighttpd_module_path()
-
-
-def lighttpd_php_path():
-    return port.lighttpd_php_path()
-
-
-def wdiff_path():
-    return port.wdiff_path()
-
-
-def test_shell_path(target):
-    return port.test_shell_path(target)
-
-
-def image_diff_path(target):
-    return port.image_diff_path(target)
-
-
-def layout_test_helper_path(target):
-    return port.layout_test_helper_path(target)
-
-
-def fuzzy_match_path():
-    return port.fuzzy_match_path()
-
-
-def shut_down_http_server(server_pid):
-    return port.shut_down_http_server(server_pid)
-
-
-def kill_all_test_shells():
-    port.kill_all_test_shells()
index c6c7527..4274676 100644 (file)
@@ -39,8 +39,6 @@ import tempfile
 import time
 import urllib
 
-import path_utils
-import port
 import http_server
 
 _WS_LOG_PREFIX = 'pywebsocket.ws.log-'
@@ -51,37 +49,29 @@ _DEFAULT_WSS_PORT = 9323
 
 
 def url_is_alive(url):
-  """Checks to see if we get an http response from |url|.
-  We poll the url 5 times with a 1 second delay.  If we don't
-  get a reply in that time, we give up and assume the httpd
-  didn't start properly.
-
-  Args:
-    url: The URL to check.
-  Return:
-    True if the url is alive.
-  """
-  wait_time = 5
-  while wait_time > 0:
-    try:
-      response = urllib.urlopen(url)
-      # Server is up and responding.
-      return True
-    except IOError:
-      pass
-    wait_time -= 1
-    # Wait a second and try again.
-    time.sleep(1)
-
-  return False
-
-
-def remove_log_files(folder, starts_with):
-    files = os.listdir(folder)
-    for file in files:
-        if file.startswith(starts_with):
-            full_path = os.path.join(folder, file)
-            os.remove(full_path)
+    """Checks to see if we get an http response from |url|.
+    We poll the url 5 times with a 1 second delay.  If we don't
+    get a reply in that time, we give up and assume the httpd
+    didn't start properly.
+
+    Args:
+      url: The URL to check.
+    Return:
+      True if the url is alive.
+    """
+    wait_time = 5
+    while wait_time > 0:
+        try:
+            response = urllib.urlopen(url)
+            # Server is up and responding.
+            return True
+        except IOError:
+            pass
+        wait_time -= 1
+        # Wait a second and try again.
+        time.sleep(1)
+
+    return False
 
 
 class PyWebSocketNotStarted(Exception):
@@ -94,18 +84,15 @@ class PyWebSocketNotFound(Exception):
 
 class PyWebSocket(http_server.Lighttpd):
 
-    def __init__(self, output_dir, port=_DEFAULT_WS_PORT,
-                 root=None,
-                 use_tls=False,
-                 private_key=http_server.Lighttpd._pem_file,
-                 certificate=http_server.Lighttpd._pem_file,
+    def __init__(self, port_obj, output_dir, port=_DEFAULT_WS_PORT,
+                 root=None, use_tls=False,
                  register_cygwin=None,
                  pidfile=None):
         """Args:
           output_dir: the absolute path to the layout test result directory
         """
-        http_server.Lighttpd.__init__(self, output_dir,
-                                      port=port,
+        http_server.Lighttpd.__init__(self, port_obj, output_dir,
+                                      port=_DEFAULT_WS_PORT,
                                       root=root,
                                       register_cygwin=register_cygwin)
         self._output_dir = output_dir
@@ -113,8 +100,8 @@ class PyWebSocket(http_server.Lighttpd):
         self._port = port
         self._root = root
         self._use_tls = use_tls
-        self._private_key = private_key
-        self._certificate = certificate
+        self._private_key = self._pem_file
+        self._certificate = self._pem_file
         if self._port:
             self._port = int(self._port)
         if self._use_tls:
@@ -131,12 +118,10 @@ class PyWebSocket(http_server.Lighttpd):
                 os.path.join(self._root, 'websocket', 'tests'))
         else:
             try:
-                self._web_socket_tests = path_utils.path_from_base(
-                    'third_party', 'WebKit', 'LayoutTests', 'websocket',
-                    'tests')
-                self._layout_tests = path_utils.path_from_base(
-                    'third_party', 'WebKit', 'LayoutTests')
-            except path_utils.PathNotFound:
+                self._layout_tests = self._port_obj.layout_tests_dir()
+                self._web_socket_tests = os.path.join(self._layout_tests,
+                     'websocket', 'tests')
+            except:
                 self._web_socket_tests = None
 
     def start(self):
@@ -155,7 +140,7 @@ class PyWebSocket(http_server.Lighttpd):
         log_file_name = log_prefix + time_str
 
         # Remove old log files. We only need to keep the last ones.
-        remove_log_files(self._output_dir, log_prefix)
+        self.remove_log_files(self._output_dir, log_prefix)
 
         error_log = os.path.join(self._output_dir, log_file_name + "-err.txt")
 
@@ -163,11 +148,12 @@ class PyWebSocket(http_server.Lighttpd):
         self._wsout = open(output_log, "w")
 
         python_interp = sys.executable
-        pywebsocket_base = path_utils.path_from_base(
-            'third_party', 'WebKit', 'WebKitTools', 'pywebsocket')
-        pywebsocket_script = path_utils.path_from_base(
-            'third_party', 'WebKit', 'WebKitTools', 'pywebsocket',
-            'mod_pywebsocket', 'standalone.py')
+        pywebsocket_base = os.path.join(
+            os.path.dirname(os.path.dirname(os.path.dirname(
+            os.path.dirname(os.path.dirname(
+            os.path.abspath(__file__)))))), 'pywebsocket')
+        pywebsocket_script = os.path.join(pywebsocket_base, 'mod_pywebsocket',
+            'standalone.py')
         start_cmd = [
             python_interp, pywebsocket_script,
             '-p', str(self._port),
@@ -193,12 +179,12 @@ class PyWebSocket(http_server.Lighttpd):
         env = os.environ
         if sys.platform in ('cygwin', 'win32'):
             env['PATH'] = '%s;%s' % (
-                path_utils.path_from_base('third_party', 'cygwin', 'bin'),
+                self._port_obj._path_from_base('third_party', 'cygwin', 'bin'),
                 env['PATH'])
 
         if sys.platform == 'win32' and self._register_cygwin:
-            setup_mount = path_utils.path_from_base('third_party', 'cygwin',
-                'setup_mount.bat')
+            setup_mount = self._port_obj._path_from_base('third_party',
+                 'cygwin', 'setup_mount.bat')
             subprocess.Popen(setup_mount).wait()
 
         env['PYTHONPATH'] = (pywebsocket_base + os.path.pathsep +
@@ -255,7 +241,7 @@ class PyWebSocket(http_server.Lighttpd):
                 'Failed to find %s server pid.' % self._server_name)
 
         logging.debug('Shutting down %s server %d.' % (self._server_name, pid))
-        port.kill_process(pid)
+        self._port_obj._kill_process(pid)
 
         if self._process:
             self._process.wait()
index 414baaf..4da32ad 100644 (file)
@@ -54,7 +54,7 @@ import urllib
 import webbrowser
 import zipfile
 
-from port import path_utils
+from layout_package import path_utils
 from layout_package import test_expectations
 from test_types import image_diff
 from test_types import text_diff
index 571ae3f..f0b68ee 100755 (executable)
@@ -54,31 +54,24 @@ import Queue
 import random
 import re
 import shutil
-import subprocess
 import sys
 import time
 import traceback
 
+import simplejson
+
 from layout_package import test_expectations
 from layout_package import json_layout_results_generator
 from layout_package import metered_stream
 from layout_package import test_failures
 from layout_package import test_shell_thread
 from layout_package import test_files
-
-import port
-from port import apache_http_server
-from port import http_server
-from port import path_utils
-from port import websocket_server
-
 from test_types import fuzzy_image_diff
 from test_types import image_diff
 from test_types import test_type_base
 from test_types import text_diff
 
-sys.path.append(path_utils.path_from_base('third_party'))
-import simplejson
+import port
 
 # Indicates that we want detailed progress updates in the output (prints
 # directory-by-directory feedback).
@@ -96,17 +89,16 @@ TestExpectationsFile = test_expectations.TestExpectationsFile
 class TestInfo:
     """Groups information about a test for easy passing of data."""
 
-    def __init__(self, filename, timeout):
+    def __init__(self, port, filename, timeout):
         """Generates the URI and stores the filename and timeout for this test.
         Args:
           filename: Full path to the test.
           timeout: Timeout for running the test in TestShell.
           """
         self.filename = filename
-        self.uri = path_utils.filename_to_uri(filename)
+        self.uri = port.filename_to_uri(filename)
         self.timeout = timeout
-        expected_hash_file = path_utils.expected_filename(filename,
-                                                          '.checksum')
+        expected_hash_file = port.expected_filename(filename, '.checksum')
         try:
             self.image_hash = open(expected_hash_file, "r").read()
         except IOError, e:
@@ -175,24 +167,18 @@ class TestRunner:
 
     NUM_RETRY_ON_UNEXPECTED_FAILURE = 1
 
-    def __init__(self, options, meter):
+    def __init__(self, port, options, meter):
         """Initialize test runner data structures.
 
         Args:
+          port: an object implementing port-specific
           options: a dictionary of command line options
           meter: a MeteredStream object to record updates to.
         """
+        self._port = port
         self._options = options
         self._meter = meter
 
-        if options.use_apache:
-            self._http_server = apache_http_server.LayoutTestApacheHttpd(
-                options.results_directory)
-        else:
-            self._http_server = http_server.Lighttpd(options.results_directory)
-
-        self._websocket_server = websocket_server.PyWebSocket(
-            options.results_directory)
         # disable wss server. need to install pyOpenSSL on buildbots.
         # self._websocket_secure_server = websocket_server.PyWebSocket(
         #        options.results_directory, use_tls=True, port=9323)
@@ -203,8 +189,6 @@ class TestRunner:
         # a set of test files, and the same tests as a list
         self._test_files = set()
         self._test_files_list = None
-        self._file_dir = path_utils.path_from_base('webkit', 'tools',
-            'layout_tests')
         self._result_queue = Queue.Queue()
 
         # These are used for --log detailed-progress to track status by
@@ -219,20 +203,18 @@ class TestRunner:
         logging.debug("flushing stderr")
         sys.stderr.flush()
         logging.debug("stopping http server")
-        # Stop the http server.
-        self._http_server.stop()
-        # Stop the Web Socket / Web Socket Secure servers.
-        self._websocket_server.stop()
-        # self._websocket_secure_server.Stop()
+        self._port.stop_http_server()
+        logging.debug("stopping websocket server")
+        self._port.stop_websocket_server()
 
     def gather_file_paths(self, paths):
         """Find all the files to test.
 
         Args:
           paths: a list of globs to use instead of the defaults."""
-        self._test_files = test_files.gather_test_files(paths)
+        self._test_files = test_files.gather_test_files(self._port, paths)
 
-    def parse_expectations(self, platform, is_debug_mode):
+    def parse_expectations(self, test_platform_name, is_debug_mode):
         """Parse the expectations from the test_list files and return a data
         structure holding them. Throws an error if the test_list files have
         invalid syntax."""
@@ -242,9 +224,10 @@ class TestRunner:
             test_files = self._test_files
 
         try:
-            self._expectations = test_expectations.TestExpectations(test_files,
-                self._file_dir, platform, is_debug_mode,
-                self._options.lint_test_files)
+            expectations_str = self._port.test_expectations()
+            self._expectations = test_expectations.TestExpectations(
+                self._port, test_files, expectations_str, test_platform_name,
+                is_debug_mode, self._options.lint_test_files)
             return self._expectations
         except Exception, err:
             if self._options.lint_test_files:
@@ -359,7 +342,8 @@ class TestRunner:
             self._test_files = set(self._test_files_list)
 
             self._expectations = self.parse_expectations(
-                path_utils.platform_name(), self._options.target == 'Debug')
+                self._port.test_platform_name(),
+                self._options.target == 'Debug')
 
             self._test_files = set(files)
             self._test_files_list = files
@@ -424,8 +408,9 @@ class TestRunner:
         is used for looking up the timeout value (in ms) to use for the given
         test."""
         if self._expectations.has_modifier(test_file, test_expectations.SLOW):
-            return TestInfo(test_file, self._options.slow_time_out_ms)
-        return TestInfo(test_file, self._options.time_out_ms)
+            return TestInfo(self._port, test_file,
+                            self._options.slow_time_out_ms)
+        return TestInfo(self._port, test_file, self._options.time_out_ms)
 
     def _get_test_file_queue(self, test_files):
         """Create the thread safe queue of lists of (test filenames, test URIs)
@@ -490,6 +475,7 @@ class TestRunner:
         """Returns the tuple of arguments for tests and for test_shell."""
         shell_args = []
         test_args = test_type_base.TestArguments()
+        png_path = None
         if not self._options.no_pixel_tests:
             png_path = os.path.join(self._options.results_directory,
                                     "png_result%s.png" % index)
@@ -506,7 +492,7 @@ class TestRunner:
         if self._options.gp_fault_error_box:
             shell_args.append('--gp-fault-error-box')
 
-        return (test_args, shell_args)
+        return test_args, png_path, shell_args
 
     def _contains_tests(self, subdir):
         for test_file in self._test_files_list:
@@ -514,23 +500,12 @@ class TestRunner:
                 return True
         return False
 
-    def _instantiate_test_shell_threads(self, test_shell_binary, test_files,
-                                     result_summary):
+    def _instantiate_test_shell_threads(self, test_files, result_summary):
         """Instantitates and starts the TestShellThread(s).
 
         Return:
           The list of threads.
         """
-        test_shell_command = [test_shell_binary]
-
-        if self._options.wrapper:
-            # This split() isn't really what we want -- it incorrectly will
-            # split quoted strings within the wrapper argument -- but in
-            # practice it shouldn't come up and the --help output warns
-            # about it anyway.
-            test_shell_command = (self._options.wrapper.split() +
-                test_shell_command)
-
         filename_queue = self._get_test_file_queue(test_files)
 
         # Instantiate TestShellThreads and start them.
@@ -539,15 +514,16 @@ class TestRunner:
             # Create separate TestTypes instances for each thread.
             test_types = []
             for t in self._test_types:
-                test_types.append(t(self._options.platform,
+                test_types.append(t(self._port, self._options.platform,
                                     self._options.results_directory))
 
-            test_args, shell_args = self._get_test_shell_args(i)
-            thread = test_shell_thread.TestShellThread(filename_queue,
+            test_args, png_path, shell_args = self._get_test_shell_args(i)
+            thread = test_shell_thread.TestShellThread(self._port,
+                                                       filename_queue,
                                                        self._result_queue,
-                                                       test_shell_command,
                                                        test_types,
                                                        test_args,
+                                                       png_path,
                                                        shell_args,
                                                        self._options)
             if self._is_single_threaded():
@@ -558,19 +534,11 @@ class TestRunner:
 
         return threads
 
-    def _stop_layout_test_helper(self, proc):
-        """Stop the layout test helper and closes it down."""
-        if proc:
-            logging.debug("Stopping layout test helper")
-            proc.stdin.write("x\n")
-            proc.stdin.close()
-            proc.wait()
-
     def _is_single_threaded(self):
         """Returns whether we should run all the tests in the main thread."""
         return int(self._options.num_test_shells) == 1
 
-    def _run_tests(self, test_shell_binary, file_list, result_summary):
+    def _run_tests(self, file_list, result_summary):
         """Runs the tests in the file_list.
 
         Return: A tuple (failures, thread_timings, test_timings,
@@ -584,9 +552,8 @@ class TestRunner:
               in the form {filename:filename, test_run_time:test_run_time}
             result_summary: summary object to populate with the results
         """
-        threads = self._instantiate_test_shell_threads(test_shell_binary,
-                                                    file_list,
-                                                    result_summary)
+        threads = self._instantiate_test_shell_threads(file_list,
+                                                       result_summary)
 
         # Wait for the threads to finish and collect test failures.
         failures = {}
@@ -612,7 +579,7 @@ class TestRunner:
         except KeyboardInterrupt:
             for thread in threads:
                 thread.cancel()
-            self._stop_layout_test_helper(layout_test_helper_proc)
+            self._port.stop_helper()
             raise
         for thread in threads:
             # Check whether a TestShellThread died before normal completion.
@@ -642,42 +609,20 @@ class TestRunner:
         if not self._test_files:
             return 0
         start_time = time.time()
-        test_shell_binary = path_utils.test_shell_path(self._options.target)
 
         # Start up any helper needed
-        layout_test_helper_proc = None
         if not self._options.no_pixel_tests:
-            helper_path = path_utils.layout_test_helper_path(
-                self._options.target)
-            if len(helper_path):
-                logging.debug("Starting layout helper %s" % helper_path)
-                layout_test_helper_proc = subprocess.Popen(
-                    [helper_path], stdin=subprocess.PIPE,
-                    stdout=subprocess.PIPE, stderr=None)
-                is_ready = layout_test_helper_proc.stdout.readline()
-                if not is_ready.startswith('ready'):
-                    logging.error("layout_test_helper failed to be ready")
-
-        # Check that the system dependencies (themes, fonts, ...) are correct.
-        if not self._options.nocheck_sys_deps:
-            proc = subprocess.Popen([test_shell_binary,
-                                    "--check-layout-test-sys-deps"])
-            if proc.wait() != 0:
-                logging.info("Aborting because system dependencies check "
-                             "failed.\n To override, invoke with "
-                             "--nocheck-sys-deps")
-                sys.exit(1)
+            self._port.start_helper()
 
         if self._contains_tests(self.HTTP_SUBDIR):
-            self._http_server.start()
+            self._port.start_http_server()
 
         if self._contains_tests(self.WEBSOCKET_SUBDIR):
-            self._websocket_server.start()
+            self._port.start_websocket_server()
             # self._websocket_secure_server.Start()
 
         thread_timings, test_timings, individual_test_timings = (
-            self._run_tests(test_shell_binary, self._test_files_list,
-                           result_summary))
+            self._run_tests(self._test_files_list, result_summary))
 
         # We exclude the crashes from the list of results to retry, because
         # we want to treat even a potentially flaky crash as an error.
@@ -689,10 +634,10 @@ class TestRunner:
             logging.debug("Retrying %d unexpected failure(s)" % len(failures))
             retries += 1
             retry_summary = ResultSummary(self._expectations, failures.keys())
-            self._run_tests(test_shell_binary, failures.keys(), retry_summary)
+            self._run_tests(failures.keys(), retry_summary)
             failures = self._get_failures(retry_summary, include_crashes=True)
 
-        self._stop_layout_test_helper(layout_test_helper_proc)
+        self._port.stop_helper()
         end_time = time.time()
 
         write = create_logging_writer(self._options, 'timing')
@@ -777,7 +722,7 @@ class TestRunner:
 
         next_test = self._test_files_list[self._current_test_number]
         next_dir = os.path.dirname(
-            path_utils.relative_test_filename(next_test))
+            self._port.relative_test_filename(next_test))
         if self._current_progress_str == "":
             self._current_progress_str = "%s: " % (next_dir)
             self._current_dir = next_dir
@@ -803,7 +748,7 @@ class TestRunner:
 
             next_test = self._test_files_list[self._current_test_number]
             next_dir = os.path.dirname(
-                path_utils.relative_test_filename(next_test))
+                self._port.relative_test_filename(next_test))
 
         if result_summary.remaining:
             remain_str = " (%d)" % (result_summary.remaining)
@@ -874,7 +819,7 @@ class TestRunner:
             # Note that if a test crashed in the original run, we ignore
             # whether or not it crashed when we retried it (if we retried it),
             # and always consider the result not flaky.
-            test = path_utils.relative_test_filename(filename)
+            test = self._port.relative_test_filename(filename)
             expected = self._expectations.get_expectations_string(filename)
             actual = [keywords[result]]
 
@@ -943,7 +888,7 @@ class TestRunner:
         expectations_file.close()
 
         json_layout_results_generator.JSONLayoutResultsGenerator(
-            self._options.builder_name, self._options.build_name,
+            self._port, self._options.builder_name, self._options.build_name,
             self._options.build_number, self._options.results_directory,
             BUILDER_BASE_URL, individual_test_timings,
             self._expectations, result_summary, self._test_files_list)
@@ -1110,7 +1055,7 @@ class TestRunner:
         write(title)
         for test_tuple in test_list:
             filename = test_tuple.filename[len(
-                path_utils.layout_tests_dir()) + 1:]
+                self._port.layout_tests_dir()) + 1:]
             filename = filename.replace('\\', '/')
             test_run_time = round(test_tuple.test_run_time, 1)
             write("  %s took %s seconds" % (filename, test_run_time))
@@ -1328,7 +1273,7 @@ class TestRunner:
         """Prints one unexpected test result line."""
         desc = TestExpectationsFile.EXPECTATION_DESCRIPTIONS[result][0]
         self._meter.write("  %s -> unexpected %s\n" %
-                          (path_utils.relative_test_filename(test), desc))
+                          (self._port.relative_test_filename(test), desc))
 
     def _write_results_html_file(self, result_summary):
         """Write results.html which is a summary of tests that failed.
@@ -1366,12 +1311,12 @@ class TestRunner:
         for test_file in test_files:
             test_failures = result_summary.failures.get(test_file, [])
             out_file.write("<p><a href='%s'>%s</a><br />\n"
-                           % (path_utils.filename_to_uri(test_file),
-                              path_utils.relative_test_filename(test_file)))
+                           % (self._port.filename_to_uri(test_file),
+                              self._port.relative_test_filename(test_file)))
             for failure in test_failures:
                 out_file.write("&nbsp;&nbsp;%s<br/>"
                                % failure.result_html_output(
-                                 path_utils.relative_test_filename(test_file)))
+                                 self._port.relative_test_filename(test_file)))
             out_file.write("</p>\n")
 
         # footer
@@ -1382,8 +1327,7 @@ class TestRunner:
         """Launches the test shell open to the results.html page."""
         results_filename = os.path.join(self._options.results_directory,
                                         "results.html")
-        subprocess.Popen([path_utils.test_shell_path(self._options.target),
-                          path_utils.filename_to_uri(results_filename)])
+        self._port.show_results_html_file(results_filename)
 
 
 def _add_to_dict_of_lists(dict, key, value):
@@ -1444,19 +1388,19 @@ def main(options, args):
         else:
             options.target = "Release"
 
+    port_obj = port.get(options.platform, options)
+
     if not options.use_apache:
         options.use_apache = sys.platform in ('darwin', 'linux2')
 
     if options.results_directory.startswith("/"):
         # Assume it's an absolute path and normalize.
-        options.results_directory = path_utils.get_absolute_path(
+        options.results_directory = port_obj.get_absolute_path(
             options.results_directory)
     else:
         # If it's a relative path, make the output directory relative to
         # Debug or Release.
-        basedir = path_utils.path_from_base('webkit')
-        options.results_directory = path_utils.get_absolute_path(
-            os.path.join(basedir, options.target, options.results_directory))
+        options.results_directory = port_obj.results_directory()
 
     if options.clobber_old_results:
         # Just clobber the actual test results directories since the other
@@ -1466,12 +1410,9 @@ def main(options, args):
         if os.path.exists(path):
             shutil.rmtree(path)
 
-    # Ensure platform is valid and force it to the form 'chromium-<platform>'.
-    options.platform = path_utils.platform_name(options.platform)
-
     if not options.num_test_shells:
         # TODO(ojan): Investigate perf/flakiness impact of using numcores + 1.
-        options.num_test_shells = port.get_num_cores()
+        options.num_test_shells = port_obj.num_cores()
 
     write = create_logging_writer(options, 'config')
     write("Running %s test_shells in parallel" % options.num_test_shells)
@@ -1499,62 +1440,46 @@ def main(options, args):
         paths += read_test_files(options.test_list)
 
     # Create the output directory if it doesn't already exist.
-    path_utils.maybe_make_directory(options.results_directory)
+    port_obj.maybe_make_directory(options.results_directory)
     meter.update("Gathering files ...")
 
-    test_runner = TestRunner(options, meter)
+    test_runner = TestRunner(port_obj, options, meter)
     test_runner.gather_file_paths(paths)
 
     if options.lint_test_files:
         # Creating the expecations for each platform/target pair does all the
         # test list parsing and ensures it's correct syntax (e.g. no dupes).
-        for platform in TestExpectationsFile.PLATFORMS:
+        for platform in port_obj.test_platform_names():
             test_runner.parse_expectations(platform, is_debug_mode=True)
             test_runner.parse_expectations(platform, is_debug_mode=False)
         print ("If there are no fail messages, errors or exceptions, then the "
             "lint succeeded.")
         sys.exit(0)
 
-    try:
-        test_shell_binary_path = path_utils.test_shell_path(options.target)
-    except path_utils.PathNotFound:
-        print "\nERROR: test_shell is not found. Be sure that you have built"
-        print "it and that you are using the correct build. This script"
-        print "will run the Release one by default. Use --debug to use the"
-        print "Debug build.\n"
-        sys.exit(1)
+    # Check that the system dependencies (themes, fonts, ...) are correct.
+    if not options.nocheck_sys_deps:
+        if not port_obj.check_sys_deps():
+            sys.exit(1)
 
     write = create_logging_writer(options, "config")
-    write("Using platform '%s'" % options.platform)
+    write("Using port '%s'" % port_obj.name())
     write("Placing test results in %s" % options.results_directory)
     if options.new_baseline:
-        write("Placing new baselines in %s" %
-              path_utils.chromium_baseline_path(options.platform))
-    write("Using %s build at %s" % (options.target, test_shell_binary_path))
+        write("Placing new baselines in %s" % port_obj.baseline_path())
+    write("Using %s build" % options.target)
     if options.no_pixel_tests:
         write("Not running pixel tests")
     write("")
 
     meter.update("Parsing expectations ...")
-    test_runner.parse_expectations(options.platform, options.target == 'Debug')
+    test_runner.parse_expectations(port_obj.test_platform_name(),
+                                   options.target == 'Debug')
 
     meter.update("Preparing tests ...")
     write = create_logging_writer(options, "expected")
     result_summary = test_runner.prepare_lists_and_print_output(write)
 
-    if 'cygwin' == sys.platform:
-        logging.warn("#" * 40)
-        logging.warn("# UNEXPECTED PYTHON VERSION")
-        logging.warn("# This script should be run using the version of python")
-        logging.warn("# in third_party/python_24/")
-        logging.warn("#" * 40)
-        sys.exit(1)
-
-    # Delete the disk cache if any to ensure a clean test run.
-    cachedir = os.path.split(test_shell_binary_path)[0]
-    cachedir = os.path.join(cachedir, "cache")
-    if os.path.exists(cachedir):
-        shutil.rmtree(cachedir)
+    port_obj.setup_test_run()
 
     test_runner.add_test_type(text_diff.TestTextDiff)
     if not options.no_pixel_tests:
index 100fd0d..89dd192 100644 (file)
@@ -35,16 +35,14 @@ import errno
 import logging
 import os
 import shutil
-import subprocess
 
-from port import path_utils
 from layout_package import test_failures
 from test_types import test_type_base
 
 
 class FuzzyImageDiff(test_type_base.TestTypeBase):
 
-    def compare_output(self, filename, proc, output, test_args, target):
+    def compare_output(self, filename, output, test_args, target):
         """Implementation of CompareOutput that checks the output image and
         checksum against the expected files from the LayoutTest directory.
         """
@@ -54,7 +52,7 @@ class FuzzyImageDiff(test_type_base.TestTypeBase):
         if test_args.hash is None:
             return failures
 
-        expected_png_file = path_utils.expected_filename(filename, '.png')
+        expected_png_file = self._port.expected_filename(filename, '.png')
 
         if test_args.show_sources:
             logging.debug('Using %s' % expected_png_file)
@@ -64,8 +62,7 @@ class FuzzyImageDiff(test_type_base.TestTypeBase):
             failures.append(test_failures.FailureMissingImage(self))
 
         # Run the fuzzymatcher
-        r = subprocess.call([path_utils.fuzzy_match_path(),
-                            test_args.png_path, expected_png_file])
+        r = port.fuzzy_diff(test_args.png_path, expected_png_file)
         if r != 0:
             failures.append(test_failures.FailureFuzzyFailure(self))
 
index 86b9659..1df7ca3 100644 (file)
@@ -38,9 +38,7 @@ import errno
 import logging
 import os
 import shutil
-import subprocess
 
-from port import path_utils
 from layout_package import test_failures
 from test_types import test_type_base
 
@@ -84,7 +82,7 @@ class ImageDiff(test_type_base.TestTypeBase):
         self._save_baseline_data(filename, png_data, ".png")
         self._save_baseline_data(filename, checksum, ".checksum")
 
-    def _create_image_diff(self, filename, target):
+    def _create_image_diff(self, port, filename, target):
         """Creates the visual diff of the expected/actual PNGs.
 
         Args:
@@ -98,32 +96,14 @@ class ImageDiff(test_type_base.TestTypeBase):
         expected_filename = self.output_filename(filename,
           self.FILENAME_SUFFIX_EXPECTED + '.png')
 
-        global _compare_available
-        cmd = ''
-
         try:
-            executable = path_utils.image_diff_path(target)
-            cmd = [executable, '--diff', actual_filename, expected_filename,
-                   diff_filename]
-        except Exception, e:
+            _compare_available = True
+            result = port.diff_image(actual_filename, expected_filename,
+                                     diff_filename)
+        except ValueError:
             _compare_available = False
 
-        result = 1
-        if _compare_available:
-            try:
-                result = subprocess.call(cmd)
-            except OSError, e:
-                if e.errno == errno.ENOENT or e.errno == errno.EACCES:
-                    _compare_available = False
-                else:
-                    raise e
-            except ValueError:
-                # work around a race condition in Python 2.4's implementation
-                # of subprocess.Popen
-                pass
-
         global _compare_msg_printed
-
         if not _compare_available and not _compare_msg_printed:
             _compare_msg_printed = True
             print('image_diff not found. Make sure you have a ' + target +
@@ -131,7 +111,7 @@ class ImageDiff(test_type_base.TestTypeBase):
 
         return result
 
-    def compare_output(self, filename, proc, output, test_args, target):
+    def compare_output(self, port, filename, output, test_args, target):
         """Implementation of CompareOutput that checks the output image and
         checksum against the expected files from the LayoutTest directory.
         """
@@ -148,9 +128,9 @@ class ImageDiff(test_type_base.TestTypeBase):
             return failures
 
         # Compare hashes.
-        expected_hash_file = path_utils.expected_filename(filename,
+        expected_hash_file = self._port.expected_filename(filename,
                                                           '.checksum')
-        expected_png_file = path_utils.expected_filename(filename, '.png')
+        expected_png_file = self._port.expected_filename(filename, '.png')
 
         if test_args.show_sources:
             logging.debug('Using %s' % expected_hash_file)
@@ -166,8 +146,9 @@ class ImageDiff(test_type_base.TestTypeBase):
 
         if not os.path.isfile(expected_png_file):
             # Report a missing expected PNG file.
-            self.write_output_files(filename, '', '.checksum', test_args.hash,
-                                    expected_hash, diff=False, wdiff=False)
+            self.write_output_files(port, filename, '', '.checksum',
+                                    test_args.hash, expected_hash,
+                                    diff=False, wdiff=False)
             self._copy_output_png(filename, test_args.png_path, '-actual.png')
             failures.append(test_failures.FailureMissingImage(self))
             return failures
@@ -176,14 +157,15 @@ class ImageDiff(test_type_base.TestTypeBase):
             return failures
 
 
-        self.write_output_files(filename, '', '.checksum', test_args.hash,
-                                expected_hash, diff=False, wdiff=False)
+        self.write_output_files(port, filename, '', '.checksum',
+                                test_args.hash, expected_hash,
+                                diff=False, wdiff=False)
         self._copy_output_png(filename, test_args.png_path, '-actual.png')
         self._copy_output_png(filename, expected_png_file, '-expected.png')
 
         # Even though we only use result in one codepath below but we
         # still need to call CreateImageDiff for other codepaths.
-        result = self._create_image_diff(filename, target)
+        result = self._create_image_diff(port, filename, target)
         if expected_hash == '':
             failures.append(test_failures.FailureMissingImageHash(self))
         elif test_args.hash != expected_hash:
@@ -196,7 +178,7 @@ class ImageDiff(test_type_base.TestTypeBase):
 
         return failures
 
-    def diff_files(self, file1, file2):
+    def diff_files(self, port, file1, file2):
         """Diff two image files.
 
         Args:
@@ -208,17 +190,8 @@ class ImageDiff(test_type_base.TestTypeBase):
         """
 
         try:
-            executable = path_utils.image_diff_path('Debug')
-        except Exception, e:
-            logging.warn('Failed to find image diff executable.')
-            return True
-
-        cmd = [executable, file1, file2]
-        result = 1
-        try:
-            result = subprocess.call(cmd)
-        except OSError, e:
-            logging.warn('Failed to compare image diff: %s', e)
+            result = port.diff_image(file1, file2)
+        except ValueError, e:
             return True
 
         return result == 1
index 79b7e34..efa2e8c 100644 (file)
@@ -33,13 +33,9 @@ Also defines the TestArguments "struct" to pass them additional arguments.
 """
 
 import cgi
-import difflib
 import errno
 import logging
 import os.path
-import subprocess
-
-from port import path_utils
 
 
 class TestArguments(object):
@@ -74,7 +70,7 @@ class TestTypeBase(object):
     FILENAME_SUFFIX_WDIFF = "-wdiff.html"
     FILENAME_SUFFIX_COMPARE = "-diff.png"
 
-    def __init__(self, platform, root_output_dir):
+    def __init__(self, port, platform, root_output_dir):
         """Initialize a TestTypeBase object.
 
         Args:
@@ -83,14 +79,15 @@ class TestTypeBase(object):
           root_output_dir: The unix style path to the output dir.
         """
         self._root_output_dir = root_output_dir
+        self._port = port
         self._platform = platform
 
     def _make_output_directory(self, filename):
         """Creates the output directory (if needed) for a given test
         filename."""
         output_filename = os.path.join(self._root_output_dir,
-            path_utils.relative_test_filename(filename))
-        path_utils.maybe_make_directory(os.path.split(output_filename)[0])
+            self._port.relative_test_filename(filename))
+        self._port.maybe_make_directory(os.path.split(output_filename)[0])
 
     def _save_baseline_data(self, filename, data, modifier):
         """Saves a new baseline file into the platform directory.
@@ -104,13 +101,13 @@ class TestTypeBase(object):
           modifier: type of the result file, e.g. ".txt" or ".png"
         """
         relative_dir = os.path.dirname(
-            path_utils.relative_test_filename(filename))
+            self._port.relative_test_filename(filename))
         output_dir = os.path.join(
-            path_utils.chromium_baseline_path(self._platform), relative_dir)
+            self._port.chromium_baseline_path(self._platform), relative_dir)
         output_file = os.path.basename(os.path.splitext(filename)[0] +
             self.FILENAME_SUFFIX_EXPECTED + modifier)
 
-        path_utils.maybe_make_directory(output_dir)
+        self._port.maybe_make_directory(output_dir)
         output_path = os.path.join(output_dir, output_file)
         logging.debug('writing new baseline to "%s"' % (output_path))
         open(output_path, "wb").write(data)
@@ -130,10 +127,10 @@ class TestTypeBase(object):
           The absolute windows path to the output filename
         """
         output_filename = os.path.join(self._root_output_dir,
-            path_utils.relative_test_filename(filename))
+            self._port.relative_test_filename(filename))
         return os.path.splitext(output_filename)[0] + modifier
 
-    def compare_output(self, filename, proc, output, test_args, target):
+    def compare_output(self, port, filename, output, test_args, target):
         """Method that compares the output from the test with the
         expected value.
 
@@ -141,7 +138,6 @@ class TestTypeBase(object):
 
         Args:
           filename: absolute filename to test file
-          proc: a reference to the test_shell process
           output: a string containing the output of the test
           test_args: a TestArguments object holding optional additional
               arguments
@@ -152,8 +148,8 @@ class TestTypeBase(object):
         """
         raise NotImplemented
 
-    def write_output_files(self, filename, test_type, file_type, output,
-                           expected, diff=True, wdiff=False):
+    def write_output_files(self, port, filename, test_type, file_type,
+                           output, expected, diff=True, wdiff=False):
         """Writes the test output, the expected output and optionally the diff
         between the two to files in the results directory.
 
@@ -186,81 +182,15 @@ class TestTypeBase(object):
             return
 
         if diff:
-            diff = difflib.unified_diff(expected.splitlines(True),
-                                        output.splitlines(True),
-                                        expected_filename,
-                                        actual_filename)
-
+            diff = port.diff_text(expected, output, expected_filename,
+                                  actual_filename)
             diff_filename = self.output_filename(filename,
                 test_type + self.FILENAME_SUFFIX_DIFF + file_type)
-            open(diff_filename, "wb").write(''.join(diff))
+            open(diff_filename, "wb").write(diff)
 
         if wdiff:
             # Shell out to wdiff to get colored inline diffs.
-            executable = path_utils.wdiff_path()
-            cmd = [executable,
-                   '--start-delete=##WDIFF_DEL##',
-                   '--end-delete=##WDIFF_END##',
-                   '--start-insert=##WDIFF_ADD##',
-                   '--end-insert=##WDIFF_END##',
-                   expected_filename,
-                   actual_filename]
-            filename = self.output_filename(filename,
-                            test_type + self.FILENAME_SUFFIX_WDIFF)
-
-            global _wdiff_available
-
-            try:
-                # Python's Popen has a bug that causes any pipes opened to a
-                # process that can't be executed to be leaked.  Since this
-                # code is specifically designed to tolerate exec failures
-                # to gracefully handle cases where wdiff is not installed,
-                # the bug results in a massive file descriptor leak. As a
-                # workaround, if an exec failure is ever experienced for
-                # wdiff, assume it's not available.  This will leak one
-                # file descriptor but that's better than leaking each time
-                # wdiff would be run.
-                #
-                # http://mail.python.org/pipermail/python-list/
-                #    2008-August/505753.html
-                # http://bugs.python.org/issue3210
-                #
-                # It also has a threading bug, so we don't output wdiff if
-                # the Popen raises a ValueError.
-                # http://bugs.python.org/issue1236
-                if _wdiff_available:
-                    wdiff = subprocess.Popen(
-                        cmd, stdout=subprocess.PIPE).communicate()[0]
-                    wdiff_failed = False
-
-            except OSError, e:
-                if (e.errno == errno.ENOENT or e.errno == errno.EACCES or
-                    e.errno == errno.ECHILD):
-                    _wdiff_available = False
-                else:
-                    raise e
-            except ValueError, e:
-                wdiff_failed = True
-
-            out = open(filename, 'wb')
-
-            if not _wdiff_available:
-                out.write(
-                    "wdiff not installed.<br/> "
-                    "If you're running OS X, you can install via macports."
-                    "<br/>"
-                    "If running Ubuntu linux, you can run "
-                    "'sudo apt-get install wdiff'.")
-            elif wdiff_failed:
-                out.write('wdiff failed due to running with multiple '
-                          'test_shells in parallel.')
-            else:
-                wdiff = cgi.escape(wdiff)
-                wdiff = wdiff.replace('##WDIFF_DEL##', '<span class=del>')
-                wdiff = wdiff.replace('##WDIFF_ADD##', '<span class=add>')
-                wdiff = wdiff.replace('##WDIFF_END##', '</span>')
-                out.write('<head><style>.del { background: #faa; } ')
-                out.write('.add { background: #afa; }</style></head>')
-                out.write('<pre>' + wdiff + '</pre>')
-
-            out.close()
+            wdiff = port.wdiff_text(expected_filename, actual_filename)
+            filename = self.output_filename(filename, test_type +
+                                            self.FILENAME_SUFFIX_WDIFF)
+            out = open(filename, 'wb').write(wdiff)
index 3c895af..54b332b 100644 (file)
@@ -37,7 +37,6 @@ import errno
 import logging
 import os.path
 
-from port import path_utils
 from layout_package import test_failures
 from test_types import test_type_base
 
@@ -61,8 +60,8 @@ class TestTextDiff(test_type_base.TestTypeBase):
         """Given the filename of the test, read the expected output from a file
         and normalize the text.  Returns a string with the expected text, or ''
         if the expected output file was not found."""
-        # Read the platform-specific expected text.
-        expected_filename = path_utils.expected_filename(filename, '.txt')
+        # Read the port-specific expected text.
+        expected_filename = self._port.expected_filename(filename, '.txt')
         if show_sources:
             logging.debug('Using %s' % expected_filename)
 
@@ -79,7 +78,7 @@ class TestTextDiff(test_type_base.TestTypeBase):
         # Normalize line endings
         return text.strip("\r\n").replace("\r\n", "\n") + "\n"
 
-    def compare_output(self, filename, proc, output, test_args, target):
+    def compare_output(self, port, filename, output, test_args, target):
         """Implementation of CompareOutput that checks the output text against
         the expected text from the LayoutTest directory."""
         failures = []
@@ -95,10 +94,10 @@ class TestTextDiff(test_type_base.TestTypeBase):
                                                      test_args.show_sources)
 
         # Write output files for new tests, too.
-        if output != expected:
+        if port.compare_text(output, expected):
             # Text doesn't match, write output files.
-            self.write_output_files(filename, "", ".txt", output, expected,
-                                    diff=True, wdiff=True)
+            self.write_output_files(port, filename, "", ".txt", output,
+                                    expected, diff=True, wdiff=True)
 
             if expected == '':
                 failures.append(test_failures.FailureMissingResult(self))
@@ -107,7 +106,7 @@ class TestTextDiff(test_type_base.TestTypeBase):
 
         return failures
 
-    def diff_files(self, file1, file2):
+    def diff_files(self, port, file1, file2):
         """Diff two text files.
 
         Args:
@@ -118,5 +117,5 @@ class TestTextDiff(test_type_base.TestTypeBase):
           False otherwise.
         """
 
-        return (self.get_normalized_text(file1) !=
-                self.get_normalized_text(file2))
+        return port.compare_text(self.get_normalized_text(file1),
+                                     self.get_normalized_text(file2))