diff options
author | Ben Murdoch <benm@google.com> | 2010-10-22 13:02:20 +0100 |
---|---|---|
committer | Ben Murdoch <benm@google.com> | 2010-10-26 15:21:41 +0100 |
commit | a94275402997c11dd2e778633dacf4b7e630a35d (patch) | |
tree | e66f56c67e3b01f22c9c23cd932271ee9ac558ed /WebKitTools/Scripts/webkitpy/common | |
parent | 09e26c78506587b3f5d930d7bc72a23287ffbec0 (diff) | |
download | external_webkit-a94275402997c11dd2e778633dacf4b7e630a35d.zip external_webkit-a94275402997c11dd2e778633dacf4b7e630a35d.tar.gz external_webkit-a94275402997c11dd2e778633dacf4b7e630a35d.tar.bz2 |
Merge WebKit at r70209: Initial merge by Git
Change-Id: Id23a68efa36e9d1126bcce0b137872db00892c8e
Diffstat (limited to 'WebKitTools/Scripts/webkitpy/common')
27 files changed, 886 insertions, 198 deletions
diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/api.py b/WebKitTools/Scripts/webkitpy/common/checkout/api.py index ca28e32..72cad8d 100644 --- a/WebKitTools/Scripts/webkitpy/common/checkout/api.py +++ b/WebKitTools/Scripts/webkitpy/common/checkout/api.py @@ -83,13 +83,20 @@ class Checkout(object): def bug_id_for_revision(self, revision): return self.commit_info_for_revision(revision).bug_id() - def modified_changelogs(self, git_commit): + def _modified_files_matching_predicate(self, git_commit, predicate, changed_files=None): # SCM returns paths relative to scm.checkout_root # Callers (especially those using the ChangeLog class) may # expect absolute paths, so this method returns absolute paths. - changed_files = self._scm.changed_files(git_commit) + if not changed_files: + changed_files = self._scm.changed_files(git_commit) absolute_paths = [os.path.join(self._scm.checkout_root, path) for path in changed_files] - return [path for path in absolute_paths if self._is_path_to_changelog(path)] + return [path for path in absolute_paths if predicate(path)] + + def modified_changelogs(self, git_commit, changed_files=None): + return self._modified_files_matching_predicate(git_commit, self._is_path_to_changelog, changed_files=changed_files) + + def modified_non_changelogs(self, git_commit, changed_files=None): + return self._modified_files_matching_predicate(git_commit, lambda path: not self._is_path_to_changelog(path), changed_files=changed_files) def commit_message_for_this_commit(self, git_commit): changelog_paths = self.modified_changelogs(git_commit) @@ -109,6 +116,17 @@ class Checkout(object): # FIXME: We should sort and label the ChangeLog messages like commit-log-editor does. return CommitMessage("".join(changelog_messages).splitlines()) + def recent_commit_infos_for_files(self, paths): + revisions = set(sum(map(self._scm.revisions_changing_file, paths), [])) + return set(map(self.commit_info_for_revision, revisions)) + + def suggested_reviewers(self, git_commit): + changed_files = self.modified_non_changelogs(git_commit) + commit_infos = self.recent_commit_infos_for_files(changed_files) + reviewers = [commit_info.reviewer() for commit_info in commit_infos if commit_info.reviewer()] + reviewers.extend([commit_info.author() for commit_info in commit_infos if commit_info.author() and commit_info.author().can_review]) + return sorted(set(reviewers)) + def bug_id_for_this_commit(self, git_commit): try: return parse_bug_id(self.commit_message_for_this_commit(git_commit).message()) diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/api_unittest.py b/WebKitTools/Scripts/webkitpy/common/checkout/api_unittest.py index fdfd879..d7bd95e 100644 --- a/WebKitTools/Scripts/webkitpy/common/checkout/api_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/checkout/api_unittest.py @@ -173,3 +173,24 @@ class CheckoutTest(unittest.TestCase): checkout = Checkout(scm) expected_changlogs = ["/foo/bar/ChangeLog", "/foo/bar/relative/path/ChangeLog"] self.assertEqual(checkout.modified_changelogs(git_commit=None), expected_changlogs) + + def test_suggested_reviewers(self): + def mock_changelog_entries_for_revision(revision): + if revision % 2 == 0: + return [ChangeLogEntry(_changelog1entry1)] + return [ChangeLogEntry(_changelog1entry2)] + + def mock_revisions_changing_file(path, limit=5): + if path.endswith("ChangeLog"): + return [3] + return [4, 8] + + scm = Mock() + scm.checkout_root = "/foo/bar" + scm.changed_files = lambda git_commit: ["file1", "file2", "relative/path/ChangeLog"] + scm.revisions_changing_file = mock_revisions_changing_file + checkout = Checkout(scm) + checkout.changelog_entries_for_revision = mock_changelog_entries_for_revision + reviewers = checkout.suggested_reviewers(git_commit=None) + reviewer_names = [reviewer.full_name for reviewer in reviewers] + self.assertEqual(reviewer_names, [u'Tor Arne Vestb\xf8']) diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/diff_parser.py b/WebKitTools/Scripts/webkitpy/common/checkout/diff_parser.py index d8ebae6..a6ea756 100644 --- a/WebKitTools/Scripts/webkitpy/common/checkout/diff_parser.py +++ b/WebKitTools/Scripts/webkitpy/common/checkout/diff_parser.py @@ -33,9 +33,13 @@ import re _log = logging.getLogger("webkitpy.common.checkout.diff_parser") + +# FIXME: This is broken. We should compile our regexps up-front +# instead of using a custom cache. _regexp_compile_cache = {} +# FIXME: This function should be removed. def match(pattern, string): """Matches the string with the pattern, caching the compiled regexp.""" if not pattern in _regexp_compile_cache: @@ -43,12 +47,15 @@ def match(pattern, string): return _regexp_compile_cache[pattern].match(string) +# FIXME: This belongs on DiffParser (e.g. as to_svn_diff()). def git_diff_to_svn_diff(line): """Converts a git formatted diff line to a svn formatted line. Args: line: A string representing a line of the diff. """ + # FIXME: This list should be a class member on DiffParser. + # These regexp patterns should be compiled once instead of every time. conversion_patterns = (("^diff --git \w/(.+) \w/(?P<FilePath>.+)", lambda matched: "Index: " + matched.group('FilePath') + "\n"), ("^new file.*", lambda matched: "\n"), ("^index [0-9a-f]{7}\.\.[0-9a-f]{7} [0-9]{6}", lambda matched: "===================================================================\n"), @@ -62,6 +69,7 @@ def git_diff_to_svn_diff(line): return line +# FIXME: This method belongs on DiffParser def get_diff_converter(first_diff_line): """Gets a converter function of diff lines. @@ -80,7 +88,7 @@ _DECLARED_FILE_PATH = 2 _PROCESSING_CHUNK = 3 -class DiffFile: +class DiffFile(object): """Contains the information for one file in a patch. The field "lines" is a list which contains tuples in this format: @@ -88,6 +96,13 @@ class DiffFile: If deleted_line_number is zero, it means this line is newly added. If new_line_number is zero, it means this line is deleted. """ + # FIXME: Tuples generally grow into classes. We should consider + # adding a DiffLine object. + + def added_or_modified_line_numbers(self): + # This logic was moved from patchreader.py, but may not be + # the right API for this object long-term. + return [line[1] for line in self.lines if not line[0]] def __init__(self, filename): self.filename = filename @@ -103,13 +118,14 @@ class DiffFile: self.lines.append((deleted_line_number, new_line_number, line)) -class DiffParser: +class DiffParser(object): """A parser for a patch file. The field "files" is a dict whose key is the filename and value is a DiffFile object. """ + # FIXME: This function is way too long and needs to be broken up. def __init__(self, diff_input): """Parses a diff. diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/scm.py b/WebKitTools/Scripts/webkitpy/common/checkout/scm.py index 793d96d..4bd9ed6 100644 --- a/WebKitTools/Scripts/webkitpy/common/checkout/scm.py +++ b/WebKitTools/Scripts/webkitpy/common/checkout/scm.py @@ -245,7 +245,10 @@ class SCM: def changed_files(self, git_commit=None): self._subclass_must_implement() - def changed_files_for_revision(self): + def changed_files_for_revision(self, revision): + self._subclass_must_implement() + + def revisions_changing_file(self, path, limit=5): self._subclass_must_implement() def added_files(self): @@ -257,7 +260,7 @@ class SCM: def display_name(self): self._subclass_must_implement() - def create_patch(self, git_commit=None): + def create_patch(self, git_commit=None, changed_files=[]): self._subclass_must_implement() def committer_email_for_revision(self, revision): @@ -427,6 +430,16 @@ class SVN(SCM): status_command = ["svn", "diff", "--summarize", "-c", revision] return self.run_status_and_extract_filenames(status_command, self._status_regexp("ACDMR")) + def revisions_changing_file(self, path, limit=5): + revisions = [] + log_command = ['svn', 'log', '--quiet', '--limit=%s' % limit, path] + for line in self.run(log_command, cwd=self.checkout_root).splitlines(): + match = re.search('^r(?P<revision>\d+) ', line) + if not match: + continue + revisions.append(int(match.group('revision'))) + return revisions + def conflicted_files(self): return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("C")) @@ -444,11 +457,11 @@ class SVN(SCM): return "svn" # FIXME: This method should be on Checkout. - def create_patch(self, git_commit=None): + def create_patch(self, git_commit=None, changed_files=[]): """Returns a byte array (str()) representing the patch file. Patch files are effectively binary since they may contain files of multiple different encodings.""" - return self.run([self.script_path("svn-create-patch")], + return self.run([self.script_path("svn-create-patch")] + changed_files, cwd=self.checkout_root, return_stderr=False, decode_output=False) @@ -653,6 +666,10 @@ class Git(SCM): commit_id = self.git_commit_from_svn_revision(revision) return self._changes_files_for_commit(commit_id) + def revisions_changing_file(self, path, limit=5): + commit_ids = self.run(["git", "log", "--pretty=format:%H", "-%s" % limit, path]).splitlines() + return filter(lambda revision: revision, map(self.svn_revision_from_git_commit, commit_ids)) + def conflicted_files(self): # We do not need to pass decode_output for this diff command # as we're passing --name-status which does not output any data. @@ -672,12 +689,12 @@ class Git(SCM): def display_name(self): return "git" - def create_patch(self, git_commit=None): + def create_patch(self, git_commit=None, changed_files=[]): """Returns a byte array (str()) representing the patch file. Patch files are effectively binary since they may contain files of multiple different encodings.""" # FIXME: This should probably use cwd=self.checkout_root - return self.run(['git', 'diff', '--binary', "--no-ext-diff", "--full-index", "-M", self.merge_base(git_commit)], decode_output=False) + return self.run(['git', 'diff', '--binary', "--no-ext-diff", "--full-index", "-M", self.merge_base(git_commit), "--"] + changed_files, decode_output=False) @classmethod def git_commit_from_svn_revision(cls, revision): @@ -688,6 +705,12 @@ class Git(SCM): raise ScriptError(message='Failed to find git commit for revision %s, your checkout likely needs an update.' % revision) return git_commit + def svn_revision_from_git_commit(self, commit_id): + try: + return int(self.run(['git', 'svn', 'find-rev', commit_id]).rstrip()) + except ValueError, e: + return None + def contents_at_revision(self, path, revision): """Returns a byte array (str()) containing the contents of path @ revision in the repository.""" diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/scm_unittest.py b/WebKitTools/Scripts/webkitpy/common/checkout/scm_unittest.py index 87d5539..4aa5279 100644 --- a/WebKitTools/Scripts/webkitpy/common/checkout/scm_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/checkout/scm_unittest.py @@ -352,6 +352,10 @@ class SCMTest(unittest.TestCase): self.assertRaises(ScriptError, self.scm.contents_at_revision, "test_file2", 2) self.assertRaises(ScriptError, self.scm.contents_at_revision, "does_not_exist", 2) + def _shared_test_revisions_changing_file(self): + self.assertEqual(self.scm.revisions_changing_file("test_file"), [5, 4, 3, 2]) + self.assertRaises(ScriptError, self.scm.revisions_changing_file, "non_existent_file") + def _shared_test_committer_email_for_revision(self): self.assertEqual(self.scm.committer_email_for_revision(3), getpass.getuser()) # Committer "email" will be the current user @@ -696,6 +700,9 @@ Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA== def test_contents_at_revision(self): self._shared_test_contents_at_revision() + def test_revisions_changing_file(self): + self._shared_test_revisions_changing_file() + def test_committer_email_for_revision(self): self._shared_test_committer_email_for_revision() @@ -964,6 +971,10 @@ class GitSVNTest(SCMTest): self.scm.commit_locally_with_message("another test commit") self._two_local_commits() + def test_revisions_changing_files_with_local_commit(self): + self._one_local_commit() + self.assertEquals(self.scm.revisions_changing_file('test_file_commit1'), []) + def test_commit_with_message(self): self._one_local_commit_plus_working_copy_changes() scm = detect_scm_system(self.git_checkout_path) @@ -1087,6 +1098,20 @@ class GitSVNTest(SCMTest): self.assertTrue(re.search(r'test_file_commit2', patch)) self.assertTrue(re.search(r'test_file_commit1', patch)) + def test_create_patch_with_changed_files(self): + self._one_local_commit_plus_working_copy_changes() + scm = detect_scm_system(self.git_checkout_path) + patch = scm.create_patch(changed_files=['test_file_commit2']) + self.assertTrue(re.search(r'test_file_commit2', patch)) + + def test_create_patch_with_rm_and_changed_files(self): + self._one_local_commit_plus_working_copy_changes() + scm = detect_scm_system(self.git_checkout_path) + os.remove('test_file_commit1') + patch = scm.create_patch() + patch_with_changed_files = scm.create_patch(changed_files=['test_file_commit1', 'test_file_commit2']) + self.assertEquals(patch, patch_with_changed_files) + def test_create_patch_git_commit(self): self._two_local_commits() scm = detect_scm_system(self.git_checkout_path) @@ -1199,6 +1224,9 @@ class GitSVNTest(SCMTest): def test_contents_at_revision(self): self._shared_test_contents_at_revision() + def test_revisions_changing_file(self): + self._shared_test_revisions_changing_file() + def test_added_files(self): self._shared_test_added_files() diff --git a/WebKitTools/Scripts/webkitpy/common/config/committers.py b/WebKitTools/Scripts/webkitpy/common/config/committers.py index 2d07158..f768cf9 100644 --- a/WebKitTools/Scripts/webkitpy/common/config/committers.py +++ b/WebKitTools/Scripts/webkitpy/common/config/committers.py @@ -73,6 +73,7 @@ committers_unable_to_review = [ Committer("Andre Boule", "aboule@apple.com"), Committer("Andrei Popescu", "andreip@google.com", "andreip"), Committer("Andrew Wellington", ["andrew@webkit.org", "proton@wiretapped.net"], "proton"), + Committer("Andrey Kosyakov", "caseq@chromium.org", "caseq"), Committer("Andras Becsi", "abecsi@webkit.org", "bbandix"), Committer("Andy Estes", "aestes@apple.com", "estes"), Committer("Anthony Ricaud", "rik@webkit.org", "rik"), @@ -104,6 +105,7 @@ committers_unable_to_review = [ Committer("Eric Roman", "eroman@chromium.org", "eroman"), Committer("Evan Martin", "evan@chromium.org", "evmar"), Committer("Evan Stade", "estade@chromium.org", "estade"), + Committer("Fady Samuel", "fsamuel@chromium.org", "fsamuel"), Committer("Feng Qian", "feng@chromium.org"), Committer("Fumitoshi Ukai", "ukai@chromium.org", "ukai"), Committer("Gabor Loki", "loki@webkit.org", "loki04"), @@ -144,10 +146,12 @@ committers_unable_to_review = [ Committer("Luiz Agostini", ["luiz@webkit.org", "luiz.agostini@openbossa.org"], "lca"), Committer("Mads Ager", "ager@chromium.org"), Committer("Marcus Voltis Bulach", "bulach@chromium.org"), + Committer("Matt Delaney", "mdelaney@apple.com"), Committer("Matt Lilek", ["webkit@mattlilek.com", "pewtermoose@webkit.org"]), Committer("Matt Perry", "mpcomplete@chromium.org"), Committer("Maxime Britto", ["maxime.britto@gmail.com", "britto@apple.com"]), Committer("Maxime Simon", ["simon.maxime@gmail.com", "maxime.simon@webkit.org"], "maxime.simon"), + Committer("Michael Nordman", "michaeln@google.com", "michaeln"), Committer("Michael Saboff", "msaboff@apple.com"), Committer("Michelangelo De Simone", "michelangelo@webkit.org", "michelangelo"), Committer("Mihai Parparita", "mihaip@chromium.org", "mihaip"), @@ -183,6 +187,7 @@ committers_unable_to_review = [ Committer("Yaar Schnitman", ["yaar@chromium.org", "yaar@google.com"]), Committer("Yong Li", ["yong.li.webkit@gmail.com", "yong.li@torchmobile.com"], "yong"), Committer("Yongjun Zhang", "yongjun.zhang@nokia.com"), + Committer("Yuta Kitamura", "yutak@chromium.org", "yutak"), Committer("Yuzo Fujishima", "yuzo@google.com", "yuzo"), Committer("Zhenyao Mo", "zmo@google.com", "zhenyao"), Committer("Zoltan Herczeg", "zherczeg@webkit.org", "zherczeg"), @@ -205,7 +210,7 @@ reviewers_list = [ Reviewer("Alice Liu", "alice.liu@apple.com", "aliu"), Reviewer("Alp Toker", ["alp@nuanti.com", "alp@atoker.com", "alp@webkit.org"], "alp"), Reviewer("Anders Carlsson", ["andersca@apple.com", "acarlsson@apple.com"], "andersca"), - Reviewer("Andreas Kling", "andreas.kling@nokia.com", "kling"), + Reviewer("Andreas Kling", ["kling@webkit.org", "andreas.kling@nokia.com"], "kling"), Reviewer("Antonio Gomes", ["tonikitoo@webkit.org", "agomes@rim.com"], "tonikitoo"), Reviewer("Antti Koivisto", ["koivisto@iki.fi", "antti@apple.com", "antti.j.koivisto@nokia.com"], "anttik"), Reviewer("Ariya Hidayat", ["ariya@sencha.com", "ariya.hidayat@gmail.com", "ariya@webkit.org"], "ariya"), @@ -256,7 +261,7 @@ reviewers_list = [ Reviewer("Laszlo Gombos", "laszlo.1.gombos@nokia.com", "lgombos"), Reviewer("Maciej Stachowiak", "mjs@apple.com", "othermaciej"), Reviewer("Mark Rowe", "mrowe@apple.com", "bdash"), - Reviewer("Martin Robinson", ["mrobinson@igalia.com", "mrobinson@webkit.org", "martin.james.robinson@gmail.com"], "mrobinson"), + Reviewer("Martin Robinson", ["mrobinson@webkit.org", "mrobinson@igalia.com", "martin.james.robinson@gmail.com"], "mrobinson"), Reviewer("Nate Chapin", "japhet@chromium.org", "japhet"), Reviewer("Nikolas Zimmermann", ["zimmermann@kde.org", "zimmermann@physik.rwth-aachen.de", "zimmermann@webkit.org"], "wildfox"), Reviewer("Ojan Vafai", "ojan@chromium.org", "ojan"), diff --git a/WebKitTools/Scripts/webkitpy/common/config/ports.py b/WebKitTools/Scripts/webkitpy/common/config/ports.py index ebd88b1..d268865 100644 --- a/WebKitTools/Scripts/webkitpy/common/config/ports.py +++ b/WebKitTools/Scripts/webkitpy/common/config/ports.py @@ -45,6 +45,7 @@ class WebKitPort(object): def port(port_name): ports = { "chromium": ChromiumPort, + "chromium-xvfb": ChromiumXVFBPort, "gtk": GtkPort, "mac": MacPort, "win": WinPort, @@ -102,6 +103,10 @@ class WebKitPort(object): def run_perl_unittests_command(cls): return [cls.script_path("test-webkitperl")] + @classmethod + def layout_tests_results_path(cls): + return "/tmp/layout-test-results/results.html" + class MacPort(WebKitPort): @@ -217,3 +222,28 @@ class ChromiumPort(WebKitPort): command = WebKitPort.build_webkit_command(build_style=build_style) command.append("--chromium") return command + + @classmethod + def run_webkit_tests_command(cls): + return [ + cls.script_path("new-run-webkit-tests"), + "--chromium", + "--use-drt", + "--no-pixel-tests", + ] + + @classmethod + def run_javascriptcore_tests_command(cls): + return None + + +class ChromiumXVFBPort(ChromiumPort): + + @classmethod + def flag(cls): + return "--port=chromium-xvfb" + + @classmethod + def run_webkit_tests_command(cls): + # FIXME: We should find a better way to do this. + return ["xvfb-run"] + ChromiumPort.run_webkit_tests_command() diff --git a/WebKitTools/Scripts/webkitpy/common/config/ports_unittest.py b/WebKitTools/Scripts/webkitpy/common/config/ports_unittest.py index 42c4f2d..3bdf0e6 100644 --- a/WebKitTools/Scripts/webkitpy/common/config/ports_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/config/ports_unittest.py @@ -29,7 +29,7 @@ import unittest -from webkitpy.common.config.ports import WebKitPort, MacPort, GtkPort, QtPort, ChromiumPort +from webkitpy.common.config.ports import * class WebKitPortTest(unittest.TestCase): @@ -64,11 +64,13 @@ class WebKitPortTest(unittest.TestCase): def test_chromium_port(self): self.assertEquals(ChromiumPort.name(), "Chromium") self.assertEquals(ChromiumPort.flag(), "--port=chromium") - self.assertEquals(ChromiumPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests")]) + self.assertEquals(ChromiumPort.run_webkit_tests_command(), [WebKitPort.script_path("new-run-webkit-tests"), "--chromium", "--use-drt", "--no-pixel-tests"]) self.assertEquals(ChromiumPort.build_webkit_command(), [WebKitPort.script_path("build-webkit"), "--chromium"]) self.assertEquals(ChromiumPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug", "--chromium"]) self.assertEquals(ChromiumPort.update_webkit_command(), [WebKitPort.script_path("update-webkit"), "--chromium"]) + def test_chromium_xvfb_port(self): + self.assertEquals(ChromiumXVFBPort.run_webkit_tests_command(), ["xvfb-run", "WebKitTools/Scripts/new-run-webkit-tests", "--chromium", "--use-drt", "--no-pixel-tests"]) if __name__ == '__main__': unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py b/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py index cc64fac..94519a7 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py +++ b/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py @@ -301,15 +301,14 @@ class BugzillaQueries(object): review_queue_url = "buglist.cgi?query_format=advanced&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&field0-0-0=flagtypes.name&type0-0-0=equals&value0-0-0=review?" return self._fetch_bug_ids_advanced_query(review_queue_url) + # This method will make several requests to bugzilla. def fetch_patches_from_review_queue(self, limit=None): # [:None] returns the whole array. return sum([self._fetch_bug(bug_id).unreviewed_patches() for bug_id in self._fetch_bug_ids_from_review_queue()[:limit]], []) - # FIXME: Why do we have both fetch_patches_from_review_queue and - # fetch_attachment_ids_from_review_queue?? - # NOTE: This is also the only client of _fetch_attachment_ids_request_query - + # NOTE: This is the only client of _fetch_attachment_ids_request_query + # This method only makes one request to bugzilla. def fetch_attachment_ids_from_review_queue(self): review_queue_url = "request.cgi?action=queue&type=review&group=type" return self._fetch_attachment_ids_request_query(review_queue_url) diff --git a/WebKitTools/Scripts/webkitpy/common/net/bugzilla_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/bugzilla_unittest.py index 32f23cd..3a454d6 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/bugzilla_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/net/bugzilla_unittest.py @@ -33,24 +33,11 @@ import datetime from webkitpy.common.config.committers import CommitterList, Reviewer, Committer from webkitpy.common.net.bugzilla import Bugzilla, BugzillaQueries, parse_bug_id, CommitterValidator, Bug from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.tool.mocktool import MockBrowser from webkitpy.thirdparty.mock import Mock from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup -class MockBrowser(object): - def open(self, url): - pass - - def select_form(self, name): - pass - - def __setitem__(self, key, value): - pass - - def submit(self): - pass - - class BugTest(unittest.TestCase): def test_is_unassigned(self): for email in Bug.unassigned_emails: diff --git a/WebKitTools/Scripts/webkitpy/common/net/buildbot.py b/WebKitTools/Scripts/webkitpy/common/net/buildbot.py index 17f6c7a..a14bc7f 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/buildbot.py +++ b/WebKitTools/Scripts/webkitpy/common/net/buildbot.py @@ -35,12 +35,12 @@ import urllib2 import xmlrpclib from webkitpy.common.net.failuremap import FailureMap +from webkitpy.common.net.layouttestresults import LayoutTestResults from webkitpy.common.net.regressionwindow import RegressionWindow from webkitpy.common.system.logutils import get_logger from webkitpy.thirdparty.autoinstalled.mechanize import Browser from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup - _log = get_logger(__file__) @@ -108,6 +108,7 @@ class Builder(object): def _fetch_revision_to_build_map(self): # All _fetch requests go through _buildbot for easier mocking + # FIXME: This should use NetworkTransaction's 404 handling instead. try: # FIXME: This method is horribly slow due to the huge network load. # FIXME: This is a poor way to do revision -> build mapping. @@ -166,22 +167,23 @@ class Builder(object): failures = set(results.failing_tests()) if common_failures == None: common_failures = failures - common_failures = common_failures.intersection(failures) - if not common_failures: - # current_build doesn't have any failures in common with - # the red build we're worried about. We assume that any - # failures in current_build were due to flakiness. - break + else: + common_failures = common_failures.intersection(failures) + if not common_failures: + # current_build doesn't have any failures in common with + # the red build we're worried about. We assume that any + # failures in current_build were due to flakiness. + break look_back_count += 1 if look_back_count > look_back_limit: - return RegressionWindow(None, current_build, common_failures=common_failures) + return RegressionWindow(None, current_build, failing_tests=common_failures) build_after_current_build = current_build current_build = current_build.previous_build() # We must iterate at least once because red_build is red. assert(build_after_current_build) # Current build must either be green or have no failures in common # with red build, so we've found our failure transition. - return RegressionWindow(current_build, build_after_current_build, common_failures=common_failures) + return RegressionWindow(current_build, build_after_current_build, failing_tests=common_failures) def find_blameworthy_regression_window(self, red_build_number, look_back_limit=30, avoid_flakey_tests=True): red_build = self.build(red_build_number) @@ -195,66 +197,6 @@ class Builder(object): return regression_window -# FIXME: This should be unified with all the layout test results code in the layout_tests package -class LayoutTestResults(object): - stderr_key = u'Tests that had stderr output:' - fail_key = u'Tests where results did not match expected results:' - timeout_key = u'Tests that timed out:' - crash_key = u'Tests that caused the DumpRenderTree tool to crash:' - missing_key = u'Tests that had no expected results (probably new):' - - expected_keys = [ - stderr_key, - fail_key, - crash_key, - timeout_key, - missing_key, - ] - - @classmethod - def _parse_results_html(cls, page): - parsed_results = {} - tables = BeautifulSoup(page).findAll("table") - for table in tables: - table_title = unicode(table.findPreviousSibling("p").string) - if table_title not in cls.expected_keys: - # This Exception should only ever be hit if run-webkit-tests changes its results.html format. - raise Exception("Unhandled title: %s" % table_title) - # We might want to translate table titles into identifiers before storing. - parsed_results[table_title] = [unicode(row.find("a").string) for row in table.findAll("tr")] - - return parsed_results - - @classmethod - def _fetch_results_html(cls, base_url): - results_html = "%s/results.html" % base_url - # FIXME: We need to move this sort of 404 logic into NetworkTransaction or similar. - try: - page = urllib2.urlopen(results_html) - return cls._parse_results_html(page) - except urllib2.HTTPError, error: - if error.code != 404: - raise - - @classmethod - def results_from_url(cls, base_url): - parsed_results = cls._fetch_results_html(base_url) - if not parsed_results: - return None - return cls(base_url, parsed_results) - - def __init__(self, base_url, parsed_results): - self._base_url = base_url - self._parsed_results = parsed_results - - def parsed_results(self): - return self._parsed_results - - def failing_tests(self): - failing_keys = [self.fail_key, self.crash_key, self.timeout_key] - return sorted(sum([tests for key, tests in self._parsed_results.items() if key in failing_keys], [])) - - class Build(object): def __init__(self, builder, build_number, revision, is_green): self._builder = builder @@ -274,9 +216,19 @@ class Build(object): results_directory = "r%s (%s)" % (self.revision(), self._number) return "%s/%s" % (self._builder.results_url(), urllib.quote(results_directory)) + def _fetch_results_html(self): + results_html = "%s/results.html" % (self.results_url()) + # FIXME: This should use NetworkTransaction's 404 handling instead. + try: + return urllib2.urlopen(results_html) + except urllib2.HTTPError, error: + if error.code != 404: + raise + def layout_test_results(self): if not self._layout_test_results: - self._layout_test_results = LayoutTestResults.results_from_url(self.results_url()) + # FIXME: This should cache that the result was a 404 and stop hitting the network. + self._layout_test_results = LayoutTestResults.results_from_string(self._fetch_results_html()) return self._layout_test_results def builder(self): @@ -461,7 +413,8 @@ class BuildBot(object): continue builder = self.builder_with_name(builder_status["name"]) regression_window = builder.find_blameworthy_regression_window(builder_status["build_number"]) - failure_map.add_regression_window(builder, regression_window) + if regression_window: + failure_map.add_regression_window(builder, regression_window) return failure_map # This makes fewer requests than calling Builder.latest_build would. It grabs all builder diff --git a/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py index c99ab32..afc9a39 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py @@ -29,7 +29,6 @@ import unittest from webkitpy.common.net.buildbot import BuildBot, Builder, Build, LayoutTestResults - from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup @@ -42,10 +41,8 @@ class BuilderTest(unittest.TestCase): revision=build_number + 1000, is_green=build_number < 4 ) - build._layout_test_results = LayoutTestResults( - "http://buildbot.example.com/foo", { - LayoutTestResults.fail_key: failure(build_number), - }) + parsed_results = {LayoutTestResults.fail_key: failure(build_number)} + build._layout_test_results = LayoutTestResults(parsed_results) return build self.builder._fetch_build = _mock_fetch_build @@ -114,45 +111,6 @@ class BuilderTest(unittest.TestCase): self.assertEqual(self.builder._revision_and_build_for_filename(filename), revision_and_build) -class LayoutTestResultsTest(unittest.TestCase): - _example_results_html = """ -<html> -<head> -<title>Layout Test Results</title> -</head> -<body> -<p>Tests that had stderr output:</p> -<table> -<tr> -<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/accessibility/aria-activedescendant-crash.html">accessibility/aria-activedescendant-crash.html</a></td> -<td><a href="accessibility/aria-activedescendant-crash-stderr.txt">stderr</a></td> -</tr> -<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/http/tests/security/canvas-remote-read-svg-image.html">http/tests/security/canvas-remote-read-svg-image.html</a></td> -<td><a href="http/tests/security/canvas-remote-read-svg-image-stderr.txt">stderr</a></td> -</tr> -</table><p>Tests that had no expected results (probably new):</p> -<table> -<tr> -<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/fast/repaint/no-caret-repaint-in-non-content-editable-element.html">fast/repaint/no-caret-repaint-in-non-content-editable-element.html</a></td> -<td><a href="fast/repaint/no-caret-repaint-in-non-content-editable-element-actual.txt">result</a></td> -</tr> -</table></body> -</html> -""" - - _expected_layout_test_results = { - 'Tests that had stderr output:' : [ - 'accessibility/aria-activedescendant-crash.html' - ], - 'Tests that had no expected results (probably new):' : [ - 'fast/repaint/no-caret-repaint-in-non-content-editable-element.html' - ] - } - def test_parse_layout_test_results(self): - results = LayoutTestResults._parse_results_html(self._example_results_html) - self.assertEqual(self._expected_layout_test_results, results) - - class BuildBotTest(unittest.TestCase): _example_one_box_status = ''' diff --git a/WebKitTools/Scripts/webkitpy/common/net/failuremap.py b/WebKitTools/Scripts/webkitpy/common/net/failuremap.py index 98e4b8f..e2d53ae 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/failuremap.py +++ b/WebKitTools/Scripts/webkitpy/common/net/failuremap.py @@ -37,12 +37,48 @@ class FailureMap(object): 'regression_window': regression_window, }) - def revisions_causing_failures(self): - revision_to_failing_bots = {} - for failure_info in self._failures: - revisions = failure_info['regression_window'].revisions() - for revision in revisions: - failing_bots = revision_to_failing_bots.get(revision, []) - failing_bots.append(failure_info['builder']) - revision_to_failing_bots[revision] = failing_bots - return revision_to_failing_bots + def is_empty(self): + return not self._failures + + def failing_revisions(self): + failing_revisions = [failure_info['regression_window'].revisions() + for failure_info in self._failures] + return sorted(set(sum(failing_revisions, []))) + + def builders_failing_for(self, revision): + return self._builders_failing_because_of([revision]) + + def tests_failing_for(self, revision): + tests = [failure_info['regression_window'].failing_tests() + for failure_info in self._failures + if revision in failure_info['regression_window'].revisions() + and failure_info['regression_window'].failing_tests()] + result = set() + for test in tests: + result = result.union(test) + return sorted(result) + + def _old_failures(self, is_old_failure): + return filter(lambda revision: is_old_failure(revision), + self.failing_revisions()) + + def _builders_failing_because_of(self, revisions): + revision_set = set(revisions) + return [failure_info['builder'] for failure_info in self._failures + if revision_set.intersection( + failure_info['regression_window'].revisions())] + + # FIXME: We should re-process old failures after some time delay. + # https://bugs.webkit.org/show_bug.cgi?id=36581 + def filter_out_old_failures(self, is_old_failure): + old_failures = self._old_failures(is_old_failure) + old_failing_builder_names = set([builder.name() + for builder in self._builders_failing_because_of(old_failures)]) + + # We filter out all the failing builders that could have been caused + # by old_failures. We could miss some new failures this way, but + # emperically, this reduces the amount of spam we generate. + failures = self._failures + self._failures = [failure_info for failure_info in failures + if failure_info['builder'].name() not in old_failing_builder_names] + self._cache = {} diff --git a/WebKitTools/Scripts/webkitpy/common/net/failuremap_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/failuremap_unittest.py new file mode 100644 index 0000000..2f0b49d --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/net/failuremap_unittest.py @@ -0,0 +1,76 @@ +# 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. + +import unittest + +from webkitpy.common.net.buildbot import Build +from webkitpy.common.net.failuremap import * +from webkitpy.common.net.regressionwindow import RegressionWindow +from webkitpy.tool.mocktool import MockBuilder + + +class FailureMapTest(unittest.TestCase): + builder1 = MockBuilder("Builder1") + builder2 = MockBuilder("Builder2") + + build1a = Build(builder1, build_number=22, revision=1233, is_green=True) + build1b = Build(builder1, build_number=23, revision=1234, is_green=False) + build2a = Build(builder2, build_number=89, revision=1233, is_green=True) + build2b = Build(builder2, build_number=90, revision=1235, is_green=False) + + regression_window1 = RegressionWindow(build1a, build1b, failing_tests=[u'test1', u'test1']) + regression_window2 = RegressionWindow(build2a, build2b, failing_tests=[u'test1']) + + def _make_failure_map(self): + failure_map = FailureMap() + failure_map.add_regression_window(self.builder1, self.regression_window1) + failure_map.add_regression_window(self.builder2, self.regression_window2) + return failure_map + + def test_failing_revisions(self): + failure_map = self._make_failure_map() + self.assertEquals(failure_map.failing_revisions(), [1234, 1235]) + + def test_new_failures(self): + failure_map = self._make_failure_map() + failure_map.filter_out_old_failures(lambda revision: False) + self.assertEquals(failure_map.failing_revisions(), [1234, 1235]) + + def test_new_failures_with_old_revisions(self): + failure_map = self._make_failure_map() + failure_map.filter_out_old_failures(lambda revision: revision == 1234) + self.assertEquals(failure_map.failing_revisions(), []) + + def test_new_failures_with_more_old_revisions(self): + failure_map = self._make_failure_map() + failure_map.filter_out_old_failures(lambda revision: revision == 1235) + self.assertEquals(failure_map.failing_revisions(), [1234]) + + def test_tests_failing_for(self): + failure_map = self._make_failure_map() + self.assertEquals(failure_map.tests_failing_for(1234), [u'test1']) diff --git a/WebKitTools/Scripts/webkitpy/common/net/layouttestresults.py b/WebKitTools/Scripts/webkitpy/common/net/layouttestresults.py new file mode 100644 index 0000000..2f7b3e6 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/net/layouttestresults.py @@ -0,0 +1,88 @@ +# 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. +# +# A module for parsing results.html files generated by old-run-webkit-tests + +from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup, SoupStrainer + + +# FIXME: This should be unified with all the layout test results code in the layout_tests package +# This doesn't belong in common.net, but we don't have a better place for it yet. +def path_for_layout_test(test_name): + return "LayoutTests/%s" % test_name + + +# FIXME: This should be unified with all the layout test results code in the layout_tests package +# This doesn't belong in common.net, but we don't have a better place for it yet. +class LayoutTestResults(object): + """This class knows how to parse old-run-webkit-tests results.html files.""" + + stderr_key = u'Tests that had stderr output:' + fail_key = u'Tests where results did not match expected results:' + timeout_key = u'Tests that timed out:' + crash_key = u'Tests that caused the DumpRenderTree tool to crash:' + missing_key = u'Tests that had no expected results (probably new):' + + expected_keys = [ + stderr_key, + fail_key, + crash_key, + timeout_key, + missing_key, + ] + + @classmethod + def _parse_results_html(cls, page): + parsed_results = {} + tables = BeautifulSoup(page).findAll("table") + for table in tables: + table_title = unicode(table.findPreviousSibling("p").string) + if table_title not in cls.expected_keys: + # This Exception should only ever be hit if run-webkit-tests changes its results.html format. + raise Exception("Unhandled title: %s" % table_title) + # We might want to translate table titles into identifiers before storing. + parsed_results[table_title] = [unicode(row.find("a").string) for row in table.findAll("tr")] + + return parsed_results + + @classmethod + def results_from_string(cls, string): + parsed_results = cls._parse_results_html(string) + if not parsed_results: + return None + return cls(parsed_results) + + def __init__(self, parsed_results): + self._parsed_results = parsed_results + + def parsed_results(self): + return self._parsed_results + + def failing_tests(self): + failing_keys = [self.fail_key, self.crash_key, self.timeout_key] + return sorted(sum([tests for key, tests in self._parsed_results.items() if key in failing_keys], [])) diff --git a/WebKitTools/Scripts/webkitpy/common/net/layouttestresults_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/layouttestresults_unittest.py new file mode 100644 index 0000000..44e4dbc --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/net/layouttestresults_unittest.py @@ -0,0 +1,76 @@ +# 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. + +import unittest + +from webkitpy.common.net.layouttestresults import LayoutTestResults + + +class LayoutTestResultsTest(unittest.TestCase): + _example_results_html = """ +<html> +<head> +<title>Layout Test Results</title> +</head> +<body> +<p>Tests that had stderr output:</p> +<table> +<tr> +<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/accessibility/aria-activedescendant-crash.html">accessibility/aria-activedescendant-crash.html</a></td> +<td><a href="accessibility/aria-activedescendant-crash-stderr.txt">stderr</a></td> +</tr> +<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/http/tests/security/canvas-remote-read-svg-image.html">http/tests/security/canvas-remote-read-svg-image.html</a></td> +<td><a href="http/tests/security/canvas-remote-read-svg-image-stderr.txt">stderr</a></td> +</tr> +</table><p>Tests that had no expected results (probably new):</p> +<table> +<tr> +<td><a href="/var/lib/buildbot/build/gtk-linux-64-release/build/LayoutTests/fast/repaint/no-caret-repaint-in-non-content-editable-element.html">fast/repaint/no-caret-repaint-in-non-content-editable-element.html</a></td> +<td><a href="fast/repaint/no-caret-repaint-in-non-content-editable-element-actual.txt">result</a></td> +</tr> +</table></body> +</html> +""" + + _expected_layout_test_results = { + 'Tests that had stderr output:': [ + 'accessibility/aria-activedescendant-crash.html', + ], + 'Tests that had no expected results (probably new):': [ + 'fast/repaint/no-caret-repaint-in-non-content-editable-element.html', + ], + } + + def test_parse_layout_test_results(self): + results = LayoutTestResults._parse_results_html(self._example_results_html) + self.assertEqual(self._expected_layout_test_results, results) + + def test_results_from_string(self): + self.assertEqual(LayoutTestResults.results_from_string(""), None) + results = LayoutTestResults.results_from_string(self._example_results_html) + self.assertEqual(len(results.failing_tests()), 0) diff --git a/WebKitTools/Scripts/webkitpy/common/net/networktransaction.py b/WebKitTools/Scripts/webkitpy/common/net/networktransaction.py index c82fc6f..de19e94 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/networktransaction.py +++ b/WebKitTools/Scripts/webkitpy/common/net/networktransaction.py @@ -29,7 +29,7 @@ import logging import time -from webkitpy.thirdparty.autoinstalled.mechanize import HTTPError +from webkitpy.thirdparty.autoinstalled import mechanize from webkitpy.common.system.deprecated_logging import log @@ -41,10 +41,11 @@ class NetworkTimeout(Exception): class NetworkTransaction(object): - def __init__(self, initial_backoff_seconds=10, grown_factor=1.5, timeout_seconds=10*60): + def __init__(self, initial_backoff_seconds=10, grown_factor=1.5, timeout_seconds=(10 * 60), convert_404_to_None=False): self._initial_backoff_seconds = initial_backoff_seconds self._grown_factor = grown_factor self._timeout_seconds = timeout_seconds + self._convert_404_to_None = convert_404_to_None def run(self, request): self._total_sleep = 0 @@ -52,7 +53,10 @@ class NetworkTransaction(object): while True: try: return request() - except HTTPError, e: + # FIXME: We should catch urllib2.HTTPError here too. + except mechanize.HTTPError, e: + if self._convert_404_to_None and e.code == 404: + return None self._check_for_timeout() _log.warn("Received HTTP status %s from server. Retrying in " "%s seconds..." % (e.code, self._backoff_seconds)) diff --git a/WebKitTools/Scripts/webkitpy/common/net/networktransaction_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/networktransaction_unittest.py index cd0702b..49aaeed 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/networktransaction_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/net/networktransaction_unittest.py @@ -56,29 +56,36 @@ class NetworkTransactionTest(LoggingTestCase): self.assertTrue(did_throw_exception) self.assertTrue(did_process_exception) - def _raise_http_error(self): + def _raise_500_error(self): self._run_count += 1 if self._run_count < 3: - raise HTTPError("http://example.com/", 500, "inteneral server error", None, None) + raise HTTPError("http://example.com/", 500, "internal server error", None, None) return 42 + def _raise_404_error(self): + raise HTTPError("http://foo.com/", 404, "not found", None, None) + def test_retry(self): self._run_count = 0 transaction = NetworkTransaction(initial_backoff_seconds=0) - self.assertEqual(transaction.run(lambda: self._raise_http_error()), 42) + self.assertEqual(transaction.run(lambda: self._raise_500_error()), 42) self.assertEqual(self._run_count, 3) self.assertLog(['WARNING: Received HTTP status 500 from server. ' 'Retrying in 0 seconds...\n', 'WARNING: Received HTTP status 500 from server. ' 'Retrying in 0.0 seconds...\n']) + def test_convert_404_to_None(self): + transaction = NetworkTransaction(convert_404_to_None=True) + self.assertEqual(transaction.run(lambda: self._raise_404_error()), None) + def test_timeout(self): self._run_count = 0 transaction = NetworkTransaction(initial_backoff_seconds=60*60, timeout_seconds=60) did_process_exception = False did_throw_exception = True try: - transaction.run(lambda: self._raise_http_error()) + transaction.run(lambda: self._raise_500_error()) did_throw_exception = False except NetworkTimeout, e: did_process_exception = True diff --git a/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py b/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py index 231459f..ad89815 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py +++ b/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py @@ -28,10 +28,11 @@ class RegressionWindow(object): - def __init__(self, build_before_failure, failing_build, common_failures=None): + def __init__(self, build_before_failure, failing_build, failing_tests=None): self._build_before_failure = build_before_failure self._failing_build = failing_build - self._common_failures = common_failures + self._failing_tests = failing_tests + self._revisions = None def build_before_failure(self): return self._build_before_failure @@ -39,10 +40,12 @@ class RegressionWindow(object): def failing_build(self): return self._failing_build - def common_failures(self): - return self._common_failures + def failing_tests(self): + return self._failing_tests def revisions(self): - revisions = range(self._failing_build.revision(), self._build_before_failure.revision(), -1) - revisions.reverse() - return revisions + # Cache revisions to avoid excessive allocations. + if not self._revisions: + self._revisions = range(self._failing_build.revision(), self._build_before_failure.revision(), -1) + self._revisions.reverse() + return self._revisions diff --git a/WebKitTools/Scripts/webkitpy/common/net/statusserver.py b/WebKitTools/Scripts/webkitpy/common/net/statusserver.py index 57390b8..3d03dcd 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/statusserver.py +++ b/WebKitTools/Scripts/webkitpy/common/net/statusserver.py @@ -41,14 +41,18 @@ _log = logging.getLogger("webkitpy.common.net.statusserver") class StatusServer: default_host = "queues.webkit.org" - def __init__(self, host=default_host): + def __init__(self, host=default_host, browser=None, bot_id=None): self.set_host(host) - self.browser = Browser() + self._browser = browser or Browser() + self.set_bot_id(bot_id) def set_host(self, host): self.host = host self.url = "http://%s" % self.host + def set_bot_id(self, bot_id): + self.bot_id = bot_id + def results_url_for_status(self, status_id): return "%s/results/%s" % (self.url, status_id) @@ -56,14 +60,14 @@ class StatusServer: if not patch: return if patch.bug_id(): - self.browser["bug_id"] = unicode(patch.bug_id()) + self._browser["bug_id"] = unicode(patch.bug_id()) if patch.id(): - self.browser["patch_id"] = unicode(patch.id()) + self._browser["patch_id"] = unicode(patch.id()) def _add_results_file(self, results_file): if not results_file: return - self.browser.add_file(results_file, "text/plain", "results.txt", 'results_file') + self._browser.add_file(results_file, "text/plain", "results.txt", 'results_file') def _post_status_to_server(self, queue_name, status, patch, results_file): if results_file: @@ -71,36 +75,61 @@ class StatusServer: results_file.seek(0) update_status_url = "%s/update-status" % self.url - self.browser.open(update_status_url) - self.browser.select_form(name="update_status") - self.browser["queue_name"] = queue_name + self._browser.open(update_status_url) + self._browser.select_form(name="update_status") + self._browser["queue_name"] = queue_name + if self.bot_id: + self._browser["bot_id"] = self.bot_id self._add_patch(patch) - self.browser["status"] = status + self._browser["status"] = status self._add_results_file(results_file) - return self.browser.submit().read() # This is the id of the newly created status object. + return self._browser.submit().read() # This is the id of the newly created status object. def _post_svn_revision_to_server(self, svn_revision_number, broken_bot): update_svn_revision_url = "%s/update-svn-revision" % self.url - self.browser.open(update_svn_revision_url) - self.browser.select_form(name="update_svn_revision") - self.browser["number"] = unicode(svn_revision_number) - self.browser["broken_bot"] = broken_bot - return self.browser.submit().read() + self._browser.open(update_svn_revision_url) + self._browser.select_form(name="update_svn_revision") + self._browser["number"] = unicode(svn_revision_number) + self._browser["broken_bot"] = broken_bot + return self._browser.submit().read() def _post_work_items_to_server(self, queue_name, work_items): update_work_items_url = "%s/update-work-items" % self.url - self.browser.open(update_work_items_url) - self.browser.select_form(name="update_work_items") - self.browser["queue_name"] = queue_name + self._browser.open(update_work_items_url) + self._browser.select_form(name="update_work_items") + self._browser["queue_name"] = queue_name work_items = map(unicode, work_items) # .join expects strings - self.browser["work_items"] = " ".join(work_items) - return self.browser.submit().read() + self._browser["work_items"] = " ".join(work_items) + return self._browser.submit().read() + + def _post_work_item_to_ews(self, attachment_id): + submit_to_ews_url = "%s/submit-to-ews" % self.url + self._browser.open(submit_to_ews_url) + self._browser.select_form(name="submit_to_ews") + self._browser["attachment_id"] = unicode(attachment_id) + self._browser.submit() + + def submit_to_ews(self, attachment_id): + _log.info("Submitting attachment %s to EWS queues" % attachment_id) + return NetworkTransaction().run(lambda: self._post_work_item_to_ews(attachment_id)) def next_work_item(self, queue_name): _log.debug("Fetching next work item for %s" % queue_name) patch_status_url = "%s/next-patch/%s" % (self.url, queue_name) return self._fetch_url(patch_status_url) + def _post_release_work_item(self, queue_name, patch): + release_patch_url = "%s/release-patch" % (self.url) + self._browser.open(release_patch_url) + self._browser.select_form(name="release_patch") + self._browser["queue_name"] = queue_name + self._browser["attachment_id"] = unicode(patch.id()) + self._browser.submit() + + def release_work_item(self, queue_name, patch): + _log.debug("Releasing work item %s from %s" % (patch.id(), queue_name)) + return NetworkTransaction(convert_404_to_None=True).run(lambda: self._post_release_work_item(queue_name, patch)) + def update_work_items(self, queue_name, work_items): _log.debug("Recording work items: %s for %s" % (work_items, queue_name)) return NetworkTransaction().run(lambda: self._post_work_items_to_server(queue_name, work_items)) @@ -114,6 +143,7 @@ class StatusServer: return NetworkTransaction().run(lambda: self._post_svn_revision_to_server(svn_revision_number, broken_bot)) def _fetch_url(self, url): + # FIXME: This should use NetworkTransaction's 404 handling instead. try: return urllib2.urlopen(url).read() except urllib2.HTTPError, e: diff --git a/WebKitTools/Scripts/webkitpy/common/net/statusserver_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/statusserver_unittest.py new file mode 100644 index 0000000..1169ba0 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/net/statusserver_unittest.py @@ -0,0 +1,43 @@ +# 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. + +import unittest + +from webkitpy.common.net.statusserver import StatusServer +from webkitpy.common.system.outputcapture import OutputCaptureTestCaseBase +from webkitpy.tool.mocktool import MockBrowser + + +class StatusServerTest(OutputCaptureTestCaseBase): + def test_url_for_issue(self): + mock_browser = MockBrowser() + status_server = StatusServer(browser=mock_browser, bot_id='123') + status_server.update_status('queue name', 'the status') + self.assertEqual('queue name', mock_browser.params['queue_name']) + self.assertEqual('the status', mock_browser.params['status']) + self.assertEqual('123', mock_browser.params['bot_id']) diff --git a/WebKitTools/Scripts/webkitpy/common/system/executive.py b/WebKitTools/Scripts/webkitpy/common/system/executive.py index 7c00f22..216cf58 100644 --- a/WebKitTools/Scripts/webkitpy/common/system/executive.py +++ b/WebKitTools/Scripts/webkitpy/common/system/executive.py @@ -103,6 +103,13 @@ class Executive(object): def _run_command_with_teed_output(self, args, teed_output): args = map(unicode, args) # Popen will throw an exception if args are non-strings (like int()) + if sys.platform == 'cygwin': + # Cygwin's Python's os.execv doesn't support unicode command + # arguments, and neither does Cygwin's execv itself. + # FIXME: Using UTF-8 here will confuse Windows-native commands + # which will expect arguments to be encoded using the current code + # page. + args = [arg.encode('utf-8') for arg in args] child_process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -281,6 +288,13 @@ class Executive(object): assert(isinstance(args, list) or isinstance(args, tuple)) start_time = time.time() args = map(unicode, args) # Popen will throw an exception if args are non-strings (like int()) + if sys.platform == 'cygwin': + # Cygwin's Python's os.execv doesn't support unicode command + # arguments, and neither does Cygwin's execv itself. + # FIXME: Using UTF-8 here will confuse Windows-native commands + # which will expect arguments to be encoded using the current code + # page. + args = [arg.encode('utf-8') for arg in args] stdin, string_to_communicate = self._compute_stdin(input) stderr = subprocess.STDOUT if return_stderr else None diff --git a/WebKitTools/Scripts/webkitpy/common/system/outputcapture.py b/WebKitTools/Scripts/webkitpy/common/system/outputcapture.py index 68a3919..45e0e3f 100644 --- a/WebKitTools/Scripts/webkitpy/common/system/outputcapture.py +++ b/WebKitTools/Scripts/webkitpy/common/system/outputcapture.py @@ -29,6 +29,7 @@ # Class for unittest support. Used for capturing stderr/stdout. import sys +import unittest from StringIO import StringIO class OutputCapture(object): @@ -37,7 +38,9 @@ class OutputCapture(object): def _capture_output_with_name(self, output_name): self.saved_outputs[output_name] = getattr(sys, output_name) - setattr(sys, output_name, StringIO()) + captured_output = StringIO() + setattr(sys, output_name, captured_output) + return captured_output def _restore_output_with_name(self, output_name): captured_output = getattr(sys, output_name).getvalue() @@ -46,8 +49,7 @@ class OutputCapture(object): return captured_output def capture_output(self): - self._capture_output_with_name("stdout") - self._capture_output_with_name("stderr") + return (self._capture_output_with_name("stdout"), self._capture_output_with_name("stderr")) def restore_output(self): return (self._restore_output_with_name("stdout"), self._restore_output_with_name("stderr")) @@ -63,3 +65,22 @@ class OutputCapture(object): testcase.assertEqual(stderr_string, expected_stderr) # This is a little strange, but I don't know where else to return this information. return return_value + + +class OutputCaptureTestCaseBase(unittest.TestCase): + def setUp(self): + unittest.TestCase.setUp(self) + self.output_capture = OutputCapture() + (self.__captured_stdout, self.__captured_stderr) = self.output_capture.capture_output() + + def tearDown(self): + del self.__captured_stdout + del self.__captured_stderr + self.output_capture.restore_output() + unittest.TestCase.tearDown(self) + + def assertStdout(self, expected_stdout): + self.assertEquals(expected_stdout, self.__captured_stdout.getvalue()) + + def assertStderr(self, expected_stderr): + self.assertEquals(expected_stderr, self.__captured_stderr.getvalue()) diff --git a/WebKitTools/Scripts/webkitpy/common/system/path.py b/WebKitTools/Scripts/webkitpy/common/system/path.py new file mode 100644 index 0000000..43c6410 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/system/path.py @@ -0,0 +1,134 @@ +# 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. + +"""generic routines to convert platform-specific paths to URIs.""" +from __future__ import with_statement + +import atexit +import subprocess +import sys +import threading +import urllib + + +def abspath_to_uri(path, platform=None): + """Converts a platform-specific absolute path to a file: URL.""" + if platform is None: + platform = sys.platform + return "file:" + _escape(_convert_path(path, platform)) + + +def cygpath(path): + """Converts a cygwin path to Windows path.""" + return _CygPath.convert_using_singleton(path) + + +# Note that this object is not threadsafe and must only be called +# from multiple threads under protection of a lock (as is done in cygpath()) +class _CygPath(object): + """Manages a long-running 'cygpath' process for file conversion.""" + _lock = None + _singleton = None + + @staticmethod + def stop_cygpath_subprocess(): + if not _CygPath._lock: + return + + with _CygPath._lock: + if _CygPath._singleton: + _CygPath._singleton.stop() + + @staticmethod + def convert_using_singleton(path): + if not _CygPath._lock: + _CygPath._lock = threading.Lock() + + with _CygPath._lock: + if not _CygPath._singleton: + _CygPath._singleton = _CygPath() + # Make sure the cygpath subprocess always gets shutdown cleanly. + atexit.register(_CygPath.stop_cygpath_subprocess) + + return _CygPath._singleton.convert(path) + + def __init__(self): + self._child_process = None + + def start(self): + assert(self._child_process is None) + args = ['cygpath', '-f', '-', '-wa'] + self._child_process = subprocess.Popen(args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + + def is_running(self): + if not self._child_process: + return False + return self._child_process.returncode is None + + def stop(self): + if self._child_process: + self._child_process.stdin.close() + self._child_process.wait() + self._child_process = None + + def convert(self, path): + if not self.is_running(): + self.start() + self._child_process.stdin.write("%s\r\n" % path) + self._child_process.stdin.flush() + return self._child_process.stdout.readline().rstrip() + + +def _escape(path): + """Handle any characters in the path that should be escaped.""" + # FIXME: web browsers don't appear to blindly quote every character + # when converting filenames to files. Instead of using urllib's default + # rules, we allow a small list of other characters through un-escaped. + # It's unclear if this is the best possible solution. + return urllib.quote(path, safe='/+:') + + +def _convert_path(path, platform): + """Handles any os-specific path separators, mappings, etc.""" + if platform == 'win32': + return _winpath_to_uri(path) + if platform == 'cygwin': + return _winpath_to_uri(cygpath(path)) + return _unixypath_to_uri(path) + + +def _winpath_to_uri(path): + """Converts a window absolute path to a file: URL.""" + return "///" + path.replace("\\", "/") + + +def _unixypath_to_uri(path): + """Converts a unix-style path to a file: URL.""" + return "//" + path diff --git a/WebKitTools/Scripts/webkitpy/common/system/path_unittest.py b/WebKitTools/Scripts/webkitpy/common/system/path_unittest.py new file mode 100644 index 0000000..4dbd38a --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/system/path_unittest.py @@ -0,0 +1,105 @@ +# 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. + +import unittest +import sys + +import path + +class AbspathTest(unittest.TestCase): + def assertMatch(self, test_path, expected_uri, + platform=None): + if platform == 'cygwin' and sys.platform != 'cygwin': + return + self.assertEqual(path.abspath_to_uri(test_path, platform=platform), + expected_uri) + + def test_abspath_to_uri_cygwin(self): + if sys.platform != 'cygwin': + return + + self.assertMatch('/cygdrive/c/foo/bar.html', + 'file:///C:/foo/bar.html', + platform='cygwin') + self.assertEqual(path.abspath_to_uri('/cygdrive/c/foo/bar.html', + platform='cygwin'), + 'file:///C:/foo/bar.html') + + def test_abspath_to_uri_darwin(self): + self.assertMatch('/foo/bar.html', + 'file:///foo/bar.html', + platform='darwin') + self.assertEqual(path.abspath_to_uri("/foo/bar.html", + platform='darwin'), + "file:///foo/bar.html") + + def test_abspath_to_uri_linux2(self): + self.assertMatch('/foo/bar.html', + 'file:///foo/bar.html', + platform='darwin') + self.assertEqual(path.abspath_to_uri("/foo/bar.html", + platform='linux2'), + "file:///foo/bar.html") + + def test_abspath_to_uri_win(self): + self.assertMatch('c:\\foo\\bar.html', + 'file:///c:/foo/bar.html', + platform='win32') + self.assertEqual(path.abspath_to_uri("c:\\foo\\bar.html", + platform='win32'), + "file:///c:/foo/bar.html") + + def test_abspath_to_uri_escaping(self): + self.assertMatch('/foo/bar + baz%?.html', + 'file:///foo/bar%20+%20baz%25%3F.html', + platform='darwin') + self.assertMatch('/foo/bar + baz%?.html', + 'file:///foo/bar%20+%20baz%25%3F.html', + platform='linux2') + + # Note that you can't have '?' in a filename on windows. + self.assertMatch('/cygdrive/c/foo/bar + baz%.html', + 'file:///C:/foo/bar%20+%20baz%25.html', + platform='cygwin') + + def test_stop_cygpath_subprocess(self): + if sys.platform != 'cygwin': + return + + # Call cygpath to ensure the subprocess is running. + path.cygpath("/cygdrive/c/foo.txt") + self.assertTrue(path._CygPath._singleton.is_running()) + + # Stop it. + path._CygPath.stop_cygpath_subprocess() + + # Ensure that it is stopped. + self.assertFalse(path._CygPath._singleton.is_running()) + +if __name__ == '__main__': + unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/common/system/user.py b/WebKitTools/Scripts/webkitpy/common/system/user.py index 240b67b..8917137 100644 --- a/WebKitTools/Scripts/webkitpy/common/system/user.py +++ b/WebKitTools/Scripts/webkitpy/common/system/user.py @@ -96,6 +96,10 @@ class User(object): # Note: Not thread safe: http://bugs.python.org/issue2320 subprocess.call(args + files) + def _warn_if_application_is_xcode(self, edit_application): + if "Xcode" in edit_application: + print "Instead of using Xcode.app, consider using EDITOR=\"xed --wait\"." + def edit_changelog(self, files): edit_application = os.environ.get("CHANGE_LOG_EDIT_APPLICATION") if edit_application and sys.platform == "darwin": @@ -103,8 +107,7 @@ class User(object): args = shlex.split(edit_application) print "Using editor in the CHANGE_LOG_EDIT_APPLICATION environment variable." print "Please quit the editor application when done editing." - if edit_application.find("Xcode.app"): - print "Instead of using Xcode.app, consider using EDITOR=\"xed --wait\"." + self._warn_if_application_is_xcode(edit_application) subprocess.call(["open", "-W", "-n", "-a"] + args + files) return self.edit(files) diff --git a/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py b/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py index ae1bad5..7ec9b34 100644 --- a/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py @@ -97,5 +97,13 @@ class UserTest(unittest.TestCase): raw_input=mock_raw_input) self.assertEquals(expected[1], result) -if __name__ == '__main__': - unittest.main() + def test_warn_if_application_is_xcode(self): + output = OutputCapture() + user = User() + output.assert_outputs(self, user._warn_if_application_is_xcode, ["TextMate"]) + output.assert_outputs(self, user._warn_if_application_is_xcode, ["/Applications/TextMate.app"]) + output.assert_outputs(self, user._warn_if_application_is_xcode, ["XCode"]) # case sensitive matching + + xcode_warning = "Instead of using Xcode.app, consider using EDITOR=\"xed --wait\".\n" + output.assert_outputs(self, user._warn_if_application_is_xcode, ["Xcode"], expected_stdout=xcode_warning) + output.assert_outputs(self, user._warn_if_application_is_xcode, ["/Developer/Applications/Xcode.app"], expected_stdout=xcode_warning) |