diff options
author | Iain Merrick <husky@google.com> | 2010-08-19 17:55:56 +0100 |
---|---|---|
committer | Iain Merrick <husky@google.com> | 2010-08-23 11:05:40 +0100 |
commit | f486d19d62f1bc33246748b14b14a9dfa617b57f (patch) | |
tree | 195485454c93125455a30e553a73981c3816144d /WebKitTools/Scripts/webkitpy/layout_tests | |
parent | 6ba0b43722d16bc295606bec39f396f596e4fef1 (diff) | |
download | external_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')
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)) |