[SCM] WebKit Debian packaging branch, webkit-1.1, updated. upstream/1.1.17-1283-gcf603cf
eric at webkit.org
eric at webkit.org
Wed Jan 6 00:16:09 UTC 2010
The following commit has been merged in the webkit-1.1 branch:
commit d88a4e606e7a5c23ed99952e35edbd7693ba29ae
Author: eric at webkit.org <eric at webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Date: Mon Jan 4 07:04:57 2010 +0000
2010-01-03 Eric Seidel <eric at webkit.org>
Reviewed by Adam Barth.
Rename Scripts/modules to Scripts/webkitpy
https://bugs.webkit.org/show_bug.cgi?id=33128
Just search-replace and svn mv commands.
* Scripts/bugzilla-tool:
* Scripts/check-webkit-style:
* Scripts/modules: Removed.
* Scripts/modules/BeautifulSoup.py: Removed.
* Scripts/modules/__init__.py: Removed.
* Scripts/modules/bugzilla.py: Removed.
* Scripts/modules/bugzilla_unittest.py: Removed.
* Scripts/modules/buildbot.py: Removed.
* Scripts/modules/buildbot_unittest.py: Removed.
* Scripts/modules/buildsteps.py: Removed.
* Scripts/modules/buildsteps_unittest.py: Removed.
* Scripts/modules/changelogs.py: Removed.
* Scripts/modules/changelogs_unittest.py: Removed.
* Scripts/modules/commands: Removed.
* Scripts/modules/commands/__init__.py: Removed.
* Scripts/modules/commands/commandtest.py: Removed.
* Scripts/modules/commands/download.py: Removed.
* Scripts/modules/commands/download_unittest.py: Removed.
* Scripts/modules/commands/early_warning_system.py: Removed.
* Scripts/modules/commands/early_warning_system_unittest.py: Removed.
* Scripts/modules/commands/queries.py: Removed.
* Scripts/modules/commands/queries_unittest.py: Removed.
* Scripts/modules/commands/queues.py: Removed.
* Scripts/modules/commands/queues_unittest.py: Removed.
* Scripts/modules/commands/queuestest.py: Removed.
* Scripts/modules/commands/upload.py: Removed.
* Scripts/modules/commands/upload_unittest.py: Removed.
* Scripts/modules/comments.py: Removed.
* Scripts/modules/committers.py: Removed.
* Scripts/modules/committers_unittest.py: Removed.
* Scripts/modules/cpp_style.py: Removed.
* Scripts/modules/cpp_style_unittest.py: Removed.
* Scripts/modules/credentials.py: Removed.
* Scripts/modules/credentials_unittest.py: Removed.
* Scripts/modules/diff_parser.py: Removed.
* Scripts/modules/diff_parser_unittest.py: Removed.
* Scripts/modules/executive.py: Removed.
* Scripts/modules/executive_unittest.py: Removed.
* Scripts/modules/grammar.py: Removed.
* Scripts/modules/mock.py: Removed.
* Scripts/modules/mock_bugzillatool.py: Removed.
* Scripts/modules/multicommandtool.py: Removed.
* Scripts/modules/multicommandtool_unittest.py: Removed.
* Scripts/modules/outputcapture.py: Removed.
* Scripts/modules/patchcollection.py: Removed.
* Scripts/modules/queueengine.py: Removed.
* Scripts/modules/queueengine_unittest.py: Removed.
* Scripts/modules/scm.py: Removed.
* Scripts/modules/scm_unittest.py: Removed.
* Scripts/modules/statusbot.py: Removed.
* Scripts/modules/stepsequence.py: Removed.
* Scripts/modules/style: Removed.
* Scripts/modules/style.py: Removed.
* Scripts/modules/style_unittest.py: Removed.
* Scripts/modules/text_style.py: Removed.
* Scripts/modules/text_style_unittest.py: Removed.
* Scripts/modules/user.py: Removed.
* Scripts/modules/webkit_logging.py: Removed.
* Scripts/modules/webkit_logging_unittest.py: Removed.
* Scripts/modules/webkit_mechanize.py: Removed.
* Scripts/modules/webkitport.py: Removed.
* Scripts/modules/webkitport_unittest.py: Removed.
* Scripts/test-webkit-python: Removed.
* Scripts/test-webkitpy: Copied from WebKitTools/Scripts/test-webkit-python.
* Scripts/validate-committer-lists:
* Scripts/webkitpy: Copied from WebKitTools/Scripts/modules.
* Scripts/webkitpy/bugzilla.py:
* Scripts/webkitpy/bugzilla_unittest.py:
* Scripts/webkitpy/buildbot.py:
* Scripts/webkitpy/buildbot_unittest.py:
* Scripts/webkitpy/buildsteps.py:
* Scripts/webkitpy/buildsteps_unittest.py:
* Scripts/webkitpy/commands/commandtest.py:
* Scripts/webkitpy/commands/download.py:
* Scripts/webkitpy/commands/download_unittest.py:
* Scripts/webkitpy/commands/early_warning_system.py:
* Scripts/webkitpy/commands/early_warning_system_unittest.py:
* Scripts/webkitpy/commands/queries.py:
* Scripts/webkitpy/commands/queries_unittest.py:
* Scripts/webkitpy/commands/queues.py:
* Scripts/webkitpy/commands/queues_unittest.py:
* Scripts/webkitpy/commands/queuestest.py:
* Scripts/webkitpy/commands/upload.py:
* Scripts/webkitpy/commands/upload_unittest.py:
* Scripts/webkitpy/comments.py:
* Scripts/webkitpy/credentials.py:
* Scripts/webkitpy/credentials_unittest.py:
* Scripts/webkitpy/executive.py:
* Scripts/webkitpy/executive_unittest.py:
* Scripts/webkitpy/mock_bugzillatool.py:
* Scripts/webkitpy/multicommandtool.py:
* Scripts/webkitpy/multicommandtool_unittest.py:
* Scripts/webkitpy/queueengine.py:
* Scripts/webkitpy/queueengine_unittest.py:
* Scripts/webkitpy/scm.py:
* Scripts/webkitpy/scm_unittest.py:
* Scripts/webkitpy/statusbot.py:
* Scripts/webkitpy/stepsequence.py:
* Scripts/webkitpy/webkit_logging_unittest.py:
* Scripts/webkitpy/webkitport_unittest.py:
git-svn-id: http://svn.webkit.org/repository/webkit/trunk@52703 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/WebKitTools/ChangeLog b/WebKitTools/ChangeLog
index eed06c3..74cf749 100644
--- a/WebKitTools/ChangeLog
+++ b/WebKitTools/ChangeLog
@@ -1,3 +1,113 @@
+2010-01-03 Eric Seidel <eric at webkit.org>
+
+ Reviewed by Adam Barth.
+
+ Rename Scripts/modules to Scripts/webkitpy
+ https://bugs.webkit.org/show_bug.cgi?id=33128
+
+ Just search-replace and svn mv commands.
+
+ * Scripts/bugzilla-tool:
+ * Scripts/check-webkit-style:
+ * Scripts/modules: Removed.
+ * Scripts/modules/BeautifulSoup.py: Removed.
+ * Scripts/modules/__init__.py: Removed.
+ * Scripts/modules/bugzilla.py: Removed.
+ * Scripts/modules/bugzilla_unittest.py: Removed.
+ * Scripts/modules/buildbot.py: Removed.
+ * Scripts/modules/buildbot_unittest.py: Removed.
+ * Scripts/modules/buildsteps.py: Removed.
+ * Scripts/modules/buildsteps_unittest.py: Removed.
+ * Scripts/modules/changelogs.py: Removed.
+ * Scripts/modules/changelogs_unittest.py: Removed.
+ * Scripts/modules/commands: Removed.
+ * Scripts/modules/commands/__init__.py: Removed.
+ * Scripts/modules/commands/commandtest.py: Removed.
+ * Scripts/modules/commands/download.py: Removed.
+ * Scripts/modules/commands/download_unittest.py: Removed.
+ * Scripts/modules/commands/early_warning_system.py: Removed.
+ * Scripts/modules/commands/early_warning_system_unittest.py: Removed.
+ * Scripts/modules/commands/queries.py: Removed.
+ * Scripts/modules/commands/queries_unittest.py: Removed.
+ * Scripts/modules/commands/queues.py: Removed.
+ * Scripts/modules/commands/queues_unittest.py: Removed.
+ * Scripts/modules/commands/queuestest.py: Removed.
+ * Scripts/modules/commands/upload.py: Removed.
+ * Scripts/modules/commands/upload_unittest.py: Removed.
+ * Scripts/modules/comments.py: Removed.
+ * Scripts/modules/committers.py: Removed.
+ * Scripts/modules/committers_unittest.py: Removed.
+ * Scripts/modules/cpp_style.py: Removed.
+ * Scripts/modules/cpp_style_unittest.py: Removed.
+ * Scripts/modules/credentials.py: Removed.
+ * Scripts/modules/credentials_unittest.py: Removed.
+ * Scripts/modules/diff_parser.py: Removed.
+ * Scripts/modules/diff_parser_unittest.py: Removed.
+ * Scripts/modules/executive.py: Removed.
+ * Scripts/modules/executive_unittest.py: Removed.
+ * Scripts/modules/grammar.py: Removed.
+ * Scripts/modules/mock.py: Removed.
+ * Scripts/modules/mock_bugzillatool.py: Removed.
+ * Scripts/modules/multicommandtool.py: Removed.
+ * Scripts/modules/multicommandtool_unittest.py: Removed.
+ * Scripts/modules/outputcapture.py: Removed.
+ * Scripts/modules/patchcollection.py: Removed.
+ * Scripts/modules/queueengine.py: Removed.
+ * Scripts/modules/queueengine_unittest.py: Removed.
+ * Scripts/modules/scm.py: Removed.
+ * Scripts/modules/scm_unittest.py: Removed.
+ * Scripts/modules/statusbot.py: Removed.
+ * Scripts/modules/stepsequence.py: Removed.
+ * Scripts/modules/style: Removed.
+ * Scripts/modules/style.py: Removed.
+ * Scripts/modules/style_unittest.py: Removed.
+ * Scripts/modules/text_style.py: Removed.
+ * Scripts/modules/text_style_unittest.py: Removed.
+ * Scripts/modules/user.py: Removed.
+ * Scripts/modules/webkit_logging.py: Removed.
+ * Scripts/modules/webkit_logging_unittest.py: Removed.
+ * Scripts/modules/webkit_mechanize.py: Removed.
+ * Scripts/modules/webkitport.py: Removed.
+ * Scripts/modules/webkitport_unittest.py: Removed.
+ * Scripts/test-webkit-python: Removed.
+ * Scripts/test-webkitpy: Copied from WebKitTools/Scripts/test-webkit-python.
+ * Scripts/validate-committer-lists:
+ * Scripts/webkitpy: Copied from WebKitTools/Scripts/modules.
+ * Scripts/webkitpy/bugzilla.py:
+ * Scripts/webkitpy/bugzilla_unittest.py:
+ * Scripts/webkitpy/buildbot.py:
+ * Scripts/webkitpy/buildbot_unittest.py:
+ * Scripts/webkitpy/buildsteps.py:
+ * Scripts/webkitpy/buildsteps_unittest.py:
+ * Scripts/webkitpy/commands/commandtest.py:
+ * Scripts/webkitpy/commands/download.py:
+ * Scripts/webkitpy/commands/download_unittest.py:
+ * Scripts/webkitpy/commands/early_warning_system.py:
+ * Scripts/webkitpy/commands/early_warning_system_unittest.py:
+ * Scripts/webkitpy/commands/queries.py:
+ * Scripts/webkitpy/commands/queries_unittest.py:
+ * Scripts/webkitpy/commands/queues.py:
+ * Scripts/webkitpy/commands/queues_unittest.py:
+ * Scripts/webkitpy/commands/queuestest.py:
+ * Scripts/webkitpy/commands/upload.py:
+ * Scripts/webkitpy/commands/upload_unittest.py:
+ * Scripts/webkitpy/comments.py:
+ * Scripts/webkitpy/credentials.py:
+ * Scripts/webkitpy/credentials_unittest.py:
+ * Scripts/webkitpy/executive.py:
+ * Scripts/webkitpy/executive_unittest.py:
+ * Scripts/webkitpy/mock_bugzillatool.py:
+ * Scripts/webkitpy/multicommandtool.py:
+ * Scripts/webkitpy/multicommandtool_unittest.py:
+ * Scripts/webkitpy/queueengine.py:
+ * Scripts/webkitpy/queueengine_unittest.py:
+ * Scripts/webkitpy/scm.py:
+ * Scripts/webkitpy/scm_unittest.py:
+ * Scripts/webkitpy/statusbot.py:
+ * Scripts/webkitpy/stepsequence.py:
+ * Scripts/webkitpy/webkit_logging_unittest.py:
+ * Scripts/webkitpy/webkitport_unittest.py:
+
2010-01-03 Chris Jerdonek <chris.jerdonek at gmail.com>
Reviewed by Eric Seidel.
diff --git a/WebKitTools/Scripts/bugzilla-tool b/WebKitTools/Scripts/bugzilla-tool
index 9305b66..c8a52e9 100755
--- a/WebKitTools/Scripts/bugzilla-tool
+++ b/WebKitTools/Scripts/bugzilla-tool
@@ -32,18 +32,18 @@
import os
-from modules.bugzilla import Bugzilla
-from modules.buildbot import BuildBot
-from modules.commands.download import *
-from modules.commands.early_warning_system import *
-from modules.commands.queries import *
-from modules.commands.queues import *
-from modules.commands.upload import *
-from modules.executive import Executive
-from modules.webkit_logging import log
-from modules.multicommandtool import MultiCommandTool
-from modules.scm import detect_scm_system
-from modules.user import User
+from webkitpy.bugzilla import Bugzilla
+from webkitpy.buildbot import BuildBot
+from webkitpy.commands.download import *
+from webkitpy.commands.early_warning_system import *
+from webkitpy.commands.queries import *
+from webkitpy.commands.queues import *
+from webkitpy.commands.upload import *
+from webkitpy.executive import Executive
+from webkitpy.webkit_logging import log
+from webkitpy.multicommandtool import MultiCommandTool
+from webkitpy.scm import detect_scm_system
+from webkitpy.user import User
class BugzillaTool(MultiCommandTool):
diff --git a/WebKitTools/Scripts/check-webkit-style b/WebKitTools/Scripts/check-webkit-style
index 6634854..08983d8 100755
--- a/WebKitTools/Scripts/check-webkit-style
+++ b/WebKitTools/Scripts/check-webkit-style
@@ -46,8 +46,8 @@ import os
import os.path
import sys
-import modules.style as style
-from modules.scm import detect_scm_system
+import webkitpy.style as style
+from webkitpy.scm import detect_scm_system
def main():
diff --git a/WebKitTools/Scripts/modules/bugzilla.py b/WebKitTools/Scripts/modules/bugzilla.py
deleted file mode 100644
index e6536a6..0000000
--- a/WebKitTools/Scripts/modules/bugzilla.py
+++ /dev/null
@@ -1,563 +0,0 @@
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple 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.
-#
-# WebKit's Python module for interacting with Bugzilla
-
-import re
-import subprocess
-
-from datetime import datetime # used in timestamp()
-
-# Import WebKit-specific modules.
-from modules.webkit_logging import error, log
-from modules.committers import CommitterList
-from modules.credentials import Credentials
-
-# WebKit includes a built copy of BeautifulSoup in Scripts/modules
-# so this import should always succeed.
-from .BeautifulSoup import BeautifulSoup, SoupStrainer
-
-from modules.webkit_mechanize import Browser
-
-def parse_bug_id(message):
- match = re.search("http\://webkit\.org/b/(?P<bug_id>\d+)", message)
- if match:
- return int(match.group('bug_id'))
- match = re.search(Bugzilla.bug_server_regex + "show_bug\.cgi\?id=(?P<bug_id>\d+)", message)
- if match:
- return int(match.group('bug_id'))
- return None
-
-
-def timestamp():
- return datetime.now().strftime("%Y%m%d%H%M%S")
-
-
-# FIXME: This class is kinda a hack for now. It exists so we have one place
-# to hold bug logic, even if much of the code deals with dictionaries still.
-class Bug(object):
- def __init__(self, bug_dictionary):
- self.bug_dictionary = bug_dictionary
-
- def assigned_to_email(self):
- return self.bug_dictionary["assigned_to_email"]
-
- # Rarely do we actually want obsolete attachments
- def attachments(self, include_obsolete=False):
- if include_obsolete:
- return self.bug_dictionary["attachments"][:] # Return a copy in either case.
- return [attachment for attachment in self.bug_dictionary["attachments"] if not attachment["is_obsolete"]]
-
- def patches(self, include_obsolete=False):
- return [patch for patch in self.attachments(include_obsolete) if patch["is_patch"]]
-
- def unreviewed_patches(self):
- return [patch for patch in self.patches() if patch.get("review") == "?"]
-
-
-# A container for all of the logic for making a parsing buzilla queries.
-class BugzillaQueries(object):
- def __init__(self, bugzilla):
- self.bugzilla = bugzilla
-
- def _load_query(self, query):
- full_url = "%s%s" % (self.bugzilla.bug_server_url, query)
- return self.bugzilla.browser.open(full_url)
-
- def _fetch_bug_ids_advanced_query(self, query):
- soup = BeautifulSoup(self._load_query(query))
- # The contents of the <a> inside the cells in the first column happen to be the bug id.
- return [int(bug_link_cell.find("a").string) for bug_link_cell in soup('td', "first-child")]
-
- def _parse_attachment_ids_request_query(self, page):
- digits = re.compile("\d+")
- attachment_href = re.compile("attachment.cgi\?id=\d+&action=review")
- attachment_links = SoupStrainer("a", href=attachment_href)
- return [int(digits.search(tag["href"]).group(0)) for tag in BeautifulSoup(page, parseOnlyThese=attachment_links)]
-
- def _fetch_attachment_ids_request_query(self, query):
- return self._parse_attachment_ids_request_query(self._load_query(query))
-
- # List of all r+'d bugs.
- def fetch_bug_ids_from_pending_commit_list(self):
- needs_commit_query_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%2B"
- return self._fetch_bug_ids_advanced_query(needs_commit_query_url)
-
- def fetch_patches_from_pending_commit_list(self):
- # FIXME: This should not have to go through self.bugzilla
- return sum([self.bugzilla.fetch_reviewed_patches_from_bug(bug_id) for bug_id in self.fetch_bug_ids_from_pending_commit_list()], [])
-
- def fetch_bug_ids_from_commit_queue(self):
- commit_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=commit-queue%2B"
- return self._fetch_bug_ids_advanced_query(commit_queue_url)
-
- def fetch_patches_from_commit_queue(self, reject_invalid_patches=False):
- # FIXME: Once reject_invalid_patches is moved out of this function this becomes a simple list comprehension using fetch_bug_ids_from_commit_queue.
- patches_to_land = []
- for bug_id in self.fetch_bug_ids_from_commit_queue():
- # FIXME: This should not have to go through self.bugzilla
- patches = self.bugzilla.fetch_commit_queue_patches_from_bug(bug_id, reject_invalid_patches)
- patches_to_land += patches
- return patches_to_land
-
- def _fetch_bug_ids_from_review_queue(self):
- 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)
-
- def fetch_patches_from_review_queue(self, limit=None):
- # FIXME: We should probably have a self.fetch_bug to minimize the number of self.bugzilla calls.
- return sum([self.bugzilla.fetch_bug(bug_id).unreviewed_patches() for bug_id in self._fetch_bug_ids_from_review_queue()[:limit]], []) # [:None] returns the whole array.
-
- # 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
- 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)
-
-
-class Bugzilla(object):
- def __init__(self, dryrun=False, committers=CommitterList()):
- self.dryrun = dryrun
- self.authenticated = False
- self.queries = BugzillaQueries(self)
-
- # FIXME: We should use some sort of Browser mock object when in dryrun mode (to prevent any mistakes).
- self.browser = Browser()
- # Ignore bugs.webkit.org/robots.txt until we fix it to allow this script
- self.browser.set_handle_robots(False)
- self.committers = committers
-
- # FIXME: Much of this should go into some sort of config module:
- bug_server_host = "bugs.webkit.org"
- bug_server_regex = "https?://%s/" % re.sub('\.', '\\.', bug_server_host)
- bug_server_url = "https://%s/" % bug_server_host
- unassigned_email = "webkit-unassigned at lists.webkit.org"
-
- def bug_url_for_bug_id(self, bug_id, xml=False):
- content_type = "&ctype=xml" if xml else ""
- return "%sshow_bug.cgi?id=%s%s" % (self.bug_server_url, bug_id, content_type)
-
- def short_bug_url_for_bug_id(self, bug_id):
- return "http://webkit.org/b/%s" % bug_id
-
- def attachment_url_for_id(self, attachment_id, action="view"):
- action_param = ""
- if action and action != "view":
- action_param = "&action=%s" % action
- return "%sattachment.cgi?id=%s%s" % (self.bug_server_url, attachment_id, action_param)
-
- def _parse_attachment_flag(self, element, flag_name, attachment, result_key):
- flag = element.find('flag', attrs={'name' : flag_name})
- if flag:
- attachment[flag_name] = flag['status']
- if flag['status'] == '+':
- attachment[result_key] = flag['setter']
-
- def _parse_attachment_element(self, element, bug_id):
- attachment = {}
- attachment['bug_id'] = bug_id
- attachment['is_obsolete'] = (element.has_key('isobsolete') and element['isobsolete'] == "1")
- attachment['is_patch'] = (element.has_key('ispatch') and element['ispatch'] == "1")
- attachment['id'] = int(element.find('attachid').string)
- attachment['url'] = self.attachment_url_for_id(attachment['id'])
- attachment['name'] = unicode(element.find('desc').string)
- attachment['attacher_email'] = str(element.find('attacher').string)
- attachment['type'] = str(element.find('type').string)
- self._parse_attachment_flag(element, 'review', attachment, 'reviewer_email')
- self._parse_attachment_flag(element, 'commit-queue', attachment, 'committer_email')
- return attachment
-
- def _parse_bug_page(self, page):
- soup = BeautifulSoup(page)
- bug = {}
- bug["id"] = int(soup.find("bug_id").string)
- bug["title"] = unicode(soup.find("short_desc").string)
- bug["reporter_email"] = str(soup.find("reporter").string)
- bug["assigned_to_email"] = str(soup.find("assigned_to").string)
- bug["cc_emails"] = [str(element.string) for element in soup.findAll('cc')]
- bug["attachments"] = [self._parse_attachment_element(element, bug["id"]) for element in soup.findAll('attachment')]
- return bug
-
- # Makes testing fetch_*_from_bug() possible until we have a better BugzillaNetwork abstration.
- def _fetch_bug_page(self, bug_id):
- bug_url = self.bug_url_for_bug_id(bug_id, xml=True)
- log("Fetching: %s" % bug_url)
- return self.browser.open(bug_url)
-
- def fetch_bug_dictionary(self, bug_id):
- return self._parse_bug_page(self._fetch_bug_page(bug_id))
-
- # FIXME: A BugzillaCache object should provide all these fetch_ methods.
- def fetch_bug(self, bug_id):
- return Bug(self.fetch_bug_dictionary(bug_id))
-
- def _parse_bug_id_from_attachment_page(self, page):
- up_link = BeautifulSoup(page).find('link', rel='Up') # The "Up" relation happens to point to the bug.
- if not up_link:
- return None # This attachment does not exist (or you don't have permissions to view it).
- match = re.search("show_bug.cgi\?id=(?P<bug_id>\d+)", up_link['href'])
- return int(match.group('bug_id'))
-
- def bug_id_for_attachment_id(self, attachment_id):
- attachment_url = self.attachment_url_for_id(attachment_id, 'edit')
- log("Fetching: %s" % attachment_url)
- page = self.browser.open(attachment_url)
- return self._parse_bug_id_from_attachment_page(page)
-
- # This should really return an Attachment object
- # which can lazily fetch any missing data.
- def fetch_attachment(self, attachment_id):
- # We could grab all the attachment details off of the attachment edit page
- # but we already have working code to do so off of the bugs page, so re-use that.
- bug_id = self.bug_id_for_attachment_id(attachment_id)
- if not bug_id:
- return None
- for attachment in self.fetch_bug(bug_id).attachments(include_obsolete=True):
- # FIXME: Once we have a real Attachment class we shouldn't paper over this possible comparison failure
- # and we should remove the int() == int() hacks and leave it just ==.
- if int(attachment['id']) == int(attachment_id):
- self._validate_committer_and_reviewer(attachment)
- return attachment
- return None # This should never be hit.
-
- # fetch_patches_from_bug exists until we expose a Bug class outside of bugzilla.py
- def fetch_patches_from_bug(self, bug_id):
- return self.fetch_bug(bug_id).patches()
-
- # _view_source_link belongs in some sort of webkit_config.py module.
- def _view_source_link(self, local_path):
- return "http://trac.webkit.org/browser/trunk/%s" % local_path
-
- def _flag_permission_rejection_message(self, setter_email, flag_name):
- committer_list = "WebKitTools/Scripts/modules/committers.py" # This could be computed from CommitterList.__file__
- contribution_guidlines_url = "http://webkit.org/coding/contributing.html" # Should come from some webkit_config.py
- queue_administrator = "eseidel at chromium.org" # This could be queried from the status_bot.
- queue_name = "commit-queue" # This could be queried from the tool.
- rejection_message = "%s does not have %s permissions according to %s." % (setter_email, flag_name, self._view_source_link(committer_list))
- rejection_message += "\n\n- If you do not have %s rights please read %s for instructions on how to use bugzilla flags." % (flag_name, contribution_guidlines_url)
- rejection_message += "\n\n- If you have %s rights please correct the error in %s by adding yourself to the file (no review needed)." % (flag_name, committer_list)
- rejection_message += " Due to bug 30084 the %s will require a restart after your change." % queue_name
- rejection_message += " Please contact %s to request a %s restart." % (queue_administrator, queue_name)
- rejection_message += " After restart the %s will correctly respect your %s rights." % (queue_name, flag_name)
- return rejection_message
-
- def _validate_setter_email(self, patch, result_key, lookup_function, rejection_function, reject_invalid_patches):
- setter_email = patch.get(result_key + '_email')
- if not setter_email:
- return None
-
- committer = lookup_function(setter_email)
- if committer:
- patch[result_key] = committer.full_name
- return patch[result_key]
-
- if reject_invalid_patches:
- rejection_function(patch['id'], self._flag_permission_rejection_message(setter_email, result_key))
- else:
- log("Warning, attachment %s on bug %s has invalid %s (%s)" % (patch['id'], patch['bug_id'], result_key, setter_email))
- return None
-
- def _validate_reviewer(self, patch, reject_invalid_patches):
- return self._validate_setter_email(patch, 'reviewer', self.committers.reviewer_by_email, self.reject_patch_from_review_queue, reject_invalid_patches)
-
- def _validate_committer(self, patch, reject_invalid_patches):
- return self._validate_setter_email(patch, 'committer', self.committers.committer_by_email, self.reject_patch_from_commit_queue, reject_invalid_patches)
-
- # FIXME: This is a hack until we have a real Attachment object.
- # _validate_committer and _validate_reviewer fill in the 'reviewer' and 'committer'
- # keys which other parts of the code expect to be filled in.
- def _validate_committer_and_reviewer(self, patch):
- self._validate_reviewer(patch, reject_invalid_patches=False)
- self._validate_committer(patch, reject_invalid_patches=False)
-
- # FIXME: fetch_reviewed_patches_from_bug and fetch_commit_queue_patches_from_bug
- # should share more code and use list comprehensions.
- def fetch_reviewed_patches_from_bug(self, bug_id, reject_invalid_patches=False):
- reviewed_patches = []
- for attachment in self.fetch_bug(bug_id).attachments():
- if self._validate_reviewer(attachment, reject_invalid_patches):
- reviewed_patches.append(attachment)
- return reviewed_patches
-
- def fetch_commit_queue_patches_from_bug(self, bug_id, reject_invalid_patches=False):
- commit_queue_patches = []
- for attachment in self.fetch_reviewed_patches_from_bug(bug_id, reject_invalid_patches):
- if self._validate_committer(attachment, reject_invalid_patches):
- commit_queue_patches.append(attachment)
- return commit_queue_patches
-
- def authenticate(self):
- if self.authenticated:
- return
-
- if self.dryrun:
- log("Skipping log in for dry run...")
- self.authenticated = True
- return
-
- (username, password) = Credentials(self.bug_server_host, git_prefix="bugzilla").read_credentials()
-
- log("Logging in as %s..." % username)
- self.browser.open(self.bug_server_url + "index.cgi?GoAheadAndLogIn=1")
- self.browser.select_form(name="login")
- self.browser['Bugzilla_login'] = username
- self.browser['Bugzilla_password'] = password
- response = self.browser.submit()
-
- match = re.search("<title>(.+?)</title>", response.read())
- # If the resulting page has a title, and it contains the word "invalid" assume it's the login failure page.
- if match and re.search("Invalid", match.group(1), re.IGNORECASE):
- # FIXME: We could add the ability to try again on failure.
- raise Exception("Bugzilla login failed: %s" % match.group(1))
-
- self.authenticated = True
-
- def _fill_attachment_form(self, description, patch_file_object, comment_text=None, mark_for_review=False, mark_for_commit_queue=False, bug_id=None):
- self.browser['description'] = description
- self.browser['ispatch'] = ("1",)
- self.browser['flag_type-1'] = ('?',) if mark_for_review else ('X',)
- self.browser['flag_type-3'] = ('?',) if mark_for_commit_queue else ('X',)
- if bug_id:
- patch_name = "bug-%s-%s.patch" % (bug_id, timestamp())
- else:
- patch_name ="%s.patch" % timestamp()
- self.browser.add_file(patch_file_object, "text/plain", patch_name, 'data')
-
- def add_patch_to_bug(self, bug_id, patch_file_object, description, comment_text=None, mark_for_review=False, mark_for_commit_queue=False):
- self.authenticate()
-
- log('Adding patch "%s" to bug %s' % (description, bug_id))
- if self.dryrun:
- log(comment_text)
- return
-
- self.browser.open("%sattachment.cgi?action=enter&bugid=%s" % (self.bug_server_url, bug_id))
- self.browser.select_form(name="entryform")
- self._fill_attachment_form(description, patch_file_object, mark_for_review=mark_for_review, mark_for_commit_queue=mark_for_commit_queue, bug_id=bug_id)
- if comment_text:
- log(comment_text)
- self.browser['comment'] = comment_text
- self.browser.submit()
-
- def prompt_for_component(self, components):
- log("Please pick a component:")
- i = 0
- for name in components:
- i += 1
- log("%2d. %s" % (i, name))
- result = int(raw_input("Enter a number: ")) - 1
- return components[result]
-
- def _check_create_bug_response(self, response_html):
- match = re.search("<title>Bug (?P<bug_id>\d+) Submitted</title>", response_html)
- if match:
- return match.group('bug_id')
-
- match = re.search('<div id="bugzilla-body">(?P<error_message>.+)<div id="footer">', response_html, re.DOTALL)
- error_message = "FAIL"
- if match:
- text_lines = BeautifulSoup(match.group('error_message')).findAll(text=True)
- error_message = "\n" + '\n'.join([" " + line.strip() for line in text_lines if line.strip()])
- raise Exception("Bug not created: %s" % error_message)
-
- def create_bug(self, bug_title, bug_description, component=None, patch_file_object=None, patch_description=None, cc=None, mark_for_review=False, mark_for_commit_queue=False):
- self.authenticate()
-
- log('Creating bug with title "%s"' % bug_title)
- if self.dryrun:
- log(bug_description)
- return
-
- self.browser.open(self.bug_server_url + "enter_bug.cgi?product=WebKit")
- self.browser.select_form(name="Create")
- component_items = self.browser.find_control('component').items
- component_names = map(lambda item: item.name, component_items)
- if not component or component not in component_names:
- component = self.prompt_for_component(component_names)
- self.browser['component'] = [component]
- if cc:
- self.browser['cc'] = cc
- self.browser['short_desc'] = bug_title
- self.browser['comment'] = bug_description
-
- if patch_file_object:
- self._fill_attachment_form(patch_description, patch_file_object, mark_for_review=mark_for_review, mark_for_commit_queue=mark_for_commit_queue)
-
- response = self.browser.submit()
-
- bug_id = self._check_create_bug_response(response.read())
- log("Bug %s created." % bug_id)
- log("%sshow_bug.cgi?id=%s" % (self.bug_server_url, bug_id))
- return bug_id
-
- def _find_select_element_for_flag(self, flag_name):
- # FIXME: This will break if we ever re-order attachment flags
- if flag_name == "review":
- return self.browser.find_control(type='select', nr=0)
- if flag_name == "commit-queue":
- return self.browser.find_control(type='select', nr=1)
- raise Exception("Don't know how to find flag named \"%s\"" % flag_name)
-
- def clear_attachment_flags(self, attachment_id, additional_comment_text=None):
- self.authenticate()
-
- comment_text = "Clearing flags on attachment: %s" % attachment_id
- if additional_comment_text:
- comment_text += "\n\n%s" % additional_comment_text
- log(comment_text)
-
- if self.dryrun:
- return
-
- self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
- self.browser.select_form(nr=1)
- self.browser.set_value(comment_text, name='comment', nr=0)
- self._find_select_element_for_flag('review').value = ("X",)
- self._find_select_element_for_flag('commit-queue').value = ("X",)
- self.browser.submit()
-
- # FIXME: We need a way to test this on a live bugzilla instance.
- def _set_flag_on_attachment(self, attachment_id, flag_name, flag_value, comment_text, additional_comment_text):
- self.authenticate()
-
- if additional_comment_text:
- comment_text += "\n\n%s" % additional_comment_text
- log(comment_text)
-
- if self.dryrun:
- return
-
- self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
- self.browser.select_form(nr=1)
- self.browser.set_value(comment_text, name='comment', nr=0)
- self._find_select_element_for_flag(flag_name).value = (flag_value,)
- self.browser.submit()
-
- def reject_patch_from_commit_queue(self, attachment_id, additional_comment_text=None):
- comment_text = "Rejecting patch %s from commit-queue." % attachment_id
- self._set_flag_on_attachment(attachment_id, 'commit-queue', '-', comment_text, additional_comment_text)
-
- def reject_patch_from_review_queue(self, attachment_id, additional_comment_text=None):
- comment_text = "Rejecting patch %s from review queue." % attachment_id
- self._set_flag_on_attachment(attachment_id, 'review', '-', comment_text, additional_comment_text)
-
- # FIXME: All of these bug editing methods have a ridiculous amount of copy/paste code.
- def obsolete_attachment(self, attachment_id, comment_text = None):
- self.authenticate()
-
- log("Obsoleting attachment: %s" % attachment_id)
- if self.dryrun:
- log(comment_text)
- return
-
- self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
- self.browser.select_form(nr=1)
- self.browser.find_control('isobsolete').items[0].selected = True
- # Also clear any review flag (to remove it from review/commit queues)
- self._find_select_element_for_flag('review').value = ("X",)
- self._find_select_element_for_flag('commit-queue').value = ("X",)
- if comment_text:
- log(comment_text)
- # Bugzilla has two textareas named 'comment', one is somehow hidden. We want the first.
- self.browser.set_value(comment_text, name='comment', nr=0)
- self.browser.submit()
-
- def add_cc_to_bug(self, bug_id, email_address_list):
- self.authenticate()
-
- log("Adding %s to the CC list for bug %s" % (email_address_list, bug_id))
- if self.dryrun:
- return
-
- self.browser.open(self.bug_url_for_bug_id(bug_id))
- self.browser.select_form(name="changeform")
- self.browser["newcc"] = ", ".join(email_address_list)
- self.browser.submit()
-
- def post_comment_to_bug(self, bug_id, comment_text, cc=None):
- self.authenticate()
-
- log("Adding comment to bug %s" % bug_id)
- if self.dryrun:
- log(comment_text)
- return
-
- self.browser.open(self.bug_url_for_bug_id(bug_id))
- self.browser.select_form(name="changeform")
- self.browser["comment"] = comment_text
- if cc:
- self.browser["newcc"] = ", ".join(cc)
- self.browser.submit()
-
- def close_bug_as_fixed(self, bug_id, comment_text=None):
- self.authenticate()
-
- log("Closing bug %s as fixed" % bug_id)
- if self.dryrun:
- log(comment_text)
- return
-
- self.browser.open(self.bug_url_for_bug_id(bug_id))
- self.browser.select_form(name="changeform")
- if comment_text:
- log(comment_text)
- self.browser['comment'] = comment_text
- self.browser['bug_status'] = ['RESOLVED']
- self.browser['resolution'] = ['FIXED']
- self.browser.submit()
-
- def reassign_bug(self, bug_id, assignee, comment_text=None):
- self.authenticate()
-
- log("Assigning bug %s to %s" % (bug_id, assignee))
- if self.dryrun:
- log(comment_text)
- return
-
- self.browser.open(self.bug_url_for_bug_id(bug_id))
- self.browser.select_form(name="changeform")
- if comment_text:
- log(comment_text)
- self.browser["comment"] = comment_text
- self.browser["assigned_to"] = assignee
- self.browser.submit()
-
- def reopen_bug(self, bug_id, comment_text):
- self.authenticate()
-
- log("Re-opening bug %s" % bug_id)
- log(comment_text) # Bugzilla requires a comment when re-opening a bug, so we know it will never be None.
- if self.dryrun:
- return
-
- self.browser.open(self.bug_url_for_bug_id(bug_id))
- self.browser.select_form(name="changeform")
- self.browser['bug_status'] = ['REOPENED']
- self.browser['comment'] = comment_text
- self.browser.submit()
diff --git a/WebKitTools/Scripts/modules/bugzilla_unittest.py b/WebKitTools/Scripts/modules/bugzilla_unittest.py
deleted file mode 100644
index bcc4b81..0000000
--- a/WebKitTools/Scripts/modules/bugzilla_unittest.py
+++ /dev/null
@@ -1,302 +0,0 @@
-# Copyright (C) 2009 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 modules.committers import CommitterList, Reviewer, Committer
-from modules.bugzilla import Bugzilla, BugzillaQueries, parse_bug_id
-from modules.outputcapture import OutputCapture
-from modules.mock import Mock
-
-from modules.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 BugzillaTest(unittest.TestCase):
- _example_attachment = '''
- <attachment
- isobsolete="1"
- ispatch="1"
- isprivate="0"
- >
- <attachid>33721</attachid>
- <date>2009-07-29 10:23 PDT</date>
- <desc>Fixed whitespace issue</desc>
- <filename>patch</filename>
- <type>text/plain</type>
- <size>9719</size>
- <attacher>christian.plesner.hansen at gmail.com</attacher>
- <flag name="review"
- id="17931"
- status="+"
- setter="one at test.com"
- />
- <flag name="commit-queue"
- id="17932"
- status="+"
- setter="two at test.com"
- />
- </attachment>
-'''
- _expected_example_attachment_parsing = {
- 'bug_id' : 100,
- 'is_obsolete' : True,
- 'is_patch' : True,
- 'id' : 33721,
- 'url' : "https://bugs.webkit.org/attachment.cgi?id=33721",
- 'name' : "Fixed whitespace issue",
- 'type' : "text/plain",
- 'review' : '+',
- 'reviewer_email' : 'one at test.com',
- 'commit-queue' : '+',
- 'committer_email' : 'two at test.com',
- 'attacher_email' : 'christian.plesner.hansen at gmail.com',
- }
-
- def test_parse_bug_id(self):
- # FIXME: These would be all better as doctests
- bugs = Bugzilla()
- self.assertEquals(12345, parse_bug_id("http://webkit.org/b/12345"))
- self.assertEquals(12345, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?id=12345"))
- self.assertEquals(12345, parse_bug_id(bugs.short_bug_url_for_bug_id(12345)))
- self.assertEquals(12345, parse_bug_id(bugs.bug_url_for_bug_id(12345)))
- self.assertEquals(12345, parse_bug_id(bugs.bug_url_for_bug_id(12345, xml=True)))
-
- # Our bug parser is super-fragile, but at least we're testing it.
- self.assertEquals(None, parse_bug_id("http://www.webkit.org/b/12345"))
- self.assertEquals(None, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?ctype=xml&id=12345"))
-
- _example_bug = """
-<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
-<!DOCTYPE bugzilla SYSTEM "https://bugs.webkit.org/bugzilla.dtd">
-<bugzilla version="3.2.3"
- urlbase="https://bugs.webkit.org/"
- maintainer="admin at webkit.org"
- exporter="eric at webkit.org"
->
- <bug>
- <bug_id>32585</bug_id>
- <creation_ts>2009-12-15 15:17 PST</creation_ts>
- <short_desc>bug to test bugzilla-tool and commit-queue failures</short_desc>
- <delta_ts>2009-12-27 21:04:50 PST</delta_ts>
- <reporter_accessible>1</reporter_accessible>
- <cclist_accessible>1</cclist_accessible>
- <classification_id>1</classification_id>
- <classification>Unclassified</classification>
- <product>WebKit</product>
- <component>Tools / Tests</component>
- <version>528+ (Nightly build)</version>
- <rep_platform>PC</rep_platform>
- <op_sys>Mac OS X 10.5</op_sys>
- <bug_status>NEW</bug_status>
- <priority>P2</priority>
- <bug_severity>Normal</bug_severity>
- <target_milestone>---</target_milestone>
- <everconfirmed>1</everconfirmed>
- <reporter name="Eric Seidel">eric at webkit.org</reporter>
- <assigned_to name="Nobody">webkit-unassigned at lists.webkit.org</assigned_to>
- <cc>foo at bar.com</cc>
- <cc>example at example.com</cc>
- <long_desc isprivate="0">
- <who name="Eric Seidel">eric at webkit.org</who>
- <bug_when>2009-12-15 15:17:28 PST</bug_when>
- <thetext>bug to test bugzilla-tool and commit-queue failures
-
-Ignore this bug. Just for testing failure modes of bugzilla-tool and the commit-queue.</thetext>
- </long_desc>
- <attachment
- isobsolete="0"
- ispatch="1"
- isprivate="0"
- >
- <attachid>45548</attachid>
- <date>2009-12-27 23:51 PST</date>
- <desc>Patch</desc>
- <filename>bug-32585-20091228005112.patch</filename>
- <type>text/plain</type>
- <size>10882</size>
- <attacher>mjs at apple.com</attacher>
-
- <token>1261988248-dc51409e9c421a4358f365fa8bec8357</token>
- <data encoding="base64">SW5kZXg6IFdlYktpdC9tYWMvQ2hhbmdlTG9nCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09
-removed-because-it-was-really-long
-ZEZpbmlzaExvYWRXaXRoUmVhc29uOnJlYXNvbl07Cit9CisKIEBlbmQKIAogI2VuZGlmCg==
-</data>
-
- <flag name="review"
- id="27602"
- status="?"
- setter="mjs at apple.com"
- />
- </attachment>
- </bug>
-</bugzilla>
-"""
- _expected_example_bug_parsing = {
- "id" : 32585,
- "title" : u"bug to test bugzilla-tool and commit-queue failures",
- "cc_emails" : ["foo at bar.com", "example at example.com"],
- "reporter_email" : "eric at webkit.org",
- "assigned_to_email" : "webkit-unassigned at lists.webkit.org",
- "attachments" : [{
- 'name': u'Patch',
- 'url': 'https://bugs.webkit.org/attachment.cgi?id=45548',
- 'is_obsolete': False,
- 'review': '?',
- 'is_patch': True,
- 'attacher_email': 'mjs at apple.com',
- 'bug_id': 32585,
- 'type': 'text/plain',
- 'id': 45548
- }],
- }
-
- def _assert_dictionaries_equal(self, actual, expected):
- # Make sure we aren't parsing more or less than we expect
- self.assertEquals(sorted(actual.keys()), sorted(expected.keys()))
-
- for key, expected_value in expected.items():
- self.assertEquals(actual[key], expected_value, ("Failure for key: %s: Actual='%s' Expected='%s'" % (key, actual[key], expected_value)))
-
- def test_bug_parsing(self):
- bug = Bugzilla()._parse_bug_page(self._example_bug)
- self._assert_dictionaries_equal(bug, self._expected_example_bug_parsing)
-
- # This could be combined into test_bug_parsing later if desired.
- def test_attachment_parsing(self):
- bugzilla = Bugzilla()
- soup = BeautifulSoup(self._example_attachment)
- attachment_element = soup.find("attachment")
- attachment = bugzilla._parse_attachment_element(attachment_element, self._expected_example_attachment_parsing['bug_id'])
- self.assertTrue(attachment)
- self._assert_dictionaries_equal(attachment, self._expected_example_attachment_parsing)
-
- _sample_attachment_detail_page = """
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
- "http://www.w3.org/TR/html4/loose.dtd">
-<html>
- <head>
- <title>
- Attachment 41073 Details for Bug 27314</title>
-<link rel="Top" href="https://bugs.webkit.org/">
- <link rel="Up" href="show_bug.cgi?id=27314">
-"""
-
- def test_attachment_detail_bug_parsing(self):
- bugzilla = Bugzilla()
- self.assertEquals(27314, bugzilla._parse_bug_id_from_attachment_page(self._sample_attachment_detail_page))
-
- def test_add_cc_to_bug(self):
- bugzilla = Bugzilla()
- bugzilla.browser = MockBrowser()
- bugzilla.authenticate = lambda: None
- expected_stderr = "Adding ['adam at example.com'] to the CC list for bug 42\n"
- OutputCapture().assert_outputs(self, bugzilla.add_cc_to_bug, [42, ["adam at example.com"]], expected_stderr=expected_stderr)
-
- def test_flag_permission_rejection_message(self):
- bugzilla = Bugzilla()
- expected_messsage="""foo at foo.com does not have review permissions according to http://trac.webkit.org/browser/trunk/WebKitTools/Scripts/modules/committers.py.
-
-- If you do not have review rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags.
-
-- If you have review rights please correct the error in WebKitTools/Scripts/modules/committers.py by adding yourself to the file (no review needed). Due to bug 30084 the commit-queue will require a restart after your change. Please contact eseidel at chromium.org to request a commit-queue restart. After restart the commit-queue will correctly respect your review rights."""
- self.assertEqual(bugzilla._flag_permission_rejection_message("foo at foo.com", "review"), expected_messsage)
-
-
-class BugzillaQueriesTest(unittest.TestCase):
- _sample_request_page = """
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
- "http://www.w3.org/TR/html4/loose.dtd">
-<html>
- <head>
- <title>Request Queue</title>
- </head>
-<body>
-
-<h3>Flag: review</h3>
- <table class="requests" cellspacing="0" cellpadding="4" border="1">
- <tr>
- <th>Requester</th>
- <th>Requestee</th>
- <th>Bug</th>
- <th>Attachment</th>
- <th>Created</th>
- </tr>
- <tr>
- <td>Shinichiro Hamaji <hamaji@chromium.org></td>
- <td></td>
- <td><a href="show_bug.cgi?id=30015">30015: text-transform:capitalize is failing in CSS2.1 test suite</a></td>
- <td><a href="attachment.cgi?id=40511&action=review">
-40511: Patch v0</a></td>
- <td>2009-10-02 04:58 PST</td>
- </tr>
- <tr>
- <td>Zan Dobersek <zandobersek@gmail.com></td>
- <td></td>
- <td><a href="show_bug.cgi?id=26304">26304: [GTK] Add controls for playing html5 video.</a></td>
- <td><a href="attachment.cgi?id=40722&action=review">
-40722: Media controls, the simple approach</a></td>
- <td>2009-10-06 09:13 PST</td>
- </tr>
- <tr>
- <td>Zan Dobersek <zandobersek@gmail.com></td>
- <td></td>
- <td><a href="show_bug.cgi?id=26304">26304: [GTK] Add controls for playing html5 video.</a></td>
- <td><a href="attachment.cgi?id=40723&action=review">
-40723: Adjust the media slider thumb size</a></td>
- <td>2009-10-06 09:15 PST</td>
- </tr>
- </table>
-</body>
-</html>
-"""
-
- def test_request_page_parsing(self):
- queries = BugzillaQueries(None)
- self.assertEquals([40511, 40722, 40723], queries._parse_attachment_ids_request_query(self._sample_request_page))
-
- def test_load_query(self):
- queries = BugzillaQueries(Mock())
- queries._load_query("request.cgi?action=queue&type=review&group=type")
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/WebKitTools/Scripts/modules/buildbot.py b/WebKitTools/Scripts/modules/buildbot.py
deleted file mode 100644
index df5d8c2..0000000
--- a/WebKitTools/Scripts/modules/buildbot.py
+++ /dev/null
@@ -1,111 +0,0 @@
-# Copyright (c) 2009, 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.
-#
-# WebKit's Python module for interacting with WebKit's buildbot
-
-import re
-import urllib2
-
-# Import WebKit-specific modules.
-from modules.webkit_logging import log
-
-# WebKit includes a built copy of BeautifulSoup in Scripts/modules
-# so this import should always succeed.
-from .BeautifulSoup import BeautifulSoup
-
-class BuildBot:
- default_host = "build.webkit.org"
- def __init__(self, host=default_host):
- self.buildbot_host = host
- self.buildbot_server_url = "http://%s/" % self.buildbot_host
-
- # If any of the Leopard build/test bots or the Windows builders are red we should not be landing patches.
- # Other builders should be added to this list once they're known to be stable.
- self.core_builder_names_regexps = [ 'Leopard', "Windows.*Build" ]
-
- # If WebKit's buildbot has an XMLRPC interface we could use, we could do something more sophisticated here.
- # For now we just parse out the basics, enough to support basic questions like "is the tree green?"
- def _parse_builder_status_from_row(self, status_row):
- status_cells = status_row.findAll('td')
- builder = {}
-
- name_link = status_cells[0].find('a')
- builder['name'] = name_link.string
- # We could generate the builder_url from the name in a future version of this code.
- builder['builder_url'] = self.buildbot_server_url + name_link['href']
-
- status_link = status_cells[1].find('a')
- if not status_link:
- # We failed to find a link in the first cell, just give up.
- # This can happen if a builder is just-added, the first cell will just be "no build"
- builder['is_green'] = False # Other parts of the code depend on is_green being present.
- return builder
- revision_string = status_link.string # Will be either a revision number or a build number
- # If revision_string has non-digits assume it's not a revision number.
- builder['built_revision'] = int(revision_string) if not re.match('\D', revision_string) else None
- builder['is_green'] = not re.search('fail', status_cells[1].renderContents())
- # We could parse out the build number instead, but for now just store the URL.
- builder['build_url'] = self.buildbot_server_url + status_link['href']
-
- # We could parse out the current activity too.
-
- return builder
-
- def _builder_statuses_with_names_matching_regexps(self, builder_statuses, name_regexps):
- builders = []
- for builder in builder_statuses:
- for name_regexp in name_regexps:
- if re.match(name_regexp, builder['name']):
- builders.append(builder)
- return builders
-
- def red_core_builders(self):
- red_builders = []
- for builder in self._builder_statuses_with_names_matching_regexps(self.builder_statuses(), self.core_builder_names_regexps):
- if not builder['is_green']:
- red_builders.append(builder)
- return red_builders
-
- def red_core_builders_names(self):
- red_builders = self.red_core_builders()
- return map(lambda builder: builder['name'], red_builders)
-
- def core_builders_are_green(self):
- return not self.red_core_builders()
-
- def builder_statuses(self):
- build_status_url = self.buildbot_server_url + 'one_box_per_builder'
- page = urllib2.urlopen(build_status_url)
- soup = BeautifulSoup(page)
-
- builders = []
- status_table = soup.find('table')
- for status_row in status_table.findAll('tr'):
- builder = self._parse_builder_status_from_row(status_row)
- builders.append(builder)
- return builders
diff --git a/WebKitTools/Scripts/modules/buildbot_unittest.py b/WebKitTools/Scripts/modules/buildbot_unittest.py
deleted file mode 100644
index a85f2ea..0000000
--- a/WebKitTools/Scripts/modules/buildbot_unittest.py
+++ /dev/null
@@ -1,134 +0,0 @@
-# Copyright (C) 2009 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 modules.buildbot import BuildBot
-
-from modules.BeautifulSoup import BeautifulSoup
-
-class BuildBotTest(unittest.TestCase):
-
- _example_one_box_status = '''
- <table>
- <tr>
- <td class="box"><a href="builders/Windows%20Debug%20%28Tests%29">Windows Debug (Tests)</a></td>
- <td align="center" class="LastBuild box success"><a href="builders/Windows%20Debug%20%28Tests%29/builds/3693">47380</a><br />build<br />successful</td>
- <td align="center" class="Activity building">building<br />ETA in<br />~ 14 mins<br />at 13:40</td>
- <tr>
- <td class="box"><a href="builders/SnowLeopard%20Intel%20Release">SnowLeopard Intel Release</a></td>
- <td class="LastBuild box" >no build</td>
- <td align="center" class="Activity building">building<br />< 1 min</td>
- <tr>
- <td class="box"><a href="builders/Qt%20Linux%20Release">Qt Linux Release</a></td>
- <td align="center" class="LastBuild box failure"><a href="builders/Qt%20Linux%20Release/builds/654">47383</a><br />failed<br />compile-webkit</td>
- <td align="center" class="Activity idle">idle</td>
- </table>
-'''
- _expected_example_one_box_parsings = [
- {
- 'builder_url': u'http://build.webkit.org/builders/Windows%20Debug%20%28Tests%29',
- 'build_url': u'http://build.webkit.org/builders/Windows%20Debug%20%28Tests%29/builds/3693',
- 'is_green': True,
- 'name': u'Windows Debug (Tests)',
- 'built_revision': 47380
- },
- {
- 'builder_url': u'http://build.webkit.org/builders/SnowLeopard%20Intel%20Release',
- 'is_green': False,
- 'name': u'SnowLeopard Intel Release',
- },
- {
- 'builder_url': u'http://build.webkit.org/builders/Qt%20Linux%20Release',
- 'build_url': u'http://build.webkit.org/builders/Qt%20Linux%20Release/builds/654',
- 'is_green': False,
- 'name': u'Qt Linux Release',
- 'built_revision': 47383
- },
- ]
-
- def test_status_parsing(self):
- buildbot = BuildBot()
-
- soup = BeautifulSoup(self._example_one_box_status)
- status_table = soup.find("table")
- input_rows = status_table.findAll('tr')
-
- for x in range(len(input_rows)):
- status_row = input_rows[x]
- expected_parsing = self._expected_example_one_box_parsings[x]
-
- builder = buildbot._parse_builder_status_from_row(status_row)
-
- # Make sure we aren't parsing more or less than we expect
- self.assertEquals(builder.keys(), expected_parsing.keys())
-
- for key, expected_value in expected_parsing.items():
- self.assertEquals(builder[key], expected_value, ("Builder %d parse failure for key: %s: Actual='%s' Expected='%s'" % (x, key, builder[key], expected_value)))
-
- def test_core_builder_methods(self):
- buildbot = BuildBot()
-
- # Override builder_statuses function to not touch the network.
- def example_builder_statuses(): # We could use instancemethod() to bind 'self' but we don't need to.
- return BuildBotTest._expected_example_one_box_parsings
- buildbot.builder_statuses = example_builder_statuses
-
- buildbot.core_builder_names_regexps = [ 'Leopard', "Windows.*Build" ]
- self.assertEquals(buildbot.red_core_builders_names(), [])
- self.assertTrue(buildbot.core_builders_are_green())
-
- buildbot.core_builder_names_regexps = [ 'SnowLeopard', 'Qt' ]
- self.assertEquals(buildbot.red_core_builders_names(), [ u'SnowLeopard Intel Release', u'Qt Linux Release' ])
- self.assertFalse(buildbot.core_builders_are_green())
-
- def test_builder_name_regexps(self):
- buildbot = BuildBot()
-
- example_builders = [
- { 'name': u'Leopard Debug (Build)', },
- { 'name': u'Leopard Debug (Tests)', },
- { 'name': u'Windows Release (Build)', },
- { 'name': u'Windows Debug (Tests)', },
- { 'name': u'Qt Linux Release', },
- ]
- name_regexps = [ 'Leopard', "Windows.*Build" ]
- expected_builders = [
- { 'name': u'Leopard Debug (Build)', },
- { 'name': u'Leopard Debug (Tests)', },
- { 'name': u'Windows Release (Build)', },
- ]
-
- # This test should probably be updated if the default regexp list changes
- self.assertEquals(buildbot.core_builder_names_regexps, name_regexps)
-
- builders = buildbot._builder_statuses_with_names_matching_regexps(example_builders, name_regexps)
- self.assertEquals(builders, expected_builders)
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/WebKitTools/Scripts/modules/buildsteps.py b/WebKitTools/Scripts/modules/buildsteps.py
deleted file mode 100644
index 1b12e32..0000000
--- a/WebKitTools/Scripts/modules/buildsteps.py
+++ /dev/null
@@ -1,541 +0,0 @@
-# Copyright (C) 2009 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 os
-import StringIO
-
-from optparse import make_option
-
-from modules.comments import bug_comment_from_commit_text
-from modules.grammar import pluralize
-from modules.webkit_logging import log, error
-from modules.webkitport import WebKitPort
-from modules.changelogs import ChangeLog
-
-# FIXME: Why do some of these have "Step" in their name but not all?
-__all__ = [
- "ApplyPatchStep",
- "ApplyPatchWithLocalCommitStep",
- "BuildStep",
- "CheckStyleStep",
- "CleanWorkingDirectoryStep",
- "CleanWorkingDirectoryWithLocalCommitsStep",
- "CloseBugForLandDiffStep",
- "CloseBugStep",
- "ClosePatchStep",
- "CommitStep",
- "CompleteRollout",
- "CreateBugStep",
- "EnsureBuildersAreGreenStep",
- "EnsureLocalCommitIfNeeded",
- "ObsoletePatchesOnBugStep",
- "PostDiffToBugStep",
- "PrepareChangeLogForRevertStep",
- "PrepareChangeLogStep",
- "PromptForBugOrTitleStep",
- "RevertRevisionStep",
- "RunTestsStep",
- "UpdateChangeLogsWithReviewerStep",
- "UpdateStep",
-]
-
-class CommandOptions(object):
- force_clean = make_option("--force-clean", action="store_true", dest="force_clean", default=False, help="Clean working directory before applying patches (removes local changes and commits)")
- clean = make_option("--no-clean", action="store_false", dest="clean", default=True, help="Don't check if the working directory is clean before applying patches")
- check_builders = make_option("--ignore-builders", action="store_false", dest="check_builders", default=True, help="Don't check to see if the build.webkit.org builders are green before landing.")
- quiet = make_option("--quiet", action="store_true", dest="quiet", default=False, help="Produce less console output.")
- non_interactive = make_option("--non-interactive", action="store_true", dest="non_interactive", default=False, help="Never prompt the user, fail as fast as possible.")
- parent_command = make_option("--parent-command", action="store", dest="parent_command", default=None, help="(Internal) The command that spawned this instance.")
- update = make_option("--no-update", action="store_false", dest="update", default=True, help="Don't update the working directory.")
- local_commit = make_option("--local-commit", action="store_true", dest="local_commit", default=False, help="Make a local commit for each applied patch")
- build = make_option("--no-build", action="store_false", dest="build", default=True, help="Commit without building first, implies --no-test.")
- build_style = make_option("--build-style", action="store", dest="build_style", default=None, help="Whether to build debug, release, or both.")
- test = make_option("--no-test", action="store_false", dest="test", default=True, help="Commit without running run-webkit-tests.")
- close_bug = make_option("--no-close", action="store_false", dest="close_bug", default=True, help="Leave bug open after landing.")
- port = make_option("--port", action="store", dest="port", default=None, help="Specify a port (e.g., mac, qt, gtk, ...).")
- reviewer = make_option("-r", "--reviewer", action="store", type="string", dest="reviewer", help="Update ChangeLogs to say Reviewed by REVIEWER.")
- complete_rollout = make_option("--complete-rollout", action="store_true", dest="complete_rollout", help="Commit the revert and re-open the original bug.")
- obsolete_patches = make_option("--no-obsolete", action="store_false", dest="obsolete_patches", default=True, help="Do not obsolete old patches before posting this one.")
- review = make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review.")
- request_commit = make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review.")
- description = make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: \"patch\")")
- cc = make_option("--cc", action="store", type="string", dest="cc", help="Comma-separated list of email addresses to carbon-copy.")
- component = make_option("--component", action="store", type="string", dest="component", help="Component for the new bug.")
- confirm = make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Skip confirmation steps.")
-
-
-class AbstractStep(object):
- def __init__(self, tool, options):
- self._tool = tool
- self._options = options
- self._port = None
-
- def _run_script(self, script_name, quiet=False, port=WebKitPort):
- log("Running %s" % script_name)
- # FIXME: This should use self.port()
- self._tool.executive.run_and_throw_if_fail(port.script_path(script_name), quiet)
-
- # FIXME: The port should live on the tool.
- def port(self):
- if self._port:
- return self._port
- self._port = WebKitPort.port(self._options.port)
- return self._port
-
- @classmethod
- def options(cls):
- return []
-
- def run(self, state):
- raise NotImplementedError, "subclasses must implement"
-
-
-# FIXME: Unify with StepSequence? I'm not sure yet which is the better design.
-class MetaStep(AbstractStep):
- substeps = [] # Override in subclasses
- def __init__(self, tool, options):
- AbstractStep.__init__(self, tool, options)
- self._step_instances = []
- for step_class in self.substeps:
- self._step_instances.append(step_class(tool, options))
-
- @staticmethod
- def _collect_options_from_steps(steps):
- collected_options = []
- for step in steps:
- collected_options = collected_options + step.options()
- return collected_options
-
- @classmethod
- def options(cls):
- return cls._collect_options_from_steps(cls.substeps)
-
- def run(self, state):
- for step in self._step_instances:
- step.run(state)
-
-
-class PromptForBugOrTitleStep(AbstractStep):
- def run(self, state):
- # No need to prompt if we alrady have the bug_id.
- if state.get("bug_id"):
- return
- user_response = self._tool.user.prompt("Please enter a bug number or a title for a new bug:\n")
- # If the user responds with a number, we assume it's bug number.
- # Otherwise we assume it's a bug subject.
- try:
- state["bug_id"] = int(user_response)
- except ValueError, TypeError:
- state["bug_title"] = user_response
- # FIXME: This is kind of a lame description.
- state["bug_description"] = user_response
-
-
-class CreateBugStep(AbstractStep):
- @classmethod
- def options(cls):
- return [
- CommandOptions.cc,
- CommandOptions.component,
- ]
-
- def run(self, state):
- # No need to create a bug if we already have one.
- if state.get("bug_id"):
- return
- state["bug_id"] = self._tool.bugs.create_bug(state["bug_title"], state["bug_description"], component=self._options.component, cc=self._options.cc)
-
-
-class PrepareChangeLogStep(AbstractStep):
- @classmethod
- def options(cls):
- return [
- CommandOptions.port,
- CommandOptions.quiet,
- ]
-
- def run(self, state):
- os.chdir(self._tool.scm().checkout_root)
- args = [self.port().script_path("prepare-ChangeLog")]
- if state["bug_id"]:
- args.append("--bug=%s" % state["bug_id"])
- self._tool.executive.run_and_throw_if_fail(args, self._options.quiet)
-
-
-class EditChangeLogStep(AbstractStep):
- def run(self, state):
- self._tool.user.edit(self._tool.scm().modified_changelogs())
-
-
-class ObsoletePatchesOnBugStep(AbstractStep):
- @classmethod
- def options(cls):
- return [
- CommandOptions.obsolete_patches,
- ]
-
- def run(self, state):
- if not self._options.obsolete_patches:
- return
- bug_id = state["bug_id"]
- patches = self._tool.bugs.fetch_patches_from_bug(bug_id)
- if not patches:
- return
- log("Obsoleting %s on bug %s" % (pluralize("old patch", len(patches)), bug_id))
- for patch in patches:
- self._tool.bugs.obsolete_attachment(patch["id"])
-
-
-class AbstractDiffStep(AbstractStep):
- def diff(self, state):
- diff = state.get("diff")
- if not diff:
- diff = self._tool.scm().create_patch()
- state["diff"] = diff
- return diff
-
-
-class ConfirmDiffStep(AbstractDiffStep):
- @classmethod
- def options(cls):
- return [
- CommandOptions.confirm,
- ]
-
- def run(self, state):
- if not self._options.confirm:
- return
- diff = self.diff(state)
- self._tool.user.page(diff)
- if not self._tool.user.confirm():
- error("User declined to continue.")
-
-
-class PostDiffToBugStep(AbstractDiffStep):
- @classmethod
- def options(cls):
- return [
- CommandOptions.description,
- CommandOptions.review,
- CommandOptions.request_commit,
- ]
-
- def run(self, state):
- diff = self.diff(state)
- diff_file = StringIO.StringIO(diff) # add_patch_to_bug expects a file-like object
- description = self._options.description or "Patch"
- self._tool.bugs.add_patch_to_bug(state["bug_id"], diff_file, description, mark_for_review=self._options.review, mark_for_commit_queue=self._options.request_commit)
-
-
-class PrepareChangeLogForRevertStep(AbstractStep):
- def run(self, state):
- # First, discard the ChangeLog changes from the rollout.
- os.chdir(self._tool.scm().checkout_root)
- changelog_paths = self._tool.scm().modified_changelogs()
- self._tool.scm().revert_files(changelog_paths)
-
- # Second, make new ChangeLog entries for this rollout.
- # This could move to prepare-ChangeLog by adding a --revert= option.
- self._run_script("prepare-ChangeLog")
- for changelog_path in changelog_paths:
- ChangeLog(changelog_path).update_for_revert(state["revision"])
-
-
-class CleanWorkingDirectoryStep(AbstractStep):
- def __init__(self, tool, options, allow_local_commits=False):
- AbstractStep.__init__(self, tool, options)
- self._allow_local_commits = allow_local_commits
-
- @classmethod
- def options(cls):
- return [
- CommandOptions.force_clean,
- CommandOptions.clean,
- ]
-
- def run(self, state):
- os.chdir(self._tool.scm().checkout_root)
- if not self._allow_local_commits:
- self._tool.scm().ensure_no_local_commits(self._options.force_clean)
- if self._options.clean:
- self._tool.scm().ensure_clean_working_directory(force_clean=self._options.force_clean)
-
-
-class CleanWorkingDirectoryWithLocalCommitsStep(CleanWorkingDirectoryStep):
- def __init__(self, tool, options):
- # FIXME: This a bit of a hack. Consider doing this more cleanly.
- CleanWorkingDirectoryStep.__init__(self, tool, options, allow_local_commits=True)
-
-
-class UpdateStep(AbstractStep):
- @classmethod
- def options(cls):
- return [
- CommandOptions.update,
- CommandOptions.port,
- ]
-
- def run(self, state):
- if not self._options.update:
- return
- log("Updating working directory")
- self._tool.executive.run_and_throw_if_fail(self.port().update_webkit_command(), quiet=True)
-
-
-class ApplyPatchStep(AbstractStep):
- @classmethod
- def options(cls):
- return [
- CommandOptions.non_interactive,
- ]
-
- def run(self, state):
- log("Processing patch %s from bug %s." % (state["patch"]["id"], state["patch"]["bug_id"]))
- self._tool.scm().apply_patch(state["patch"], force=self._options.non_interactive)
-
-
-class RevertRevisionStep(AbstractStep):
- def run(self, state):
- self._tool.scm().apply_reverse_diff(state["revision"])
-
-
-class ApplyPatchWithLocalCommitStep(ApplyPatchStep):
- @classmethod
- def options(cls):
- return [
- CommandOptions.local_commit,
- ] + ApplyPatchStep.options()
-
- def run(self, state):
- ApplyPatchStep.run(self, state)
- if self._options.local_commit:
- commit_message = self._tool.scm().commit_message_for_this_commit()
- self._tool.scm().commit_locally_with_message(commit_message.message() or state["patch"]["name"])
-
-
-class EnsureBuildersAreGreenStep(AbstractStep):
- @classmethod
- def options(cls):
- return [
- CommandOptions.check_builders,
- ]
-
- def run(self, state):
- if not self._options.check_builders:
- return
- red_builders_names = self._tool.buildbot.red_core_builders_names()
- if not red_builders_names:
- return
- red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names.
- error("Builders [%s] are red, please do not commit.\nSee http://%s.\nPass --ignore-builders to bypass this check." % (", ".join(red_builders_names), self._tool.buildbot.buildbot_host))
-
-
-class EnsureLocalCommitIfNeeded(AbstractStep):
- @classmethod
- def options(cls):
- return [
- CommandOptions.local_commit,
- ]
-
- def run(self, state):
- if self._options.local_commit and not self._tool.scm().supports_local_commits():
- error("--local-commit passed, but %s does not support local commits" % self._tool.scm.display_name())
-
-
-class UpdateChangeLogsWithReviewerStep(AbstractStep):
- @classmethod
- def options(cls):
- return [
- CommandOptions.reviewer,
- ]
-
- def _guess_reviewer_from_bug(self, bug_id):
- patches = self._tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
- if len(patches) != 1:
- log("%s on bug %s, cannot infer reviewer." % (pluralize("reviewed patch", len(patches)), bug_id))
- return None
- patch = patches[0]
- reviewer = patch["reviewer"]
- log("Guessing \"%s\" as reviewer from attachment %s on bug %s." % (reviewer, patch["id"], bug_id))
- return reviewer
-
- def run(self, state):
- bug_id = state["patch"]["bug_id"]
- reviewer = self._options.reviewer
- if not reviewer:
- if not bug_id:
- log("No bug id provided and --reviewer= not provided. Not updating ChangeLogs with reviewer.")
- return
- reviewer = self._guess_reviewer_from_bug(bug_id)
-
- if not reviewer:
- log("Failed to guess reviewer from bug %s and --reviewer= not provided. Not updating ChangeLogs with reviewer." % bug_id)
- return
-
- os.chdir(self._tool.scm().checkout_root)
- for changelog_path in self._tool.scm().modified_changelogs():
- ChangeLog(changelog_path).set_reviewer(reviewer)
-
-
-class BuildStep(AbstractStep):
- @classmethod
- def options(cls):
- return [
- CommandOptions.build,
- CommandOptions.quiet,
- CommandOptions.build_style,
- ]
-
- def build(self, build_style):
- self._tool.executive.run_and_throw_if_fail(self.port().build_webkit_command(build_style=build_style), self._options.quiet)
-
- def run(self, state):
- if not self._options.build:
- return
- log("Building WebKit")
- if self._options.build_style == "both":
- self.build("debug")
- self.build("release")
- else:
- self.build(self._options.build_style)
-
-
-class CheckStyleStep(AbstractStep):
- def run(self, state):
- self._run_script("check-webkit-style")
-
-
-class RunTestsStep(AbstractStep):
- @classmethod
- def options(cls):
- return [
- CommandOptions.build,
- CommandOptions.test,
- CommandOptions.non_interactive,
- CommandOptions.quiet,
- CommandOptions.port,
- ]
-
- def run(self, state):
- if not self._options.build:
- return
- if not self._options.test:
- return
- args = self.port().run_webkit_tests_command()
- if self._options.non_interactive:
- args.append("--no-launch-safari")
- args.append("--exit-after-n-failures=1")
- if self._options.quiet:
- args.append("--quiet")
- self._tool.executive.run_and_throw_if_fail(args)
-
-
-class CommitStep(AbstractStep):
- def run(self, state):
- commit_message = self._tool.scm().commit_message_for_this_commit()
- state["commit_text"] = self._tool.scm().commit_with_message(commit_message.message())
-
-
-class ClosePatchStep(AbstractStep):
- def run(self, state):
- comment_text = bug_comment_from_commit_text(self._tool.scm(), state["commit_text"])
- self._tool.bugs.clear_attachment_flags(state["patch"]["id"], comment_text)
-
-
-class CloseBugStep(AbstractStep):
- @classmethod
- def options(cls):
- return [
- CommandOptions.close_bug,
- ]
-
- def run(self, state):
- if not self._options.close_bug:
- return
- # Check to make sure there are no r? or r+ patches on the bug before closing.
- # Assume that r- patches are just previous patches someone forgot to obsolete.
- patches = self._tool.bugs.fetch_patches_from_bug(state["patch"]["bug_id"])
- for patch in patches:
- review_flag = patch.get("review")
- if review_flag == "?" or review_flag == "+":
- log("Not closing bug %s as attachment %s has review=%s. Assuming there are more patches to land from this bug." % (patch["bug_id"], patch["id"], review_flag))
- return
- self._tool.bugs.close_bug_as_fixed(state["patch"]["bug_id"], "All reviewed patches have been landed. Closing bug.")
-
-
-class CloseBugForLandDiffStep(AbstractStep):
- @classmethod
- def options(cls):
- return [
- CommandOptions.close_bug,
- ]
-
- def run(self, state):
- comment_text = bug_comment_from_commit_text(self._tool.scm(), state["commit_text"])
- bug_id = state["patch"]["bug_id"]
- if bug_id:
- log("Updating bug %s" % bug_id)
- if self._options.close_bug:
- self._tool.bugs.close_bug_as_fixed(bug_id, comment_text)
- else:
- # FIXME: We should a smart way to figure out if the patch is attached
- # to the bug, and if so obsolete it.
- self._tool.bugs.post_comment_to_bug(bug_id, comment_text)
- else:
- log(comment_text)
- log("No bug id provided.")
-
-
-class CompleteRollout(MetaStep):
- substeps = [
- BuildStep,
- CommitStep,
- ]
-
- @classmethod
- def options(cls):
- collected_options = cls._collect_options_from_steps(cls.substeps)
- collected_options.append(CommandOptions.complete_rollout)
- return collected_options
-
- def run(self, state):
- bug_id = state["bug_id"]
- # FIXME: Fully automated rollout is not 100% idiot-proof yet, so for now just log with instructions on how to complete the rollout.
- # Once we trust rollout we will remove this option.
- if not self._options.complete_rollout:
- log("\nNOTE: Rollout support is experimental.\nPlease verify the rollout diff and use \"bugzilla-tool land-diff %s\" to commit the rollout." % bug_id)
- return
-
- MetaStep.run(self, state)
-
- if not bug_id:
- log(state["commit_text"])
- log("No bugs were updated or re-opened to reflect this rollout.")
- return
- # FIXME: I'm not sure state["commit_text"] is quite right here.
- self._tool.bugs.reopen_bug(bug_id, state["commit_text"])
diff --git a/WebKitTools/Scripts/modules/buildsteps_unittest.py b/WebKitTools/Scripts/modules/buildsteps_unittest.py
deleted file mode 100644
index c603491..0000000
--- a/WebKitTools/Scripts/modules/buildsteps_unittest.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# Copyright (C) 2009 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 modules.buildsteps import UpdateChangeLogsWithReviewerStep, UpdateStep, PromptForBugOrTitleStep
-from modules.mock_bugzillatool import MockBugzillaTool
-from modules.outputcapture import OutputCapture
-from modules.mock import Mock
-
-
-class UpdateChangeLogsWithReviewerStepTest(unittest.TestCase):
- def test_guess_reviewer_from_bug(self):
- capture = OutputCapture()
- step = UpdateChangeLogsWithReviewerStep(MockBugzillaTool(), [])
- expected_stderr = "0 reviewed patches on bug 75, cannot infer reviewer.\n"
- capture.assert_outputs(self, step._guess_reviewer_from_bug, [75], expected_stderr=expected_stderr)
-
-
-class StepsTest(unittest.TestCase):
- def _run_step(self, step, tool=None, options=None, state=None):
- if not tool:
- tool = MockBugzillaTool()
- if not options:
- options = Mock()
- if not state:
- state = {}
- step(tool, options).run(state)
-
- def test_update_step(self):
- options = Mock()
- options.update = True
- self._run_step(UpdateStep, options)
-
- def test_prompt_for_bug_or_title_step(self):
- tool = MockBugzillaTool()
- tool.user.prompt = lambda message: 42
- self._run_step(PromptForBugOrTitleStep, tool=tool)
diff --git a/WebKitTools/Scripts/modules/commands/commandtest.py b/WebKitTools/Scripts/modules/commands/commandtest.py
deleted file mode 100644
index e6ba4f7..0000000
--- a/WebKitTools/Scripts/modules/commands/commandtest.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Copyright (C) 2009 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 modules.mock import Mock
-from modules.mock_bugzillatool import MockBugzillaTool
-from modules.outputcapture import OutputCapture
-
-class CommandsTest(unittest.TestCase):
- def assert_execute_outputs(self, command, args, expected_stdout="", expected_stderr="", options=Mock(), tool=MockBugzillaTool()):
- command.bind_to_tool(tool)
- OutputCapture().assert_outputs(self, command.execute, [options, args, tool], expected_stdout=expected_stdout, expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/modules/commands/download.py b/WebKitTools/Scripts/modules/commands/download.py
deleted file mode 100644
index 7ea956c..0000000
--- a/WebKitTools/Scripts/modules/commands/download.py
+++ /dev/null
@@ -1,287 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple 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 os
-
-from optparse import make_option
-
-from modules.bugzilla import parse_bug_id
-# We could instead use from modules import buildsteps and then prefix every buildstep with "buildsteps."
-from modules.buildsteps import *
-from modules.changelogs import ChangeLog
-from modules.comments import bug_comment_from_commit_text
-from modules.executive import ScriptError
-from modules.grammar import pluralize
-from modules.webkit_logging import error, log
-from modules.multicommandtool import AbstractDeclarativeCommmand, Command
-from modules.stepsequence import StepSequence
-
-
-# FIXME: Move this to a more general location.
-class AbstractSequencedCommmand(AbstractDeclarativeCommmand):
- steps = None
- def __init__(self):
- self._sequence = StepSequence(self.steps)
- AbstractDeclarativeCommmand.__init__(self, self._sequence.options())
-
- def _prepare_state(self, options, args, tool):
- return None
-
- def execute(self, options, args, tool):
- self._sequence.run_and_handle_errors(tool, options, self._prepare_state(options, args, tool))
-
-
-class Build(AbstractSequencedCommmand):
- name = "build"
- help_text = "Update working copy and build"
- steps = [
- CleanWorkingDirectoryStep,
- UpdateStep,
- BuildStep,
- ]
-
-
-class BuildAndTest(AbstractSequencedCommmand):
- name = "build-and-test"
- help_text = "Update working copy, build, and run the tests"
- steps = [
- CleanWorkingDirectoryStep,
- UpdateStep,
- BuildStep,
- RunTestsStep,
- ]
-
-
-class LandDiff(AbstractSequencedCommmand):
- name = "land-diff"
- help_text = "Land the current working directory diff and updates the associated bug if any"
- argument_names = "[BUGID]"
- show_in_main_help = True
- steps = [
- EnsureBuildersAreGreenStep,
- UpdateChangeLogsWithReviewerStep,
- EnsureBuildersAreGreenStep,
- BuildStep,
- RunTestsStep,
- CommitStep,
- CloseBugForLandDiffStep,
- ]
-
- def _prepare_state(self, options, args, tool):
- return {
- "patch" : {
- "id" : None,
- "bug_id" : (args and args[0]) or parse_bug_id(tool.scm().create_patch()),
- }
- }
-
-
-class AbstractPatchProcessingCommand(AbstractDeclarativeCommmand):
- # Subclasses must implement the methods below. We don't declare them here
- # because we want to be able to implement them with mix-ins.
- #
- # def _fetch_list_of_patches_to_process(self, options, args, tool):
- # def _prepare_to_process(self, options, args, tool):
-
- @staticmethod
- def _collect_patches_by_bug(patches):
- bugs_to_patches = {}
- for patch in patches:
- bug_id = patch["bug_id"]
- bugs_to_patches[bug_id] = bugs_to_patches.get(bug_id, []) + [patch]
- return bugs_to_patches
-
- def execute(self, options, args, tool):
- self._prepare_to_process(options, args, tool)
- patches = self._fetch_list_of_patches_to_process(options, args, tool)
-
- # It's nice to print out total statistics.
- bugs_to_patches = self._collect_patches_by_bug(patches)
- log("Processing %s from %s." % (pluralize("patch", len(patches)), pluralize("bug", len(bugs_to_patches))))
-
- for patch in patches:
- self._process_patch(patch, options, args, tool)
-
-
-class AbstractPatchSequencingCommand(AbstractPatchProcessingCommand):
- prepare_steps = None
- main_steps = None
-
- def __init__(self):
- options = []
- self._prepare_sequence = StepSequence(self.prepare_steps)
- self._main_sequence = StepSequence(self.main_steps)
- options = sorted(set(self._prepare_sequence.options() + self._main_sequence.options()))
- AbstractPatchProcessingCommand.__init__(self, options)
-
- def _prepare_to_process(self, options, args, tool):
- self._prepare_sequence.run_and_handle_errors(tool, options)
-
- def _process_patch(self, patch, options, args, tool):
- state = { "patch" : patch }
- self._main_sequence.run_and_handle_errors(tool, options, state)
-
-
-class ProcessAttachmentsMixin(object):
- def _fetch_list_of_patches_to_process(self, options, args, tool):
- return map(lambda patch_id: tool.bugs.fetch_attachment(patch_id), args)
-
-
-class ProcessBugsMixin(object):
- def _fetch_list_of_patches_to_process(self, options, args, tool):
- all_patches = []
- for bug_id in args:
- patches = tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
- log("%s found on bug %s." % (pluralize("reviewed patch", len(patches)), bug_id))
- all_patches += patches
- return all_patches
-
-
-class CheckStyle(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
- name = "check-style"
- help_text = "Run check-webkit-style on the specified attachments"
- argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
- main_steps = [
- CleanWorkingDirectoryStep,
- UpdateStep,
- ApplyPatchStep,
- CheckStyleStep,
- ]
-
-
-class BuildAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
- name = "build-attachment"
- help_text = "Apply and build patches from bugzilla"
- argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
- main_steps = [
- CleanWorkingDirectoryStep,
- UpdateStep,
- ApplyPatchStep,
- BuildStep,
- ]
-
-
-class AbstractPatchApplyingCommand(AbstractPatchSequencingCommand):
- prepare_steps = [
- EnsureLocalCommitIfNeeded,
- CleanWorkingDirectoryWithLocalCommitsStep,
- UpdateStep,
- ]
- main_steps = [
- ApplyPatchWithLocalCommitStep,
- ]
-
-
-class ApplyAttachment(AbstractPatchApplyingCommand, ProcessAttachmentsMixin):
- name = "apply-attachment"
- help_text = "Apply an attachment to the local working directory"
- argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
- show_in_main_help = True
-
-
-class ApplyPatches(AbstractPatchApplyingCommand, ProcessBugsMixin):
- name = "apply-patches"
- help_text = "Apply reviewed patches from provided bugs to the local working directory"
- argument_names = "BUGID [BUGIDS]"
- show_in_main_help = True
-
-
-class AbstractPatchLandingCommand(AbstractPatchSequencingCommand):
- prepare_steps = [
- EnsureBuildersAreGreenStep,
- ]
- main_steps = [
- CleanWorkingDirectoryStep,
- UpdateStep,
- ApplyPatchStep,
- EnsureBuildersAreGreenStep,
- BuildStep,
- RunTestsStep,
- CommitStep,
- ClosePatchStep,
- CloseBugStep,
- ]
-
-
-class LandAttachment(AbstractPatchLandingCommand, ProcessAttachmentsMixin):
- name = "land-attachment"
- help_text = "Land patches from bugzilla, optionally building and testing them first"
- argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
- show_in_main_help = True
-
-
-class LandPatches(AbstractPatchLandingCommand, ProcessBugsMixin):
- name = "land-patches"
- help_text = "Land all patches on the given bugs, optionally building and testing them first"
- argument_names = "BUGID [BUGIDS]"
- show_in_main_help = True
-
-
-# FIXME: Make Rollout more declarative.
-class Rollout(Command):
- name = "rollout"
- show_in_main_help = True
- def __init__(self):
- self._sequence = StepSequence([
- CleanWorkingDirectoryStep,
- UpdateStep,
- RevertRevisionStep,
- PrepareChangeLogForRevertStep,
- CompleteRollout,
- ])
- Command.__init__(self, "Revert the given revision in the working copy and optionally commit the revert and re-open the original bug", "REVISION [BUGID]", options=self._sequence.options())
-
- @staticmethod
- def _parse_bug_id_from_revision_diff(tool, revision):
- original_diff = tool.scm().diff_for_revision(revision)
- return parse_bug_id(original_diff)
-
- @staticmethod
- def _reopen_bug_after_rollout(tool, bug_id, comment_text):
- if bug_id:
- tool.bugs.reopen_bug(bug_id, comment_text)
- else:
- log(comment_text)
- log("No bugs were updated or re-opened to reflect this rollout.")
-
- def execute(self, options, args, tool):
- revision = args[0]
- bug_id = self._parse_bug_id_from_revision_diff(tool, revision)
- if options.complete_rollout:
- if bug_id:
- log("Will re-open bug %s after rollout." % bug_id)
- else:
- log("Failed to parse bug number from diff. No bugs will be updated/reopened after the rollout.")
-
- state = {
- "revision": revision,
- "bug_id": bug_id,
- }
- self._sequence.run_and_handle_errors(tool, options, state)
diff --git a/WebKitTools/Scripts/modules/commands/download_unittest.py b/WebKitTools/Scripts/modules/commands/download_unittest.py
deleted file mode 100644
index c2b4f86..0000000
--- a/WebKitTools/Scripts/modules/commands/download_unittest.py
+++ /dev/null
@@ -1,100 +0,0 @@
-# Copyright (C) 2009 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 modules.commands.commandtest import CommandsTest
-from modules.commands.download import *
-from modules.mock import Mock
-
-class DownloadCommandsTest(CommandsTest):
- def _default_options(self):
- options = Mock()
- options.force_clean = False
- options.clean = True
- options.check_builders = True
- options.quiet = False
- options.non_interactive = False
- options.update = True
- options.build = True
- options.test = True
- options.close_bug = True
- options.complete_rollout = False
- return options
-
- def test_build(self):
- expected_stderr = "Updating working directory\nBuilding WebKit\n"
- self.assert_execute_outputs(Build(), [], options=self._default_options(), expected_stderr=expected_stderr)
-
- def test_build_and_test(self):
- expected_stderr = "Updating working directory\nBuilding WebKit\n"
- self.assert_execute_outputs(BuildAndTest(), [], options=self._default_options(), expected_stderr=expected_stderr)
-
- def test_apply_attachment(self):
- options = self._default_options()
- options.update = True
- options.local_commit = True
- expected_stderr = "Updating working directory\nProcessing 1 patch from 1 bug.\nProcessing patch 197 from bug 42.\n"
- self.assert_execute_outputs(ApplyAttachment(), [197], options=options, expected_stderr=expected_stderr)
-
- def test_apply_patches(self):
- options = self._default_options()
- options.update = True
- options.local_commit = True
- expected_stderr = "Updating working directory\n2 reviewed patches found on bug 42.\nProcessing 2 patches from 1 bug.\nProcessing patch 197 from bug 42.\nProcessing patch 128 from bug 42.\n"
- self.assert_execute_outputs(ApplyPatches(), [42], options=options, expected_stderr=expected_stderr)
-
- def test_land_diff(self):
- expected_stderr = "Building WebKit\nUpdating bug 42\n"
- self.assert_execute_outputs(LandDiff(), [42], options=self._default_options(), expected_stderr=expected_stderr)
-
- def test_check_style(self):
- expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nRunning check-webkit-style\n"
- self.assert_execute_outputs(CheckStyle(), [197], options=self._default_options(), expected_stderr=expected_stderr)
-
- def test_build_attachment(self):
- expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nBuilding WebKit\n"
- self.assert_execute_outputs(BuildAttachment(), [197], options=self._default_options(), expected_stderr=expected_stderr)
-
- def test_land_attachment(self):
- expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nBuilding WebKit\n"
- self.assert_execute_outputs(LandAttachment(), [197], options=self._default_options(), expected_stderr=expected_stderr)
-
- def test_land_patches(self):
- expected_stderr = "2 reviewed patches found on bug 42.\nProcessing 2 patches from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nBuilding WebKit\nUpdating working directory\nProcessing patch 128 from bug 42.\nBuilding WebKit\n"
- self.assert_execute_outputs(LandPatches(), [42], options=self._default_options(), expected_stderr=expected_stderr)
-
- def test_rollout(self):
- expected_stderr = "Updating working directory\nRunning prepare-ChangeLog\n\nNOTE: Rollout support is experimental.\nPlease verify the rollout diff and use \"bugzilla-tool land-diff 12345\" to commit the rollout.\n"
- self.assert_execute_outputs(Rollout(), [852], options=self._default_options(), expected_stderr=expected_stderr)
-
- def test_complete_rollout(self):
- options = self._default_options()
- options.complete_rollout = True
- expected_stderr = "Will re-open bug 12345 after rollout.\nUpdating working directory\nRunning prepare-ChangeLog\nBuilding WebKit\n"
- self.assert_execute_outputs(Rollout(), [852], options=options, expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/modules/commands/early_warning_system.py b/WebKitTools/Scripts/modules/commands/early_warning_system.py
deleted file mode 100644
index a96eaf1..0000000
--- a/WebKitTools/Scripts/modules/commands/early_warning_system.py
+++ /dev/null
@@ -1,113 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, 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.
-
-from StringIO import StringIO
-
-from modules.commands.queues import AbstractReviewQueue
-from modules.committers import CommitterList
-from modules.executive import ScriptError
-from modules.webkitport import WebKitPort
-
-
-class AbstractEarlyWarningSystem(AbstractReviewQueue):
- def __init__(self):
- AbstractReviewQueue.__init__(self)
- self.port = WebKitPort.port(self.port_name)
-
- def should_proceed_with_work_item(self, patch):
- try:
- self.run_bugzilla_tool(["build", self.port.flag(), "--force-clean", "--quiet"])
- self._update_status("Building", patch)
- except ScriptError, e:
- self._update_status("Unable to perform a build")
- return False
- return True
-
- def process_work_item(self, patch):
- try:
- self.run_bugzilla_tool([
- "build-attachment",
- self.port.flag(),
- "--force-clean",
- "--quiet",
- "--non-interactive",
- "--parent-command=%s" % self.name,
- "--no-update",
- patch["id"]])
- self._did_pass(patch)
- except ScriptError, e:
- self._did_fail(patch)
- raise e
-
- @classmethod
- def handle_script_error(cls, tool, state, script_error):
- status_id = cls._update_status_for_script_error(tool, state, script_error)
- # FIXME: This won't be right for ports that don't use build-webkit!
- if not script_error.command_name() == "build-webkit":
- return
- results_link = tool.status_bot.results_url_for_status(status_id)
- message = "Attachment %s did not build on %s:\nBuild output: %s" % (state["patch"]["id"], cls.port_name, results_link)
- tool.bugs.post_comment_to_bug(state["patch"]["bug_id"], message, cc=cls.watchers)
-
-
-class GtkEWS(AbstractEarlyWarningSystem):
- name = "gtk-ews"
- port_name = "gtk"
-
-
-class QtEWS(AbstractEarlyWarningSystem):
- name = "qt-ews"
- port_name = "qt"
-
-
-class ChromiumEWS(AbstractEarlyWarningSystem):
- name = "chromium-ews"
- port_name = "chromium"
- watchers = AbstractEarlyWarningSystem.watchers + [
- "dglazkov at chromium.org",
- ]
-
-
-# For platforms that we can't run inside a VM (like Mac OS X), we require
-# patches to be uploaded by committers, who are generally trustworthy folk. :)
-class AbstractCommitterOnlyEWS(AbstractEarlyWarningSystem):
- def __init__(self, committers=CommitterList()):
- AbstractEarlyWarningSystem.__init__(self)
- self._committers = committers
-
- def process_work_item(self, patch):
- if not self._committers.committer_by_email(patch["attacher_email"]):
- self._did_error(patch, "%s cannot process patches from non-committers :(" % self.name)
- return
- AbstractEarlyWarningSystem.process_work_item(self, patch)
-
-
-class MacEWS(AbstractCommitterOnlyEWS):
- name = "mac-ews"
- port_name = "mac"
diff --git a/WebKitTools/Scripts/modules/commands/early_warning_system_unittest.py b/WebKitTools/Scripts/modules/commands/early_warning_system_unittest.py
deleted file mode 100644
index 356e91e..0000000
--- a/WebKitTools/Scripts/modules/commands/early_warning_system_unittest.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# Copyright (C) 2009 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 os
-import unittest
-
-from modules.commands.early_warning_system import *
-from modules.commands.queuestest import QueuesTest
-from modules.mock import Mock
-
-class EarlyWarningSytemTest(QueuesTest):
- def test_chromium_ews(self):
- expected_stderr = {
- "begin_work_queue" : "CAUTION: chromium-ews will discard all local changes in \"%s\"\nRunning WebKit chromium-ews.\n" % os.getcwd(),
- "handle_unexpected_error" : "Mock error message\n",
- }
- self.assert_queue_outputs(ChromiumEWS(), expected_stderr=expected_stderr)
-
- def test_qt_ews(self):
- expected_stderr = {
- "begin_work_queue" : "CAUTION: qt-ews will discard all local changes in \"%s\"\nRunning WebKit qt-ews.\n" % os.getcwd(),
- "handle_unexpected_error" : "Mock error message\n",
- }
- self.assert_queue_outputs(QtEWS(), expected_stderr=expected_stderr)
-
- def test_gtk_ews(self):
- expected_stderr = {
- "begin_work_queue" : "CAUTION: gtk-ews will discard all local changes in \"%s\"\nRunning WebKit gtk-ews.\n" % os.getcwd(),
- "handle_unexpected_error" : "Mock error message\n",
- }
- self.assert_queue_outputs(GtkEWS(), expected_stderr=expected_stderr)
-
- def test_mac_ews(self):
- expected_stderr = {
- "begin_work_queue" : "CAUTION: mac-ews will discard all local changes in \"%s\"\nRunning WebKit mac-ews.\n" % os.getcwd(),
- "handle_unexpected_error" : "Mock error message\n",
- }
- self.assert_queue_outputs(MacEWS(), expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/modules/commands/queries.py b/WebKitTools/Scripts/modules/commands/queries.py
deleted file mode 100644
index d37fa06..0000000
--- a/WebKitTools/Scripts/modules/commands/queries.py
+++ /dev/null
@@ -1,130 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple 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.
-
-
-from optparse import make_option
-
-from modules.buildbot import BuildBot
-from modules.committers import CommitterList
-from modules.webkit_logging import log
-from modules.multicommandtool import Command
-
-
-class BugsToCommit(Command):
- name = "bugs-to-commit"
- def __init__(self):
- Command.__init__(self, "List bugs in the commit-queue")
-
- def execute(self, options, args, tool):
- bug_ids = tool.bugs.queries.fetch_bug_ids_from_commit_queue()
- for bug_id in bug_ids:
- print "%s" % bug_id
-
-
-class PatchesToCommit(Command):
- name = "patches-to-commit"
- def __init__(self):
- Command.__init__(self, "List patches in the commit-queue")
-
- def execute(self, options, args, tool):
- patches = tool.bugs.queries.fetch_patches_from_commit_queue()
- log("Patches in commit queue:")
- for patch in patches:
- print "%s" % patch["url"]
-
-
-class PatchesToCommitQueue(Command):
- name = "patches-to-commit-queue"
- def __init__(self):
- options = [
- make_option("--bugs", action="store_true", dest="bugs", help="Output bug links instead of patch links"),
- ]
- Command.__init__(self, "Patches which should be added to the commit queue", options=options)
-
- @staticmethod
- def _needs_commit_queue(patch):
- commit_queue_flag = patch.get("commit-queue")
- if (commit_queue_flag and commit_queue_flag == '+'): # If it's already cq+, ignore the patch.
- log("%s already has cq=%s" % (patch["id"], commit_queue_flag))
- return False
-
- # We only need to worry about patches from contributers who are not yet committers.
- committer_record = CommitterList().committer_by_email(patch["attacher_email"])
- if committer_record:
- log("%s committer = %s" % (patch["id"], committer_record))
- return not committer_record
-
- def execute(self, options, args, tool):
- patches = tool.bugs.queries.fetch_patches_from_pending_commit_list()
- patches_needing_cq = filter(self._needs_commit_queue, patches)
- if options.bugs:
- bugs_needing_cq = map(lambda patch: patch['bug_id'], patches_needing_cq)
- bugs_needing_cq = sorted(set(bugs_needing_cq))
- for bug_id in bugs_needing_cq:
- print "%s" % tool.bugs.bug_url_for_bug_id(bug_id)
- else:
- for patch in patches_needing_cq:
- print "%s" % tool.bugs.attachment_url_for_id(patch["id"], action="edit")
-
-
-class PatchesToReview(Command):
- name = "patches-to-review"
- def __init__(self):
- Command.__init__(self, "List patches that are pending review")
-
- def execute(self, options, args, tool):
- patch_ids = tool.bugs.queries.fetch_attachment_ids_from_review_queue()
- log("Patches pending review:")
- for patch_id in patch_ids:
- print patch_id
-
-
-class ReviewedPatches(Command):
- name = "reviewed-patches"
- def __init__(self):
- Command.__init__(self, "List r+'d patches on a bug", "BUGID")
-
- def execute(self, options, args, tool):
- bug_id = args[0]
- patches_to_land = tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
- for patch in patches_to_land:
- print "%s" % patch["url"]
-
-
-class TreeStatus(Command):
- name = "tree-status"
- show_in_main_help = True
- def __init__(self):
- Command.__init__(self, "Print the status of the %s buildbots" % BuildBot.default_host)
-
- def execute(self, options, args, tool):
- for builder in tool.buildbot.builder_statuses():
- status_string = "ok" if builder["is_green"] else "FAIL"
- print "%s : %s" % (status_string.ljust(4), builder["name"])
diff --git a/WebKitTools/Scripts/modules/commands/queries_unittest.py b/WebKitTools/Scripts/modules/commands/queries_unittest.py
deleted file mode 100644
index aa5f7f1..0000000
--- a/WebKitTools/Scripts/modules/commands/queries_unittest.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# Copyright (C) 2009 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 modules.bugzilla import Bugzilla
-from modules.commands.commandtest import CommandsTest
-from modules.commands.queries import *
-from modules.mock import Mock
-from modules.mock_bugzillatool import MockBugzillaTool
-
-class QueryCommandsTest(CommandsTest):
- def test_bugs_to_commit(self):
- self.assert_execute_outputs(BugsToCommit(), None, "42\n75\n")
-
- def test_patches_to_commit(self):
- expected_stdout = "http://example.com/197\nhttp://example.com/128\n"
- expected_stderr = "Patches in commit queue:\n"
- self.assert_execute_outputs(PatchesToCommit(), None, expected_stdout, expected_stderr)
-
- def test_patches_to_commit_queue(self):
- expected_stdout = "http://example.com/197&action=edit\n"
- expected_stderr = "128 committer = \"Eric Seidel\" <eric at webkit.org>\n"
- options = Mock()
- options.bugs = False
- self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options)
-
- expected_stdout = "http://example.com/42\n"
- options.bugs = True
- self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options)
-
- def test_patches_to_review(self):
- expected_stdout = "197\n128\n"
- expected_stderr = "Patches pending review:\n"
- self.assert_execute_outputs(PatchesToReview(), None, expected_stdout, expected_stderr)
-
- def test_reviewed_patches(self):
- expected_stdout = "http://example.com/197\nhttp://example.com/128\n"
- self.assert_execute_outputs(ReviewedPatches(), [42], expected_stdout)
-
- def test_tree_status(self):
- expected_stdout = "ok : Builder1\nok : Builder2\n"
- self.assert_execute_outputs(TreeStatus(), None, expected_stdout)
diff --git a/WebKitTools/Scripts/modules/commands/queues.py b/WebKitTools/Scripts/modules/commands/queues.py
deleted file mode 100644
index e14404c..0000000
--- a/WebKitTools/Scripts/modules/commands/queues.py
+++ /dev/null
@@ -1,252 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple 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 os
-
-from datetime import datetime
-from optparse import make_option
-from StringIO import StringIO
-
-from modules.executive import ScriptError
-from modules.grammar import pluralize
-from modules.webkit_logging import error, log
-from modules.multicommandtool import Command
-from modules.patchcollection import PersistentPatchCollection, PersistentPatchCollectionDelegate
-from modules.statusbot import StatusBot
-from modules.stepsequence import StepSequenceErrorHandler
-from modules.queueengine import QueueEngine, QueueEngineDelegate
-
-class AbstractQueue(Command, QueueEngineDelegate):
- watchers = [
- "webkit-bot-watchers at googlegroups.com",
- ]
-
- _pass_status = "Pass"
- _fail_status = "Fail"
- _error_status = "Error"
-
- def __init__(self, options=None): # Default values should never be collections (like []) as default values are shared between invocations
- options_list = (options or []) + [
- make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue. Dangerous!"),
- ]
- Command.__init__(self, "Run the %s" % self.name, options=options_list)
-
- def _cc_watchers(self, bug_id):
- try:
- self.tool.bugs.add_cc_to_bug(bug_id, self.watchers)
- except Exception, e:
- log("Failed to CC watchers: %s." % e)
-
- def _update_status(self, message, patch=None, results_file=None):
- self.tool.status_bot.update_status(self.name, message, patch, results_file)
-
- def _did_pass(self, patch):
- self._update_status(self._pass_status, patch)
-
- def _did_fail(self, patch):
- self._update_status(self._fail_status, patch)
-
- def _did_error(self, patch, reason):
- message = "%s: %s" % (self._error_status, reason)
- self._update_status(message, patch)
-
- def queue_log_path(self):
- return "%s.log" % self.name
-
- def work_item_log_path(self, patch):
- return os.path.join("%s-logs" % self.name, "%s.log" % patch["bug_id"])
-
- def begin_work_queue(self):
- log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self.tool.scm().checkout_root))
- if self.options.confirm:
- response = raw_input("Are you sure? Type \"yes\" to continue: ")
- if (response != "yes"):
- error("User declined.")
- log("Running WebKit %s." % self.name)
-
- def should_continue_work_queue(self):
- return True
-
- def next_work_item(self):
- raise NotImplementedError, "subclasses must implement"
-
- def should_proceed_with_work_item(self, work_item):
- raise NotImplementedError, "subclasses must implement"
-
- def process_work_item(self, work_item):
- raise NotImplementedError, "subclasses must implement"
-
- def handle_unexpected_error(self, work_item, message):
- raise NotImplementedError, "subclasses must implement"
-
- def run_bugzilla_tool(self, args):
- bugzilla_tool_args = [self.tool.path()]
- # FIXME: This is a hack, we should have a more general way to pass global options.
- bugzilla_tool_args += ["--status-host=%s" % self.tool.status_bot.statusbot_host]
- bugzilla_tool_args += map(str, args)
- self.tool.executive.run_and_throw_if_fail(bugzilla_tool_args)
-
- def log_progress(self, patch_ids):
- log("%s in %s [%s]" % (pluralize("patch", len(patch_ids)), self.name, ", ".join(map(str, patch_ids))))
-
- def execute(self, options, args, tool, engine=QueueEngine):
- self.options = options
- self.tool = tool
- return engine(self.name, self).run()
-
- @classmethod
- def _update_status_for_script_error(cls, tool, state, script_error):
- return tool.status_bot.update_status(cls.name, script_error.message, state["patch"], StringIO(script_error.output))
-
-
-class CommitQueue(AbstractQueue, StepSequenceErrorHandler):
- name = "commit-queue"
- def __init__(self):
- AbstractQueue.__init__(self)
-
- # AbstractQueue methods
-
- def begin_work_queue(self):
- AbstractQueue.begin_work_queue(self)
-
- def next_work_item(self):
- patches = self.tool.bugs.queries.fetch_patches_from_commit_queue(reject_invalid_patches=True)
- if not patches:
- self._update_status("Empty queue")
- return None
- # Only bother logging if we have patches in the queue.
- self.log_progress([patch['id'] for patch in patches])
- return patches[0]
-
- def should_proceed_with_work_item(self, patch):
- red_builders_names = self.tool.buildbot.red_core_builders_names()
- if red_builders_names:
- red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names.
- self._update_status("Builders [%s] are red. See http://build.webkit.org" % ", ".join(red_builders_names), None)
- return False
- self._update_status("Landing patch", patch)
- return True
-
- def process_work_item(self, patch):
- try:
- self._cc_watchers(patch["bug_id"])
- self.run_bugzilla_tool(["land-attachment", "--force-clean", "--non-interactive", "--parent-command=commit-queue", "--build-style=both", "--quiet", patch["id"]])
- self._did_pass(patch)
- except ScriptError, e:
- self._did_fail(patch)
- raise e
-
- def handle_unexpected_error(self, patch, message):
- self.tool.bugs.reject_patch_from_commit_queue(patch["id"], message)
-
- # StepSequenceErrorHandler methods
-
- @staticmethod
- def _error_message_for_bug(tool, status_id, script_error):
- if not script_error.output:
- return script_error.message_with_output()
- results_link = tool.status_bot.results_url_for_status(status_id)
- return "%s\nFull output: %s" % (script_error.message_with_output(), results_link)
-
- @classmethod
- def handle_script_error(cls, tool, state, script_error):
- status_id = cls._update_status_for_script_error(tool, state, script_error)
- tool.bugs.reject_patch_from_commit_queue(state["patch"]["id"], cls._error_message_for_bug(tool, status_id, script_error))
-
-
-class AbstractReviewQueue(AbstractQueue, PersistentPatchCollectionDelegate, StepSequenceErrorHandler):
- def __init__(self, options=None):
- AbstractQueue.__init__(self, options)
-
- # PersistentPatchCollectionDelegate methods
-
- def collection_name(self):
- return self.name
-
- def fetch_potential_patch_ids(self):
- return self.tool.bugs.queries.fetch_attachment_ids_from_review_queue()
-
- def status_server(self):
- return self.tool.status_bot
-
- # AbstractQueue methods
-
- def begin_work_queue(self):
- AbstractQueue.begin_work_queue(self)
- self._patches = PersistentPatchCollection(self)
-
- def next_work_item(self):
- patch_id = self._patches.next()
- if patch_id:
- return self.tool.bugs.fetch_attachment(patch_id)
- self._update_status("Empty queue")
-
- def should_proceed_with_work_item(self, patch):
- raise NotImplementedError, "subclasses must implement"
-
- def process_work_item(self, patch):
- raise NotImplementedError, "subclasses must implement"
-
- def handle_unexpected_error(self, patch, message):
- log(message)
-
- # StepSequenceErrorHandler methods
-
- @classmethod
- def handle_script_error(cls, tool, state, script_error):
- log(script_error.message_with_output())
-
-
-class StyleQueue(AbstractReviewQueue):
- name = "style-queue"
- def __init__(self):
- AbstractReviewQueue.__init__(self)
-
- def should_proceed_with_work_item(self, patch):
- self._update_status("Checking style", patch)
- return True
-
- def process_work_item(self, patch):
- try:
- self.run_bugzilla_tool(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch["id"]])
- message = "%s ran check-webkit-style on attachment %s without any errors." % (self.name, patch["id"])
- self.tool.bugs.post_comment_to_bug(patch["bug_id"], message, cc=self.watchers)
- self._did_pass(patch)
- except ScriptError, e:
- self._did_fail(patch)
- raise e
-
- @classmethod
- def handle_script_error(cls, tool, state, script_error):
- status_id = cls._update_status_for_script_error(tool, state, script_error)
- if not script_error.command_name() == "check-webkit-style":
- return
- message = "Attachment %s did not pass %s:\n\n%s" % (state["patch"]["id"], cls.name, script_error.message_with_output(output_limit=3*1024))
- tool.bugs.post_comment_to_bug(state["patch"]["bug_id"], message, cc=cls.watchers)
diff --git a/WebKitTools/Scripts/modules/commands/queues_unittest.py b/WebKitTools/Scripts/modules/commands/queues_unittest.py
deleted file mode 100644
index 8647af9..0000000
--- a/WebKitTools/Scripts/modules/commands/queues_unittest.py
+++ /dev/null
@@ -1,81 +0,0 @@
-# Copyright (C) 2009 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 os
-import unittest
-
-from modules.commands.commandtest import CommandsTest
-from modules.commands.queues import *
-from modules.commands.queuestest import QueuesTest
-from modules.mock_bugzillatool import MockBugzillaTool
-from modules.outputcapture import OutputCapture
-
-
-class TestQueue(AbstractQueue):
- name = "test-queue"
-
-
-class AbstractQueueTest(CommandsTest):
- def _assert_log_progress_output(self, patch_ids, progress_output):
- OutputCapture().assert_outputs(self, TestQueue().log_progress, [patch_ids], expected_stderr=progress_output)
-
- def test_log_progress(self):
- self._assert_log_progress_output([1,2,3], "3 patches in test-queue [1, 2, 3]\n")
- self._assert_log_progress_output(["1","2","3"], "3 patches in test-queue [1, 2, 3]\n")
- self._assert_log_progress_output([1], "1 patch in test-queue [1]\n")
-
- def _assert_run_bugzilla_tool(self, run_args):
- queue = TestQueue()
- tool = MockBugzillaTool()
- queue.bind_to_tool(tool)
-
- queue.run_bugzilla_tool(run_args)
- expected_run_args = ["echo", "--status-host=example.com"] + map(str, run_args)
- tool.executive.run_and_throw_if_fail.assert_called_with(expected_run_args)
-
- def test_run_bugzilla_tool(self):
- self._assert_run_bugzilla_tool([1])
- self._assert_run_bugzilla_tool(["one", 2])
-
-
-class CommitQueueTest(QueuesTest):
- def test_style_queue(self):
- expected_stderr = {
- "begin_work_queue" : "CAUTION: commit-queue will discard all local changes in \"%s\"\nRunning WebKit commit-queue.\n" % os.getcwd(),
- "next_work_item" : "2 patches in commit-queue [197, 128]\n",
- }
- self.assert_queue_outputs(CommitQueue(), expected_stderr=expected_stderr)
-
-
-class StyleQueueTest(QueuesTest):
- def test_style_queue(self):
- expected_stderr = {
- "begin_work_queue" : "CAUTION: style-queue will discard all local changes in \"%s\"\nRunning WebKit style-queue.\n" % os.getcwd(),
- "handle_unexpected_error" : "Mock error message\n",
- }
- self.assert_queue_outputs(StyleQueue(), expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/modules/commands/queuestest.py b/WebKitTools/Scripts/modules/commands/queuestest.py
deleted file mode 100644
index 6cb22a2..0000000
--- a/WebKitTools/Scripts/modules/commands/queuestest.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# Copyright (C) 2009 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 modules.mock import Mock
-from modules.mock_bugzillatool import MockBugzillaTool
-from modules.outputcapture import OutputCapture
-
-
-class MockQueueEngine(object):
- def __init__(self, name, queue):
- pass
-
- def run(self):
- pass
-
-
-class QueuesTest(unittest.TestCase):
- mock_work_item = {
- "id" : 1234,
- "bug_id" : 345,
- "attacher_email": "adam at example.com",
- }
-
- def assert_queue_outputs(self, queue, args=None, work_item=None, expected_stdout=None, expected_stderr=None, options=Mock(), tool=MockBugzillaTool()):
- if not expected_stdout:
- expected_stdout = {}
- if not expected_stderr:
- expected_stderr = {}
- if not args:
- args = []
- if not work_item:
- work_item = self.mock_work_item
- options.confirm = False # FIXME: We should have a tool.user that we can mock.
-
- queue.execute(options, args, tool, engine=MockQueueEngine)
-
- OutputCapture().assert_outputs(self,
- queue.queue_log_path,
- expected_stdout=expected_stdout.get("queue_log_path", ""),
- expected_stderr=expected_stderr.get("queue_log_path", ""))
- OutputCapture().assert_outputs(self,
- queue.work_item_log_path,
- args=[work_item],
- expected_stdout=expected_stdout.get("work_item_log_path", ""),
- expected_stderr=expected_stderr.get("work_item_log_path", ""))
- OutputCapture().assert_outputs(self,
- queue.begin_work_queue,
- expected_stdout=expected_stdout.get("begin_work_queue", ""),
- expected_stderr=expected_stderr.get("begin_work_queue", ""))
- OutputCapture().assert_outputs(self,
- queue.should_continue_work_queue,
- expected_stdout=expected_stdout.get("should_continue_work_queue", ""), expected_stderr=expected_stderr.get("should_continue_work_queue", ""))
- OutputCapture().assert_outputs(self,
- queue.next_work_item,
- expected_stdout=expected_stdout.get("next_work_item", ""),
- expected_stderr=expected_stderr.get("next_work_item", ""))
- OutputCapture().assert_outputs(self,
- queue.should_proceed_with_work_item,
- args=[work_item],
- expected_stdout=expected_stdout.get("should_proceed_with_work_item", ""),
- expected_stderr=expected_stderr.get("should_proceed_with_work_item", ""))
- OutputCapture().assert_outputs(self,
- queue.process_work_item,
- args=[work_item],
- expected_stdout=expected_stdout.get("process_work_item", ""),
- expected_stderr=expected_stderr.get("process_work_item", ""))
- OutputCapture().assert_outputs(self,
- queue.handle_unexpected_error,
- args=[work_item, "Mock error message"],
- expected_stdout=expected_stdout.get("handle_unexpected_error", ""),
- expected_stderr=expected_stderr.get("handle_unexpected_error", ""))
diff --git a/WebKitTools/Scripts/modules/commands/upload.py b/WebKitTools/Scripts/modules/commands/upload.py
deleted file mode 100644
index ac7995f..0000000
--- a/WebKitTools/Scripts/modules/commands/upload.py
+++ /dev/null
@@ -1,384 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple 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 os
-import re
-import StringIO
-import sys
-
-from optparse import make_option
-
-from modules.bugzilla import parse_bug_id
-from modules.buildsteps import PrepareChangeLogStep, EditChangeLogStep, ConfirmDiffStep, CommandOptions, ObsoletePatchesOnBugStep, PostDiffToBugStep, PromptForBugOrTitleStep, CreateBugStep
-from modules.commands.download import AbstractSequencedCommmand
-from modules.comments import bug_comment_from_svn_revision
-from modules.committers import CommitterList
-from modules.grammar import pluralize
-from modules.webkit_logging import error, log
-from modules.multicommandtool import Command, AbstractDeclarativeCommmand
-
-# FIXME: Requires unit test.
-class CommitMessageForCurrentDiff(Command):
- name = "commit-message"
- def __init__(self):
- Command.__init__(self, "Print a commit message suitable for the uncommitted changes")
-
- def execute(self, options, args, tool):
- os.chdir(tool.scm().checkout_root)
- print "%s" % tool.scm().commit_message_for_this_commit().message()
-
-
-class AssignToCommitter(AbstractDeclarativeCommmand):
- name = "assign-to-committer"
- help_text = "Assign bug to whoever attached the most recent r+'d patch"
-
- def _assign_bug_to_last_patch_attacher(self, bug_id):
- committers = CommitterList()
- bug = self.tool.bugs.fetch_bug(bug_id)
- assigned_to_email = bug.assigned_to_email()
- if assigned_to_email != self.tool.bugs.unassigned_email:
- log("Bug %s is already assigned to %s (%s)." % (bug_id, assigned_to_email, committers.committer_by_email(assigned_to_email)))
- return
-
- # FIXME: This should call a reviewed_patches() method on bug instead of re-fetching.
- reviewed_patches = self.tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
- if not reviewed_patches:
- log("Bug %s has no non-obsolete patches, ignoring." % bug_id)
- return
- latest_patch = reviewed_patches[-1]
- attacher_email = latest_patch["attacher_email"]
- committer = committers.committer_by_email(attacher_email)
- if not committer:
- log("Attacher %s is not a committer. Bug %s likely needs commit-queue+." % (attacher_email, bug_id))
- return
-
- reassign_message = "Attachment %s was posted by a committer and has review+, assigning to %s for commit." % (latest_patch["id"], committer.full_name)
- self.tool.bugs.reassign_bug(bug_id, committer.bugzilla_email(), reassign_message)
-
- def execute(self, options, args, tool):
- for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list():
- self._assign_bug_to_last_patch_attacher(bug_id)
-
-
-class ObsoleteAttachments(AbstractSequencedCommmand):
- name = "obsolete-attachments"
- help_text = "Mark all attachments on a bug as obsolete"
- argument_names = "BUGID"
- steps = [
- ObsoletePatchesOnBugStep,
- ]
-
- def _prepare_state(self, options, args, tool):
- return { "bug_id" : args[0] }
-
-
-class PostDiff(AbstractSequencedCommmand):
- name = "post-diff"
- help_text = "Attach the current working directory diff to a bug as a patch file"
- argument_names = "[BUGID]"
- show_in_main_help = True
- steps = [
- ConfirmDiffStep,
- ObsoletePatchesOnBugStep,
- PostDiffToBugStep,
- ]
-
- def _prepare_state(self, options, args, tool):
- # Perfer a bug id passed as an argument over a bug url in the diff (i.e. ChangeLogs).
- state = {}
- bug_id = args and args[0]
- if not bug_id:
- state["diff"] = tool.scm().create_patch()
- bug_id = parse_bug_id(state["diff"])
- if not bug_id:
- error("No bug id passed and no bug url found in diff, can't post.")
- state["bug_id"] = bug_id
- return state
-
-
-class PrepareDiff(AbstractSequencedCommmand):
- name = "prepare-diff"
- help_text = "Creates a bug (or prompts for an existing bug) and prepares the ChangeLogs"
- argument_names = "[BUGID]"
- steps = [
- PromptForBugOrTitleStep,
- CreateBugStep,
- PrepareChangeLogStep,
- ]
-
- def _prepare_state(self, options, args, tool):
- bug_id = args and args[0]
- return { "bug_id" : bug_id }
-
-
-class CreateReview(AbstractSequencedCommmand):
- name = "create-review"
- help_text = "Adds a ChangeLog to the current diff and posts it to a (possibly new) bug"
- argument_names = "[BUGID]"
- steps = [
- PromptForBugOrTitleStep,
- CreateBugStep,
- PrepareChangeLogStep,
- EditChangeLogStep,
- ConfirmDiffStep,
- ObsoletePatchesOnBugStep,
- PostDiffToBugStep,
- ]
-
- def _prepare_state(self, options, args, tool):
- bug_id = args and args[0]
- return { "bug_id" : bug_id }
-
-
-class EditChangeLog(AbstractSequencedCommmand):
- name = "edit-changelog"
- help_text = "Opens modified ChangeLogs in $EDITOR"
- steps = [
- EditChangeLogStep,
- ]
-
-
-class PostCommits(Command):
- name = "post-commits"
- show_in_main_help = True
- def __init__(self):
- options = [
- make_option("-b", "--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
- make_option("--add-log-as-comment", action="store_true", dest="add_log_as_comment", default=False, help="Add commit log message as a comment when uploading the patch."),
- make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: description from commit message)"),
- CommandOptions.obsolete_patches,
- CommandOptions.review,
- CommandOptions.request_commit,
- ]
- Command.__init__(self, "Attach a range of local commits to bugs as patch files", "COMMITISH", options=options, requires_local_commits=True)
-
- def _comment_text_for_commit(self, options, commit_message, tool, commit_id):
- comment_text = None
- if (options.add_log_as_comment):
- comment_text = commit_message.body(lstrip=True)
- comment_text += "---\n"
- comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
- return comment_text
-
- def _diff_file_for_commit(self, tool, commit_id):
- diff = tool.scm().create_patch_from_local_commit(commit_id)
- return StringIO.StringIO(diff) # add_patch_to_bug expects a file-like object
-
- def execute(self, options, args, tool):
- commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
- if len(commit_ids) > 10: # We could lower this limit, 10 is too many for one bug as-is.
- error("bugzilla-tool does not support attaching %s at once. Are you sure you passed the right commit range?" % (pluralize("patch", len(commit_ids))))
-
- have_obsoleted_patches = set()
- for commit_id in commit_ids:
- commit_message = tool.scm().commit_message_for_local_commit(commit_id)
-
- # Prefer --bug-id=, then a bug url in the commit message, then a bug url in the entire commit diff (i.e. ChangeLogs).
- bug_id = options.bug_id or parse_bug_id(commit_message.message()) or parse_bug_id(tool.scm().create_patch_from_local_commit(commit_id))
- if not bug_id:
- log("Skipping %s: No bug id found in commit or specified with --bug-id." % commit_id)
- continue
-
- if options.obsolete_patches and bug_id not in have_obsoleted_patches:
- state = { "bug_id": bug_id }
- ObsoletePatchesOnBugStep(tool, options).run(state)
- have_obsoleted_patches.add(bug_id)
-
- diff_file = self._diff_file_for_commit(tool, commit_id)
- description = options.description or commit_message.description(lstrip=True, strip_url=True)
- comment_text = self._comment_text_for_commit(options, commit_message, tool, commit_id)
- tool.bugs.add_patch_to_bug(bug_id, diff_file, description, comment_text, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
-
-
-# FIXME: Requires unit test. Blocking issue: too complex for now.
-class MarkBugFixed(Command):
- name = "mark-bug-fixed"
- show_in_main_help = True
- def __init__(self):
- options = [
- make_option("--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
- make_option("--comment", action="store", type="string", dest="comment", help="Text to include in bug comment."),
- make_option("--open", action="store_true", default=False, dest="open_bug", help="Open bug in default web browser (Mac only)."),
- make_option("--update-only", action="store_true", default=False, dest="update_only", help="Add comment to the bug, but do not close it."),
- ]
- Command.__init__(self, "Mark the specified bug as fixed", "[SVN_REVISION]", options=options)
-
- def _fetch_commit_log(self, tool, svn_revision):
- if not svn_revision:
- return tool.scm().last_svn_commit_log()
- return tool.scm().svn_commit_log(svn_revision)
-
- def _determine_bug_id_and_svn_revision(self, tool, bug_id, svn_revision):
- commit_log = self._fetch_commit_log(tool, svn_revision)
-
- if not bug_id:
- bug_id = parse_bug_id(commit_log)
-
- if not svn_revision:
- match = re.search("^r(?P<svn_revision>\d+) \|", commit_log, re.MULTILINE)
- if match:
- svn_revision = match.group('svn_revision')
-
- if not bug_id or not svn_revision:
- not_found = []
- if not bug_id:
- not_found.append("bug id")
- if not svn_revision:
- not_found.append("svn revision")
- error("Could not find %s on command-line or in %s."
- % (" or ".join(not_found), "r%s" % svn_revision if svn_revision else "last commit"))
-
- return (bug_id, svn_revision)
-
- def _open_bug_in_web_browser(self, tool, bug_id):
- if sys.platform == "darwin":
- tool.executive.run_command(["open", tool.bugs.short_bug_url_for_bug_id(bug_id)])
- return
- log("WARNING: --open is only supported on Mac OS X.")
-
- def _prompt_user_for_correctness(self, bug_id, svn_revision):
- answer = raw_input("Is this correct (y/N)? ")
- if not re.match("^\s*y(es)?", answer, re.IGNORECASE):
- exit(1)
-
- def execute(self, options, args, tool):
- bug_id = options.bug_id
-
- svn_revision = args and args[0]
- if svn_revision:
- if re.match("^r[0-9]+$", svn_revision, re.IGNORECASE):
- svn_revision = svn_revision[1:]
- if not re.match("^[0-9]+$", svn_revision):
- error("Invalid svn revision: '%s'" % svn_revision)
-
- needs_prompt = False
- if not bug_id or not svn_revision:
- needs_prompt = True
- (bug_id, svn_revision) = self._determine_bug_id_and_svn_revision(tool, bug_id, svn_revision)
-
- log("Bug: <%s> %s" % (tool.bugs.short_bug_url_for_bug_id(bug_id), tool.bugs.fetch_bug_dictionary(bug_id)["title"]))
- log("Revision: %s" % svn_revision)
-
- if options.open_bug:
- self._open_bug_in_web_browser(tool, bug_id)
-
- if needs_prompt:
- self._prompt_user_for_correctness(bug_id, svn_revision)
-
- bug_comment = bug_comment_from_svn_revision(svn_revision)
- if options.comment:
- bug_comment = "%s\n\n%s" % (options.comment, bug_comment)
-
- if options.update_only:
- log("Adding comment to Bug %s." % bug_id)
- tool.bugs.post_comment_to_bug(bug_id, bug_comment)
- else:
- log("Adding comment to Bug %s and marking as Resolved/Fixed." % bug_id)
- tool.bugs.close_bug_as_fixed(bug_id, bug_comment)
-
-
-# FIXME: Requires unit test. Blocking issue: too complex for now.
-class CreateBug(Command):
- name = "create-bug"
- show_in_main_help = True
- def __init__(self):
- options = [
- CommandOptions.cc,
- CommandOptions.component,
- make_option("--no-prompt", action="store_false", dest="prompt", default=True, help="Do not prompt for bug title and comment; use commit log instead."),
- make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."),
- make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review."),
- ]
- Command.__init__(self, "Create a bug from local changes or local commits", "[COMMITISH]", options=options)
-
- def create_bug_from_commit(self, options, args, tool):
- commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
- if len(commit_ids) > 3:
- error("Are you sure you want to create one bug with %s patches?" % len(commit_ids))
-
- commit_id = commit_ids[0]
-
- bug_title = ""
- comment_text = ""
- if options.prompt:
- (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
- else:
- commit_message = tool.scm().commit_message_for_local_commit(commit_id)
- bug_title = commit_message.description(lstrip=True, strip_url=True)
- comment_text = commit_message.body(lstrip=True)
- comment_text += "---\n"
- comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
-
- diff = tool.scm().create_patch_from_local_commit(commit_id)
- diff_file = StringIO.StringIO(diff) # create_bug expects a file-like object
- bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff_file, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
-
- if bug_id and len(commit_ids) > 1:
- options.bug_id = bug_id
- options.obsolete_patches = False
- # FIXME: We should pass through --no-comment switch as well.
- PostCommits.execute(self, options, commit_ids[1:], tool)
-
- def create_bug_from_patch(self, options, args, tool):
- bug_title = ""
- comment_text = ""
- if options.prompt:
- (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
- else:
- commit_message = tool.scm().commit_message_for_this_commit()
- bug_title = commit_message.description(lstrip=True, strip_url=True)
- comment_text = commit_message.body(lstrip=True)
-
- diff = tool.scm().create_patch()
- diff_file = StringIO.StringIO(diff) # create_bug expects a file-like object
- bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff_file, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
-
- def prompt_for_bug_title_and_comment(self):
- bug_title = raw_input("Bug title: ")
- print "Bug comment (hit ^D on blank line to end):"
- lines = sys.stdin.readlines()
- try:
- sys.stdin.seek(0, os.SEEK_END)
- except IOError:
- # Cygwin raises an Illegal Seek (errno 29) exception when the above
- # seek() call is made. Ignoring it seems to cause no harm.
- # FIXME: Figure out a way to get avoid the exception in the first
- # place.
- pass
- comment_text = "".join(lines)
- return (bug_title, comment_text)
-
- def execute(self, options, args, tool):
- if len(args):
- if (not tool.scm().supports_local_commits()):
- error("Extra arguments not supported; patch is taken from working directory.")
- self.create_bug_from_commit(options, args, tool)
- else:
- self.create_bug_from_patch(options, args, tool)
diff --git a/WebKitTools/Scripts/modules/commands/upload_unittest.py b/WebKitTools/Scripts/modules/commands/upload_unittest.py
deleted file mode 100644
index bf10740..0000000
--- a/WebKitTools/Scripts/modules/commands/upload_unittest.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# Copyright (C) 2009 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 modules.commands.commandtest import CommandsTest
-from modules.commands.upload import *
-from modules.mock_bugzillatool import MockBugzillaTool
-
-class UploadCommandsTest(CommandsTest):
- def test_assign_to_committer(self):
- tool = MockBugzillaTool()
- expected_stderr = "Bug 75 is already assigned to foo at foo.com (None).\nBug 76 has no non-obsolete patches, ignoring.\n"
- self.assert_execute_outputs(AssignToCommitter(), [], expected_stderr=expected_stderr, tool=tool)
- tool.bugs.reassign_bug.assert_called_with(42, "eric at webkit.org", "Attachment 128 was posted by a committer and has review+, assigning to Eric Seidel for commit.")
-
- def test_obsolete_attachments(self):
- expected_stderr = "Obsoleting 2 old patches on bug 42\n"
- self.assert_execute_outputs(ObsoleteAttachments(), [42], expected_stderr=expected_stderr)
-
- def test_post_diff(self):
- expected_stderr = "Obsoleting 2 old patches on bug 42\n"
- self.assert_execute_outputs(PostDiff(), [42], expected_stderr=expected_stderr)
-
- def test_prepare_diff_with_arg(self):
- self.assert_execute_outputs(PrepareDiff(), [42])
-
- def test_prepare_diff(self):
- self.assert_execute_outputs(PrepareDiff(), [])
-
- def test_create_review(self):
- expected_stderr = "Obsoleting 2 old patches on bug 42\n"
- self.assert_execute_outputs(CreateReview(), [42], expected_stderr=expected_stderr)
-
- def test_edit_changelog(self):
- self.assert_execute_outputs(EditChangeLog(), [])
diff --git a/WebKitTools/Scripts/modules/comments.py b/WebKitTools/Scripts/modules/comments.py
deleted file mode 100755
index eeee655..0000000
--- a/WebKitTools/Scripts/modules/comments.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple 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 tool for automating dealing with bugzilla, posting patches, committing patches, etc.
-
-from modules.changelogs import view_source_url
-
-def bug_comment_from_svn_revision(svn_revision):
- return "Committed r%s: <%s>" % (svn_revision, view_source_url(svn_revision))
-
-def bug_comment_from_commit_text(scm, commit_text):
- svn_revision = scm.svn_revision_from_commit_text(commit_text)
- return bug_comment_from_svn_revision(svn_revision)
diff --git a/WebKitTools/Scripts/modules/credentials.py b/WebKitTools/Scripts/modules/credentials.py
deleted file mode 100644
index 5ca5983..0000000
--- a/WebKitTools/Scripts/modules/credentials.py
+++ /dev/null
@@ -1,102 +0,0 @@
-# Copyright (c) 2009 Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple 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.
-#
-# Python module for reading stored web credentials from the OS.
-
-import getpass
-import os
-import platform
-import re
-
-from modules.executive import Executive
-from modules.webkit_logging import log
-from modules.scm import Git
-
-class Credentials(object):
- keychain_entry_not_found = "security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain."
-
- def __init__(self, host, git_prefix=None, executive=None, cwd=os.getcwd()):
- self.host = host
- self.git_prefix = git_prefix
- self.executive = executive or Executive()
- self.cwd = cwd
-
- def _credentials_from_git(self):
- return [self._read_git_config("username"), self._read_git_config("password")]
-
- def _read_git_config(self, key):
- config_key = "%s.%s" % (self.git_prefix, key) if self.git_prefix else key
- return self.executive.run_command(["git", "config", "--get", config_key], error_handler=Executive.ignore_error).rstrip('\n')
-
- def _keychain_value_with_label(self, label, source_text):
- match = re.search("%s\"(?P<value>.+)\"" % label, source_text, re.MULTILINE)
- if match:
- return match.group('value')
-
- def _is_mac_os_x(self):
- return platform.mac_ver()[0]
-
- def _parse_security_tool_output(self, security_output):
- if security_output == self.keychain_entry_not_found:
- return [None, None]
- username = self._keychain_value_with_label("^\s*\"acct\"<blob>=", security_output)
- password = self._keychain_value_with_label("^password: ", security_output)
- return [username, password]
-
- def _run_security_tool(self, username=None):
- security_command = ["/usr/bin/security", "find-internet-password", "-g", "-s", self.host]
- if username:
- security_command += ["-a", username]
-
- log("Reading Keychain for %s account and password. Click \"Allow\" to continue..." % self.host)
- return self.executive.run_command(security_command)
-
- def _credentials_from_keychain(self, username=None):
- if not self._is_mac_os_x():
- return [username, None]
-
- security_output = self._run_security_tool(username)
- return self._parse_security_tool_output(security_output)
-
- def read_credentials(self):
- username = None
- password = None
-
- if Git.in_working_directory(self.cwd):
- (username, password) = self._credentials_from_git()
-
- if not username or not password:
- (username, password) = self._credentials_from_keychain(username)
-
- if not username:
- username = raw_input("%s login: " % self.host)
- if not password:
- password = getpass.getpass("%s password for %s: " % (self.host, username))
-
- return [username, password]
diff --git a/WebKitTools/Scripts/modules/credentials_unittest.py b/WebKitTools/Scripts/modules/credentials_unittest.py
deleted file mode 100644
index 0e82932..0000000
--- a/WebKitTools/Scripts/modules/credentials_unittest.py
+++ /dev/null
@@ -1,119 +0,0 @@
-# Copyright (C) 2009 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 os
-import tempfile
-import unittest
-from modules.credentials import Credentials
-from modules.executive import Executive
-from modules.outputcapture import OutputCapture
-from modules.mock import Mock
-
-class CredentialsTest(unittest.TestCase):
- example_security_output = """keychain: "/Users/test/Library/Keychains/login.keychain"
-class: "inet"
-attributes:
- 0x00000007 <blob>="bugs.webkit.org (test at webkit.org)"
- 0x00000008 <blob>=<NULL>
- "acct"<blob>="test at webkit.org"
- "atyp"<blob>="form"
- "cdat"<timedate>=0x32303039303832353233353231365A00 "20090825235216Z\000"
- "crtr"<uint32>=<NULL>
- "cusi"<sint32>=<NULL>
- "desc"<blob>="Web form password"
- "icmt"<blob>="default"
- "invi"<sint32>=<NULL>
- "mdat"<timedate>=0x32303039303930393137323635315A00 "20090909172651Z\000"
- "nega"<sint32>=<NULL>
- "path"<blob>=<NULL>
- "port"<uint32>=0x00000000
- "prot"<blob>=<NULL>
- "ptcl"<uint32>="htps"
- "scrp"<sint32>=<NULL>
- "sdmn"<blob>=<NULL>
- "srvr"<blob>="bugs.webkit.org"
- "type"<uint32>=<NULL>
-password: "SECRETSAUCE"
-"""
-
- def test_keychain_lookup_on_non_mac(self):
- class FakeCredentials(Credentials):
- def _is_mac_os_x(self):
- return False
- credentials = FakeCredentials("bugs.webkit.org")
- self.assertEqual(credentials._is_mac_os_x(), False)
- self.assertEqual(credentials._credentials_from_keychain("foo"), ["foo", None])
-
- def test_security_output_parse(self):
- credentials = Credentials("bugs.webkit.org")
- self.assertEqual(credentials._parse_security_tool_output(self.example_security_output), ["test at webkit.org", "SECRETSAUCE"])
-
- def test_security_output_parse_entry_not_found(self):
- credentials = Credentials("foo.example.com")
- self.assertEqual(credentials._parse_security_tool_output(Credentials.keychain_entry_not_found), [None, None])
-
- def _assert_security_call(self, username=None):
- executive_mock = Mock()
- credentials = Credentials("example.com", executive=executive_mock)
-
- expected_stderr = "Reading Keychain for example.com account and password. Click \"Allow\" to continue...\n"
- OutputCapture().assert_outputs(self, credentials._run_security_tool, [username], expected_stderr=expected_stderr)
-
- security_args = ["/usr/bin/security", "find-internet-password", "-g", "-s", "example.com"]
- if username:
- security_args += ["-a", username]
- executive_mock.run_command.assert_called_with(security_args)
-
- def test_security_calls(self):
- self._assert_security_call()
- self._assert_security_call(username="foo")
-
- def test_git_config_calls(self):
- executive_mock = Mock()
- credentials = Credentials("example.com", executive=executive_mock)
- credentials._read_git_config("foo")
- executive_mock.run_command.assert_called_with(["git", "config", "--get", "foo"], error_handler=Executive.ignore_error)
-
- credentials = Credentials("example.com", git_prefix="test_prefix", executive=executive_mock)
- credentials._read_git_config("foo")
- executive_mock.run_command.assert_called_with(["git", "config", "--get", "test_prefix.foo"], error_handler=Executive.ignore_error)
-
- def test_read_credentials_without_git_repo(self):
- class FakeCredentials(Credentials):
- def _is_mac_os_x(self):
- return True
- def _credentials_from_keychain(self, username):
- return ["test at webkit.org", "SECRETSAUCE"]
-
- temp_dir_path = tempfile.mkdtemp(suffix="not_a_git_repo")
- credentials = FakeCredentials("bugs.webkit.org", cwd=temp_dir_path)
- self.assertEqual(credentials.read_credentials(), ["test at webkit.org", "SECRETSAUCE"])
- os.rmdir(temp_dir_path)
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/WebKitTools/Scripts/modules/executive.py b/WebKitTools/Scripts/modules/executive.py
deleted file mode 100644
index 115a8bc..0000000
--- a/WebKitTools/Scripts/modules/executive.py
+++ /dev/null
@@ -1,136 +0,0 @@
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple 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 os
-import StringIO
-import subprocess
-import sys
-
-from modules.webkit_logging import tee
-
-
-class ScriptError(Exception):
- def __init__(self, message=None, script_args=None, exit_code=None, output=None, cwd=None):
- if not message:
- message = 'Failed to run "%s"' % script_args
- if exit_code:
- message += " exit_code: %d" % exit_code
- if cwd:
- message += " cwd: %s" % cwd
-
- Exception.__init__(self, message)
- self.script_args = script_args # 'args' is already used by Exception
- self.exit_code = exit_code
- self.output = output
- self.cwd = cwd
-
- def message_with_output(self, output_limit=500):
- if self.output:
- if output_limit and len(self.output) > output_limit:
- return "%s\nLast %s characters of output:\n%s" % (self, output_limit, self.output[-output_limit:])
- return "%s\n%s" % (self, self.output)
- return str(self)
-
- def command_name(self):
- command_path = self.script_args
- if type(command_path) is list:
- command_path = command_path[0]
- return os.path.basename(command_path)
-
-
-# FIXME: This should not be a global static.
-# New code should use Executive.run_command directly instead
-def run_command(*args, **kwargs):
- return Executive().run_command(*args, **kwargs)
-
-
-class Executive(object):
- def _run_command_with_teed_output(self, args, teed_output):
- child_process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-
- # Use our own custom wait loop because Popen ignores a tee'd stderr/stdout.
- # FIXME: This could be improved not to flatten output to stdout.
- while True:
- output_line = child_process.stdout.readline()
- if output_line == "" and child_process.poll() != None:
- return child_process.poll()
- teed_output.write(output_line)
-
- def run_and_throw_if_fail(self, args, quiet=False):
- # Cache the child's output locally so it can be used for error reports.
- child_out_file = StringIO.StringIO()
- if quiet:
- dev_null = open(os.devnull, "w")
- child_stdout = tee(child_out_file, dev_null if quiet else sys.stdout)
- exit_code = self._run_command_with_teed_output(args, child_stdout)
- if quiet:
- dev_null.close()
-
- child_output = child_out_file.getvalue()
- child_out_file.close()
-
- if exit_code:
- raise ScriptError(script_args=args, exit_code=exit_code, output=child_output)
-
- # Error handlers do not need to be static methods once all callers are updated to use an Executive object.
- @staticmethod
- def default_error_handler(error):
- raise error
-
- @staticmethod
- def ignore_error(error):
- pass
-
- # FIXME: This should be merged with run_and_throw_if_fail
- def run_command(self, args, cwd=None, input=None, error_handler=None, return_exit_code=False, return_stderr=True):
- if hasattr(input, 'read'): # Check if the input is a file.
- stdin = input
- string_to_communicate = None
- else:
- stdin = subprocess.PIPE if input else None
- string_to_communicate = input
- if return_stderr:
- stderr = subprocess.STDOUT
- else:
- stderr = None
- try:
- process = subprocess.Popen(args, stdin=stdin, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
- output = process.communicate(string_to_communicate)[0]
- exit_code = process.wait()
- except OSError, e:
- # Catch OSError exceptions. For example, "no such file or directory" (i.e. OSError errno 2),
- # when the command cannot be found.
- output = e.strerror
- exit_code = e.errno
- if exit_code:
- script_error = ScriptError(script_args=args, exit_code=exit_code, output=output, cwd=cwd)
- (error_handler or self.default_error_handler)(script_error)
- if return_exit_code:
- return exit_code
- return output
diff --git a/WebKitTools/Scripts/modules/executive_unittest.py b/WebKitTools/Scripts/modules/executive_unittest.py
deleted file mode 100644
index 580a2e8..0000000
--- a/WebKitTools/Scripts/modules/executive_unittest.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-# Copyright (C) 2009 Daniel Bates (dbates at intudata.com). 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 modules.executive import Executive, ScriptError, run_command
-
-class ExecutiveTest(unittest.TestCase):
-
- def test_run_command_with_bad_command_check_return_code(self):
- self.assertEqual(run_command(["foo_bar_command_blah"], error_handler=Executive.ignore_error, return_exit_code=True), 2)
-
- def test_run_command_with_bad_command_check_calls_error_handler(self):
- self.didHandleErrorGetCalled = False
- def handleError(scriptError):
- self.didHandleErrorGetCalled = True
- self.assertEqual(scriptError.exit_code, 2)
-
- run_command(["foo_bar_command_blah"], error_handler=handleError)
- self.assertTrue(self.didHandleErrorGetCalled)
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/WebKitTools/Scripts/modules/mock_bugzillatool.py b/WebKitTools/Scripts/modules/mock_bugzillatool.py
deleted file mode 100644
index 533302d..0000000
--- a/WebKitTools/Scripts/modules/mock_bugzillatool.py
+++ /dev/null
@@ -1,214 +0,0 @@
-# Copyright (C) 2009 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 os
-
-from modules.mock import Mock
-from modules.scm import CommitMessage
-from modules.bugzilla import Bug
-
-def _id_to_object_dictionary(*objects):
- dictionary = {}
- for thing in objects:
- dictionary[thing["id"]] = thing
- return dictionary
-
-# FIXME: The ids should be 1, 2, 3 instead of crazy numbers.
-_patch1 = {
- "id" : 197,
- "bug_id" : 42,
- "url" : "http://example.com/197",
- "is_obsolete" : False,
- "is_patch" : True,
- "reviewer" : "Reviewer1",
- "attacher_email" : "Contributer1",
-}
-_patch2 = {
- "id" : 128,
- "bug_id" : 42,
- "url" : "http://example.com/128",
- "is_obsolete" : False,
- "is_patch" : True,
- "reviewer" : "Reviewer2",
- "attacher_email" : "eric at webkit.org",
-}
-
-# This must be defined before we define the bugs, thus we don't use MockBugzilla.unassigned_email directly.
-_unassigned_email = "unassigned at example.com"
-
-# FIXME: The ids should be 1, 2, 3 instead of crazy numbers.
-_bug1 = {
- "id" : 42,
- "assigned_to_email" : _unassigned_email,
- "attachments" : [_patch1, _patch2],
-}
-_bug2 = {
- "id" : 75,
- "assigned_to_email" : "foo at foo.com",
- "attachments" : [],
-}
-_bug3 = {
- "id" : 76,
- "assigned_to_email" : _unassigned_email,
- "attachments" : [],
-}
-
-class MockBugzillaQueries(Mock):
- def fetch_bug_ids_from_commit_queue(self):
- return [42, 75]
-
- def fetch_attachment_ids_from_review_queue(self):
- return [197, 128]
-
- def fetch_patches_from_commit_queue(self, reject_invalid_patches=False):
- return [_patch1, _patch2]
-
- def fetch_bug_ids_from_pending_commit_list(self):
- return [42, 75, 76]
-
- def fetch_patches_from_pending_commit_list(self):
- return [_patch1, _patch2]
-
-
-class MockBugzilla(Mock):
- bug_server_url = "http://example.com"
- unassigned_email = _unassigned_email
- bug_cache = _id_to_object_dictionary(_bug1, _bug2, _bug3)
- attachment_cache = _id_to_object_dictionary(_patch1, _patch2)
- queries = MockBugzillaQueries()
-
- def fetch_bug(self, bug_id):
- return Bug(self.bug_cache.get(bug_id))
-
- def fetch_reviewed_patches_from_bug(self, bug_id):
- return self.fetch_patches_from_bug(bug_id) # Return them all for now.
-
- def fetch_attachment(self, attachment_id):
- return self.attachment_cache[attachment_id] # This could be changed to .get() if we wish to allow failed lookups.
-
- # NOTE: Functions below this are direct copies from bugzilla.py
- def fetch_patches_from_bug(self, bug_id):
- return self.fetch_bug(bug_id).patches()
-
- def bug_url_for_bug_id(self, bug_id):
- return "%s/%s" % (self.bug_server_url, bug_id)
-
- def attachment_url_for_id(self, attachment_id, action):
- action_param = ""
- if action and action != "view":
- action_param = "&action=%s" % action
- return "%s/%s%s" % (self.bug_server_url, attachment_id, action_param)
-
-
-class MockBuildBot(Mock):
- def builder_statuses(self):
- return [{
- "name": "Builder1",
- "is_green": True
- }, {
- "name": "Builder2",
- "is_green": True
- }]
-
- def red_core_builders_names(self):
- return []
-
-
-class MockSCM(Mock):
- def __init__(self):
- Mock.__init__(self)
- self.checkout_root = os.getcwd()
-
- def create_patch(self):
- return "Patch1"
-
- def commit_ids_from_commitish_arguments(self, args):
- return ["Commitish1", "Commitish2"]
-
- def commit_message_for_local_commit(self, commit_id):
- if commit_id == "Commitish1":
- return CommitMessage("CommitMessage1\nhttps://bugs.example.org/show_bug.cgi?id=42\n")
- if commit_id == "Commitish2":
- return CommitMessage("CommitMessage2\nhttps://bugs.example.org/show_bug.cgi?id=75\n")
- raise Exception("Bogus commit_id in commit_message_for_local_commit.")
-
- def create_patch_from_local_commit(self, commit_id):
- if commit_id == "Commitish1":
- return "Patch1"
- if commit_id == "Commitish2":
- return "Patch2"
- raise Exception("Bogus commit_id in commit_message_for_local_commit.")
-
- def diff_for_revision(self, revision):
- return "DiffForRevision%s\nhttp://bugs.webkit.org/show_bug.cgi?id=12345" % revision
-
- def modified_changelogs(self):
- # Ideally we'd return something more interesting here.
- # The problem is that LandDiff will try to actually read the path from disk!
- return []
-
-
-class MockUser(object):
- def prompt(self, message):
- return "Mock user response"
-
- def edit(self, files):
- pass
-
- def page(self, message):
- pass
-
- def confirm(self):
- return True
-
-
-class MockStatusBot(object):
- def __init__(self):
- self.statusbot_host = "example.com"
-
- def patch_status(self, queue_name, patch_id):
- return None
-
- def update_status(self, queue_name, status, patch=None, results_file=None):
- return 187
-
-
-class MockBugzillaTool():
- def __init__(self):
- self.bugs = MockBugzilla()
- self.buildbot = MockBuildBot()
- self.executive = Mock()
- self.user = MockUser()
- self._scm = MockSCM()
- self.status_bot = MockStatusBot()
-
- def scm(self):
- return self._scm
-
- def path(self):
- return "echo"
diff --git a/WebKitTools/Scripts/modules/multicommandtool.py b/WebKitTools/Scripts/modules/multicommandtool.py
deleted file mode 100644
index 215e948..0000000
--- a/WebKitTools/Scripts/modules/multicommandtool.py
+++ /dev/null
@@ -1,293 +0,0 @@
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple 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.
-#
-# MultiCommandTool provides a framework for writing svn-like/git-like tools
-# which are called with the following format:
-# tool-name [global options] command-name [command options]
-
-import sys
-
-from optparse import OptionParser, IndentedHelpFormatter, SUPPRESS_USAGE, make_option
-
-from modules.grammar import pluralize
-from modules.webkit_logging import log
-
-
-class Command(object):
- name = None
- show_in_main_help = False
- def __init__(self, help_text, argument_names=None, options=None, requires_local_commits=False):
- self.help_text = help_text
- self.argument_names = argument_names
- self.required_arguments = self._parse_required_arguments(argument_names)
- self.options = options
- self.requires_local_commits = requires_local_commits
- self.tool = None
- # option_parser can be overriden by the tool using set_option_parser
- # This default parser will be used for standalone_help printing.
- self.option_parser = HelpPrintingOptionParser(usage=SUPPRESS_USAGE, add_help_option=False, option_list=self.options)
-
- # This design is slightly awkward, but we need the
- # the tool to be able to create and modify the option_parser
- # before it knows what Command to run.
- def set_option_parser(self, option_parser):
- self.option_parser = option_parser
- self._add_options_to_parser()
-
- def _add_options_to_parser(self):
- options = self.options or []
- for option in options:
- self.option_parser.add_option(option)
-
- # The tool calls bind_to_tool on each Command after adding it to its list.
- def bind_to_tool(self, tool):
- # Command instances can only be bound to one tool at a time.
- if self.tool and tool != self.tool:
- raise Exception("Command already bound to tool!")
- self.tool = tool
-
- @staticmethod
- def _parse_required_arguments(argument_names):
- required_args = []
- if not argument_names:
- return required_args
- split_args = argument_names.split(" ")
- for argument in split_args:
- if argument[0] == '[':
- # For now our parser is rather dumb. Do some minimal validation that
- # we haven't confused it.
- if argument[-1] != ']':
- raise Exception("Failure to parse argument string %s. Argument %s is missing ending ]" % (argument_names, argument))
- else:
- required_args.append(argument)
- return required_args
-
- def name_with_arguments(self):
- usage_string = self.name
- if self.options:
- usage_string += " [options]"
- if self.argument_names:
- usage_string += " " + self.argument_names
- return usage_string
-
- def parse_args(self, args):
- return self.option_parser.parse_args(args)
-
- def check_arguments_and_execute(self, options, args, tool=None):
- if len(args) < len(self.required_arguments):
- log("%s required, %s provided. Provided: %s Required: %s\nSee '%s help %s' for usage." % (
- pluralize("argument", len(self.required_arguments)),
- pluralize("argument", len(args)),
- "'%s'" % " ".join(args),
- " ".join(self.required_arguments),
- tool.name(),
- self.name))
- return 1
- return self.execute(options, args, tool) or 0
-
- def standalone_help(self):
- help_text = self.name_with_arguments().ljust(len(self.name_with_arguments()) + 3) + self.help_text + "\n"
- help_text += self.option_parser.format_option_help(IndentedHelpFormatter())
- return help_text
-
- def execute(self, options, args, tool):
- raise NotImplementedError, "subclasses must implement"
-
- # main() exists so that Commands can be turned into stand-alone scripts.
- # Other parts of the code will likely require modification to work stand-alone.
- def main(self, args=sys.argv):
- (options, args) = self.parse_args(args)
- # Some commands might require a dummy tool
- return self.check_arguments_and_execute(options, args)
-
-
-# FIXME: This should just be rolled into Command. help_text and argument_names do not need to be instance variables.
-class AbstractDeclarativeCommmand(Command):
- help_text = None
- argument_names = None
- def __init__(self, options=None):
- Command.__init__(self, self.help_text, self.argument_names, options)
-
-
-class HelpPrintingOptionParser(OptionParser):
- def __init__(self, epilog_method=None, *args, **kwargs):
- self.epilog_method = epilog_method
- OptionParser.__init__(self, *args, **kwargs)
-
- def error(self, msg):
- self.print_usage(sys.stderr)
- error_message = "%s: error: %s\n" % (self.get_prog_name(), msg)
- # This method is overriden to add this one line to the output:
- error_message += "\nType \"%s --help\" to see usage.\n" % self.get_prog_name()
- self.exit(1, error_message)
-
- # We override format_epilog to avoid the default formatting which would paragraph-wrap the epilog
- # and also to allow us to compute the epilog lazily instead of in the constructor (allowing it to be context sensitive).
- def format_epilog(self, epilog):
- if self.epilog_method:
- return "\n%s\n" % self.epilog_method()
- return ""
-
-
-class HelpCommand(Command):
- name = "help"
-
- def __init__(self):
- options = [
- make_option("-a", "--all-commands", action="store_true", dest="show_all_commands", help="Print all available commands"),
- ]
- Command.__init__(self, "Display information about this program or its subcommands", "[COMMAND]", options=options)
- self.show_all_commands = False # A hack used to pass --all-commands to _help_epilog even though it's called by the OptionParser.
-
- def _help_epilog(self):
- # Only show commands which are relevant to this checkout's SCM system. Might this be confusing to some users?
- if self.show_all_commands:
- epilog = "All %prog commands:\n"
- relevant_commands = self.tool.commands[:]
- else:
- epilog = "Common %prog commands:\n"
- relevant_commands = filter(self.tool.should_show_in_main_help, self.tool.commands)
- longest_name_length = max(map(lambda command: len(command.name), relevant_commands))
- relevant_commands.sort(lambda a, b: cmp(a.name, b.name))
- command_help_texts = map(lambda command: " %s %s\n" % (command.name.ljust(longest_name_length), command.help_text), relevant_commands)
- epilog += "%s\n" % "".join(command_help_texts)
- epilog += "See '%prog help --all-commands' to list all commands.\n"
- epilog += "See '%prog help COMMAND' for more information on a specific command.\n"
- return epilog.replace("%prog", self.tool.name()) # Use of %prog here mimics OptionParser.expand_prog_name().
-
- # FIXME: This is a hack so that we don't show --all-commands as a global option:
- def _remove_help_options(self):
- for option in self.options:
- self.option_parser.remove_option(option.get_opt_string())
-
- def execute(self, options, args, tool):
- if args:
- command = self.tool.command_by_name(args[0])
- if command:
- print command.standalone_help()
- return 0
-
- self.show_all_commands = options.show_all_commands
- self._remove_help_options()
- self.option_parser.print_help()
- return 0
-
-
-class MultiCommandTool(object):
- global_options = None
-
- def __init__(self, name=None, commands=None):
- self._name = name or OptionParser(prog=name).get_prog_name() # OptionParser has nice logic for fetching the name.
- # Allow the unit tests to disable command auto-discovery.
- self.commands = commands or [cls() for cls in self._find_all_commands() if cls.name]
- self.help_command = self.command_by_name(HelpCommand.name)
- # Require a help command, even if the manual test list doesn't include one.
- if not self.help_command:
- self.help_command = HelpCommand()
- self.commands.append(self.help_command)
- for command in self.commands:
- command.bind_to_tool(self)
-
- @classmethod
- def _add_all_subclasses(cls, class_to_crawl, seen_classes):
- for subclass in class_to_crawl.__subclasses__():
- if subclass not in seen_classes:
- seen_classes.add(subclass)
- cls._add_all_subclasses(subclass, seen_classes)
-
- @classmethod
- def _find_all_commands(cls):
- commands = set()
- cls._add_all_subclasses(Command, commands)
- return sorted(commands)
-
- def name(self):
- return self._name
-
- def _create_option_parser(self):
- usage = "Usage: %prog [options] COMMAND [ARGS]"
- return HelpPrintingOptionParser(epilog_method=self.help_command._help_epilog, prog=self.name(), usage=usage)
-
- @staticmethod
- def _split_command_name_from_args(args):
- # Assume the first argument which doesn't start with "-" is the command name.
- command_index = 0
- for arg in args:
- if arg[0] != "-":
- break
- command_index += 1
- else:
- return (None, args[:])
-
- command = args[command_index]
- return (command, args[:command_index] + args[command_index + 1:])
-
- def command_by_name(self, command_name):
- for command in self.commands:
- if command_name == command.name:
- return command
- return None
-
- def path(self):
- raise NotImplementedError, "subclasses must implement"
-
- def should_show_in_main_help(self, command):
- return command.show_in_main_help
-
- def should_execute_command(self, command):
- return True
-
- def _add_global_options(self, option_parser):
- global_options = self.global_options or []
- for option in global_options:
- option_parser.add_option(option)
-
- def handle_global_options(self, options):
- pass
-
- def main(self, argv=sys.argv):
- (command_name, args) = self._split_command_name_from_args(argv[1:])
-
- option_parser = self._create_option_parser()
- self._add_global_options(option_parser)
-
- command = self.command_by_name(command_name) or self.help_command
- if not command:
- option_parser.error("%s is not a recognized command" % command_name)
-
- command.set_option_parser(option_parser)
- (options, args) = command.parse_args(args)
- self.handle_global_options(options)
-
- (should_execute, failure_reason) = self.should_execute_command(command)
- if not should_execute:
- log(failure_reason)
- return 0 # FIXME: Should this really be 0?
-
- return command.check_arguments_and_execute(options, args, self)
diff --git a/WebKitTools/Scripts/modules/multicommandtool_unittest.py b/WebKitTools/Scripts/modules/multicommandtool_unittest.py
deleted file mode 100644
index 7f485a2..0000000
--- a/WebKitTools/Scripts/modules/multicommandtool_unittest.py
+++ /dev/null
@@ -1,153 +0,0 @@
-# Copyright (c) 2009, 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 sys
-import unittest
-from multicommandtool import MultiCommandTool, Command
-from modules.outputcapture import OutputCapture
-
-from optparse import make_option
-
-class TrivialCommand(Command):
- name = "trivial"
- show_in_main_help = True
- def __init__(self, **kwargs):
- Command.__init__(self, "help text", **kwargs)
-
- def execute(self, options, args, tool):
- pass
-
-class UncommonCommand(TrivialCommand):
- name = "uncommon"
- show_in_main_help = False
-
-class CommandTest(unittest.TestCase):
- def test_name_with_arguments(self):
- command_with_args = TrivialCommand(argument_names="ARG1 ARG2")
- self.assertEqual(command_with_args.name_with_arguments(), "trivial ARG1 ARG2")
-
- command_with_args = TrivialCommand(options=[make_option("--my_option")])
- self.assertEqual(command_with_args.name_with_arguments(), "trivial [options]")
-
- def test_parse_required_arguments(self):
- self.assertEqual(Command._parse_required_arguments("ARG1 ARG2"), ["ARG1", "ARG2"])
- self.assertEqual(Command._parse_required_arguments("[ARG1] [ARG2]"), [])
- self.assertEqual(Command._parse_required_arguments("[ARG1] ARG2"), ["ARG2"])
- # Note: We might make our arg parsing smarter in the future and allow this type of arguments string.
- self.assertRaises(Exception, Command._parse_required_arguments, "[ARG1 ARG2]")
-
- def test_required_arguments(self):
- two_required_arguments = TrivialCommand(argument_names="ARG1 ARG2 [ARG3]")
- expected_missing_args_error = "2 arguments required, 1 argument provided. Provided: 'foo' Required: ARG1 ARG2\nSee 'trivial-tool help trivial' for usage.\n"
- exit_code = OutputCapture().assert_outputs(self, two_required_arguments.check_arguments_and_execute, [None, ["foo"], TrivialTool()], expected_stderr=expected_missing_args_error)
- self.assertEqual(exit_code, 1)
-
-
-class TrivialTool(MultiCommandTool):
- def __init__(self, commands=None):
- MultiCommandTool.__init__(self, name="trivial-tool", commands=commands)
-
- def path():
- return __file__
-
- def should_execute_command(self, command):
- return (True, None)
-
-
-class MultiCommandToolTest(unittest.TestCase):
- def _assert_split(self, args, expected_split):
- self.assertEqual(MultiCommandTool._split_command_name_from_args(args), expected_split)
-
- def test_split_args(self):
- # MultiCommandToolTest._split_command_name_from_args returns: (command, args)
- full_args = ["--global-option", "command", "--option", "arg"]
- full_args_expected = ("command", ["--global-option", "--option", "arg"])
- self._assert_split(full_args, full_args_expected)
-
- full_args = []
- full_args_expected = (None, [])
- self._assert_split(full_args, full_args_expected)
-
- full_args = ["command", "arg"]
- full_args_expected = ("command", ["arg"])
- self._assert_split(full_args, full_args_expected)
-
- def test_command_by_name(self):
- # This also tests Command auto-discovery.
- tool = TrivialTool()
- self.assertEqual(tool.command_by_name("trivial").name, "trivial")
- self.assertEqual(tool.command_by_name("bar"), None)
-
- def _assert_tool_main_outputs(self, tool, main_args, expected_stdout, expected_stderr = "", expected_exit_code=0):
- exit_code = OutputCapture().assert_outputs(self, tool.main, [main_args], expected_stdout=expected_stdout, expected_stderr=expected_stderr)
- self.assertEqual(exit_code, expected_exit_code)
-
- def test_global_help(self):
- tool = TrivialTool(commands=[TrivialCommand(), UncommonCommand()])
- expected_common_commands_help = """Usage: trivial-tool [options] COMMAND [ARGS]
-
-Options:
- -h, --help show this help message and exit
-
-Common trivial-tool commands:
- trivial help text
-
-See 'trivial-tool help --all-commands' to list all commands.
-See 'trivial-tool help COMMAND' for more information on a specific command.
-
-"""
- self._assert_tool_main_outputs(tool, ["tool"], expected_common_commands_help)
- self._assert_tool_main_outputs(tool, ["tool", "help"], expected_common_commands_help)
- expected_all_commands_help = """Usage: trivial-tool [options] COMMAND [ARGS]
-
-Options:
- -h, --help show this help message and exit
-
-All trivial-tool commands:
- help Display information about this program or its subcommands
- trivial help text
- uncommon help text
-
-See 'trivial-tool help --all-commands' to list all commands.
-See 'trivial-tool help COMMAND' for more information on a specific command.
-
-"""
- self._assert_tool_main_outputs(tool, ["tool", "help", "--all-commands"], expected_all_commands_help)
- # Test that arguments can be passed before commands as well
- self._assert_tool_main_outputs(tool, ["tool", "--all-commands", "help"], expected_all_commands_help)
-
-
- def test_command_help(self):
- command_with_options = TrivialCommand(options=[make_option("--my_option")])
- tool = TrivialTool(commands=[command_with_options])
- expected_subcommand_help = "trivial [options] help text\nOptions:\n --my_option=MY_OPTION\n\n"
- self._assert_tool_main_outputs(tool, ["tool", "help", "trivial"], expected_subcommand_help)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/WebKitTools/Scripts/modules/queueengine.py b/WebKitTools/Scripts/modules/queueengine.py
deleted file mode 100644
index 8640f02..0000000
--- a/WebKitTools/Scripts/modules/queueengine.py
+++ /dev/null
@@ -1,144 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple 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 os
-import time
-import traceback
-
-from datetime import datetime, timedelta
-
-from modules.executive import ScriptError
-from modules.webkit_logging import log, OutputTee
-from modules.statusbot import StatusBot
-
-class QueueEngineDelegate:
- def queue_log_path(self):
- raise NotImplementedError, "subclasses must implement"
-
- def work_item_log_path(self, work_item):
- raise NotImplementedError, "subclasses must implement"
-
- def begin_work_queue(self):
- raise NotImplementedError, "subclasses must implement"
-
- def should_continue_work_queue(self):
- raise NotImplementedError, "subclasses must implement"
-
- def next_work_item(self):
- raise NotImplementedError, "subclasses must implement"
-
- def should_proceed_with_work_item(self, work_item):
- # returns (safe_to_proceed, waiting_message, patch)
- raise NotImplementedError, "subclasses must implement"
-
- def process_work_item(self, work_item):
- raise NotImplementedError, "subclasses must implement"
-
- def handle_unexpected_error(self, work_item, message):
- raise NotImplementedError, "subclasses must implement"
-
-
-class QueueEngine:
- def __init__(self, name, delegate):
- self._name = name
- self._delegate = delegate
- self._output_tee = OutputTee()
-
- log_date_format = "%Y-%m-%d %H:%M:%S"
- sleep_duration_text = "5 mins"
- seconds_to_sleep = 300
- handled_error_code = 2
-
- # Child processes exit with a special code to the parent queue process can detect the error was handled.
- @classmethod
- def exit_after_handled_error(cls, error):
- log(error)
- exit(cls.handled_error_code)
-
- def run(self):
- self._begin_logging()
-
- self._delegate.begin_work_queue()
- while (self._delegate.should_continue_work_queue()):
- try:
- self._ensure_work_log_closed()
- work_item = self._delegate.next_work_item()
- if not work_item:
- self._sleep("No work item.")
- continue
- if not self._delegate.should_proceed_with_work_item(work_item):
- self._sleep("Not proceeding with work item.")
- continue
-
- # FIXME: Work logs should not depend on bug_id specificaly.
- # This looks fixed, no?
- self._open_work_log(work_item)
- try:
- self._delegate.process_work_item(work_item)
- except ScriptError, e:
- # Use a special exit code to indicate that the error was already
- # handled in the child process and we should just keep looping.
- if e.exit_code == self.handled_error_code:
- continue
- message = "Unexpected failure when landing patch! Please file a bug against bugzilla-tool.\n%s" % e.message_with_output()
- self._delegate.handle_unexpected_error(work_item, message)
- except KeyboardInterrupt, e:
- log("\nUser terminated queue.")
- return 1
- except Exception, e:
- traceback.print_exc()
- # Don't try tell the status bot, in case telling it causes an exception.
- self._sleep("Exception while preparing queue: %s." % e)
- # Never reached.
- self._ensure_work_log_closed()
-
- def _begin_logging(self):
- self._queue_log = self._output_tee.add_log(self._delegate.queue_log_path())
- self._work_log = None
-
- def _open_work_log(self, work_item):
- work_item_log_path = self._delegate.work_item_log_path(work_item)
- self._work_log = self._output_tee.add_log(work_item_log_path)
-
- def _ensure_work_log_closed(self):
- # If we still have a bug log open, close it.
- if self._work_log:
- self._output_tee.remove_log(self._work_log)
- self._work_log = None
-
- @classmethod
- def _sleep_message(cls, message):
- wake_time = datetime.now() + timedelta(seconds=cls.seconds_to_sleep)
- return "%s Sleeping until %s (%s)." % (message, wake_time.strftime(cls.log_date_format), cls.sleep_duration_text)
-
- @classmethod
- def _sleep(cls, message):
- log(cls._sleep_message(message))
- time.sleep(cls.seconds_to_sleep)
diff --git a/WebKitTools/Scripts/modules/queueengine_unittest.py b/WebKitTools/Scripts/modules/queueengine_unittest.py
deleted file mode 100644
index 8f1093d..0000000
--- a/WebKitTools/Scripts/modules/queueengine_unittest.py
+++ /dev/null
@@ -1,170 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, 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 os
-import shutil
-import tempfile
-import unittest
-
-from modules.executive import ScriptError
-from modules.queueengine import QueueEngine, QueueEngineDelegate
-
-class LoggingDelegate(QueueEngineDelegate):
- def __init__(self, test):
- self._test = test
- self._callbacks = []
- self._run_before = False
-
- expected_callbacks = [
- 'queue_log_path',
- 'begin_work_queue',
- 'should_continue_work_queue',
- 'next_work_item',
- 'should_proceed_with_work_item',
- 'work_item_log_path',
- 'process_work_item',
- 'should_continue_work_queue'
- ]
-
- def record(self, method_name):
- self._callbacks.append(method_name)
-
- def queue_log_path(self):
- self.record("queue_log_path")
- return os.path.join(self._test.temp_dir, "queue_log_path")
-
- def work_item_log_path(self, work_item):
- self.record("work_item_log_path")
- return os.path.join(self._test.temp_dir, "work_log_path", "%s.log" % work_item)
-
- def begin_work_queue(self):
- self.record("begin_work_queue")
-
- def should_continue_work_queue(self):
- self.record("should_continue_work_queue")
- if not self._run_before:
- self._run_before = True
- return True
- return False
-
- def next_work_item(self):
- self.record("next_work_item")
- return "work_item"
-
- def should_proceed_with_work_item(self, work_item):
- self.record("should_proceed_with_work_item")
- self._test.assertEquals(work_item, "work_item")
- fake_patch = { 'bug_id' : 42 }
- return (True, "waiting_message", fake_patch)
-
- def process_work_item(self, work_item):
- self.record("process_work_item")
- self._test.assertEquals(work_item, "work_item")
-
- def handle_unexpected_error(self, work_item, message):
- self.record("handle_unexpected_error")
- self._test.assertEquals(work_item, "work_item")
-
-
-class ThrowErrorDelegate(LoggingDelegate):
- def __init__(self, test, error_code):
- LoggingDelegate.__init__(self, test)
- self.error_code = error_code
-
- def process_work_item(self, work_item):
- self.record("process_work_item")
- raise ScriptError(exit_code=self.error_code)
-
-
-class NotSafeToProceedDelegate(LoggingDelegate):
- def should_proceed_with_work_item(self, work_item):
- self.record("should_proceed_with_work_item")
- self._test.assertEquals(work_item, "work_item")
- return False
-
-
-class FastQueueEngine(QueueEngine):
- def __init__(self, delegate):
- QueueEngine.__init__(self, "fast-queue", delegate)
-
- # No sleep for the wicked.
- seconds_to_sleep = 0
-
- def _sleep(self, message):
- pass
-
-
-class QueueEngineTest(unittest.TestCase):
- def test_trivial(self):
- delegate = LoggingDelegate(self)
- work_queue = QueueEngine("trivial-queue", delegate)
- work_queue.run()
- self.assertEquals(delegate._callbacks, LoggingDelegate.expected_callbacks)
- self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "queue_log_path")))
- self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "work_log_path", "work_item.log")))
-
- def test_unexpected_error(self):
- delegate = ThrowErrorDelegate(self, 3)
- work_queue = QueueEngine("error-queue", delegate)
- work_queue.run()
- expected_callbacks = LoggingDelegate.expected_callbacks[:]
- work_item_index = expected_callbacks.index('process_work_item')
- # The unexpected error should be handled right after process_work_item starts
- # but before any other callback. Otherwise callbacks should be normal.
- expected_callbacks.insert(work_item_index + 1, 'handle_unexpected_error')
- self.assertEquals(delegate._callbacks, expected_callbacks)
-
- def test_handled_error(self):
- delegate = ThrowErrorDelegate(self, QueueEngine.handled_error_code)
- work_queue = QueueEngine("handled-error-queue", delegate)
- work_queue.run()
- self.assertEquals(delegate._callbacks, LoggingDelegate.expected_callbacks)
-
- def test_not_safe_to_proceed(self):
- delegate = NotSafeToProceedDelegate(self)
- work_queue = FastQueueEngine(delegate)
- work_queue.run()
- expected_callbacks = LoggingDelegate.expected_callbacks[:]
- next_work_item_index = expected_callbacks.index('next_work_item')
- # We slice out the common part of the expected callbacks.
- # We add 2 here to include should_proceed_with_work_item, which is
- # a pain to search for directly because it occurs twice.
- expected_callbacks = expected_callbacks[:next_work_item_index + 2]
- expected_callbacks.append('should_continue_work_queue')
- self.assertEquals(delegate._callbacks, expected_callbacks)
-
- def setUp(self):
- self.temp_dir = tempfile.mkdtemp(suffix="work_queue_test_logs")
-
- def tearDown(self):
- shutil.rmtree(self.temp_dir)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/WebKitTools/Scripts/modules/scm.py b/WebKitTools/Scripts/modules/scm.py
deleted file mode 100644
index 3ef3093..0000000
--- a/WebKitTools/Scripts/modules/scm.py
+++ /dev/null
@@ -1,512 +0,0 @@
-# Copyright (c) 2009, Google Inc. All rights reserved.
-# Copyright (c) 2009 Apple 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.
-#
-# Python module for interacting with an SCM system (like SVN or Git)
-
-import os
-import re
-import subprocess
-
-# Import WebKit-specific modules.
-from modules.changelogs import ChangeLog
-from modules.executive import Executive, run_command, ScriptError
-from modules.webkit_logging import error, log
-
-def detect_scm_system(path):
- if SVN.in_working_directory(path):
- return SVN(cwd=path)
-
- if Git.in_working_directory(path):
- return Git(cwd=path)
-
- return None
-
-def first_non_empty_line_after_index(lines, index=0):
- first_non_empty_line = index
- for line in lines[index:]:
- if re.match("^\s*$", line):
- first_non_empty_line += 1
- else:
- break
- return first_non_empty_line
-
-
-class CommitMessage:
- def __init__(self, message):
- self.message_lines = message[first_non_empty_line_after_index(message, 0):]
-
- def body(self, lstrip=False):
- lines = self.message_lines[first_non_empty_line_after_index(self.message_lines, 1):]
- if lstrip:
- lines = [line.lstrip() for line in lines]
- return "\n".join(lines) + "\n"
-
- def description(self, lstrip=False, strip_url=False):
- line = self.message_lines[0]
- if lstrip:
- line = line.lstrip()
- if strip_url:
- line = re.sub("^(\s*)<.+> ", "\1", line)
- return line
-
- def message(self):
- return "\n".join(self.message_lines) + "\n"
-
-
-class CheckoutNeedsUpdate(ScriptError):
- def __init__(self, script_args, exit_code, output, cwd):
- ScriptError.__init__(self, script_args=script_args, exit_code=exit_code, output=output, cwd=cwd)
-
-
-def commit_error_handler(error):
- if re.search("resource out of date", error.output):
- raise CheckoutNeedsUpdate(script_args=error.script_args, exit_code=error.exit_code, output=error.output, cwd=error.cwd)
- Executive.default_error_handler(error)
-
-
-class SCM:
- def __init__(self, cwd, dryrun=False):
- self.cwd = cwd
- self.checkout_root = self.find_checkout_root(self.cwd)
- self.dryrun = dryrun
-
- def scripts_directory(self):
- return os.path.join(self.checkout_root, "WebKitTools", "Scripts")
-
- def script_path(self, script_name):
- return os.path.join(self.scripts_directory(), script_name)
-
- def ensure_clean_working_directory(self, force_clean):
- if not force_clean and not self.working_directory_is_clean():
- print run_command(self.status_command(), error_handler=Executive.ignore_error)
- raise ScriptError(message="Working directory has modifications, pass --force-clean or --no-clean to continue.")
-
- log("Cleaning working directory")
- self.clean_working_directory()
-
- def ensure_no_local_commits(self, force):
- if not self.supports_local_commits():
- return
- commits = self.local_commits()
- if not len(commits):
- return
- if not force:
- error("Working directory has local commits, pass --force-clean to continue.")
- self.discard_local_commits()
-
- def apply_patch(self, patch, force=False):
- # It's possible that the patch was not made from the root directory.
- # We should detect and handle that case.
- curl_process = subprocess.Popen(['curl', '--location', '--silent', '--show-error', patch['url']], stdout=subprocess.PIPE)
- args = [self.script_path('svn-apply')]
- if patch.get('reviewer'):
- args += ['--reviewer', patch['reviewer']]
- if force:
- args.append('--force')
-
- run_command(args, input=curl_process.stdout)
-
- def run_status_and_extract_filenames(self, status_command, status_regexp):
- filenames = []
- for line in run_command(status_command).splitlines():
- match = re.search(status_regexp, line)
- if not match:
- continue
- # status = match.group('status')
- filename = match.group('filename')
- filenames.append(filename)
- return filenames
-
- def strip_r_from_svn_revision(self, svn_revision):
- match = re.match("^r(?P<svn_revision>\d+)", svn_revision)
- if (match):
- return match.group('svn_revision')
- return svn_revision
-
- def svn_revision_from_commit_text(self, commit_text):
- match = re.search(self.commit_success_regexp(), commit_text, re.MULTILINE)
- return match.group('svn_revision')
-
- # ChangeLog-specific code doesn't really belong in scm.py, but this function is very useful.
- def modified_changelogs(self):
- changelog_paths = []
- paths = self.changed_files()
- for path in paths:
- if os.path.basename(path) == "ChangeLog":
- changelog_paths.append(path)
- return changelog_paths
-
- # FIXME: Requires unit test
- # FIXME: commit_message_for_this_commit and modified_changelogs don't
- # really belong here. We should have a separate module for
- # handling ChangeLogs.
- def commit_message_for_this_commit(self):
- changelog_paths = self.modified_changelogs()
- if not len(changelog_paths):
- raise ScriptError(message="Found no modified ChangeLogs, cannot create a commit message.\n"
- "All changes require a ChangeLog. See:\n"
- "http://webkit.org/coding/contributing.html")
-
- changelog_messages = []
- for changelog_path in changelog_paths:
- log("Parsing ChangeLog: %s" % changelog_path)
- changelog_entry = ChangeLog(changelog_path).latest_entry()
- if not changelog_entry:
- raise ScriptError(message="Failed to parse ChangeLog: " + os.path.abspath(changelog_path))
- changelog_messages.append(changelog_entry)
-
- # FIXME: We should sort and label the ChangeLog messages like commit-log-editor does.
- return CommitMessage("".join(changelog_messages).splitlines())
-
- @staticmethod
- def in_working_directory(path):
- raise NotImplementedError, "subclasses must implement"
-
- @staticmethod
- def find_checkout_root(path):
- raise NotImplementedError, "subclasses must implement"
-
- @staticmethod
- def commit_success_regexp():
- raise NotImplementedError, "subclasses must implement"
-
- def working_directory_is_clean(self):
- raise NotImplementedError, "subclasses must implement"
-
- def clean_working_directory(self):
- raise NotImplementedError, "subclasses must implement"
-
- def status_command(self):
- raise NotImplementedError, "subclasses must implement"
-
- def changed_files(self):
- raise NotImplementedError, "subclasses must implement"
-
- def display_name(self):
- raise NotImplementedError, "subclasses must implement"
-
- def create_patch(self):
- raise NotImplementedError, "subclasses must implement"
-
- def diff_for_revision(self, revision):
- raise NotImplementedError, "subclasses must implement"
-
- def apply_reverse_diff(self, revision):
- raise NotImplementedError, "subclasses must implement"
-
- def revert_files(self, file_paths):
- raise NotImplementedError, "subclasses must implement"
-
- def commit_with_message(self, message):
- raise NotImplementedError, "subclasses must implement"
-
- def svn_commit_log(self, svn_revision):
- raise NotImplementedError, "subclasses must implement"
-
- def last_svn_commit_log(self):
- raise NotImplementedError, "subclasses must implement"
-
- # Subclasses must indicate if they support local commits,
- # but the SCM baseclass will only call local_commits methods when this is true.
- @staticmethod
- def supports_local_commits():
- raise NotImplementedError, "subclasses must implement"
-
- def create_patch_from_local_commit(self, commit_id):
- error("Your source control manager does not support creating a patch from a local commit.")
-
- def create_patch_since_local_commit(self, commit_id):
- error("Your source control manager does not support creating a patch from a local commit.")
-
- def commit_locally_with_message(self, message):
- error("Your source control manager does not support local commits.")
-
- def discard_local_commits(self):
- pass
-
- def local_commits(self):
- return []
-
-
-class SVN(SCM):
- def __init__(self, cwd, dryrun=False):
- SCM.__init__(self, cwd, dryrun)
- self.cached_version = None
-
- @staticmethod
- def in_working_directory(path):
- return os.path.isdir(os.path.join(path, '.svn'))
-
- @classmethod
- def find_uuid(cls, path):
- if not cls.in_working_directory(path):
- return None
- return cls.value_from_svn_info(path, 'Repository UUID')
-
- @classmethod
- def value_from_svn_info(cls, path, field_name):
- svn_info_args = ['svn', 'info', path]
- info_output = run_command(svn_info_args).rstrip()
- match = re.search("^%s: (?P<value>.+)$" % field_name, info_output, re.MULTILINE)
- if not match:
- raise ScriptError(script_args=svn_info_args, message='svn info did not contain a %s.' % field_name)
- return match.group('value')
-
- @staticmethod
- def find_checkout_root(path):
- uuid = SVN.find_uuid(path)
- # If |path| is not in a working directory, we're supposed to return |path|.
- if not uuid:
- return path
- # Search up the directory hierarchy until we find a different UUID.
- last_path = None
- while True:
- if uuid != SVN.find_uuid(path):
- return last_path
- last_path = path
- (path, last_component) = os.path.split(path)
- if last_path == path:
- return None
-
- @staticmethod
- def commit_success_regexp():
- return "^Committed revision (?P<svn_revision>\d+)\.$"
-
- def svn_version(self):
- if not self.cached_version:
- self.cached_version = run_command(['svn', '--version', '--quiet'])
-
- return self.cached_version
-
- def working_directory_is_clean(self):
- return run_command(['svn', 'diff']) == ""
-
- def clean_working_directory(self):
- run_command(['svn', 'revert', '-R', '.'])
-
- def status_command(self):
- return ['svn', 'status']
-
- def changed_files(self):
- if self.svn_version() > "1.6":
- status_regexp = "^(?P<status>[ACDMR]).{6} (?P<filename>.+)$"
- else:
- status_regexp = "^(?P<status>[ACDMR]).{5} (?P<filename>.+)$"
- return self.run_status_and_extract_filenames(self.status_command(), status_regexp)
-
- @staticmethod
- def supports_local_commits():
- return False
-
- def display_name(self):
- return "svn"
-
- def create_patch(self):
- return run_command(self.script_path("svn-create-patch"), cwd=self.checkout_root, return_stderr=False)
-
- def diff_for_revision(self, revision):
- return run_command(['svn', 'diff', '-c', str(revision)])
-
- def _repository_url(self):
- return self.value_from_svn_info(self.checkout_root, 'URL')
-
- def apply_reverse_diff(self, revision):
- # '-c -revision' applies the inverse diff of 'revision'
- svn_merge_args = ['svn', 'merge', '--non-interactive', '-c', '-%s' % revision, self._repository_url()]
- log("WARNING: svn merge has been known to take more than 10 minutes to complete. It is recommended you use git for rollouts.")
- log("Running '%s'" % " ".join(svn_merge_args))
- run_command(svn_merge_args)
-
- def revert_files(self, file_paths):
- run_command(['svn', 'revert'] + file_paths)
-
- def commit_with_message(self, message):
- if self.dryrun:
- # Return a string which looks like a commit so that things which parse this output will succeed.
- return "Dry run, no commit.\nCommitted revision 0."
- return run_command(['svn', 'commit', '-m', message], error_handler=commit_error_handler)
-
- def svn_commit_log(self, svn_revision):
- svn_revision = self.strip_r_from_svn_revision(str(svn_revision))
- return run_command(['svn', 'log', '--non-interactive', '--revision', svn_revision]);
-
- def last_svn_commit_log(self):
- # BASE is the checkout revision, HEAD is the remote repository revision
- # http://svnbook.red-bean.com/en/1.0/ch03s03.html
- return self.svn_commit_log('BASE')
-
-# All git-specific logic should go here.
-class Git(SCM):
- def __init__(self, cwd, dryrun=False):
- SCM.__init__(self, cwd, dryrun)
-
- @classmethod
- def in_working_directory(cls, path):
- return run_command(['git', 'rev-parse', '--is-inside-work-tree'], cwd=path, error_handler=Executive.ignore_error).rstrip() == "true"
-
- @classmethod
- def find_checkout_root(cls, path):
- # "git rev-parse --show-cdup" would be another way to get to the root
- (checkout_root, dot_git) = os.path.split(run_command(['git', 'rev-parse', '--git-dir'], cwd=path))
- # If we were using 2.6 # checkout_root = os.path.relpath(checkout_root, path)
- if not os.path.isabs(checkout_root): # Sometimes git returns relative paths
- checkout_root = os.path.join(path, checkout_root)
- return checkout_root
-
- @staticmethod
- def commit_success_regexp():
- return "^Committed r(?P<svn_revision>\d+)$"
-
-
- def discard_local_commits(self):
- run_command(['git', 'reset', '--hard', 'trunk'])
-
- def local_commits(self):
- return run_command(['git', 'log', '--pretty=oneline', 'HEAD...trunk']).splitlines()
-
- def rebase_in_progress(self):
- return os.path.exists(os.path.join(self.checkout_root, '.git/rebase-apply'))
-
- def working_directory_is_clean(self):
- return run_command(['git', 'diff-index', 'HEAD']) == ""
-
- def clean_working_directory(self):
- # Could run git clean here too, but that wouldn't match working_directory_is_clean
- run_command(['git', 'reset', '--hard', 'HEAD'])
- # Aborting rebase even though this does not match working_directory_is_clean
- if self.rebase_in_progress():
- run_command(['git', 'rebase', '--abort'])
-
- def status_command(self):
- return ['git', 'status']
-
- def changed_files(self):
- status_command = ['git', 'diff', '-r', '--name-status', '-C', '-M', 'HEAD']
- status_regexp = '^(?P<status>[ADM])\t(?P<filename>.+)$'
- return self.run_status_and_extract_filenames(status_command, status_regexp)
-
- @staticmethod
- def supports_local_commits():
- return True
-
- def display_name(self):
- return "git"
-
- def create_patch(self):
- return run_command(['git', 'diff', '--binary', 'HEAD'])
-
- @classmethod
- def git_commit_from_svn_revision(cls, revision):
- # git svn find-rev always exits 0, even when the revision is not found.
- return run_command(['git', 'svn', 'find-rev', 'r%s' % revision]).rstrip()
-
- def diff_for_revision(self, revision):
- git_commit = self.git_commit_from_svn_revision(revision)
- return self.create_patch_from_local_commit(git_commit)
-
- def apply_reverse_diff(self, revision):
- # Assume the revision is an svn revision.
- git_commit = self.git_commit_from_svn_revision(revision)
- if not git_commit:
- raise ScriptError(message='Failed to find git commit for revision %s, git svn log output: "%s"' % (revision, git_commit))
-
- # I think this will always fail due to ChangeLogs.
- # FIXME: We need to detec specific failure conditions and handle them.
- run_command(['git', 'revert', '--no-commit', git_commit], error_handler=Executive.ignore_error)
-
- # Fix any ChangeLogs if necessary.
- changelog_paths = self.modified_changelogs()
- if len(changelog_paths):
- run_command([self.script_path('resolve-ChangeLogs')] + changelog_paths)
-
- def revert_files(self, file_paths):
- run_command(['git', 'checkout', 'HEAD'] + file_paths)
-
- def commit_with_message(self, message):
- self.commit_locally_with_message(message)
- return self.push_local_commits_to_server()
-
- def svn_commit_log(self, svn_revision):
- svn_revision = self.strip_r_from_svn_revision(svn_revision)
- return run_command(['git', 'svn', 'log', '-r', svn_revision])
-
- def last_svn_commit_log(self):
- return run_command(['git', 'svn', 'log', '--limit=1'])
-
- # Git-specific methods:
-
- def create_patch_from_local_commit(self, commit_id):
- return run_command(['git', 'diff', '--binary', commit_id + "^.." + commit_id])
-
- def create_patch_since_local_commit(self, commit_id):
- return run_command(['git', 'diff', '--binary', commit_id])
-
- def commit_locally_with_message(self, message):
- run_command(['git', 'commit', '--all', '-F', '-'], input=message)
-
- def push_local_commits_to_server(self):
- if self.dryrun:
- # Return a string which looks like a commit so that things which parse this output will succeed.
- return "Dry run, no remote commit.\nCommitted r0"
- return run_command(['git', 'svn', 'dcommit'], error_handler=commit_error_handler)
-
- # This function supports the following argument formats:
- # no args : rev-list trunk..HEAD
- # A..B : rev-list A..B
- # A...B : error!
- # A B : [A, B] (different from git diff, which would use "rev-list A..B")
- def commit_ids_from_commitish_arguments(self, args):
- if not len(args):
- # FIXME: trunk is not always the remote branch name, need a way to detect the name.
- args.append('trunk..HEAD')
-
- commit_ids = []
- for commitish in args:
- if '...' in commitish:
- raise ScriptError(message="'...' is not supported (found in '%s'). Did you mean '..'?" % commitish)
- elif '..' in commitish:
- commit_ids += reversed(run_command(['git', 'rev-list', commitish]).splitlines())
- else:
- # Turn single commits or branch or tag names into commit ids.
- commit_ids += run_command(['git', 'rev-parse', '--revs-only', commitish]).splitlines()
- return commit_ids
-
- def commit_message_for_local_commit(self, commit_id):
- commit_lines = run_command(['git', 'cat-file', 'commit', commit_id]).splitlines()
-
- # Skip the git headers.
- first_line_after_headers = 0
- for line in commit_lines:
- first_line_after_headers += 1
- if line == "":
- break
- return CommitMessage(commit_lines[first_line_after_headers:])
-
- def files_changed_summary_for_commit(self, commit_id):
- return run_command(['git', 'diff-tree', '--shortstat', '--no-commit-id', commit_id])
diff --git a/WebKitTools/Scripts/modules/scm_unittest.py b/WebKitTools/Scripts/modules/scm_unittest.py
deleted file mode 100644
index 8e82f3c..0000000
--- a/WebKitTools/Scripts/modules/scm_unittest.py
+++ /dev/null
@@ -1,594 +0,0 @@
-# Copyright (C) 2009 Google Inc. All rights reserved.
-# Copyright (C) 2009 Apple 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 base64
-import os
-import os.path
-import re
-import stat
-import subprocess
-import tempfile
-import unittest
-import urllib
-
-from datetime import date
-from modules.executive import Executive, run_command, ScriptError
-from modules.scm import detect_scm_system, SCM, CheckoutNeedsUpdate, commit_error_handler
-
-# Eventually we will want to write tests which work for both scms. (like update_webkit, changed_files, etc.)
-# Perhaps through some SCMTest base-class which both SVNTest and GitTest inherit from.
-
-# FIXME: This should be unified into one of the executive.py commands!
-def run_silent(args, cwd=None):
- process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
- process.communicate() # ignore output
- exit_code = process.wait()
- if exit_code:
- raise ScriptError('Failed to run "%s" exit_code: %d cwd: %s' % (args, exit_code, cwd))
-
-def write_into_file_at_path(file_path, contents):
- file = open(file_path, 'w')
- file.write(contents)
- file.close()
-
-def read_from_path(file_path):
- file = open(file_path, 'r')
- contents = file.read()
- file.close()
- return contents
-
-# Exists to share svn repository creation code between the git and svn tests
-class SVNTestRepository:
- @staticmethod
- def _setup_test_commits(test_object):
- # Add some test commits
- os.chdir(test_object.svn_checkout_path)
- test_file = open('test_file', 'w')
- test_file.write("test1")
- test_file.flush()
-
- run_command(['svn', 'add', 'test_file'])
- run_command(['svn', 'commit', '--quiet', '--message', 'initial commit'])
-
- test_file.write("test2")
- test_file.flush()
-
- run_command(['svn', 'commit', '--quiet', '--message', 'second commit'])
-
- test_file.write("test3\n")
- test_file.flush()
-
- run_command(['svn', 'commit', '--quiet', '--message', 'third commit'])
-
- test_file.write("test4\n")
- test_file.close()
-
- run_command(['svn', 'commit', '--quiet', '--message', 'fourth commit'])
-
- # svn does not seem to update after commit as I would expect.
- run_command(['svn', 'update'])
-
- @classmethod
- def setup(cls, test_object):
- # Create an test SVN repository
- test_object.svn_repo_path = tempfile.mkdtemp(suffix="svn_test_repo")
- test_object.svn_repo_url = "file://%s" % test_object.svn_repo_path # Not sure this will work on windows
- # git svn complains if we don't pass --pre-1.5-compatible, not sure why:
- # Expected FS format '2'; found format '3' at /usr/local/libexec/git-core//git-svn line 1477
- run_command(['svnadmin', 'create', '--pre-1.5-compatible', test_object.svn_repo_path])
-
- # Create a test svn checkout
- test_object.svn_checkout_path = tempfile.mkdtemp(suffix="svn_test_checkout")
- run_command(['svn', 'checkout', '--quiet', test_object.svn_repo_url, test_object.svn_checkout_path])
-
- cls._setup_test_commits(test_object)
-
- @classmethod
- def tear_down(cls, test_object):
- run_command(['rm', '-rf', test_object.svn_repo_path])
- run_command(['rm', '-rf', test_object.svn_checkout_path])
-
-# For testing the SCM baseclass directly.
-class SCMClassTests(unittest.TestCase):
- def setUp(self):
- self.dev_null = open(os.devnull, "w") # Used to make our Popen calls quiet.
-
- def tearDown(self):
- self.dev_null.close()
-
- def test_run_command_with_pipe(self):
- input_process = subprocess.Popen(['echo', 'foo\nbar'], stdout=subprocess.PIPE, stderr=self.dev_null)
- self.assertEqual(run_command(['grep', 'bar'], input=input_process.stdout), "bar\n")
-
- # Test the non-pipe case too:
- self.assertEqual(run_command(['grep', 'bar'], input="foo\nbar"), "bar\n")
-
- command_returns_non_zero = ['/bin/sh', '--invalid-option']
- # Test when the input pipe process fails.
- input_process = subprocess.Popen(command_returns_non_zero, stdout=subprocess.PIPE, stderr=self.dev_null)
- self.assertTrue(input_process.poll() != 0)
- self.assertRaises(ScriptError, run_command, ['grep', 'bar'], input=input_process.stdout)
-
- # Test when the run_command process fails.
- input_process = subprocess.Popen(['echo', 'foo\nbar'], stdout=subprocess.PIPE, stderr=self.dev_null) # grep shows usage and calls exit(2) when called w/o arguments.
- self.assertRaises(ScriptError, run_command, command_returns_non_zero, input=input_process.stdout)
-
- def test_error_handlers(self):
- git_failure_message="Merge conflict during commit: Your file or directory 'WebCore/ChangeLog' is probably out-of-date: resource out of date; try updating at /usr/local/libexec/git-core//git-svn line 469"
- svn_failure_message="""svn: Commit failed (details follow):
-svn: File or directory 'ChangeLog' is out of date; try updating
-svn: resource out of date; try updating
-"""
- command_does_not_exist = ['does_not_exist', 'invalid_option']
- self.assertRaises(OSError, run_command, command_does_not_exist)
- self.assertRaises(OSError, run_command, command_does_not_exist, error_handler=Executive.ignore_error)
-
- command_returns_non_zero = ['/bin/sh', '--invalid-option']
- self.assertRaises(ScriptError, run_command, command_returns_non_zero)
- # Check if returns error text:
- self.assertTrue(run_command(command_returns_non_zero, error_handler=Executive.ignore_error))
-
- self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=git_failure_message))
- self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=svn_failure_message))
- self.assertRaises(ScriptError, commit_error_handler, ScriptError(output='blah blah blah'))
-
-
-# GitTest and SVNTest inherit from this so any test_ methods here will be run once for this class and then once for each subclass.
-class SCMTest(unittest.TestCase):
- def _create_patch(self, patch_contents):
- patch_path = os.path.join(self.svn_checkout_path, 'patch.diff')
- write_into_file_at_path(patch_path, patch_contents)
- patch = {}
- patch['reviewer'] = 'Joe Cool'
- patch['bug_id'] = '12345'
- patch['url'] = 'file://%s' % urllib.pathname2url(patch_path)
- return patch
-
- def _setup_webkittools_scripts_symlink(self, local_scm):
- webkit_scm = detect_scm_system(os.path.dirname(os.path.abspath(__file__)))
- webkit_scripts_directory = webkit_scm.scripts_directory()
- local_scripts_directory = local_scm.scripts_directory()
- os.mkdir(os.path.dirname(local_scripts_directory))
- os.symlink(webkit_scripts_directory, local_scripts_directory)
-
- # Tests which both GitTest and SVNTest should run.
- # FIXME: There must be a simpler way to add these w/o adding a wrapper method to both subclasses
- def _shared_test_commit_with_message(self):
- write_into_file_at_path('test_file', 'more test content')
- commit_text = self.scm.commit_with_message('another test commit')
- self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '5')
-
- self.scm.dryrun = True
- write_into_file_at_path('test_file', 'still more test content')
- commit_text = self.scm.commit_with_message('yet another test commit')
- self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '0')
-
- def _shared_test_reverse_diff(self):
- self._setup_webkittools_scripts_symlink(self.scm) # Git's apply_reverse_diff uses resolve-ChangeLogs
- # Only test the simple case, as any other will end up with conflict markers.
- self.scm.apply_reverse_diff('4')
- self.assertEqual(read_from_path('test_file'), "test1test2test3\n")
-
- def _shared_test_diff_for_revision(self):
- # Patch formats are slightly different between svn and git, so just regexp for things we know should be there.
- r3_patch = self.scm.diff_for_revision(3)
- self.assertTrue(re.search('test3', r3_patch))
- self.assertFalse(re.search('test4', r3_patch))
- self.assertTrue(re.search('test2', r3_patch))
- self.assertTrue(re.search('test2', self.scm.diff_for_revision(2)))
-
- def _shared_test_svn_apply_git_patch(self):
- self._setup_webkittools_scripts_symlink(self.scm)
- git_binary_addition = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
-new file mode 100644
-index 0000000000000000000000000000000000000000..64a9532e7794fcd791f6f12157406d90
-60151690
-GIT binary patch
-literal 512
-zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c?
-zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap
-zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ
-zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A
-zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&)
-zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b
-zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB
-z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A at 16O26ud7H<QM=xl`toLKnz-3h at 9c9q&wm|X
-z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4
-ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H
-
-literal 0
-HcmV?d00001
-
-"""
- self.scm.apply_patch(self._create_patch(git_binary_addition))
- added = read_from_path('fizzbuzz7.gif')
- self.assertEqual(512, len(added))
- self.assertTrue(added.startswith('GIF89a'))
- self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
-
- # The file already exists.
- self.assertRaises(ScriptError, self.scm.apply_patch, self._create_patch(git_binary_addition))
-
- git_binary_modification = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
-index 64a9532e7794fcd791f6f12157406d9060151690..323fae03f4606ea9991df8befbb2fca7
-GIT binary patch
-literal 7
-OcmYex&reD$;sO8*F9L)B
-
-literal 512
-zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c?
-zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap
-zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ
-zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A
-zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&)
-zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b
-zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB
-z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A at 16O26ud7H<QM=xl`toLKnz-3h at 9c9q&wm|X
-z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4
-ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H
-
-"""
- self.scm.apply_patch(self._create_patch(git_binary_modification))
- modified = read_from_path('fizzbuzz7.gif')
- self.assertEqual('foobar\n', modified)
- self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
-
- # Applying the same modification should fail.
- self.assertRaises(ScriptError, self.scm.apply_patch, self._create_patch(git_binary_modification))
-
- git_binary_deletion = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
-deleted file mode 100644
-index 323fae0..0000000
-GIT binary patch
-literal 0
-HcmV?d00001
-
-literal 7
-OcmYex&reD$;sO8*F9L)B
-
-"""
- self.scm.apply_patch(self._create_patch(git_binary_deletion))
- self.assertFalse(os.path.exists('fizzbuzz7.gif'))
- self.assertFalse('fizzbuzz7.gif' in self.scm.changed_files())
-
- # Cannot delete again.
- self.assertRaises(ScriptError, self.scm.apply_patch, self._create_patch(git_binary_deletion))
-
-
-class SVNTest(SCMTest):
-
- @staticmethod
- def _set_date_and_reviewer(changelog_entry):
- # Joe Cool matches the reviewer set in SCMTest._create_patch
- changelog_entry = changelog_entry.replace('REVIEWER_HERE', 'Joe Cool')
- # svn-apply will update ChangeLog entries with today's date.
- return changelog_entry.replace('DATE_HERE', date.today().isoformat())
-
- def test_svn_apply(self):
- first_entry = """2009-10-26 Eric Seidel <eric at webkit.org>
-
- Reviewed by Foo Bar.
-
- Most awesome change ever.
-
- * scm_unittest.py:
-"""
- intermediate_entry = """2009-10-27 Eric Seidel <eric at webkit.org>
-
- Reviewed by Baz Bar.
-
- A more awesomer change yet!
-
- * scm_unittest.py:
-"""
- one_line_overlap_patch = """Index: ChangeLog
-===================================================================
---- ChangeLog (revision 5)
-+++ ChangeLog (working copy)
-@@ -1,5 +1,13 @@
- 2009-10-26 Eric Seidel <eric at webkit.org>
-
-+ Reviewed by NOBODY (OOPS!).
-+
-+ Second most awsome change ever.
-+
-+ * scm_unittest.py:
-+
-+2009-10-26 Eric Seidel <eric at webkit.org>
-+
- Reviewed by Foo Bar.
-
- Most awesome change ever.
-"""
- one_line_overlap_entry = """DATE_HERE Eric Seidel <eric at webkit.org>
-
- Reviewed by REVIEWER_HERE.
-
- Second most awsome change ever.
-
- * scm_unittest.py:
-"""
- two_line_overlap_patch = """Index: ChangeLog
-===================================================================
---- ChangeLog (revision 5)
-+++ ChangeLog (working copy)
-@@ -2,6 +2,14 @@
-
- Reviewed by Foo Bar.
-
-+ Second most awsome change ever.
-+
-+ * scm_unittest.py:
-+
-+2009-10-26 Eric Seidel <eric at webkit.org>
-+
-+ Reviewed by Foo Bar.
-+
- Most awesome change ever.
-
- * scm_unittest.py:
-"""
- two_line_overlap_entry = """DATE_HERE Eric Seidel <eric at webkit.org>
-
- Reviewed by Foo Bar.
-
- Second most awsome change ever.
-
- * scm_unittest.py:
-"""
- write_into_file_at_path('ChangeLog', first_entry)
- run_command(['svn', 'add', 'ChangeLog'])
- run_command(['svn', 'commit', '--quiet', '--message', 'ChangeLog commit'])
-
- # Patch files were created against just 'first_entry'.
- # Add a second commit to make svn-apply have to apply the patches with fuzz.
- changelog_contents = "%s\n%s" % (intermediate_entry, first_entry)
- write_into_file_at_path('ChangeLog', changelog_contents)
- run_command(['svn', 'commit', '--quiet', '--message', 'Intermediate commit'])
-
- self._setup_webkittools_scripts_symlink(self.scm)
- self.scm.apply_patch(self._create_patch(one_line_overlap_patch))
- expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(one_line_overlap_entry), changelog_contents)
- self.assertEquals(read_from_path('ChangeLog'), expected_changelog_contents)
-
- self.scm.revert_files(['ChangeLog'])
- self.scm.apply_patch(self._create_patch(two_line_overlap_patch))
- expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(two_line_overlap_entry), changelog_contents)
- self.assertEquals(read_from_path('ChangeLog'), expected_changelog_contents)
-
- def setUp(self):
- SVNTestRepository.setup(self)
- os.chdir(self.svn_checkout_path)
- self.scm = detect_scm_system(self.svn_checkout_path)
-
- def tearDown(self):
- SVNTestRepository.tear_down(self)
-
- def test_create_patch_is_full_patch(self):
- test_dir_path = os.path.join(self.svn_checkout_path, 'test_dir')
- os.mkdir(test_dir_path)
- test_file_path = os.path.join(test_dir_path, 'test_file2')
- write_into_file_at_path(test_file_path, 'test content')
- run_command(['svn', 'add', 'test_dir'])
-
- # create_patch depends on 'svn-create-patch', so make a dummy version.
- scripts_path = os.path.join(self.svn_checkout_path, 'WebKitTools', 'Scripts')
- os.makedirs(scripts_path)
- create_patch_path = os.path.join(scripts_path, 'svn-create-patch')
- write_into_file_at_path(create_patch_path, '#!/bin/sh\necho $PWD') # We could pass -n to prevent the \n, but not all echo accept -n.
- os.chmod(create_patch_path, stat.S_IXUSR | stat.S_IRUSR)
-
- # Change into our test directory and run the create_patch command.
- os.chdir(test_dir_path)
- scm = detect_scm_system(test_dir_path)
- self.assertEqual(scm.checkout_root, self.svn_checkout_path) # Sanity check that detection worked right.
- patch_contents = scm.create_patch()
- # Our fake 'svn-create-patch' returns $PWD instead of a patch, check that it was executed from the root of the repo.
- self.assertEqual("%s\n" % os.path.realpath(scm.checkout_root), patch_contents) # Add a \n because echo adds a \n.
-
- def test_detection(self):
- scm = detect_scm_system(self.svn_checkout_path)
- self.assertEqual(scm.display_name(), "svn")
- self.assertEqual(scm.supports_local_commits(), False)
-
- def test_apply_small_binary_patch(self):
- patch_contents = """Index: test_file.swf
-===================================================================
-Cannot display: file marked as a binary type.
-svn:mime-type = application/octet-stream
-
-Property changes on: test_file.swf
-___________________________________________________________________
-Name: svn:mime-type
- + application/octet-stream
-
-
-Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
-"""
- expected_contents = base64.b64decode("Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==")
- self._setup_webkittools_scripts_symlink(self.scm)
- patch_file = self._create_patch(patch_contents)
- self.scm.apply_patch(patch_file)
- actual_contents = read_from_path("test_file.swf")
- self.assertEqual(actual_contents, expected_contents)
-
- def test_apply_svn_patch(self):
- scm = detect_scm_system(self.svn_checkout_path)
- patch = self._create_patch(run_command(['svn', 'diff', '-r4:3']))
- self._setup_webkittools_scripts_symlink(scm)
- scm.apply_patch(patch)
-
- def test_apply_svn_patch_force(self):
- scm = detect_scm_system(self.svn_checkout_path)
- patch = self._create_patch(run_command(['svn', 'diff', '-r2:4']))
- self._setup_webkittools_scripts_symlink(scm)
- self.assertRaises(ScriptError, scm.apply_patch, patch, force=True)
-
- def test_commit_logs(self):
- # Commits have dates and usernames in them, so we can't just direct compare.
- self.assertTrue(re.search('fourth commit', self.scm.last_svn_commit_log()))
- self.assertTrue(re.search('second commit', self.scm.svn_commit_log(2)))
-
- def test_commit_text_parsing(self):
- self._shared_test_commit_with_message()
-
- def test_reverse_diff(self):
- self._shared_test_reverse_diff()
-
- def test_diff_for_revision(self):
- self._shared_test_diff_for_revision()
-
- def test_svn_apply_git_patch(self):
- self._shared_test_svn_apply_git_patch()
-
-class GitTest(SCMTest):
-
- def _setup_git_clone_of_svn_repository(self):
- self.git_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout")
- # --quiet doesn't make git svn silent, so we use run_silent to redirect output
- run_silent(['git', 'svn', '--quiet', 'clone', self.svn_repo_url, self.git_checkout_path])
-
- def _tear_down_git_clone_of_svn_repository(self):
- run_command(['rm', '-rf', self.git_checkout_path])
-
- def setUp(self):
- SVNTestRepository.setup(self)
- self._setup_git_clone_of_svn_repository()
- os.chdir(self.git_checkout_path)
- self.scm = detect_scm_system(self.git_checkout_path)
-
- def tearDown(self):
- SVNTestRepository.tear_down(self)
- self._tear_down_git_clone_of_svn_repository()
-
- def test_detection(self):
- scm = detect_scm_system(self.git_checkout_path)
- self.assertEqual(scm.display_name(), "git")
- self.assertEqual(scm.supports_local_commits(), True)
-
- def test_rebase_in_progress(self):
- svn_test_file = os.path.join(self.svn_checkout_path, 'test_file')
- write_into_file_at_path(svn_test_file, "svn_checkout")
- run_command(['svn', 'commit', '--message', 'commit to conflict with git commit'], cwd=self.svn_checkout_path)
-
- git_test_file = os.path.join(self.git_checkout_path, 'test_file')
- write_into_file_at_path(git_test_file, "git_checkout")
- run_command(['git', 'commit', '-a', '-m', 'commit to be thrown away by rebase abort'])
-
- # --quiet doesn't make git svn silent, so use run_silent to redirect output
- self.assertRaises(ScriptError, run_silent, ['git', 'svn', '--quiet', 'rebase']) # Will fail due to a conflict leaving us mid-rebase.
-
- scm = detect_scm_system(self.git_checkout_path)
- self.assertTrue(scm.rebase_in_progress())
-
- # Make sure our cleanup works.
- scm.clean_working_directory()
- self.assertFalse(scm.rebase_in_progress())
-
- # Make sure cleanup doesn't throw when no rebase is in progress.
- scm.clean_working_directory()
-
- def test_commitish_parsing(self):
- scm = detect_scm_system(self.git_checkout_path)
-
- # Multiple revisions are cherry-picked.
- self.assertEqual(len(scm.commit_ids_from_commitish_arguments(['HEAD~2'])), 1)
- self.assertEqual(len(scm.commit_ids_from_commitish_arguments(['HEAD', 'HEAD~2'])), 2)
-
- # ... is an invalid range specifier
- self.assertRaises(ScriptError, scm.commit_ids_from_commitish_arguments, ['trunk...HEAD'])
-
- def test_commitish_order(self):
- scm = detect_scm_system(self.git_checkout_path)
-
- commit_range = 'HEAD~3..HEAD'
-
- actual_commits = scm.commit_ids_from_commitish_arguments([commit_range])
- expected_commits = []
- expected_commits += reversed(run_command(['git', 'rev-list', commit_range]).splitlines())
-
- self.assertEqual(actual_commits, expected_commits)
-
- def test_apply_git_patch(self):
- scm = detect_scm_system(self.git_checkout_path)
- patch = self._create_patch(run_command(['git', 'diff', 'HEAD..HEAD^']))
- self._setup_webkittools_scripts_symlink(scm)
- scm.apply_patch(patch)
-
- def test_apply_git_patch_force(self):
- scm = detect_scm_system(self.git_checkout_path)
- patch = self._create_patch(run_command(['git', 'diff', 'HEAD~2..HEAD']))
- self._setup_webkittools_scripts_symlink(scm)
- self.assertRaises(ScriptError, scm.apply_patch, patch, force=True)
-
- def test_commit_text_parsing(self):
- self._shared_test_commit_with_message()
-
- def test_reverse_diff(self):
- self._shared_test_reverse_diff()
-
- def test_diff_for_revision(self):
- self._shared_test_diff_for_revision()
-
- def test_svn_apply_git_patch(self):
- self._shared_test_svn_apply_git_patch()
-
- def test_create_binary_patch(self):
- # Create a git binary patch and check the contents.
- scm = detect_scm_system(self.git_checkout_path)
- test_file_name = 'binary_file'
- test_file_path = os.path.join(self.git_checkout_path, test_file_name)
- file_contents = ''.join(map(chr, range(256)))
- write_into_file_at_path(test_file_path, file_contents)
- run_command(['git', 'add', test_file_name])
- patch = scm.create_patch()
- self.assertTrue(re.search(r'\nliteral 0\n', patch))
- self.assertTrue(re.search(r'\nliteral 256\n', patch))
-
- # Check if we can apply the created patch.
- run_command(['git', 'rm', '-f', test_file_name])
- self._setup_webkittools_scripts_symlink(scm)
- self.scm.apply_patch(self._create_patch(patch))
- self.assertEqual(file_contents, read_from_path(test_file_path))
-
- # Check if we can create a patch from a local commit.
- write_into_file_at_path(test_file_path, file_contents)
- run_command(['git', 'add', test_file_name])
- run_command(['git', 'commit', '-m', 'binary diff'])
- patch_from_local_commit = scm.create_patch_from_local_commit('HEAD')
- self.assertTrue(re.search(r'\nliteral 0\n', patch_from_local_commit))
- self.assertTrue(re.search(r'\nliteral 256\n', patch_from_local_commit))
- patch_since_local_commit = scm.create_patch_since_local_commit('HEAD^1')
- self.assertTrue(re.search(r'\nliteral 0\n', patch_since_local_commit))
- self.assertTrue(re.search(r'\nliteral 256\n', patch_since_local_commit))
- self.assertEqual(patch_from_local_commit, patch_since_local_commit)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/WebKitTools/Scripts/modules/statusbot.py b/WebKitTools/Scripts/modules/statusbot.py
deleted file mode 100644
index 2928a4e..0000000
--- a/WebKitTools/Scripts/modules/statusbot.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# Copyright (C) 2009 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.
-#
-# WebKit's Python module for interacting with the Commit Queue status page.
-
-from modules.webkit_logging import log
-from modules.webkit_mechanize import Browser
-
-# WebKit includes a built copy of BeautifulSoup in Scripts/modules
-# so this import should always succeed.
-from .BeautifulSoup import BeautifulSoup
-
-import urllib2
-
-
-class StatusBot:
- default_host = "webkit-commit-queue.appspot.com"
-
- def __init__(self, host=default_host):
- self.set_host(host)
- self.browser = Browser()
-
- def set_host(self, host):
- self.statusbot_host = host
- self.statusbot_server_url = "http://%s" % self.statusbot_host
-
- def results_url_for_status(self, status_id):
- return "%s/results/%s" % (self.statusbot_server_url, status_id)
-
- def update_status(self, queue_name, status, patch=None, results_file=None):
- # During unit testing, statusbot_host is None
- if not self.statusbot_host:
- return
-
- log(status)
- update_status_url = "%s/update-status" % self.statusbot_server_url
- self.browser.open(update_status_url)
- self.browser.select_form(name="update_status")
- self.browser['queue_name'] = queue_name
- if patch:
- if patch.get('bug_id'):
- self.browser['bug_id'] = str(patch['bug_id'])
- if patch.get('id'):
- self.browser['patch_id'] = str(patch['id'])
- self.browser['status'] = status
- if results_file:
- self.browser.add_file(results_file, "text/plain", "results.txt", 'results_file')
- response = self.browser.submit()
- return response.read() # This is the id of the newly created status object.
-
- def patch_status(self, queue_name, patch_id):
- update_status_url = "%s/patch-status/%s/%s" % (self.statusbot_server_url, queue_name, patch_id)
- try:
- return urllib2.urlopen(update_status_url).read()
- except urllib2.HTTPError, e:
- if e.code == 404:
- return None
- raise e
diff --git a/WebKitTools/Scripts/modules/stepsequence.py b/WebKitTools/Scripts/modules/stepsequence.py
deleted file mode 100644
index 39752c1..0000000
--- a/WebKitTools/Scripts/modules/stepsequence.py
+++ /dev/null
@@ -1,76 +0,0 @@
-# Copyright (C) 2009 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.
-
-from modules.buildsteps import CommandOptions
-from modules.executive import ScriptError
-from modules.webkit_logging import log
-from modules.scm import CheckoutNeedsUpdate
-from modules.queueengine import QueueEngine
-
-
-class StepSequenceErrorHandler():
- @classmethod
- def handle_script_error(cls, tool, patch, script_error):
- raise NotImplementedError, "subclasses must implement"
-
-
-class StepSequence(object):
- def __init__(self, steps):
- self._steps = steps or []
-
- def options(self):
- collected_options = [
- CommandOptions.parent_command,
- CommandOptions.quiet,
- ]
- for step in self._steps:
- collected_options = collected_options + step.options()
- # Remove duplicates.
- collected_options = sorted(set(collected_options))
- return collected_options
-
- def _run(self, tool, options, state):
- for step in self._steps:
- step(tool, options).run(state)
-
- def run_and_handle_errors(self, tool, options, state=None):
- if not state:
- state = {}
- try:
- self._run(tool, options, state)
- except CheckoutNeedsUpdate, e:
- log("Commit failed because the checkout is out of date. Please update and try again.")
- log("You can pass --no-build to skip building/testing after update if you believe the new commits did not affect the results.")
- QueueEngine.exit_after_handled_error(e)
- except ScriptError, e:
- if not options.quiet:
- log(e.message_with_output())
- if options.parent_command:
- command = tool.command_by_name(options.parent_command)
- command.handle_script_error(tool, state, e)
- QueueEngine.exit_after_handled_error(e)
diff --git a/WebKitTools/Scripts/modules/webkit_logging_unittest.py b/WebKitTools/Scripts/modules/webkit_logging_unittest.py
deleted file mode 100644
index cbeaa4b..0000000
--- a/WebKitTools/Scripts/modules/webkit_logging_unittest.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# Copyright (C) 2009 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 os
-import subprocess
-import StringIO
-import tempfile
-import unittest
-
-from modules.executive import ScriptError
-from modules.webkit_logging import *
-
-class LoggingTest(unittest.TestCase):
-
- def assert_log_equals(self, log_input, expected_output):
- original_stderr = sys.stderr
- test_stderr = StringIO.StringIO()
- sys.stderr = test_stderr
-
- try:
- log(log_input)
- actual_output = test_stderr.getvalue()
- finally:
- original_stderr = original_stderr
-
- self.assertEquals(actual_output, expected_output, "log(\"%s\") expected: %s actual: %s" % (log_input, expected_output, actual_output))
-
- def test_log(self):
- self.assert_log_equals("test", "test\n")
-
- # Test that log() does not throw an exception when passed an object instead of a string.
- self.assert_log_equals(ScriptError(message="ScriptError"), "ScriptError\n")
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/WebKitTools/Scripts/modules/webkitport_unittest.py b/WebKitTools/Scripts/modules/webkitport_unittest.py
deleted file mode 100644
index 3430755..0000000
--- a/WebKitTools/Scripts/modules/webkitport_unittest.py
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009, 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 modules.webkitport import WebKitPort, MacPort, GtkPort, QtPort, ChromiumPort
-
-class WebKitPortTest(unittest.TestCase):
- def test_mac_port(self):
- self.assertEquals(MacPort.name(), "Mac")
- self.assertEquals(MacPort.flag(), "--port=mac")
- self.assertEquals(MacPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests")])
- self.assertEquals(MacPort.build_webkit_command(), [WebKitPort.script_path("build-webkit")])
- self.assertEquals(MacPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug"])
- self.assertEquals(MacPort.build_webkit_command(build_style="release"), [WebKitPort.script_path("build-webkit"), "--release"])
-
- def test_gtk_port(self):
- self.assertEquals(GtkPort.name(), "Gtk")
- self.assertEquals(GtkPort.flag(), "--port=gtk")
- self.assertEquals(GtkPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests"), "--gtk"])
- self.assertEquals(GtkPort.build_webkit_command(), [WebKitPort.script_path("build-webkit"), "--gtk"])
- self.assertEquals(GtkPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug", "--gtk"])
-
- def test_qt_port(self):
- self.assertEquals(QtPort.name(), "Qt")
- self.assertEquals(QtPort.flag(), "--port=qt")
- self.assertEquals(QtPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests")])
- self.assertEquals(QtPort.build_webkit_command(), [WebKitPort.script_path("build-webkit"), "--qt", '--makeargs="-j8"'])
- self.assertEquals(QtPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug", "--qt", '--makeargs="-j8"'])
-
- 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.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"])
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/WebKitTools/Scripts/test-webkit-python b/WebKitTools/Scripts/test-webkit-python
deleted file mode 100755
index 19284e5..0000000
--- a/WebKitTools/Scripts/test-webkit-python
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2009 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 sys
-import unittest
-
-from modules.bugzilla_unittest import *
-from modules.buildbot_unittest import *
-from modules.buildsteps_unittest import *
-from modules.changelogs_unittest import *
-from modules.commands.download_unittest import *
-from modules.commands.early_warning_system_unittest import *
-from modules.commands.upload_unittest import *
-from modules.commands.queries_unittest import *
-from modules.commands.queues_unittest import *
-from modules.committers_unittest import *
-from modules.credentials_unittest import *
-from modules.cpp_style_unittest import *
-from modules.diff_parser_unittest import *
-from modules.executive_unittest import *
-from modules.multicommandtool_unittest import *
-from modules.queueengine_unittest import *
-from modules.style_unittest import *
-from modules.text_style_unittest import *
-from modules.webkit_logging_unittest import *
-from modules.webkitport_unittest import *
-
-if __name__ == "__main__":
- # FIXME: This is a hack, but I'm tired of commenting out the test.
- # See https://bugs.webkit.org/show_bug.cgi?id=31818
- if len(sys.argv) > 1 and sys.argv[1] == "--all":
- sys.argv.remove("--all")
- from modules.scm_unittest import *
-
- unittest.main()
diff --git a/WebKitTools/Scripts/test-webkitpy b/WebKitTools/Scripts/test-webkitpy
new file mode 100755
index 0000000..e919c81
--- /dev/null
+++ b/WebKitTools/Scripts/test-webkitpy
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+# Copyright (c) 2009 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 sys
+import unittest
+
+from webkitpy.bugzilla_unittest import *
+from webkitpy.buildbot_unittest import *
+from webkitpy.buildsteps_unittest import *
+from webkitpy.changelogs_unittest import *
+from webkitpy.commands.download_unittest import *
+from webkitpy.commands.early_warning_system_unittest import *
+from webkitpy.commands.upload_unittest import *
+from webkitpy.commands.queries_unittest import *
+from webkitpy.commands.queues_unittest import *
+from webkitpy.committers_unittest import *
+from webkitpy.credentials_unittest import *
+from webkitpy.cpp_style_unittest import *
+from webkitpy.diff_parser_unittest import *
+from webkitpy.executive_unittest import *
+from webkitpy.multicommandtool_unittest import *
+from webkitpy.queueengine_unittest import *
+from webkitpy.style_unittest import *
+from webkitpy.text_style_unittest import *
+from webkitpy.webkit_logging_unittest import *
+from webkitpy.webkitport_unittest import *
+
+if __name__ == "__main__":
+ # FIXME: This is a hack, but I'm tired of commenting out the test.
+ # See https://bugs.webkit.org/show_bug.cgi?id=31818
+ if len(sys.argv) > 1 and sys.argv[1] == "--all":
+ sys.argv.remove("--all")
+ from webkitpy.scm_unittest import *
+
+ unittest.main()
diff --git a/WebKitTools/Scripts/validate-committer-lists b/WebKitTools/Scripts/validate-committer-lists
index b41d9bb..e39692e 100755
--- a/WebKitTools/Scripts/validate-committer-lists
+++ b/WebKitTools/Scripts/validate-committer-lists
@@ -36,13 +36,13 @@ import subprocess
import re
import urllib2
from datetime import date, datetime, timedelta
-from modules.committers import CommitterList
-from modules.logging import log, error
-from modules.scm import Git
+from webkitpy.committers import CommitterList
+from webkitpy.logging import log, error
+from webkitpy.scm import Git
-# WebKit includes a built copy of BeautifulSoup in Scripts/modules
+# WebKit includes a built copy of BeautifulSoup in Scripts/webkitpy
# so this import should always succeed.
-from modules.BeautifulSoup import BeautifulSoup
+from webkitpy.BeautifulSoup import BeautifulSoup
def print_list_if_non_empty(title, list_to_print):
if not list_to_print:
diff --git a/WebKitTools/Scripts/modules/BeautifulSoup.py b/WebKitTools/Scripts/webkitpy/BeautifulSoup.py
similarity index 100%
rename from WebKitTools/Scripts/modules/BeautifulSoup.py
rename to WebKitTools/Scripts/webkitpy/BeautifulSoup.py
diff --git a/WebKitTools/Scripts/modules/__init__.py b/WebKitTools/Scripts/webkitpy/__init__.py
similarity index 100%
rename from WebKitTools/Scripts/modules/__init__.py
rename to WebKitTools/Scripts/webkitpy/__init__.py
diff --git a/WebKitTools/Scripts/webkitpy/bugzilla.py b/WebKitTools/Scripts/webkitpy/bugzilla.py
new file mode 100644
index 0000000..4f73d59
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/bugzilla.py
@@ -0,0 +1,563 @@
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple 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.
+#
+# WebKit's Python module for interacting with Bugzilla
+
+import re
+import subprocess
+
+from datetime import datetime # used in timestamp()
+
+# Import WebKit-specific modules.
+from webkitpy.webkit_logging import error, log
+from webkitpy.committers import CommitterList
+from webkitpy.credentials import Credentials
+
+# WebKit includes a built copy of BeautifulSoup in Scripts/modules
+# so this import should always succeed.
+from .BeautifulSoup import BeautifulSoup, SoupStrainer
+
+from webkitpy.webkit_mechanize import Browser
+
+def parse_bug_id(message):
+ match = re.search("http\://webkit\.org/b/(?P<bug_id>\d+)", message)
+ if match:
+ return int(match.group('bug_id'))
+ match = re.search(Bugzilla.bug_server_regex + "show_bug\.cgi\?id=(?P<bug_id>\d+)", message)
+ if match:
+ return int(match.group('bug_id'))
+ return None
+
+
+def timestamp():
+ return datetime.now().strftime("%Y%m%d%H%M%S")
+
+
+# FIXME: This class is kinda a hack for now. It exists so we have one place
+# to hold bug logic, even if much of the code deals with dictionaries still.
+class Bug(object):
+ def __init__(self, bug_dictionary):
+ self.bug_dictionary = bug_dictionary
+
+ def assigned_to_email(self):
+ return self.bug_dictionary["assigned_to_email"]
+
+ # Rarely do we actually want obsolete attachments
+ def attachments(self, include_obsolete=False):
+ if include_obsolete:
+ return self.bug_dictionary["attachments"][:] # Return a copy in either case.
+ return [attachment for attachment in self.bug_dictionary["attachments"] if not attachment["is_obsolete"]]
+
+ def patches(self, include_obsolete=False):
+ return [patch for patch in self.attachments(include_obsolete) if patch["is_patch"]]
+
+ def unreviewed_patches(self):
+ return [patch for patch in self.patches() if patch.get("review") == "?"]
+
+
+# A container for all of the logic for making a parsing buzilla queries.
+class BugzillaQueries(object):
+ def __init__(self, bugzilla):
+ self.bugzilla = bugzilla
+
+ def _load_query(self, query):
+ full_url = "%s%s" % (self.bugzilla.bug_server_url, query)
+ return self.bugzilla.browser.open(full_url)
+
+ def _fetch_bug_ids_advanced_query(self, query):
+ soup = BeautifulSoup(self._load_query(query))
+ # The contents of the <a> inside the cells in the first column happen to be the bug id.
+ return [int(bug_link_cell.find("a").string) for bug_link_cell in soup('td', "first-child")]
+
+ def _parse_attachment_ids_request_query(self, page):
+ digits = re.compile("\d+")
+ attachment_href = re.compile("attachment.cgi\?id=\d+&action=review")
+ attachment_links = SoupStrainer("a", href=attachment_href)
+ return [int(digits.search(tag["href"]).group(0)) for tag in BeautifulSoup(page, parseOnlyThese=attachment_links)]
+
+ def _fetch_attachment_ids_request_query(self, query):
+ return self._parse_attachment_ids_request_query(self._load_query(query))
+
+ # List of all r+'d bugs.
+ def fetch_bug_ids_from_pending_commit_list(self):
+ needs_commit_query_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%2B"
+ return self._fetch_bug_ids_advanced_query(needs_commit_query_url)
+
+ def fetch_patches_from_pending_commit_list(self):
+ # FIXME: This should not have to go through self.bugzilla
+ return sum([self.bugzilla.fetch_reviewed_patches_from_bug(bug_id) for bug_id in self.fetch_bug_ids_from_pending_commit_list()], [])
+
+ def fetch_bug_ids_from_commit_queue(self):
+ commit_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=commit-queue%2B"
+ return self._fetch_bug_ids_advanced_query(commit_queue_url)
+
+ def fetch_patches_from_commit_queue(self, reject_invalid_patches=False):
+ # FIXME: Once reject_invalid_patches is moved out of this function this becomes a simple list comprehension using fetch_bug_ids_from_commit_queue.
+ patches_to_land = []
+ for bug_id in self.fetch_bug_ids_from_commit_queue():
+ # FIXME: This should not have to go through self.bugzilla
+ patches = self.bugzilla.fetch_commit_queue_patches_from_bug(bug_id, reject_invalid_patches)
+ patches_to_land += patches
+ return patches_to_land
+
+ def _fetch_bug_ids_from_review_queue(self):
+ 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)
+
+ def fetch_patches_from_review_queue(self, limit=None):
+ # FIXME: We should probably have a self.fetch_bug to minimize the number of self.bugzilla calls.
+ return sum([self.bugzilla.fetch_bug(bug_id).unreviewed_patches() for bug_id in self._fetch_bug_ids_from_review_queue()[:limit]], []) # [:None] returns the whole array.
+
+ # 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
+ 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)
+
+
+class Bugzilla(object):
+ def __init__(self, dryrun=False, committers=CommitterList()):
+ self.dryrun = dryrun
+ self.authenticated = False
+ self.queries = BugzillaQueries(self)
+
+ # FIXME: We should use some sort of Browser mock object when in dryrun mode (to prevent any mistakes).
+ self.browser = Browser()
+ # Ignore bugs.webkit.org/robots.txt until we fix it to allow this script
+ self.browser.set_handle_robots(False)
+ self.committers = committers
+
+ # FIXME: Much of this should go into some sort of config module:
+ bug_server_host = "bugs.webkit.org"
+ bug_server_regex = "https?://%s/" % re.sub('\.', '\\.', bug_server_host)
+ bug_server_url = "https://%s/" % bug_server_host
+ unassigned_email = "webkit-unassigned at lists.webkit.org"
+
+ def bug_url_for_bug_id(self, bug_id, xml=False):
+ content_type = "&ctype=xml" if xml else ""
+ return "%sshow_bug.cgi?id=%s%s" % (self.bug_server_url, bug_id, content_type)
+
+ def short_bug_url_for_bug_id(self, bug_id):
+ return "http://webkit.org/b/%s" % bug_id
+
+ def attachment_url_for_id(self, attachment_id, action="view"):
+ action_param = ""
+ if action and action != "view":
+ action_param = "&action=%s" % action
+ return "%sattachment.cgi?id=%s%s" % (self.bug_server_url, attachment_id, action_param)
+
+ def _parse_attachment_flag(self, element, flag_name, attachment, result_key):
+ flag = element.find('flag', attrs={'name' : flag_name})
+ if flag:
+ attachment[flag_name] = flag['status']
+ if flag['status'] == '+':
+ attachment[result_key] = flag['setter']
+
+ def _parse_attachment_element(self, element, bug_id):
+ attachment = {}
+ attachment['bug_id'] = bug_id
+ attachment['is_obsolete'] = (element.has_key('isobsolete') and element['isobsolete'] == "1")
+ attachment['is_patch'] = (element.has_key('ispatch') and element['ispatch'] == "1")
+ attachment['id'] = int(element.find('attachid').string)
+ attachment['url'] = self.attachment_url_for_id(attachment['id'])
+ attachment['name'] = unicode(element.find('desc').string)
+ attachment['attacher_email'] = str(element.find('attacher').string)
+ attachment['type'] = str(element.find('type').string)
+ self._parse_attachment_flag(element, 'review', attachment, 'reviewer_email')
+ self._parse_attachment_flag(element, 'commit-queue', attachment, 'committer_email')
+ return attachment
+
+ def _parse_bug_page(self, page):
+ soup = BeautifulSoup(page)
+ bug = {}
+ bug["id"] = int(soup.find("bug_id").string)
+ bug["title"] = unicode(soup.find("short_desc").string)
+ bug["reporter_email"] = str(soup.find("reporter").string)
+ bug["assigned_to_email"] = str(soup.find("assigned_to").string)
+ bug["cc_emails"] = [str(element.string) for element in soup.findAll('cc')]
+ bug["attachments"] = [self._parse_attachment_element(element, bug["id"]) for element in soup.findAll('attachment')]
+ return bug
+
+ # Makes testing fetch_*_from_bug() possible until we have a better BugzillaNetwork abstration.
+ def _fetch_bug_page(self, bug_id):
+ bug_url = self.bug_url_for_bug_id(bug_id, xml=True)
+ log("Fetching: %s" % bug_url)
+ return self.browser.open(bug_url)
+
+ def fetch_bug_dictionary(self, bug_id):
+ return self._parse_bug_page(self._fetch_bug_page(bug_id))
+
+ # FIXME: A BugzillaCache object should provide all these fetch_ methods.
+ def fetch_bug(self, bug_id):
+ return Bug(self.fetch_bug_dictionary(bug_id))
+
+ def _parse_bug_id_from_attachment_page(self, page):
+ up_link = BeautifulSoup(page).find('link', rel='Up') # The "Up" relation happens to point to the bug.
+ if not up_link:
+ return None # This attachment does not exist (or you don't have permissions to view it).
+ match = re.search("show_bug.cgi\?id=(?P<bug_id>\d+)", up_link['href'])
+ return int(match.group('bug_id'))
+
+ def bug_id_for_attachment_id(self, attachment_id):
+ attachment_url = self.attachment_url_for_id(attachment_id, 'edit')
+ log("Fetching: %s" % attachment_url)
+ page = self.browser.open(attachment_url)
+ return self._parse_bug_id_from_attachment_page(page)
+
+ # This should really return an Attachment object
+ # which can lazily fetch any missing data.
+ def fetch_attachment(self, attachment_id):
+ # We could grab all the attachment details off of the attachment edit page
+ # but we already have working code to do so off of the bugs page, so re-use that.
+ bug_id = self.bug_id_for_attachment_id(attachment_id)
+ if not bug_id:
+ return None
+ for attachment in self.fetch_bug(bug_id).attachments(include_obsolete=True):
+ # FIXME: Once we have a real Attachment class we shouldn't paper over this possible comparison failure
+ # and we should remove the int() == int() hacks and leave it just ==.
+ if int(attachment['id']) == int(attachment_id):
+ self._validate_committer_and_reviewer(attachment)
+ return attachment
+ return None # This should never be hit.
+
+ # fetch_patches_from_bug exists until we expose a Bug class outside of bugzilla.py
+ def fetch_patches_from_bug(self, bug_id):
+ return self.fetch_bug(bug_id).patches()
+
+ # _view_source_link belongs in some sort of webkit_config.py module.
+ def _view_source_link(self, local_path):
+ return "http://trac.webkit.org/browser/trunk/%s" % local_path
+
+ def _flag_permission_rejection_message(self, setter_email, flag_name):
+ committer_list = "WebKitTools/Scripts/webkitpy/committers.py" # This could be computed from CommitterList.__file__
+ contribution_guidlines_url = "http://webkit.org/coding/contributing.html" # Should come from some webkit_config.py
+ queue_administrator = "eseidel at chromium.org" # This could be queried from the status_bot.
+ queue_name = "commit-queue" # This could be queried from the tool.
+ rejection_message = "%s does not have %s permissions according to %s." % (setter_email, flag_name, self._view_source_link(committer_list))
+ rejection_message += "\n\n- If you do not have %s rights please read %s for instructions on how to use bugzilla flags." % (flag_name, contribution_guidlines_url)
+ rejection_message += "\n\n- If you have %s rights please correct the error in %s by adding yourself to the file (no review needed)." % (flag_name, committer_list)
+ rejection_message += " Due to bug 30084 the %s will require a restart after your change." % queue_name
+ rejection_message += " Please contact %s to request a %s restart." % (queue_administrator, queue_name)
+ rejection_message += " After restart the %s will correctly respect your %s rights." % (queue_name, flag_name)
+ return rejection_message
+
+ def _validate_setter_email(self, patch, result_key, lookup_function, rejection_function, reject_invalid_patches):
+ setter_email = patch.get(result_key + '_email')
+ if not setter_email:
+ return None
+
+ committer = lookup_function(setter_email)
+ if committer:
+ patch[result_key] = committer.full_name
+ return patch[result_key]
+
+ if reject_invalid_patches:
+ rejection_function(patch['id'], self._flag_permission_rejection_message(setter_email, result_key))
+ else:
+ log("Warning, attachment %s on bug %s has invalid %s (%s)" % (patch['id'], patch['bug_id'], result_key, setter_email))
+ return None
+
+ def _validate_reviewer(self, patch, reject_invalid_patches):
+ return self._validate_setter_email(patch, 'reviewer', self.committers.reviewer_by_email, self.reject_patch_from_review_queue, reject_invalid_patches)
+
+ def _validate_committer(self, patch, reject_invalid_patches):
+ return self._validate_setter_email(patch, 'committer', self.committers.committer_by_email, self.reject_patch_from_commit_queue, reject_invalid_patches)
+
+ # FIXME: This is a hack until we have a real Attachment object.
+ # _validate_committer and _validate_reviewer fill in the 'reviewer' and 'committer'
+ # keys which other parts of the code expect to be filled in.
+ def _validate_committer_and_reviewer(self, patch):
+ self._validate_reviewer(patch, reject_invalid_patches=False)
+ self._validate_committer(patch, reject_invalid_patches=False)
+
+ # FIXME: fetch_reviewed_patches_from_bug and fetch_commit_queue_patches_from_bug
+ # should share more code and use list comprehensions.
+ def fetch_reviewed_patches_from_bug(self, bug_id, reject_invalid_patches=False):
+ reviewed_patches = []
+ for attachment in self.fetch_bug(bug_id).attachments():
+ if self._validate_reviewer(attachment, reject_invalid_patches):
+ reviewed_patches.append(attachment)
+ return reviewed_patches
+
+ def fetch_commit_queue_patches_from_bug(self, bug_id, reject_invalid_patches=False):
+ commit_queue_patches = []
+ for attachment in self.fetch_reviewed_patches_from_bug(bug_id, reject_invalid_patches):
+ if self._validate_committer(attachment, reject_invalid_patches):
+ commit_queue_patches.append(attachment)
+ return commit_queue_patches
+
+ def authenticate(self):
+ if self.authenticated:
+ return
+
+ if self.dryrun:
+ log("Skipping log in for dry run...")
+ self.authenticated = True
+ return
+
+ (username, password) = Credentials(self.bug_server_host, git_prefix="bugzilla").read_credentials()
+
+ log("Logging in as %s..." % username)
+ self.browser.open(self.bug_server_url + "index.cgi?GoAheadAndLogIn=1")
+ self.browser.select_form(name="login")
+ self.browser['Bugzilla_login'] = username
+ self.browser['Bugzilla_password'] = password
+ response = self.browser.submit()
+
+ match = re.search("<title>(.+?)</title>", response.read())
+ # If the resulting page has a title, and it contains the word "invalid" assume it's the login failure page.
+ if match and re.search("Invalid", match.group(1), re.IGNORECASE):
+ # FIXME: We could add the ability to try again on failure.
+ raise Exception("Bugzilla login failed: %s" % match.group(1))
+
+ self.authenticated = True
+
+ def _fill_attachment_form(self, description, patch_file_object, comment_text=None, mark_for_review=False, mark_for_commit_queue=False, bug_id=None):
+ self.browser['description'] = description
+ self.browser['ispatch'] = ("1",)
+ self.browser['flag_type-1'] = ('?',) if mark_for_review else ('X',)
+ self.browser['flag_type-3'] = ('?',) if mark_for_commit_queue else ('X',)
+ if bug_id:
+ patch_name = "bug-%s-%s.patch" % (bug_id, timestamp())
+ else:
+ patch_name ="%s.patch" % timestamp()
+ self.browser.add_file(patch_file_object, "text/plain", patch_name, 'data')
+
+ def add_patch_to_bug(self, bug_id, patch_file_object, description, comment_text=None, mark_for_review=False, mark_for_commit_queue=False):
+ self.authenticate()
+
+ log('Adding patch "%s" to bug %s' % (description, bug_id))
+ if self.dryrun:
+ log(comment_text)
+ return
+
+ self.browser.open("%sattachment.cgi?action=enter&bugid=%s" % (self.bug_server_url, bug_id))
+ self.browser.select_form(name="entryform")
+ self._fill_attachment_form(description, patch_file_object, mark_for_review=mark_for_review, mark_for_commit_queue=mark_for_commit_queue, bug_id=bug_id)
+ if comment_text:
+ log(comment_text)
+ self.browser['comment'] = comment_text
+ self.browser.submit()
+
+ def prompt_for_component(self, components):
+ log("Please pick a component:")
+ i = 0
+ for name in components:
+ i += 1
+ log("%2d. %s" % (i, name))
+ result = int(raw_input("Enter a number: ")) - 1
+ return components[result]
+
+ def _check_create_bug_response(self, response_html):
+ match = re.search("<title>Bug (?P<bug_id>\d+) Submitted</title>", response_html)
+ if match:
+ return match.group('bug_id')
+
+ match = re.search('<div id="bugzilla-body">(?P<error_message>.+)<div id="footer">', response_html, re.DOTALL)
+ error_message = "FAIL"
+ if match:
+ text_lines = BeautifulSoup(match.group('error_message')).findAll(text=True)
+ error_message = "\n" + '\n'.join([" " + line.strip() for line in text_lines if line.strip()])
+ raise Exception("Bug not created: %s" % error_message)
+
+ def create_bug(self, bug_title, bug_description, component=None, patch_file_object=None, patch_description=None, cc=None, mark_for_review=False, mark_for_commit_queue=False):
+ self.authenticate()
+
+ log('Creating bug with title "%s"' % bug_title)
+ if self.dryrun:
+ log(bug_description)
+ return
+
+ self.browser.open(self.bug_server_url + "enter_bug.cgi?product=WebKit")
+ self.browser.select_form(name="Create")
+ component_items = self.browser.find_control('component').items
+ component_names = map(lambda item: item.name, component_items)
+ if not component or component not in component_names:
+ component = self.prompt_for_component(component_names)
+ self.browser['component'] = [component]
+ if cc:
+ self.browser['cc'] = cc
+ self.browser['short_desc'] = bug_title
+ self.browser['comment'] = bug_description
+
+ if patch_file_object:
+ self._fill_attachment_form(patch_description, patch_file_object, mark_for_review=mark_for_review, mark_for_commit_queue=mark_for_commit_queue)
+
+ response = self.browser.submit()
+
+ bug_id = self._check_create_bug_response(response.read())
+ log("Bug %s created." % bug_id)
+ log("%sshow_bug.cgi?id=%s" % (self.bug_server_url, bug_id))
+ return bug_id
+
+ def _find_select_element_for_flag(self, flag_name):
+ # FIXME: This will break if we ever re-order attachment flags
+ if flag_name == "review":
+ return self.browser.find_control(type='select', nr=0)
+ if flag_name == "commit-queue":
+ return self.browser.find_control(type='select', nr=1)
+ raise Exception("Don't know how to find flag named \"%s\"" % flag_name)
+
+ def clear_attachment_flags(self, attachment_id, additional_comment_text=None):
+ self.authenticate()
+
+ comment_text = "Clearing flags on attachment: %s" % attachment_id
+ if additional_comment_text:
+ comment_text += "\n\n%s" % additional_comment_text
+ log(comment_text)
+
+ if self.dryrun:
+ return
+
+ self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
+ self.browser.select_form(nr=1)
+ self.browser.set_value(comment_text, name='comment', nr=0)
+ self._find_select_element_for_flag('review').value = ("X",)
+ self._find_select_element_for_flag('commit-queue').value = ("X",)
+ self.browser.submit()
+
+ # FIXME: We need a way to test this on a live bugzilla instance.
+ def _set_flag_on_attachment(self, attachment_id, flag_name, flag_value, comment_text, additional_comment_text):
+ self.authenticate()
+
+ if additional_comment_text:
+ comment_text += "\n\n%s" % additional_comment_text
+ log(comment_text)
+
+ if self.dryrun:
+ return
+
+ self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
+ self.browser.select_form(nr=1)
+ self.browser.set_value(comment_text, name='comment', nr=0)
+ self._find_select_element_for_flag(flag_name).value = (flag_value,)
+ self.browser.submit()
+
+ def reject_patch_from_commit_queue(self, attachment_id, additional_comment_text=None):
+ comment_text = "Rejecting patch %s from commit-queue." % attachment_id
+ self._set_flag_on_attachment(attachment_id, 'commit-queue', '-', comment_text, additional_comment_text)
+
+ def reject_patch_from_review_queue(self, attachment_id, additional_comment_text=None):
+ comment_text = "Rejecting patch %s from review queue." % attachment_id
+ self._set_flag_on_attachment(attachment_id, 'review', '-', comment_text, additional_comment_text)
+
+ # FIXME: All of these bug editing methods have a ridiculous amount of copy/paste code.
+ def obsolete_attachment(self, attachment_id, comment_text = None):
+ self.authenticate()
+
+ log("Obsoleting attachment: %s" % attachment_id)
+ if self.dryrun:
+ log(comment_text)
+ return
+
+ self.browser.open(self.attachment_url_for_id(attachment_id, 'edit'))
+ self.browser.select_form(nr=1)
+ self.browser.find_control('isobsolete').items[0].selected = True
+ # Also clear any review flag (to remove it from review/commit queues)
+ self._find_select_element_for_flag('review').value = ("X",)
+ self._find_select_element_for_flag('commit-queue').value = ("X",)
+ if comment_text:
+ log(comment_text)
+ # Bugzilla has two textareas named 'comment', one is somehow hidden. We want the first.
+ self.browser.set_value(comment_text, name='comment', nr=0)
+ self.browser.submit()
+
+ def add_cc_to_bug(self, bug_id, email_address_list):
+ self.authenticate()
+
+ log("Adding %s to the CC list for bug %s" % (email_address_list, bug_id))
+ if self.dryrun:
+ return
+
+ self.browser.open(self.bug_url_for_bug_id(bug_id))
+ self.browser.select_form(name="changeform")
+ self.browser["newcc"] = ", ".join(email_address_list)
+ self.browser.submit()
+
+ def post_comment_to_bug(self, bug_id, comment_text, cc=None):
+ self.authenticate()
+
+ log("Adding comment to bug %s" % bug_id)
+ if self.dryrun:
+ log(comment_text)
+ return
+
+ self.browser.open(self.bug_url_for_bug_id(bug_id))
+ self.browser.select_form(name="changeform")
+ self.browser["comment"] = comment_text
+ if cc:
+ self.browser["newcc"] = ", ".join(cc)
+ self.browser.submit()
+
+ def close_bug_as_fixed(self, bug_id, comment_text=None):
+ self.authenticate()
+
+ log("Closing bug %s as fixed" % bug_id)
+ if self.dryrun:
+ log(comment_text)
+ return
+
+ self.browser.open(self.bug_url_for_bug_id(bug_id))
+ self.browser.select_form(name="changeform")
+ if comment_text:
+ log(comment_text)
+ self.browser['comment'] = comment_text
+ self.browser['bug_status'] = ['RESOLVED']
+ self.browser['resolution'] = ['FIXED']
+ self.browser.submit()
+
+ def reassign_bug(self, bug_id, assignee, comment_text=None):
+ self.authenticate()
+
+ log("Assigning bug %s to %s" % (bug_id, assignee))
+ if self.dryrun:
+ log(comment_text)
+ return
+
+ self.browser.open(self.bug_url_for_bug_id(bug_id))
+ self.browser.select_form(name="changeform")
+ if comment_text:
+ log(comment_text)
+ self.browser["comment"] = comment_text
+ self.browser["assigned_to"] = assignee
+ self.browser.submit()
+
+ def reopen_bug(self, bug_id, comment_text):
+ self.authenticate()
+
+ log("Re-opening bug %s" % bug_id)
+ log(comment_text) # Bugzilla requires a comment when re-opening a bug, so we know it will never be None.
+ if self.dryrun:
+ return
+
+ self.browser.open(self.bug_url_for_bug_id(bug_id))
+ self.browser.select_form(name="changeform")
+ self.browser['bug_status'] = ['REOPENED']
+ self.browser['comment'] = comment_text
+ self.browser.submit()
diff --git a/WebKitTools/Scripts/webkitpy/bugzilla_unittest.py b/WebKitTools/Scripts/webkitpy/bugzilla_unittest.py
new file mode 100644
index 0000000..93a35f9
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/bugzilla_unittest.py
@@ -0,0 +1,302 @@
+# Copyright (C) 2009 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.committers import CommitterList, Reviewer, Committer
+from webkitpy.bugzilla import Bugzilla, BugzillaQueries, parse_bug_id
+from webkitpy.outputcapture import OutputCapture
+from webkitpy.mock import Mock
+
+from webkitpy.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 BugzillaTest(unittest.TestCase):
+ _example_attachment = '''
+ <attachment
+ isobsolete="1"
+ ispatch="1"
+ isprivate="0"
+ >
+ <attachid>33721</attachid>
+ <date>2009-07-29 10:23 PDT</date>
+ <desc>Fixed whitespace issue</desc>
+ <filename>patch</filename>
+ <type>text/plain</type>
+ <size>9719</size>
+ <attacher>christian.plesner.hansen at gmail.com</attacher>
+ <flag name="review"
+ id="17931"
+ status="+"
+ setter="one at test.com"
+ />
+ <flag name="commit-queue"
+ id="17932"
+ status="+"
+ setter="two at test.com"
+ />
+ </attachment>
+'''
+ _expected_example_attachment_parsing = {
+ 'bug_id' : 100,
+ 'is_obsolete' : True,
+ 'is_patch' : True,
+ 'id' : 33721,
+ 'url' : "https://bugs.webkit.org/attachment.cgi?id=33721",
+ 'name' : "Fixed whitespace issue",
+ 'type' : "text/plain",
+ 'review' : '+',
+ 'reviewer_email' : 'one at test.com',
+ 'commit-queue' : '+',
+ 'committer_email' : 'two at test.com',
+ 'attacher_email' : 'christian.plesner.hansen at gmail.com',
+ }
+
+ def test_parse_bug_id(self):
+ # FIXME: These would be all better as doctests
+ bugs = Bugzilla()
+ self.assertEquals(12345, parse_bug_id("http://webkit.org/b/12345"))
+ self.assertEquals(12345, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?id=12345"))
+ self.assertEquals(12345, parse_bug_id(bugs.short_bug_url_for_bug_id(12345)))
+ self.assertEquals(12345, parse_bug_id(bugs.bug_url_for_bug_id(12345)))
+ self.assertEquals(12345, parse_bug_id(bugs.bug_url_for_bug_id(12345, xml=True)))
+
+ # Our bug parser is super-fragile, but at least we're testing it.
+ self.assertEquals(None, parse_bug_id("http://www.webkit.org/b/12345"))
+ self.assertEquals(None, parse_bug_id("http://bugs.webkit.org/show_bug.cgi?ctype=xml&id=12345"))
+
+ _example_bug = """
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<!DOCTYPE bugzilla SYSTEM "https://bugs.webkit.org/bugzilla.dtd">
+<bugzilla version="3.2.3"
+ urlbase="https://bugs.webkit.org/"
+ maintainer="admin at webkit.org"
+ exporter="eric at webkit.org"
+>
+ <bug>
+ <bug_id>32585</bug_id>
+ <creation_ts>2009-12-15 15:17 PST</creation_ts>
+ <short_desc>bug to test bugzilla-tool and commit-queue failures</short_desc>
+ <delta_ts>2009-12-27 21:04:50 PST</delta_ts>
+ <reporter_accessible>1</reporter_accessible>
+ <cclist_accessible>1</cclist_accessible>
+ <classification_id>1</classification_id>
+ <classification>Unclassified</classification>
+ <product>WebKit</product>
+ <component>Tools / Tests</component>
+ <version>528+ (Nightly build)</version>
+ <rep_platform>PC</rep_platform>
+ <op_sys>Mac OS X 10.5</op_sys>
+ <bug_status>NEW</bug_status>
+ <priority>P2</priority>
+ <bug_severity>Normal</bug_severity>
+ <target_milestone>---</target_milestone>
+ <everconfirmed>1</everconfirmed>
+ <reporter name="Eric Seidel">eric at webkit.org</reporter>
+ <assigned_to name="Nobody">webkit-unassigned at lists.webkit.org</assigned_to>
+ <cc>foo at bar.com</cc>
+ <cc>example at example.com</cc>
+ <long_desc isprivate="0">
+ <who name="Eric Seidel">eric at webkit.org</who>
+ <bug_when>2009-12-15 15:17:28 PST</bug_when>
+ <thetext>bug to test bugzilla-tool and commit-queue failures
+
+Ignore this bug. Just for testing failure modes of bugzilla-tool and the commit-queue.</thetext>
+ </long_desc>
+ <attachment
+ isobsolete="0"
+ ispatch="1"
+ isprivate="0"
+ >
+ <attachid>45548</attachid>
+ <date>2009-12-27 23:51 PST</date>
+ <desc>Patch</desc>
+ <filename>bug-32585-20091228005112.patch</filename>
+ <type>text/plain</type>
+ <size>10882</size>
+ <attacher>mjs at apple.com</attacher>
+
+ <token>1261988248-dc51409e9c421a4358f365fa8bec8357</token>
+ <data encoding="base64">SW5kZXg6IFdlYktpdC9tYWMvQ2hhbmdlTG9nCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09
+removed-because-it-was-really-long
+ZEZpbmlzaExvYWRXaXRoUmVhc29uOnJlYXNvbl07Cit9CisKIEBlbmQKIAogI2VuZGlmCg==
+</data>
+
+ <flag name="review"
+ id="27602"
+ status="?"
+ setter="mjs at apple.com"
+ />
+ </attachment>
+ </bug>
+</bugzilla>
+"""
+ _expected_example_bug_parsing = {
+ "id" : 32585,
+ "title" : u"bug to test bugzilla-tool and commit-queue failures",
+ "cc_emails" : ["foo at bar.com", "example at example.com"],
+ "reporter_email" : "eric at webkit.org",
+ "assigned_to_email" : "webkit-unassigned at lists.webkit.org",
+ "attachments" : [{
+ 'name': u'Patch',
+ 'url': 'https://bugs.webkit.org/attachment.cgi?id=45548',
+ 'is_obsolete': False,
+ 'review': '?',
+ 'is_patch': True,
+ 'attacher_email': 'mjs at apple.com',
+ 'bug_id': 32585,
+ 'type': 'text/plain',
+ 'id': 45548
+ }],
+ }
+
+ def _assert_dictionaries_equal(self, actual, expected):
+ # Make sure we aren't parsing more or less than we expect
+ self.assertEquals(sorted(actual.keys()), sorted(expected.keys()))
+
+ for key, expected_value in expected.items():
+ self.assertEquals(actual[key], expected_value, ("Failure for key: %s: Actual='%s' Expected='%s'" % (key, actual[key], expected_value)))
+
+ def test_bug_parsing(self):
+ bug = Bugzilla()._parse_bug_page(self._example_bug)
+ self._assert_dictionaries_equal(bug, self._expected_example_bug_parsing)
+
+ # This could be combined into test_bug_parsing later if desired.
+ def test_attachment_parsing(self):
+ bugzilla = Bugzilla()
+ soup = BeautifulSoup(self._example_attachment)
+ attachment_element = soup.find("attachment")
+ attachment = bugzilla._parse_attachment_element(attachment_element, self._expected_example_attachment_parsing['bug_id'])
+ self.assertTrue(attachment)
+ self._assert_dictionaries_equal(attachment, self._expected_example_attachment_parsing)
+
+ _sample_attachment_detail_page = """
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+ <head>
+ <title>
+ Attachment 41073 Details for Bug 27314</title>
+<link rel="Top" href="https://bugs.webkit.org/">
+ <link rel="Up" href="show_bug.cgi?id=27314">
+"""
+
+ def test_attachment_detail_bug_parsing(self):
+ bugzilla = Bugzilla()
+ self.assertEquals(27314, bugzilla._parse_bug_id_from_attachment_page(self._sample_attachment_detail_page))
+
+ def test_add_cc_to_bug(self):
+ bugzilla = Bugzilla()
+ bugzilla.browser = MockBrowser()
+ bugzilla.authenticate = lambda: None
+ expected_stderr = "Adding ['adam at example.com'] to the CC list for bug 42\n"
+ OutputCapture().assert_outputs(self, bugzilla.add_cc_to_bug, [42, ["adam at example.com"]], expected_stderr=expected_stderr)
+
+ def test_flag_permission_rejection_message(self):
+ bugzilla = Bugzilla()
+ expected_messsage="""foo at foo.com does not have review permissions according to http://trac.webkit.org/browser/trunk/WebKitTools/Scripts/webkitpy/committers.py.
+
+- If you do not have review rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags.
+
+- If you have review rights please correct the error in WebKitTools/Scripts/webkitpy/committers.py by adding yourself to the file (no review needed). Due to bug 30084 the commit-queue will require a restart after your change. Please contact eseidel at chromium.org to request a commit-queue restart. After restart the commit-queue will correctly respect your review rights."""
+ self.assertEqual(bugzilla._flag_permission_rejection_message("foo at foo.com", "review"), expected_messsage)
+
+
+class BugzillaQueriesTest(unittest.TestCase):
+ _sample_request_page = """
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+ <head>
+ <title>Request Queue</title>
+ </head>
+<body>
+
+<h3>Flag: review</h3>
+ <table class="requests" cellspacing="0" cellpadding="4" border="1">
+ <tr>
+ <th>Requester</th>
+ <th>Requestee</th>
+ <th>Bug</th>
+ <th>Attachment</th>
+ <th>Created</th>
+ </tr>
+ <tr>
+ <td>Shinichiro Hamaji <hamaji@chromium.org></td>
+ <td></td>
+ <td><a href="show_bug.cgi?id=30015">30015: text-transform:capitalize is failing in CSS2.1 test suite</a></td>
+ <td><a href="attachment.cgi?id=40511&action=review">
+40511: Patch v0</a></td>
+ <td>2009-10-02 04:58 PST</td>
+ </tr>
+ <tr>
+ <td>Zan Dobersek <zandobersek@gmail.com></td>
+ <td></td>
+ <td><a href="show_bug.cgi?id=26304">26304: [GTK] Add controls for playing html5 video.</a></td>
+ <td><a href="attachment.cgi?id=40722&action=review">
+40722: Media controls, the simple approach</a></td>
+ <td>2009-10-06 09:13 PST</td>
+ </tr>
+ <tr>
+ <td>Zan Dobersek <zandobersek@gmail.com></td>
+ <td></td>
+ <td><a href="show_bug.cgi?id=26304">26304: [GTK] Add controls for playing html5 video.</a></td>
+ <td><a href="attachment.cgi?id=40723&action=review">
+40723: Adjust the media slider thumb size</a></td>
+ <td>2009-10-06 09:15 PST</td>
+ </tr>
+ </table>
+</body>
+</html>
+"""
+
+ def test_request_page_parsing(self):
+ queries = BugzillaQueries(None)
+ self.assertEquals([40511, 40722, 40723], queries._parse_attachment_ids_request_query(self._sample_request_page))
+
+ def test_load_query(self):
+ queries = BugzillaQueries(Mock())
+ queries._load_query("request.cgi?action=queue&type=review&group=type")
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/WebKitTools/Scripts/webkitpy/buildbot.py b/WebKitTools/Scripts/webkitpy/buildbot.py
new file mode 100644
index 0000000..b0eb0a4
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/buildbot.py
@@ -0,0 +1,111 @@
+# Copyright (c) 2009, 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.
+#
+# WebKit's Python module for interacting with WebKit's buildbot
+
+import re
+import urllib2
+
+# Import WebKit-specific modules.
+from webkitpy.webkit_logging import log
+
+# WebKit includes a built copy of BeautifulSoup in Scripts/webkitpy
+# so this import should always succeed.
+from .BeautifulSoup import BeautifulSoup
+
+class BuildBot:
+ default_host = "build.webkit.org"
+ def __init__(self, host=default_host):
+ self.buildbot_host = host
+ self.buildbot_server_url = "http://%s/" % self.buildbot_host
+
+ # If any of the Leopard build/test bots or the Windows builders are red we should not be landing patches.
+ # Other builders should be added to this list once they're known to be stable.
+ self.core_builder_names_regexps = [ 'Leopard', "Windows.*Build" ]
+
+ # If WebKit's buildbot has an XMLRPC interface we could use, we could do something more sophisticated here.
+ # For now we just parse out the basics, enough to support basic questions like "is the tree green?"
+ def _parse_builder_status_from_row(self, status_row):
+ status_cells = status_row.findAll('td')
+ builder = {}
+
+ name_link = status_cells[0].find('a')
+ builder['name'] = name_link.string
+ # We could generate the builder_url from the name in a future version of this code.
+ builder['builder_url'] = self.buildbot_server_url + name_link['href']
+
+ status_link = status_cells[1].find('a')
+ if not status_link:
+ # We failed to find a link in the first cell, just give up.
+ # This can happen if a builder is just-added, the first cell will just be "no build"
+ builder['is_green'] = False # Other parts of the code depend on is_green being present.
+ return builder
+ revision_string = status_link.string # Will be either a revision number or a build number
+ # If revision_string has non-digits assume it's not a revision number.
+ builder['built_revision'] = int(revision_string) if not re.match('\D', revision_string) else None
+ builder['is_green'] = not re.search('fail', status_cells[1].renderContents())
+ # We could parse out the build number instead, but for now just store the URL.
+ builder['build_url'] = self.buildbot_server_url + status_link['href']
+
+ # We could parse out the current activity too.
+
+ return builder
+
+ def _builder_statuses_with_names_matching_regexps(self, builder_statuses, name_regexps):
+ builders = []
+ for builder in builder_statuses:
+ for name_regexp in name_regexps:
+ if re.match(name_regexp, builder['name']):
+ builders.append(builder)
+ return builders
+
+ def red_core_builders(self):
+ red_builders = []
+ for builder in self._builder_statuses_with_names_matching_regexps(self.builder_statuses(), self.core_builder_names_regexps):
+ if not builder['is_green']:
+ red_builders.append(builder)
+ return red_builders
+
+ def red_core_builders_names(self):
+ red_builders = self.red_core_builders()
+ return map(lambda builder: builder['name'], red_builders)
+
+ def core_builders_are_green(self):
+ return not self.red_core_builders()
+
+ def builder_statuses(self):
+ build_status_url = self.buildbot_server_url + 'one_box_per_builder'
+ page = urllib2.urlopen(build_status_url)
+ soup = BeautifulSoup(page)
+
+ builders = []
+ status_table = soup.find('table')
+ for status_row in status_table.findAll('tr'):
+ builder = self._parse_builder_status_from_row(status_row)
+ builders.append(builder)
+ return builders
diff --git a/WebKitTools/Scripts/webkitpy/buildbot_unittest.py b/WebKitTools/Scripts/webkitpy/buildbot_unittest.py
new file mode 100644
index 0000000..a42ea85
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/buildbot_unittest.py
@@ -0,0 +1,134 @@
+# Copyright (C) 2009 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.buildbot import BuildBot
+
+from webkitpy.BeautifulSoup import BeautifulSoup
+
+class BuildBotTest(unittest.TestCase):
+
+ _example_one_box_status = '''
+ <table>
+ <tr>
+ <td class="box"><a href="builders/Windows%20Debug%20%28Tests%29">Windows Debug (Tests)</a></td>
+ <td align="center" class="LastBuild box success"><a href="builders/Windows%20Debug%20%28Tests%29/builds/3693">47380</a><br />build<br />successful</td>
+ <td align="center" class="Activity building">building<br />ETA in<br />~ 14 mins<br />at 13:40</td>
+ <tr>
+ <td class="box"><a href="builders/SnowLeopard%20Intel%20Release">SnowLeopard Intel Release</a></td>
+ <td class="LastBuild box" >no build</td>
+ <td align="center" class="Activity building">building<br />< 1 min</td>
+ <tr>
+ <td class="box"><a href="builders/Qt%20Linux%20Release">Qt Linux Release</a></td>
+ <td align="center" class="LastBuild box failure"><a href="builders/Qt%20Linux%20Release/builds/654">47383</a><br />failed<br />compile-webkit</td>
+ <td align="center" class="Activity idle">idle</td>
+ </table>
+'''
+ _expected_example_one_box_parsings = [
+ {
+ 'builder_url': u'http://build.webkit.org/builders/Windows%20Debug%20%28Tests%29',
+ 'build_url': u'http://build.webkit.org/builders/Windows%20Debug%20%28Tests%29/builds/3693',
+ 'is_green': True,
+ 'name': u'Windows Debug (Tests)',
+ 'built_revision': 47380
+ },
+ {
+ 'builder_url': u'http://build.webkit.org/builders/SnowLeopard%20Intel%20Release',
+ 'is_green': False,
+ 'name': u'SnowLeopard Intel Release',
+ },
+ {
+ 'builder_url': u'http://build.webkit.org/builders/Qt%20Linux%20Release',
+ 'build_url': u'http://build.webkit.org/builders/Qt%20Linux%20Release/builds/654',
+ 'is_green': False,
+ 'name': u'Qt Linux Release',
+ 'built_revision': 47383
+ },
+ ]
+
+ def test_status_parsing(self):
+ buildbot = BuildBot()
+
+ soup = BeautifulSoup(self._example_one_box_status)
+ status_table = soup.find("table")
+ input_rows = status_table.findAll('tr')
+
+ for x in range(len(input_rows)):
+ status_row = input_rows[x]
+ expected_parsing = self._expected_example_one_box_parsings[x]
+
+ builder = buildbot._parse_builder_status_from_row(status_row)
+
+ # Make sure we aren't parsing more or less than we expect
+ self.assertEquals(builder.keys(), expected_parsing.keys())
+
+ for key, expected_value in expected_parsing.items():
+ self.assertEquals(builder[key], expected_value, ("Builder %d parse failure for key: %s: Actual='%s' Expected='%s'" % (x, key, builder[key], expected_value)))
+
+ def test_core_builder_methods(self):
+ buildbot = BuildBot()
+
+ # Override builder_statuses function to not touch the network.
+ def example_builder_statuses(): # We could use instancemethod() to bind 'self' but we don't need to.
+ return BuildBotTest._expected_example_one_box_parsings
+ buildbot.builder_statuses = example_builder_statuses
+
+ buildbot.core_builder_names_regexps = [ 'Leopard', "Windows.*Build" ]
+ self.assertEquals(buildbot.red_core_builders_names(), [])
+ self.assertTrue(buildbot.core_builders_are_green())
+
+ buildbot.core_builder_names_regexps = [ 'SnowLeopard', 'Qt' ]
+ self.assertEquals(buildbot.red_core_builders_names(), [ u'SnowLeopard Intel Release', u'Qt Linux Release' ])
+ self.assertFalse(buildbot.core_builders_are_green())
+
+ def test_builder_name_regexps(self):
+ buildbot = BuildBot()
+
+ example_builders = [
+ { 'name': u'Leopard Debug (Build)', },
+ { 'name': u'Leopard Debug (Tests)', },
+ { 'name': u'Windows Release (Build)', },
+ { 'name': u'Windows Debug (Tests)', },
+ { 'name': u'Qt Linux Release', },
+ ]
+ name_regexps = [ 'Leopard', "Windows.*Build" ]
+ expected_builders = [
+ { 'name': u'Leopard Debug (Build)', },
+ { 'name': u'Leopard Debug (Tests)', },
+ { 'name': u'Windows Release (Build)', },
+ ]
+
+ # This test should probably be updated if the default regexp list changes
+ self.assertEquals(buildbot.core_builder_names_regexps, name_regexps)
+
+ builders = buildbot._builder_statuses_with_names_matching_regexps(example_builders, name_regexps)
+ self.assertEquals(builders, expected_builders)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/WebKitTools/Scripts/webkitpy/buildsteps.py b/WebKitTools/Scripts/webkitpy/buildsteps.py
new file mode 100644
index 0000000..230541e
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/buildsteps.py
@@ -0,0 +1,541 @@
+# Copyright (C) 2009 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 os
+import StringIO
+
+from optparse import make_option
+
+from webkitpy.comments import bug_comment_from_commit_text
+from webkitpy.grammar import pluralize
+from webkitpy.webkit_logging import log, error
+from webkitpy.webkitport import WebKitPort
+from webkitpy.changelogs import ChangeLog
+
+# FIXME: Why do some of these have "Step" in their name but not all?
+__all__ = [
+ "ApplyPatchStep",
+ "ApplyPatchWithLocalCommitStep",
+ "BuildStep",
+ "CheckStyleStep",
+ "CleanWorkingDirectoryStep",
+ "CleanWorkingDirectoryWithLocalCommitsStep",
+ "CloseBugForLandDiffStep",
+ "CloseBugStep",
+ "ClosePatchStep",
+ "CommitStep",
+ "CompleteRollout",
+ "CreateBugStep",
+ "EnsureBuildersAreGreenStep",
+ "EnsureLocalCommitIfNeeded",
+ "ObsoletePatchesOnBugStep",
+ "PostDiffToBugStep",
+ "PrepareChangeLogForRevertStep",
+ "PrepareChangeLogStep",
+ "PromptForBugOrTitleStep",
+ "RevertRevisionStep",
+ "RunTestsStep",
+ "UpdateChangeLogsWithReviewerStep",
+ "UpdateStep",
+]
+
+class CommandOptions(object):
+ force_clean = make_option("--force-clean", action="store_true", dest="force_clean", default=False, help="Clean working directory before applying patches (removes local changes and commits)")
+ clean = make_option("--no-clean", action="store_false", dest="clean", default=True, help="Don't check if the working directory is clean before applying patches")
+ check_builders = make_option("--ignore-builders", action="store_false", dest="check_builders", default=True, help="Don't check to see if the build.webkit.org builders are green before landing.")
+ quiet = make_option("--quiet", action="store_true", dest="quiet", default=False, help="Produce less console output.")
+ non_interactive = make_option("--non-interactive", action="store_true", dest="non_interactive", default=False, help="Never prompt the user, fail as fast as possible.")
+ parent_command = make_option("--parent-command", action="store", dest="parent_command", default=None, help="(Internal) The command that spawned this instance.")
+ update = make_option("--no-update", action="store_false", dest="update", default=True, help="Don't update the working directory.")
+ local_commit = make_option("--local-commit", action="store_true", dest="local_commit", default=False, help="Make a local commit for each applied patch")
+ build = make_option("--no-build", action="store_false", dest="build", default=True, help="Commit without building first, implies --no-test.")
+ build_style = make_option("--build-style", action="store", dest="build_style", default=None, help="Whether to build debug, release, or both.")
+ test = make_option("--no-test", action="store_false", dest="test", default=True, help="Commit without running run-webkit-tests.")
+ close_bug = make_option("--no-close", action="store_false", dest="close_bug", default=True, help="Leave bug open after landing.")
+ port = make_option("--port", action="store", dest="port", default=None, help="Specify a port (e.g., mac, qt, gtk, ...).")
+ reviewer = make_option("-r", "--reviewer", action="store", type="string", dest="reviewer", help="Update ChangeLogs to say Reviewed by REVIEWER.")
+ complete_rollout = make_option("--complete-rollout", action="store_true", dest="complete_rollout", help="Commit the revert and re-open the original bug.")
+ obsolete_patches = make_option("--no-obsolete", action="store_false", dest="obsolete_patches", default=True, help="Do not obsolete old patches before posting this one.")
+ review = make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review.")
+ request_commit = make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review.")
+ description = make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: \"patch\")")
+ cc = make_option("--cc", action="store", type="string", dest="cc", help="Comma-separated list of email addresses to carbon-copy.")
+ component = make_option("--component", action="store", type="string", dest="component", help="Component for the new bug.")
+ confirm = make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Skip confirmation steps.")
+
+
+class AbstractStep(object):
+ def __init__(self, tool, options):
+ self._tool = tool
+ self._options = options
+ self._port = None
+
+ def _run_script(self, script_name, quiet=False, port=WebKitPort):
+ log("Running %s" % script_name)
+ # FIXME: This should use self.port()
+ self._tool.executive.run_and_throw_if_fail(port.script_path(script_name), quiet)
+
+ # FIXME: The port should live on the tool.
+ def port(self):
+ if self._port:
+ return self._port
+ self._port = WebKitPort.port(self._options.port)
+ return self._port
+
+ @classmethod
+ def options(cls):
+ return []
+
+ def run(self, state):
+ raise NotImplementedError, "subclasses must implement"
+
+
+# FIXME: Unify with StepSequence? I'm not sure yet which is the better design.
+class MetaStep(AbstractStep):
+ substeps = [] # Override in subclasses
+ def __init__(self, tool, options):
+ AbstractStep.__init__(self, tool, options)
+ self._step_instances = []
+ for step_class in self.substeps:
+ self._step_instances.append(step_class(tool, options))
+
+ @staticmethod
+ def _collect_options_from_steps(steps):
+ collected_options = []
+ for step in steps:
+ collected_options = collected_options + step.options()
+ return collected_options
+
+ @classmethod
+ def options(cls):
+ return cls._collect_options_from_steps(cls.substeps)
+
+ def run(self, state):
+ for step in self._step_instances:
+ step.run(state)
+
+
+class PromptForBugOrTitleStep(AbstractStep):
+ def run(self, state):
+ # No need to prompt if we alrady have the bug_id.
+ if state.get("bug_id"):
+ return
+ user_response = self._tool.user.prompt("Please enter a bug number or a title for a new bug:\n")
+ # If the user responds with a number, we assume it's bug number.
+ # Otherwise we assume it's a bug subject.
+ try:
+ state["bug_id"] = int(user_response)
+ except ValueError, TypeError:
+ state["bug_title"] = user_response
+ # FIXME: This is kind of a lame description.
+ state["bug_description"] = user_response
+
+
+class CreateBugStep(AbstractStep):
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.cc,
+ CommandOptions.component,
+ ]
+
+ def run(self, state):
+ # No need to create a bug if we already have one.
+ if state.get("bug_id"):
+ return
+ state["bug_id"] = self._tool.bugs.create_bug(state["bug_title"], state["bug_description"], component=self._options.component, cc=self._options.cc)
+
+
+class PrepareChangeLogStep(AbstractStep):
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.port,
+ CommandOptions.quiet,
+ ]
+
+ def run(self, state):
+ os.chdir(self._tool.scm().checkout_root)
+ args = [self.port().script_path("prepare-ChangeLog")]
+ if state["bug_id"]:
+ args.append("--bug=%s" % state["bug_id"])
+ self._tool.executive.run_and_throw_if_fail(args, self._options.quiet)
+
+
+class EditChangeLogStep(AbstractStep):
+ def run(self, state):
+ self._tool.user.edit(self._tool.scm().modified_changelogs())
+
+
+class ObsoletePatchesOnBugStep(AbstractStep):
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.obsolete_patches,
+ ]
+
+ def run(self, state):
+ if not self._options.obsolete_patches:
+ return
+ bug_id = state["bug_id"]
+ patches = self._tool.bugs.fetch_patches_from_bug(bug_id)
+ if not patches:
+ return
+ log("Obsoleting %s on bug %s" % (pluralize("old patch", len(patches)), bug_id))
+ for patch in patches:
+ self._tool.bugs.obsolete_attachment(patch["id"])
+
+
+class AbstractDiffStep(AbstractStep):
+ def diff(self, state):
+ diff = state.get("diff")
+ if not diff:
+ diff = self._tool.scm().create_patch()
+ state["diff"] = diff
+ return diff
+
+
+class ConfirmDiffStep(AbstractDiffStep):
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.confirm,
+ ]
+
+ def run(self, state):
+ if not self._options.confirm:
+ return
+ diff = self.diff(state)
+ self._tool.user.page(diff)
+ if not self._tool.user.confirm():
+ error("User declined to continue.")
+
+
+class PostDiffToBugStep(AbstractDiffStep):
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.description,
+ CommandOptions.review,
+ CommandOptions.request_commit,
+ ]
+
+ def run(self, state):
+ diff = self.diff(state)
+ diff_file = StringIO.StringIO(diff) # add_patch_to_bug expects a file-like object
+ description = self._options.description or "Patch"
+ self._tool.bugs.add_patch_to_bug(state["bug_id"], diff_file, description, mark_for_review=self._options.review, mark_for_commit_queue=self._options.request_commit)
+
+
+class PrepareChangeLogForRevertStep(AbstractStep):
+ def run(self, state):
+ # First, discard the ChangeLog changes from the rollout.
+ os.chdir(self._tool.scm().checkout_root)
+ changelog_paths = self._tool.scm().modified_changelogs()
+ self._tool.scm().revert_files(changelog_paths)
+
+ # Second, make new ChangeLog entries for this rollout.
+ # This could move to prepare-ChangeLog by adding a --revert= option.
+ self._run_script("prepare-ChangeLog")
+ for changelog_path in changelog_paths:
+ ChangeLog(changelog_path).update_for_revert(state["revision"])
+
+
+class CleanWorkingDirectoryStep(AbstractStep):
+ def __init__(self, tool, options, allow_local_commits=False):
+ AbstractStep.__init__(self, tool, options)
+ self._allow_local_commits = allow_local_commits
+
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.force_clean,
+ CommandOptions.clean,
+ ]
+
+ def run(self, state):
+ os.chdir(self._tool.scm().checkout_root)
+ if not self._allow_local_commits:
+ self._tool.scm().ensure_no_local_commits(self._options.force_clean)
+ if self._options.clean:
+ self._tool.scm().ensure_clean_working_directory(force_clean=self._options.force_clean)
+
+
+class CleanWorkingDirectoryWithLocalCommitsStep(CleanWorkingDirectoryStep):
+ def __init__(self, tool, options):
+ # FIXME: This a bit of a hack. Consider doing this more cleanly.
+ CleanWorkingDirectoryStep.__init__(self, tool, options, allow_local_commits=True)
+
+
+class UpdateStep(AbstractStep):
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.update,
+ CommandOptions.port,
+ ]
+
+ def run(self, state):
+ if not self._options.update:
+ return
+ log("Updating working directory")
+ self._tool.executive.run_and_throw_if_fail(self.port().update_webkit_command(), quiet=True)
+
+
+class ApplyPatchStep(AbstractStep):
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.non_interactive,
+ ]
+
+ def run(self, state):
+ log("Processing patch %s from bug %s." % (state["patch"]["id"], state["patch"]["bug_id"]))
+ self._tool.scm().apply_patch(state["patch"], force=self._options.non_interactive)
+
+
+class RevertRevisionStep(AbstractStep):
+ def run(self, state):
+ self._tool.scm().apply_reverse_diff(state["revision"])
+
+
+class ApplyPatchWithLocalCommitStep(ApplyPatchStep):
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.local_commit,
+ ] + ApplyPatchStep.options()
+
+ def run(self, state):
+ ApplyPatchStep.run(self, state)
+ if self._options.local_commit:
+ commit_message = self._tool.scm().commit_message_for_this_commit()
+ self._tool.scm().commit_locally_with_message(commit_message.message() or state["patch"]["name"])
+
+
+class EnsureBuildersAreGreenStep(AbstractStep):
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.check_builders,
+ ]
+
+ def run(self, state):
+ if not self._options.check_builders:
+ return
+ red_builders_names = self._tool.buildbot.red_core_builders_names()
+ if not red_builders_names:
+ return
+ red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names.
+ error("Builders [%s] are red, please do not commit.\nSee http://%s.\nPass --ignore-builders to bypass this check." % (", ".join(red_builders_names), self._tool.buildbot.buildbot_host))
+
+
+class EnsureLocalCommitIfNeeded(AbstractStep):
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.local_commit,
+ ]
+
+ def run(self, state):
+ if self._options.local_commit and not self._tool.scm().supports_local_commits():
+ error("--local-commit passed, but %s does not support local commits" % self._tool.scm.display_name())
+
+
+class UpdateChangeLogsWithReviewerStep(AbstractStep):
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.reviewer,
+ ]
+
+ def _guess_reviewer_from_bug(self, bug_id):
+ patches = self._tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
+ if len(patches) != 1:
+ log("%s on bug %s, cannot infer reviewer." % (pluralize("reviewed patch", len(patches)), bug_id))
+ return None
+ patch = patches[0]
+ reviewer = patch["reviewer"]
+ log("Guessing \"%s\" as reviewer from attachment %s on bug %s." % (reviewer, patch["id"], bug_id))
+ return reviewer
+
+ def run(self, state):
+ bug_id = state["patch"]["bug_id"]
+ reviewer = self._options.reviewer
+ if not reviewer:
+ if not bug_id:
+ log("No bug id provided and --reviewer= not provided. Not updating ChangeLogs with reviewer.")
+ return
+ reviewer = self._guess_reviewer_from_bug(bug_id)
+
+ if not reviewer:
+ log("Failed to guess reviewer from bug %s and --reviewer= not provided. Not updating ChangeLogs with reviewer." % bug_id)
+ return
+
+ os.chdir(self._tool.scm().checkout_root)
+ for changelog_path in self._tool.scm().modified_changelogs():
+ ChangeLog(changelog_path).set_reviewer(reviewer)
+
+
+class BuildStep(AbstractStep):
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.build,
+ CommandOptions.quiet,
+ CommandOptions.build_style,
+ ]
+
+ def build(self, build_style):
+ self._tool.executive.run_and_throw_if_fail(self.port().build_webkit_command(build_style=build_style), self._options.quiet)
+
+ def run(self, state):
+ if not self._options.build:
+ return
+ log("Building WebKit")
+ if self._options.build_style == "both":
+ self.build("debug")
+ self.build("release")
+ else:
+ self.build(self._options.build_style)
+
+
+class CheckStyleStep(AbstractStep):
+ def run(self, state):
+ self._run_script("check-webkit-style")
+
+
+class RunTestsStep(AbstractStep):
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.build,
+ CommandOptions.test,
+ CommandOptions.non_interactive,
+ CommandOptions.quiet,
+ CommandOptions.port,
+ ]
+
+ def run(self, state):
+ if not self._options.build:
+ return
+ if not self._options.test:
+ return
+ args = self.port().run_webkit_tests_command()
+ if self._options.non_interactive:
+ args.append("--no-launch-safari")
+ args.append("--exit-after-n-failures=1")
+ if self._options.quiet:
+ args.append("--quiet")
+ self._tool.executive.run_and_throw_if_fail(args)
+
+
+class CommitStep(AbstractStep):
+ def run(self, state):
+ commit_message = self._tool.scm().commit_message_for_this_commit()
+ state["commit_text"] = self._tool.scm().commit_with_message(commit_message.message())
+
+
+class ClosePatchStep(AbstractStep):
+ def run(self, state):
+ comment_text = bug_comment_from_commit_text(self._tool.scm(), state["commit_text"])
+ self._tool.bugs.clear_attachment_flags(state["patch"]["id"], comment_text)
+
+
+class CloseBugStep(AbstractStep):
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.close_bug,
+ ]
+
+ def run(self, state):
+ if not self._options.close_bug:
+ return
+ # Check to make sure there are no r? or r+ patches on the bug before closing.
+ # Assume that r- patches are just previous patches someone forgot to obsolete.
+ patches = self._tool.bugs.fetch_patches_from_bug(state["patch"]["bug_id"])
+ for patch in patches:
+ review_flag = patch.get("review")
+ if review_flag == "?" or review_flag == "+":
+ log("Not closing bug %s as attachment %s has review=%s. Assuming there are more patches to land from this bug." % (patch["bug_id"], patch["id"], review_flag))
+ return
+ self._tool.bugs.close_bug_as_fixed(state["patch"]["bug_id"], "All reviewed patches have been landed. Closing bug.")
+
+
+class CloseBugForLandDiffStep(AbstractStep):
+ @classmethod
+ def options(cls):
+ return [
+ CommandOptions.close_bug,
+ ]
+
+ def run(self, state):
+ comment_text = bug_comment_from_commit_text(self._tool.scm(), state["commit_text"])
+ bug_id = state["patch"]["bug_id"]
+ if bug_id:
+ log("Updating bug %s" % bug_id)
+ if self._options.close_bug:
+ self._tool.bugs.close_bug_as_fixed(bug_id, comment_text)
+ else:
+ # FIXME: We should a smart way to figure out if the patch is attached
+ # to the bug, and if so obsolete it.
+ self._tool.bugs.post_comment_to_bug(bug_id, comment_text)
+ else:
+ log(comment_text)
+ log("No bug id provided.")
+
+
+class CompleteRollout(MetaStep):
+ substeps = [
+ BuildStep,
+ CommitStep,
+ ]
+
+ @classmethod
+ def options(cls):
+ collected_options = cls._collect_options_from_steps(cls.substeps)
+ collected_options.append(CommandOptions.complete_rollout)
+ return collected_options
+
+ def run(self, state):
+ bug_id = state["bug_id"]
+ # FIXME: Fully automated rollout is not 100% idiot-proof yet, so for now just log with instructions on how to complete the rollout.
+ # Once we trust rollout we will remove this option.
+ if not self._options.complete_rollout:
+ log("\nNOTE: Rollout support is experimental.\nPlease verify the rollout diff and use \"bugzilla-tool land-diff %s\" to commit the rollout." % bug_id)
+ return
+
+ MetaStep.run(self, state)
+
+ if not bug_id:
+ log(state["commit_text"])
+ log("No bugs were updated or re-opened to reflect this rollout.")
+ return
+ # FIXME: I'm not sure state["commit_text"] is quite right here.
+ self._tool.bugs.reopen_bug(bug_id, state["commit_text"])
diff --git a/WebKitTools/Scripts/webkitpy/buildsteps_unittest.py b/WebKitTools/Scripts/webkitpy/buildsteps_unittest.py
new file mode 100644
index 0000000..158f4c0
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/buildsteps_unittest.py
@@ -0,0 +1,63 @@
+# Copyright (C) 2009 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.buildsteps import UpdateChangeLogsWithReviewerStep, UpdateStep, PromptForBugOrTitleStep
+from webkitpy.mock_bugzillatool import MockBugzillaTool
+from webkitpy.outputcapture import OutputCapture
+from webkitpy.mock import Mock
+
+
+class UpdateChangeLogsWithReviewerStepTest(unittest.TestCase):
+ def test_guess_reviewer_from_bug(self):
+ capture = OutputCapture()
+ step = UpdateChangeLogsWithReviewerStep(MockBugzillaTool(), [])
+ expected_stderr = "0 reviewed patches on bug 75, cannot infer reviewer.\n"
+ capture.assert_outputs(self, step._guess_reviewer_from_bug, [75], expected_stderr=expected_stderr)
+
+
+class StepsTest(unittest.TestCase):
+ def _run_step(self, step, tool=None, options=None, state=None):
+ if not tool:
+ tool = MockBugzillaTool()
+ if not options:
+ options = Mock()
+ if not state:
+ state = {}
+ step(tool, options).run(state)
+
+ def test_update_step(self):
+ options = Mock()
+ options.update = True
+ self._run_step(UpdateStep, options)
+
+ def test_prompt_for_bug_or_title_step(self):
+ tool = MockBugzillaTool()
+ tool.user.prompt = lambda message: 42
+ self._run_step(PromptForBugOrTitleStep, tool=tool)
diff --git a/WebKitTools/Scripts/modules/changelogs.py b/WebKitTools/Scripts/webkitpy/changelogs.py
similarity index 100%
rename from WebKitTools/Scripts/modules/changelogs.py
rename to WebKitTools/Scripts/webkitpy/changelogs.py
diff --git a/WebKitTools/Scripts/modules/changelogs_unittest.py b/WebKitTools/Scripts/webkitpy/changelogs_unittest.py
similarity index 100%
rename from WebKitTools/Scripts/modules/changelogs_unittest.py
rename to WebKitTools/Scripts/webkitpy/changelogs_unittest.py
diff --git a/WebKitTools/Scripts/modules/commands/__init__.py b/WebKitTools/Scripts/webkitpy/commands/__init__.py
similarity index 100%
rename from WebKitTools/Scripts/modules/commands/__init__.py
rename to WebKitTools/Scripts/webkitpy/commands/__init__.py
diff --git a/WebKitTools/Scripts/webkitpy/commands/commandtest.py b/WebKitTools/Scripts/webkitpy/commands/commandtest.py
new file mode 100644
index 0000000..a56cb05
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/commandtest.py
@@ -0,0 +1,38 @@
+# Copyright (C) 2009 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.mock import Mock
+from webkitpy.mock_bugzillatool import MockBugzillaTool
+from webkitpy.outputcapture import OutputCapture
+
+class CommandsTest(unittest.TestCase):
+ def assert_execute_outputs(self, command, args, expected_stdout="", expected_stderr="", options=Mock(), tool=MockBugzillaTool()):
+ command.bind_to_tool(tool)
+ OutputCapture().assert_outputs(self, command.execute, [options, args, tool], expected_stdout=expected_stdout, expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/webkitpy/commands/download.py b/WebKitTools/Scripts/webkitpy/commands/download.py
new file mode 100644
index 0000000..8d4b2bf
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/download.py
@@ -0,0 +1,287 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple 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 os
+
+from optparse import make_option
+
+from webkitpy.bugzilla import parse_bug_id
+# We could instead use from modules import buildsteps and then prefix every buildstep with "buildsteps."
+from webkitpy.buildsteps import *
+from webkitpy.changelogs import ChangeLog
+from webkitpy.comments import bug_comment_from_commit_text
+from webkitpy.executive import ScriptError
+from webkitpy.grammar import pluralize
+from webkitpy.webkit_logging import error, log
+from webkitpy.multicommandtool import AbstractDeclarativeCommmand, Command
+from webkitpy.stepsequence import StepSequence
+
+
+# FIXME: Move this to a more general location.
+class AbstractSequencedCommmand(AbstractDeclarativeCommmand):
+ steps = None
+ def __init__(self):
+ self._sequence = StepSequence(self.steps)
+ AbstractDeclarativeCommmand.__init__(self, self._sequence.options())
+
+ def _prepare_state(self, options, args, tool):
+ return None
+
+ def execute(self, options, args, tool):
+ self._sequence.run_and_handle_errors(tool, options, self._prepare_state(options, args, tool))
+
+
+class Build(AbstractSequencedCommmand):
+ name = "build"
+ help_text = "Update working copy and build"
+ steps = [
+ CleanWorkingDirectoryStep,
+ UpdateStep,
+ BuildStep,
+ ]
+
+
+class BuildAndTest(AbstractSequencedCommmand):
+ name = "build-and-test"
+ help_text = "Update working copy, build, and run the tests"
+ steps = [
+ CleanWorkingDirectoryStep,
+ UpdateStep,
+ BuildStep,
+ RunTestsStep,
+ ]
+
+
+class LandDiff(AbstractSequencedCommmand):
+ name = "land-diff"
+ help_text = "Land the current working directory diff and updates the associated bug if any"
+ argument_names = "[BUGID]"
+ show_in_main_help = True
+ steps = [
+ EnsureBuildersAreGreenStep,
+ UpdateChangeLogsWithReviewerStep,
+ EnsureBuildersAreGreenStep,
+ BuildStep,
+ RunTestsStep,
+ CommitStep,
+ CloseBugForLandDiffStep,
+ ]
+
+ def _prepare_state(self, options, args, tool):
+ return {
+ "patch" : {
+ "id" : None,
+ "bug_id" : (args and args[0]) or parse_bug_id(tool.scm().create_patch()),
+ }
+ }
+
+
+class AbstractPatchProcessingCommand(AbstractDeclarativeCommmand):
+ # Subclasses must implement the methods below. We don't declare them here
+ # because we want to be able to implement them with mix-ins.
+ #
+ # def _fetch_list_of_patches_to_process(self, options, args, tool):
+ # def _prepare_to_process(self, options, args, tool):
+
+ @staticmethod
+ def _collect_patches_by_bug(patches):
+ bugs_to_patches = {}
+ for patch in patches:
+ bug_id = patch["bug_id"]
+ bugs_to_patches[bug_id] = bugs_to_patches.get(bug_id, []) + [patch]
+ return bugs_to_patches
+
+ def execute(self, options, args, tool):
+ self._prepare_to_process(options, args, tool)
+ patches = self._fetch_list_of_patches_to_process(options, args, tool)
+
+ # It's nice to print out total statistics.
+ bugs_to_patches = self._collect_patches_by_bug(patches)
+ log("Processing %s from %s." % (pluralize("patch", len(patches)), pluralize("bug", len(bugs_to_patches))))
+
+ for patch in patches:
+ self._process_patch(patch, options, args, tool)
+
+
+class AbstractPatchSequencingCommand(AbstractPatchProcessingCommand):
+ prepare_steps = None
+ main_steps = None
+
+ def __init__(self):
+ options = []
+ self._prepare_sequence = StepSequence(self.prepare_steps)
+ self._main_sequence = StepSequence(self.main_steps)
+ options = sorted(set(self._prepare_sequence.options() + self._main_sequence.options()))
+ AbstractPatchProcessingCommand.__init__(self, options)
+
+ def _prepare_to_process(self, options, args, tool):
+ self._prepare_sequence.run_and_handle_errors(tool, options)
+
+ def _process_patch(self, patch, options, args, tool):
+ state = { "patch" : patch }
+ self._main_sequence.run_and_handle_errors(tool, options, state)
+
+
+class ProcessAttachmentsMixin(object):
+ def _fetch_list_of_patches_to_process(self, options, args, tool):
+ return map(lambda patch_id: tool.bugs.fetch_attachment(patch_id), args)
+
+
+class ProcessBugsMixin(object):
+ def _fetch_list_of_patches_to_process(self, options, args, tool):
+ all_patches = []
+ for bug_id in args:
+ patches = tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
+ log("%s found on bug %s." % (pluralize("reviewed patch", len(patches)), bug_id))
+ all_patches += patches
+ return all_patches
+
+
+class CheckStyle(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
+ name = "check-style"
+ help_text = "Run check-webkit-style on the specified attachments"
+ argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+ main_steps = [
+ CleanWorkingDirectoryStep,
+ UpdateStep,
+ ApplyPatchStep,
+ CheckStyleStep,
+ ]
+
+
+class BuildAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
+ name = "build-attachment"
+ help_text = "Apply and build patches from bugzilla"
+ argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+ main_steps = [
+ CleanWorkingDirectoryStep,
+ UpdateStep,
+ ApplyPatchStep,
+ BuildStep,
+ ]
+
+
+class AbstractPatchApplyingCommand(AbstractPatchSequencingCommand):
+ prepare_steps = [
+ EnsureLocalCommitIfNeeded,
+ CleanWorkingDirectoryWithLocalCommitsStep,
+ UpdateStep,
+ ]
+ main_steps = [
+ ApplyPatchWithLocalCommitStep,
+ ]
+
+
+class ApplyAttachment(AbstractPatchApplyingCommand, ProcessAttachmentsMixin):
+ name = "apply-attachment"
+ help_text = "Apply an attachment to the local working directory"
+ argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+ show_in_main_help = True
+
+
+class ApplyPatches(AbstractPatchApplyingCommand, ProcessBugsMixin):
+ name = "apply-patches"
+ help_text = "Apply reviewed patches from provided bugs to the local working directory"
+ argument_names = "BUGID [BUGIDS]"
+ show_in_main_help = True
+
+
+class AbstractPatchLandingCommand(AbstractPatchSequencingCommand):
+ prepare_steps = [
+ EnsureBuildersAreGreenStep,
+ ]
+ main_steps = [
+ CleanWorkingDirectoryStep,
+ UpdateStep,
+ ApplyPatchStep,
+ EnsureBuildersAreGreenStep,
+ BuildStep,
+ RunTestsStep,
+ CommitStep,
+ ClosePatchStep,
+ CloseBugStep,
+ ]
+
+
+class LandAttachment(AbstractPatchLandingCommand, ProcessAttachmentsMixin):
+ name = "land-attachment"
+ help_text = "Land patches from bugzilla, optionally building and testing them first"
+ argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+ show_in_main_help = True
+
+
+class LandPatches(AbstractPatchLandingCommand, ProcessBugsMixin):
+ name = "land-patches"
+ help_text = "Land all patches on the given bugs, optionally building and testing them first"
+ argument_names = "BUGID [BUGIDS]"
+ show_in_main_help = True
+
+
+# FIXME: Make Rollout more declarative.
+class Rollout(Command):
+ name = "rollout"
+ show_in_main_help = True
+ def __init__(self):
+ self._sequence = StepSequence([
+ CleanWorkingDirectoryStep,
+ UpdateStep,
+ RevertRevisionStep,
+ PrepareChangeLogForRevertStep,
+ CompleteRollout,
+ ])
+ Command.__init__(self, "Revert the given revision in the working copy and optionally commit the revert and re-open the original bug", "REVISION [BUGID]", options=self._sequence.options())
+
+ @staticmethod
+ def _parse_bug_id_from_revision_diff(tool, revision):
+ original_diff = tool.scm().diff_for_revision(revision)
+ return parse_bug_id(original_diff)
+
+ @staticmethod
+ def _reopen_bug_after_rollout(tool, bug_id, comment_text):
+ if bug_id:
+ tool.bugs.reopen_bug(bug_id, comment_text)
+ else:
+ log(comment_text)
+ log("No bugs were updated or re-opened to reflect this rollout.")
+
+ def execute(self, options, args, tool):
+ revision = args[0]
+ bug_id = self._parse_bug_id_from_revision_diff(tool, revision)
+ if options.complete_rollout:
+ if bug_id:
+ log("Will re-open bug %s after rollout." % bug_id)
+ else:
+ log("Failed to parse bug number from diff. No bugs will be updated/reopened after the rollout.")
+
+ state = {
+ "revision": revision,
+ "bug_id": bug_id,
+ }
+ self._sequence.run_and_handle_errors(tool, options, state)
diff --git a/WebKitTools/Scripts/webkitpy/commands/download_unittest.py b/WebKitTools/Scripts/webkitpy/commands/download_unittest.py
new file mode 100644
index 0000000..d914425
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/download_unittest.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2009 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.commands.commandtest import CommandsTest
+from webkitpy.commands.download import *
+from webkitpy.mock import Mock
+
+class DownloadCommandsTest(CommandsTest):
+ def _default_options(self):
+ options = Mock()
+ options.force_clean = False
+ options.clean = True
+ options.check_builders = True
+ options.quiet = False
+ options.non_interactive = False
+ options.update = True
+ options.build = True
+ options.test = True
+ options.close_bug = True
+ options.complete_rollout = False
+ return options
+
+ def test_build(self):
+ expected_stderr = "Updating working directory\nBuilding WebKit\n"
+ self.assert_execute_outputs(Build(), [], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_build_and_test(self):
+ expected_stderr = "Updating working directory\nBuilding WebKit\n"
+ self.assert_execute_outputs(BuildAndTest(), [], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_apply_attachment(self):
+ options = self._default_options()
+ options.update = True
+ options.local_commit = True
+ expected_stderr = "Updating working directory\nProcessing 1 patch from 1 bug.\nProcessing patch 197 from bug 42.\n"
+ self.assert_execute_outputs(ApplyAttachment(), [197], options=options, expected_stderr=expected_stderr)
+
+ def test_apply_patches(self):
+ options = self._default_options()
+ options.update = True
+ options.local_commit = True
+ expected_stderr = "Updating working directory\n2 reviewed patches found on bug 42.\nProcessing 2 patches from 1 bug.\nProcessing patch 197 from bug 42.\nProcessing patch 128 from bug 42.\n"
+ self.assert_execute_outputs(ApplyPatches(), [42], options=options, expected_stderr=expected_stderr)
+
+ def test_land_diff(self):
+ expected_stderr = "Building WebKit\nUpdating bug 42\n"
+ self.assert_execute_outputs(LandDiff(), [42], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_check_style(self):
+ expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nRunning check-webkit-style\n"
+ self.assert_execute_outputs(CheckStyle(), [197], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_build_attachment(self):
+ expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nBuilding WebKit\n"
+ self.assert_execute_outputs(BuildAttachment(), [197], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_land_attachment(self):
+ expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nBuilding WebKit\n"
+ self.assert_execute_outputs(LandAttachment(), [197], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_land_patches(self):
+ expected_stderr = "2 reviewed patches found on bug 42.\nProcessing 2 patches from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nBuilding WebKit\nUpdating working directory\nProcessing patch 128 from bug 42.\nBuilding WebKit\n"
+ self.assert_execute_outputs(LandPatches(), [42], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_rollout(self):
+ expected_stderr = "Updating working directory\nRunning prepare-ChangeLog\n\nNOTE: Rollout support is experimental.\nPlease verify the rollout diff and use \"bugzilla-tool land-diff 12345\" to commit the rollout.\n"
+ self.assert_execute_outputs(Rollout(), [852], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_complete_rollout(self):
+ options = self._default_options()
+ options.complete_rollout = True
+ expected_stderr = "Will re-open bug 12345 after rollout.\nUpdating working directory\nRunning prepare-ChangeLog\nBuilding WebKit\n"
+ self.assert_execute_outputs(Rollout(), [852], options=options, expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/webkitpy/commands/early_warning_system.py b/WebKitTools/Scripts/webkitpy/commands/early_warning_system.py
new file mode 100644
index 0000000..0d69c49
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/early_warning_system.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, 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.
+
+from StringIO import StringIO
+
+from webkitpy.commands.queues import AbstractReviewQueue
+from webkitpy.committers import CommitterList
+from webkitpy.executive import ScriptError
+from webkitpy.webkitport import WebKitPort
+
+
+class AbstractEarlyWarningSystem(AbstractReviewQueue):
+ def __init__(self):
+ AbstractReviewQueue.__init__(self)
+ self.port = WebKitPort.port(self.port_name)
+
+ def should_proceed_with_work_item(self, patch):
+ try:
+ self.run_bugzilla_tool(["build", self.port.flag(), "--force-clean", "--quiet"])
+ self._update_status("Building", patch)
+ except ScriptError, e:
+ self._update_status("Unable to perform a build")
+ return False
+ return True
+
+ def process_work_item(self, patch):
+ try:
+ self.run_bugzilla_tool([
+ "build-attachment",
+ self.port.flag(),
+ "--force-clean",
+ "--quiet",
+ "--non-interactive",
+ "--parent-command=%s" % self.name,
+ "--no-update",
+ patch["id"]])
+ self._did_pass(patch)
+ except ScriptError, e:
+ self._did_fail(patch)
+ raise e
+
+ @classmethod
+ def handle_script_error(cls, tool, state, script_error):
+ status_id = cls._update_status_for_script_error(tool, state, script_error)
+ # FIXME: This won't be right for ports that don't use build-webkit!
+ if not script_error.command_name() == "build-webkit":
+ return
+ results_link = tool.status_bot.results_url_for_status(status_id)
+ message = "Attachment %s did not build on %s:\nBuild output: %s" % (state["patch"]["id"], cls.port_name, results_link)
+ tool.bugs.post_comment_to_bug(state["patch"]["bug_id"], message, cc=cls.watchers)
+
+
+class GtkEWS(AbstractEarlyWarningSystem):
+ name = "gtk-ews"
+ port_name = "gtk"
+
+
+class QtEWS(AbstractEarlyWarningSystem):
+ name = "qt-ews"
+ port_name = "qt"
+
+
+class ChromiumEWS(AbstractEarlyWarningSystem):
+ name = "chromium-ews"
+ port_name = "chromium"
+ watchers = AbstractEarlyWarningSystem.watchers + [
+ "dglazkov at chromium.org",
+ ]
+
+
+# For platforms that we can't run inside a VM (like Mac OS X), we require
+# patches to be uploaded by committers, who are generally trustworthy folk. :)
+class AbstractCommitterOnlyEWS(AbstractEarlyWarningSystem):
+ def __init__(self, committers=CommitterList()):
+ AbstractEarlyWarningSystem.__init__(self)
+ self._committers = committers
+
+ def process_work_item(self, patch):
+ if not self._committers.committer_by_email(patch["attacher_email"]):
+ self._did_error(patch, "%s cannot process patches from non-committers :(" % self.name)
+ return
+ AbstractEarlyWarningSystem.process_work_item(self, patch)
+
+
+class MacEWS(AbstractCommitterOnlyEWS):
+ name = "mac-ews"
+ port_name = "mac"
diff --git a/WebKitTools/Scripts/webkitpy/commands/early_warning_system_unittest.py b/WebKitTools/Scripts/webkitpy/commands/early_warning_system_unittest.py
new file mode 100644
index 0000000..dec8981
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/early_warning_system_unittest.py
@@ -0,0 +1,63 @@
+# Copyright (C) 2009 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 os
+import unittest
+
+from webkitpy.commands.early_warning_system import *
+from webkitpy.commands.queuestest import QueuesTest
+from webkitpy.mock import Mock
+
+class EarlyWarningSytemTest(QueuesTest):
+ def test_chromium_ews(self):
+ expected_stderr = {
+ "begin_work_queue" : "CAUTION: chromium-ews will discard all local changes in \"%s\"\nRunning WebKit chromium-ews.\n" % os.getcwd(),
+ "handle_unexpected_error" : "Mock error message\n",
+ }
+ self.assert_queue_outputs(ChromiumEWS(), expected_stderr=expected_stderr)
+
+ def test_qt_ews(self):
+ expected_stderr = {
+ "begin_work_queue" : "CAUTION: qt-ews will discard all local changes in \"%s\"\nRunning WebKit qt-ews.\n" % os.getcwd(),
+ "handle_unexpected_error" : "Mock error message\n",
+ }
+ self.assert_queue_outputs(QtEWS(), expected_stderr=expected_stderr)
+
+ def test_gtk_ews(self):
+ expected_stderr = {
+ "begin_work_queue" : "CAUTION: gtk-ews will discard all local changes in \"%s\"\nRunning WebKit gtk-ews.\n" % os.getcwd(),
+ "handle_unexpected_error" : "Mock error message\n",
+ }
+ self.assert_queue_outputs(GtkEWS(), expected_stderr=expected_stderr)
+
+ def test_mac_ews(self):
+ expected_stderr = {
+ "begin_work_queue" : "CAUTION: mac-ews will discard all local changes in \"%s\"\nRunning WebKit mac-ews.\n" % os.getcwd(),
+ "handle_unexpected_error" : "Mock error message\n",
+ }
+ self.assert_queue_outputs(MacEWS(), expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/webkitpy/commands/queries.py b/WebKitTools/Scripts/webkitpy/commands/queries.py
new file mode 100644
index 0000000..c9577ba
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/queries.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple 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.
+
+
+from optparse import make_option
+
+from webkitpy.buildbot import BuildBot
+from webkitpy.committers import CommitterList
+from webkitpy.webkit_logging import log
+from webkitpy.multicommandtool import Command
+
+
+class BugsToCommit(Command):
+ name = "bugs-to-commit"
+ def __init__(self):
+ Command.__init__(self, "List bugs in the commit-queue")
+
+ def execute(self, options, args, tool):
+ bug_ids = tool.bugs.queries.fetch_bug_ids_from_commit_queue()
+ for bug_id in bug_ids:
+ print "%s" % bug_id
+
+
+class PatchesToCommit(Command):
+ name = "patches-to-commit"
+ def __init__(self):
+ Command.__init__(self, "List patches in the commit-queue")
+
+ def execute(self, options, args, tool):
+ patches = tool.bugs.queries.fetch_patches_from_commit_queue()
+ log("Patches in commit queue:")
+ for patch in patches:
+ print "%s" % patch["url"]
+
+
+class PatchesToCommitQueue(Command):
+ name = "patches-to-commit-queue"
+ def __init__(self):
+ options = [
+ make_option("--bugs", action="store_true", dest="bugs", help="Output bug links instead of patch links"),
+ ]
+ Command.__init__(self, "Patches which should be added to the commit queue", options=options)
+
+ @staticmethod
+ def _needs_commit_queue(patch):
+ commit_queue_flag = patch.get("commit-queue")
+ if (commit_queue_flag and commit_queue_flag == '+'): # If it's already cq+, ignore the patch.
+ log("%s already has cq=%s" % (patch["id"], commit_queue_flag))
+ return False
+
+ # We only need to worry about patches from contributers who are not yet committers.
+ committer_record = CommitterList().committer_by_email(patch["attacher_email"])
+ if committer_record:
+ log("%s committer = %s" % (patch["id"], committer_record))
+ return not committer_record
+
+ def execute(self, options, args, tool):
+ patches = tool.bugs.queries.fetch_patches_from_pending_commit_list()
+ patches_needing_cq = filter(self._needs_commit_queue, patches)
+ if options.bugs:
+ bugs_needing_cq = map(lambda patch: patch['bug_id'], patches_needing_cq)
+ bugs_needing_cq = sorted(set(bugs_needing_cq))
+ for bug_id in bugs_needing_cq:
+ print "%s" % tool.bugs.bug_url_for_bug_id(bug_id)
+ else:
+ for patch in patches_needing_cq:
+ print "%s" % tool.bugs.attachment_url_for_id(patch["id"], action="edit")
+
+
+class PatchesToReview(Command):
+ name = "patches-to-review"
+ def __init__(self):
+ Command.__init__(self, "List patches that are pending review")
+
+ def execute(self, options, args, tool):
+ patch_ids = tool.bugs.queries.fetch_attachment_ids_from_review_queue()
+ log("Patches pending review:")
+ for patch_id in patch_ids:
+ print patch_id
+
+
+class ReviewedPatches(Command):
+ name = "reviewed-patches"
+ def __init__(self):
+ Command.__init__(self, "List r+'d patches on a bug", "BUGID")
+
+ def execute(self, options, args, tool):
+ bug_id = args[0]
+ patches_to_land = tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
+ for patch in patches_to_land:
+ print "%s" % patch["url"]
+
+
+class TreeStatus(Command):
+ name = "tree-status"
+ show_in_main_help = True
+ def __init__(self):
+ Command.__init__(self, "Print the status of the %s buildbots" % BuildBot.default_host)
+
+ def execute(self, options, args, tool):
+ for builder in tool.buildbot.builder_statuses():
+ status_string = "ok" if builder["is_green"] else "FAIL"
+ print "%s : %s" % (status_string.ljust(4), builder["name"])
diff --git a/WebKitTools/Scripts/webkitpy/commands/queries_unittest.py b/WebKitTools/Scripts/webkitpy/commands/queries_unittest.py
new file mode 100644
index 0000000..c7fd7a8
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/queries_unittest.py
@@ -0,0 +1,68 @@
+# Copyright (C) 2009 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.bugzilla import Bugzilla
+from webkitpy.commands.commandtest import CommandsTest
+from webkitpy.commands.queries import *
+from webkitpy.mock import Mock
+from webkitpy.mock_bugzillatool import MockBugzillaTool
+
+class QueryCommandsTest(CommandsTest):
+ def test_bugs_to_commit(self):
+ self.assert_execute_outputs(BugsToCommit(), None, "42\n75\n")
+
+ def test_patches_to_commit(self):
+ expected_stdout = "http://example.com/197\nhttp://example.com/128\n"
+ expected_stderr = "Patches in commit queue:\n"
+ self.assert_execute_outputs(PatchesToCommit(), None, expected_stdout, expected_stderr)
+
+ def test_patches_to_commit_queue(self):
+ expected_stdout = "http://example.com/197&action=edit\n"
+ expected_stderr = "128 committer = \"Eric Seidel\" <eric at webkit.org>\n"
+ options = Mock()
+ options.bugs = False
+ self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options)
+
+ expected_stdout = "http://example.com/42\n"
+ options.bugs = True
+ self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options)
+
+ def test_patches_to_review(self):
+ expected_stdout = "197\n128\n"
+ expected_stderr = "Patches pending review:\n"
+ self.assert_execute_outputs(PatchesToReview(), None, expected_stdout, expected_stderr)
+
+ def test_reviewed_patches(self):
+ expected_stdout = "http://example.com/197\nhttp://example.com/128\n"
+ self.assert_execute_outputs(ReviewedPatches(), [42], expected_stdout)
+
+ def test_tree_status(self):
+ expected_stdout = "ok : Builder1\nok : Builder2\n"
+ self.assert_execute_outputs(TreeStatus(), None, expected_stdout)
diff --git a/WebKitTools/Scripts/webkitpy/commands/queues.py b/WebKitTools/Scripts/webkitpy/commands/queues.py
new file mode 100644
index 0000000..e6f4f1b
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/queues.py
@@ -0,0 +1,252 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple 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 os
+
+from datetime import datetime
+from optparse import make_option
+from StringIO import StringIO
+
+from webkitpy.executive import ScriptError
+from webkitpy.grammar import pluralize
+from webkitpy.webkit_logging import error, log
+from webkitpy.multicommandtool import Command
+from webkitpy.patchcollection import PersistentPatchCollection, PersistentPatchCollectionDelegate
+from webkitpy.statusbot import StatusBot
+from webkitpy.stepsequence import StepSequenceErrorHandler
+from webkitpy.queueengine import QueueEngine, QueueEngineDelegate
+
+class AbstractQueue(Command, QueueEngineDelegate):
+ watchers = [
+ "webkit-bot-watchers at googlegroups.com",
+ ]
+
+ _pass_status = "Pass"
+ _fail_status = "Fail"
+ _error_status = "Error"
+
+ def __init__(self, options=None): # Default values should never be collections (like []) as default values are shared between invocations
+ options_list = (options or []) + [
+ make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Do not ask the user for confirmation before running the queue. Dangerous!"),
+ ]
+ Command.__init__(self, "Run the %s" % self.name, options=options_list)
+
+ def _cc_watchers(self, bug_id):
+ try:
+ self.tool.bugs.add_cc_to_bug(bug_id, self.watchers)
+ except Exception, e:
+ log("Failed to CC watchers: %s." % e)
+
+ def _update_status(self, message, patch=None, results_file=None):
+ self.tool.status_bot.update_status(self.name, message, patch, results_file)
+
+ def _did_pass(self, patch):
+ self._update_status(self._pass_status, patch)
+
+ def _did_fail(self, patch):
+ self._update_status(self._fail_status, patch)
+
+ def _did_error(self, patch, reason):
+ message = "%s: %s" % (self._error_status, reason)
+ self._update_status(message, patch)
+
+ def queue_log_path(self):
+ return "%s.log" % self.name
+
+ def work_item_log_path(self, patch):
+ return os.path.join("%s-logs" % self.name, "%s.log" % patch["bug_id"])
+
+ def begin_work_queue(self):
+ log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self.tool.scm().checkout_root))
+ if self.options.confirm:
+ response = raw_input("Are you sure? Type \"yes\" to continue: ")
+ if (response != "yes"):
+ error("User declined.")
+ log("Running WebKit %s." % self.name)
+
+ def should_continue_work_queue(self):
+ return True
+
+ def next_work_item(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def should_proceed_with_work_item(self, work_item):
+ raise NotImplementedError, "subclasses must implement"
+
+ def process_work_item(self, work_item):
+ raise NotImplementedError, "subclasses must implement"
+
+ def handle_unexpected_error(self, work_item, message):
+ raise NotImplementedError, "subclasses must implement"
+
+ def run_bugzilla_tool(self, args):
+ bugzilla_tool_args = [self.tool.path()]
+ # FIXME: This is a hack, we should have a more general way to pass global options.
+ bugzilla_tool_args += ["--status-host=%s" % self.tool.status_bot.statusbot_host]
+ bugzilla_tool_args += map(str, args)
+ self.tool.executive.run_and_throw_if_fail(bugzilla_tool_args)
+
+ def log_progress(self, patch_ids):
+ log("%s in %s [%s]" % (pluralize("patch", len(patch_ids)), self.name, ", ".join(map(str, patch_ids))))
+
+ def execute(self, options, args, tool, engine=QueueEngine):
+ self.options = options
+ self.tool = tool
+ return engine(self.name, self).run()
+
+ @classmethod
+ def _update_status_for_script_error(cls, tool, state, script_error):
+ return tool.status_bot.update_status(cls.name, script_error.message, state["patch"], StringIO(script_error.output))
+
+
+class CommitQueue(AbstractQueue, StepSequenceErrorHandler):
+ name = "commit-queue"
+ def __init__(self):
+ AbstractQueue.__init__(self)
+
+ # AbstractQueue methods
+
+ def begin_work_queue(self):
+ AbstractQueue.begin_work_queue(self)
+
+ def next_work_item(self):
+ patches = self.tool.bugs.queries.fetch_patches_from_commit_queue(reject_invalid_patches=True)
+ if not patches:
+ self._update_status("Empty queue")
+ return None
+ # Only bother logging if we have patches in the queue.
+ self.log_progress([patch['id'] for patch in patches])
+ return patches[0]
+
+ def should_proceed_with_work_item(self, patch):
+ red_builders_names = self.tool.buildbot.red_core_builders_names()
+ if red_builders_names:
+ red_builders_names = map(lambda name: "\"%s\"" % name, red_builders_names) # Add quotes around the names.
+ self._update_status("Builders [%s] are red. See http://build.webkit.org" % ", ".join(red_builders_names), None)
+ return False
+ self._update_status("Landing patch", patch)
+ return True
+
+ def process_work_item(self, patch):
+ try:
+ self._cc_watchers(patch["bug_id"])
+ self.run_bugzilla_tool(["land-attachment", "--force-clean", "--non-interactive", "--parent-command=commit-queue", "--build-style=both", "--quiet", patch["id"]])
+ self._did_pass(patch)
+ except ScriptError, e:
+ self._did_fail(patch)
+ raise e
+
+ def handle_unexpected_error(self, patch, message):
+ self.tool.bugs.reject_patch_from_commit_queue(patch["id"], message)
+
+ # StepSequenceErrorHandler methods
+
+ @staticmethod
+ def _error_message_for_bug(tool, status_id, script_error):
+ if not script_error.output:
+ return script_error.message_with_output()
+ results_link = tool.status_bot.results_url_for_status(status_id)
+ return "%s\nFull output: %s" % (script_error.message_with_output(), results_link)
+
+ @classmethod
+ def handle_script_error(cls, tool, state, script_error):
+ status_id = cls._update_status_for_script_error(tool, state, script_error)
+ tool.bugs.reject_patch_from_commit_queue(state["patch"]["id"], cls._error_message_for_bug(tool, status_id, script_error))
+
+
+class AbstractReviewQueue(AbstractQueue, PersistentPatchCollectionDelegate, StepSequenceErrorHandler):
+ def __init__(self, options=None):
+ AbstractQueue.__init__(self, options)
+
+ # PersistentPatchCollectionDelegate methods
+
+ def collection_name(self):
+ return self.name
+
+ def fetch_potential_patch_ids(self):
+ return self.tool.bugs.queries.fetch_attachment_ids_from_review_queue()
+
+ def status_server(self):
+ return self.tool.status_bot
+
+ # AbstractQueue methods
+
+ def begin_work_queue(self):
+ AbstractQueue.begin_work_queue(self)
+ self._patches = PersistentPatchCollection(self)
+
+ def next_work_item(self):
+ patch_id = self._patches.next()
+ if patch_id:
+ return self.tool.bugs.fetch_attachment(patch_id)
+ self._update_status("Empty queue")
+
+ def should_proceed_with_work_item(self, patch):
+ raise NotImplementedError, "subclasses must implement"
+
+ def process_work_item(self, patch):
+ raise NotImplementedError, "subclasses must implement"
+
+ def handle_unexpected_error(self, patch, message):
+ log(message)
+
+ # StepSequenceErrorHandler methods
+
+ @classmethod
+ def handle_script_error(cls, tool, state, script_error):
+ log(script_error.message_with_output())
+
+
+class StyleQueue(AbstractReviewQueue):
+ name = "style-queue"
+ def __init__(self):
+ AbstractReviewQueue.__init__(self)
+
+ def should_proceed_with_work_item(self, patch):
+ self._update_status("Checking style", patch)
+ return True
+
+ def process_work_item(self, patch):
+ try:
+ self.run_bugzilla_tool(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch["id"]])
+ message = "%s ran check-webkit-style on attachment %s without any errors." % (self.name, patch["id"])
+ self.tool.bugs.post_comment_to_bug(patch["bug_id"], message, cc=self.watchers)
+ self._did_pass(patch)
+ except ScriptError, e:
+ self._did_fail(patch)
+ raise e
+
+ @classmethod
+ def handle_script_error(cls, tool, state, script_error):
+ status_id = cls._update_status_for_script_error(tool, state, script_error)
+ if not script_error.command_name() == "check-webkit-style":
+ return
+ message = "Attachment %s did not pass %s:\n\n%s" % (state["patch"]["id"], cls.name, script_error.message_with_output(output_limit=3*1024))
+ tool.bugs.post_comment_to_bug(state["patch"]["bug_id"], message, cc=cls.watchers)
diff --git a/WebKitTools/Scripts/webkitpy/commands/queues_unittest.py b/WebKitTools/Scripts/webkitpy/commands/queues_unittest.py
new file mode 100644
index 0000000..b985254
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/queues_unittest.py
@@ -0,0 +1,81 @@
+# Copyright (C) 2009 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 os
+import unittest
+
+from webkitpy.commands.commandtest import CommandsTest
+from webkitpy.commands.queues import *
+from webkitpy.commands.queuestest import QueuesTest
+from webkitpy.mock_bugzillatool import MockBugzillaTool
+from webkitpy.outputcapture import OutputCapture
+
+
+class TestQueue(AbstractQueue):
+ name = "test-queue"
+
+
+class AbstractQueueTest(CommandsTest):
+ def _assert_log_progress_output(self, patch_ids, progress_output):
+ OutputCapture().assert_outputs(self, TestQueue().log_progress, [patch_ids], expected_stderr=progress_output)
+
+ def test_log_progress(self):
+ self._assert_log_progress_output([1,2,3], "3 patches in test-queue [1, 2, 3]\n")
+ self._assert_log_progress_output(["1","2","3"], "3 patches in test-queue [1, 2, 3]\n")
+ self._assert_log_progress_output([1], "1 patch in test-queue [1]\n")
+
+ def _assert_run_bugzilla_tool(self, run_args):
+ queue = TestQueue()
+ tool = MockBugzillaTool()
+ queue.bind_to_tool(tool)
+
+ queue.run_bugzilla_tool(run_args)
+ expected_run_args = ["echo", "--status-host=example.com"] + map(str, run_args)
+ tool.executive.run_and_throw_if_fail.assert_called_with(expected_run_args)
+
+ def test_run_bugzilla_tool(self):
+ self._assert_run_bugzilla_tool([1])
+ self._assert_run_bugzilla_tool(["one", 2])
+
+
+class CommitQueueTest(QueuesTest):
+ def test_style_queue(self):
+ expected_stderr = {
+ "begin_work_queue" : "CAUTION: commit-queue will discard all local changes in \"%s\"\nRunning WebKit commit-queue.\n" % os.getcwd(),
+ "next_work_item" : "2 patches in commit-queue [197, 128]\n",
+ }
+ self.assert_queue_outputs(CommitQueue(), expected_stderr=expected_stderr)
+
+
+class StyleQueueTest(QueuesTest):
+ def test_style_queue(self):
+ expected_stderr = {
+ "begin_work_queue" : "CAUTION: style-queue will discard all local changes in \"%s\"\nRunning WebKit style-queue.\n" % os.getcwd(),
+ "handle_unexpected_error" : "Mock error message\n",
+ }
+ self.assert_queue_outputs(StyleQueue(), expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/webkitpy/commands/queuestest.py b/WebKitTools/Scripts/webkitpy/commands/queuestest.py
new file mode 100644
index 0000000..a525fcc
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/queuestest.py
@@ -0,0 +1,98 @@
+# Copyright (C) 2009 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.mock import Mock
+from webkitpy.mock_bugzillatool import MockBugzillaTool
+from webkitpy.outputcapture import OutputCapture
+
+
+class MockQueueEngine(object):
+ def __init__(self, name, queue):
+ pass
+
+ def run(self):
+ pass
+
+
+class QueuesTest(unittest.TestCase):
+ mock_work_item = {
+ "id" : 1234,
+ "bug_id" : 345,
+ "attacher_email": "adam at example.com",
+ }
+
+ def assert_queue_outputs(self, queue, args=None, work_item=None, expected_stdout=None, expected_stderr=None, options=Mock(), tool=MockBugzillaTool()):
+ if not expected_stdout:
+ expected_stdout = {}
+ if not expected_stderr:
+ expected_stderr = {}
+ if not args:
+ args = []
+ if not work_item:
+ work_item = self.mock_work_item
+ options.confirm = False # FIXME: We should have a tool.user that we can mock.
+
+ queue.execute(options, args, tool, engine=MockQueueEngine)
+
+ OutputCapture().assert_outputs(self,
+ queue.queue_log_path,
+ expected_stdout=expected_stdout.get("queue_log_path", ""),
+ expected_stderr=expected_stderr.get("queue_log_path", ""))
+ OutputCapture().assert_outputs(self,
+ queue.work_item_log_path,
+ args=[work_item],
+ expected_stdout=expected_stdout.get("work_item_log_path", ""),
+ expected_stderr=expected_stderr.get("work_item_log_path", ""))
+ OutputCapture().assert_outputs(self,
+ queue.begin_work_queue,
+ expected_stdout=expected_stdout.get("begin_work_queue", ""),
+ expected_stderr=expected_stderr.get("begin_work_queue", ""))
+ OutputCapture().assert_outputs(self,
+ queue.should_continue_work_queue,
+ expected_stdout=expected_stdout.get("should_continue_work_queue", ""), expected_stderr=expected_stderr.get("should_continue_work_queue", ""))
+ OutputCapture().assert_outputs(self,
+ queue.next_work_item,
+ expected_stdout=expected_stdout.get("next_work_item", ""),
+ expected_stderr=expected_stderr.get("next_work_item", ""))
+ OutputCapture().assert_outputs(self,
+ queue.should_proceed_with_work_item,
+ args=[work_item],
+ expected_stdout=expected_stdout.get("should_proceed_with_work_item", ""),
+ expected_stderr=expected_stderr.get("should_proceed_with_work_item", ""))
+ OutputCapture().assert_outputs(self,
+ queue.process_work_item,
+ args=[work_item],
+ expected_stdout=expected_stdout.get("process_work_item", ""),
+ expected_stderr=expected_stderr.get("process_work_item", ""))
+ OutputCapture().assert_outputs(self,
+ queue.handle_unexpected_error,
+ args=[work_item, "Mock error message"],
+ expected_stdout=expected_stdout.get("handle_unexpected_error", ""),
+ expected_stderr=expected_stderr.get("handle_unexpected_error", ""))
diff --git a/WebKitTools/Scripts/webkitpy/commands/upload.py b/WebKitTools/Scripts/webkitpy/commands/upload.py
new file mode 100644
index 0000000..d552901
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/upload.py
@@ -0,0 +1,384 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple 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 os
+import re
+import StringIO
+import sys
+
+from optparse import make_option
+
+from webkitpy.bugzilla import parse_bug_id
+from webkitpy.buildsteps import PrepareChangeLogStep, EditChangeLogStep, ConfirmDiffStep, CommandOptions, ObsoletePatchesOnBugStep, PostDiffToBugStep, PromptForBugOrTitleStep, CreateBugStep
+from webkitpy.commands.download import AbstractSequencedCommmand
+from webkitpy.comments import bug_comment_from_svn_revision
+from webkitpy.committers import CommitterList
+from webkitpy.grammar import pluralize
+from webkitpy.webkit_logging import error, log
+from webkitpy.multicommandtool import Command, AbstractDeclarativeCommmand
+
+# FIXME: Requires unit test.
+class CommitMessageForCurrentDiff(Command):
+ name = "commit-message"
+ def __init__(self):
+ Command.__init__(self, "Print a commit message suitable for the uncommitted changes")
+
+ def execute(self, options, args, tool):
+ os.chdir(tool.scm().checkout_root)
+ print "%s" % tool.scm().commit_message_for_this_commit().message()
+
+
+class AssignToCommitter(AbstractDeclarativeCommmand):
+ name = "assign-to-committer"
+ help_text = "Assign bug to whoever attached the most recent r+'d patch"
+
+ def _assign_bug_to_last_patch_attacher(self, bug_id):
+ committers = CommitterList()
+ bug = self.tool.bugs.fetch_bug(bug_id)
+ assigned_to_email = bug.assigned_to_email()
+ if assigned_to_email != self.tool.bugs.unassigned_email:
+ log("Bug %s is already assigned to %s (%s)." % (bug_id, assigned_to_email, committers.committer_by_email(assigned_to_email)))
+ return
+
+ # FIXME: This should call a reviewed_patches() method on bug instead of re-fetching.
+ reviewed_patches = self.tool.bugs.fetch_reviewed_patches_from_bug(bug_id)
+ if not reviewed_patches:
+ log("Bug %s has no non-obsolete patches, ignoring." % bug_id)
+ return
+ latest_patch = reviewed_patches[-1]
+ attacher_email = latest_patch["attacher_email"]
+ committer = committers.committer_by_email(attacher_email)
+ if not committer:
+ log("Attacher %s is not a committer. Bug %s likely needs commit-queue+." % (attacher_email, bug_id))
+ return
+
+ reassign_message = "Attachment %s was posted by a committer and has review+, assigning to %s for commit." % (latest_patch["id"], committer.full_name)
+ self.tool.bugs.reassign_bug(bug_id, committer.bugzilla_email(), reassign_message)
+
+ def execute(self, options, args, tool):
+ for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list():
+ self._assign_bug_to_last_patch_attacher(bug_id)
+
+
+class ObsoleteAttachments(AbstractSequencedCommmand):
+ name = "obsolete-attachments"
+ help_text = "Mark all attachments on a bug as obsolete"
+ argument_names = "BUGID"
+ steps = [
+ ObsoletePatchesOnBugStep,
+ ]
+
+ def _prepare_state(self, options, args, tool):
+ return { "bug_id" : args[0] }
+
+
+class PostDiff(AbstractSequencedCommmand):
+ name = "post-diff"
+ help_text = "Attach the current working directory diff to a bug as a patch file"
+ argument_names = "[BUGID]"
+ show_in_main_help = True
+ steps = [
+ ConfirmDiffStep,
+ ObsoletePatchesOnBugStep,
+ PostDiffToBugStep,
+ ]
+
+ def _prepare_state(self, options, args, tool):
+ # Perfer a bug id passed as an argument over a bug url in the diff (i.e. ChangeLogs).
+ state = {}
+ bug_id = args and args[0]
+ if not bug_id:
+ state["diff"] = tool.scm().create_patch()
+ bug_id = parse_bug_id(state["diff"])
+ if not bug_id:
+ error("No bug id passed and no bug url found in diff, can't post.")
+ state["bug_id"] = bug_id
+ return state
+
+
+class PrepareDiff(AbstractSequencedCommmand):
+ name = "prepare-diff"
+ help_text = "Creates a bug (or prompts for an existing bug) and prepares the ChangeLogs"
+ argument_names = "[BUGID]"
+ steps = [
+ PromptForBugOrTitleStep,
+ CreateBugStep,
+ PrepareChangeLogStep,
+ ]
+
+ def _prepare_state(self, options, args, tool):
+ bug_id = args and args[0]
+ return { "bug_id" : bug_id }
+
+
+class CreateReview(AbstractSequencedCommmand):
+ name = "create-review"
+ help_text = "Adds a ChangeLog to the current diff and posts it to a (possibly new) bug"
+ argument_names = "[BUGID]"
+ steps = [
+ PromptForBugOrTitleStep,
+ CreateBugStep,
+ PrepareChangeLogStep,
+ EditChangeLogStep,
+ ConfirmDiffStep,
+ ObsoletePatchesOnBugStep,
+ PostDiffToBugStep,
+ ]
+
+ def _prepare_state(self, options, args, tool):
+ bug_id = args and args[0]
+ return { "bug_id" : bug_id }
+
+
+class EditChangeLog(AbstractSequencedCommmand):
+ name = "edit-changelog"
+ help_text = "Opens modified ChangeLogs in $EDITOR"
+ steps = [
+ EditChangeLogStep,
+ ]
+
+
+class PostCommits(Command):
+ name = "post-commits"
+ show_in_main_help = True
+ def __init__(self):
+ options = [
+ make_option("-b", "--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
+ make_option("--add-log-as-comment", action="store_true", dest="add_log_as_comment", default=False, help="Add commit log message as a comment when uploading the patch."),
+ make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: description from commit message)"),
+ CommandOptions.obsolete_patches,
+ CommandOptions.review,
+ CommandOptions.request_commit,
+ ]
+ Command.__init__(self, "Attach a range of local commits to bugs as patch files", "COMMITISH", options=options, requires_local_commits=True)
+
+ def _comment_text_for_commit(self, options, commit_message, tool, commit_id):
+ comment_text = None
+ if (options.add_log_as_comment):
+ comment_text = commit_message.body(lstrip=True)
+ comment_text += "---\n"
+ comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
+ return comment_text
+
+ def _diff_file_for_commit(self, tool, commit_id):
+ diff = tool.scm().create_patch_from_local_commit(commit_id)
+ return StringIO.StringIO(diff) # add_patch_to_bug expects a file-like object
+
+ def execute(self, options, args, tool):
+ commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
+ if len(commit_ids) > 10: # We could lower this limit, 10 is too many for one bug as-is.
+ error("bugzilla-tool does not support attaching %s at once. Are you sure you passed the right commit range?" % (pluralize("patch", len(commit_ids))))
+
+ have_obsoleted_patches = set()
+ for commit_id in commit_ids:
+ commit_message = tool.scm().commit_message_for_local_commit(commit_id)
+
+ # Prefer --bug-id=, then a bug url in the commit message, then a bug url in the entire commit diff (i.e. ChangeLogs).
+ bug_id = options.bug_id or parse_bug_id(commit_message.message()) or parse_bug_id(tool.scm().create_patch_from_local_commit(commit_id))
+ if not bug_id:
+ log("Skipping %s: No bug id found in commit or specified with --bug-id." % commit_id)
+ continue
+
+ if options.obsolete_patches and bug_id not in have_obsoleted_patches:
+ state = { "bug_id": bug_id }
+ ObsoletePatchesOnBugStep(tool, options).run(state)
+ have_obsoleted_patches.add(bug_id)
+
+ diff_file = self._diff_file_for_commit(tool, commit_id)
+ description = options.description or commit_message.description(lstrip=True, strip_url=True)
+ comment_text = self._comment_text_for_commit(options, commit_message, tool, commit_id)
+ tool.bugs.add_patch_to_bug(bug_id, diff_file, description, comment_text, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
+
+
+# FIXME: Requires unit test. Blocking issue: too complex for now.
+class MarkBugFixed(Command):
+ name = "mark-bug-fixed"
+ show_in_main_help = True
+ def __init__(self):
+ options = [
+ make_option("--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
+ make_option("--comment", action="store", type="string", dest="comment", help="Text to include in bug comment."),
+ make_option("--open", action="store_true", default=False, dest="open_bug", help="Open bug in default web browser (Mac only)."),
+ make_option("--update-only", action="store_true", default=False, dest="update_only", help="Add comment to the bug, but do not close it."),
+ ]
+ Command.__init__(self, "Mark the specified bug as fixed", "[SVN_REVISION]", options=options)
+
+ def _fetch_commit_log(self, tool, svn_revision):
+ if not svn_revision:
+ return tool.scm().last_svn_commit_log()
+ return tool.scm().svn_commit_log(svn_revision)
+
+ def _determine_bug_id_and_svn_revision(self, tool, bug_id, svn_revision):
+ commit_log = self._fetch_commit_log(tool, svn_revision)
+
+ if not bug_id:
+ bug_id = parse_bug_id(commit_log)
+
+ if not svn_revision:
+ match = re.search("^r(?P<svn_revision>\d+) \|", commit_log, re.MULTILINE)
+ if match:
+ svn_revision = match.group('svn_revision')
+
+ if not bug_id or not svn_revision:
+ not_found = []
+ if not bug_id:
+ not_found.append("bug id")
+ if not svn_revision:
+ not_found.append("svn revision")
+ error("Could not find %s on command-line or in %s."
+ % (" or ".join(not_found), "r%s" % svn_revision if svn_revision else "last commit"))
+
+ return (bug_id, svn_revision)
+
+ def _open_bug_in_web_browser(self, tool, bug_id):
+ if sys.platform == "darwin":
+ tool.executive.run_command(["open", tool.bugs.short_bug_url_for_bug_id(bug_id)])
+ return
+ log("WARNING: --open is only supported on Mac OS X.")
+
+ def _prompt_user_for_correctness(self, bug_id, svn_revision):
+ answer = raw_input("Is this correct (y/N)? ")
+ if not re.match("^\s*y(es)?", answer, re.IGNORECASE):
+ exit(1)
+
+ def execute(self, options, args, tool):
+ bug_id = options.bug_id
+
+ svn_revision = args and args[0]
+ if svn_revision:
+ if re.match("^r[0-9]+$", svn_revision, re.IGNORECASE):
+ svn_revision = svn_revision[1:]
+ if not re.match("^[0-9]+$", svn_revision):
+ error("Invalid svn revision: '%s'" % svn_revision)
+
+ needs_prompt = False
+ if not bug_id or not svn_revision:
+ needs_prompt = True
+ (bug_id, svn_revision) = self._determine_bug_id_and_svn_revision(tool, bug_id, svn_revision)
+
+ log("Bug: <%s> %s" % (tool.bugs.short_bug_url_for_bug_id(bug_id), tool.bugs.fetch_bug_dictionary(bug_id)["title"]))
+ log("Revision: %s" % svn_revision)
+
+ if options.open_bug:
+ self._open_bug_in_web_browser(tool, bug_id)
+
+ if needs_prompt:
+ self._prompt_user_for_correctness(bug_id, svn_revision)
+
+ bug_comment = bug_comment_from_svn_revision(svn_revision)
+ if options.comment:
+ bug_comment = "%s\n\n%s" % (options.comment, bug_comment)
+
+ if options.update_only:
+ log("Adding comment to Bug %s." % bug_id)
+ tool.bugs.post_comment_to_bug(bug_id, bug_comment)
+ else:
+ log("Adding comment to Bug %s and marking as Resolved/Fixed." % bug_id)
+ tool.bugs.close_bug_as_fixed(bug_id, bug_comment)
+
+
+# FIXME: Requires unit test. Blocking issue: too complex for now.
+class CreateBug(Command):
+ name = "create-bug"
+ show_in_main_help = True
+ def __init__(self):
+ options = [
+ CommandOptions.cc,
+ CommandOptions.component,
+ make_option("--no-prompt", action="store_false", dest="prompt", default=True, help="Do not prompt for bug title and comment; use commit log instead."),
+ make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."),
+ make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review."),
+ ]
+ Command.__init__(self, "Create a bug from local changes or local commits", "[COMMITISH]", options=options)
+
+ def create_bug_from_commit(self, options, args, tool):
+ commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
+ if len(commit_ids) > 3:
+ error("Are you sure you want to create one bug with %s patches?" % len(commit_ids))
+
+ commit_id = commit_ids[0]
+
+ bug_title = ""
+ comment_text = ""
+ if options.prompt:
+ (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
+ else:
+ commit_message = tool.scm().commit_message_for_local_commit(commit_id)
+ bug_title = commit_message.description(lstrip=True, strip_url=True)
+ comment_text = commit_message.body(lstrip=True)
+ comment_text += "---\n"
+ comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
+
+ diff = tool.scm().create_patch_from_local_commit(commit_id)
+ diff_file = StringIO.StringIO(diff) # create_bug expects a file-like object
+ bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff_file, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
+
+ if bug_id and len(commit_ids) > 1:
+ options.bug_id = bug_id
+ options.obsolete_patches = False
+ # FIXME: We should pass through --no-comment switch as well.
+ PostCommits.execute(self, options, commit_ids[1:], tool)
+
+ def create_bug_from_patch(self, options, args, tool):
+ bug_title = ""
+ comment_text = ""
+ if options.prompt:
+ (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
+ else:
+ commit_message = tool.scm().commit_message_for_this_commit()
+ bug_title = commit_message.description(lstrip=True, strip_url=True)
+ comment_text = commit_message.body(lstrip=True)
+
+ diff = tool.scm().create_patch()
+ diff_file = StringIO.StringIO(diff) # create_bug expects a file-like object
+ bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff_file, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
+
+ def prompt_for_bug_title_and_comment(self):
+ bug_title = raw_input("Bug title: ")
+ print "Bug comment (hit ^D on blank line to end):"
+ lines = sys.stdin.readlines()
+ try:
+ sys.stdin.seek(0, os.SEEK_END)
+ except IOError:
+ # Cygwin raises an Illegal Seek (errno 29) exception when the above
+ # seek() call is made. Ignoring it seems to cause no harm.
+ # FIXME: Figure out a way to get avoid the exception in the first
+ # place.
+ pass
+ comment_text = "".join(lines)
+ return (bug_title, comment_text)
+
+ def execute(self, options, args, tool):
+ if len(args):
+ if (not tool.scm().supports_local_commits()):
+ error("Extra arguments not supported; patch is taken from working directory.")
+ self.create_bug_from_commit(options, args, tool)
+ else:
+ self.create_bug_from_patch(options, args, tool)
diff --git a/WebKitTools/Scripts/webkitpy/commands/upload_unittest.py b/WebKitTools/Scripts/webkitpy/commands/upload_unittest.py
new file mode 100644
index 0000000..b7a1c99
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/commands/upload_unittest.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2009 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.commands.commandtest import CommandsTest
+from webkitpy.commands.upload import *
+from webkitpy.mock_bugzillatool import MockBugzillaTool
+
+class UploadCommandsTest(CommandsTest):
+ def test_assign_to_committer(self):
+ tool = MockBugzillaTool()
+ expected_stderr = "Bug 75 is already assigned to foo at foo.com (None).\nBug 76 has no non-obsolete patches, ignoring.\n"
+ self.assert_execute_outputs(AssignToCommitter(), [], expected_stderr=expected_stderr, tool=tool)
+ tool.bugs.reassign_bug.assert_called_with(42, "eric at webkit.org", "Attachment 128 was posted by a committer and has review+, assigning to Eric Seidel for commit.")
+
+ def test_obsolete_attachments(self):
+ expected_stderr = "Obsoleting 2 old patches on bug 42\n"
+ self.assert_execute_outputs(ObsoleteAttachments(), [42], expected_stderr=expected_stderr)
+
+ def test_post_diff(self):
+ expected_stderr = "Obsoleting 2 old patches on bug 42\n"
+ self.assert_execute_outputs(PostDiff(), [42], expected_stderr=expected_stderr)
+
+ def test_prepare_diff_with_arg(self):
+ self.assert_execute_outputs(PrepareDiff(), [42])
+
+ def test_prepare_diff(self):
+ self.assert_execute_outputs(PrepareDiff(), [])
+
+ def test_create_review(self):
+ expected_stderr = "Obsoleting 2 old patches on bug 42\n"
+ self.assert_execute_outputs(CreateReview(), [42], expected_stderr=expected_stderr)
+
+ def test_edit_changelog(self):
+ self.assert_execute_outputs(EditChangeLog(), [])
diff --git a/WebKitTools/Scripts/webkitpy/comments.py b/WebKitTools/Scripts/webkitpy/comments.py
new file mode 100755
index 0000000..657a373
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/comments.py
@@ -0,0 +1,39 @@
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple 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 tool for automating dealing with bugzilla, posting patches, committing patches, etc.
+
+from webkitpy.changelogs import view_source_url
+
+def bug_comment_from_svn_revision(svn_revision):
+ return "Committed r%s: <%s>" % (svn_revision, view_source_url(svn_revision))
+
+def bug_comment_from_commit_text(scm, commit_text):
+ svn_revision = scm.svn_revision_from_commit_text(commit_text)
+ return bug_comment_from_svn_revision(svn_revision)
diff --git a/WebKitTools/Scripts/modules/committers.py b/WebKitTools/Scripts/webkitpy/committers.py
similarity index 100%
rename from WebKitTools/Scripts/modules/committers.py
rename to WebKitTools/Scripts/webkitpy/committers.py
diff --git a/WebKitTools/Scripts/modules/committers_unittest.py b/WebKitTools/Scripts/webkitpy/committers_unittest.py
similarity index 100%
rename from WebKitTools/Scripts/modules/committers_unittest.py
rename to WebKitTools/Scripts/webkitpy/committers_unittest.py
diff --git a/WebKitTools/Scripts/modules/cpp_style.py b/WebKitTools/Scripts/webkitpy/cpp_style.py
similarity index 100%
rename from WebKitTools/Scripts/modules/cpp_style.py
rename to WebKitTools/Scripts/webkitpy/cpp_style.py
diff --git a/WebKitTools/Scripts/modules/cpp_style_unittest.py b/WebKitTools/Scripts/webkitpy/cpp_style_unittest.py
similarity index 100%
rename from WebKitTools/Scripts/modules/cpp_style_unittest.py
rename to WebKitTools/Scripts/webkitpy/cpp_style_unittest.py
diff --git a/WebKitTools/Scripts/webkitpy/credentials.py b/WebKitTools/Scripts/webkitpy/credentials.py
new file mode 100644
index 0000000..0a38907
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/credentials.py
@@ -0,0 +1,102 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple 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.
+#
+# Python module for reading stored web credentials from the OS.
+
+import getpass
+import os
+import platform
+import re
+
+from webkitpy.executive import Executive
+from webkitpy.webkit_logging import log
+from webkitpy.scm import Git
+
+class Credentials(object):
+ keychain_entry_not_found = "security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain."
+
+ def __init__(self, host, git_prefix=None, executive=None, cwd=os.getcwd()):
+ self.host = host
+ self.git_prefix = git_prefix
+ self.executive = executive or Executive()
+ self.cwd = cwd
+
+ def _credentials_from_git(self):
+ return [self._read_git_config("username"), self._read_git_config("password")]
+
+ def _read_git_config(self, key):
+ config_key = "%s.%s" % (self.git_prefix, key) if self.git_prefix else key
+ return self.executive.run_command(["git", "config", "--get", config_key], error_handler=Executive.ignore_error).rstrip('\n')
+
+ def _keychain_value_with_label(self, label, source_text):
+ match = re.search("%s\"(?P<value>.+)\"" % label, source_text, re.MULTILINE)
+ if match:
+ return match.group('value')
+
+ def _is_mac_os_x(self):
+ return platform.mac_ver()[0]
+
+ def _parse_security_tool_output(self, security_output):
+ if security_output == self.keychain_entry_not_found:
+ return [None, None]
+ username = self._keychain_value_with_label("^\s*\"acct\"<blob>=", security_output)
+ password = self._keychain_value_with_label("^password: ", security_output)
+ return [username, password]
+
+ def _run_security_tool(self, username=None):
+ security_command = ["/usr/bin/security", "find-internet-password", "-g", "-s", self.host]
+ if username:
+ security_command += ["-a", username]
+
+ log("Reading Keychain for %s account and password. Click \"Allow\" to continue..." % self.host)
+ return self.executive.run_command(security_command)
+
+ def _credentials_from_keychain(self, username=None):
+ if not self._is_mac_os_x():
+ return [username, None]
+
+ security_output = self._run_security_tool(username)
+ return self._parse_security_tool_output(security_output)
+
+ def read_credentials(self):
+ username = None
+ password = None
+
+ if Git.in_working_directory(self.cwd):
+ (username, password) = self._credentials_from_git()
+
+ if not username or not password:
+ (username, password) = self._credentials_from_keychain(username)
+
+ if not username:
+ username = raw_input("%s login: " % self.host)
+ if not password:
+ password = getpass.getpass("%s password for %s: " % (self.host, username))
+
+ return [username, password]
diff --git a/WebKitTools/Scripts/webkitpy/credentials_unittest.py b/WebKitTools/Scripts/webkitpy/credentials_unittest.py
new file mode 100644
index 0000000..ed36c26
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/credentials_unittest.py
@@ -0,0 +1,119 @@
+# Copyright (C) 2009 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 os
+import tempfile
+import unittest
+from webkitpy.credentials import Credentials
+from webkitpy.executive import Executive
+from webkitpy.outputcapture import OutputCapture
+from webkitpy.mock import Mock
+
+class CredentialsTest(unittest.TestCase):
+ example_security_output = """keychain: "/Users/test/Library/Keychains/login.keychain"
+class: "inet"
+attributes:
+ 0x00000007 <blob>="bugs.webkit.org (test at webkit.org)"
+ 0x00000008 <blob>=<NULL>
+ "acct"<blob>="test at webkit.org"
+ "atyp"<blob>="form"
+ "cdat"<timedate>=0x32303039303832353233353231365A00 "20090825235216Z\000"
+ "crtr"<uint32>=<NULL>
+ "cusi"<sint32>=<NULL>
+ "desc"<blob>="Web form password"
+ "icmt"<blob>="default"
+ "invi"<sint32>=<NULL>
+ "mdat"<timedate>=0x32303039303930393137323635315A00 "20090909172651Z\000"
+ "nega"<sint32>=<NULL>
+ "path"<blob>=<NULL>
+ "port"<uint32>=0x00000000
+ "prot"<blob>=<NULL>
+ "ptcl"<uint32>="htps"
+ "scrp"<sint32>=<NULL>
+ "sdmn"<blob>=<NULL>
+ "srvr"<blob>="bugs.webkit.org"
+ "type"<uint32>=<NULL>
+password: "SECRETSAUCE"
+"""
+
+ def test_keychain_lookup_on_non_mac(self):
+ class FakeCredentials(Credentials):
+ def _is_mac_os_x(self):
+ return False
+ credentials = FakeCredentials("bugs.webkit.org")
+ self.assertEqual(credentials._is_mac_os_x(), False)
+ self.assertEqual(credentials._credentials_from_keychain("foo"), ["foo", None])
+
+ def test_security_output_parse(self):
+ credentials = Credentials("bugs.webkit.org")
+ self.assertEqual(credentials._parse_security_tool_output(self.example_security_output), ["test at webkit.org", "SECRETSAUCE"])
+
+ def test_security_output_parse_entry_not_found(self):
+ credentials = Credentials("foo.example.com")
+ self.assertEqual(credentials._parse_security_tool_output(Credentials.keychain_entry_not_found), [None, None])
+
+ def _assert_security_call(self, username=None):
+ executive_mock = Mock()
+ credentials = Credentials("example.com", executive=executive_mock)
+
+ expected_stderr = "Reading Keychain for example.com account and password. Click \"Allow\" to continue...\n"
+ OutputCapture().assert_outputs(self, credentials._run_security_tool, [username], expected_stderr=expected_stderr)
+
+ security_args = ["/usr/bin/security", "find-internet-password", "-g", "-s", "example.com"]
+ if username:
+ security_args += ["-a", username]
+ executive_mock.run_command.assert_called_with(security_args)
+
+ def test_security_calls(self):
+ self._assert_security_call()
+ self._assert_security_call(username="foo")
+
+ def test_git_config_calls(self):
+ executive_mock = Mock()
+ credentials = Credentials("example.com", executive=executive_mock)
+ credentials._read_git_config("foo")
+ executive_mock.run_command.assert_called_with(["git", "config", "--get", "foo"], error_handler=Executive.ignore_error)
+
+ credentials = Credentials("example.com", git_prefix="test_prefix", executive=executive_mock)
+ credentials._read_git_config("foo")
+ executive_mock.run_command.assert_called_with(["git", "config", "--get", "test_prefix.foo"], error_handler=Executive.ignore_error)
+
+ def test_read_credentials_without_git_repo(self):
+ class FakeCredentials(Credentials):
+ def _is_mac_os_x(self):
+ return True
+ def _credentials_from_keychain(self, username):
+ return ["test at webkit.org", "SECRETSAUCE"]
+
+ temp_dir_path = tempfile.mkdtemp(suffix="not_a_git_repo")
+ credentials = FakeCredentials("bugs.webkit.org", cwd=temp_dir_path)
+ self.assertEqual(credentials.read_credentials(), ["test at webkit.org", "SECRETSAUCE"])
+ os.rmdir(temp_dir_path)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/WebKitTools/Scripts/modules/diff_parser.py b/WebKitTools/Scripts/webkitpy/diff_parser.py
similarity index 100%
rename from WebKitTools/Scripts/modules/diff_parser.py
rename to WebKitTools/Scripts/webkitpy/diff_parser.py
diff --git a/WebKitTools/Scripts/modules/diff_parser_unittest.py b/WebKitTools/Scripts/webkitpy/diff_parser_unittest.py
similarity index 100%
rename from WebKitTools/Scripts/modules/diff_parser_unittest.py
rename to WebKitTools/Scripts/webkitpy/diff_parser_unittest.py
diff --git a/WebKitTools/Scripts/webkitpy/executive.py b/WebKitTools/Scripts/webkitpy/executive.py
new file mode 100644
index 0000000..dd21489
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/executive.py
@@ -0,0 +1,136 @@
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple 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 os
+import StringIO
+import subprocess
+import sys
+
+from webkitpy.webkit_logging import tee
+
+
+class ScriptError(Exception):
+ def __init__(self, message=None, script_args=None, exit_code=None, output=None, cwd=None):
+ if not message:
+ message = 'Failed to run "%s"' % script_args
+ if exit_code:
+ message += " exit_code: %d" % exit_code
+ if cwd:
+ message += " cwd: %s" % cwd
+
+ Exception.__init__(self, message)
+ self.script_args = script_args # 'args' is already used by Exception
+ self.exit_code = exit_code
+ self.output = output
+ self.cwd = cwd
+
+ def message_with_output(self, output_limit=500):
+ if self.output:
+ if output_limit and len(self.output) > output_limit:
+ return "%s\nLast %s characters of output:\n%s" % (self, output_limit, self.output[-output_limit:])
+ return "%s\n%s" % (self, self.output)
+ return str(self)
+
+ def command_name(self):
+ command_path = self.script_args
+ if type(command_path) is list:
+ command_path = command_path[0]
+ return os.path.basename(command_path)
+
+
+# FIXME: This should not be a global static.
+# New code should use Executive.run_command directly instead
+def run_command(*args, **kwargs):
+ return Executive().run_command(*args, **kwargs)
+
+
+class Executive(object):
+ def _run_command_with_teed_output(self, args, teed_output):
+ child_process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+
+ # Use our own custom wait loop because Popen ignores a tee'd stderr/stdout.
+ # FIXME: This could be improved not to flatten output to stdout.
+ while True:
+ output_line = child_process.stdout.readline()
+ if output_line == "" and child_process.poll() != None:
+ return child_process.poll()
+ teed_output.write(output_line)
+
+ def run_and_throw_if_fail(self, args, quiet=False):
+ # Cache the child's output locally so it can be used for error reports.
+ child_out_file = StringIO.StringIO()
+ if quiet:
+ dev_null = open(os.devnull, "w")
+ child_stdout = tee(child_out_file, dev_null if quiet else sys.stdout)
+ exit_code = self._run_command_with_teed_output(args, child_stdout)
+ if quiet:
+ dev_null.close()
+
+ child_output = child_out_file.getvalue()
+ child_out_file.close()
+
+ if exit_code:
+ raise ScriptError(script_args=args, exit_code=exit_code, output=child_output)
+
+ # Error handlers do not need to be static methods once all callers are updated to use an Executive object.
+ @staticmethod
+ def default_error_handler(error):
+ raise error
+
+ @staticmethod
+ def ignore_error(error):
+ pass
+
+ # FIXME: This should be merged with run_and_throw_if_fail
+ def run_command(self, args, cwd=None, input=None, error_handler=None, return_exit_code=False, return_stderr=True):
+ if hasattr(input, 'read'): # Check if the input is a file.
+ stdin = input
+ string_to_communicate = None
+ else:
+ stdin = subprocess.PIPE if input else None
+ string_to_communicate = input
+ if return_stderr:
+ stderr = subprocess.STDOUT
+ else:
+ stderr = None
+ try:
+ process = subprocess.Popen(args, stdin=stdin, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
+ output = process.communicate(string_to_communicate)[0]
+ exit_code = process.wait()
+ except OSError, e:
+ # Catch OSError exceptions. For example, "no such file or directory" (i.e. OSError errno 2),
+ # when the command cannot be found.
+ output = e.strerror
+ exit_code = e.errno
+ if exit_code:
+ script_error = ScriptError(script_args=args, exit_code=exit_code, output=output, cwd=cwd)
+ (error_handler or self.default_error_handler)(script_error)
+ if return_exit_code:
+ return exit_code
+ return output
diff --git a/WebKitTools/Scripts/webkitpy/executive_unittest.py b/WebKitTools/Scripts/webkitpy/executive_unittest.py
new file mode 100644
index 0000000..6ab0d10
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/executive_unittest.py
@@ -0,0 +1,48 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2009 Daniel Bates (dbates at intudata.com). 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.executive import Executive, ScriptError, run_command
+
+class ExecutiveTest(unittest.TestCase):
+
+ def test_run_command_with_bad_command_check_return_code(self):
+ self.assertEqual(run_command(["foo_bar_command_blah"], error_handler=Executive.ignore_error, return_exit_code=True), 2)
+
+ def test_run_command_with_bad_command_check_calls_error_handler(self):
+ self.didHandleErrorGetCalled = False
+ def handleError(scriptError):
+ self.didHandleErrorGetCalled = True
+ self.assertEqual(scriptError.exit_code, 2)
+
+ run_command(["foo_bar_command_blah"], error_handler=handleError)
+ self.assertTrue(self.didHandleErrorGetCalled)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/WebKitTools/Scripts/modules/grammar.py b/WebKitTools/Scripts/webkitpy/grammar.py
similarity index 100%
rename from WebKitTools/Scripts/modules/grammar.py
rename to WebKitTools/Scripts/webkitpy/grammar.py
diff --git a/WebKitTools/Scripts/modules/mock.py b/WebKitTools/Scripts/webkitpy/mock.py
similarity index 100%
rename from WebKitTools/Scripts/modules/mock.py
rename to WebKitTools/Scripts/webkitpy/mock.py
diff --git a/WebKitTools/Scripts/webkitpy/mock_bugzillatool.py b/WebKitTools/Scripts/webkitpy/mock_bugzillatool.py
new file mode 100644
index 0000000..d570d40
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/mock_bugzillatool.py
@@ -0,0 +1,214 @@
+# Copyright (C) 2009 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 os
+
+from webkitpy.mock import Mock
+from webkitpy.scm import CommitMessage
+from webkitpy.bugzilla import Bug
+
+def _id_to_object_dictionary(*objects):
+ dictionary = {}
+ for thing in objects:
+ dictionary[thing["id"]] = thing
+ return dictionary
+
+# FIXME: The ids should be 1, 2, 3 instead of crazy numbers.
+_patch1 = {
+ "id" : 197,
+ "bug_id" : 42,
+ "url" : "http://example.com/197",
+ "is_obsolete" : False,
+ "is_patch" : True,
+ "reviewer" : "Reviewer1",
+ "attacher_email" : "Contributer1",
+}
+_patch2 = {
+ "id" : 128,
+ "bug_id" : 42,
+ "url" : "http://example.com/128",
+ "is_obsolete" : False,
+ "is_patch" : True,
+ "reviewer" : "Reviewer2",
+ "attacher_email" : "eric at webkit.org",
+}
+
+# This must be defined before we define the bugs, thus we don't use MockBugzilla.unassigned_email directly.
+_unassigned_email = "unassigned at example.com"
+
+# FIXME: The ids should be 1, 2, 3 instead of crazy numbers.
+_bug1 = {
+ "id" : 42,
+ "assigned_to_email" : _unassigned_email,
+ "attachments" : [_patch1, _patch2],
+}
+_bug2 = {
+ "id" : 75,
+ "assigned_to_email" : "foo at foo.com",
+ "attachments" : [],
+}
+_bug3 = {
+ "id" : 76,
+ "assigned_to_email" : _unassigned_email,
+ "attachments" : [],
+}
+
+class MockBugzillaQueries(Mock):
+ def fetch_bug_ids_from_commit_queue(self):
+ return [42, 75]
+
+ def fetch_attachment_ids_from_review_queue(self):
+ return [197, 128]
+
+ def fetch_patches_from_commit_queue(self, reject_invalid_patches=False):
+ return [_patch1, _patch2]
+
+ def fetch_bug_ids_from_pending_commit_list(self):
+ return [42, 75, 76]
+
+ def fetch_patches_from_pending_commit_list(self):
+ return [_patch1, _patch2]
+
+
+class MockBugzilla(Mock):
+ bug_server_url = "http://example.com"
+ unassigned_email = _unassigned_email
+ bug_cache = _id_to_object_dictionary(_bug1, _bug2, _bug3)
+ attachment_cache = _id_to_object_dictionary(_patch1, _patch2)
+ queries = MockBugzillaQueries()
+
+ def fetch_bug(self, bug_id):
+ return Bug(self.bug_cache.get(bug_id))
+
+ def fetch_reviewed_patches_from_bug(self, bug_id):
+ return self.fetch_patches_from_bug(bug_id) # Return them all for now.
+
+ def fetch_attachment(self, attachment_id):
+ return self.attachment_cache[attachment_id] # This could be changed to .get() if we wish to allow failed lookups.
+
+ # NOTE: Functions below this are direct copies from bugzilla.py
+ def fetch_patches_from_bug(self, bug_id):
+ return self.fetch_bug(bug_id).patches()
+
+ def bug_url_for_bug_id(self, bug_id):
+ return "%s/%s" % (self.bug_server_url, bug_id)
+
+ def attachment_url_for_id(self, attachment_id, action):
+ action_param = ""
+ if action and action != "view":
+ action_param = "&action=%s" % action
+ return "%s/%s%s" % (self.bug_server_url, attachment_id, action_param)
+
+
+class MockBuildBot(Mock):
+ def builder_statuses(self):
+ return [{
+ "name": "Builder1",
+ "is_green": True
+ }, {
+ "name": "Builder2",
+ "is_green": True
+ }]
+
+ def red_core_builders_names(self):
+ return []
+
+
+class MockSCM(Mock):
+ def __init__(self):
+ Mock.__init__(self)
+ self.checkout_root = os.getcwd()
+
+ def create_patch(self):
+ return "Patch1"
+
+ def commit_ids_from_commitish_arguments(self, args):
+ return ["Commitish1", "Commitish2"]
+
+ def commit_message_for_local_commit(self, commit_id):
+ if commit_id == "Commitish1":
+ return CommitMessage("CommitMessage1\nhttps://bugs.example.org/show_bug.cgi?id=42\n")
+ if commit_id == "Commitish2":
+ return CommitMessage("CommitMessage2\nhttps://bugs.example.org/show_bug.cgi?id=75\n")
+ raise Exception("Bogus commit_id in commit_message_for_local_commit.")
+
+ def create_patch_from_local_commit(self, commit_id):
+ if commit_id == "Commitish1":
+ return "Patch1"
+ if commit_id == "Commitish2":
+ return "Patch2"
+ raise Exception("Bogus commit_id in commit_message_for_local_commit.")
+
+ def diff_for_revision(self, revision):
+ return "DiffForRevision%s\nhttp://bugs.webkit.org/show_bug.cgi?id=12345" % revision
+
+ def modified_changelogs(self):
+ # Ideally we'd return something more interesting here.
+ # The problem is that LandDiff will try to actually read the path from disk!
+ return []
+
+
+class MockUser(object):
+ def prompt(self, message):
+ return "Mock user response"
+
+ def edit(self, files):
+ pass
+
+ def page(self, message):
+ pass
+
+ def confirm(self):
+ return True
+
+
+class MockStatusBot(object):
+ def __init__(self):
+ self.statusbot_host = "example.com"
+
+ def patch_status(self, queue_name, patch_id):
+ return None
+
+ def update_status(self, queue_name, status, patch=None, results_file=None):
+ return 187
+
+
+class MockBugzillaTool():
+ def __init__(self):
+ self.bugs = MockBugzilla()
+ self.buildbot = MockBuildBot()
+ self.executive = Mock()
+ self.user = MockUser()
+ self._scm = MockSCM()
+ self.status_bot = MockStatusBot()
+
+ def scm(self):
+ return self._scm
+
+ def path(self):
+ return "echo"
diff --git a/WebKitTools/Scripts/webkitpy/multicommandtool.py b/WebKitTools/Scripts/webkitpy/multicommandtool.py
new file mode 100644
index 0000000..5f89852
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/multicommandtool.py
@@ -0,0 +1,293 @@
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple 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.
+#
+# MultiCommandTool provides a framework for writing svn-like/git-like tools
+# which are called with the following format:
+# tool-name [global options] command-name [command options]
+
+import sys
+
+from optparse import OptionParser, IndentedHelpFormatter, SUPPRESS_USAGE, make_option
+
+from webkitpy.grammar import pluralize
+from webkitpy.webkit_logging import log
+
+
+class Command(object):
+ name = None
+ show_in_main_help = False
+ def __init__(self, help_text, argument_names=None, options=None, requires_local_commits=False):
+ self.help_text = help_text
+ self.argument_names = argument_names
+ self.required_arguments = self._parse_required_arguments(argument_names)
+ self.options = options
+ self.requires_local_commits = requires_local_commits
+ self.tool = None
+ # option_parser can be overriden by the tool using set_option_parser
+ # This default parser will be used for standalone_help printing.
+ self.option_parser = HelpPrintingOptionParser(usage=SUPPRESS_USAGE, add_help_option=False, option_list=self.options)
+
+ # This design is slightly awkward, but we need the
+ # the tool to be able to create and modify the option_parser
+ # before it knows what Command to run.
+ def set_option_parser(self, option_parser):
+ self.option_parser = option_parser
+ self._add_options_to_parser()
+
+ def _add_options_to_parser(self):
+ options = self.options or []
+ for option in options:
+ self.option_parser.add_option(option)
+
+ # The tool calls bind_to_tool on each Command after adding it to its list.
+ def bind_to_tool(self, tool):
+ # Command instances can only be bound to one tool at a time.
+ if self.tool and tool != self.tool:
+ raise Exception("Command already bound to tool!")
+ self.tool = tool
+
+ @staticmethod
+ def _parse_required_arguments(argument_names):
+ required_args = []
+ if not argument_names:
+ return required_args
+ split_args = argument_names.split(" ")
+ for argument in split_args:
+ if argument[0] == '[':
+ # For now our parser is rather dumb. Do some minimal validation that
+ # we haven't confused it.
+ if argument[-1] != ']':
+ raise Exception("Failure to parse argument string %s. Argument %s is missing ending ]" % (argument_names, argument))
+ else:
+ required_args.append(argument)
+ return required_args
+
+ def name_with_arguments(self):
+ usage_string = self.name
+ if self.options:
+ usage_string += " [options]"
+ if self.argument_names:
+ usage_string += " " + self.argument_names
+ return usage_string
+
+ def parse_args(self, args):
+ return self.option_parser.parse_args(args)
+
+ def check_arguments_and_execute(self, options, args, tool=None):
+ if len(args) < len(self.required_arguments):
+ log("%s required, %s provided. Provided: %s Required: %s\nSee '%s help %s' for usage." % (
+ pluralize("argument", len(self.required_arguments)),
+ pluralize("argument", len(args)),
+ "'%s'" % " ".join(args),
+ " ".join(self.required_arguments),
+ tool.name(),
+ self.name))
+ return 1
+ return self.execute(options, args, tool) or 0
+
+ def standalone_help(self):
+ help_text = self.name_with_arguments().ljust(len(self.name_with_arguments()) + 3) + self.help_text + "\n"
+ help_text += self.option_parser.format_option_help(IndentedHelpFormatter())
+ return help_text
+
+ def execute(self, options, args, tool):
+ raise NotImplementedError, "subclasses must implement"
+
+ # main() exists so that Commands can be turned into stand-alone scripts.
+ # Other parts of the code will likely require modification to work stand-alone.
+ def main(self, args=sys.argv):
+ (options, args) = self.parse_args(args)
+ # Some commands might require a dummy tool
+ return self.check_arguments_and_execute(options, args)
+
+
+# FIXME: This should just be rolled into Command. help_text and argument_names do not need to be instance variables.
+class AbstractDeclarativeCommmand(Command):
+ help_text = None
+ argument_names = None
+ def __init__(self, options=None):
+ Command.__init__(self, self.help_text, self.argument_names, options)
+
+
+class HelpPrintingOptionParser(OptionParser):
+ def __init__(self, epilog_method=None, *args, **kwargs):
+ self.epilog_method = epilog_method
+ OptionParser.__init__(self, *args, **kwargs)
+
+ def error(self, msg):
+ self.print_usage(sys.stderr)
+ error_message = "%s: error: %s\n" % (self.get_prog_name(), msg)
+ # This method is overriden to add this one line to the output:
+ error_message += "\nType \"%s --help\" to see usage.\n" % self.get_prog_name()
+ self.exit(1, error_message)
+
+ # We override format_epilog to avoid the default formatting which would paragraph-wrap the epilog
+ # and also to allow us to compute the epilog lazily instead of in the constructor (allowing it to be context sensitive).
+ def format_epilog(self, epilog):
+ if self.epilog_method:
+ return "\n%s\n" % self.epilog_method()
+ return ""
+
+
+class HelpCommand(Command):
+ name = "help"
+
+ def __init__(self):
+ options = [
+ make_option("-a", "--all-commands", action="store_true", dest="show_all_commands", help="Print all available commands"),
+ ]
+ Command.__init__(self, "Display information about this program or its subcommands", "[COMMAND]", options=options)
+ self.show_all_commands = False # A hack used to pass --all-commands to _help_epilog even though it's called by the OptionParser.
+
+ def _help_epilog(self):
+ # Only show commands which are relevant to this checkout's SCM system. Might this be confusing to some users?
+ if self.show_all_commands:
+ epilog = "All %prog commands:\n"
+ relevant_commands = self.tool.commands[:]
+ else:
+ epilog = "Common %prog commands:\n"
+ relevant_commands = filter(self.tool.should_show_in_main_help, self.tool.commands)
+ longest_name_length = max(map(lambda command: len(command.name), relevant_commands))
+ relevant_commands.sort(lambda a, b: cmp(a.name, b.name))
+ command_help_texts = map(lambda command: " %s %s\n" % (command.name.ljust(longest_name_length), command.help_text), relevant_commands)
+ epilog += "%s\n" % "".join(command_help_texts)
+ epilog += "See '%prog help --all-commands' to list all commands.\n"
+ epilog += "See '%prog help COMMAND' for more information on a specific command.\n"
+ return epilog.replace("%prog", self.tool.name()) # Use of %prog here mimics OptionParser.expand_prog_name().
+
+ # FIXME: This is a hack so that we don't show --all-commands as a global option:
+ def _remove_help_options(self):
+ for option in self.options:
+ self.option_parser.remove_option(option.get_opt_string())
+
+ def execute(self, options, args, tool):
+ if args:
+ command = self.tool.command_by_name(args[0])
+ if command:
+ print command.standalone_help()
+ return 0
+
+ self.show_all_commands = options.show_all_commands
+ self._remove_help_options()
+ self.option_parser.print_help()
+ return 0
+
+
+class MultiCommandTool(object):
+ global_options = None
+
+ def __init__(self, name=None, commands=None):
+ self._name = name or OptionParser(prog=name).get_prog_name() # OptionParser has nice logic for fetching the name.
+ # Allow the unit tests to disable command auto-discovery.
+ self.commands = commands or [cls() for cls in self._find_all_commands() if cls.name]
+ self.help_command = self.command_by_name(HelpCommand.name)
+ # Require a help command, even if the manual test list doesn't include one.
+ if not self.help_command:
+ self.help_command = HelpCommand()
+ self.commands.append(self.help_command)
+ for command in self.commands:
+ command.bind_to_tool(self)
+
+ @classmethod
+ def _add_all_subclasses(cls, class_to_crawl, seen_classes):
+ for subclass in class_to_crawl.__subclasses__():
+ if subclass not in seen_classes:
+ seen_classes.add(subclass)
+ cls._add_all_subclasses(subclass, seen_classes)
+
+ @classmethod
+ def _find_all_commands(cls):
+ commands = set()
+ cls._add_all_subclasses(Command, commands)
+ return sorted(commands)
+
+ def name(self):
+ return self._name
+
+ def _create_option_parser(self):
+ usage = "Usage: %prog [options] COMMAND [ARGS]"
+ return HelpPrintingOptionParser(epilog_method=self.help_command._help_epilog, prog=self.name(), usage=usage)
+
+ @staticmethod
+ def _split_command_name_from_args(args):
+ # Assume the first argument which doesn't start with "-" is the command name.
+ command_index = 0
+ for arg in args:
+ if arg[0] != "-":
+ break
+ command_index += 1
+ else:
+ return (None, args[:])
+
+ command = args[command_index]
+ return (command, args[:command_index] + args[command_index + 1:])
+
+ def command_by_name(self, command_name):
+ for command in self.commands:
+ if command_name == command.name:
+ return command
+ return None
+
+ def path(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def should_show_in_main_help(self, command):
+ return command.show_in_main_help
+
+ def should_execute_command(self, command):
+ return True
+
+ def _add_global_options(self, option_parser):
+ global_options = self.global_options or []
+ for option in global_options:
+ option_parser.add_option(option)
+
+ def handle_global_options(self, options):
+ pass
+
+ def main(self, argv=sys.argv):
+ (command_name, args) = self._split_command_name_from_args(argv[1:])
+
+ option_parser = self._create_option_parser()
+ self._add_global_options(option_parser)
+
+ command = self.command_by_name(command_name) or self.help_command
+ if not command:
+ option_parser.error("%s is not a recognized command" % command_name)
+
+ command.set_option_parser(option_parser)
+ (options, args) = command.parse_args(args)
+ self.handle_global_options(options)
+
+ (should_execute, failure_reason) = self.should_execute_command(command)
+ if not should_execute:
+ log(failure_reason)
+ return 0 # FIXME: Should this really be 0?
+
+ return command.check_arguments_and_execute(options, args, self)
diff --git a/WebKitTools/Scripts/webkitpy/multicommandtool_unittest.py b/WebKitTools/Scripts/webkitpy/multicommandtool_unittest.py
new file mode 100644
index 0000000..ea99507
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/multicommandtool_unittest.py
@@ -0,0 +1,153 @@
+# Copyright (c) 2009, 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 sys
+import unittest
+from multicommandtool import MultiCommandTool, Command
+from webkitpy.outputcapture import OutputCapture
+
+from optparse import make_option
+
+class TrivialCommand(Command):
+ name = "trivial"
+ show_in_main_help = True
+ def __init__(self, **kwargs):
+ Command.__init__(self, "help text", **kwargs)
+
+ def execute(self, options, args, tool):
+ pass
+
+class UncommonCommand(TrivialCommand):
+ name = "uncommon"
+ show_in_main_help = False
+
+class CommandTest(unittest.TestCase):
+ def test_name_with_arguments(self):
+ command_with_args = TrivialCommand(argument_names="ARG1 ARG2")
+ self.assertEqual(command_with_args.name_with_arguments(), "trivial ARG1 ARG2")
+
+ command_with_args = TrivialCommand(options=[make_option("--my_option")])
+ self.assertEqual(command_with_args.name_with_arguments(), "trivial [options]")
+
+ def test_parse_required_arguments(self):
+ self.assertEqual(Command._parse_required_arguments("ARG1 ARG2"), ["ARG1", "ARG2"])
+ self.assertEqual(Command._parse_required_arguments("[ARG1] [ARG2]"), [])
+ self.assertEqual(Command._parse_required_arguments("[ARG1] ARG2"), ["ARG2"])
+ # Note: We might make our arg parsing smarter in the future and allow this type of arguments string.
+ self.assertRaises(Exception, Command._parse_required_arguments, "[ARG1 ARG2]")
+
+ def test_required_arguments(self):
+ two_required_arguments = TrivialCommand(argument_names="ARG1 ARG2 [ARG3]")
+ expected_missing_args_error = "2 arguments required, 1 argument provided. Provided: 'foo' Required: ARG1 ARG2\nSee 'trivial-tool help trivial' for usage.\n"
+ exit_code = OutputCapture().assert_outputs(self, two_required_arguments.check_arguments_and_execute, [None, ["foo"], TrivialTool()], expected_stderr=expected_missing_args_error)
+ self.assertEqual(exit_code, 1)
+
+
+class TrivialTool(MultiCommandTool):
+ def __init__(self, commands=None):
+ MultiCommandTool.__init__(self, name="trivial-tool", commands=commands)
+
+ def path():
+ return __file__
+
+ def should_execute_command(self, command):
+ return (True, None)
+
+
+class MultiCommandToolTest(unittest.TestCase):
+ def _assert_split(self, args, expected_split):
+ self.assertEqual(MultiCommandTool._split_command_name_from_args(args), expected_split)
+
+ def test_split_args(self):
+ # MultiCommandToolTest._split_command_name_from_args returns: (command, args)
+ full_args = ["--global-option", "command", "--option", "arg"]
+ full_args_expected = ("command", ["--global-option", "--option", "arg"])
+ self._assert_split(full_args, full_args_expected)
+
+ full_args = []
+ full_args_expected = (None, [])
+ self._assert_split(full_args, full_args_expected)
+
+ full_args = ["command", "arg"]
+ full_args_expected = ("command", ["arg"])
+ self._assert_split(full_args, full_args_expected)
+
+ def test_command_by_name(self):
+ # This also tests Command auto-discovery.
+ tool = TrivialTool()
+ self.assertEqual(tool.command_by_name("trivial").name, "trivial")
+ self.assertEqual(tool.command_by_name("bar"), None)
+
+ def _assert_tool_main_outputs(self, tool, main_args, expected_stdout, expected_stderr = "", expected_exit_code=0):
+ exit_code = OutputCapture().assert_outputs(self, tool.main, [main_args], expected_stdout=expected_stdout, expected_stderr=expected_stderr)
+ self.assertEqual(exit_code, expected_exit_code)
+
+ def test_global_help(self):
+ tool = TrivialTool(commands=[TrivialCommand(), UncommonCommand()])
+ expected_common_commands_help = """Usage: trivial-tool [options] COMMAND [ARGS]
+
+Options:
+ -h, --help show this help message and exit
+
+Common trivial-tool commands:
+ trivial help text
+
+See 'trivial-tool help --all-commands' to list all commands.
+See 'trivial-tool help COMMAND' for more information on a specific command.
+
+"""
+ self._assert_tool_main_outputs(tool, ["tool"], expected_common_commands_help)
+ self._assert_tool_main_outputs(tool, ["tool", "help"], expected_common_commands_help)
+ expected_all_commands_help = """Usage: trivial-tool [options] COMMAND [ARGS]
+
+Options:
+ -h, --help show this help message and exit
+
+All trivial-tool commands:
+ help Display information about this program or its subcommands
+ trivial help text
+ uncommon help text
+
+See 'trivial-tool help --all-commands' to list all commands.
+See 'trivial-tool help COMMAND' for more information on a specific command.
+
+"""
+ self._assert_tool_main_outputs(tool, ["tool", "help", "--all-commands"], expected_all_commands_help)
+ # Test that arguments can be passed before commands as well
+ self._assert_tool_main_outputs(tool, ["tool", "--all-commands", "help"], expected_all_commands_help)
+
+
+ def test_command_help(self):
+ command_with_options = TrivialCommand(options=[make_option("--my_option")])
+ tool = TrivialTool(commands=[command_with_options])
+ expected_subcommand_help = "trivial [options] help text\nOptions:\n --my_option=MY_OPTION\n\n"
+ self._assert_tool_main_outputs(tool, ["tool", "help", "trivial"], expected_subcommand_help)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/WebKitTools/Scripts/modules/outputcapture.py b/WebKitTools/Scripts/webkitpy/outputcapture.py
similarity index 100%
rename from WebKitTools/Scripts/modules/outputcapture.py
rename to WebKitTools/Scripts/webkitpy/outputcapture.py
diff --git a/WebKitTools/Scripts/modules/patchcollection.py b/WebKitTools/Scripts/webkitpy/patchcollection.py
similarity index 100%
rename from WebKitTools/Scripts/modules/patchcollection.py
rename to WebKitTools/Scripts/webkitpy/patchcollection.py
diff --git a/WebKitTools/Scripts/webkitpy/queueengine.py b/WebKitTools/Scripts/webkitpy/queueengine.py
new file mode 100644
index 0000000..b234cc1
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/queueengine.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple 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 os
+import time
+import traceback
+
+from datetime import datetime, timedelta
+
+from webkitpy.executive import ScriptError
+from webkitpy.webkit_logging import log, OutputTee
+from webkitpy.statusbot import StatusBot
+
+class QueueEngineDelegate:
+ def queue_log_path(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def work_item_log_path(self, work_item):
+ raise NotImplementedError, "subclasses must implement"
+
+ def begin_work_queue(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def should_continue_work_queue(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def next_work_item(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def should_proceed_with_work_item(self, work_item):
+ # returns (safe_to_proceed, waiting_message, patch)
+ raise NotImplementedError, "subclasses must implement"
+
+ def process_work_item(self, work_item):
+ raise NotImplementedError, "subclasses must implement"
+
+ def handle_unexpected_error(self, work_item, message):
+ raise NotImplementedError, "subclasses must implement"
+
+
+class QueueEngine:
+ def __init__(self, name, delegate):
+ self._name = name
+ self._delegate = delegate
+ self._output_tee = OutputTee()
+
+ log_date_format = "%Y-%m-%d %H:%M:%S"
+ sleep_duration_text = "5 mins"
+ seconds_to_sleep = 300
+ handled_error_code = 2
+
+ # Child processes exit with a special code to the parent queue process can detect the error was handled.
+ @classmethod
+ def exit_after_handled_error(cls, error):
+ log(error)
+ exit(cls.handled_error_code)
+
+ def run(self):
+ self._begin_logging()
+
+ self._delegate.begin_work_queue()
+ while (self._delegate.should_continue_work_queue()):
+ try:
+ self._ensure_work_log_closed()
+ work_item = self._delegate.next_work_item()
+ if not work_item:
+ self._sleep("No work item.")
+ continue
+ if not self._delegate.should_proceed_with_work_item(work_item):
+ self._sleep("Not proceeding with work item.")
+ continue
+
+ # FIXME: Work logs should not depend on bug_id specificaly.
+ # This looks fixed, no?
+ self._open_work_log(work_item)
+ try:
+ self._delegate.process_work_item(work_item)
+ except ScriptError, e:
+ # Use a special exit code to indicate that the error was already
+ # handled in the child process and we should just keep looping.
+ if e.exit_code == self.handled_error_code:
+ continue
+ message = "Unexpected failure when landing patch! Please file a bug against bugzilla-tool.\n%s" % e.message_with_output()
+ self._delegate.handle_unexpected_error(work_item, message)
+ except KeyboardInterrupt, e:
+ log("\nUser terminated queue.")
+ return 1
+ except Exception, e:
+ traceback.print_exc()
+ # Don't try tell the status bot, in case telling it causes an exception.
+ self._sleep("Exception while preparing queue: %s." % e)
+ # Never reached.
+ self._ensure_work_log_closed()
+
+ def _begin_logging(self):
+ self._queue_log = self._output_tee.add_log(self._delegate.queue_log_path())
+ self._work_log = None
+
+ def _open_work_log(self, work_item):
+ work_item_log_path = self._delegate.work_item_log_path(work_item)
+ self._work_log = self._output_tee.add_log(work_item_log_path)
+
+ def _ensure_work_log_closed(self):
+ # If we still have a bug log open, close it.
+ if self._work_log:
+ self._output_tee.remove_log(self._work_log)
+ self._work_log = None
+
+ @classmethod
+ def _sleep_message(cls, message):
+ wake_time = datetime.now() + timedelta(seconds=cls.seconds_to_sleep)
+ return "%s Sleeping until %s (%s)." % (message, wake_time.strftime(cls.log_date_format), cls.sleep_duration_text)
+
+ @classmethod
+ def _sleep(cls, message):
+ log(cls._sleep_message(message))
+ time.sleep(cls.seconds_to_sleep)
diff --git a/WebKitTools/Scripts/webkitpy/queueengine_unittest.py b/WebKitTools/Scripts/webkitpy/queueengine_unittest.py
new file mode 100644
index 0000000..a4036ea
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/queueengine_unittest.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, 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 os
+import shutil
+import tempfile
+import unittest
+
+from webkitpy.executive import ScriptError
+from webkitpy.queueengine import QueueEngine, QueueEngineDelegate
+
+class LoggingDelegate(QueueEngineDelegate):
+ def __init__(self, test):
+ self._test = test
+ self._callbacks = []
+ self._run_before = False
+
+ expected_callbacks = [
+ 'queue_log_path',
+ 'begin_work_queue',
+ 'should_continue_work_queue',
+ 'next_work_item',
+ 'should_proceed_with_work_item',
+ 'work_item_log_path',
+ 'process_work_item',
+ 'should_continue_work_queue'
+ ]
+
+ def record(self, method_name):
+ self._callbacks.append(method_name)
+
+ def queue_log_path(self):
+ self.record("queue_log_path")
+ return os.path.join(self._test.temp_dir, "queue_log_path")
+
+ def work_item_log_path(self, work_item):
+ self.record("work_item_log_path")
+ return os.path.join(self._test.temp_dir, "work_log_path", "%s.log" % work_item)
+
+ def begin_work_queue(self):
+ self.record("begin_work_queue")
+
+ def should_continue_work_queue(self):
+ self.record("should_continue_work_queue")
+ if not self._run_before:
+ self._run_before = True
+ return True
+ return False
+
+ def next_work_item(self):
+ self.record("next_work_item")
+ return "work_item"
+
+ def should_proceed_with_work_item(self, work_item):
+ self.record("should_proceed_with_work_item")
+ self._test.assertEquals(work_item, "work_item")
+ fake_patch = { 'bug_id' : 42 }
+ return (True, "waiting_message", fake_patch)
+
+ def process_work_item(self, work_item):
+ self.record("process_work_item")
+ self._test.assertEquals(work_item, "work_item")
+
+ def handle_unexpected_error(self, work_item, message):
+ self.record("handle_unexpected_error")
+ self._test.assertEquals(work_item, "work_item")
+
+
+class ThrowErrorDelegate(LoggingDelegate):
+ def __init__(self, test, error_code):
+ LoggingDelegate.__init__(self, test)
+ self.error_code = error_code
+
+ def process_work_item(self, work_item):
+ self.record("process_work_item")
+ raise ScriptError(exit_code=self.error_code)
+
+
+class NotSafeToProceedDelegate(LoggingDelegate):
+ def should_proceed_with_work_item(self, work_item):
+ self.record("should_proceed_with_work_item")
+ self._test.assertEquals(work_item, "work_item")
+ return False
+
+
+class FastQueueEngine(QueueEngine):
+ def __init__(self, delegate):
+ QueueEngine.__init__(self, "fast-queue", delegate)
+
+ # No sleep for the wicked.
+ seconds_to_sleep = 0
+
+ def _sleep(self, message):
+ pass
+
+
+class QueueEngineTest(unittest.TestCase):
+ def test_trivial(self):
+ delegate = LoggingDelegate(self)
+ work_queue = QueueEngine("trivial-queue", delegate)
+ work_queue.run()
+ self.assertEquals(delegate._callbacks, LoggingDelegate.expected_callbacks)
+ self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "queue_log_path")))
+ self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "work_log_path", "work_item.log")))
+
+ def test_unexpected_error(self):
+ delegate = ThrowErrorDelegate(self, 3)
+ work_queue = QueueEngine("error-queue", delegate)
+ work_queue.run()
+ expected_callbacks = LoggingDelegate.expected_callbacks[:]
+ work_item_index = expected_callbacks.index('process_work_item')
+ # The unexpected error should be handled right after process_work_item starts
+ # but before any other callback. Otherwise callbacks should be normal.
+ expected_callbacks.insert(work_item_index + 1, 'handle_unexpected_error')
+ self.assertEquals(delegate._callbacks, expected_callbacks)
+
+ def test_handled_error(self):
+ delegate = ThrowErrorDelegate(self, QueueEngine.handled_error_code)
+ work_queue = QueueEngine("handled-error-queue", delegate)
+ work_queue.run()
+ self.assertEquals(delegate._callbacks, LoggingDelegate.expected_callbacks)
+
+ def test_not_safe_to_proceed(self):
+ delegate = NotSafeToProceedDelegate(self)
+ work_queue = FastQueueEngine(delegate)
+ work_queue.run()
+ expected_callbacks = LoggingDelegate.expected_callbacks[:]
+ next_work_item_index = expected_callbacks.index('next_work_item')
+ # We slice out the common part of the expected callbacks.
+ # We add 2 here to include should_proceed_with_work_item, which is
+ # a pain to search for directly because it occurs twice.
+ expected_callbacks = expected_callbacks[:next_work_item_index + 2]
+ expected_callbacks.append('should_continue_work_queue')
+ self.assertEquals(delegate._callbacks, expected_callbacks)
+
+ def setUp(self):
+ self.temp_dir = tempfile.mkdtemp(suffix="work_queue_test_logs")
+
+ def tearDown(self):
+ shutil.rmtree(self.temp_dir)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/WebKitTools/Scripts/webkitpy/scm.py b/WebKitTools/Scripts/webkitpy/scm.py
new file mode 100644
index 0000000..9a2fc4a
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/scm.py
@@ -0,0 +1,512 @@
+# Copyright (c) 2009, Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple 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.
+#
+# Python module for interacting with an SCM system (like SVN or Git)
+
+import os
+import re
+import subprocess
+
+# Import WebKit-specific modules.
+from webkitpy.changelogs import ChangeLog
+from webkitpy.executive import Executive, run_command, ScriptError
+from webkitpy.webkit_logging import error, log
+
+def detect_scm_system(path):
+ if SVN.in_working_directory(path):
+ return SVN(cwd=path)
+
+ if Git.in_working_directory(path):
+ return Git(cwd=path)
+
+ return None
+
+def first_non_empty_line_after_index(lines, index=0):
+ first_non_empty_line = index
+ for line in lines[index:]:
+ if re.match("^\s*$", line):
+ first_non_empty_line += 1
+ else:
+ break
+ return first_non_empty_line
+
+
+class CommitMessage:
+ def __init__(self, message):
+ self.message_lines = message[first_non_empty_line_after_index(message, 0):]
+
+ def body(self, lstrip=False):
+ lines = self.message_lines[first_non_empty_line_after_index(self.message_lines, 1):]
+ if lstrip:
+ lines = [line.lstrip() for line in lines]
+ return "\n".join(lines) + "\n"
+
+ def description(self, lstrip=False, strip_url=False):
+ line = self.message_lines[0]
+ if lstrip:
+ line = line.lstrip()
+ if strip_url:
+ line = re.sub("^(\s*)<.+> ", "\1", line)
+ return line
+
+ def message(self):
+ return "\n".join(self.message_lines) + "\n"
+
+
+class CheckoutNeedsUpdate(ScriptError):
+ def __init__(self, script_args, exit_code, output, cwd):
+ ScriptError.__init__(self, script_args=script_args, exit_code=exit_code, output=output, cwd=cwd)
+
+
+def commit_error_handler(error):
+ if re.search("resource out of date", error.output):
+ raise CheckoutNeedsUpdate(script_args=error.script_args, exit_code=error.exit_code, output=error.output, cwd=error.cwd)
+ Executive.default_error_handler(error)
+
+
+class SCM:
+ def __init__(self, cwd, dryrun=False):
+ self.cwd = cwd
+ self.checkout_root = self.find_checkout_root(self.cwd)
+ self.dryrun = dryrun
+
+ def scripts_directory(self):
+ return os.path.join(self.checkout_root, "WebKitTools", "Scripts")
+
+ def script_path(self, script_name):
+ return os.path.join(self.scripts_directory(), script_name)
+
+ def ensure_clean_working_directory(self, force_clean):
+ if not force_clean and not self.working_directory_is_clean():
+ print run_command(self.status_command(), error_handler=Executive.ignore_error)
+ raise ScriptError(message="Working directory has modifications, pass --force-clean or --no-clean to continue.")
+
+ log("Cleaning working directory")
+ self.clean_working_directory()
+
+ def ensure_no_local_commits(self, force):
+ if not self.supports_local_commits():
+ return
+ commits = self.local_commits()
+ if not len(commits):
+ return
+ if not force:
+ error("Working directory has local commits, pass --force-clean to continue.")
+ self.discard_local_commits()
+
+ def apply_patch(self, patch, force=False):
+ # It's possible that the patch was not made from the root directory.
+ # We should detect and handle that case.
+ curl_process = subprocess.Popen(['curl', '--location', '--silent', '--show-error', patch['url']], stdout=subprocess.PIPE)
+ args = [self.script_path('svn-apply')]
+ if patch.get('reviewer'):
+ args += ['--reviewer', patch['reviewer']]
+ if force:
+ args.append('--force')
+
+ run_command(args, input=curl_process.stdout)
+
+ def run_status_and_extract_filenames(self, status_command, status_regexp):
+ filenames = []
+ for line in run_command(status_command).splitlines():
+ match = re.search(status_regexp, line)
+ if not match:
+ continue
+ # status = match.group('status')
+ filename = match.group('filename')
+ filenames.append(filename)
+ return filenames
+
+ def strip_r_from_svn_revision(self, svn_revision):
+ match = re.match("^r(?P<svn_revision>\d+)", svn_revision)
+ if (match):
+ return match.group('svn_revision')
+ return svn_revision
+
+ def svn_revision_from_commit_text(self, commit_text):
+ match = re.search(self.commit_success_regexp(), commit_text, re.MULTILINE)
+ return match.group('svn_revision')
+
+ # ChangeLog-specific code doesn't really belong in scm.py, but this function is very useful.
+ def modified_changelogs(self):
+ changelog_paths = []
+ paths = self.changed_files()
+ for path in paths:
+ if os.path.basename(path) == "ChangeLog":
+ changelog_paths.append(path)
+ return changelog_paths
+
+ # FIXME: Requires unit test
+ # FIXME: commit_message_for_this_commit and modified_changelogs don't
+ # really belong here. We should have a separate module for
+ # handling ChangeLogs.
+ def commit_message_for_this_commit(self):
+ changelog_paths = self.modified_changelogs()
+ if not len(changelog_paths):
+ raise ScriptError(message="Found no modified ChangeLogs, cannot create a commit message.\n"
+ "All changes require a ChangeLog. See:\n"
+ "http://webkit.org/coding/contributing.html")
+
+ changelog_messages = []
+ for changelog_path in changelog_paths:
+ log("Parsing ChangeLog: %s" % changelog_path)
+ changelog_entry = ChangeLog(changelog_path).latest_entry()
+ if not changelog_entry:
+ raise ScriptError(message="Failed to parse ChangeLog: " + os.path.abspath(changelog_path))
+ changelog_messages.append(changelog_entry)
+
+ # FIXME: We should sort and label the ChangeLog messages like commit-log-editor does.
+ return CommitMessage("".join(changelog_messages).splitlines())
+
+ @staticmethod
+ def in_working_directory(path):
+ raise NotImplementedError, "subclasses must implement"
+
+ @staticmethod
+ def find_checkout_root(path):
+ raise NotImplementedError, "subclasses must implement"
+
+ @staticmethod
+ def commit_success_regexp():
+ raise NotImplementedError, "subclasses must implement"
+
+ def working_directory_is_clean(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def clean_working_directory(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def status_command(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def changed_files(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def display_name(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def create_patch(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def diff_for_revision(self, revision):
+ raise NotImplementedError, "subclasses must implement"
+
+ def apply_reverse_diff(self, revision):
+ raise NotImplementedError, "subclasses must implement"
+
+ def revert_files(self, file_paths):
+ raise NotImplementedError, "subclasses must implement"
+
+ def commit_with_message(self, message):
+ raise NotImplementedError, "subclasses must implement"
+
+ def svn_commit_log(self, svn_revision):
+ raise NotImplementedError, "subclasses must implement"
+
+ def last_svn_commit_log(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ # Subclasses must indicate if they support local commits,
+ # but the SCM baseclass will only call local_commits methods when this is true.
+ @staticmethod
+ def supports_local_commits():
+ raise NotImplementedError, "subclasses must implement"
+
+ def create_patch_from_local_commit(self, commit_id):
+ error("Your source control manager does not support creating a patch from a local commit.")
+
+ def create_patch_since_local_commit(self, commit_id):
+ error("Your source control manager does not support creating a patch from a local commit.")
+
+ def commit_locally_with_message(self, message):
+ error("Your source control manager does not support local commits.")
+
+ def discard_local_commits(self):
+ pass
+
+ def local_commits(self):
+ return []
+
+
+class SVN(SCM):
+ def __init__(self, cwd, dryrun=False):
+ SCM.__init__(self, cwd, dryrun)
+ self.cached_version = None
+
+ @staticmethod
+ def in_working_directory(path):
+ return os.path.isdir(os.path.join(path, '.svn'))
+
+ @classmethod
+ def find_uuid(cls, path):
+ if not cls.in_working_directory(path):
+ return None
+ return cls.value_from_svn_info(path, 'Repository UUID')
+
+ @classmethod
+ def value_from_svn_info(cls, path, field_name):
+ svn_info_args = ['svn', 'info', path]
+ info_output = run_command(svn_info_args).rstrip()
+ match = re.search("^%s: (?P<value>.+)$" % field_name, info_output, re.MULTILINE)
+ if not match:
+ raise ScriptError(script_args=svn_info_args, message='svn info did not contain a %s.' % field_name)
+ return match.group('value')
+
+ @staticmethod
+ def find_checkout_root(path):
+ uuid = SVN.find_uuid(path)
+ # If |path| is not in a working directory, we're supposed to return |path|.
+ if not uuid:
+ return path
+ # Search up the directory hierarchy until we find a different UUID.
+ last_path = None
+ while True:
+ if uuid != SVN.find_uuid(path):
+ return last_path
+ last_path = path
+ (path, last_component) = os.path.split(path)
+ if last_path == path:
+ return None
+
+ @staticmethod
+ def commit_success_regexp():
+ return "^Committed revision (?P<svn_revision>\d+)\.$"
+
+ def svn_version(self):
+ if not self.cached_version:
+ self.cached_version = run_command(['svn', '--version', '--quiet'])
+
+ return self.cached_version
+
+ def working_directory_is_clean(self):
+ return run_command(['svn', 'diff']) == ""
+
+ def clean_working_directory(self):
+ run_command(['svn', 'revert', '-R', '.'])
+
+ def status_command(self):
+ return ['svn', 'status']
+
+ def changed_files(self):
+ if self.svn_version() > "1.6":
+ status_regexp = "^(?P<status>[ACDMR]).{6} (?P<filename>.+)$"
+ else:
+ status_regexp = "^(?P<status>[ACDMR]).{5} (?P<filename>.+)$"
+ return self.run_status_and_extract_filenames(self.status_command(), status_regexp)
+
+ @staticmethod
+ def supports_local_commits():
+ return False
+
+ def display_name(self):
+ return "svn"
+
+ def create_patch(self):
+ return run_command(self.script_path("svn-create-patch"), cwd=self.checkout_root, return_stderr=False)
+
+ def diff_for_revision(self, revision):
+ return run_command(['svn', 'diff', '-c', str(revision)])
+
+ def _repository_url(self):
+ return self.value_from_svn_info(self.checkout_root, 'URL')
+
+ def apply_reverse_diff(self, revision):
+ # '-c -revision' applies the inverse diff of 'revision'
+ svn_merge_args = ['svn', 'merge', '--non-interactive', '-c', '-%s' % revision, self._repository_url()]
+ log("WARNING: svn merge has been known to take more than 10 minutes to complete. It is recommended you use git for rollouts.")
+ log("Running '%s'" % " ".join(svn_merge_args))
+ run_command(svn_merge_args)
+
+ def revert_files(self, file_paths):
+ run_command(['svn', 'revert'] + file_paths)
+
+ def commit_with_message(self, message):
+ if self.dryrun:
+ # Return a string which looks like a commit so that things which parse this output will succeed.
+ return "Dry run, no commit.\nCommitted revision 0."
+ return run_command(['svn', 'commit', '-m', message], error_handler=commit_error_handler)
+
+ def svn_commit_log(self, svn_revision):
+ svn_revision = self.strip_r_from_svn_revision(str(svn_revision))
+ return run_command(['svn', 'log', '--non-interactive', '--revision', svn_revision]);
+
+ def last_svn_commit_log(self):
+ # BASE is the checkout revision, HEAD is the remote repository revision
+ # http://svnbook.red-bean.com/en/1.0/ch03s03.html
+ return self.svn_commit_log('BASE')
+
+# All git-specific logic should go here.
+class Git(SCM):
+ def __init__(self, cwd, dryrun=False):
+ SCM.__init__(self, cwd, dryrun)
+
+ @classmethod
+ def in_working_directory(cls, path):
+ return run_command(['git', 'rev-parse', '--is-inside-work-tree'], cwd=path, error_handler=Executive.ignore_error).rstrip() == "true"
+
+ @classmethod
+ def find_checkout_root(cls, path):
+ # "git rev-parse --show-cdup" would be another way to get to the root
+ (checkout_root, dot_git) = os.path.split(run_command(['git', 'rev-parse', '--git-dir'], cwd=path))
+ # If we were using 2.6 # checkout_root = os.path.relpath(checkout_root, path)
+ if not os.path.isabs(checkout_root): # Sometimes git returns relative paths
+ checkout_root = os.path.join(path, checkout_root)
+ return checkout_root
+
+ @staticmethod
+ def commit_success_regexp():
+ return "^Committed r(?P<svn_revision>\d+)$"
+
+
+ def discard_local_commits(self):
+ run_command(['git', 'reset', '--hard', 'trunk'])
+
+ def local_commits(self):
+ return run_command(['git', 'log', '--pretty=oneline', 'HEAD...trunk']).splitlines()
+
+ def rebase_in_progress(self):
+ return os.path.exists(os.path.join(self.checkout_root, '.git/rebase-apply'))
+
+ def working_directory_is_clean(self):
+ return run_command(['git', 'diff-index', 'HEAD']) == ""
+
+ def clean_working_directory(self):
+ # Could run git clean here too, but that wouldn't match working_directory_is_clean
+ run_command(['git', 'reset', '--hard', 'HEAD'])
+ # Aborting rebase even though this does not match working_directory_is_clean
+ if self.rebase_in_progress():
+ run_command(['git', 'rebase', '--abort'])
+
+ def status_command(self):
+ return ['git', 'status']
+
+ def changed_files(self):
+ status_command = ['git', 'diff', '-r', '--name-status', '-C', '-M', 'HEAD']
+ status_regexp = '^(?P<status>[ADM])\t(?P<filename>.+)$'
+ return self.run_status_and_extract_filenames(status_command, status_regexp)
+
+ @staticmethod
+ def supports_local_commits():
+ return True
+
+ def display_name(self):
+ return "git"
+
+ def create_patch(self):
+ return run_command(['git', 'diff', '--binary', 'HEAD'])
+
+ @classmethod
+ def git_commit_from_svn_revision(cls, revision):
+ # git svn find-rev always exits 0, even when the revision is not found.
+ return run_command(['git', 'svn', 'find-rev', 'r%s' % revision]).rstrip()
+
+ def diff_for_revision(self, revision):
+ git_commit = self.git_commit_from_svn_revision(revision)
+ return self.create_patch_from_local_commit(git_commit)
+
+ def apply_reverse_diff(self, revision):
+ # Assume the revision is an svn revision.
+ git_commit = self.git_commit_from_svn_revision(revision)
+ if not git_commit:
+ raise ScriptError(message='Failed to find git commit for revision %s, git svn log output: "%s"' % (revision, git_commit))
+
+ # I think this will always fail due to ChangeLogs.
+ # FIXME: We need to detec specific failure conditions and handle them.
+ run_command(['git', 'revert', '--no-commit', git_commit], error_handler=Executive.ignore_error)
+
+ # Fix any ChangeLogs if necessary.
+ changelog_paths = self.modified_changelogs()
+ if len(changelog_paths):
+ run_command([self.script_path('resolve-ChangeLogs')] + changelog_paths)
+
+ def revert_files(self, file_paths):
+ run_command(['git', 'checkout', 'HEAD'] + file_paths)
+
+ def commit_with_message(self, message):
+ self.commit_locally_with_message(message)
+ return self.push_local_commits_to_server()
+
+ def svn_commit_log(self, svn_revision):
+ svn_revision = self.strip_r_from_svn_revision(svn_revision)
+ return run_command(['git', 'svn', 'log', '-r', svn_revision])
+
+ def last_svn_commit_log(self):
+ return run_command(['git', 'svn', 'log', '--limit=1'])
+
+ # Git-specific methods:
+
+ def create_patch_from_local_commit(self, commit_id):
+ return run_command(['git', 'diff', '--binary', commit_id + "^.." + commit_id])
+
+ def create_patch_since_local_commit(self, commit_id):
+ return run_command(['git', 'diff', '--binary', commit_id])
+
+ def commit_locally_with_message(self, message):
+ run_command(['git', 'commit', '--all', '-F', '-'], input=message)
+
+ def push_local_commits_to_server(self):
+ if self.dryrun:
+ # Return a string which looks like a commit so that things which parse this output will succeed.
+ return "Dry run, no remote commit.\nCommitted r0"
+ return run_command(['git', 'svn', 'dcommit'], error_handler=commit_error_handler)
+
+ # This function supports the following argument formats:
+ # no args : rev-list trunk..HEAD
+ # A..B : rev-list A..B
+ # A...B : error!
+ # A B : [A, B] (different from git diff, which would use "rev-list A..B")
+ def commit_ids_from_commitish_arguments(self, args):
+ if not len(args):
+ # FIXME: trunk is not always the remote branch name, need a way to detect the name.
+ args.append('trunk..HEAD')
+
+ commit_ids = []
+ for commitish in args:
+ if '...' in commitish:
+ raise ScriptError(message="'...' is not supported (found in '%s'). Did you mean '..'?" % commitish)
+ elif '..' in commitish:
+ commit_ids += reversed(run_command(['git', 'rev-list', commitish]).splitlines())
+ else:
+ # Turn single commits or branch or tag names into commit ids.
+ commit_ids += run_command(['git', 'rev-parse', '--revs-only', commitish]).splitlines()
+ return commit_ids
+
+ def commit_message_for_local_commit(self, commit_id):
+ commit_lines = run_command(['git', 'cat-file', 'commit', commit_id]).splitlines()
+
+ # Skip the git headers.
+ first_line_after_headers = 0
+ for line in commit_lines:
+ first_line_after_headers += 1
+ if line == "":
+ break
+ return CommitMessage(commit_lines[first_line_after_headers:])
+
+ def files_changed_summary_for_commit(self, commit_id):
+ return run_command(['git', 'diff-tree', '--shortstat', '--no-commit-id', commit_id])
diff --git a/WebKitTools/Scripts/webkitpy/scm_unittest.py b/WebKitTools/Scripts/webkitpy/scm_unittest.py
new file mode 100644
index 0000000..c68d367
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/scm_unittest.py
@@ -0,0 +1,594 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+# Copyright (C) 2009 Apple 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 base64
+import os
+import os.path
+import re
+import stat
+import subprocess
+import tempfile
+import unittest
+import urllib
+
+from datetime import date
+from webkitpy.executive import Executive, run_command, ScriptError
+from webkitpy.scm import detect_scm_system, SCM, CheckoutNeedsUpdate, commit_error_handler
+
+# Eventually we will want to write tests which work for both scms. (like update_webkit, changed_files, etc.)
+# Perhaps through some SCMTest base-class which both SVNTest and GitTest inherit from.
+
+# FIXME: This should be unified into one of the executive.py commands!
+def run_silent(args, cwd=None):
+ process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
+ process.communicate() # ignore output
+ exit_code = process.wait()
+ if exit_code:
+ raise ScriptError('Failed to run "%s" exit_code: %d cwd: %s' % (args, exit_code, cwd))
+
+def write_into_file_at_path(file_path, contents):
+ file = open(file_path, 'w')
+ file.write(contents)
+ file.close()
+
+def read_from_path(file_path):
+ file = open(file_path, 'r')
+ contents = file.read()
+ file.close()
+ return contents
+
+# Exists to share svn repository creation code between the git and svn tests
+class SVNTestRepository:
+ @staticmethod
+ def _setup_test_commits(test_object):
+ # Add some test commits
+ os.chdir(test_object.svn_checkout_path)
+ test_file = open('test_file', 'w')
+ test_file.write("test1")
+ test_file.flush()
+
+ run_command(['svn', 'add', 'test_file'])
+ run_command(['svn', 'commit', '--quiet', '--message', 'initial commit'])
+
+ test_file.write("test2")
+ test_file.flush()
+
+ run_command(['svn', 'commit', '--quiet', '--message', 'second commit'])
+
+ test_file.write("test3\n")
+ test_file.flush()
+
+ run_command(['svn', 'commit', '--quiet', '--message', 'third commit'])
+
+ test_file.write("test4\n")
+ test_file.close()
+
+ run_command(['svn', 'commit', '--quiet', '--message', 'fourth commit'])
+
+ # svn does not seem to update after commit as I would expect.
+ run_command(['svn', 'update'])
+
+ @classmethod
+ def setup(cls, test_object):
+ # Create an test SVN repository
+ test_object.svn_repo_path = tempfile.mkdtemp(suffix="svn_test_repo")
+ test_object.svn_repo_url = "file://%s" % test_object.svn_repo_path # Not sure this will work on windows
+ # git svn complains if we don't pass --pre-1.5-compatible, not sure why:
+ # Expected FS format '2'; found format '3' at /usr/local/libexec/git-core//git-svn line 1477
+ run_command(['svnadmin', 'create', '--pre-1.5-compatible', test_object.svn_repo_path])
+
+ # Create a test svn checkout
+ test_object.svn_checkout_path = tempfile.mkdtemp(suffix="svn_test_checkout")
+ run_command(['svn', 'checkout', '--quiet', test_object.svn_repo_url, test_object.svn_checkout_path])
+
+ cls._setup_test_commits(test_object)
+
+ @classmethod
+ def tear_down(cls, test_object):
+ run_command(['rm', '-rf', test_object.svn_repo_path])
+ run_command(['rm', '-rf', test_object.svn_checkout_path])
+
+# For testing the SCM baseclass directly.
+class SCMClassTests(unittest.TestCase):
+ def setUp(self):
+ self.dev_null = open(os.devnull, "w") # Used to make our Popen calls quiet.
+
+ def tearDown(self):
+ self.dev_null.close()
+
+ def test_run_command_with_pipe(self):
+ input_process = subprocess.Popen(['echo', 'foo\nbar'], stdout=subprocess.PIPE, stderr=self.dev_null)
+ self.assertEqual(run_command(['grep', 'bar'], input=input_process.stdout), "bar\n")
+
+ # Test the non-pipe case too:
+ self.assertEqual(run_command(['grep', 'bar'], input="foo\nbar"), "bar\n")
+
+ command_returns_non_zero = ['/bin/sh', '--invalid-option']
+ # Test when the input pipe process fails.
+ input_process = subprocess.Popen(command_returns_non_zero, stdout=subprocess.PIPE, stderr=self.dev_null)
+ self.assertTrue(input_process.poll() != 0)
+ self.assertRaises(ScriptError, run_command, ['grep', 'bar'], input=input_process.stdout)
+
+ # Test when the run_command process fails.
+ input_process = subprocess.Popen(['echo', 'foo\nbar'], stdout=subprocess.PIPE, stderr=self.dev_null) # grep shows usage and calls exit(2) when called w/o arguments.
+ self.assertRaises(ScriptError, run_command, command_returns_non_zero, input=input_process.stdout)
+
+ def test_error_handlers(self):
+ git_failure_message="Merge conflict during commit: Your file or directory 'WebCore/ChangeLog' is probably out-of-date: resource out of date; try updating at /usr/local/libexec/git-core//git-svn line 469"
+ svn_failure_message="""svn: Commit failed (details follow):
+svn: File or directory 'ChangeLog' is out of date; try updating
+svn: resource out of date; try updating
+"""
+ command_does_not_exist = ['does_not_exist', 'invalid_option']
+ self.assertRaises(OSError, run_command, command_does_not_exist)
+ self.assertRaises(OSError, run_command, command_does_not_exist, error_handler=Executive.ignore_error)
+
+ command_returns_non_zero = ['/bin/sh', '--invalid-option']
+ self.assertRaises(ScriptError, run_command, command_returns_non_zero)
+ # Check if returns error text:
+ self.assertTrue(run_command(command_returns_non_zero, error_handler=Executive.ignore_error))
+
+ self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=git_failure_message))
+ self.assertRaises(CheckoutNeedsUpdate, commit_error_handler, ScriptError(output=svn_failure_message))
+ self.assertRaises(ScriptError, commit_error_handler, ScriptError(output='blah blah blah'))
+
+
+# GitTest and SVNTest inherit from this so any test_ methods here will be run once for this class and then once for each subclass.
+class SCMTest(unittest.TestCase):
+ def _create_patch(self, patch_contents):
+ patch_path = os.path.join(self.svn_checkout_path, 'patch.diff')
+ write_into_file_at_path(patch_path, patch_contents)
+ patch = {}
+ patch['reviewer'] = 'Joe Cool'
+ patch['bug_id'] = '12345'
+ patch['url'] = 'file://%s' % urllib.pathname2url(patch_path)
+ return patch
+
+ def _setup_webkittools_scripts_symlink(self, local_scm):
+ webkit_scm = detect_scm_system(os.path.dirname(os.path.abspath(__file__)))
+ webkit_scripts_directory = webkit_scm.scripts_directory()
+ local_scripts_directory = local_scm.scripts_directory()
+ os.mkdir(os.path.dirname(local_scripts_directory))
+ os.symlink(webkit_scripts_directory, local_scripts_directory)
+
+ # Tests which both GitTest and SVNTest should run.
+ # FIXME: There must be a simpler way to add these w/o adding a wrapper method to both subclasses
+ def _shared_test_commit_with_message(self):
+ write_into_file_at_path('test_file', 'more test content')
+ commit_text = self.scm.commit_with_message('another test commit')
+ self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '5')
+
+ self.scm.dryrun = True
+ write_into_file_at_path('test_file', 'still more test content')
+ commit_text = self.scm.commit_with_message('yet another test commit')
+ self.assertEqual(self.scm.svn_revision_from_commit_text(commit_text), '0')
+
+ def _shared_test_reverse_diff(self):
+ self._setup_webkittools_scripts_symlink(self.scm) # Git's apply_reverse_diff uses resolve-ChangeLogs
+ # Only test the simple case, as any other will end up with conflict markers.
+ self.scm.apply_reverse_diff('4')
+ self.assertEqual(read_from_path('test_file'), "test1test2test3\n")
+
+ def _shared_test_diff_for_revision(self):
+ # Patch formats are slightly different between svn and git, so just regexp for things we know should be there.
+ r3_patch = self.scm.diff_for_revision(3)
+ self.assertTrue(re.search('test3', r3_patch))
+ self.assertFalse(re.search('test4', r3_patch))
+ self.assertTrue(re.search('test2', r3_patch))
+ self.assertTrue(re.search('test2', self.scm.diff_for_revision(2)))
+
+ def _shared_test_svn_apply_git_patch(self):
+ self._setup_webkittools_scripts_symlink(self.scm)
+ git_binary_addition = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
+new file mode 100644
+index 0000000000000000000000000000000000000000..64a9532e7794fcd791f6f12157406d90
+60151690
+GIT binary patch
+literal 512
+zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c?
+zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap
+zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ
+zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A
+zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&)
+zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b
+zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB
+z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A at 16O26ud7H<QM=xl`toLKnz-3h at 9c9q&wm|X
+z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4
+ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H
+
+literal 0
+HcmV?d00001
+
+"""
+ self.scm.apply_patch(self._create_patch(git_binary_addition))
+ added = read_from_path('fizzbuzz7.gif')
+ self.assertEqual(512, len(added))
+ self.assertTrue(added.startswith('GIF89a'))
+ self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
+
+ # The file already exists.
+ self.assertRaises(ScriptError, self.scm.apply_patch, self._create_patch(git_binary_addition))
+
+ git_binary_modification = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
+index 64a9532e7794fcd791f6f12157406d9060151690..323fae03f4606ea9991df8befbb2fca7
+GIT binary patch
+literal 7
+OcmYex&reD$;sO8*F9L)B
+
+literal 512
+zcmZ?wbhEHbRAx|MU|?iW{Kxc~?KofD;ckY;H+&5HnHl!!GQMD7h+sU{_)e9f^V3c?
+zhJP##HdZC#4K}7F68@!1jfWQg2daCm-gs#3|JREDT>c+pG4L<_2;w##WMO#ysPPap
+zLqpAf1OE938xAsSp4!5f-o><?VKe(#0jEcwfHGF4%M1^kRs14oVBp2ZEL{E1N<-zJ
+zsfLmOtKta;2_;2c#^S1-8cf<nb!QnGl>c!Xe6RXvrEtAWBvSDTgTO1j3vA31Puw!A
+zs(87q)j_mVDTqBo-P+03-P5mHCEnJ+x}YdCuS7#bCCyePUe(ynK+|4b-3qK)T?Z&)
+zYG+`tl4h?GZv_$t82}X4*DTE|$;{DEiPyF@)U-1+FaX++T9H{&%cag`W1|zVP@`%b
+zqiSkp6{BTpWTkCr!=<C6Q=?#~R8^JfrliAF6Q^gV9Iup8RqCXqqhqC`qsyhk<-nlB
+z00f{QZvfK&|Nm#oZ0TQl`Yr$BIa6A at 16O26ud7H<QM=xl`toLKnz-3h at 9c9q&wm|X
+z{89I|WPyD!*M?gv?q`;L=2YFeXrJQNti4?}s!zFo=5CzeBxC69xA<zrjP<wUcCRh4
+ptUl-ZG<%a~#LwkIWv&q!KSCH7tQ8cJDiw+|GV?MN)RjY50RTb-xvT&H
+
+"""
+ self.scm.apply_patch(self._create_patch(git_binary_modification))
+ modified = read_from_path('fizzbuzz7.gif')
+ self.assertEqual('foobar\n', modified)
+ self.assertTrue('fizzbuzz7.gif' in self.scm.changed_files())
+
+ # Applying the same modification should fail.
+ self.assertRaises(ScriptError, self.scm.apply_patch, self._create_patch(git_binary_modification))
+
+ git_binary_deletion = """diff --git a/fizzbuzz7.gif b/fizzbuzz7.gif
+deleted file mode 100644
+index 323fae0..0000000
+GIT binary patch
+literal 0
+HcmV?d00001
+
+literal 7
+OcmYex&reD$;sO8*F9L)B
+
+"""
+ self.scm.apply_patch(self._create_patch(git_binary_deletion))
+ self.assertFalse(os.path.exists('fizzbuzz7.gif'))
+ self.assertFalse('fizzbuzz7.gif' in self.scm.changed_files())
+
+ # Cannot delete again.
+ self.assertRaises(ScriptError, self.scm.apply_patch, self._create_patch(git_binary_deletion))
+
+
+class SVNTest(SCMTest):
+
+ @staticmethod
+ def _set_date_and_reviewer(changelog_entry):
+ # Joe Cool matches the reviewer set in SCMTest._create_patch
+ changelog_entry = changelog_entry.replace('REVIEWER_HERE', 'Joe Cool')
+ # svn-apply will update ChangeLog entries with today's date.
+ return changelog_entry.replace('DATE_HERE', date.today().isoformat())
+
+ def test_svn_apply(self):
+ first_entry = """2009-10-26 Eric Seidel <eric at webkit.org>
+
+ Reviewed by Foo Bar.
+
+ Most awesome change ever.
+
+ * scm_unittest.py:
+"""
+ intermediate_entry = """2009-10-27 Eric Seidel <eric at webkit.org>
+
+ Reviewed by Baz Bar.
+
+ A more awesomer change yet!
+
+ * scm_unittest.py:
+"""
+ one_line_overlap_patch = """Index: ChangeLog
+===================================================================
+--- ChangeLog (revision 5)
++++ ChangeLog (working copy)
+@@ -1,5 +1,13 @@
+ 2009-10-26 Eric Seidel <eric at webkit.org>
+
++ Reviewed by NOBODY (OOPS!).
++
++ Second most awsome change ever.
++
++ * scm_unittest.py:
++
++2009-10-26 Eric Seidel <eric at webkit.org>
++
+ Reviewed by Foo Bar.
+
+ Most awesome change ever.
+"""
+ one_line_overlap_entry = """DATE_HERE Eric Seidel <eric at webkit.org>
+
+ Reviewed by REVIEWER_HERE.
+
+ Second most awsome change ever.
+
+ * scm_unittest.py:
+"""
+ two_line_overlap_patch = """Index: ChangeLog
+===================================================================
+--- ChangeLog (revision 5)
++++ ChangeLog (working copy)
+@@ -2,6 +2,14 @@
+
+ Reviewed by Foo Bar.
+
++ Second most awsome change ever.
++
++ * scm_unittest.py:
++
++2009-10-26 Eric Seidel <eric at webkit.org>
++
++ Reviewed by Foo Bar.
++
+ Most awesome change ever.
+
+ * scm_unittest.py:
+"""
+ two_line_overlap_entry = """DATE_HERE Eric Seidel <eric at webkit.org>
+
+ Reviewed by Foo Bar.
+
+ Second most awsome change ever.
+
+ * scm_unittest.py:
+"""
+ write_into_file_at_path('ChangeLog', first_entry)
+ run_command(['svn', 'add', 'ChangeLog'])
+ run_command(['svn', 'commit', '--quiet', '--message', 'ChangeLog commit'])
+
+ # Patch files were created against just 'first_entry'.
+ # Add a second commit to make svn-apply have to apply the patches with fuzz.
+ changelog_contents = "%s\n%s" % (intermediate_entry, first_entry)
+ write_into_file_at_path('ChangeLog', changelog_contents)
+ run_command(['svn', 'commit', '--quiet', '--message', 'Intermediate commit'])
+
+ self._setup_webkittools_scripts_symlink(self.scm)
+ self.scm.apply_patch(self._create_patch(one_line_overlap_patch))
+ expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(one_line_overlap_entry), changelog_contents)
+ self.assertEquals(read_from_path('ChangeLog'), expected_changelog_contents)
+
+ self.scm.revert_files(['ChangeLog'])
+ self.scm.apply_patch(self._create_patch(two_line_overlap_patch))
+ expected_changelog_contents = "%s\n%s" % (self._set_date_and_reviewer(two_line_overlap_entry), changelog_contents)
+ self.assertEquals(read_from_path('ChangeLog'), expected_changelog_contents)
+
+ def setUp(self):
+ SVNTestRepository.setup(self)
+ os.chdir(self.svn_checkout_path)
+ self.scm = detect_scm_system(self.svn_checkout_path)
+
+ def tearDown(self):
+ SVNTestRepository.tear_down(self)
+
+ def test_create_patch_is_full_patch(self):
+ test_dir_path = os.path.join(self.svn_checkout_path, 'test_dir')
+ os.mkdir(test_dir_path)
+ test_file_path = os.path.join(test_dir_path, 'test_file2')
+ write_into_file_at_path(test_file_path, 'test content')
+ run_command(['svn', 'add', 'test_dir'])
+
+ # create_patch depends on 'svn-create-patch', so make a dummy version.
+ scripts_path = os.path.join(self.svn_checkout_path, 'WebKitTools', 'Scripts')
+ os.makedirs(scripts_path)
+ create_patch_path = os.path.join(scripts_path, 'svn-create-patch')
+ write_into_file_at_path(create_patch_path, '#!/bin/sh\necho $PWD') # We could pass -n to prevent the \n, but not all echo accept -n.
+ os.chmod(create_patch_path, stat.S_IXUSR | stat.S_IRUSR)
+
+ # Change into our test directory and run the create_patch command.
+ os.chdir(test_dir_path)
+ scm = detect_scm_system(test_dir_path)
+ self.assertEqual(scm.checkout_root, self.svn_checkout_path) # Sanity check that detection worked right.
+ patch_contents = scm.create_patch()
+ # Our fake 'svn-create-patch' returns $PWD instead of a patch, check that it was executed from the root of the repo.
+ self.assertEqual("%s\n" % os.path.realpath(scm.checkout_root), patch_contents) # Add a \n because echo adds a \n.
+
+ def test_detection(self):
+ scm = detect_scm_system(self.svn_checkout_path)
+ self.assertEqual(scm.display_name(), "svn")
+ self.assertEqual(scm.supports_local_commits(), False)
+
+ def test_apply_small_binary_patch(self):
+ patch_contents = """Index: test_file.swf
+===================================================================
+Cannot display: file marked as a binary type.
+svn:mime-type = application/octet-stream
+
+Property changes on: test_file.swf
+___________________________________________________________________
+Name: svn:mime-type
+ + application/octet-stream
+
+
+Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==
+"""
+ expected_contents = base64.b64decode("Q1dTBx0AAAB42itg4GlgYJjGwMDDyODMxMDw34GBgQEAJPQDJA==")
+ self._setup_webkittools_scripts_symlink(self.scm)
+ patch_file = self._create_patch(patch_contents)
+ self.scm.apply_patch(patch_file)
+ actual_contents = read_from_path("test_file.swf")
+ self.assertEqual(actual_contents, expected_contents)
+
+ def test_apply_svn_patch(self):
+ scm = detect_scm_system(self.svn_checkout_path)
+ patch = self._create_patch(run_command(['svn', 'diff', '-r4:3']))
+ self._setup_webkittools_scripts_symlink(scm)
+ scm.apply_patch(patch)
+
+ def test_apply_svn_patch_force(self):
+ scm = detect_scm_system(self.svn_checkout_path)
+ patch = self._create_patch(run_command(['svn', 'diff', '-r2:4']))
+ self._setup_webkittools_scripts_symlink(scm)
+ self.assertRaises(ScriptError, scm.apply_patch, patch, force=True)
+
+ def test_commit_logs(self):
+ # Commits have dates and usernames in them, so we can't just direct compare.
+ self.assertTrue(re.search('fourth commit', self.scm.last_svn_commit_log()))
+ self.assertTrue(re.search('second commit', self.scm.svn_commit_log(2)))
+
+ def test_commit_text_parsing(self):
+ self._shared_test_commit_with_message()
+
+ def test_reverse_diff(self):
+ self._shared_test_reverse_diff()
+
+ def test_diff_for_revision(self):
+ self._shared_test_diff_for_revision()
+
+ def test_svn_apply_git_patch(self):
+ self._shared_test_svn_apply_git_patch()
+
+class GitTest(SCMTest):
+
+ def _setup_git_clone_of_svn_repository(self):
+ self.git_checkout_path = tempfile.mkdtemp(suffix="git_test_checkout")
+ # --quiet doesn't make git svn silent, so we use run_silent to redirect output
+ run_silent(['git', 'svn', '--quiet', 'clone', self.svn_repo_url, self.git_checkout_path])
+
+ def _tear_down_git_clone_of_svn_repository(self):
+ run_command(['rm', '-rf', self.git_checkout_path])
+
+ def setUp(self):
+ SVNTestRepository.setup(self)
+ self._setup_git_clone_of_svn_repository()
+ os.chdir(self.git_checkout_path)
+ self.scm = detect_scm_system(self.git_checkout_path)
+
+ def tearDown(self):
+ SVNTestRepository.tear_down(self)
+ self._tear_down_git_clone_of_svn_repository()
+
+ def test_detection(self):
+ scm = detect_scm_system(self.git_checkout_path)
+ self.assertEqual(scm.display_name(), "git")
+ self.assertEqual(scm.supports_local_commits(), True)
+
+ def test_rebase_in_progress(self):
+ svn_test_file = os.path.join(self.svn_checkout_path, 'test_file')
+ write_into_file_at_path(svn_test_file, "svn_checkout")
+ run_command(['svn', 'commit', '--message', 'commit to conflict with git commit'], cwd=self.svn_checkout_path)
+
+ git_test_file = os.path.join(self.git_checkout_path, 'test_file')
+ write_into_file_at_path(git_test_file, "git_checkout")
+ run_command(['git', 'commit', '-a', '-m', 'commit to be thrown away by rebase abort'])
+
+ # --quiet doesn't make git svn silent, so use run_silent to redirect output
+ self.assertRaises(ScriptError, run_silent, ['git', 'svn', '--quiet', 'rebase']) # Will fail due to a conflict leaving us mid-rebase.
+
+ scm = detect_scm_system(self.git_checkout_path)
+ self.assertTrue(scm.rebase_in_progress())
+
+ # Make sure our cleanup works.
+ scm.clean_working_directory()
+ self.assertFalse(scm.rebase_in_progress())
+
+ # Make sure cleanup doesn't throw when no rebase is in progress.
+ scm.clean_working_directory()
+
+ def test_commitish_parsing(self):
+ scm = detect_scm_system(self.git_checkout_path)
+
+ # Multiple revisions are cherry-picked.
+ self.assertEqual(len(scm.commit_ids_from_commitish_arguments(['HEAD~2'])), 1)
+ self.assertEqual(len(scm.commit_ids_from_commitish_arguments(['HEAD', 'HEAD~2'])), 2)
+
+ # ... is an invalid range specifier
+ self.assertRaises(ScriptError, scm.commit_ids_from_commitish_arguments, ['trunk...HEAD'])
+
+ def test_commitish_order(self):
+ scm = detect_scm_system(self.git_checkout_path)
+
+ commit_range = 'HEAD~3..HEAD'
+
+ actual_commits = scm.commit_ids_from_commitish_arguments([commit_range])
+ expected_commits = []
+ expected_commits += reversed(run_command(['git', 'rev-list', commit_range]).splitlines())
+
+ self.assertEqual(actual_commits, expected_commits)
+
+ def test_apply_git_patch(self):
+ scm = detect_scm_system(self.git_checkout_path)
+ patch = self._create_patch(run_command(['git', 'diff', 'HEAD..HEAD^']))
+ self._setup_webkittools_scripts_symlink(scm)
+ scm.apply_patch(patch)
+
+ def test_apply_git_patch_force(self):
+ scm = detect_scm_system(self.git_checkout_path)
+ patch = self._create_patch(run_command(['git', 'diff', 'HEAD~2..HEAD']))
+ self._setup_webkittools_scripts_symlink(scm)
+ self.assertRaises(ScriptError, scm.apply_patch, patch, force=True)
+
+ def test_commit_text_parsing(self):
+ self._shared_test_commit_with_message()
+
+ def test_reverse_diff(self):
+ self._shared_test_reverse_diff()
+
+ def test_diff_for_revision(self):
+ self._shared_test_diff_for_revision()
+
+ def test_svn_apply_git_patch(self):
+ self._shared_test_svn_apply_git_patch()
+
+ def test_create_binary_patch(self):
+ # Create a git binary patch and check the contents.
+ scm = detect_scm_system(self.git_checkout_path)
+ test_file_name = 'binary_file'
+ test_file_path = os.path.join(self.git_checkout_path, test_file_name)
+ file_contents = ''.join(map(chr, range(256)))
+ write_into_file_at_path(test_file_path, file_contents)
+ run_command(['git', 'add', test_file_name])
+ patch = scm.create_patch()
+ self.assertTrue(re.search(r'\nliteral 0\n', patch))
+ self.assertTrue(re.search(r'\nliteral 256\n', patch))
+
+ # Check if we can apply the created patch.
+ run_command(['git', 'rm', '-f', test_file_name])
+ self._setup_webkittools_scripts_symlink(scm)
+ self.scm.apply_patch(self._create_patch(patch))
+ self.assertEqual(file_contents, read_from_path(test_file_path))
+
+ # Check if we can create a patch from a local commit.
+ write_into_file_at_path(test_file_path, file_contents)
+ run_command(['git', 'add', test_file_name])
+ run_command(['git', 'commit', '-m', 'binary diff'])
+ patch_from_local_commit = scm.create_patch_from_local_commit('HEAD')
+ self.assertTrue(re.search(r'\nliteral 0\n', patch_from_local_commit))
+ self.assertTrue(re.search(r'\nliteral 256\n', patch_from_local_commit))
+ patch_since_local_commit = scm.create_patch_since_local_commit('HEAD^1')
+ self.assertTrue(re.search(r'\nliteral 0\n', patch_since_local_commit))
+ self.assertTrue(re.search(r'\nliteral 256\n', patch_since_local_commit))
+ self.assertEqual(patch_from_local_commit, patch_since_local_commit)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/WebKitTools/Scripts/webkitpy/statusbot.py b/WebKitTools/Scripts/webkitpy/statusbot.py
new file mode 100644
index 0000000..00177a5
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/statusbot.py
@@ -0,0 +1,83 @@
+# Copyright (C) 2009 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.
+#
+# WebKit's Python module for interacting with the Commit Queue status page.
+
+from webkitpy.webkit_logging import log
+from webkitpy.webkit_mechanize import Browser
+
+# WebKit includes a built copy of BeautifulSoup in Scripts/webkitpy
+# so this import should always succeed.
+from .BeautifulSoup import BeautifulSoup
+
+import urllib2
+
+
+class StatusBot:
+ default_host = "webkit-commit-queue.appspot.com"
+
+ def __init__(self, host=default_host):
+ self.set_host(host)
+ self.browser = Browser()
+
+ def set_host(self, host):
+ self.statusbot_host = host
+ self.statusbot_server_url = "http://%s" % self.statusbot_host
+
+ def results_url_for_status(self, status_id):
+ return "%s/results/%s" % (self.statusbot_server_url, status_id)
+
+ def update_status(self, queue_name, status, patch=None, results_file=None):
+ # During unit testing, statusbot_host is None
+ if not self.statusbot_host:
+ return
+
+ log(status)
+ update_status_url = "%s/update-status" % self.statusbot_server_url
+ self.browser.open(update_status_url)
+ self.browser.select_form(name="update_status")
+ self.browser['queue_name'] = queue_name
+ if patch:
+ if patch.get('bug_id'):
+ self.browser['bug_id'] = str(patch['bug_id'])
+ if patch.get('id'):
+ self.browser['patch_id'] = str(patch['id'])
+ self.browser['status'] = status
+ if results_file:
+ self.browser.add_file(results_file, "text/plain", "results.txt", 'results_file')
+ response = self.browser.submit()
+ return response.read() # This is the id of the newly created status object.
+
+ def patch_status(self, queue_name, patch_id):
+ update_status_url = "%s/patch-status/%s/%s" % (self.statusbot_server_url, queue_name, patch_id)
+ try:
+ return urllib2.urlopen(update_status_url).read()
+ except urllib2.HTTPError, e:
+ if e.code == 404:
+ return None
+ raise e
diff --git a/WebKitTools/Scripts/webkitpy/stepsequence.py b/WebKitTools/Scripts/webkitpy/stepsequence.py
new file mode 100644
index 0000000..b7e544a
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/stepsequence.py
@@ -0,0 +1,76 @@
+# Copyright (C) 2009 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.
+
+from webkitpy.buildsteps import CommandOptions
+from webkitpy.executive import ScriptError
+from webkitpy.webkit_logging import log
+from webkitpy.scm import CheckoutNeedsUpdate
+from webkitpy.queueengine import QueueEngine
+
+
+class StepSequenceErrorHandler():
+ @classmethod
+ def handle_script_error(cls, tool, patch, script_error):
+ raise NotImplementedError, "subclasses must implement"
+
+
+class StepSequence(object):
+ def __init__(self, steps):
+ self._steps = steps or []
+
+ def options(self):
+ collected_options = [
+ CommandOptions.parent_command,
+ CommandOptions.quiet,
+ ]
+ for step in self._steps:
+ collected_options = collected_options + step.options()
+ # Remove duplicates.
+ collected_options = sorted(set(collected_options))
+ return collected_options
+
+ def _run(self, tool, options, state):
+ for step in self._steps:
+ step(tool, options).run(state)
+
+ def run_and_handle_errors(self, tool, options, state=None):
+ if not state:
+ state = {}
+ try:
+ self._run(tool, options, state)
+ except CheckoutNeedsUpdate, e:
+ log("Commit failed because the checkout is out of date. Please update and try again.")
+ log("You can pass --no-build to skip building/testing after update if you believe the new commits did not affect the results.")
+ QueueEngine.exit_after_handled_error(e)
+ except ScriptError, e:
+ if not options.quiet:
+ log(e.message_with_output())
+ if options.parent_command:
+ command = tool.command_by_name(options.parent_command)
+ command.handle_script_error(tool, state, e)
+ QueueEngine.exit_after_handled_error(e)
diff --git a/WebKitTools/Scripts/modules/style.py b/WebKitTools/Scripts/webkitpy/style.py
similarity index 100%
rename from WebKitTools/Scripts/modules/style.py
rename to WebKitTools/Scripts/webkitpy/style.py
diff --git a/WebKitTools/Scripts/modules/style_unittest.py b/WebKitTools/Scripts/webkitpy/style_unittest.py
similarity index 100%
rename from WebKitTools/Scripts/modules/style_unittest.py
rename to WebKitTools/Scripts/webkitpy/style_unittest.py
diff --git a/WebKitTools/Scripts/modules/text_style.py b/WebKitTools/Scripts/webkitpy/text_style.py
similarity index 100%
rename from WebKitTools/Scripts/modules/text_style.py
rename to WebKitTools/Scripts/webkitpy/text_style.py
diff --git a/WebKitTools/Scripts/modules/text_style_unittest.py b/WebKitTools/Scripts/webkitpy/text_style_unittest.py
similarity index 100%
rename from WebKitTools/Scripts/modules/text_style_unittest.py
rename to WebKitTools/Scripts/webkitpy/text_style_unittest.py
diff --git a/WebKitTools/Scripts/modules/user.py b/WebKitTools/Scripts/webkitpy/user.py
similarity index 100%
rename from WebKitTools/Scripts/modules/user.py
rename to WebKitTools/Scripts/webkitpy/user.py
diff --git a/WebKitTools/Scripts/modules/webkit_logging.py b/WebKitTools/Scripts/webkitpy/webkit_logging.py
similarity index 100%
rename from WebKitTools/Scripts/modules/webkit_logging.py
rename to WebKitTools/Scripts/webkitpy/webkit_logging.py
diff --git a/WebKitTools/Scripts/webkitpy/webkit_logging_unittest.py b/WebKitTools/Scripts/webkitpy/webkit_logging_unittest.py
new file mode 100644
index 0000000..b940a4d
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/webkit_logging_unittest.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2009 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 os
+import subprocess
+import StringIO
+import tempfile
+import unittest
+
+from webkitpy.executive import ScriptError
+from webkitpy.webkit_logging import *
+
+class LoggingTest(unittest.TestCase):
+
+ def assert_log_equals(self, log_input, expected_output):
+ original_stderr = sys.stderr
+ test_stderr = StringIO.StringIO()
+ sys.stderr = test_stderr
+
+ try:
+ log(log_input)
+ actual_output = test_stderr.getvalue()
+ finally:
+ original_stderr = original_stderr
+
+ self.assertEquals(actual_output, expected_output, "log(\"%s\") expected: %s actual: %s" % (log_input, expected_output, actual_output))
+
+ def test_log(self):
+ self.assert_log_equals("test", "test\n")
+
+ # Test that log() does not throw an exception when passed an object instead of a string.
+ self.assert_log_equals(ScriptError(message="ScriptError"), "ScriptError\n")
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/WebKitTools/Scripts/modules/webkit_mechanize.py b/WebKitTools/Scripts/webkitpy/webkit_mechanize.py
similarity index 100%
rename from WebKitTools/Scripts/modules/webkit_mechanize.py
rename to WebKitTools/Scripts/webkitpy/webkit_mechanize.py
diff --git a/WebKitTools/Scripts/modules/webkitport.py b/WebKitTools/Scripts/webkitpy/webkitport.py
similarity index 100%
rename from WebKitTools/Scripts/modules/webkitport.py
rename to WebKitTools/Scripts/webkitpy/webkitport.py
diff --git a/WebKitTools/Scripts/webkitpy/webkitport_unittest.py b/WebKitTools/Scripts/webkitpy/webkitport_unittest.py
new file mode 100644
index 0000000..b699038
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/webkitport_unittest.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, 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.webkitport import WebKitPort, MacPort, GtkPort, QtPort, ChromiumPort
+
+class WebKitPortTest(unittest.TestCase):
+ def test_mac_port(self):
+ self.assertEquals(MacPort.name(), "Mac")
+ self.assertEquals(MacPort.flag(), "--port=mac")
+ self.assertEquals(MacPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests")])
+ self.assertEquals(MacPort.build_webkit_command(), [WebKitPort.script_path("build-webkit")])
+ self.assertEquals(MacPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug"])
+ self.assertEquals(MacPort.build_webkit_command(build_style="release"), [WebKitPort.script_path("build-webkit"), "--release"])
+
+ def test_gtk_port(self):
+ self.assertEquals(GtkPort.name(), "Gtk")
+ self.assertEquals(GtkPort.flag(), "--port=gtk")
+ self.assertEquals(GtkPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests"), "--gtk"])
+ self.assertEquals(GtkPort.build_webkit_command(), [WebKitPort.script_path("build-webkit"), "--gtk"])
+ self.assertEquals(GtkPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug", "--gtk"])
+
+ def test_qt_port(self):
+ self.assertEquals(QtPort.name(), "Qt")
+ self.assertEquals(QtPort.flag(), "--port=qt")
+ self.assertEquals(QtPort.run_webkit_tests_command(), [WebKitPort.script_path("run-webkit-tests")])
+ self.assertEquals(QtPort.build_webkit_command(), [WebKitPort.script_path("build-webkit"), "--qt", '--makeargs="-j8"'])
+ self.assertEquals(QtPort.build_webkit_command(build_style="debug"), [WebKitPort.script_path("build-webkit"), "--debug", "--qt", '--makeargs="-j8"'])
+
+ 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.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"])
+
+
+if __name__ == '__main__':
+ unittest.main()
--
WebKit Debian packaging
More information about the Pkg-webkit-commits
mailing list