summaryrefslogtreecommitdiffstats
path: root/WebKitTools/Scripts/webkitpy/tool/bot
diff options
context:
space:
mode:
Diffstat (limited to 'WebKitTools/Scripts/webkitpy/tool/bot')
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/bot/__init__.py1
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask.py196
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py204
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/bot/feeders.py90
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/bot/feeders_unittest.py70
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/bot/irc_command.py109
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/bot/irc_command_unittest.py38
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/bot/queueengine.py165
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/bot/queueengine_unittest.py209
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/bot/sheriff.py91
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/bot/sheriff_unittest.py90
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/bot/sheriffircbot.py83
-rw-r--r--WebKitTools/Scripts/webkitpy/tool/bot/sheriffircbot_unittest.py95
13 files changed, 1441 insertions, 0 deletions
diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/__init__.py b/WebKitTools/Scripts/webkitpy/tool/bot/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/bot/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask.py b/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask.py
new file mode 100644
index 0000000..ea12702
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask.py
@@ -0,0 +1,196 @@
+# 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.common.system.executive import ScriptError
+from webkitpy.common.net.layouttestresults import LayoutTestResults
+
+
+class CommitQueueTaskDelegate(object):
+ def run_command(self, command):
+ raise NotImplementedError("subclasses must implement")
+
+ def command_passed(self, message, patch):
+ raise NotImplementedError("subclasses must implement")
+
+ def command_failed(self, message, script_error, patch):
+ raise NotImplementedError("subclasses must implement")
+
+ def refetch_patch(self, patch):
+ raise NotImplementedError("subclasses must implement")
+
+ def layout_test_results(self):
+ raise NotImplementedError("subclasses must implement")
+
+ def report_flaky_tests(self, patch, flaky_tests):
+ raise NotImplementedError("subclasses must implement")
+
+
+class CommitQueueTask(object):
+ def __init__(self, delegate, patch):
+ self._delegate = delegate
+ self._patch = patch
+ self._script_error = None
+
+ def _validate(self):
+ # Bugs might get closed, or patches might be obsoleted or r-'d while the
+ # commit-queue is processing.
+ self._patch = self._delegate.refetch_patch(self._patch)
+ if self._patch.is_obsolete():
+ return False
+ if self._patch.bug().is_closed():
+ return False
+ if not self._patch.committer():
+ return False
+ # Reviewer is not required. Missing reviewers will be caught during
+ # the ChangeLog check during landing.
+ return True
+
+ def _run_command(self, command, success_message, failure_message):
+ try:
+ self._delegate.run_command(command)
+ self._delegate.command_passed(success_message, patch=self._patch)
+ return True
+ except ScriptError, e:
+ self._script_error = e
+ self.failure_status_id = self._delegate.command_failed(failure_message, script_error=self._script_error, patch=self._patch)
+ return False
+
+ def _apply(self):
+ return self._run_command([
+ "apply-attachment",
+ "--force-clean",
+ "--non-interactive",
+ self._patch.id(),
+ ],
+ "Applied patch",
+ "Patch does not apply")
+
+ def _build(self):
+ return self._run_command([
+ "build",
+ "--no-clean",
+ "--no-update",
+ "--build-style=both",
+ ],
+ "Built patch",
+ "Patch does not build")
+
+ def _build_without_patch(self):
+ return self._run_command([
+ "build",
+ "--force-clean",
+ "--no-update",
+ "--build-style=both",
+ ],
+ "Able to build without patch",
+ "Unable to build without patch")
+
+ def _test(self):
+ return self._run_command([
+ "build-and-test",
+ "--no-clean",
+ "--no-update",
+ # Notice that we don't pass --build, which means we won't build!
+ "--test",
+ "--non-interactive",
+ ],
+ "Passed tests",
+ "Patch does not pass tests")
+
+ def _build_and_test_without_patch(self):
+ return self._run_command([
+ "build-and-test",
+ "--force-clean",
+ "--no-update",
+ "--build",
+ "--test",
+ "--non-interactive",
+ ],
+ "Able to pass tests without patch",
+ "Unable to pass tests without patch (tree is red?)")
+
+ def _failing_tests_from_last_run(self):
+ results = self._delegate.layout_test_results()
+ if not results:
+ return None
+ return results.failing_tests()
+
+ def _land(self):
+ # Unclear if this should pass --quiet or not. If --parent-command always does the reporting, then it should.
+ return self._run_command([
+ "land-attachment",
+ "--force-clean",
+ "--ignore-builders",
+ "--non-interactive",
+ "--parent-command=commit-queue",
+ self._patch.id(),
+ ],
+ "Landed patch",
+ "Unable to land patch")
+
+ def _report_flaky_tests(self, flaky_tests):
+ self._delegate.report_flaky_tests(self._patch, flaky_tests)
+
+ def _test_patch(self):
+ if self._patch.is_rollout():
+ return True
+ if self._test():
+ return True
+
+ first_failing_tests = self._failing_tests_from_last_run()
+ if self._test():
+ self._report_flaky_tests(first_failing_tests)
+ return True
+
+ second_failing_tests = self._failing_tests_from_last_run()
+ if first_failing_tests != second_failing_tests:
+ self._report_flaky_tests(first_failing_tests + second_failing_tests)
+ return False
+
+ if self._build_and_test_without_patch():
+ raise self._script_error # The error from the previous ._test() run is real, report it.
+ return False # Tree must be red, just retry later.
+
+ def run(self):
+ if not self._validate():
+ return False
+ if not self._apply():
+ raise self._script_error
+ if not self._build():
+ if not self._build_without_patch():
+ return False
+ raise self._script_error
+ if not self._test_patch():
+ return False
+ # Make sure the patch is still valid before landing (e.g., make sure
+ # no one has set commit-queue- since we started working on the patch.)
+ if not self._validate():
+ return False
+ if not self._land():
+ raise self._script_error
+ return True
diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py b/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py
new file mode 100644
index 0000000..15a4a6b
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py
@@ -0,0 +1,204 @@
+# 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 datetime import datetime
+import unittest
+
+from webkitpy.common.system.deprecated_logging import error, log
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.bot.commitqueuetask import *
+from webkitpy.tool.mocktool import MockTool
+
+
+class MockCommitQueue(CommitQueueTaskDelegate):
+ def __init__(self, error_plan):
+ self._error_plan = error_plan
+
+ def run_command(self, command):
+ log("run_webkit_patch: %s" % command)
+ if self._error_plan:
+ error = self._error_plan.pop(0)
+ if error:
+ raise error
+
+ def command_passed(self, success_message, patch):
+ log("command_passed: success_message='%s' patch='%s'" % (
+ success_message, patch.id()))
+
+ def command_failed(self, failure_message, script_error, patch):
+ log("command_failed: failure_message='%s' script_error='%s' patch='%s'" % (
+ failure_message, script_error, patch.id()))
+ return 3947
+
+ def refetch_patch(self, patch):
+ return patch
+
+ def layout_test_results(self):
+ return None
+
+ def report_flaky_tests(self, patch, flaky_tests):
+ log("report_flaky_tests: patch='%s' flaky_tests='%s'" % (patch.id(), flaky_tests))
+
+
+class CommitQueueTaskTest(unittest.TestCase):
+ def _run_through_task(self, commit_queue, expected_stderr, expected_exception=None):
+ tool = MockTool(log_executive=True)
+ patch = tool.bugs.fetch_attachment(197)
+ task = CommitQueueTask(commit_queue, patch)
+ OutputCapture().assert_outputs(self, task.run, expected_stderr=expected_stderr, expected_exception=expected_exception)
+
+ def test_success_case(self):
+ commit_queue = MockCommitQueue([])
+ expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', 197]
+command_passed: success_message='Applied patch' patch='197'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Built patch' patch='197'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_passed: success_message='Passed tests' patch='197'
+run_webkit_patch: ['land-attachment', '--force-clean', '--ignore-builders', '--non-interactive', '--parent-command=commit-queue', 197]
+command_passed: success_message='Landed patch' patch='197'
+"""
+ self._run_through_task(commit_queue, expected_stderr)
+
+ def test_apply_failure(self):
+ commit_queue = MockCommitQueue([
+ ScriptError("MOCK apply failure"),
+ ])
+ expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', 197]
+command_failed: failure_message='Patch does not apply' script_error='MOCK apply failure' patch='197'
+"""
+ self._run_through_task(commit_queue, expected_stderr, ScriptError)
+
+ def test_build_failure(self):
+ commit_queue = MockCommitQueue([
+ None,
+ ScriptError("MOCK build failure"),
+ ])
+ expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', 197]
+command_passed: success_message='Applied patch' patch='197'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_failed: failure_message='Patch does not build' script_error='MOCK build failure' patch='197'
+run_webkit_patch: ['build', '--force-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Able to build without patch' patch='197'
+"""
+ self._run_through_task(commit_queue, expected_stderr, ScriptError)
+
+ def test_red_build_failure(self):
+ commit_queue = MockCommitQueue([
+ None,
+ ScriptError("MOCK build failure"),
+ ScriptError("MOCK clean build failure"),
+ ])
+ expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', 197]
+command_passed: success_message='Applied patch' patch='197'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_failed: failure_message='Patch does not build' script_error='MOCK build failure' patch='197'
+run_webkit_patch: ['build', '--force-clean', '--no-update', '--build-style=both']
+command_failed: failure_message='Unable to build without patch' script_error='MOCK clean build failure' patch='197'
+"""
+ self._run_through_task(commit_queue, expected_stderr)
+
+ def test_flaky_test_failure(self):
+ commit_queue = MockCommitQueue([
+ None,
+ None,
+ ScriptError("MOCK tests failure"),
+ ])
+ expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', 197]
+command_passed: success_message='Applied patch' patch='197'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Built patch' patch='197'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK tests failure' patch='197'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_passed: success_message='Passed tests' patch='197'
+report_flaky_tests: patch='197' flaky_tests='None'
+run_webkit_patch: ['land-attachment', '--force-clean', '--ignore-builders', '--non-interactive', '--parent-command=commit-queue', 197]
+command_passed: success_message='Landed patch' patch='197'
+"""
+ self._run_through_task(commit_queue, expected_stderr)
+
+ def test_test_failure(self):
+ commit_queue = MockCommitQueue([
+ None,
+ None,
+ ScriptError("MOCK test failure"),
+ ScriptError("MOCK test failure again"),
+ ])
+ expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', 197]
+command_passed: success_message='Applied patch' patch='197'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Built patch' patch='197'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure' patch='197'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure again' patch='197'
+run_webkit_patch: ['build-and-test', '--force-clean', '--no-update', '--build', '--test', '--non-interactive']
+command_passed: success_message='Able to pass tests without patch' patch='197'
+"""
+ self._run_through_task(commit_queue, expected_stderr, ScriptError)
+
+ def test_red_test_failure(self):
+ commit_queue = MockCommitQueue([
+ None,
+ None,
+ ScriptError("MOCK test failure"),
+ ScriptError("MOCK test failure again"),
+ ScriptError("MOCK clean test failure"),
+ ])
+ expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', 197]
+command_passed: success_message='Applied patch' patch='197'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Built patch' patch='197'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure' patch='197'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_failed: failure_message='Patch does not pass tests' script_error='MOCK test failure again' patch='197'
+run_webkit_patch: ['build-and-test', '--force-clean', '--no-update', '--build', '--test', '--non-interactive']
+command_failed: failure_message='Unable to pass tests without patch (tree is red?)' script_error='MOCK clean test failure' patch='197'
+"""
+ self._run_through_task(commit_queue, expected_stderr)
+
+ def test_land_failure(self):
+ commit_queue = MockCommitQueue([
+ None,
+ None,
+ None,
+ ScriptError("MOCK land failure"),
+ ])
+ expected_stderr = """run_webkit_patch: ['apply-attachment', '--force-clean', '--non-interactive', 197]
+command_passed: success_message='Applied patch' patch='197'
+run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both']
+command_passed: success_message='Built patch' patch='197'
+run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive']
+command_passed: success_message='Passed tests' patch='197'
+run_webkit_patch: ['land-attachment', '--force-clean', '--ignore-builders', '--non-interactive', '--parent-command=commit-queue', 197]
+command_failed: failure_message='Unable to land patch' script_error='MOCK land failure' patch='197'
+"""
+ self._run_through_task(commit_queue, expected_stderr, ScriptError)
diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/feeders.py b/WebKitTools/Scripts/webkitpy/tool/bot/feeders.py
new file mode 100644
index 0000000..046c4c1
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/bot/feeders.py
@@ -0,0 +1,90 @@
+# 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.common.config.committervalidator import CommitterValidator
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.tool.grammar import pluralize
+
+
+class AbstractFeeder(object):
+ def __init__(self, tool):
+ self._tool = tool
+
+ def feed(self):
+ raise NotImplementedError("subclasses must implement")
+
+
+class CommitQueueFeeder(AbstractFeeder):
+ queue_name = "commit-queue"
+
+ def __init__(self, tool):
+ AbstractFeeder.__init__(self, tool)
+ self.committer_validator = CommitterValidator(self._tool.bugs)
+
+ def _update_work_items(self, item_ids):
+ # FIXME: This is the last use of update_work_items, the commit-queue
+ # should move to feeding patches one at a time like the EWS does.
+ self._tool.status_server.update_work_items(self.queue_name, item_ids)
+ log("Feeding %s items %s" % (self.queue_name, item_ids))
+
+ def feed(self):
+ patches = self._validate_patches()
+ patches = sorted(patches, self._patch_cmp)
+ patch_ids = [patch.id() for patch in patches]
+ self._update_work_items(patch_ids)
+
+ def _patches_for_bug(self, bug_id):
+ return self._tool.bugs.fetch_bug(bug_id).commit_queued_patches(include_invalid=True)
+
+ def _validate_patches(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._patches_for_bug(bug_id) 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())
+
+
+class EWSFeeder(AbstractFeeder):
+ def __init__(self, tool):
+ self._ids_sent_to_server = set()
+ AbstractFeeder.__init__(self, tool)
+
+ def feed(self):
+ ids_needing_review = set(self._tool.bugs.queries.fetch_attachment_ids_from_review_queue())
+ new_ids = ids_needing_review.difference(self._ids_sent_to_server)
+ log("Feeding EWS (%s, %s new)" % (pluralize("r? patch", len(ids_needing_review)), len(new_ids)))
+ for attachment_id in new_ids: # Order doesn't really matter for the EWS.
+ self._tool.status_server.submit_to_ews(attachment_id)
+ self._ids_sent_to_server.add(attachment_id)
diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/feeders_unittest.py b/WebKitTools/Scripts/webkitpy/tool/bot/feeders_unittest.py
new file mode 100644
index 0000000..5ce00b4
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/bot/feeders_unittest.py
@@ -0,0 +1,70 @@
+# 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 datetime import datetime
+import unittest
+
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.bot.feeders import *
+from webkitpy.tool.mocktool import MockTool
+
+
+class FeedersTest(unittest.TestCase):
+ def test_commit_queue_feeder(self):
+ feeder = CommitQueueFeeder(MockTool())
+ expected_stderr = u"""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]
+Feeding commit-queue items [106, 197]
+"""
+ OutputCapture().assert_outputs(self, feeder.feed, expected_stderr=expected_stderr)
+
+ 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 = CommitQueueFeeder(MockTool())
+ attachments.sort(queue._patch_cmp)
+ self.assertEqual(attachments, expected_sort)
diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/irc_command.py b/WebKitTools/Scripts/webkitpy/tool/bot/irc_command.py
new file mode 100644
index 0000000..a848472
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/bot/irc_command.py
@@ -0,0 +1,109 @@
+# 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 random
+import webkitpy.common.config.irc as config_irc
+
+from webkitpy.common.checkout.changelog import view_source_url
+from webkitpy.tool.bot.queueengine import TerminateQueue
+from webkitpy.common.net.bugzilla import parse_bug_id
+from webkitpy.common.system.executive import ScriptError
+
+# FIXME: Merge with Command?
+class IRCCommand(object):
+ def execute(self, nick, args, tool, sheriff):
+ raise NotImplementedError, "subclasses must implement"
+
+
+class LastGreenRevision(IRCCommand):
+ def execute(self, nick, args, tool, sheriff):
+ return "%s: %s" % (nick,
+ view_source_url(tool.buildbot.last_green_revision()))
+
+
+class Restart(IRCCommand):
+ def execute(self, nick, args, tool, sheriff):
+ tool.irc().post("Restarting...")
+ raise TerminateQueue()
+
+
+class Rollout(IRCCommand):
+ def execute(self, nick, args, tool, sheriff):
+ if len(args) < 2:
+ tool.irc().post("%s: Usage: SVN_REVISION REASON" % nick)
+ return
+ svn_revision = args[0].lstrip("r")
+ rollout_reason = " ".join(args[1:])
+ tool.irc().post("Preparing rollout for r%s..." % svn_revision)
+ try:
+ complete_reason = "%s (Requested by %s on %s)." % (
+ rollout_reason, nick, config_irc.channel)
+ bug_id = sheriff.post_rollout_patch(svn_revision, complete_reason)
+ bug_url = tool.bugs.bug_url_for_bug_id(bug_id)
+ tool.irc().post("%s: Created rollout: %s" % (nick, bug_url))
+ except ScriptError, e:
+ tool.irc().post("%s: Failed to create rollout patch:" % nick)
+ tool.irc().post("%s" % e)
+ bug_id = parse_bug_id(e.output)
+ if bug_id:
+ tool.irc().post("Ugg... Might have created %s" %
+ tool.bugs.bug_url_for_bug_id(bug_id))
+
+
+class Help(IRCCommand):
+ def execute(self, nick, args, tool, sheriff):
+ return "%s: Available commands: %s" % (nick, ", ".join(commands.keys()))
+
+
+class Hi(IRCCommand):
+ def execute(self, nick, args, tool, sheriff):
+ quips = tool.bugs.quips()
+ quips.append('"Only you can prevent forest fires." -- Smokey the Bear')
+ return random.choice(quips)
+
+
+class Eliza(IRCCommand):
+ therapist = None
+
+ def __init__(self):
+ if not self.therapist:
+ import webkitpy.thirdparty.autoinstalled.eliza as eliza
+ Eliza.therapist = eliza.eliza()
+
+ def execute(self, nick, args, tool, sheriff):
+ return "%s: %s" % (nick, self.therapist.respond(" ".join(args)))
+
+
+# FIXME: Lame. We should have an auto-registering CommandCenter.
+commands = {
+ "last-green-revision": LastGreenRevision,
+ "restart": Restart,
+ "rollout": Rollout,
+ "help": Help,
+ "hi": Hi,
+}
diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/irc_command_unittest.py b/WebKitTools/Scripts/webkitpy/tool/bot/irc_command_unittest.py
new file mode 100644
index 0000000..7aeb6a0
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/bot/irc_command_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.bot.irc_command import *
+
+
+class IRCCommandTest(unittest.TestCase):
+ def test_eliza(self):
+ eliza = Eliza()
+ eliza.execute("tom", "hi", None, None)
+ eliza.execute("tom", "bye", None, None)
diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/queueengine.py b/WebKitTools/Scripts/webkitpy/tool/bot/queueengine.py
new file mode 100644
index 0000000..8b016e8
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/bot/queueengine.py
@@ -0,0 +1,165 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import time
+import traceback
+
+from datetime import datetime, timedelta
+
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.system.deprecated_logging import log, OutputTee
+
+
+class TerminateQueue(Exception):
+ pass
+
+
+class QueueEngineDelegate:
+ def queue_log_path(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def work_item_log_path(self, work_item):
+ raise NotImplementedError, "subclasses must implement"
+
+ def begin_work_queue(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def should_continue_work_queue(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def next_work_item(self):
+ raise NotImplementedError, "subclasses must implement"
+
+ def should_proceed_with_work_item(self, work_item):
+ # returns (safe_to_proceed, waiting_message, patch)
+ raise NotImplementedError, "subclasses must implement"
+
+ def process_work_item(self, work_item):
+ raise NotImplementedError, "subclasses must implement"
+
+ def handle_unexpected_error(self, work_item, message):
+ raise NotImplementedError, "subclasses must implement"
+
+
+class QueueEngine:
+ def __init__(self, name, delegate, wakeup_event):
+ self._name = name
+ self._delegate = delegate
+ self._wakeup_event = wakeup_event
+ self._output_tee = OutputTee()
+
+ log_date_format = "%Y-%m-%d %H:%M:%S"
+ sleep_duration_text = "2 mins" # This could be generated from seconds_to_sleep
+ seconds_to_sleep = 120
+ handled_error_code = 2
+
+ # Child processes exit with a special code to the parent queue process can detect the error was handled.
+ @classmethod
+ def exit_after_handled_error(cls, error):
+ log(error)
+ exit(cls.handled_error_code)
+
+ def run(self):
+ self._begin_logging()
+
+ self._delegate.begin_work_queue()
+ while (self._delegate.should_continue_work_queue()):
+ try:
+ self._ensure_work_log_closed()
+ work_item = self._delegate.next_work_item()
+ if not work_item:
+ self._sleep("No work item.")
+ continue
+ if not self._delegate.should_proceed_with_work_item(work_item):
+ self._sleep("Not proceeding with work item.")
+ continue
+
+ # FIXME: Work logs should not depend on bug_id specificaly.
+ # This looks fixed, no?
+ self._open_work_log(work_item)
+ try:
+ if not self._delegate.process_work_item(work_item):
+ log("Unable to process work item.")
+ continue
+ except ScriptError, e:
+ # Use a special exit code to indicate that the error was already
+ # handled in the child process and we should just keep looping.
+ if e.exit_code == self.handled_error_code:
+ continue
+ message = "Unexpected failure when processing patch! Please file a bug against webkit-patch.\n%s" % e.message_with_output()
+ self._delegate.handle_unexpected_error(work_item, message)
+ except TerminateQueue, e:
+ self._stopping("TerminateQueue exception received.")
+ return 0
+ except KeyboardInterrupt, e:
+ self._stopping("User terminated queue.")
+ return 1
+ except Exception, e:
+ traceback.print_exc()
+ # Don't try tell the status bot, in case telling it causes an exception.
+ self._sleep("Exception while preparing queue")
+ self._stopping("Delegate terminated queue.")
+ return 0
+
+ def _stopping(self, message):
+ log("\n%s" % message)
+ self._delegate.stop_work_queue(message)
+ # Be careful to shut down our OutputTee or the unit tests will be unhappy.
+ self._ensure_work_log_closed()
+ self._output_tee.remove_log(self._queue_log)
+
+ def _begin_logging(self):
+ self._queue_log = self._output_tee.add_log(self._delegate.queue_log_path())
+ self._work_log = None
+
+ def _open_work_log(self, work_item):
+ work_item_log_path = self._delegate.work_item_log_path(work_item)
+ if not work_item_log_path:
+ return
+ self._work_log = self._output_tee.add_log(work_item_log_path)
+
+ def _ensure_work_log_closed(self):
+ # If we still have a bug log open, close it.
+ if self._work_log:
+ self._output_tee.remove_log(self._work_log)
+ self._work_log = None
+
+ def _now(self):
+ """Overriden by the unit tests to allow testing _sleep_message"""
+ return datetime.now()
+
+ def _sleep_message(self, message):
+ wake_time = self._now() + timedelta(seconds=self.seconds_to_sleep)
+ return "%s Sleeping until %s (%s)." % (message, wake_time.strftime(self.log_date_format), self.sleep_duration_text)
+
+ def _sleep(self, message):
+ log(self._sleep_message(message))
+ self._wakeup_event.wait(self.seconds_to_sleep)
+ self._wakeup_event.clear()
diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/queueengine_unittest.py b/WebKitTools/Scripts/webkitpy/tool/bot/queueengine_unittest.py
new file mode 100644
index 0000000..37d8502
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/bot/queueengine_unittest.py
@@ -0,0 +1,209 @@
+# 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 datetime
+import os
+import shutil
+import tempfile
+import threading
+import unittest
+
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate, TerminateQueue
+
+
+class LoggingDelegate(QueueEngineDelegate):
+ def __init__(self, test):
+ self._test = test
+ self._callbacks = []
+ self._run_before = False
+ self.stop_message = None
+
+ expected_callbacks = [
+ 'queue_log_path',
+ 'begin_work_queue',
+ 'should_continue_work_queue',
+ 'next_work_item',
+ 'should_proceed_with_work_item',
+ 'work_item_log_path',
+ 'process_work_item',
+ 'should_continue_work_queue',
+ 'stop_work_queue',
+ ]
+
+ def record(self, method_name):
+ self._callbacks.append(method_name)
+
+ def queue_log_path(self):
+ self.record("queue_log_path")
+ return os.path.join(self._test.temp_dir, "queue_log_path")
+
+ def work_item_log_path(self, work_item):
+ self.record("work_item_log_path")
+ return os.path.join(self._test.temp_dir, "work_log_path", "%s.log" % work_item)
+
+ def begin_work_queue(self):
+ self.record("begin_work_queue")
+
+ def should_continue_work_queue(self):
+ self.record("should_continue_work_queue")
+ if not self._run_before:
+ self._run_before = True
+ return True
+ return False
+
+ def next_work_item(self):
+ self.record("next_work_item")
+ return "work_item"
+
+ def should_proceed_with_work_item(self, work_item):
+ self.record("should_proceed_with_work_item")
+ self._test.assertEquals(work_item, "work_item")
+ fake_patch = { 'bug_id' : 42 }
+ return (True, "waiting_message", fake_patch)
+
+ def process_work_item(self, work_item):
+ self.record("process_work_item")
+ self._test.assertEquals(work_item, "work_item")
+ return True
+
+ def handle_unexpected_error(self, work_item, message):
+ self.record("handle_unexpected_error")
+ self._test.assertEquals(work_item, "work_item")
+
+ def stop_work_queue(self, message):
+ self.record("stop_work_queue")
+ self.stop_message = message
+
+
+class RaisingDelegate(LoggingDelegate):
+ def __init__(self, test, exception):
+ LoggingDelegate.__init__(self, test)
+ self._exception = exception
+
+ def process_work_item(self, work_item):
+ self.record("process_work_item")
+ raise self._exception
+
+
+class NotSafeToProceedDelegate(LoggingDelegate):
+ def should_proceed_with_work_item(self, work_item):
+ self.record("should_proceed_with_work_item")
+ self._test.assertEquals(work_item, "work_item")
+ return False
+
+
+class FastQueueEngine(QueueEngine):
+ def __init__(self, delegate):
+ QueueEngine.__init__(self, "fast-queue", delegate, threading.Event())
+
+ # No sleep for the wicked.
+ seconds_to_sleep = 0
+
+ def _sleep(self, message):
+ pass
+
+
+class QueueEngineTest(unittest.TestCase):
+ def test_trivial(self):
+ delegate = LoggingDelegate(self)
+ self._run_engine(delegate)
+ self.assertEquals(delegate.stop_message, "Delegate terminated queue.")
+ self.assertEquals(delegate._callbacks, LoggingDelegate.expected_callbacks)
+ self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "queue_log_path")))
+ self.assertTrue(os.path.exists(os.path.join(self.temp_dir, "work_log_path", "work_item.log")))
+
+ def test_unexpected_error(self):
+ delegate = RaisingDelegate(self, ScriptError(exit_code=3))
+ self._run_engine(delegate)
+ expected_callbacks = LoggingDelegate.expected_callbacks[:]
+ work_item_index = expected_callbacks.index('process_work_item')
+ # The unexpected error should be handled right after process_work_item starts
+ # but before any other callback. Otherwise callbacks should be normal.
+ expected_callbacks.insert(work_item_index + 1, 'handle_unexpected_error')
+ self.assertEquals(delegate._callbacks, expected_callbacks)
+
+ def test_handled_error(self):
+ delegate = RaisingDelegate(self, ScriptError(exit_code=QueueEngine.handled_error_code))
+ self._run_engine(delegate)
+ self.assertEquals(delegate._callbacks, LoggingDelegate.expected_callbacks)
+
+ def _run_engine(self, delegate, engine=None, termination_message=None):
+ if not engine:
+ engine = QueueEngine("test-queue", delegate, threading.Event())
+ if not termination_message:
+ termination_message = "Delegate terminated queue."
+ expected_stderr = "\n%s\n" % termination_message
+ OutputCapture().assert_outputs(self, engine.run, [], expected_stderr=expected_stderr)
+
+ def _test_terminating_queue(self, exception, termination_message):
+ work_item_index = LoggingDelegate.expected_callbacks.index('process_work_item')
+ # The terminating error should be handled right after process_work_item.
+ # There should be no other callbacks after stop_work_queue.
+ expected_callbacks = LoggingDelegate.expected_callbacks[:work_item_index + 1]
+ expected_callbacks.append("stop_work_queue")
+
+ delegate = RaisingDelegate(self, exception)
+ self._run_engine(delegate, termination_message=termination_message)
+
+ self.assertEquals(delegate._callbacks, expected_callbacks)
+ self.assertEquals(delegate.stop_message, termination_message)
+
+ def test_terminating_error(self):
+ self._test_terminating_queue(KeyboardInterrupt(), "User terminated queue.")
+ self._test_terminating_queue(TerminateQueue(), "TerminateQueue exception received.")
+
+ def test_not_safe_to_proceed(self):
+ delegate = NotSafeToProceedDelegate(self)
+ self._run_engine(delegate, engine=FastQueueEngine(delegate))
+ expected_callbacks = LoggingDelegate.expected_callbacks[:]
+ expected_callbacks.remove('work_item_log_path')
+ expected_callbacks.remove('process_work_item')
+ self.assertEquals(delegate._callbacks, expected_callbacks)
+
+ def test_now(self):
+ """Make sure there are no typos in the QueueEngine.now() method."""
+ engine = QueueEngine("test", None, None)
+ self.assertTrue(isinstance(engine._now(), datetime.datetime))
+
+ def test_sleep_message(self):
+ engine = QueueEngine("test", None, None)
+ engine._now = lambda: datetime.datetime(2010, 1, 1)
+ expected_sleep_message = "MESSAGE Sleeping until 2010-01-01 00:02:00 (2 mins)."
+ self.assertEqual(engine._sleep_message("MESSAGE"), expected_sleep_message)
+
+ def setUp(self):
+ self.temp_dir = tempfile.mkdtemp(suffix="work_queue_test_logs")
+
+ def tearDown(self):
+ shutil.rmtree(self.temp_dir)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/sheriff.py b/WebKitTools/Scripts/webkitpy/tool/bot/sheriff.py
new file mode 100644
index 0000000..da506bc
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/bot/sheriff.py
@@ -0,0 +1,91 @@
+# 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.common.checkout.changelog import view_source_url
+from webkitpy.common.net.bugzilla import parse_bug_id
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.tool.grammar import join_with_separators
+
+
+class Sheriff(object):
+ def __init__(self, tool, sheriffbot):
+ self._tool = tool
+ self._sheriffbot = sheriffbot
+
+ def post_irc_warning(self, commit_info, builders):
+ irc_nicknames = sorted([party.irc_nickname for
+ party in commit_info.responsible_parties()
+ if party.irc_nickname])
+ irc_prefix = ": " if irc_nicknames else ""
+ irc_message = "%s%s%s might have broken %s" % (
+ ", ".join(irc_nicknames),
+ irc_prefix,
+ view_source_url(commit_info.revision()),
+ join_with_separators([builder.name() for builder in builders]))
+
+ self._tool.irc().post(irc_message)
+
+ def post_rollout_patch(self, svn_revision, rollout_reason):
+ # Ensure that svn_revision is a number (and not an option to
+ # create-rollout).
+ try:
+ svn_revision = int(svn_revision)
+ except:
+ raise ScriptError(message="Invalid svn revision number \"%s\"."
+ % svn_revision)
+
+ if rollout_reason.startswith("-"):
+ raise ScriptError(message="The rollout reason may not begin "
+ "with - (\"%s\")." % rollout_reason)
+
+ output = self._sheriffbot.run_webkit_patch([
+ "create-rollout",
+ "--force-clean",
+ # In principle, we should pass --non-interactive here, but it
+ # turns out that create-rollout doesn't need it yet. We can't
+ # pass it prophylactically because we reject unrecognized command
+ # line switches.
+ "--parent-command=sheriff-bot",
+ svn_revision,
+ rollout_reason,
+ ])
+ return parse_bug_id(output)
+
+ def post_blame_comment_on_bug(self, commit_info, builders, tests):
+ if not commit_info.bug_id():
+ return
+ comment = "%s might have broken %s" % (
+ view_source_url(commit_info.revision()),
+ join_with_separators([builder.name() for builder in builders]))
+ if tests:
+ comment += "\nThe following tests are not passing:\n"
+ comment += "\n".join(tests)
+ self._tool.bugs.post_comment_to_bug(commit_info.bug_id(),
+ comment,
+ cc=self._sheriffbot.watchers)
diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/sheriff_unittest.py b/WebKitTools/Scripts/webkitpy/tool/bot/sheriff_unittest.py
new file mode 100644
index 0000000..690af1f
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/bot/sheriff_unittest.py
@@ -0,0 +1,90 @@
+# 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
+import unittest
+
+from webkitpy.common.net.buildbot import Builder
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.bot.sheriff import Sheriff
+from webkitpy.tool.mocktool import MockTool
+
+
+class MockSheriffBot(object):
+ name = "mock-sheriff-bot"
+ watchers = [
+ "watcher@example.com",
+ ]
+
+ def run_webkit_patch(self, args):
+ return "Created bug https://bugs.webkit.org/show_bug.cgi?id=36936\n"
+
+
+class SheriffTest(unittest.TestCase):
+ def test_post_blame_comment_on_bug(self):
+ def run():
+ sheriff = Sheriff(MockTool(), MockSheriffBot())
+ builders = [
+ Builder("Foo", None),
+ Builder("Bar", None),
+ ]
+ commit_info = Mock()
+ commit_info.bug_id = lambda: None
+ commit_info.revision = lambda: 4321
+ # Should do nothing with no bug_id
+ sheriff.post_blame_comment_on_bug(commit_info, builders, [])
+ sheriff.post_blame_comment_on_bug(commit_info, builders, ["mock-test-1", "mock-test-2"])
+ # Should try to post a comment to the bug, but MockTool.bugs does nothing.
+ commit_info.bug_id = lambda: 1234
+ sheriff.post_blame_comment_on_bug(commit_info, builders, [])
+ sheriff.post_blame_comment_on_bug(commit_info, builders, ["mock-test-1"])
+ sheriff.post_blame_comment_on_bug(commit_info, builders, ["mock-test-1", "mock-test-2"])
+
+ expected_stderr = u"""MOCK bug comment: bug_id=1234, cc=['watcher@example.com']
+--- Begin comment ---
+http://trac.webkit.org/changeset/4321 might have broken Foo and Bar
+--- End comment ---
+
+MOCK bug comment: bug_id=1234, cc=['watcher@example.com']
+--- Begin comment ---
+http://trac.webkit.org/changeset/4321 might have broken Foo and Bar
+The following tests are not passing:
+mock-test-1
+--- End comment ---
+
+MOCK bug comment: bug_id=1234, cc=['watcher@example.com']
+--- Begin comment ---
+http://trac.webkit.org/changeset/4321 might have broken Foo and Bar
+The following tests are not passing:
+mock-test-1
+mock-test-2
+--- End comment ---
+
+"""
+ OutputCapture().assert_outputs(self, run, expected_stderr=expected_stderr)
diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/sheriffircbot.py b/WebKitTools/Scripts/webkitpy/tool/bot/sheriffircbot.py
new file mode 100644
index 0000000..de77222
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/bot/sheriffircbot.py
@@ -0,0 +1,83 @@
+# 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 webkitpy.tool.bot.irc_command as irc_command
+
+from webkitpy.common.net.irc.ircbot import IRCBotDelegate
+from webkitpy.common.thread.threadedmessagequeue import ThreadedMessageQueue
+
+
+class _IRCThreadTearoff(IRCBotDelegate):
+ def __init__(self, password, message_queue, wakeup_event):
+ self._password = password
+ self._message_queue = message_queue
+ self._wakeup_event = wakeup_event
+
+ # IRCBotDelegate methods
+
+ def irc_message_received(self, nick, message):
+ self._message_queue.post([nick, message])
+ self._wakeup_event.set()
+
+ def irc_nickname(self):
+ return "sheriffbot"
+
+ def irc_password(self):
+ return self._password
+
+
+class SheriffIRCBot(object):
+ def __init__(self, tool, sheriff):
+ self._tool = tool
+ self._sheriff = sheriff
+ self._message_queue = ThreadedMessageQueue()
+
+ def irc_delegate(self):
+ return _IRCThreadTearoff(self._tool.irc_password,
+ self._message_queue,
+ self._tool.wakeup_event)
+
+ def process_message(self, message):
+ (nick, request) = message
+ tokenized_request = request.strip().split(" ")
+ if not tokenized_request:
+ return
+ command = irc_command.commands.get(tokenized_request[0])
+ args = tokenized_request[1:]
+ if not command:
+ # Give the peoples someone to talk with.
+ command = irc_command.Eliza
+ args = tokenized_request
+ response = command().execute(nick, args, self._tool, self._sheriff)
+ if response:
+ self._tool.irc().post(response)
+
+ def process_pending_messages(self):
+ (messages, is_running) = self._message_queue.take_all()
+ for message in messages:
+ self.process_message(message)
diff --git a/WebKitTools/Scripts/webkitpy/tool/bot/sheriffircbot_unittest.py b/WebKitTools/Scripts/webkitpy/tool/bot/sheriffircbot_unittest.py
new file mode 100644
index 0000000..08023bd
--- /dev/null
+++ b/WebKitTools/Scripts/webkitpy/tool/bot/sheriffircbot_unittest.py
@@ -0,0 +1,95 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+import random
+
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.tool.bot.sheriff import Sheriff
+from webkitpy.tool.bot.sheriffircbot import SheriffIRCBot
+from webkitpy.tool.bot.sheriff_unittest import MockSheriffBot
+from webkitpy.tool.mocktool import MockTool
+
+
+def run(message):
+ tool = MockTool()
+ tool.ensure_irc_connected(None)
+ bot = SheriffIRCBot(tool, Sheriff(tool, MockSheriffBot()))
+ bot._message_queue.post(["mock_nick", message])
+ bot.process_pending_messages()
+
+
+class SheriffIRCBotTest(unittest.TestCase):
+ def test_hi(self):
+ random.seed(23324)
+ expected_stderr = 'MOCK: irc.post: "Only you can prevent forest fires." -- Smokey the Bear\n'
+ OutputCapture().assert_outputs(self, run, args=["hi"], expected_stderr=expected_stderr)
+
+ def test_help(self):
+ expected_stderr = "MOCK: irc.post: mock_nick: Available commands: rollout, hi, help, restart, last-green-revision\n"
+ OutputCapture().assert_outputs(self, run, args=["help"], expected_stderr=expected_stderr)
+
+ def test_lgr(self):
+ expected_stderr = "MOCK: irc.post: mock_nick: http://trac.webkit.org/changeset/9479\n"
+ OutputCapture().assert_outputs(self, run, args=["last-green-revision"], expected_stderr=expected_stderr)
+
+ def test_rollout(self):
+ expected_stderr = "MOCK: irc.post: Preparing rollout for r21654...\nMOCK: irc.post: mock_nick: Created rollout: http://example.com/36936\n"
+ OutputCapture().assert_outputs(self, run, args=["rollout 21654 This patch broke the world"], expected_stderr=expected_stderr)
+
+ def test_rollout_with_r_in_svn_revision(self):
+ expected_stderr = "MOCK: irc.post: Preparing rollout for r21654...\nMOCK: irc.post: mock_nick: Created rollout: http://example.com/36936\n"
+ OutputCapture().assert_outputs(self, run, args=["rollout r21654 This patch broke the world"], expected_stderr=expected_stderr)
+
+ def test_rollout_bananas(self):
+ expected_stderr = "MOCK: irc.post: mock_nick: Usage: SVN_REVISION REASON\n"
+ OutputCapture().assert_outputs(self, run, args=["rollout bananas"], expected_stderr=expected_stderr)
+
+ def test_rollout_invalidate_revision(self):
+ expected_stderr = ("MOCK: irc.post: Preparing rollout for r--component=Tools...\n"
+ "MOCK: irc.post: mock_nick: Failed to create rollout patch:\n"
+ "MOCK: irc.post: Invalid svn revision number \"--component=Tools\".\n")
+ OutputCapture().assert_outputs(self, run,
+ args=["rollout "
+ "--component=Tools 21654"],
+ expected_stderr=expected_stderr)
+
+ def test_rollout_invalidate_reason(self):
+ expected_stderr = ("MOCK: irc.post: Preparing rollout for "
+ "r21654...\nMOCK: irc.post: mock_nick: Failed to "
+ "create rollout patch:\nMOCK: irc.post: The rollout"
+ " reason may not begin with - (\"-bad (Requested "
+ "by mock_nick on #webkit).\").\n")
+ OutputCapture().assert_outputs(self, run,
+ args=["rollout "
+ "21654 -bad"],
+ expected_stderr=expected_stderr)
+
+ def test_rollout_no_reason(self):
+ expected_stderr = "MOCK: irc.post: mock_nick: Usage: SVN_REVISION REASON\n"
+ OutputCapture().assert_outputs(self, run, args=["rollout 21654"], expected_stderr=expected_stderr)