summaryrefslogtreecommitdiffstats
path: root/WebKitTools/Scripts/webkitpy/layout_tests
diff options
context:
space:
mode:
authorIain Merrick <husky@google.com>2010-08-19 17:55:56 +0100
committerIain Merrick <husky@google.com>2010-08-23 11:05:40 +0100
commitf486d19d62f1bc33246748b14b14a9dfa617b57f (patch)
tree195485454c93125455a30e553a73981c3816144d /WebKitTools/Scripts/webkitpy/layout_tests
parent6ba0b43722d16bc295606bec39f396f596e4fef1 (diff)
downloadexternal_webkit-f486d19d62f1bc33246748b14b14a9dfa617b57f.zip
external_webkit-f486d19d62f1bc33246748b14b14a9dfa617b57f.tar.gz
external_webkit-f486d19d62f1bc33246748b14b14a9dfa617b57f.tar.bz2
Merge WebKit at r65615 : Initial merge by git.
Change-Id: Ifbf384f4531e3b58475a662e38195c2d9152ae79
Diffstat (limited to 'WebKitTools/Scripts/webkitpy/layout_tests')
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/deduplicate_tests.py167
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/deduplicate_tests_unittest.py147
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py90
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py4
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py93
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/layout_package/printing.py6
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations.py3
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_failures.py9
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py4
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/port/factory.py3
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome.py74
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py46
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/port/test.py3
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py3
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py14
-rwxr-xr-xWebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py142
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py113
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/test_types/fuzzy_image_diff.py71
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/test_types/image_diff.py6
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/test_types/test_type_base.py3
-rw-r--r--WebKitTools/Scripts/webkitpy/layout_tests/test_types/text_diff.py22
21 files changed, 790 insertions, 233 deletions
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/deduplicate_tests.py b/WebKitTools/Scripts/webkitpy/layout_tests/deduplicate_tests.py
new file mode 100644
index 0000000..bb63f5e
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/deduplicate_tests.py
@@ -0,0 +1,167 @@
+#!/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:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS 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 APPLE OR ITS 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.
+
+"""deduplicate_tests -- lists duplicated between platforms.
+
+If platform/mac-leopard is missing an expected test output, we fall back on
+platform/mac. This means it's possible to grow redundant test outputs,
+where we have the same expected data in both a platform directory and another
+platform it falls back on.
+"""
+
+import collections
+import fnmatch
+import os
+import subprocess
+import sys
+import re
+import webkitpy.common.system.executive as executive
+import webkitpy.common.system.logutils as logutils
+import webkitpy.layout_tests.port.factory as port_factory
+
+_log = logutils.get_logger(__file__)
+
+_BASE_PLATFORM = 'base'
+
+
+def port_fallbacks():
+ """Get the port fallback information.
+ Returns:
+ A dictionary mapping platform name to a list of other platforms to fall
+ back on. All platforms fall back on 'base'.
+ """
+ fallbacks = {_BASE_PLATFORM: []}
+ for port_name in os.listdir(os.path.join('LayoutTests', 'platform')):
+ try:
+ platforms = port_factory.get(port_name).baseline_search_path()
+ except NotImplementedError:
+ _log.error("'%s' lacks baseline_search_path(), please fix." % port_name)
+ fallbacks[port_name] = [_BASE_PLATFORM]
+ continue
+ fallbacks[port_name] = [os.path.basename(p) for p in platforms][1:]
+ fallbacks[port_name].append(_BASE_PLATFORM)
+ return fallbacks
+
+
+def parse_git_output(git_output, glob_pattern):
+ """Parses the output of git ls-tree and filters based on glob_pattern.
+ Args:
+ git_output: result of git ls-tree -r HEAD LayoutTests.
+ glob_pattern: a pattern to filter the files.
+ Returns:
+ A dictionary mapping (test name, hash of content) => [paths]
+ """
+ hashes = collections.defaultdict(set)
+ for line in git_output.split('\n'):
+ if not line:
+ break
+ attrs, path = line.strip().split('\t')
+ if not fnmatch.fnmatch(path, glob_pattern):
+ continue
+ path = path[len('LayoutTests/'):]
+ match = re.match(r'^(platform/.*?/)?(.*)', path)
+ test = match.group(2)
+ _, _, hash = attrs.split(' ')
+ hashes[(test, hash)].add(path)
+ return hashes
+
+
+def cluster_file_hashes(glob_pattern):
+ """Get the hashes of all the test expectations in the tree.
+ We cheat and use git's hashes.
+ Args:
+ glob_pattern: a pattern to filter the files.
+ Returns:
+ A dictionary mapping (test name, hash of content) => [paths]
+ """
+
+ # A map of file hash => set of all files with that hash.
+ hashes = collections.defaultdict(set)
+
+ # Fill in the map.
+ cmd = ('git', 'ls-tree', '-r', 'HEAD', 'LayoutTests')
+ try:
+ git_output = executive.Executive().run_command(cmd)
+ except OSError, e:
+ if e.errno == 2: # No such file or directory.
+ _log.error("Error: 'No such file' when running git.")
+ _log.error("This script requires git.")
+ sys.exit(1)
+ raise e
+ return parse_git_output(git_output, glob_pattern)
+
+
+def extract_platforms(paths):
+ """Extracts the platforms from a list of paths matching ^platform/(.*?)/.
+ Args:
+ paths: a list of paths.
+ Returns:
+ A dictionary containing all platforms from paths.
+ """
+ platforms = {}
+ for path in paths:
+ match = re.match(r'^platform/(.*?)/', path)
+ if match:
+ platform = match.group(1)
+ else:
+ platform = _BASE_PLATFORM
+ platforms[platform] = path
+ return platforms
+
+
+def find_dups(hashes, port_fallbacks):
+ """Yields info about redundant test expectations.
+ Args:
+ hashes: a list of hashes as returned by cluster_file_hashes.
+ port_fallbacks: a list of fallback information as returned by get_port_fallbacks.
+ Returns:
+ a tuple containing (test, platform, fallback, platforms)
+ """
+ for (test, hash), cluster in hashes.items():
+ if len(cluster) < 2:
+ continue # Common case: only one file with that hash.
+
+ # Compute the list of platforms we have this particular hash for.
+ platforms = extract_platforms(cluster)
+ if len(platforms) == 1:
+ continue
+
+ # See if any of the platforms are redundant with each other.
+ for platform in platforms.keys():
+ for fallback in port_fallbacks[platform]:
+ if fallback in platforms.keys():
+ yield test, platform, fallback, platforms[platform]
+
+
+def deduplicate(glob_pattern):
+ """Traverses LayoutTests and returns information about duplicated files.
+ Args:
+ glob pattern to filter the files in LayoutTests.
+ Returns:
+ a dictionary containing test, path, platform and fallback.
+ """
+ fallbacks = port_fallbacks()
+ hashes = cluster_file_hashes(glob_pattern)
+ return [{'test': test, 'path': path, 'platform': platform, 'fallback': fallback}
+ for test, platform, fallback, path in find_dups(hashes, fallbacks)]
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/deduplicate_tests_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/deduplicate_tests_unittest.py
new file mode 100644
index 0000000..66dda32
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/deduplicate_tests_unittest.py
@@ -0,0 +1,147 @@
+#!/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:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. 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.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS 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 APPLE OR ITS 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.
+
+"""Unit tests for deduplicate_tests.py."""
+
+import deduplicate_tests
+import os
+import unittest
+import webkitpy.common.checkout.scm as scm
+
+
+class MockExecutive(object):
+ last_run_command = []
+ response = ''
+
+ class Executive(object):
+ def run_command(self,
+ args,
+ cwd=None,
+ input=None,
+ error_handler=None,
+ return_exit_code=False,
+ return_stderr=True,
+ decode_output=True):
+ MockExecutive.last_run_command += [args]
+ return MockExecutive.response
+
+
+class ListDuplicatesTest(unittest.TestCase):
+ def setUp(self):
+ MockExecutive.last_run_command = []
+ MockExecutive.response = ''
+ deduplicate_tests.executive = MockExecutive
+ self._original_cwd = os.getcwd()
+ checkout_root = scm.find_checkout_root()
+ self.assertNotEqual(checkout_root, None)
+ os.chdir(checkout_root)
+
+ def tearDown(self):
+ os.chdir(self._original_cwd)
+
+ def test_parse_git_output(self):
+ git_output = (
+ '100644 blob 5053240b3353f6eb39f7cb00259785f16d121df2\tLayoutTests/mac/foo-expected.txt\n'
+ '100644 blob a004548d107ecc4e1ea08019daf0a14e8634a1ff\tLayoutTests/platform/chromium/foo-expected.txt\n'
+ '100644 blob d6bb0bc762e3aec5df03b5c04485b2cb3b65ffb1\tLayoutTests/platform/chromium-linux/foo-expected.txt\n'
+ '100644 blob abcdebc762e3aec5df03b5c04485b2cb3b65ffb1\tLayoutTests/platform/chromium-linux/animage.png\n'
+ '100644 blob d6bb0bc762e3aec5df03b5c04485b2cb3b65ffb1\tLayoutTests/platform/chromium-win/foo-expected.txt\n'
+ '100644 blob abcdebc762e3aec5df03b5c04485b2cb3b65ffb1\tLayoutTests/platform/chromium-win/animage.png\n'
+ '100644 blob 4303df5389ca87cae83dd3236b8dd84e16606517\tLayoutTests/platform/mac/foo-expected.txt\n')
+ hashes = deduplicate_tests.parse_git_output(git_output, '*')
+ expected = {('mac/foo-expected.txt', '5053240b3353f6eb39f7cb00259785f16d121df2'): set(['mac/foo-expected.txt']),
+ ('animage.png', 'abcdebc762e3aec5df03b5c04485b2cb3b65ffb1'): set(['platform/chromium-linux/animage.png', 'platform/chromium-win/animage.png']),
+ ('foo-expected.txt', '4303df5389ca87cae83dd3236b8dd84e16606517'): set(['platform/mac/foo-expected.txt']),
+ ('foo-expected.txt', 'd6bb0bc762e3aec5df03b5c04485b2cb3b65ffb1'): set(['platform/chromium-linux/foo-expected.txt', 'platform/chromium-win/foo-expected.txt']),
+ ('foo-expected.txt', 'a004548d107ecc4e1ea08019daf0a14e8634a1ff'): set(['platform/chromium/foo-expected.txt'])}
+ self.assertEquals(expected, hashes)
+
+ hashes = deduplicate_tests.parse_git_output(git_output, '*.png')
+ expected = {('animage.png', 'abcdebc762e3aec5df03b5c04485b2cb3b65ffb1'): set(['platform/chromium-linux/animage.png', 'platform/chromium-win/animage.png'])}
+ self.assertEquals(expected, hashes)
+
+ def test_extract_platforms(self):
+ self.assertEquals({'foo': 'platform/foo/bar',
+ 'zoo': 'platform/zoo/com'},
+ deduplicate_tests.extract_platforms(['platform/foo/bar', 'platform/zoo/com']))
+ self.assertEquals({'foo': 'platform/foo/bar',
+ deduplicate_tests._BASE_PLATFORM: 'what/'},
+ deduplicate_tests.extract_platforms(['platform/foo/bar', 'what/']))
+
+ def test_unique(self):
+ MockExecutive.response = (
+ '100644 blob 5053240b3353f6eb39f7cb00259785f16d121df2\tLayoutTests/mac/foo-expected.txt\n'
+ '100644 blob a004548d107ecc4e1ea08019daf0a14e8634a1ff\tLayoutTests/platform/chromium/foo-expected.txt\n'
+ '100644 blob abcd0bc762e3aec5df03b5c04485b2cb3b65ffb1\tLayoutTests/platform/chromium-linux/foo-expected.txt\n'
+ '100644 blob d6bb0bc762e3aec5df03b5c04485b2cb3b65ffb1\tLayoutTests/platform/chromium-win/foo-expected.txt\n'
+ '100644 blob 4303df5389ca87cae83dd3236b8dd84e16606517\tLayoutTests/platform/mac/foo-expected.txt\n')
+ result = deduplicate_tests.deduplicate('*')
+ self.assertEquals(1, len(MockExecutive.last_run_command))
+ self.assertEquals(('git', 'ls-tree', '-r', 'HEAD', 'LayoutTests'), MockExecutive.last_run_command[-1])
+ self.assertEquals(0, len(result))
+
+ def test_duplicates(self):
+ MockExecutive.response = (
+ '100644 blob 5053240b3353f6eb39f7cb00259785f16d121df2\tLayoutTests/mac/foo-expected.txt\n'
+ '100644 blob a004548d107ecc4e1ea08019daf0a14e8634a1ff\tLayoutTests/platform/chromium/foo-expected.txt\n'
+ '100644 blob d6bb0bc762e3aec5df03b5c04485b2cb3b65ffb1\tLayoutTests/platform/chromium-linux/foo-expected.txt\n'
+ '100644 blob abcdebc762e3aec5df03b5c04485b2cb3b65ffb1\tLayoutTests/platform/chromium-linux/animage.png\n'
+ '100644 blob d6bb0bc762e3aec5df03b5c04485b2cb3b65ffb1\tLayoutTests/platform/chromium-win/foo-expected.txt\n'
+ '100644 blob abcdebc762e3aec5df03b5c04485b2cb3b65ffb1\tLayoutTests/platform/chromium-win/animage.png\n'
+ '100644 blob 4303df5389ca87cae83dd3236b8dd84e16606517\tLayoutTests/platform/mac/foo-expected.txt\n')
+
+ result = deduplicate_tests.deduplicate('*')
+ self.assertEquals(1, len(MockExecutive.last_run_command))
+ self.assertEquals(('git', 'ls-tree', '-r', 'HEAD', 'LayoutTests'), MockExecutive.last_run_command[-1])
+ self.assertEquals(2, len(result))
+ self.assertEquals({'test': 'animage.png',
+ 'path': 'platform/chromium-linux/animage.png',
+ 'fallback': 'chromium-win',
+ 'platform': 'chromium-linux'},
+ result[0])
+ self.assertEquals({'test': 'foo-expected.txt',
+ 'path': 'platform/chromium-linux/foo-expected.txt',
+ 'fallback': 'chromium-win',
+ 'platform': 'chromium-linux'},
+ result[1])
+
+ result = deduplicate_tests.deduplicate('*.txt')
+ self.assertEquals(2, len(MockExecutive.last_run_command))
+ self.assertEquals(('git', 'ls-tree', '-r', 'HEAD', 'LayoutTests'), MockExecutive.last_run_command[-1])
+ self.assertEquals(1, len(result))
+ self.assertEquals({'test': 'foo-expected.txt',
+ 'path': 'platform/chromium-linux/foo-expected.txt',
+ 'fallback': 'chromium-win',
+ 'platform': 'chromium-linux'},
+ result[0])
+
+ result = deduplicate_tests.deduplicate('*.png')
+ self.assertEquals(3, len(MockExecutive.last_run_command))
+ self.assertEquals(('git', 'ls-tree', '-r', 'HEAD', 'LayoutTests'), MockExecutive.last_run_command[-1])
+ self.assertEquals(1, len(result))
+ self.assertEquals({'test': 'animage.png',
+ 'path': 'platform/chromium-linux/animage.png',
+ 'fallback': 'chromium-win',
+ 'platform': 'chromium-linux'},
+ result[0])
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py
index 6364511..6343400 100644
--- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py
@@ -54,9 +54,9 @@ _log = logging.getLogger("webkitpy.layout_tests.layout_package."
"dump_render_tree_thread")
-def process_output(port, test_info, test_types, test_args, configuration,
- output_dir, crash, timeout, test_run_time, actual_checksum,
- output, error):
+def _process_output(port, test_info, test_types, test_args, configuration,
+ output_dir, crash, timeout, test_run_time, actual_checksum,
+ output, error):
"""Receives the output from a DumpRenderTree process, subjects it to a
number of tests, and returns a list of failure types the test produced.
@@ -118,6 +118,21 @@ def process_output(port, test_info, test_types, test_args, configuration,
total_time_for_all_diffs, time_for_diffs)
+def _pad_timeout(timeout):
+ """Returns a safe multiple of the per-test timeout value to use
+ to detect hung test threads.
+
+ """
+ # When we're running one test per DumpRenderTree process, we can
+ # enforce a hard timeout. The DumpRenderTree watchdog uses 2.5x
+ # the timeout; we want to be larger than that.
+ return timeout * 3
+
+
+def _milliseconds_to_seconds(msecs):
+ return float(msecs) / 1000.0
+
+
class TestResult(object):
def __init__(self, filename, failures, test_run_time,
@@ -162,7 +177,7 @@ class SingleTestThread(threading.Thread):
driver.run_test(test_info.uri.strip(), test_info.timeout,
test_info.image_hash())
end = time.time()
- self._test_result = process_output(self._port,
+ self._test_result = _process_output(self._port,
test_info, self._test_types, self._test_args,
self._configuration, self._output_dir, crash, timeout, end - start,
actual_checksum, output, error)
@@ -172,8 +187,42 @@ class SingleTestThread(threading.Thread):
return self._test_result
-class TestShellThread(threading.Thread):
+class WatchableThread(threading.Thread):
+ """This class abstracts an interface used by
+ run_webkit_tests.TestRunner._wait_for_threads_to_finish for thread
+ management."""
+ def __init__(self):
+ threading.Thread.__init__(self)
+ self._canceled = False
+ self._exception_info = None
+ self._next_timeout = None
+ self._thread_id = None
+
+ def cancel(self):
+ """Set a flag telling this thread to quit."""
+ self._canceled = True
+
+ def clear_next_timeout(self):
+ """Mark a flag telling this thread to stop setting timeouts."""
+ self._timeout = 0
+
+ def exception_info(self):
+ """If run() terminated on an uncaught exception, return it here
+ ((type, value, traceback) tuple).
+ Returns None if run() terminated normally. Meant to be called after
+ joining this thread."""
+ return self._exception_info
+
+ def id(self):
+ """Return a thread identifier."""
+ return self._thread_id
+
+ def next_timeout(self):
+ """Return the time the test is supposed to finish by."""
+ return self._next_timeout
+
+class TestShellThread(WatchableThread):
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 DumpRenderTree thread.
@@ -192,7 +241,7 @@ class TestShellThread(threading.Thread):
command-line options should match those expected by
run_webkit_tests; they are typically passed via the
run_webkit_tests.TestRunner class."""
- threading.Thread.__init__(self)
+ WatchableThread.__init__(self)
self._port = port
self._filename_list_queue = filename_list_queue
self._result_queue = result_queue
@@ -203,8 +252,6 @@ class TestShellThread(threading.Thread):
self._image_path = image_path
self._shell_args = shell_args
self._options = options
- self._canceled = False
- self._exception_info = None
self._directory_timing_stats = {}
self._test_results = []
self._num_tests = 0
@@ -231,17 +278,6 @@ class TestShellThread(threading.Thread):
"""
return self._test_results
- def cancel(self):
- """Set a flag telling this thread to quit."""
- self._canceled = True
-
- def get_exception_info(self):
- """If run() terminated on an uncaught exception, return it here
- ((type, value, traceback) tuple).
- Returns None if run() terminated normally. Meant to be called after
- joining this thread."""
- return self._exception_info
-
def get_total_time(self):
return max(self._stop_time - self._start_time, 0.0)
@@ -251,6 +287,7 @@ class TestShellThread(threading.Thread):
def run(self):
"""Delegate main work to a helper method and watch for uncaught
exceptions."""
+ self._thread_id = thread.get_ident()
self._start_time = time.time()
self._num_tests = 0
try:
@@ -384,10 +421,10 @@ class TestShellThread(threading.Thread):
worker.start()
- # When we're running one test per DumpRenderTree process, we can
- # enforce a hard timeout. The DumpRenderTree watchdog uses 2.5x
- # the timeout; we want to be larger than that.
- worker.join(int(test_info.timeout) * 3.0 / 1000.0)
+ thread_timeout = _milliseconds_to_seconds(
+ _pad_timeout(test_info.timeout))
+ thread._next_timeout = time.time() + thread_timeout
+ worker.join(thread_timeout)
if worker.isAlive():
# If join() returned with the thread still running, the
# DumpRenderTree is completely hung and there's nothing
@@ -433,11 +470,16 @@ class TestShellThread(threading.Thread):
not self._options.pixel_tests)):
image_hash = ""
start = time.time()
+
+ thread_timeout = _milliseconds_to_seconds(
+ _pad_timeout(test_info.timeout))
+ self._next_timeout = start + thread_timeout
+
crash, timeout, actual_checksum, output, error = \
self._driver.run_test(test_info.uri, test_info.timeout, image_hash)
end = time.time()
- result = process_output(self._port, test_info, self._test_types,
+ result = _process_output(self._port, test_info, self._test_types,
self._test_args, self._options.configuration,
self._options.results_directory, crash,
timeout, end - start, actual_checksum,
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py
index 6c36c93..c6c3066 100644
--- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py
@@ -57,7 +57,7 @@ class JSONLayoutResultsGenerator(json_results_generator.JSONResultsGeneratorBase
def __init__(self, port, builder_name, build_name, build_number,
results_file_base_path, builder_base_url,
test_timings, expectations, result_summary, all_tests,
- generate_incremental_results=False):
+ generate_incremental_results=False, test_results_server=None):
"""Modifies the results.json file. Grabs it off the archive directory
if it is not found locally.
@@ -68,7 +68,7 @@ class JSONLayoutResultsGenerator(json_results_generator.JSONResultsGeneratorBase
super(JSONLayoutResultsGenerator, self).__init__(
builder_name, build_name, build_number, results_file_base_path,
builder_base_url, {}, port.test_repository_paths(),
- generate_incremental_results)
+ generate_incremental_results, test_results_server)
self._port = port
self._expectations = expectations
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py
index e746bc0..15eceee 100644
--- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/json_results_generator.py
@@ -84,10 +84,14 @@ class JSONResultsGeneratorBase(object):
RESULTS_FILENAME = "results.json"
INCREMENTAL_RESULTS_FILENAME = "incremental_results.json"
+ URL_FOR_TEST_LIST_JSON = \
+ "http://%s/testfile?builder=%s&name=%s&testlistjson=1"
+
def __init__(self, builder_name, build_name, build_number,
results_file_base_path, builder_base_url,
test_results_map, svn_repositories=None,
- generate_incremental_results=False):
+ generate_incremental_results=False,
+ test_results_server=None):
"""Modifies the results.json file. Grabs it off the archive directory
if it is not found locally.
@@ -103,6 +107,9 @@ class JSONResultsGeneratorBase(object):
svn_repositories: A (json_field_name, svn_path) pair for SVN
repositories that tests rely on. The SVN revision will be
included in the JSON with the given json_field_name.
+ generate_incremental_results: If true, generate incremental json file
+ from current run results.
+ test_results_server: server that hosts test results json.
"""
self._builder_name = builder_name
self._build_name = build_name
@@ -121,6 +128,8 @@ class JSONResultsGeneratorBase(object):
if not self._svn_repositories:
self._svn_repositories = {}
+ self._test_results_server = test_results_server
+
self._json = None
self._archived_results = None
@@ -144,25 +153,24 @@ class JSONResultsGeneratorBase(object):
def get_json(self, incremental=False):
"""Gets the results for the results.json file."""
- if incremental:
- results_json = {}
- else:
+ results_json = {}
+ if not incremental:
if self._json:
return self._json
- if not self._archived_results:
- self._archived_results, error = \
- self._get_archived_json_results()
- if error:
- # If there was an error don't write a results.json
- # file at all as it would lose all the information on the
- # bot.
- _log.error("Archive directory is inaccessible. Not "
- "modifying or clobbering the results.json "
- "file: " + str(error))
- return None
+ if self._archived_results:
+ results_json = self._archived_results
- results_json = self._archived_results
+ if not results_json:
+ results_json, error = self._get_archived_json_results(incremental)
+ if error:
+ # If there was an error don't write a results.json
+ # file at all as it would lose all the information on the
+ # bot.
+ _log.error("Archive directory is inaccessible. Not "
+ "modifying or clobbering the results.json "
+ "file: " + str(error))
+ return None
builder_name = self._builder_name
if results_json and builder_name not in results_json:
@@ -186,7 +194,7 @@ class JSONResultsGeneratorBase(object):
all_failing_tests = self._get_failed_test_names()
all_failing_tests.update(tests.iterkeys())
for test in all_failing_tests:
- self._insert_test_time_and_result(test, tests)
+ self._insert_test_time_and_result(test, tests, incremental)
return results_json
@@ -253,24 +261,40 @@ class JSONResultsGeneratorBase(object):
return ""
return ""
- def _get_archived_json_results(self):
+ def _get_archived_json_results(self, for_incremental=False):
"""Reads old results JSON file if it exists.
Returns (archived_results, error) tuple where error is None if results
were successfully read.
+
+ if for_incremental is True, download JSON file that only contains test
+ name list from test-results server. This is for generating incremental
+ JSON so the file generated has info for tests that failed before but
+ pass or are skipped from current run.
"""
results_json = {}
old_results = None
error = None
- if os.path.exists(self._results_file_path):
+ if os.path.exists(self._results_file_path) and not for_incremental:
with codecs.open(self._results_file_path, "r", "utf-8") as file:
old_results = file.read()
- elif self._builder_base_url:
- # Check if we have the archived JSON file on the buildbot server.
- results_file_url = (self._builder_base_url +
- self._build_name + "/" + self.RESULTS_FILENAME)
- _log.error("Local results.json file does not exist. Grabbing "
- "it off the archive at " + results_file_url)
+ elif self._builder_base_url or for_incremental:
+ if for_incremental:
+ if not self._test_results_server:
+ # starting from fresh if no test results server specified.
+ return {}, None
+
+ results_file_url = (self.URL_FOR_TEST_LIST_JSON %
+ (urllib2.quote(self._test_results_server),
+ urllib2.quote(self._builder_name),
+ self.RESULTS_FILENAME))
+ else:
+ # Check if we have the archived JSON file on the buildbot
+ # server.
+ results_file_url = (self._builder_base_url +
+ self._build_name + "/" + self.RESULTS_FILENAME)
+ _log.error("Local results.json file does not exist. Grabbing "
+ "it off the archive at " + results_file_url)
try:
results_file = urllib2.urlopen(results_file_url)
@@ -387,7 +411,7 @@ class JSONResultsGeneratorBase(object):
int(time.time()),
self.TIME)
- def _insert_test_time_and_result(self, test_name, tests):
+ def _insert_test_time_and_result(self, test_name, tests, incremental=False):
""" Insert a test item with its results to the given tests dictionary.
Args:
@@ -401,9 +425,20 @@ class JSONResultsGeneratorBase(object):
tests[test_name] = self._create_results_and_times_json()
thisTest = tests[test_name]
- self._insert_item_run_length_encoded(result, thisTest[self.RESULTS])
- self._insert_item_run_length_encoded(time, thisTest[self.TIMES])
- self._normalize_results_json(thisTest, test_name, tests)
+ if self.RESULTS in thisTest:
+ self._insert_item_run_length_encoded(result, thisTest[self.RESULTS])
+ else:
+ thisTest[self.RESULTS] = [[1, result]]
+
+ if self.TIMES in thisTest:
+ self._insert_item_run_length_encoded(time, thisTest[self.TIMES])
+ else:
+ thisTest[self.TIMES] = [[1, time]]
+
+ # Don't normalize the incremental results json because we need results
+ # for tests that pass or have no data from current run.
+ if not incremental:
+ self._normalize_results_json(thisTest, test_name, tests)
def _convert_json_to_current_version(self, results_json):
"""If the JSON does not match the current version, converts it to the
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/printing.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/printing.py
index f838a7b..81cdc9b 100644
--- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/printing.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/printing.py
@@ -123,12 +123,6 @@ def print_options():
help="show detailed help on controlling print output"),
optparse.make_option("-v", "--verbose", action="store_true",
default=False, help="include debug-level logging"),
-
- # FIXME: we should remove this; it's pretty much obsolete with the
- # --print trace-everything option.
- optparse.make_option("--sources", action="store_true",
- help=("show expected result file path for each test "
- "(implies --verbose)")),
]
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations.py
index 38223dd..e154932 100644
--- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_expectations.py
@@ -460,6 +460,9 @@ class TestExpectationsFile:
return ExpectationsJsonEncoder(separators=(',', ':')).encode(
self._all_expectations)
+ def get_non_fatal_errors(self):
+ return self._non_fatal_errors
+
def contains(self, test):
return test in self._test_to_expectations
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_failures.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_failures.py
index 60bdbca..3be9240 100644
--- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_failures.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/test_failures.py
@@ -250,15 +250,6 @@ class FailureImageHashMismatch(FailureWithType):
return "Image mismatch"
-class FailureFuzzyFailure(FailureWithType):
- """Image hashes didn't match."""
- OUT_FILENAMES = ["-actual.png", "-expected.png"]
-
- @staticmethod
- def message():
- return "Fuzzy image match also failed"
-
-
class FailureImageHashIncorrect(FailureWithType):
"""Actual result hash is incorrect."""
# Chrome doesn't know to display a .checksum file as text, so don't bother
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py
index 8072bc0..e9a81e7 100644
--- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_win.py
@@ -69,9 +69,9 @@ class ChromiumWinPort(chromium.ChromiumPort):
def baseline_search_path(self):
port_names = []
- if self._name == 'chromium-win-xp':
+ if self._name.endswith('-win-xp'):
port_names.append("chromium-win-xp")
- if self._name in ('chromium-win-xp', 'chromium-win-vista'):
+ if self._name.endswith('-win-xp') or self._name.endswith('-win-vista'):
port_names.append("chromium-win-vista")
# FIXME: This may need to include mac-snowleopard like win.py.
port_names.extend(["chromium-win", "chromium", "win", "mac"])
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/factory.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/factory.py
index 95b90da..258bf33 100644
--- a/WebKitTools/Scripts/webkitpy/layout_tests/port/factory.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/factory.py
@@ -83,5 +83,8 @@ def get(port_name=None, options=None):
elif port_to_use.startswith('chromium-win'):
import chromium_win
return chromium_win.ChromiumWinPort(port_name, options)
+ elif port_to_use.startswith('google-chrome'):
+ import google_chrome
+ return google_chrome.GetGoogleChromePort(port_name, options)
raise NotImplementedError('unsupported port: %s' % port_to_use)
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome.py
new file mode 100644
index 0000000..1ea053b
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome.py
@@ -0,0 +1,74 @@
+#!/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.
+
+# 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.
+
+
+def GetGoogleChromePort(port_name, options):
+ """Some tests have slightly different results when compiled as Google
+ Chrome vs Chromium. In those cases, we prepend an additional directory to
+ to the baseline paths."""
+ if port_name == 'google-chrome-linux32':
+ import chromium_linux
+
+ class GoogleChromeLinux32Port(chromium_linux.ChromiumLinuxPort):
+ def baseline_search_path(self):
+ paths = chromium_linux.ChromiumLinuxPort.baseline_search_path(
+ self)
+ paths.insert(0, self._webkit_baseline_path(self._name))
+ return paths
+ return GoogleChromeLinux32Port(port_name, options)
+ elif port_name == 'google-chrome-linux64':
+ import chromium_linux
+
+ class GoogleChromeLinux64Port(chromium_linux.ChromiumLinuxPort):
+ def baseline_search_path(self):
+ paths = chromium_linux.ChromiumLinuxPort.baseline_search_path(
+ self)
+ paths.insert(0, self._webkit_baseline_path(self._name))
+ return paths
+ return GoogleChromeLinux64Port(port_name, options)
+ elif port_name.startswith('google-chrome-mac'):
+ import chromium_mac
+
+ class GoogleChromeMacPort(chromium_mac.ChromiumMacPort):
+ def baseline_search_path(self):
+ paths = chromium_mac.ChromiumMacPort.baseline_search_path(
+ self)
+ paths.insert(0, self._webkit_baseline_path(
+ 'google-chrome-mac'))
+ return paths
+ return GoogleChromeMacPort(port_name, options)
+ elif port_name.startswith('google-chrome-win'):
+ import chromium_win
+
+ class GoogleChromeWinPort(chromium_win.ChromiumWinPort):
+ def baseline_search_path(self):
+ paths = chromium_win.ChromiumWinPort.baseline_search_path(
+ self)
+ paths.insert(0, self._webkit_baseline_path(
+ 'google-chrome-win'))
+ return paths
+ return GoogleChromeWinPort(port_name, options)
+ raise NotImplementedError('unsupported port: %s' % port_name)
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py
new file mode 100644
index 0000000..a2d7056
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py
@@ -0,0 +1,46 @@
+#!/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.
+
+# 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.
+
+import os
+import unittest
+import google_chrome
+
+
+class GetGoogleChromePortTest(unittest.TestCase):
+ def test_get_google_chrome_port(self):
+ test_ports = ('google-chrome-linux32', 'google-chrome-linux64',
+ 'google-chrome-mac', 'google-chrome-win')
+ for port in test_ports:
+ self._verify_baseline_path(port, port)
+
+ self._verify_baseline_path('google-chrome-mac', 'google-chrome-mac-leopard')
+ self._verify_baseline_path('google-chrome-win', 'google-chrome-win-xp')
+ self._verify_baseline_path('google-chrome-win', 'google-chrome-win-vista')
+
+ def _verify_baseline_path(self, expected_path, port_name):
+ port = google_chrome.GetGoogleChromePort(port_name, None)
+ path = port.baseline_search_path()[0]
+ self.assertEqual(expected_path, os.path.split(path)[1])
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/test.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/test.py
index 6eef54e..9c9ab0a 100644
--- a/WebKitTools/Scripts/webkitpy/layout_tests/port/test.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/test.py
@@ -124,6 +124,9 @@ class TestPort(base.Port):
def test_platform_names(self):
return self.test_base_platform_names()
+ def test_platform_name_to_name(self, test_platform_name):
+ return test_platform_name
+
def version():
return ''
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py
index fa4df9b..92f1032 100644
--- a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py
@@ -59,7 +59,6 @@ import webbrowser
import zipfile
from webkitpy.common.system.executive import run_command, ScriptError
-from webkitpy.common.checkout.scm import detect_scm_system
import webkitpy.common.checkout.scm as scm
import port
@@ -240,7 +239,7 @@ class Rebaseliner(object):
self._platform,
False,
False)
- self._scm = detect_scm_system(os.getcwd())
+ self._scm = scm.default_scm()
def run(self, backup):
"""Run rebaseline process."""
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py
index fa03238..121b64e 100644
--- a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py
@@ -84,5 +84,19 @@ class TestGetHostPortObject(unittest.TestCase):
port.get = old_get
+class TestRebaseliner(unittest.TestCase):
+
+ def test_noop(self):
+ # this method tests that was can at least instantiate an object, even
+ # if there is nothing to do.
+ options = MockOptions()
+ host_port_obj = port.get('test', options)
+ target_options = options
+ target_port_obj = port.get('test', target_options)
+ platform = 'test'
+ rebaseliner = rebaseline_chromium_webkit_tests.Rebaseliner(
+ host_port_obj, target_port_obj, platform, options)
+ self.assertNotEqual(rebaseliner, None)
+
if __name__ == '__main__':
unittest.main()
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py
index 490ac3c..b26bc6c 100755
--- a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py
@@ -53,6 +53,7 @@ import logging
import math
import optparse
import os
+import pdb
import platform
import Queue
import random
@@ -70,7 +71,6 @@ from layout_package import test_expectations
from layout_package import test_failures
from layout_package import test_files
from layout_package import test_results_uploader
-from test_types import fuzzy_image_diff
from test_types import image_diff
from test_types import text_diff
from test_types import test_type_base
@@ -578,8 +578,6 @@ class TestRunner:
test_args.new_baseline = self._options.new_baseline
test_args.reset_results = self._options.reset_results
- test_args.show_sources = self._options.sources
-
if self._options.startup_dialog:
shell_args.append('--testshell-startup-dialog')
@@ -629,35 +627,12 @@ class TestRunner:
"""Returns whether we should run all the tests in the main thread."""
return int(self._options.child_processes) == 1
- def _dump_thread_states(self):
- for thread_id, stack in sys._current_frames().items():
- # FIXME: Python 2.6 has thread.ident which we could
- # use to map from thread_id back to thread.name
- print "\n# Thread: %d" % thread_id
- for filename, lineno, name, line in traceback.extract_stack(stack):
- print 'File: "%s", line %d, in %s' % (filename, lineno, name)
- if line:
- print " %s" % (line.strip())
-
- def _dump_thread_states_if_necessary(self):
- # HACK: Dump thread states every minute to figure out what's
- # hanging on the bots.
- if not self._options.verbose:
- return
- dump_threads_every = 60 # Dump every minute
- if not self._last_thread_dump:
- self._last_thread_dump = time.time()
- time_since_last_dump = time.time() - self._last_thread_dump
- if time_since_last_dump > dump_threads_every:
- self._dump_thread_states()
- self._last_thread_dump = time.time()
-
def _run_tests(self, file_list, result_summary):
"""Runs the tests in the file_list.
- Return: A tuple (failures, thread_timings, test_timings,
+ Return: A tuple (keyboard_interrupted, thread_timings, test_timings,
individual_test_timings)
- failures is a map from test to list of failure types
+ keyboard_interrupted is whether someone typed Ctrl^C
thread_timings is a list of dicts with the total runtime
of each thread with 'name', 'num_tests', 'total_time' properties
test_timings is a list of timings for each sharded subdirectory
@@ -676,44 +651,55 @@ class TestRunner:
result_summary)
self._printer.print_update("Starting testing ...")
- # Wait for the threads to finish and collect test failures.
- failures = {}
- test_timings = {}
- individual_test_timings = []
- thread_timings = []
+ keyboard_interrupted = self._wait_for_threads_to_finish(threads,
+ result_summary)
+ (thread_timings, test_timings, individual_test_timings) = \
+ self._collect_timing_info(threads)
+
+ return (keyboard_interrupted, thread_timings, test_timings,
+ individual_test_timings)
+
+ def _wait_for_threads_to_finish(self, threads, result_summary):
keyboard_interrupted = False
try:
# Loop through all the threads waiting for them to finish.
- for thread in threads:
- # FIXME: We'll end up waiting on the first thread the whole
- # time. That means we won't notice exceptions on other
- # threads until the first one exits.
- # We should instead while True: in the outer loop
- # and then loop through threads joining and checking
- # isAlive and get_exception_info. Exiting on any exception.
- while thread.isAlive():
- # Wake the main thread every 0.1 seconds so we
- # can call update_summary in a timely fashion.
- thread.join(0.1)
- # HACK: Used for debugging threads on the bots.
- self._dump_thread_states_if_necessary()
- self.update_summary(result_summary)
+ some_thread_is_alive = True
+ while some_thread_is_alive:
+ some_thread_is_alive = False
+ t = time.time()
+ for thread in threads:
+ exception_info = thread.exception_info()
+ if exception_info is not None:
+ # Re-raise the thread's exception here to make it
+ # clear that testing was aborted. Otherwise,
+ # the tests that did not run would be assumed
+ # to have passed.
+ raise (exception_info[0], exception_info[1],
+ exception_info[2])
+
+ if thread.isAlive():
+ some_thread_is_alive = True
+ next_timeout = thread.next_timeout()
+ if (next_timeout and t > next_timeout):
+ _log_wedged_thread(thread)
+ thread.clear_next_timeout()
+
+ self.update_summary(result_summary)
+
+ if some_thread_is_alive:
+ time.sleep(0.1)
except KeyboardInterrupt:
keyboard_interrupted = True
for thread in threads:
thread.cancel()
- if not keyboard_interrupted:
- for thread in threads:
- # Check whether a thread died before normal completion.
- exception_info = thread.get_exception_info()
- if exception_info is not None:
- # Re-raise the thread's exception here to make it clear
- # something went wrong. Otherwise, the tests that did not
- # run would be assumed to have passed.
- raise (exception_info[0], exception_info[1],
- exception_info[2])
+ return keyboard_interrupted
+
+ def _collect_timing_info(self, threads):
+ test_timings = {}
+ individual_test_timings = []
+ thread_timings = []
for thread in threads:
thread_timings.append({'name': thread.getName(),
@@ -721,8 +707,8 @@ class TestRunner:
'total_time': thread.get_total_time()})
test_timings.update(thread.get_directory_timing_stats())
individual_test_timings.extend(thread.get_test_results())
- return (keyboard_interrupted, thread_timings, test_timings,
- individual_test_timings)
+
+ return (thread_timings, test_timings, individual_test_timings)
def needs_http(self):
"""Returns whether the test runner needs an HTTP server."""
@@ -890,7 +876,8 @@ class TestRunner:
self._options.build_number, self._options.results_directory,
BUILDER_BASE_URL, individual_test_timings,
self._expectations, result_summary, self._test_files_list,
- not self._options.upload_full_results)
+ not self._options.upload_full_results,
+ self._options.test_results_server)
_log.debug("Finished writing JSON files.")
@@ -1440,8 +1427,6 @@ def run(port_obj, options, args, regular_output=sys.stderr,
test_runner.add_test_type(text_diff.TestTextDiff)
if options.pixel_tests:
test_runner.add_test_type(image_diff.ImageDiff)
- if options.fuzzy_pixel_tests:
- test_runner.add_test_type(fuzzy_image_diff.FuzzyImageDiff)
num_unexpected_results = test_runner.run(result_summary)
@@ -1524,9 +1509,6 @@ def parse_args(args=None):
dest="pixel_tests", help="Enable pixel-to-pixel PNG comparisons"),
optparse.make_option("--no-pixel-tests", action="store_false",
dest="pixel_tests", help="Disable pixel-to-pixel PNG comparisons"),
- optparse.make_option("--fuzzy-pixel-tests", action="store_true",
- default=False,
- help="Also use fuzzy matching to compare pixel test outputs."),
# old-run-webkit-tests allows a specific tolerance: --tolerance t
# Ignore image differences less than this percentage (default: 0.1)
optparse.make_option("--results-directory",
@@ -1674,12 +1656,38 @@ def parse_args(args=None):
option_parser = optparse.OptionParser(option_list=option_list)
options, args = option_parser.parse_args(args)
- if options.sources:
- options.verbose = True
return options, args
+def _find_thread_stack(id):
+ """Returns a stack object that can be used to dump a stack trace for
+ the given thread id (or None if the id is not found)."""
+ for thread_id, stack in sys._current_frames().items():
+ if thread_id == id:
+ return stack
+ return None
+
+
+def _log_stack(stack):
+ """Log a stack trace to log.error()."""
+ for filename, lineno, name, line in traceback.extract_stack(stack):
+ _log.error('File: "%s", line %d, in %s' % (filename, lineno, name))
+ if line:
+ _log.error(' %s' % line.strip())
+
+
+def _log_wedged_thread(thread):
+ """Log information about the given thread state."""
+ id = thread.id()
+ stack = _find_thread_stack(id)
+ assert(stack is not None)
+ _log.error("")
+ _log.error("thread %s (%d) is wedged" % (thread.getName(), id))
+ _log_stack(stack)
+ _log.error("")
+
+
def main():
options, args = parse_args()
port_obj = port.get(options.platform, options)
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py
index 1c751d6..e1b3746 100644
--- a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py
@@ -30,13 +30,20 @@
"""Unit tests for run_webkit_tests."""
import codecs
+import logging
import os
+import pdb
+import Queue
import sys
+import thread
+import time
+import threading
import unittest
from webkitpy.common import array_stream
from webkitpy.layout_tests import port
from webkitpy.layout_tests import run_webkit_tests
+from webkitpy.layout_tests.layout_package import dump_render_tree_thread
from webkitpy.thirdparty.mock import Mock
@@ -92,6 +99,7 @@ class MainTest(unittest.TestCase):
self.assertEqual(buildbot_output.get(), [])
+
def _mocked_open(original_open, file_list):
def _wrapper(name, mode, encoding):
if name.find("-expected.") != -1 and mode == "w":
@@ -191,5 +199,110 @@ class DryrunTest(unittest.TestCase):
'fast/html']))
+class TestThread(dump_render_tree_thread.WatchableThread):
+ def __init__(self, started_queue, stopping_queue):
+ dump_render_tree_thread.WatchableThread.__init__(self)
+ self._started_queue = started_queue
+ self._stopping_queue = stopping_queue
+ self._timeout = False
+ self._timeout_queue = Queue.Queue()
+
+ def run(self):
+ self._thread_id = thread.get_ident()
+ try:
+ self._started_queue.put('')
+ msg = self._stopping_queue.get()
+ if msg == 'KeyboardInterrupt':
+ raise KeyboardInterrupt
+ elif msg == 'Exception':
+ raise ValueError()
+ elif msg == 'Timeout':
+ self._timeout = True
+ self._timeout_queue.get()
+ except:
+ self._exception_info = sys.exc_info()
+
+ def next_timeout(self):
+ if self._timeout:
+ self._timeout_queue.put('done')
+ return time.time() - 10
+ return time.time()
+
+
+class TestHandler(logging.Handler):
+ def __init__(self, astream):
+ logging.Handler.__init__(self)
+ self._stream = astream
+
+ def emit(self, record):
+ self._stream.write(self.format(record))
+
+
+class WaitForThreadsToFinishTest(unittest.TestCase):
+ class MockTestRunner(run_webkit_tests.TestRunner):
+ def __init__(self):
+ pass
+
+ def __del__(self):
+ pass
+
+ def update_summary(self, result_summary):
+ pass
+
+ def run_one_thread(self, msg):
+ runner = self.MockTestRunner()
+ starting_queue = Queue.Queue()
+ stopping_queue = Queue.Queue()
+ child_thread = TestThread(starting_queue, stopping_queue)
+ child_thread.start()
+ started_msg = starting_queue.get()
+ stopping_queue.put(msg)
+ threads = [child_thread]
+ return runner._wait_for_threads_to_finish(threads, None)
+
+ def test_basic(self):
+ interrupted = self.run_one_thread('')
+ self.assertFalse(interrupted)
+
+ def test_interrupt(self):
+ interrupted = self.run_one_thread('KeyboardInterrupt')
+ self.assertTrue(interrupted)
+
+ def test_timeout(self):
+ interrupted = self.run_one_thread('Timeout')
+ self.assertFalse(interrupted)
+
+ def test_exception(self):
+ self.assertRaises(ValueError, self.run_one_thread, 'Exception')
+
+
+class StandaloneFunctionsTest(unittest.TestCase):
+ def test_log_wedged_thread(self):
+ logger = run_webkit_tests._log
+ astream = array_stream.ArrayStream()
+ handler = TestHandler(astream)
+ logger.addHandler(handler)
+
+ starting_queue = Queue.Queue()
+ stopping_queue = Queue.Queue()
+ child_thread = TestThread(starting_queue, stopping_queue)
+ child_thread.start()
+ msg = starting_queue.get()
+
+ run_webkit_tests._log_wedged_thread(child_thread)
+ stopping_queue.put('')
+ child_thread.join(timeout=1.0)
+
+ self.assertFalse(astream.empty())
+ self.assertFalse(child_thread.isAlive())
+
+ def test_find_thread_stack(self):
+ id, stack = sys._current_frames().items()[0]
+ found_stack = run_webkit_tests._find_thread_stack(id)
+ self.assertNotEqual(found_stack, None)
+
+ found_stack = run_webkit_tests._find_thread_stack(0)
+ self.assertEqual(found_stack, None)
+
if __name__ == '__main__':
unittest.main()
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/fuzzy_image_diff.py b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/fuzzy_image_diff.py
deleted file mode 100644
index 64dfb20..0000000
--- a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/fuzzy_image_diff.py
+++ /dev/null
@@ -1,71 +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.
-
-"""Compares the image output of a test to the expected image output using
-fuzzy matching.
-"""
-
-import errno
-import logging
-import os
-import shutil
-
-from webkitpy.layout_tests.layout_package import test_failures
-from webkitpy.layout_tests.test_types import test_type_base
-
-_log = logging.getLogger("webkitpy.layout_tests.test_types.fuzzy_image_diff")
-
-
-class FuzzyImageDiff(test_type_base.TestTypeBase):
-
- def compare_output(self, filename, output, test_args, configuration):
- """Implementation of CompareOutput that checks the output image and
- checksum against the expected files from the LayoutTest directory.
- """
- failures = []
-
- # If we didn't produce a hash file, this test must be text-only.
- if test_args.hash is None:
- return failures
-
- expected_png_file = self._port.expected_filename(filename, '.png')
-
- if test_args.show_sources:
- _log.debug('Using %s' % expected_png_file)
-
- # Also report a missing expected PNG file.
- if not os.path.isfile(expected_png_file):
- failures.append(test_failures.FailureMissingImage(self))
-
- # Run the fuzzymatcher
- r = self._port.fuzzy_diff(test_args.png_path, expected_png_file)
- if r != 0:
- failures.append(test_failures.FailureFuzzyFailure(self))
-
- return failures
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/image_diff.py b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/image_diff.py
index 65f8f3a..c9f4107 100644
--- a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/image_diff.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/image_diff.py
@@ -136,7 +136,7 @@ class ImageDiff(test_type_base.TestTypeBase):
# If we're generating a new baseline, we pass.
if test_args.new_baseline or test_args.reset_results:
self._save_baseline_files(filename, test_args.png_path,
- test_args.hash, test_args.new_baseline)
+ test_args.hash, test_args.new_baseline)
return failures
# Compare hashes.
@@ -144,10 +144,6 @@ class ImageDiff(test_type_base.TestTypeBase):
'.checksum')
expected_png_file = self._port.expected_filename(filename, '.png')
- if test_args.show_sources:
- _log.debug('Using %s' % expected_hash_file)
- _log.debug('Using %s' % expected_png_file)
-
# FIXME: We repeat this pattern often, we should share code.
try:
with codecs.open(expected_hash_file, "r", "ascii") as file:
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/test_type_base.py b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/test_type_base.py
index 8db2e3d..dd44642 100644
--- a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/test_type_base.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/test_type_base.py
@@ -58,9 +58,6 @@ class TestArguments(object):
# Whether to use wdiff to generate by-word diffs.
wdiff = False
- # Whether to report the locations of the expected result files used.
- show_sources = False
-
# Python bug workaround. See the wdiff code in WriteOutputFiles for an
# explanation.
_wdiff_available = True
diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/text_diff.py b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/text_diff.py
index 18f74b8..d06ec8d 100644
--- a/WebKitTools/Scripts/webkitpy/layout_tests/test_types/text_diff.py
+++ b/WebKitTools/Scripts/webkitpy/layout_tests/test_types/text_diff.py
@@ -48,25 +48,22 @@ _log = logging.getLogger("webkitpy.layout_tests.test_types.text_diff")
class TestTextDiff(test_type_base.TestTypeBase):
- def get_normalized_output_text(self, output):
+ def _get_normalized_output_text(self, output):
# Some tests produce "\r\n" explicitly. Our system (Python/Cygwin)
# helpfully changes the "\n" to "\r\n", resulting in "\r\r\n".
norm = output.replace("\r\r\n", "\r\n").strip("\r\n").replace(
"\r\n", "\n")
return norm + "\n"
- def get_normalized_expected_text(self, filename, show_sources):
+ def _get_normalized_expected_text(self, filename):
"""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 port-specific expected text.
expected_filename = self._port.expected_filename(filename, '.txt')
- if show_sources:
- _log.debug('Using %s' % expected_filename)
+ return self._get_normalized_text(expected_filename)
- return self.get_normalized_text(expected_filename)
-
- def get_normalized_text(self, filename):
+ def _get_normalized_text(self, filename):
# FIXME: We repeat this pattern often, we should share code.
try:
# NOTE: -expected.txt files are ALWAYS utf-8. However,
@@ -94,13 +91,12 @@ class TestTextDiff(test_type_base.TestTypeBase):
# we do not ever decode it inside run-webkit-tests. For some tests
# DumpRenderTree may not output utf-8 text (e.g. webarchives).
self._save_baseline_data(filename, output, ".txt", encoding=None,
- generate_new_baseline=test_args.new_baseline)
+ generate_new_baseline=test_args.new_baseline)
return failures
# Normalize text to diff
- output = self.get_normalized_output_text(output)
- expected = self.get_normalized_expected_text(filename,
- test_args.show_sources)
+ output = self._get_normalized_output_text(output)
+ expected = self._get_normalized_expected_text(filename)
# Write output files for new tests, too.
if port.compare_text(output, expected):
@@ -127,5 +123,5 @@ class TestTextDiff(test_type_base.TestTypeBase):
False otherwise.
"""
- return port.compare_text(self.get_normalized_text(file1),
- self.get_normalized_text(file2))
+ return port.compare_text(self._get_normalized_text(file1),
+ self._get_normalized_text(file2))