summaryrefslogtreecommitdiffstats
path: root/WebKitTools/Scripts/webkitpy/common
diff options
context:
space:
mode:
authorBen Murdoch <benm@google.com>2010-10-22 13:02:20 +0100
committerBen Murdoch <benm@google.com>2010-10-26 15:21:41 +0100
commita94275402997c11dd2e778633dacf4b7e630a35d (patch)
treee66f56c67e3b01f22c9c23cd932271ee9ac558ed /WebKitTools/Scripts/webkitpy/common
parent09e26c78506587b3f5d930d7bc72a23287ffbec0 (diff)
downloadexternal_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')
-rw-r--r--WebKitTools/Scripts/webkitpy/common/checkout/api.py24
-rw-r--r--WebKitTools/Scripts/webkitpy/common/checkout/api_unittest.py21
-rw-r--r--WebKitTools/Scripts/webkitpy/common/checkout/diff_parser.py20
-rw-r--r--WebKitTools/Scripts/webkitpy/common/checkout/scm.py35
-rw-r--r--WebKitTools/Scripts/webkitpy/common/checkout/scm_unittest.py28
-rw-r--r--WebKitTools/Scripts/webkitpy/common/config/committers.py9
-rw-r--r--WebKitTools/Scripts/webkitpy/common/config/ports.py30
-rw-r--r--WebKitTools/Scripts/webkitpy/common/config/ports_unittest.py6
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/bugzilla.py7
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/bugzilla_unittest.py15
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/buildbot.py95
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py46
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/failuremap.py54
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/failuremap_unittest.py76
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/layouttestresults.py88
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/layouttestresults_unittest.py76
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/networktransaction.py10
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/networktransaction_unittest.py15
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py17
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/statusserver.py70
-rw-r--r--WebKitTools/Scripts/webkitpy/common/net/statusserver_unittest.py43
-rw-r--r--WebKitTools/Scripts/webkitpy/common/system/executive.py14
-rw-r--r--WebKitTools/Scripts/webkitpy/common/system/outputcapture.py27
-rw-r--r--WebKitTools/Scripts/webkitpy/common/system/path.py134
-rw-r--r--WebKitTools/Scripts/webkitpy/common/system/path_unittest.py105
-rw-r--r--WebKitTools/Scripts/webkitpy/common/system/user.py7
-rw-r--r--WebKitTools/Scripts/webkitpy/common/system/user_unittest.py12
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)