summaryrefslogtreecommitdiffstats
path: root/WebKitTools/Scripts/webkitpy/tool/commands
diff options
context:
space:
mode:
Diffstat (limited to 'WebKitTools/Scripts/webkitpy/tool/commands')
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/__init__.py5
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py43
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/commandtest.py37
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/download.py379
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/download_unittest.py186
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem.py172
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py88
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/openbugs.py63
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/openbugs_unittest.py50
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/prettydiff.py38
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/queries.py286
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/queries_unittest.py63
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/queues.py427
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/queues_unittest.py255
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/queuestest.py95
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/rebaseline.py113
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py38
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot.py136
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py73
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/stepsequence.py83
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/upload.py453
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/commands/upload_unittest.py110
22 files changed, 3193 insertions, 0 deletions
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/__init__.py b/WebKitTools/Scripts/webkitpy/tool/commands/__init__.py
new file mode 100644
index 0000000..9bdec8f
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/__init__.py
@@ -0,0 +1,5 @@
+# Required for Python to search this directory for module files
+
+from webkitpy.tool.commands.prettydiff import PrettyDiff
+from webkitpy.tool.commands.rebaseline import Rebaseline
+# FIXME: Add the rest of the commands here.
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py b/WebKitTools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py
new file mode 100644
index 0000000..fc5a794
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py
@@ -0,0 +1,43 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.commands.stepsequence import StepSequence
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+class AbstractSequencedCommand(AbstractDeclarativeCommand):
+ steps = None
+ def __init__(self):
+ self._sequence = StepSequence(self.steps)
+ AbstractDeclarativeCommand.__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))
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/commandtest.py b/WebKitTools/Scripts/webkitpy/tool/commands/commandtest.py
new file mode 100644
index 0000000..de92cd3
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/commandtest.py
@@ -0,0 +1,37 @@
+# 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.common.system.outputcapture import OutputCapture
+from webkitpy.tool.mocktool import MockOptions, MockTool
+
+class CommandsTest(unittest.TestCase):
+ def assert_execute_outputs(self, command, args, expected_stdout="", expected_stderr="", options=MockOptions(), tool=MockTool()):
+ 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/tool/commands/download.py b/WebKitTools/Scripts/webkitpy/tool/commands/download.py
new file mode 100644
index 0000000..d27ab0e
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/download.py
@@ -0,0 +1,379 @@
+# 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
+
+import webkitpy.tool.steps as steps
+
+from webkitpy.common.checkout.changelog import ChangeLog, view_source_url
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand
+from webkitpy.tool.commands.stepsequence import StepSequence
+from webkitpy.tool.comments import bug_comment_from_commit_text
+from webkitpy.tool.grammar import pluralize
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+from webkitpy.common.system.deprecated_logging import error, log
+
+
+class Update(AbstractSequencedCommand):
+ name = "update"
+ help_text = "Update working copy (used internally)"
+ steps = [
+ steps.CleanWorkingDirectory,
+ steps.Update,
+ ]
+
+
+class Build(AbstractSequencedCommand):
+ name = "build"
+ help_text = "Update working copy and build"
+ steps = [
+ steps.CleanWorkingDirectory,
+ steps.Update,
+ steps.Build,
+ ]
+
+
+class BuildAndTest(AbstractSequencedCommand):
+ name = "build-and-test"
+ help_text = "Update working copy, build, and run the tests"
+ steps = [
+ steps.CleanWorkingDirectory,
+ steps.Update,
+ steps.Build,
+ steps.RunTests,
+ ]
+
+
+class Land(AbstractSequencedCommand):
+ name = "land"
+ help_text = "Land the current working directory diff and updates the associated bug if any"
+ argument_names = "[BUGID]"
+ show_in_main_help = True
+ steps = [
+ steps.EnsureBuildersAreGreen,
+ steps.UpdateChangeLogsWithReviewer,
+ steps.ValidateReviewer,
+ steps.Build,
+ steps.RunTests,
+ steps.Commit,
+ steps.CloseBugForLandDiff,
+ ]
+ long_help = """land commits the current working copy diff (just as svn or git commit would).
+land will NOT build and run the tests before committing, but you can use the --build option for that.
+If a bug id is provided, or one can be found in the ChangeLog land will update the bug after committing."""
+
+ def _prepare_state(self, options, args, tool):
+ return {
+ "bug_id": (args and args[0]) or tool.checkout().bug_id_for_this_commit(options.git_commit),
+ }
+
+
+class LandCowboy(AbstractSequencedCommand):
+ name = "land-cowboy"
+ help_text = "Prepares a ChangeLog and lands the current working directory diff."
+ steps = [
+ steps.PrepareChangeLog,
+ steps.EditChangeLog,
+ steps.ConfirmDiff,
+ steps.Build,
+ steps.RunTests,
+ steps.Commit,
+ ]
+
+
+class AbstractPatchProcessingCommand(AbstractDeclarativeCommand):
+ # 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:
+ bugs_to_patches[patch.bug_id()] = bugs_to_patches.get(patch.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_bug(bug_id).reviewed_patches()
+ 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 = [
+ steps.CleanWorkingDirectory,
+ steps.Update,
+ steps.ApplyPatch,
+ steps.CheckStyle,
+ ]
+
+
+class BuildAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
+ name = "build-attachment"
+ help_text = "Apply and build patches from bugzilla"
+ argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+ main_steps = [
+ steps.CleanWorkingDirectory,
+ steps.Update,
+ steps.ApplyPatch,
+ steps.Build,
+ ]
+
+
+class PostAttachmentToRietveld(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
+ name = "post-attachment-to-rietveld"
+ help_text = "Uploads a bugzilla attachment to rietveld"
+ arguments_names = "ATTACHMENTID"
+ main_steps = [
+ steps.CleanWorkingDirectory,
+ steps.Update,
+ steps.ApplyPatch,
+ steps.PostCodeReview,
+ ]
+
+
+class AbstractPatchApplyingCommand(AbstractPatchSequencingCommand):
+ prepare_steps = [
+ steps.EnsureLocalCommitIfNeeded,
+ steps.CleanWorkingDirectoryWithLocalCommits,
+ steps.Update,
+ ]
+ main_steps = [
+ steps.ApplyPatchWithLocalCommit,
+ ]
+ long_help = """Updates the working copy.
+Downloads and applies the patches, creating local commits if necessary."""
+
+
+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 ApplyFromBug(AbstractPatchApplyingCommand, ProcessBugsMixin):
+ name = "apply-from-bug"
+ 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 = [
+ steps.EnsureBuildersAreGreen,
+ ]
+ main_steps = [
+ steps.CleanWorkingDirectory,
+ steps.Update,
+ steps.ApplyPatch,
+ steps.ValidateReviewer,
+ steps.Build,
+ steps.RunTests,
+ steps.Commit,
+ steps.ClosePatch,
+ steps.CloseBug,
+ ]
+ long_help = """Checks to make sure builders are green.
+Updates the working copy.
+Applies the patch.
+Builds.
+Runs the layout tests.
+Commits the patch.
+Clears the flags on the patch.
+Closes the bug if no patches are marked for review."""
+
+
+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 LandFromBug(AbstractPatchLandingCommand, ProcessBugsMixin):
+ name = "land-from-bug"
+ help_text = "Land all patches on the given bugs, optionally building and testing them first"
+ argument_names = "BUGID [BUGIDS]"
+ show_in_main_help = True
+
+
+class AbstractRolloutPrepCommand(AbstractSequencedCommand):
+ argument_names = "REVISION REASON"
+
+ def _commit_info(self, revision):
+ commit_info = self.tool.checkout().commit_info_for_revision(revision)
+ if commit_info and commit_info.bug_id():
+ # Note: Don't print a bug URL here because it will confuse the
+ # SheriffBot because the SheriffBot just greps the output
+ # of create-rollout for bug URLs. It should do better
+ # parsing instead.
+ log("Preparing rollout for bug %s." % commit_info.bug_id())
+ else:
+ log("Unable to parse bug number from diff.")
+ return commit_info
+
+ def _prepare_state(self, options, args, tool):
+ revision = args[0]
+ commit_info = self._commit_info(revision)
+ cc_list = sorted([party.bugzilla_email()
+ for party in commit_info.responsible_parties()
+ if party.bugzilla_email()])
+ return {
+ "revision": revision,
+ "bug_id": commit_info.bug_id(),
+ # FIXME: We should used the list as the canonical representation.
+ "bug_cc": ",".join(cc_list),
+ "reason": args[1],
+ }
+
+
+class PrepareRollout(AbstractRolloutPrepCommand):
+ name = "prepare-rollout"
+ help_text = "Revert the given revision in the working copy and prepare ChangeLogs with revert reason"
+ long_help = """Updates the working copy.
+Applies the inverse diff for the provided revision.
+Creates an appropriate rollout ChangeLog, including a trac link and bug link.
+"""
+ steps = [
+ steps.CleanWorkingDirectory,
+ steps.Update,
+ steps.RevertRevision,
+ steps.PrepareChangeLogForRevert,
+ ]
+
+
+class CreateRollout(AbstractRolloutPrepCommand):
+ name = "create-rollout"
+ help_text = "Creates a bug to track a broken SVN revision and uploads a rollout patch."
+ steps = [
+ steps.CleanWorkingDirectory,
+ steps.Update,
+ steps.RevertRevision,
+ steps.CreateBug,
+ steps.PrepareChangeLogForRevert,
+ steps.PostDiffForRevert,
+ ]
+
+ def _prepare_state(self, options, args, tool):
+ state = AbstractRolloutPrepCommand._prepare_state(self, options, args, tool)
+ # Currently, state["bug_id"] points to the bug that caused the
+ # regression. We want to create a new bug that blocks the old bug
+ # so we move state["bug_id"] to state["bug_blocked"] and delete the
+ # old state["bug_id"] so that steps.CreateBug will actually create
+ # the new bug that we want (and subsequently store its bug id into
+ # state["bug_id"])
+ state["bug_blocked"] = state["bug_id"]
+ del state["bug_id"]
+ state["bug_title"] = "REGRESSION(r%s): %s" % (state["revision"], state["reason"])
+ state["bug_description"] = "%s broke the build:\n%s" % (view_source_url(state["revision"]), state["reason"])
+ # FIXME: If we had more context here, we could link to other open bugs
+ # that mention the test that regressed.
+ if options.parent_command == "sheriff-bot":
+ state["bug_description"] += """
+
+This is an automatic bug report generated by the sheriff-bot. If this bug
+report was created because of a flaky test, please file a bug for the flaky
+test (if we don't already have one on file) and dup this bug against that bug
+so that we can track how often these flaky tests case pain.
+
+"Only you can prevent forest fires." -- Smokey the Bear
+"""
+ return state
+
+
+class Rollout(AbstractRolloutPrepCommand):
+ name = "rollout"
+ show_in_main_help = True
+ help_text = "Revert the given revision in the working copy and optionally commit the revert and re-open the original bug"
+ long_help = """Updates the working copy.
+Applies the inverse diff for the provided revision.
+Creates an appropriate rollout ChangeLog, including a trac link and bug link.
+Opens the generated ChangeLogs in $EDITOR.
+Shows the prepared diff for confirmation.
+Commits the revert and updates the bug (including re-opening the bug if necessary)."""
+ steps = [
+ steps.CleanWorkingDirectory,
+ steps.Update,
+ steps.RevertRevision,
+ steps.PrepareChangeLogForRevert,
+ steps.EditChangeLog,
+ steps.ConfirmDiff,
+ steps.Build,
+ steps.Commit,
+ steps.ReopenBugAfterRollout,
+ ]
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/download_unittest.py b/WebKitTools/Scripts/webkitpy/tool/commands/download_unittest.py
new file mode 100644
index 0000000..faddd50
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/download_unittest.py
@@ -0,0 +1,186 @@
+# 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.common.system.outputcapture import OutputCapture
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.download import *
+from webkitpy.tool.mocktool import MockOptions, MockTool
+
+
+class AbstractRolloutPrepCommandTest(unittest.TestCase):
+ def test_commit_info(self):
+ command = AbstractRolloutPrepCommand()
+ tool = MockTool()
+ command.bind_to_tool(tool)
+ output = OutputCapture()
+
+ expected_stderr = "Preparing rollout for bug 42.\n"
+ commit_info = output.assert_outputs(self, command._commit_info, [1234], expected_stderr=expected_stderr)
+ self.assertTrue(commit_info)
+
+ mock_commit_info = Mock()
+ mock_commit_info.bug_id = lambda: None
+ tool._checkout.commit_info_for_revision = lambda revision: mock_commit_info
+ expected_stderr = "Unable to parse bug number from diff.\n"
+ commit_info = output.assert_outputs(self, command._commit_info, [1234], expected_stderr=expected_stderr)
+ self.assertEqual(commit_info, mock_commit_info)
+
+
+class DownloadCommandsTest(CommandsTest):
+ def _default_options(self):
+ options = MockOptions()
+ 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
+ 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\nRunning Python unit tests\nRunning Perl unit tests\nRunning JavaScriptCore tests\nRunning run-webkit-tests\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(ApplyFromBug(), [42], options=options, expected_stderr=expected_stderr)
+
+ def test_land_diff(self):
+ expected_stderr = "Building WebKit\nRunning Python unit tests\nRunning Perl unit tests\nRunning JavaScriptCore tests\nRunning run-webkit-tests\nUpdating bug 42\n"
+ mock_tool = MockTool()
+ mock_tool.scm().create_patch = Mock()
+ mock_tool.checkout().modified_changelogs = Mock(return_value=[])
+ self.assert_execute_outputs(Land(), [42], options=self._default_options(), expected_stderr=expected_stderr, tool=mock_tool)
+ # Make sure we're not calling expensive calls too often.
+ self.assertEqual(mock_tool.scm().create_patch.call_count, 0)
+ self.assertEqual(mock_tool.checkout().modified_changelogs.call_count, 1)
+
+ def test_land_red_builders(self):
+ expected_stderr = '\nWARNING: Builders ["Builder2"] are red, please watch your commit carefully.\nSee http://dummy_buildbot_host/console?category=core\n\nBuilding WebKit\nRunning Python unit tests\nRunning Perl unit tests\nRunning JavaScriptCore tests\nRunning run-webkit-tests\nUpdating bug 42\n'
+ mock_tool = MockTool()
+ mock_tool.buildbot.light_tree_on_fire()
+ self.assert_execute_outputs(Land(), [42], options=self._default_options(), expected_stderr=expected_stderr, tool=mock_tool)
+
+ 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_post_attachment_to_rietveld(self):
+ expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nMOCK: Uploading patch to rietveld\nMOCK setting flag 'in-rietveld' to '+' on attachment '197' with comment 'None' and additional comment 'None'\n"
+ self.assert_execute_outputs(PostAttachmentToRietveld(), [197], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_land_attachment(self):
+ # FIXME: This expected result is imperfect, notice how it's seeing the same patch as still there after it thought it would have cleared the flags.
+ expected_stderr = """Processing 1 patch from 1 bug.
+Updating working directory
+Processing patch 197 from bug 42.
+Building WebKit
+Running Python unit tests
+Running Perl unit tests
+Running JavaScriptCore tests
+Running run-webkit-tests
+Not closing bug 42 as attachment 197 has review=+. Assuming there are more patches to land from this bug.
+"""
+ self.assert_execute_outputs(LandAttachment(), [197], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_land_patches(self):
+ # FIXME: This expected result is imperfect, notice how it's seeing the same patch as still there after it thought it would have cleared the flags.
+ expected_stderr = """2 reviewed patches found on bug 42.
+Processing 2 patches from 1 bug.
+Updating working directory
+Processing patch 197 from bug 42.
+Building WebKit
+Running Python unit tests
+Running Perl unit tests
+Running JavaScriptCore tests
+Running run-webkit-tests
+Not closing bug 42 as attachment 197 has review=+. Assuming there are more patches to land from this bug.
+Updating working directory
+Processing patch 128 from bug 42.
+Building WebKit
+Running Python unit tests
+Running Perl unit tests
+Running JavaScriptCore tests
+Running run-webkit-tests
+Not closing bug 42 as attachment 197 has review=+. Assuming there are more patches to land from this bug.
+"""
+ self.assert_execute_outputs(LandFromBug(), [42], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_prepare_rollout(self):
+ expected_stderr = "Preparing rollout for bug 42.\nUpdating working directory\nRunning prepare-ChangeLog\n"
+ self.assert_execute_outputs(PrepareRollout(), [852, "Reason"], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_create_rollout(self):
+ expected_stderr = """Preparing rollout for bug 42.
+Updating working directory
+MOCK create_bug
+bug_title: REGRESSION(r852): Reason
+bug_description: http://trac.webkit.org/changeset/852 broke the build:
+Reason
+Running prepare-ChangeLog
+MOCK add_patch_to_bug: bug_id=None, description=ROLLOUT of r852, mark_for_review=False, mark_for_commit_queue=True, mark_for_landing=False
+-- Begin comment --
+Any committer can land this patch automatically by marking it commit-queue+. The commit-queue will build and test the patch before landing to ensure that the rollout will be successful. This process takes approximately 15 minutes.
+
+If you would like to land the rollout faster, you can use the following command:
+
+ webkit-patch land-attachment ATTACHMENT_ID --ignore-builders
+
+where ATTACHMENT_ID is the ID of this attachment.
+-- End comment --
+"""
+ self.assert_execute_outputs(CreateRollout(), [852, "Reason"], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_rollout(self):
+ expected_stderr = "Preparing rollout for bug 42.\nUpdating working directory\nRunning prepare-ChangeLog\nMOCK: user.open_url: file://...\nBuilding WebKit\n"
+ self.assert_execute_outputs(Rollout(), [852, "Reason"], options=self._default_options(), expected_stderr=expected_stderr)
+
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem.py b/WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem.py
new file mode 100644
index 0000000..86e2e15
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem.py
@@ -0,0 +1,172 @@
+# 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.tool.commands.queues import AbstractReviewQueue
+from webkitpy.common.config.committers import CommitterList
+from webkitpy.common.config.ports import WebKitPort
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.tool.bot.queueengine import QueueEngine
+
+
+class AbstractEarlyWarningSystem(AbstractReviewQueue):
+ _build_style = "release"
+
+ def __init__(self):
+ AbstractReviewQueue.__init__(self)
+ self.port = WebKitPort.port(self.port_name)
+
+ def should_proceed_with_work_item(self, patch):
+ return True
+
+ def _can_build(self):
+ try:
+ self.run_webkit_patch([
+ "build",
+ self.port.flag(),
+ "--build",
+ "--build-style=%s" % self._build_style,
+ "--force-clean",
+ "--no-update",
+ "--quiet"])
+ return True
+ except ScriptError, e:
+ failure_log = self._log_from_script_error_for_upload(e)
+ self._update_status("Unable to perform a build", results_file=failure_log)
+ return False
+
+ def _build(self, patch, first_run=False):
+ try:
+ args = [
+ "build-attachment",
+ self.port.flag(),
+ "--build",
+ "--build-style=%s" % self._build_style,
+ "--force-clean",
+ "--quiet",
+ "--non-interactive",
+ patch.id()]
+ if not first_run:
+ # See commit-queue for an explanation of what we're doing here.
+ args.append("--no-update")
+ args.append("--parent-command=%s" % self.name)
+ self.run_webkit_patch(args)
+ return True
+ except ScriptError, e:
+ if first_run:
+ return False
+ raise
+
+ def review_patch(self, patch):
+ if not self._build(patch, first_run=True):
+ if not self._can_build():
+ return False
+ self._build(patch)
+ return True
+
+ @classmethod
+ def handle_script_error(cls, tool, state, script_error):
+ is_svn_apply = script_error.command_name() == "svn-apply"
+ status_id = cls._update_status_for_script_error(tool, state, script_error, is_error=is_svn_apply)
+ if is_svn_apply:
+ QueueEngine.exit_after_handled_error(script_error)
+ results_link = tool.status_server.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)
+ exit(1)
+
+
+class GtkEWS(AbstractEarlyWarningSystem):
+ name = "gtk-ews"
+ port_name = "gtk"
+ watchers = AbstractEarlyWarningSystem.watchers + [
+ "gns@gnome.org",
+ "xan.lopez@gmail.com",
+ ]
+
+
+class EflEWS(AbstractEarlyWarningSystem):
+ name = "efl-ews"
+ port_name = "efl"
+ watchers = AbstractEarlyWarningSystem.watchers + [
+ "leandro@profusion.mobi",
+ "antognolli@profusion.mobi",
+ "lucas.demarchi@profusion.mobi",
+ ]
+
+
+class QtEWS(AbstractEarlyWarningSystem):
+ name = "qt-ews"
+ port_name = "qt"
+
+
+class WinEWS(AbstractEarlyWarningSystem):
+ name = "win-ews"
+ port_name = "win"
+ # Use debug, the Apple Win port fails to link Release on 32-bit Windows.
+ # https://bugs.webkit.org/show_bug.cgi?id=39197
+ _build_style = "debug"
+
+
+class AbstractChromiumEWS(AbstractEarlyWarningSystem):
+ port_name = "chromium"
+ watchers = AbstractEarlyWarningSystem.watchers + [
+ "dglazkov@chromium.org",
+ ]
+
+
+class ChromiumLinuxEWS(AbstractChromiumEWS):
+ # FIXME: We should rename this command to cr-linux-ews, but that requires
+ # a database migration. :(
+ name = "chromium-ews"
+
+
+class ChromiumWindowsEWS(AbstractChromiumEWS):
+ name = "cr-win-ews"
+
+
+class ChromiumMacEWS(AbstractChromiumEWS):
+ name = "cr-mac-ews"
+
+
+# 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 False
+ return AbstractEarlyWarningSystem.process_work_item(self, patch)
+
+
+class MacEWS(AbstractCommitterOnlyEWS):
+ name = "mac-ews"
+ port_name = "mac"
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py b/WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py
new file mode 100644
index 0000000..1f04923
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py
@@ -0,0 +1,88 @@
+# 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.thirdparty.mock import Mock
+from webkitpy.tool.commands.earlywarningsystem import *
+from webkitpy.tool.commands.queuestest import QueuesTest
+
+class EarlyWarningSytemTest(QueuesTest):
+ def test_failed_builds(self):
+ ews = ChromiumLinuxEWS()
+ ews._build = lambda patch, first_run=False: False
+ ews._can_build = lambda: True
+ ews.review_patch(Mock())
+
+ def _default_expected_stderr(self, ews):
+ string_replacemnts = {
+ "name": ews.name,
+ "checkout_dir": os.getcwd(), # FIXME: Use of os.getcwd() is wrong, should be scm.checkout_root
+ "port": ews.port_name,
+ "watchers": ews.watchers,
+ }
+ expected_stderr = {
+ "begin_work_queue": "CAUTION: %(name)s will discard all local changes in \"%(checkout_dir)s\"\nRunning WebKit %(name)s.\n" % string_replacemnts,
+ "handle_unexpected_error": "Mock error message\n",
+ "next_work_item": "MOCK: update_work_items: %(name)s [103]\n" % string_replacemnts,
+ "process_work_item": "MOCK: update_status: %(name)s Pass\n" % string_replacemnts,
+ "handle_script_error": "MOCK: update_status: %(name)s ScriptError error message\nMOCK bug comment: bug_id=345, cc=%(watchers)s\n--- Begin comment ---\\Attachment 1234 did not build on %(port)s:\nBuild output: http://dummy_url\n--- End comment ---\n\n" % string_replacemnts,
+ }
+ return expected_stderr
+
+ def _test_ews(self, ews):
+ expected_exceptions = {
+ "handle_script_error": SystemExit,
+ }
+ self.assert_queue_outputs(ews, expected_stderr=self._default_expected_stderr(ews), expected_exceptions=expected_exceptions)
+
+ # FIXME: If all EWSes are going to output the same text, we
+ # could test them all in one method with a for loop over an array.
+ def test_chromium_linux_ews(self):
+ self._test_ews(ChromiumLinuxEWS())
+
+ def test_chromium_windows_ews(self):
+ self._test_ews(ChromiumWindowsEWS())
+
+ def test_qt_ews(self):
+ self._test_ews(QtEWS())
+
+ def test_gtk_ews(self):
+ self._test_ews(GtkEWS())
+
+ def test_efl_ews(self):
+ self._test_ews(EflEWS())
+
+ def test_mac_ews(self):
+ ews = MacEWS()
+ expected_stderr = self._default_expected_stderr(ews)
+ expected_stderr["process_work_item"] = "MOCK: update_status: mac-ews Error: mac-ews cannot process patches from non-committers :(\n"
+ expected_exceptions = {
+ "handle_script_error": SystemExit,
+ }
+ self.assert_queue_outputs(ews, expected_stderr=expected_stderr, expected_exceptions=expected_exceptions)
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/openbugs.py b/WebKitTools/Scripts/webkitpy/tool/commands/openbugs.py
new file mode 100644
index 0000000..5da5bbb
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/openbugs.py
@@ -0,0 +1,63 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import re
+import sys
+
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+from webkitpy.common.system.deprecated_logging import log
+
+
+class OpenBugs(AbstractDeclarativeCommand):
+ name = "open-bugs"
+ help_text = "Finds all bug numbers passed in arguments (or stdin if no args provided) and opens them in a web browser"
+
+ bug_number_regexp = re.compile(r"\b\d{4,6}\b")
+
+ def _open_bugs(self, bug_ids):
+ for bug_id in bug_ids:
+ bug_url = self.tool.bugs.bug_url_for_bug_id(bug_id)
+ self.tool.user.open_url(bug_url)
+
+ # _find_bugs_in_string mostly exists for easy unit testing.
+ def _find_bugs_in_string(self, string):
+ return self.bug_number_regexp.findall(string)
+
+ def _find_bugs_in_iterable(self, iterable):
+ return sum([self._find_bugs_in_string(string) for string in iterable], [])
+
+ def execute(self, options, args, tool):
+ if args:
+ bug_ids = self._find_bugs_in_iterable(args)
+ else:
+ # This won't open bugs until stdin is closed but could be made to easily. That would just make unit testing slightly harder.
+ bug_ids = self._find_bugs_in_iterable(sys.stdin)
+
+ log("%s bugs found in input." % len(bug_ids))
+
+ self._open_bugs(bug_ids)
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/openbugs_unittest.py b/WebKitTools/Scripts/webkitpy/tool/commands/openbugs_unittest.py
new file mode 100644
index 0000000..40a6e1b
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/openbugs_unittest.py
@@ -0,0 +1,50 @@
+# 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.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.openbugs import OpenBugs
+
+class OpenBugsTest(CommandsTest):
+
+ find_bugs_in_string_expectations = [
+ ["123", []],
+ ["1234", ["1234"]],
+ ["12345", ["12345"]],
+ ["123456", ["123456"]],
+ ["1234567", []],
+ [" 123456 234567", ["123456", "234567"]],
+ ]
+
+ def test_find_bugs_in_string(self):
+ openbugs = OpenBugs()
+ for expectation in self.find_bugs_in_string_expectations:
+ self.assertEquals(openbugs._find_bugs_in_string(expectation[0]), expectation[1])
+
+ def test_args_parsing(self):
+ expected_stderr = "2 bugs found in input.\nMOCK: user.open_url: http://example.com/12345\nMOCK: user.open_url: http://example.com/23456\n"
+ self.assert_execute_outputs(OpenBugs(), ["12345\n23456"], expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/prettydiff.py b/WebKitTools/Scripts/webkitpy/tool/commands/prettydiff.py
new file mode 100644
index 0000000..e3fc00c
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/prettydiff.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand
+import webkitpy.tool.steps as steps
+
+
+class PrettyDiff(AbstractSequencedCommand):
+ name = "pretty-diff"
+ help_text = "Shows the pretty diff in the default browser"
+ steps = [
+ steps.ConfirmDiff,
+ ]
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/queries.py b/WebKitTools/Scripts/webkitpy/tool/commands/queries.py
new file mode 100644
index 0000000..91ce5e9
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/queries.py
@@ -0,0 +1,286 @@
+# 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.common.checkout.commitinfo import CommitInfo
+from webkitpy.common.config.committers import CommitterList
+from webkitpy.common.net.buildbot import BuildBot
+from webkitpy.common.system.user import User
+from webkitpy.tool.grammar import pluralize
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+from webkitpy.common.system.deprecated_logging import log
+
+
+class BugsToCommit(AbstractDeclarativeCommand):
+ name = "bugs-to-commit"
+ help_text = "List bugs in the commit-queue"
+
+ def execute(self, options, args, tool):
+ # FIXME: This command is poorly named. It's fetching the commit-queue list here. The name implies it's fetching pending-commit (all r+'d patches).
+ bug_ids = tool.bugs.queries.fetch_bug_ids_from_commit_queue()
+ for bug_id in bug_ids:
+ print "%s" % bug_id
+
+
+class PatchesInCommitQueue(AbstractDeclarativeCommand):
+ name = "patches-in-commit-queue"
+ help_text = "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 patch.url()
+
+
+class PatchesToCommitQueue(AbstractDeclarativeCommand):
+ name = "patches-to-commit-queue"
+ help_text = "Patches which should be added to the commit queue"
+ def __init__(self):
+ options = [
+ make_option("--bugs", action="store_true", dest="bugs", help="Output bug links instead of patch links"),
+ ]
+ AbstractDeclarativeCommand.__init__(self, options=options)
+
+ @staticmethod
+ def _needs_commit_queue(patch):
+ if patch.commit_queue() == "+": # If it's already cq+, ignore the patch.
+ log("%s already has cq=%s" % (patch.id(), patch.commit_queue()))
+ 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(AbstractDeclarativeCommand):
+ name = "patches-to-review"
+ help_text = "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 LastGreenRevision(AbstractDeclarativeCommand):
+ name = "last-green-revision"
+ help_text = "Prints the last known good revision"
+
+ def execute(self, options, args, tool):
+ print self.tool.buildbot.last_green_revision()
+
+
+class WhatBroke(AbstractDeclarativeCommand):
+ name = "what-broke"
+ help_text = "Print failing buildbots (%s) and what revisions broke them" % BuildBot.default_host
+
+ def _print_builder_line(self, builder_name, max_name_width, status_message):
+ print "%s : %s" % (builder_name.ljust(max_name_width), status_message)
+
+ # FIXME: This is slightly different from Builder.suspect_revisions_for_green_to_red_transition
+ # due to needing to detect the "hit the limit" case an print a special message.
+ def _print_blame_information_for_builder(self, builder_status, name_width, avoid_flakey_tests=True):
+ builder = self.tool.buildbot.builder_with_name(builder_status["name"])
+ red_build = builder.build(builder_status["build_number"])
+ (last_green_build, first_red_build) = builder.find_failure_transition(red_build)
+ if not first_red_build:
+ self._print_builder_line(builder.name(), name_width, "FAIL (error loading build information)")
+ return
+ if not last_green_build:
+ self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: sometime before %s?)" % first_red_build.revision())
+ return
+
+ suspect_revisions = range(first_red_build.revision(), last_green_build.revision(), -1)
+ suspect_revisions.reverse()
+ first_failure_message = ""
+ if (first_red_build == builder.build(builder_status["build_number"])):
+ first_failure_message = " FIRST FAILURE, possibly a flaky test"
+ self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: %s%s)" % (suspect_revisions, first_failure_message))
+ for revision in suspect_revisions:
+ commit_info = self.tool.checkout().commit_info_for_revision(revision)
+ if commit_info:
+ print commit_info.blame_string(self.tool.bugs)
+ else:
+ print "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision
+
+ def execute(self, options, args, tool):
+ builder_statuses = tool.buildbot.builder_statuses()
+ longest_builder_name = max(map(len, map(lambda builder: builder["name"], builder_statuses)))
+ failing_builders = 0
+ for builder_status in builder_statuses:
+ # If the builder is green, print OK, exit.
+ if builder_status["is_green"]:
+ continue
+ self._print_blame_information_for_builder(builder_status, name_width=longest_builder_name)
+ failing_builders += 1
+ if failing_builders:
+ print "%s of %s are failing" % (failing_builders, pluralize("builder", len(builder_statuses)))
+ else:
+ print "All builders are passing!"
+
+
+class WhoBrokeIt(AbstractDeclarativeCommand):
+ name = "who-broke-it"
+ help_text = "Print a list of revisions causing failures on %s" % BuildBot.default_host
+
+ def execute(self, options, args, tool):
+ for revision, builders in self.tool.buildbot.revisions_causing_failures(False).items():
+ print "r%s appears to have broken %s" % (revision, [builder.name() for builder in builders])
+
+
+class ResultsFor(AbstractDeclarativeCommand):
+ name = "results-for"
+ help_text = "Print a list of failures for the passed revision from bots on %s" % BuildBot.default_host
+ argument_names = "REVISION"
+
+ def _print_layout_test_results(self, results):
+ if not results:
+ print " No results."
+ return
+ for title, files in results.parsed_results().items():
+ print " %s" % title
+ for filename in files:
+ print " %s" % filename
+
+ def execute(self, options, args, tool):
+ builders = self.tool.buildbot.builders()
+ for builder in builders:
+ print "%s:" % builder.name()
+ build = builder.build_for_revision(args[0], allow_failed_lookups=True)
+ self._print_layout_test_results(build.layout_test_results())
+
+
+class FailureReason(AbstractDeclarativeCommand):
+ name = "failure-reason"
+ help_text = "Lists revisions where individual test failures started at %s" % BuildBot.default_host
+
+ def _print_blame_information_for_transition(self, green_build, red_build, failing_tests):
+ suspect_revisions = green_build.builder().suspect_revisions_for_transition(green_build, red_build)
+ print "SUCCESS: Build %s (r%s) was the first to show failures: %s" % (red_build._number, red_build.revision(), failing_tests)
+ print "Suspect revisions:"
+ for revision in suspect_revisions:
+ commit_info = self.tool.checkout().commit_info_for_revision(revision)
+ if commit_info:
+ print commit_info.blame_string(self.tool.bugs)
+ else:
+ print "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision
+
+ def _explain_failures_for_builder(self, builder, start_revision):
+ print "Examining failures for \"%s\", starting at r%s" % (builder.name(), start_revision)
+ revision_to_test = start_revision
+ build = builder.build_for_revision(revision_to_test, allow_failed_lookups=True)
+ layout_test_results = build.layout_test_results()
+ if not layout_test_results:
+ # FIXME: This could be made more user friendly.
+ print "Failed to load layout test results; can't continue. (start revision = r%s)" % start_revision
+ return 1
+
+ results_to_explain = set(layout_test_results.failing_tests())
+ last_build_with_results = build
+ print "Starting at %s" % revision_to_test
+ while results_to_explain:
+ revision_to_test -= 1
+ new_build = builder.build_for_revision(revision_to_test, allow_failed_lookups=True)
+ if not new_build:
+ print "No build for %s" % revision_to_test
+ continue
+ build = new_build
+ latest_results = build.layout_test_results()
+ if not latest_results:
+ print "No results build %s (r%s)" % (build._number, build.revision())
+ continue
+ failures = set(latest_results.failing_tests())
+ if len(failures) >= 20:
+ # FIXME: We may need to move this logic into the LayoutTestResults class.
+ # The buildbot stops runs after 20 failures so we don't have full results to work with here.
+ print "Too many failures in build %s (r%s), ignoring." % (build._number, build.revision())
+ continue
+ fixed_results = results_to_explain - failures
+ if not fixed_results:
+ print "No change in build %s (r%s), %s unexplained failures (%s in this build)" % (build._number, build.revision(), len(results_to_explain), len(failures))
+ last_build_with_results = build
+ continue
+ self._print_blame_information_for_transition(build, last_build_with_results, fixed_results)
+ last_build_with_results = build
+ results_to_explain -= fixed_results
+ if results_to_explain:
+ print "Failed to explain failures: %s" % results_to_explain
+ return 1
+ print "Explained all results for %s" % builder.name()
+ return 0
+
+ def _builder_to_explain(self):
+ builder_statuses = self.tool.buildbot.builder_statuses()
+ red_statuses = [status for status in builder_statuses if not status["is_green"]]
+ print "%s failing" % (pluralize("builder", len(red_statuses)))
+ builder_choices = [status["name"] for status in red_statuses]
+ # We could offer an "All" choice here.
+ chosen_name = User.prompt_with_list("Which builder to diagnose:", builder_choices)
+ # FIXME: prompt_with_list should really take a set of objects and a set of names and then return the object.
+ for status in red_statuses:
+ if status["name"] == chosen_name:
+ return (self.tool.buildbot.builder_with_name(chosen_name), status["built_revision"])
+
+ def execute(self, options, args, tool):
+ (builder, latest_revision) = self._builder_to_explain()
+ start_revision = self.tool.user.prompt("Revision to walk backwards from? [%s] " % latest_revision) or latest_revision
+ if not start_revision:
+ print "Revision required."
+ return 1
+ return self._explain_failures_for_builder(builder, start_revision=int(start_revision))
+
+
+class TreeStatus(AbstractDeclarativeCommand):
+ name = "tree-status"
+ help_text = "Print the status of the %s buildbots" % BuildBot.default_host
+ long_help = """Fetches build status from http://build.webkit.org/one_box_per_builder
+and displayes the status of each builder."""
+
+ 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/tool/commands/queries_unittest.py b/WebKitTools/Scripts/webkitpy/tool/commands/queries_unittest.py
new file mode 100644
index 0000000..98ed545
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/queries_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.
+
+from webkitpy.common.net.bugzilla import Bugzilla
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.queries import *
+from webkitpy.tool.mocktool import MockTool
+
+class QueryCommandsTest(CommandsTest):
+ def test_bugs_to_commit(self):
+ expected_stderr = "Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)\n"
+ self.assert_execute_outputs(BugsToCommit(), None, "42\n77\n", expected_stderr)
+
+ def test_patches_in_commit_queue(self):
+ expected_stdout = "http://example.com/197\nhttp://example.com/103\n"
+ expected_stderr = "Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)\nPatches in commit queue:\n"
+ self.assert_execute_outputs(PatchesInCommitQueue(), None, expected_stdout, expected_stderr)
+
+ def test_patches_to_commit_queue(self):
+ expected_stdout = "http://example.com/104&action=edit\n"
+ expected_stderr = "197 already has cq=+\n128 already has cq=+\n105 committer = \"Eric Seidel\" <eric@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/77\n"
+ options.bugs = True
+ self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options)
+
+ def test_patches_to_review(self):
+ expected_stdout = "103\n"
+ expected_stderr = "Patches pending review:\n"
+ self.assert_execute_outputs(PatchesToReview(), None, expected_stdout, expected_stderr)
+
+ 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/tool/commands/queues.py b/WebKitTools/Scripts/webkitpy/tool/commands/queues.py
new file mode 100644
index 0000000..4d2a9df
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/queues.py
@@ -0,0 +1,427 @@
+# 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 traceback
+import os
+
+from datetime import datetime
+from optparse import make_option
+from StringIO import StringIO
+
+from webkitpy.common.net.bugzilla import CommitterValidator
+from webkitpy.common.net.statusserver import StatusServer
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.system.deprecated_logging import error, log
+from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler
+from webkitpy.tool.bot.patchcollection import PersistentPatchCollection, PersistentPatchCollectionDelegate
+from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate
+from webkitpy.tool.grammar import pluralize
+from webkitpy.tool.multicommandtool import Command, TryAgain
+
+class AbstractQueue(Command, QueueEngineDelegate):
+ watchers = [
+ ]
+
+ _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!"),
+ make_option("--exit-after-iteration", action="store", type="int", dest="iterations", default=None, help="Stop running the queue after iterating this number of times."),
+ ]
+ Command.__init__(self, "Run the %s" % self.name, options=options_list)
+ self._iteration_count = 0
+
+ def _cc_watchers(self, bug_id):
+ try:
+ self.tool.bugs.add_cc_to_bug(bug_id, self.watchers)
+ except Exception, e:
+ traceback.print_exc()
+ log("Failed to CC watchers.")
+
+ def run_webkit_patch(self, args):
+ webkit_patch_args = [self.tool.path()]
+ # FIXME: This is a hack, we should have a more general way to pass global options.
+ # FIXME: We must always pass global options and their value in one argument
+ # because our global option code looks for the first argument which does
+ # not begin with "-" and assumes that is the command name.
+ webkit_patch_args += ["--status-host=%s" % self.tool.status_server.host]
+ webkit_patch_args.extend(args)
+ return self.tool.executive.run_and_throw_if_fail(webkit_patch_args)
+
+ def _log_directory(self):
+ return "%s-logs" % self.name
+
+ # QueueEngineDelegate methods
+
+ def queue_log_path(self):
+ return os.path.join(self._log_directory(), "%s.log" % self.name)
+
+ def work_item_log_path(self, work_item):
+ raise NotImplementedError, "subclasses must implement"
+
+ 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 = self.tool.user.prompt("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):
+ self._iteration_count += 1
+ return not self.options.iterations or self._iteration_count <= self.options.iterations
+
+ 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"
+
+ # Command methods
+
+ def execute(self, options, args, tool, engine=QueueEngine):
+ self.options = options
+ self.tool = tool
+ return engine(self.name, self, self.tool.wakeup_event).run()
+
+ @classmethod
+ def _log_from_script_error_for_upload(cls, script_error, output_limit=None):
+ # We have seen request timeouts with app engine due to large
+ # log uploads. Trying only the last 512k.
+ if not output_limit:
+ output_limit = 512 * 1024 # 512k
+ output = script_error.message_with_output(output_limit=output_limit)
+ # We pre-encode the string to a byte array before passing it
+ # to status_server, because ClientForm (part of mechanize)
+ # wants a file-like object with pre-encoded data.
+ return StringIO(output.encode("utf-8"))
+
+ @classmethod
+ def _update_status_for_script_error(cls, tool, state, script_error, is_error=False):
+ message = str(script_error)
+ if is_error:
+ message = "Error: %s" % message
+ failure_log = cls._log_from_script_error_for_upload(script_error)
+ return tool.status_server.update_status(cls.name, message, state["patch"], failure_log)
+
+
+class AbstractPatchQueue(AbstractQueue):
+ def _update_status(self, message, patch=None, results_file=None):
+ self.tool.status_server.update_status(self.name, message, patch, results_file)
+
+ def _update_work_items(self, patch_ids):
+ self.tool.status_server.update_work_items(self.name, patch_ids)
+
+ 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 work_item_log_path(self, patch):
+ return os.path.join(self._log_directory(), "%s.log" % patch.bug_id())
+
+ def log_progress(self, patch_ids):
+ log("%s in %s [%s]" % (pluralize("patch", len(patch_ids)), self.name, ", ".join(map(str, patch_ids))))
+
+
+class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler):
+ name = "commit-queue"
+ def __init__(self):
+ AbstractPatchQueue.__init__(self)
+
+ # AbstractPatchQueue methods
+
+ def begin_work_queue(self):
+ AbstractPatchQueue.begin_work_queue(self)
+ self.committer_validator = CommitterValidator(self.tool.bugs)
+
+ def _validate_patches_in_commit_queue(self):
+ # Not using BugzillaQueries.fetch_patches_from_commit_queue() so we can reject patches with invalid committers/reviewers.
+ bug_ids = self.tool.bugs.queries.fetch_bug_ids_from_commit_queue()
+ all_patches = sum([self.tool.bugs.fetch_bug(bug_id).commit_queued_patches(include_invalid=True) for bug_id in bug_ids], [])
+ return self.committer_validator.patches_after_rejecting_invalid_commiters_and_reviewers(all_patches)
+
+ def _patch_cmp(self, a, b):
+ # Sort first by is_rollout, then by attach_date.
+ # Reversing the order so that is_rollout is first.
+ rollout_cmp = cmp(b.is_rollout(), a.is_rollout())
+ if (rollout_cmp != 0):
+ return rollout_cmp
+ return cmp(a.attach_date(), b.attach_date())
+
+ def next_work_item(self):
+ patches = self._validate_patches_in_commit_queue()
+ patches = sorted(patches, self._patch_cmp)
+ self._update_work_items([patch.id() for patch in patches])
+ 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 _can_build_and_test(self):
+ try:
+ self.run_webkit_patch([
+ "build-and-test",
+ "--force-clean",
+ "--build",
+ "--test",
+ "--non-interactive",
+ "--no-update",
+ "--build-style=both",
+ "--quiet"])
+ except ScriptError, e:
+ failure_log = self._log_from_script_error_for_upload(e)
+ self._update_status("Unable to successfully build and test", results_file=failure_log)
+ return False
+ return True
+
+ def should_proceed_with_work_item(self, patch):
+ patch_text = "rollout patch" if patch.is_rollout() else "patch"
+ self._update_status("Landing %s" % patch_text, patch)
+ return True
+
+ def _land(self, patch, first_run=False):
+ try:
+ args = [
+ "land-attachment",
+ "--force-clean",
+ "--build",
+ "--non-interactive",
+ "--ignore-builders",
+ "--build-style=both",
+ "--quiet",
+ patch.id()
+ ]
+ # We don't bother to run tests for rollouts as that makes them too slow.
+ if not patch.is_rollout():
+ args.append("--test")
+ if not first_run:
+ # The first time through, we don't reject the patch from the
+ # commit queue because we want to make sure we can build and
+ # test ourselves. However, the second time through, we
+ # register ourselves as the parent-command so we can reject
+ # the patch on failure.
+ args.append("--parent-command=commit-queue")
+ # The second time through, we also don't want to update so we
+ # know we're testing the same revision that we successfully
+ # built and tested.
+ args.append("--no-update")
+ self.run_webkit_patch(args)
+ self._did_pass(patch)
+ return True
+ except ScriptError, e:
+ if first_run:
+ return False
+ self._did_fail(patch)
+ raise
+
+ def process_work_item(self, patch):
+ self._cc_watchers(patch.bug_id())
+ if not self._land(patch, first_run=True):
+ # The patch failed to land, but the bots were green. It's possible
+ # that the bots were behind. To check that case, we try to build and
+ # test ourselves.
+ if not self._can_build_and_test():
+ return False
+ # Hum, looks like the patch is actually bad. Of course, we could
+ # have been bitten by a flaky test the first time around. We try
+ # to land again. If it fails a second time, we're pretty sure its
+ # a bad test and re can reject it outright.
+ self._land(patch)
+ return True
+
+ def handle_unexpected_error(self, patch, message):
+ self.committer_validator.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_server.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)
+ validator = CommitterValidator(tool.bugs)
+ validator.reject_patch_from_commit_queue(state["patch"].id(), cls._error_message_for_bug(tool, status_id, script_error))
+
+ @classmethod
+ def handle_checkout_needs_update(cls, tool, state, options, error):
+ # The only time when we find out that out checkout needs update is
+ # when we were ready to actually pull the trigger and land the patch.
+ # Rather than spinning in the master process, we retry without
+ # building or testing, which is much faster.
+ options.build = False
+ options.test = False
+ options.update = True
+ raise TryAgain()
+
+
+class RietveldUploadQueue(AbstractPatchQueue, StepSequenceErrorHandler):
+ name = "rietveld-upload-queue"
+
+ def __init__(self):
+ AbstractPatchQueue.__init__(self)
+
+ # AbstractPatchQueue methods
+
+ def next_work_item(self):
+ patch_id = self.tool.bugs.queries.fetch_first_patch_from_rietveld_queue()
+ if patch_id:
+ return patch_id
+ self._update_status("Empty queue")
+
+ def should_proceed_with_work_item(self, patch):
+ self._update_status("Uploading patch", patch)
+ return True
+
+ def process_work_item(self, patch):
+ try:
+ self.run_webkit_patch(["post-attachment-to-rietveld", "--force-clean", "--non-interactive", "--parent-command=rietveld-upload-queue", patch.id()])
+ self._did_pass(patch)
+ return True
+ except ScriptError, e:
+ if e.exit_code != QueueEngine.handled_error_code:
+ self._did_fail(patch)
+ raise e
+
+ @classmethod
+ def _reject_patch(cls, tool, patch_id):
+ tool.bugs.set_flag_on_attachment(patch_id, "in-rietveld", "-")
+
+ def handle_unexpected_error(self, patch, message):
+ log(message)
+ self._reject_patch(self.tool, patch.id())
+
+ # StepSequenceErrorHandler methods
+
+ @classmethod
+ def handle_script_error(cls, tool, state, script_error):
+ log(script_error.message_with_output())
+ cls._update_status_for_script_error(tool, state, script_error)
+ cls._reject_patch(tool, state["patch"].id())
+
+
+class AbstractReviewQueue(AbstractPatchQueue, PersistentPatchCollectionDelegate, StepSequenceErrorHandler):
+ def __init__(self, options=None):
+ AbstractPatchQueue.__init__(self, options)
+
+ def review_patch(self, patch):
+ raise NotImplementedError, "subclasses must implement"
+
+ # 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_server
+
+ def is_terminal_status(self, status):
+ return status == "Pass" or status == "Fail" or status.startswith("Error:")
+
+ # AbstractPatchQueue methods
+
+ def begin_work_queue(self):
+ AbstractPatchQueue.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):
+ try:
+ if not self.review_patch(patch):
+ return False
+ self._did_pass(patch)
+ return True
+ except ScriptError, e:
+ if e.exit_code != QueueEngine.handled_error_code:
+ self._did_fail(patch)
+ raise e
+
+ 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 review_patch(self, patch):
+ self.run_webkit_patch(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch.id()])
+ return True
+
+ @classmethod
+ def handle_script_error(cls, tool, state, script_error):
+ is_svn_apply = script_error.command_name() == "svn-apply"
+ status_id = cls._update_status_for_script_error(tool, state, script_error, is_error=is_svn_apply)
+ if is_svn_apply:
+ QueueEngine.exit_after_handled_error(script_error)
+ message = "Attachment %s did not pass %s:\n\n%s\n\nIf any of these errors are false positives, please file a bug against check-webkit-style." % (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)
+ exit(1)
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/queues_unittest.py b/WebKitTools/Scripts/webkitpy/tool/commands/queues_unittest.py
new file mode 100644
index 0000000..f86e9a2
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/queues_unittest.py
@@ -0,0 +1,255 @@
+# 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.common.net.bugzilla import Attachment
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.queues import *
+from webkitpy.tool.commands.queuestest import QueuesTest
+from webkitpy.tool.mocktool import MockTool, MockSCM
+
+
+class TestQueue(AbstractPatchQueue):
+ name = "test-queue"
+
+
+class TestReviewQueue(AbstractReviewQueue):
+ name = "test-review-queue"
+
+
+class MockPatch(object):
+ def is_rollout(self):
+ return True
+
+ def bug_id(self):
+ return 12345
+
+ def id(self):
+ return 76543
+
+
+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 test_log_directory(self):
+ self.assertEquals(TestQueue()._log_directory(), "test-queue-logs")
+
+ def _assert_run_webkit_patch(self, run_args):
+ queue = TestQueue()
+ tool = MockTool()
+ tool.executive = Mock()
+ queue.bind_to_tool(tool)
+
+ queue.run_webkit_patch(run_args)
+ expected_run_args = ["echo", "--status-host=example.com"] + run_args
+ tool.executive.run_and_throw_if_fail.assert_called_with(expected_run_args)
+
+ def test_run_webkit_patch(self):
+ self._assert_run_webkit_patch([1])
+ self._assert_run_webkit_patch(["one", 2])
+
+ def test_iteration_count(self):
+ queue = TestQueue()
+ queue.options = Mock()
+ queue.options.iterations = 3
+ self.assertTrue(queue.should_continue_work_queue())
+ self.assertTrue(queue.should_continue_work_queue())
+ self.assertTrue(queue.should_continue_work_queue())
+ self.assertFalse(queue.should_continue_work_queue())
+
+ def test_no_iteration_count(self):
+ queue = TestQueue()
+ queue.options = Mock()
+ self.assertTrue(queue.should_continue_work_queue())
+ self.assertTrue(queue.should_continue_work_queue())
+ self.assertTrue(queue.should_continue_work_queue())
+ self.assertTrue(queue.should_continue_work_queue())
+
+ def _assert_log_message(self, script_error, log_message):
+ failure_log = AbstractQueue._log_from_script_error_for_upload(script_error, output_limit=10)
+ self.assertTrue(failure_log.read(), log_message)
+
+ def test_log_from_script_error_for_upload(self):
+ self._assert_log_message(ScriptError("test"), "test")
+ # In python 2.5 unicode(Exception) is busted. See:
+ # http://bugs.python.org/issue2517
+ # With no good workaround, we just ignore these tests.
+ if not hasattr(Exception, "__unicode__"):
+ return
+
+ unicode_tor = u"WebKit \u2661 Tor Arne Vestb\u00F8!"
+ utf8_tor = unicode_tor.encode("utf-8")
+ self._assert_log_message(ScriptError(unicode_tor), utf8_tor)
+ script_error = ScriptError(unicode_tor, output=unicode_tor)
+ expected_output = "%s\nLast %s characters of output:\n%s" % (utf8_tor, 10, utf8_tor[-10:])
+ self._assert_log_message(script_error, expected_output)
+
+
+class AbstractReviewQueueTest(CommandsTest):
+ def test_patch_collection_delegate_methods(self):
+ queue = TestReviewQueue()
+ tool = MockTool()
+ queue.bind_to_tool(tool)
+ self.assertEquals(queue.collection_name(), "test-review-queue")
+ self.assertEquals(queue.fetch_potential_patch_ids(), [103])
+ queue.status_server()
+ self.assertTrue(queue.is_terminal_status("Pass"))
+ self.assertTrue(queue.is_terminal_status("Fail"))
+ self.assertTrue(queue.is_terminal_status("Error: Your patch exploded"))
+ self.assertFalse(queue.is_terminal_status("Foo"))
+
+
+class CommitQueueTest(QueuesTest):
+ def test_commit_queue(self):
+ expected_stderr = {
+ "begin_work_queue" : "CAUTION: commit-queue will discard all local changes in \"%s\"\nRunning WebKit commit-queue.\n" % MockSCM.fake_checkout_root,
+ "should_proceed_with_work_item": "MOCK: update_status: commit-queue Landing patch\n",
+ # FIXME: The commit-queue warns about bad committers twice. This is due to the fact that we access Attachment.reviewer() twice and it logs each time.
+ "next_work_item" : """Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)
+Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)
+MOCK setting flag 'commit-queue' to '-' on attachment '128' with comment 'Rejecting patch 128 from commit-queue.' and additional comment 'non-committer@example.com does not have committer permissions according to http://trac.webkit.org/browser/trunk/WebKitTools/Scripts/webkitpy/common/config/committers.py.\n\n- If you do not have committer rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags.\n\n- If you have committer rights please correct the error in WebKitTools/Scripts/webkitpy/common/config/committers.py by adding yourself to the file (no review needed). The commit-queue restarts itself every 2 hours. After restart the commit-queue will correctly respect your committer rights.'
+MOCK: update_work_items: commit-queue [106, 197]
+2 patches in commit-queue [106, 197]
+""",
+ "process_work_item" : "MOCK: update_status: commit-queue Pass\n",
+ "handle_unexpected_error" : "MOCK setting flag 'commit-queue' to '-' on attachment '1234' with comment 'Rejecting patch 1234 from commit-queue.' and additional comment 'Mock error message'\n",
+ "handle_script_error": "MOCK: update_status: commit-queue ScriptError error message\nMOCK setting flag 'commit-queue' to '-' on attachment '1234' with comment 'Rejecting patch 1234 from commit-queue.' and additional comment 'ScriptError error message'\n",
+ }
+ self.assert_queue_outputs(CommitQueue(), expected_stderr=expected_stderr)
+
+ def test_rollout(self):
+ tool = MockTool(log_executive=True)
+ tool.buildbot.light_tree_on_fire()
+ expected_stderr = {
+ "begin_work_queue" : "CAUTION: commit-queue will discard all local changes in \"%s\"\nRunning WebKit commit-queue.\n" % MockSCM.fake_checkout_root,
+ "should_proceed_with_work_item": "MOCK: update_status: commit-queue Landing patch\n",
+ # FIXME: The commit-queue warns about bad committers twice. This is due to the fact that we access Attachment.reviewer() twice and it logs each time.
+ "next_work_item" : """Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)
+Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)
+MOCK setting flag 'commit-queue' to '-' on attachment '128' with comment 'Rejecting patch 128 from commit-queue.' and additional comment 'non-committer@example.com does not have committer permissions according to http://trac.webkit.org/browser/trunk/WebKitTools/Scripts/webkitpy/common/config/committers.py.
+
+- If you do not have committer rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags.
+
+- If you have committer rights please correct the error in WebKitTools/Scripts/webkitpy/common/config/committers.py by adding yourself to the file (no review needed). The commit-queue restarts itself every 2 hours. After restart the commit-queue will correctly respect your committer rights.'
+MOCK: update_work_items: commit-queue [106, 197]
+2 patches in commit-queue [106, 197]
+""",
+ "process_work_item" : "MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--build', '--non-interactive', '--ignore-builders', '--build-style=both', '--quiet', 1234, '--test']\nMOCK: update_status: commit-queue Pass\n",
+ "handle_unexpected_error" : "MOCK setting flag 'commit-queue' to '-' on attachment '1234' with comment 'Rejecting patch 1234 from commit-queue.' and additional comment 'Mock error message'\n",
+ "handle_script_error": "MOCK: update_status: commit-queue ScriptError error message\nMOCK setting flag 'commit-queue' to '-' on attachment '1234' with comment 'Rejecting patch 1234 from commit-queue.' and additional comment 'ScriptError error message'\n",
+ }
+ self.assert_queue_outputs(CommitQueue(), tool=tool, expected_stderr=expected_stderr)
+
+ def test_rollout_lands(self):
+ tool = MockTool(log_executive=True)
+ tool.buildbot.light_tree_on_fire()
+ rollout_patch = MockPatch()
+ expected_stderr = {
+ "begin_work_queue": "CAUTION: commit-queue will discard all local changes in \"%s\"\nRunning WebKit commit-queue.\n" % MockSCM.fake_checkout_root,
+ "should_proceed_with_work_item": "MOCK: update_status: commit-queue Landing rollout patch\n",
+ # FIXME: The commit-queue warns about bad committers twice. This is due to the fact that we access Attachment.reviewer() twice and it logs each time.
+ "next_work_item": """Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)
+Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)
+MOCK setting flag 'commit-queue' to '-' on attachment '128' with comment 'Rejecting patch 128 from commit-queue.' and additional comment 'non-committer@example.com does not have committer permissions according to http://trac.webkit.org/browser/trunk/WebKitTools/Scripts/webkitpy/common/config/committers.py.
+
+- If you do not have committer rights please read http://webkit.org/coding/contributing.html for instructions on how to use bugzilla flags.
+
+- If you have committer rights please correct the error in WebKitTools/Scripts/webkitpy/common/config/committers.py by adding yourself to the file (no review needed). The commit-queue restarts itself every 2 hours. After restart the commit-queue will correctly respect your committer rights.'
+MOCK: update_work_items: commit-queue [106, 197]
+2 patches in commit-queue [106, 197]
+""",
+ "process_work_item": "MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--build', '--non-interactive', '--ignore-builders', '--build-style=both', '--quiet', 76543]\nMOCK: update_status: commit-queue Pass\n",
+ "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '76543' with comment 'Rejecting patch 76543 from commit-queue.' and additional comment 'Mock error message'\n",
+ "handle_script_error": "MOCK: update_status: commit-queue ScriptError error message\nMOCK setting flag 'commit-queue' to '-' on attachment '1234' with comment 'Rejecting patch 1234 from commit-queue.' and additional comment 'ScriptError error message'\n",
+ }
+ self.assert_queue_outputs(CommitQueue(), tool=tool, work_item=rollout_patch, expected_stderr=expected_stderr)
+
+ def test_can_build_and_test(self):
+ queue = CommitQueue()
+ tool = MockTool()
+ tool.executive = Mock()
+ queue.bind_to_tool(tool)
+ self.assertTrue(queue._can_build_and_test())
+ expected_run_args = ["echo", "--status-host=example.com", "build-and-test", "--force-clean", "--build", "--test", "--non-interactive", "--no-update", "--build-style=both", "--quiet"]
+ tool.executive.run_and_throw_if_fail.assert_called_with(expected_run_args)
+
+ def _mock_attachment(self, is_rollout, attach_date):
+ attachment = Mock()
+ attachment.is_rollout = lambda: is_rollout
+ attachment.attach_date = lambda: attach_date
+ return attachment
+
+ def test_patch_cmp(self):
+ long_ago_date = datetime(1900, 1, 21)
+ recent_date = datetime(2010, 1, 21)
+ attachment1 = self._mock_attachment(is_rollout=False, attach_date=recent_date)
+ attachment2 = self._mock_attachment(is_rollout=False, attach_date=long_ago_date)
+ attachment3 = self._mock_attachment(is_rollout=True, attach_date=recent_date)
+ attachment4 = self._mock_attachment(is_rollout=True, attach_date=long_ago_date)
+ attachments = [attachment1, attachment2, attachment3, attachment4]
+ expected_sort = [attachment4, attachment3, attachment2, attachment1]
+ queue = CommitQueue()
+ attachments.sort(queue._patch_cmp)
+ self.assertEqual(attachments, expected_sort)
+
+
+class RietveldUploadQueueTest(QueuesTest):
+ def test_rietveld_upload_queue(self):
+ expected_stderr = {
+ "begin_work_queue": "CAUTION: rietveld-upload-queue will discard all local changes in \"%s\"\nRunning WebKit rietveld-upload-queue.\n" % MockSCM.fake_checkout_root,
+ "should_proceed_with_work_item": "MOCK: update_status: rietveld-upload-queue Uploading patch\n",
+ "process_work_item": "MOCK: update_status: rietveld-upload-queue Pass\n",
+ "handle_unexpected_error": "Mock error message\nMOCK setting flag 'in-rietveld' to '-' on attachment '1234' with comment 'None' and additional comment 'None'\n",
+ "handle_script_error": "ScriptError error message\nMOCK: update_status: rietveld-upload-queue ScriptError error message\nMOCK setting flag 'in-rietveld' to '-' on attachment '1234' with comment 'None' and additional comment 'None'\n",
+ }
+ self.assert_queue_outputs(RietveldUploadQueue(), 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" % MockSCM.fake_checkout_root,
+ "next_work_item": "MOCK: update_work_items: style-queue [103]\n",
+ "should_proceed_with_work_item": "MOCK: update_status: style-queue Checking style\n",
+ "process_work_item" : "MOCK: update_status: style-queue Pass\n",
+ "handle_unexpected_error" : "Mock error message\n",
+ "handle_script_error": "MOCK: update_status: style-queue ScriptError error message\nMOCK bug comment: bug_id=345, cc=[]\n--- Begin comment ---\\Attachment 1234 did not pass style-queue:\n\nScriptError error message\n\nIf any of these errors are false positives, please file a bug against check-webkit-style.\n--- End comment ---\n\n",
+ }
+ expected_exceptions = {
+ "handle_script_error": SystemExit,
+ }
+ self.assert_queue_outputs(StyleQueue(), expected_stderr=expected_stderr, expected_exceptions=expected_exceptions)
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/queuestest.py b/WebKitTools/Scripts/webkitpy/tool/commands/queuestest.py
new file mode 100644
index 0000000..9e17c5c
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/queuestest.py
@@ -0,0 +1,95 @@
+# 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.common.net.bugzilla import Attachment
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.mocktool import MockTool
+
+
+class MockQueueEngine(object):
+ def __init__(self, name, queue, wakeup_event):
+ pass
+
+ def run(self):
+ pass
+
+
+class MockPatch():
+ def id(self):
+ return 1234
+
+ def bug_id(self):
+ return 345
+
+
+class QueuesTest(unittest.TestCase):
+ mock_work_item = Attachment({
+ "id": 1234,
+ "bug_id": 345,
+ "name": "Patch",
+ "attacher_email": "adam@example.com",
+ }, None)
+
+ def assert_outputs(self, func, func_name, args, expected_stdout, expected_stderr, expected_exceptions):
+ exception = None
+ if expected_exceptions and func_name in expected_exceptions:
+ exception = expected_exceptions[func_name]
+
+ OutputCapture().assert_outputs(self,
+ func,
+ args=args,
+ expected_stdout=expected_stdout.get(func_name, ""),
+ expected_stderr=expected_stderr.get(func_name, ""),
+ expected_exception=exception)
+
+ def assert_queue_outputs(self, queue, args=None, work_item=None, expected_stdout=None, expected_stderr=None, expected_exceptions=None, options=Mock(), tool=MockTool()):
+ 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
+ tool.user.prompt = lambda message: "yes"
+
+ queue.execute(options, args, tool, engine=MockQueueEngine)
+
+ self.assert_outputs(queue.queue_log_path, "queue_log_path", [], expected_stdout, expected_stderr, expected_exceptions)
+ self.assert_outputs(queue.work_item_log_path, "work_item_log_path", [work_item], expected_stdout, expected_stderr, expected_exceptions)
+ self.assert_outputs(queue.begin_work_queue, "begin_work_queue", [], expected_stdout, expected_stderr, expected_exceptions)
+ self.assert_outputs(queue.should_continue_work_queue, "should_continue_work_queue", [], expected_stdout, expected_stderr, expected_exceptions)
+ self.assert_outputs(queue.next_work_item, "next_work_item", [], expected_stdout, expected_stderr, expected_exceptions)
+ self.assert_outputs(queue.should_proceed_with_work_item, "should_proceed_with_work_item", [work_item], expected_stdout, expected_stderr, expected_exceptions)
+ self.assert_outputs(queue.process_work_item, "process_work_item", [work_item], expected_stdout, expected_stderr, expected_exceptions)
+ self.assert_outputs(queue.handle_unexpected_error, "handle_unexpected_error", [work_item, "Mock error message"], expected_stdout, expected_stderr, expected_exceptions)
+ self.assert_outputs(queue.handle_script_error, "handle_script_error", [tool, {"patch": MockPatch()}, ScriptError(message="ScriptError error message", script_args="MockErrorCommand")], expected_stdout, expected_stderr, expected_exceptions)
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/rebaseline.py b/WebKitTools/Scripts/webkitpy/tool/commands/rebaseline.py
new file mode 100644
index 0000000..78e06c6
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/rebaseline.py
@@ -0,0 +1,113 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os.path
+import re
+import shutil
+import urllib
+
+from webkitpy.common.net.buildbot import BuildBot, LayoutTestResults
+from webkitpy.common.system.user import User
+from webkitpy.layout_tests.port import factory
+from webkitpy.tool.grammar import pluralize
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+# FIXME: I'm not sure where this logic should go in the end.
+# For now it's here, until we have a second need for it.
+class BuilderToPort(object):
+ _builder_name_to_port_name = {
+ r"SnowLeopard": "mac-snowleopard",
+ r"Leopard": "mac-leopard",
+ r"Tiger": "mac-tiger",
+ r"Windows": "win",
+ r"GTK": "gtk",
+ r"Qt": "qt",
+ r"Chromium Mac": "chromium-mac",
+ r"Chromium Linux": "chromium-linux",
+ r"Chromium Win": "chromium-win",
+ }
+
+ def _port_name_for_builder_name(self, builder_name):
+ for regexp, port_name in self._builder_name_to_port_name.items():
+ if re.match(regexp, builder_name):
+ return port_name
+
+ def port_for_builder(self, builder_name):
+ port_name = self._port_name_for_builder_name(builder_name)
+ assert(port_name) # Need to update _builder_name_to_port_name
+ port = factory.get(port_name)
+ assert(port) # Need to update _builder_name_to_port_name
+ return port
+
+
+class Rebaseline(AbstractDeclarativeCommand):
+ name = "rebaseline"
+ help_text = "Replaces local expected.txt files with new results from build bots"
+
+ # FIXME: This should share more code with FailureReason._builder_to_explain
+ def _builder_to_pull_from(self):
+ builder_statuses = self.tool.buildbot.builder_statuses()
+ red_statuses = [status for status in builder_statuses if not status["is_green"]]
+ print "%s failing" % (pluralize("builder", len(red_statuses)))
+ builder_choices = [status["name"] for status in red_statuses]
+ chosen_name = self.tool.user.prompt_with_list("Which builder to pull results from:", builder_choices)
+ # FIXME: prompt_with_list should really take a set of objects and a set of names and then return the object.
+ for status in red_statuses:
+ if status["name"] == chosen_name:
+ return (self.tool.buildbot.builder_with_name(chosen_name), status["build_number"])
+
+ def _replace_expectation_with_remote_result(self, local_file, remote_file):
+ (downloaded_file, headers) = urllib.urlretrieve(remote_file)
+ shutil.move(downloaded_file, local_file)
+
+ def _tests_to_update(self, build):
+ parsed_results = build.layout_test_results().parsed_results()
+ # FIXME: This probably belongs as API on LayoutTestResults
+ # but .failing_tests() already means something else.
+ return parsed_results[LayoutTestResults.fail_key]
+
+ def _results_url_for_test(self, build, test):
+ test_base = os.path.splitext(test)[0]
+ actual_path = test_base + "-actual.txt"
+ return build.results_url() + "/" + actual_path
+
+ def execute(self, options, args, tool):
+ builder, build_number = self._builder_to_pull_from()
+ build = builder.build(build_number)
+ port = BuilderToPort().port_for_builder(builder.name())
+
+ for test in self._tests_to_update(build):
+ results_url = self._results_url_for_test(build, test)
+ # Port operates with absolute paths.
+ absolute_path = os.path.join(port.layout_tests_dir(), test)
+ expected_file = port.expected_filename(absolute_path, ".txt")
+ print test
+ self._replace_expectation_with_remote_result(expected_file, results_url)
+
+ # FIXME: We should handle new results too.
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py b/WebKitTools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py
new file mode 100644
index 0000000..d6582a7
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py
@@ -0,0 +1,38 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.tool.commands.rebaseline import BuilderToPort
+
+
+class BuilderToPortTest(unittest.TestCase):
+ def test_port_for_builder(self):
+ converter = BuilderToPort()
+ port = converter.port_for_builder("Leopard Intel Debug (Tests)")
+ self.assertEqual(port.name(), "mac-leopard")
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot.py b/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot.py
new file mode 100644
index 0000000..24c8517
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot.py
@@ -0,0 +1,136 @@
+# 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.common.system.deprecated_logging import log
+from webkitpy.common.config.ports import WebKitPort
+from webkitpy.tool.bot.sheriff import Sheriff
+from webkitpy.tool.bot.sheriffircbot import SheriffIRCBot
+from webkitpy.tool.commands.queues import AbstractQueue
+from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler
+
+
+class SheriffBot(AbstractQueue, StepSequenceErrorHandler):
+ name = "sheriff-bot"
+ watchers = AbstractQueue.watchers + [
+ "abarth@webkit.org",
+ "eric@webkit.org",
+ ]
+
+ def _update(self):
+ self.run_webkit_patch(["update", "--force-clean", "--quiet"])
+
+ # AbstractQueue methods
+
+ def begin_work_queue(self):
+ AbstractQueue.begin_work_queue(self)
+ self._sheriff = Sheriff(self.tool, self)
+ self._irc_bot = SheriffIRCBot(self.tool, self._sheriff)
+ self.tool.ensure_irc_connected(self._irc_bot.irc_delegate())
+
+ def work_item_log_path(self, new_failures):
+ return os.path.join("%s-logs" % self.name, "%s.log" % new_failures.keys()[0])
+
+ def _new_failures(self, revisions_causing_failures, old_failing_svn_revisions):
+ # We ignore failures that might have been caused by svn_revisions that
+ # we've already complained about. This is conservative in the sense
+ # that we might be ignoring some new failures, but our experience has
+ # been that skipping this check causes a lot of spam for builders that
+ # take a long time to cycle.
+ old_failing_builder_names = []
+ for svn_revision in old_failing_svn_revisions:
+ old_failing_builder_names.extend(
+ [builder.name() for builder in revisions_causing_failures[svn_revision]])
+
+ new_failures = {}
+ for svn_revision, builders in revisions_causing_failures.items():
+ if svn_revision in old_failing_svn_revisions:
+ # FIXME: We should re-process the work item after some time delay.
+ # https://bugs.webkit.org/show_bug.cgi?id=36581
+ continue
+ new_builders = [builder for builder in builders
+ if builder.name() not in old_failing_builder_names]
+ if new_builders:
+ new_failures[svn_revision] = new_builders
+
+ return new_failures
+
+ def next_work_item(self):
+ self._irc_bot.process_pending_messages()
+ self._update()
+
+ # We do one read from buildbot to ensure a consistent view.
+ revisions_causing_failures = self.tool.buildbot.revisions_causing_failures()
+
+ # Similarly, we read once from our the status_server.
+ old_failing_svn_revisions = []
+ for svn_revision in revisions_causing_failures.keys():
+ if self.tool.status_server.svn_revision(svn_revision):
+ old_failing_svn_revisions.append(svn_revision)
+
+ new_failures = self._new_failures(revisions_causing_failures,
+ old_failing_svn_revisions)
+
+ self._sheriff.provoke_flaky_builders(revisions_causing_failures)
+ return new_failures
+
+ def should_proceed_with_work_item(self, new_failures):
+ # Currently, we don't have any reasons not to proceed with work items.
+ return True
+
+ def process_work_item(self, new_failures):
+ blame_list = new_failures.keys()
+ for svn_revision, builders in new_failures.items():
+ try:
+ commit_info = self.tool.checkout().commit_info_for_revision(svn_revision)
+ if not commit_info:
+ print "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision
+ continue
+ self._sheriff.post_irc_warning(commit_info, builders)
+ self._sheriff.post_blame_comment_on_bug(commit_info,
+ builders,
+ blame_list)
+ self._sheriff.post_automatic_rollout_patch(commit_info,
+ builders)
+ finally:
+ for builder in builders:
+ self.tool.status_server.update_svn_revision(svn_revision,
+ builder.name())
+ return True
+
+ def handle_unexpected_error(self, new_failures, message):
+ log(message)
+
+ # StepSequenceErrorHandler methods
+
+ @classmethod
+ def handle_script_error(cls, tool, state, script_error):
+ # Ideally we would post some information to IRC about what went wrong
+ # here, but we don't have the IRC password in the child process.
+ pass
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py b/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py
new file mode 100644
index 0000000..4b4b8b6
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py
@@ -0,0 +1,73 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+
+from webkitpy.tool.commands.queuestest import QueuesTest
+from webkitpy.tool.commands.sheriffbot import SheriffBot
+from webkitpy.tool.mocktool import MockBuilder
+
+
+class SheriffBotTest(QueuesTest):
+ builder1 = MockBuilder("Builder1")
+ builder2 = MockBuilder("Builder2")
+
+ def test_sheriff_bot(self):
+ mock_work_item = {
+ 29837: [self.builder1],
+ }
+ expected_stderr = {
+ "begin_work_queue": "CAUTION: sheriff-bot will discard all local changes in \"%s\"\nRunning WebKit sheriff-bot.\n" % os.getcwd(),
+ "next_work_item": "",
+ "process_work_item": "MOCK: irc.post: abarth, darin, eseidel: http://trac.webkit.org/changeset/29837 might have broken Builder1\nMOCK bug comment: bug_id=42, cc=['abarth@webkit.org', 'eric@webkit.org']\n--- Begin comment ---\\http://trac.webkit.org/changeset/29837 might have broken Builder1\n--- End comment ---\n\n",
+ "handle_unexpected_error": "Mock error message\n"
+ }
+ self.assert_queue_outputs(SheriffBot(), work_item=mock_work_item, expected_stderr=expected_stderr)
+
+ revisions_causing_failures = {
+ 1234: [builder1],
+ 1235: [builder1, builder2],
+ }
+
+ def test_new_failures(self):
+ old_failing_svn_revisions = []
+ self.assertEquals(SheriffBot()._new_failures(self.revisions_causing_failures,
+ old_failing_svn_revisions),
+ self.revisions_causing_failures)
+
+ def test_new_failures_with_old_revisions(self):
+ old_failing_svn_revisions = [1234]
+ self.assertEquals(SheriffBot()._new_failures(self.revisions_causing_failures,
+ old_failing_svn_revisions),
+ {1235: [builder2]})
+
+ def test_new_failures_with_old_revisions(self):
+ old_failing_svn_revisions = [1235]
+ self.assertEquals(SheriffBot()._new_failures(self.revisions_causing_failures,
+ old_failing_svn_revisions),
+ {})
diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/stepsequence.py b/WebKitTools/Scripts/webkitpy/tool/commands/stepsequence.py
new file mode 100644
index 0000000..be2ed4c
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/stepsequence.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.
+
+import webkitpy.tool.steps as steps
+
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.checkout.scm import CheckoutNeedsUpdate
+from webkitpy.tool.bot.queueengine import QueueEngine
+from webkitpy.common.system.deprecated_logging import log
+
+
+class StepSequenceErrorHandler():
+ @classmethod
+ def handle_script_error(cls, tool, patch, script_error):
+ raise NotImplementedError, "subclasses must implement"
+
+ @classmethod
+ def handle_checkout_needs_update(cls, tool, state, options, error):
+ raise NotImplementedError, "subclasses must implement"
+
+
+class StepSequence(object):
+ def __init__(self, steps):
+ self._steps = steps or []
+
+ def options(self):
+ collected_options = [
+ steps.Options.parent_command,
+ steps.Options.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.")
+ if options.parent_command:
+ command = tool.command_by_name(options.parent_command)
+ command.handle_checkout_needs_update(tool, state, options, e)
+ 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/webkitpy/tool/commands/upload.py b/WebKitTools/Scripts/webkitpy/tool/commands/upload.py
new file mode 100644
index 0000000..4a15ed6
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/upload.py
@@ -0,0 +1,453 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, 2010 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 sys
+
+from optparse import make_option
+
+import webkitpy.tool.steps as steps
+
+from webkitpy.common.config.committers import CommitterList
+from webkitpy.common.net.bugzilla import parse_bug_id
+from webkitpy.common.system.user import User
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand
+from webkitpy.tool.grammar import pluralize, join_with_separators
+from webkitpy.tool.comments import bug_comment_from_svn_revision
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+from webkitpy.common.system.deprecated_logging import error, log
+
+
+class CommitMessageForCurrentDiff(AbstractDeclarativeCommand):
+ name = "commit-message"
+ help_text = "Print a commit message suitable for the uncommitted changes"
+
+ def __init__(self):
+ options = [
+ steps.Options.git_commit,
+ ]
+ AbstractDeclarativeCommand.__init__(self, options=options)
+
+ def execute(self, options, args, tool):
+ # This command is a useful test to make sure commit_message_for_this_commit
+ # always returns the right value regardless of the current working directory.
+ print "%s" % tool.checkout().commit_message_for_this_commit(options.git_commit).message()
+
+
+class CleanPendingCommit(AbstractDeclarativeCommand):
+ name = "clean-pending-commit"
+ help_text = "Clear r+ on obsolete patches so they do not appear in the pending-commit list."
+
+ # NOTE: This was designed to be generic, but right now we're only processing patches from the pending-commit list, so only r+ matters.
+ def _flags_to_clear_on_patch(self, patch):
+ if not patch.is_obsolete():
+ return None
+ what_was_cleared = []
+ if patch.review() == "+":
+ if patch.reviewer():
+ what_was_cleared.append("%s's review+" % patch.reviewer().full_name)
+ else:
+ what_was_cleared.append("review+")
+ return join_with_separators(what_was_cleared)
+
+ def execute(self, options, args, tool):
+ committers = CommitterList()
+ for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list():
+ bug = self.tool.bugs.fetch_bug(bug_id)
+ patches = bug.patches(include_obsolete=True)
+ for patch in patches:
+ flags_to_clear = self._flags_to_clear_on_patch(patch)
+ if not flags_to_clear:
+ continue
+ message = "Cleared %s from obsolete attachment %s so that this bug does not appear in http://webkit.org/pending-commit." % (flags_to_clear, patch.id())
+ self.tool.bugs.obsolete_attachment(patch.id(), message)
+
+
+class AssignToCommitter(AbstractDeclarativeCommand):
+ name = "assign-to-committer"
+ help_text = "Assign bug to whoever attached the most recent r+'d patch"
+
+ def _patches_have_commiters(self, reviewed_patches):
+ for patch in reviewed_patches:
+ if not patch.committer():
+ return False
+ return True
+
+ def _assign_bug_to_last_patch_attacher(self, bug_id):
+ committers = CommitterList()
+ bug = self.tool.bugs.fetch_bug(bug_id)
+ if not bug.is_unassigned():
+ assigned_to_email = bug.assigned_to_email()
+ log("Bug %s is already assigned to %s (%s)." % (bug_id, assigned_to_email, committers.committer_by_email(assigned_to_email)))
+ return
+
+ reviewed_patches = bug.reviewed_patches()
+ if not reviewed_patches:
+ log("Bug %s has no non-obsolete patches, ignoring." % bug_id)
+ return
+
+ # We only need to do anything with this bug if one of the r+'d patches does not have a valid committer (cq+ set).
+ if self._patches_have_commiters(reviewed_patches):
+ log("All reviewed patches on bug %s already have commit-queue+, 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(AbstractSequencedCommand):
+ name = "obsolete-attachments"
+ help_text = "Mark all attachments on a bug as obsolete"
+ argument_names = "BUGID"
+ steps = [
+ steps.ObsoletePatches,
+ ]
+
+ def _prepare_state(self, options, args, tool):
+ return { "bug_id" : args[0] }
+
+
+class AbstractPatchUploadingCommand(AbstractSequencedCommand):
+ def _bug_id(self, options, args, tool, state):
+ # Perfer a bug id passed as an argument over a bug url in the diff (i.e. ChangeLogs).
+ bug_id = args and args[0]
+ if not bug_id:
+ bug_id = tool.checkout().bug_id_for_this_commit(options.git_commit)
+ return bug_id
+
+ def _prepare_state(self, options, args, tool):
+ state = {}
+ state["bug_id"] = self._bug_id(options, args, tool, state)
+ if not state["bug_id"]:
+ error("No bug id passed and no bug url found in ChangeLogs.")
+ return state
+
+
+class Post(AbstractPatchUploadingCommand):
+ name = "post"
+ help_text = "Attach the current working directory diff to a bug as a patch file"
+ argument_names = "[BUGID]"
+ steps = [
+ steps.CheckStyle,
+ steps.ConfirmDiff,
+ steps.ObsoletePatches,
+ steps.PostDiff,
+ ]
+
+
+class LandSafely(AbstractPatchUploadingCommand):
+ name = "land-safely"
+ help_text = "Land the current diff via the commit-queue"
+ argument_names = "[BUGID]"
+ long_help = """land-safely updates the ChangeLog with the reviewer listed
+ in bugs.webkit.org for BUGID (or the bug ID detected from the ChangeLog).
+ The command then uploads the current diff to the bug and marks it for
+ commit by the commit-queue."""
+ show_in_main_help = True
+ steps = [
+ steps.UpdateChangeLogsWithReviewer,
+ steps.ObsoletePatches,
+ steps.PostDiffForCommit,
+ ]
+
+
+class Prepare(AbstractSequencedCommand):
+ name = "prepare"
+ help_text = "Creates a bug (or prompts for an existing bug) and prepares the ChangeLogs"
+ argument_names = "[BUGID]"
+ steps = [
+ steps.PromptForBugOrTitle,
+ steps.CreateBug,
+ steps.PrepareChangeLog,
+ ]
+
+ def _prepare_state(self, options, args, tool):
+ bug_id = args and args[0]
+ return { "bug_id" : bug_id }
+
+
+class Upload(AbstractPatchUploadingCommand):
+ name = "upload"
+ help_text = "Automates the process of uploading a patch for review"
+ argument_names = "[BUGID]"
+ show_in_main_help = True
+ steps = [
+ steps.CheckStyle,
+ steps.PromptForBugOrTitle,
+ steps.CreateBug,
+ steps.PrepareChangeLog,
+ steps.EditChangeLog,
+ steps.ConfirmDiff,
+ steps.ObsoletePatches,
+ steps.PostDiff,
+ ]
+ long_help = """upload uploads the current diff to bugs.webkit.org.
+ If no bug id is provided, upload will create a bug.
+ If the current diff does not have a ChangeLog, upload
+ will prepare a ChangeLog. Once a patch is read, upload
+ will open the ChangeLogs for editing using the command in the
+ EDITOR environment variable and will display the diff using the
+ command in the PAGER environment variable."""
+
+ def _prepare_state(self, options, args, tool):
+ state = {}
+ state["bug_id"] = self._bug_id(options, args, tool, state)
+ return state
+
+
+class EditChangeLogs(AbstractSequencedCommand):
+ name = "edit-changelogs"
+ help_text = "Opens modified ChangeLogs in $EDITOR"
+ show_in_main_help = True
+ steps = [
+ steps.EditChangeLog,
+ ]
+
+
+class PostCommits(AbstractDeclarativeCommand):
+ name = "post-commits"
+ help_text = "Attach a range of local commits to bugs as patch files"
+ argument_names = "COMMITISH"
+
+ 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)"),
+ steps.Options.obsolete_patches,
+ steps.Options.review,
+ steps.Options.request_commit,
+ ]
+ AbstractDeclarativeCommand.__init__(self, 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 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("webkit-patch 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(git_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 }
+ steps.ObsoletePatches(tool, options).run(state)
+ have_obsoleted_patches.add(bug_id)
+
+ diff = tool.scm().create_patch(git_commit=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, description, comment_text, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
+
+
+# FIXME: This command needs to be brought into the modern age with steps and CommitInfo.
+class MarkBugFixed(AbstractDeclarativeCommand):
+ name = "mark-bug-fixed"
+ help_text = "Mark the specified bug as fixed"
+ argument_names = "[SVN_REVISION]"
+ 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."),
+ ]
+ AbstractDeclarativeCommand.__init__(self, options=options)
+
+ # FIXME: We should be using checkout().changelog_entries_for_revision(...) instead here.
+ 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 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.bug_url_for_bug_id(bug_id), tool.bugs.fetch_bug_dictionary(bug_id)["title"]))
+ log("Revision: %s" % svn_revision)
+
+ if options.open_bug:
+ tool.user.open_url(tool.bugs.bug_url_for_bug_id(bug_id))
+
+ if needs_prompt:
+ if not tool.user.confirm("Is this correct?"):
+ exit(1)
+
+ 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(AbstractDeclarativeCommand):
+ name = "create-bug"
+ help_text = "Create a bug from local changes or local commits"
+ argument_names = "[COMMITISH]"
+
+ def __init__(self):
+ options = [
+ steps.Options.cc,
+ steps.Options.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."),
+ ]
+ AbstractDeclarativeCommand.__init__(self, 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(git_commit=commit_id)
+ bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff, "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.checkout().commit_message_for_this_commit(options.git_commit)
+ bug_title = commit_message.description(lstrip=True, strip_url=True)
+ comment_text = commit_message.body(lstrip=True)
+
+ diff = tool.scm().create_patch(options.git_commit)
+ bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff, "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 = User.prompt("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/tool/commands/upload_unittest.py b/WebKitTools/Scripts/webkitpy/tool/commands/upload_unittest.py
new file mode 100644
index 0000000..5f3f400
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/commands/upload_unittest.py
@@ -0,0 +1,110 @@
+# 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.thirdparty.mock import Mock
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.upload import *
+from webkitpy.tool.mocktool import MockOptions, MockTool
+
+class UploadCommandsTest(CommandsTest):
+ def test_commit_message_for_current_diff(self):
+ tool = MockTool()
+ expected_stdout = "This is a fake commit message that is at least 50 characters.\n"
+ self.assert_execute_outputs(CommitMessageForCurrentDiff(), [], expected_stdout=expected_stdout, tool=tool)
+
+ def test_clean_pending_commit(self):
+ self.assert_execute_outputs(CleanPendingCommit(), [])
+
+ def test_assign_to_committer(self):
+ tool = MockTool()
+ expected_stderr = "Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)\nBug 77 is already assigned to foo@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@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(self):
+ options = MockOptions()
+ options.description = "MOCK description"
+ options.request_commit = False
+ options.review = True
+ options.comment = None
+ options.cc = None
+ expected_stderr = """Running check-webkit-style
+MOCK: user.open_url: file://...
+Obsoleting 2 old patches on bug 42
+MOCK add_patch_to_bug: bug_id=42, description=MOCK description, mark_for_review=True, mark_for_commit_queue=False, mark_for_landing=False
+-- Begin comment --
+None
+-- End comment --
+MOCK: user.open_url: http://example.com/42
+"""
+ self.assert_execute_outputs(Post(), [42], options=options, expected_stderr=expected_stderr)
+
+ def test_land_safely(self):
+ expected_stderr = "Obsoleting 2 old patches on bug 42\nMOCK add_patch_to_bug: bug_id=42, description=Patch for landing, mark_for_review=False, mark_for_commit_queue=False, mark_for_landing=True\n-- Begin comment --\nNone\n-- End comment --\n"
+ self.assert_execute_outputs(LandSafely(), [42], expected_stderr=expected_stderr)
+
+ def test_prepare_diff_with_arg(self):
+ self.assert_execute_outputs(Prepare(), [42])
+
+ def test_prepare(self):
+ expected_stderr = "MOCK create_bug\nbug_title: Mock user response\nbug_description: Mock user response\n"
+ self.assert_execute_outputs(Prepare(), [], expected_stderr=expected_stderr)
+
+ def test_upload(self):
+ options = MockOptions()
+ options.description = "MOCK description"
+ options.request_commit = False
+ options.review = True
+ options.comment = None
+ options.cc = None
+ expected_stderr = """Running check-webkit-style
+MOCK: user.open_url: file://...
+Obsoleting 2 old patches on bug 42
+MOCK add_patch_to_bug: bug_id=42, description=MOCK description, mark_for_review=True, mark_for_commit_queue=False, mark_for_landing=False
+-- Begin comment --
+None
+-- End comment --
+MOCK: user.open_url: http://example.com/42
+"""
+ self.assert_execute_outputs(Upload(), [42], options=options, expected_stderr=expected_stderr)
+
+ def test_mark_bug_fixed(self):
+ tool = MockTool()
+ tool._scm.last_svn_commit_log = lambda: "r9876 |"
+ options = Mock()
+ options.bug_id = 42
+ options.comment = "MOCK comment"
+ expected_stderr = "Bug: <http://example.com/42> Bug with two r+'d and cq+'d patches, one of which has an invalid commit-queue setter.\nRevision: 9876\nMOCK: user.open_url: http://example.com/42\nAdding comment to Bug 42.\nMOCK bug comment: bug_id=42, cc=None\n--- Begin comment ---\\MOCK comment\n\nCommitted r9876: <http://trac.webkit.org/changeset/9876>\n--- End comment ---\n\n"
+ self.assert_execute_outputs(MarkBugFixed(), [], expected_stderr=expected_stderr, tool=tool, options=options)
+
+ def test_edit_changelog(self):
+ self.assert_execute_outputs(EditChangeLogs(), [])