diff options
| author | Simon Hausmann <simon.hausmann@nokia.com> | 2012-01-06 14:44:00 +0100 |
|---|---|---|
| committer | Simon Hausmann <simon.hausmann@nokia.com> | 2012-01-06 14:44:00 +0100 |
| commit | 40736c5763bf61337c8c14e16d8587db021a87d4 (patch) | |
| tree | b17a9c00042ad89cb1308e2484491799aa14e9f8 /Tools/Scripts/webkitpy/tool | |
| download | qtwebkit-40736c5763bf61337c8c14e16d8587db021a87d4.tar.gz | |
Imported WebKit commit 2ea9d364d0f6efa8fa64acf19f451504c59be0e4 (http://svn.webkit.org/repository/webkit/trunk@104285)
Diffstat (limited to 'Tools/Scripts/webkitpy/tool')
132 files changed, 15343 insertions, 0 deletions
diff --git a/Tools/Scripts/webkitpy/tool/__init__.py b/Tools/Scripts/webkitpy/tool/__init__.py new file mode 100644 index 000000000..ef65bee5b --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/__init__.py @@ -0,0 +1 @@ +# Required for Python to search this directory for module files diff --git a/Tools/Scripts/webkitpy/tool/bot/__init__.py b/Tools/Scripts/webkitpy/tool/bot/__init__.py new file mode 100644 index 000000000..ef65bee5b --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/__init__.py @@ -0,0 +1 @@ +# Required for Python to search this directory for module files diff --git a/Tools/Scripts/webkitpy/tool/bot/botinfo.py b/Tools/Scripts/webkitpy/tool/bot/botinfo.py new file mode 100644 index 000000000..b9fd938aa --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/botinfo.py @@ -0,0 +1,39 @@ +# Copyright (c) 2011 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. + + +# FIXME: We should consider hanging one of these off the tool object. +class BotInfo(object): + def __init__(self, tool): + self._tool = tool + + def summary_text(self): + # bot_id is also stored on the options dictionary on the tool. + bot_id = self._tool.status_server.bot_id + bot_id_string = "Bot: %s " % (bot_id) if bot_id else "" + return "%sPort: %s Platform: %s" % (bot_id_string, self._tool.port().name(), self._tool.platform.display_name()) diff --git a/Tools/Scripts/webkitpy/tool/bot/botinfo_unittest.py b/Tools/Scripts/webkitpy/tool/bot/botinfo_unittest.py new file mode 100644 index 000000000..820ff559e --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/botinfo_unittest.py @@ -0,0 +1,41 @@ +# Copyright (c) 2011 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.botinfo import BotInfo +from webkitpy.tool.mocktool import MockTool +from webkitpy.common.net.statusserver_mock import MockStatusServer + + +class BotInfoTest(unittest.TestCase): + + def test_summary_text(self): + tool = MockTool() + tool.status_server = MockStatusServer("MockBotId") + self.assertEqual(BotInfo(tool).summary_text(), "Bot: MockBotId Port: MockPort Platform: MockPlatform 1.0") diff --git a/Tools/Scripts/webkitpy/tool/bot/commitqueuetask.py b/Tools/Scripts/webkitpy/tool/bot/commitqueuetask.py new file mode 100644 index 000000000..8b2d5c2c2 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/commitqueuetask.py @@ -0,0 +1,86 @@ +# 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.bot.patchanalysistask import PatchAnalysisTask, PatchAnalysisTaskDelegate + + +class CommitQueueTaskDelegate(PatchAnalysisTaskDelegate): + def parent_command(self): + return "commit-queue" + + +class CommitQueueTask(PatchAnalysisTask): + 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 + if self._patch.review() == "-": + return False + return True + + def _validate_changelog(self): + return self._run_command([ + "validate-changelog", + "--non-interactive", + self._patch.id(), + ], + "ChangeLog validated", + "ChangeLog did not pass validation") + + def run(self): + if not self.validate(): + return False + if not self._clean(): + return False + if not self._update(): + return False + if not self._apply(): + return self.report_failure() + if not self._validate_changelog(): + return self.report_failure() + if not self._patch.is_rollout(): + if not self._build(): + if not self._build_without_patch(): + return False + return self.report_failure() + 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 + # FIXME: We should understand why the land failure occured and retry if possible. + if not self._land(): + return self.report_failure() + return True diff --git a/Tools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py b/Tools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py new file mode 100644 index 000000000..67d11e7c0 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/commitqueuetask_unittest.py @@ -0,0 +1,555 @@ +# 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.net import bugzilla +from webkitpy.common.net.layouttestresults import LayoutTestResults +from webkitpy.common.system.deprecated_logging import error, log +from webkitpy.common.system.executive import ScriptError +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.layout_tests.models import test_results +from webkitpy.layout_tests.models import test_failures +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.bot.commitqueuetask import * +from webkitpy.tool.bot.expectedfailures import ExpectedFailures +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 expected_failures(self): + return ExpectedFailures() + + def layout_test_results(self): + return None + + def report_flaky_tests(self, patch, flaky_results, results_archive): + flaky_tests = [result.filename for result in flaky_results] + log("report_flaky_tests: patch='%s' flaky_tests='%s' archive='%s'" % (patch.id(), flaky_tests, results_archive.filename)) + + def archive_last_layout_test_results(self, patch): + log("archive_last_layout_test_results: patch='%s'" % patch.id()) + archive = Mock() + archive.filename = "mock-archive-%s.zip" % patch.id() + return archive + + def build_style(self): + return "both" + + +class FailingTestCommitQueue(MockCommitQueue): + def __init__(self, error_plan, test_failure_plan): + MockCommitQueue.__init__(self, error_plan) + self._test_run_counter = -1 # Special value to indicate tests have never been run. + self._test_failure_plan = test_failure_plan + + def run_command(self, command): + if command[0] == "build-and-test": + self._test_run_counter += 1 + MockCommitQueue.run_command(self, command) + + def _mock_test_result(self, testname): + return test_results.TestResult(testname, [test_failures.FailureTextMismatch()]) + + def layout_test_results(self): + # Doesn't make sense to ask for the layout_test_results until the tests have run at least once. + assert(self._test_run_counter >= 0) + failures_for_run = self._test_failure_plan[self._test_run_counter] + results = LayoutTestResults(map(self._mock_test_result, failures_for_run)) + # This makes the results trustable by ExpectedFailures. + results.set_failure_limit_count(10) + return results + + +# We use GoldenScriptError to make sure that the code under test throws the +# correct (i.e., golden) exception. +class GoldenScriptError(ScriptError): + pass + + +class CommitQueueTaskTest(unittest.TestCase): + def _run_through_task(self, commit_queue, expected_stderr, expected_exception=None, expect_retry=False): + tool = MockTool(log_executive=True) + patch = tool.bugs.fetch_attachment(10000) + task = CommitQueueTask(commit_queue, patch) + success = OutputCapture().assert_outputs(self, task.run, expected_stderr=expected_stderr, expected_exception=expected_exception) + if not expected_exception: + self.assertEqual(success, not expect_retry) + return task + + def test_success_case(self): + commit_queue = MockCommitQueue([]) + expected_stderr = """run_webkit_patch: ['clean'] +command_passed: success_message='Cleaned working directory' patch='10000' +run_webkit_patch: ['update'] +command_passed: success_message='Updated working directory' patch='10000' +run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000] +command_passed: success_message='Applied patch' patch='10000' +run_webkit_patch: ['validate-changelog', '--non-interactive', 10000] +command_passed: success_message='ChangeLog validated' patch='10000' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both'] +command_passed: success_message='Built patch' patch='10000' +run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive'] +command_passed: success_message='Passed tests' patch='10000' +run_webkit_patch: ['land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10000] +command_passed: success_message='Landed patch' patch='10000' +""" + self._run_through_task(commit_queue, expected_stderr) + + def test_clean_failure(self): + commit_queue = MockCommitQueue([ + ScriptError("MOCK clean failure"), + ]) + expected_stderr = """run_webkit_patch: ['clean'] +command_failed: failure_message='Unable to clean working directory' script_error='MOCK clean failure' patch='10000' +""" + self._run_through_task(commit_queue, expected_stderr, expect_retry=True) + + def test_update_failure(self): + commit_queue = MockCommitQueue([ + None, + ScriptError("MOCK update failure"), + ]) + expected_stderr = """run_webkit_patch: ['clean'] +command_passed: success_message='Cleaned working directory' patch='10000' +run_webkit_patch: ['update'] +command_failed: failure_message='Unable to update working directory' script_error='MOCK update failure' patch='10000' +""" + self._run_through_task(commit_queue, expected_stderr, expect_retry=True) + + def test_apply_failure(self): + commit_queue = MockCommitQueue([ + None, + None, + GoldenScriptError("MOCK apply failure"), + ]) + expected_stderr = """run_webkit_patch: ['clean'] +command_passed: success_message='Cleaned working directory' patch='10000' +run_webkit_patch: ['update'] +command_passed: success_message='Updated working directory' patch='10000' +run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000] +command_failed: failure_message='Patch does not apply' script_error='MOCK apply failure' patch='10000' +""" + self._run_through_task(commit_queue, expected_stderr, GoldenScriptError) + + def test_validate_changelog_failure(self): + commit_queue = MockCommitQueue([ + None, + None, + None, + GoldenScriptError("MOCK validate failure"), + ]) + expected_stderr = """run_webkit_patch: ['clean'] +command_passed: success_message='Cleaned working directory' patch='10000' +run_webkit_patch: ['update'] +command_passed: success_message='Updated working directory' patch='10000' +run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000] +command_passed: success_message='Applied patch' patch='10000' +run_webkit_patch: ['validate-changelog', '--non-interactive', 10000] +command_failed: failure_message='ChangeLog did not pass validation' script_error='MOCK validate failure' patch='10000' +""" + self._run_through_task(commit_queue, expected_stderr, GoldenScriptError) + + def test_build_failure(self): + commit_queue = MockCommitQueue([ + None, + None, + None, + None, + GoldenScriptError("MOCK build failure"), + ]) + expected_stderr = """run_webkit_patch: ['clean'] +command_passed: success_message='Cleaned working directory' patch='10000' +run_webkit_patch: ['update'] +command_passed: success_message='Updated working directory' patch='10000' +run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000] +command_passed: success_message='Applied patch' patch='10000' +run_webkit_patch: ['validate-changelog', '--non-interactive', 10000] +command_passed: success_message='ChangeLog validated' patch='10000' +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='10000' +run_webkit_patch: ['build', '--force-clean', '--no-update', '--build-style=both'] +command_passed: success_message='Able to build without patch' patch='10000' +""" + self._run_through_task(commit_queue, expected_stderr, GoldenScriptError) + + def test_red_build_failure(self): + commit_queue = MockCommitQueue([ + None, + None, + None, + None, + ScriptError("MOCK build failure"), + ScriptError("MOCK clean build failure"), + ]) + expected_stderr = """run_webkit_patch: ['clean'] +command_passed: success_message='Cleaned working directory' patch='10000' +run_webkit_patch: ['update'] +command_passed: success_message='Updated working directory' patch='10000' +run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000] +command_passed: success_message='Applied patch' patch='10000' +run_webkit_patch: ['validate-changelog', '--non-interactive', 10000] +command_passed: success_message='ChangeLog validated' patch='10000' +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='10000' +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='10000' +""" + self._run_through_task(commit_queue, expected_stderr, expect_retry=True) + + def test_flaky_test_failure(self): + commit_queue = MockCommitQueue([ + None, + None, + None, + None, + None, + ScriptError("MOCK tests failure"), + ]) + # CommitQueueTask will only report flaky tests if we successfully parsed + # results.html and returned a LayoutTestResults object, so we fake one. + commit_queue.layout_test_results = lambda: LayoutTestResults([]) + expected_stderr = """run_webkit_patch: ['clean'] +command_passed: success_message='Cleaned working directory' patch='10000' +run_webkit_patch: ['update'] +command_passed: success_message='Updated working directory' patch='10000' +run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000] +command_passed: success_message='Applied patch' patch='10000' +run_webkit_patch: ['validate-changelog', '--non-interactive', 10000] +command_passed: success_message='ChangeLog validated' patch='10000' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both'] +command_passed: success_message='Built patch' patch='10000' +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='10000' +archive_last_layout_test_results: patch='10000' +run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive'] +command_passed: success_message='Passed tests' patch='10000' +report_flaky_tests: patch='10000' flaky_tests='[]' archive='mock-archive-10000.zip' +run_webkit_patch: ['land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10000] +command_passed: success_message='Landed patch' patch='10000' +""" + self._run_through_task(commit_queue, expected_stderr) + + def test_failed_archive(self): + commit_queue = MockCommitQueue([ + None, + None, + None, + None, + None, + ScriptError("MOCK tests failure"), + ]) + commit_queue.layout_test_results = lambda: LayoutTestResults([]) + # It's possible delegate to fail to archive layout tests, don't try to report + # flaky tests when that happens. + commit_queue.archive_last_layout_test_results = lambda patch: None + expected_stderr = """run_webkit_patch: ['clean'] +command_passed: success_message='Cleaned working directory' patch='10000' +run_webkit_patch: ['update'] +command_passed: success_message='Updated working directory' patch='10000' +run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000] +command_passed: success_message='Applied patch' patch='10000' +run_webkit_patch: ['validate-changelog', '--non-interactive', 10000] +command_passed: success_message='ChangeLog validated' patch='10000' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both'] +command_passed: success_message='Built patch' patch='10000' +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='10000' +run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive'] +command_passed: success_message='Passed tests' patch='10000' +run_webkit_patch: ['land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10000] +command_passed: success_message='Landed patch' patch='10000' +""" + self._run_through_task(commit_queue, expected_stderr) + + def test_double_flaky_test_failure(self): + commit_queue = FailingTestCommitQueue([ + None, + None, + None, + None, + None, + ScriptError("MOCK test failure"), + ScriptError("MOCK test failure again"), + ], [ + "foo.html", + "bar.html", + "foo.html", + ]) + # The (subtle) point of this test is that report_flaky_tests does not appear + # in the expected_stderr for this run. + # Note also that there is no attempt to run the tests w/o the patch. + expected_stderr = """run_webkit_patch: ['clean'] +command_passed: success_message='Cleaned working directory' patch='10000' +run_webkit_patch: ['update'] +command_passed: success_message='Updated working directory' patch='10000' +run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000] +command_passed: success_message='Applied patch' patch='10000' +run_webkit_patch: ['validate-changelog', '--non-interactive', 10000] +command_passed: success_message='ChangeLog validated' patch='10000' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both'] +command_passed: success_message='Built patch' patch='10000' +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='10000' +archive_last_layout_test_results: patch='10000' +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='10000' +""" + tool = MockTool(log_executive=True) + patch = tool.bugs.fetch_attachment(10000) + task = CommitQueueTask(commit_queue, patch) + success = OutputCapture().assert_outputs(self, task.run, expected_stderr=expected_stderr) + self.assertEqual(success, False) + + def test_test_failure(self): + commit_queue = MockCommitQueue([ + None, + None, + None, + None, + None, + GoldenScriptError("MOCK test failure"), + ScriptError("MOCK test failure again"), + ]) + expected_stderr = """run_webkit_patch: ['clean'] +command_passed: success_message='Cleaned working directory' patch='10000' +run_webkit_patch: ['update'] +command_passed: success_message='Updated working directory' patch='10000' +run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000] +command_passed: success_message='Applied patch' patch='10000' +run_webkit_patch: ['validate-changelog', '--non-interactive', 10000] +command_passed: success_message='ChangeLog validated' patch='10000' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both'] +command_passed: success_message='Built patch' patch='10000' +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='10000' +archive_last_layout_test_results: patch='10000' +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='10000' +archive_last_layout_test_results: patch='10000' +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='10000' +""" + self._run_through_task(commit_queue, expected_stderr, GoldenScriptError) + + def test_red_test_failure(self): + commit_queue = FailingTestCommitQueue([ + None, + None, + None, + None, + None, + ScriptError("MOCK test failure"), + ScriptError("MOCK test failure again"), + ScriptError("MOCK clean test failure"), + ], [ + "foo.html", + "foo.html", + "foo.html", + ]) + + # Tests always fail, and always return the same results, but we + # should still be able to land in this case! + expected_stderr = """run_webkit_patch: ['clean'] +command_passed: success_message='Cleaned working directory' patch='10000' +run_webkit_patch: ['update'] +command_passed: success_message='Updated working directory' patch='10000' +run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000] +command_passed: success_message='Applied patch' patch='10000' +run_webkit_patch: ['validate-changelog', '--non-interactive', 10000] +command_passed: success_message='ChangeLog validated' patch='10000' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both'] +command_passed: success_message='Built patch' patch='10000' +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='10000' +archive_last_layout_test_results: patch='10000' +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='10000' +archive_last_layout_test_results: patch='10000' +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='10000' +run_webkit_patch: ['land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10000] +command_passed: success_message='Landed patch' patch='10000' +""" + self._run_through_task(commit_queue, expected_stderr) + + def test_very_red_tree_retry(self): + lots_of_failing_tests = map(lambda num: "test-%s.html" % num, range(0, 100)) + commit_queue = FailingTestCommitQueue([ + None, + None, + None, + None, + None, + ScriptError("MOCK test failure"), + ScriptError("MOCK test failure again"), + ScriptError("MOCK clean test failure"), + ], [ + lots_of_failing_tests, + lots_of_failing_tests, + lots_of_failing_tests, + ]) + + # Tests always fail, and return so many failures that we do not + # trust the results (see ExpectedFailures._can_trust_results) so we + # just give up and retry the patch. + expected_stderr = """run_webkit_patch: ['clean'] +command_passed: success_message='Cleaned working directory' patch='10000' +run_webkit_patch: ['update'] +command_passed: success_message='Updated working directory' patch='10000' +run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000] +command_passed: success_message='Applied patch' patch='10000' +run_webkit_patch: ['validate-changelog', '--non-interactive', 10000] +command_passed: success_message='ChangeLog validated' patch='10000' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both'] +command_passed: success_message='Built patch' patch='10000' +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='10000' +archive_last_layout_test_results: patch='10000' +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='10000' +archive_last_layout_test_results: patch='10000' +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='10000' +""" + self._run_through_task(commit_queue, expected_stderr, expect_retry=True) + + def test_red_tree_patch_rejection(self): + commit_queue = FailingTestCommitQueue([ + None, + None, + None, + None, + None, + GoldenScriptError("MOCK test failure"), + ScriptError("MOCK test failure again"), + ScriptError("MOCK clean test failure"), + ], [ + ["foo.html", "bar.html"], + ["foo.html", "bar.html"], + ["foo.html"], + ]) + + # Tests always fail, but the clean tree only fails one test + # while the patch fails two. So we should reject the patch! + expected_stderr = """run_webkit_patch: ['clean'] +command_passed: success_message='Cleaned working directory' patch='10000' +run_webkit_patch: ['update'] +command_passed: success_message='Updated working directory' patch='10000' +run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000] +command_passed: success_message='Applied patch' patch='10000' +run_webkit_patch: ['validate-changelog', '--non-interactive', 10000] +command_passed: success_message='ChangeLog validated' patch='10000' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both'] +command_passed: success_message='Built patch' patch='10000' +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='10000' +archive_last_layout_test_results: patch='10000' +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='10000' +archive_last_layout_test_results: patch='10000' +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='10000' +""" + task = self._run_through_task(commit_queue, expected_stderr, GoldenScriptError) + self.assertEqual(task.results_from_patch_test_run(task._patch).failing_tests(), ["foo.html", "bar.html"]) + + def test_land_failure(self): + commit_queue = MockCommitQueue([ + None, + None, + None, + None, + None, + None, + GoldenScriptError("MOCK land failure"), + ]) + expected_stderr = """run_webkit_patch: ['clean'] +command_passed: success_message='Cleaned working directory' patch='10000' +run_webkit_patch: ['update'] +command_passed: success_message='Updated working directory' patch='10000' +run_webkit_patch: ['apply-attachment', '--no-update', '--non-interactive', 10000] +command_passed: success_message='Applied patch' patch='10000' +run_webkit_patch: ['validate-changelog', '--non-interactive', 10000] +command_passed: success_message='ChangeLog validated' patch='10000' +run_webkit_patch: ['build', '--no-clean', '--no-update', '--build-style=both'] +command_passed: success_message='Built patch' patch='10000' +run_webkit_patch: ['build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive'] +command_passed: success_message='Passed tests' patch='10000' +run_webkit_patch: ['land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10000] +command_failed: failure_message='Unable to land patch' script_error='MOCK land failure' patch='10000' +""" + # FIXME: This should really be expect_retry=True for a better user experiance. + self._run_through_task(commit_queue, expected_stderr, GoldenScriptError) + + def _expect_validate(self, patch, is_valid): + class MockDelegate(object): + def refetch_patch(self, patch): + return patch + + def expected_failures(self): + return ExpectedFailures() + + task = CommitQueueTask(MockDelegate(), patch) + self.assertEquals(task.validate(), is_valid) + + def _mock_patch(self, attachment_dict={}, bug_dict={'bug_status': 'NEW'}, committer="fake"): + bug = bugzilla.Bug(bug_dict, None) + patch = bugzilla.Attachment(attachment_dict, bug) + patch._committer = committer + return patch + + def test_validate(self): + self._expect_validate(self._mock_patch(), True) + self._expect_validate(self._mock_patch({'is_obsolete': True}), False) + self._expect_validate(self._mock_patch(bug_dict={'bug_status': 'CLOSED'}), False) + self._expect_validate(self._mock_patch(committer=None), False) + self._expect_validate(self._mock_patch({'review': '-'}), False) diff --git a/Tools/Scripts/webkitpy/tool/bot/earlywarningsystemtask.py b/Tools/Scripts/webkitpy/tool/bot/earlywarningsystemtask.py new file mode 100644 index 000000000..65a71a701 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/earlywarningsystemtask.py @@ -0,0 +1,72 @@ +# Copyright (c) 2011 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.bot.patchanalysistask import PatchAnalysisTask, PatchAnalysisTaskDelegate + + +class UnableToApplyPatch(Exception): + def __init__(self, patch): + Exception.__init__(self) + self.patch = patch + + +class EarlyWarningSystemTaskDelegate(PatchAnalysisTaskDelegate): + pass + + +class EarlyWarningSystemTask(PatchAnalysisTask): + def __init__(self, delegate, patch, should_run_tests=True): + PatchAnalysisTask.__init__(self, delegate, patch) + self._should_run_tests = should_run_tests + + def validate(self): + self._patch = self._delegate.refetch_patch(self._patch) + if self._patch.is_obsolete(): + return False + if self._patch.bug().is_closed(): + return False + if self._patch.review() == "-": + return False + return True + + def run(self): + if not self.validate(): + return False + if not self._clean(): + return False + if not self._update(): + return False + if not self._apply(): + raise UnableToApplyPatch(self._patch) + if not self._build(): + if not self._build_without_patch(): + return False + return self.report_failure() + if not self._should_run_tests: + return True + return self._test_patch() diff --git a/Tools/Scripts/webkitpy/tool/bot/expectedfailures.py b/Tools/Scripts/webkitpy/tool/bot/expectedfailures.py new file mode 100644 index 000000000..adbd79515 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/expectedfailures.py @@ -0,0 +1,74 @@ +# Copyright (c) 2011 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. + + +class ExpectedFailures(object): + def __init__(self): + self._failures = set() + # If the set of failures is unbounded, self._failures isn't very + # meaningful because we can't store an unbounded set in memory. + self._failures_are_bounded = True + + def _has_failures(self, results): + return bool(results and len(results.failing_tests()) != 0) + + def has_bounded_failures(self, results): + assert(results) # You probably want to call _has_failures first! + return bool(results.failure_limit_count() and len(results.failing_tests()) < results.failure_limit_count()) + + def _can_trust_results(self, results): + return self._has_failures(results) and self.has_bounded_failures(results) + + def failures_were_expected(self, results): + if not self._can_trust_results(results): + return False + return set(results.failing_tests()) <= self._failures + + def unexpected_failures_observed(self, results): + if not self._has_failures(results): + return None + if not self._failures_are_bounded: + return None + return set(results.failing_tests()) - self._failures + + def shrink_expected_failures(self, results, run_success): + if run_success: + self._failures = set() + self._failures_are_bounded = True + elif self._can_trust_results(results): + # Remove all expected failures which are not in the new failing results. + self._failures.intersection_update(set(results.failing_tests())) + self._failures_are_bounded = True + + def grow_expected_failures(self, results): + if not self._can_trust_results(results): + self._failures_are_bounded = False + return + self._failures.update(results.failing_tests()) + self._failures_are_bounded = True + # FIXME: Should we assert() here that expected_failures never crosses a certain size? diff --git a/Tools/Scripts/webkitpy/tool/bot/expectedfailures_unittest.py b/Tools/Scripts/webkitpy/tool/bot/expectedfailures_unittest.py new file mode 100644 index 000000000..0668746a2 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/expectedfailures_unittest.py @@ -0,0 +1,94 @@ +# 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.tool.bot.expectedfailures import ExpectedFailures + + +class MockResults(object): + def __init__(self, failing_tests=[], failure_limit=10): + self._failing_tests = failing_tests + self._failure_limit_count = failure_limit + + def failure_limit_count(self): + return self._failure_limit_count + + def failing_tests(self): + return self._failing_tests + + +class ExpectedFailuresTest(unittest.TestCase): + def _assert_can_trust(self, results, can_trust): + self.assertEquals(ExpectedFailures()._can_trust_results(results), can_trust) + + def test_can_trust_results(self): + self._assert_can_trust(None, False) + self._assert_can_trust(MockResults(failing_tests=[], failure_limit=None), False) + self._assert_can_trust(MockResults(failing_tests=[], failure_limit=10), False) + self._assert_can_trust(MockResults(failing_tests=[1], failure_limit=None), False) + self._assert_can_trust(MockResults(failing_tests=[1], failure_limit=2), True) + self._assert_can_trust(MockResults(failing_tests=[1], failure_limit=1), False) + self._assert_can_trust(MockResults(failing_tests=[1, 2], failure_limit=1), False) + + def _assert_expected(self, expected_failures, failures, expected): + self.assertEqual(expected_failures.failures_were_expected(MockResults(failures)), expected) + + def test_failures_were_expected(self): + failures = ExpectedFailures() + failures.grow_expected_failures(MockResults(['foo.html'])) + self._assert_expected(failures, ['foo.html'], True) + self._assert_expected(failures, ['bar.html'], False) + failures.shrink_expected_failures(MockResults(['baz.html']), False) + self._assert_expected(failures, ['foo.html'], False) + self._assert_expected(failures, ['baz.html'], False) + + failures.grow_expected_failures(MockResults(['baz.html'])) + self._assert_expected(failures, ['baz.html'], True) + failures.shrink_expected_failures(MockResults(), True) + self._assert_expected(failures, ['baz.html'], False) + + def test_unexpected_failures_observed(self): + failures = ExpectedFailures() + failures.grow_expected_failures(MockResults(['foo.html'])) + self.assertEquals(failures.unexpected_failures_observed(MockResults(['foo.html', 'bar.html'])), set(['bar.html'])) + self.assertEquals(failures.unexpected_failures_observed(MockResults(['baz.html'])), set(['baz.html'])) + unbounded_results = MockResults(['baz.html', 'qux.html', 'taco.html'], failure_limit=3) + self.assertEquals(failures.unexpected_failures_observed(unbounded_results), set(['baz.html', 'qux.html', 'taco.html'])) + unbounded_results_with_existing_failure = MockResults(['foo.html', 'baz.html', 'qux.html', 'taco.html'], failure_limit=4) + self.assertEquals(failures.unexpected_failures_observed(unbounded_results_with_existing_failure), set(['baz.html', 'qux.html', 'taco.html'])) + + def test_unexpected_failures_observed_when_tree_is_hosed(self): + failures = ExpectedFailures() + failures.grow_expected_failures(MockResults(['foo.html', 'banana.html'], failure_limit=2)) + self.assertEquals(failures.unexpected_failures_observed(MockResults(['foo.html', 'bar.html'])), None) + self.assertEquals(failures.unexpected_failures_observed(MockResults(['baz.html'])), None) + unbounded_results = MockResults(['baz.html', 'qux.html', 'taco.html'], failure_limit=3) + self.assertEquals(failures.unexpected_failures_observed(unbounded_results), None) + unbounded_results_with_existing_failure = MockResults(['foo.html', 'baz.html', 'qux.html', 'taco.html'], failure_limit=4) + self.assertEquals(failures.unexpected_failures_observed(unbounded_results_with_existing_failure), None) diff --git a/Tools/Scripts/webkitpy/tool/bot/feeders.py b/Tools/Scripts/webkitpy/tool/bot/feeders.py new file mode 100644 index 000000000..4ba2f0485 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/feeders.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. + +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) + + 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 = self._patches_with_acceptable_review_flag(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) + + # Filters out patches with r? or r-, only r+ or no review are OK to land. + def _patches_with_acceptable_review_flag(self, patches): + return [patch for patch in patches if patch.review() in [None, '+']] + + 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/Tools/Scripts/webkitpy/tool/bot/feeders_unittest.py b/Tools/Scripts/webkitpy/tool/bot/feeders_unittest.py new file mode 100644 index 000000000..dff48ad8b --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/feeders_unittest.py @@ -0,0 +1,80 @@ +# 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 10001 on bug 50000 has invalid committer (non-committer@example.com) +Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com) +MOCK setting flag 'commit-queue' to '-' on attachment '10001' with comment 'Rejecting attachment 10001 from commit-queue.' and additional comment 'non-committer@example.com does not have committer permissions according to http://trac.webkit.org/browser/trunk/Tools/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 Tools/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 [10005, 10000] +Feeding commit-queue items [10005, 10000] +""" + 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) + + def test_patches_with_acceptable_review_flag(self): + class MockPatch(object): + def __init__(self, patch_id, review): + self.id = patch_id + self.review = lambda: review + + feeder = CommitQueueFeeder(MockTool()) + patches = [MockPatch(1, None), MockPatch(2, '-'), MockPatch(3, "+")] + self.assertEquals([patch.id for patch in feeder._patches_with_acceptable_review_flag(patches)], [1, 3]) diff --git a/Tools/Scripts/webkitpy/tool/bot/flakytestreporter.py b/Tools/Scripts/webkitpy/tool/bot/flakytestreporter.py new file mode 100644 index 000000000..3f2c1fca6 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/flakytestreporter.py @@ -0,0 +1,200 @@ +# 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 codecs +import logging +import platform +import os.path + +from webkitpy.common.net.layouttestresults import path_for_layout_test, LayoutTestResults +from webkitpy.common.config import urls +from webkitpy.tool.bot.botinfo import BotInfo +from webkitpy.tool.grammar import plural, pluralize, join_with_separators + +_log = logging.getLogger(__name__) + + +class FlakyTestReporter(object): + def __init__(self, tool, bot_name): + self._tool = tool + self._bot_name = bot_name + self._bot_info = BotInfo(tool) + + def _author_emails_for_test(self, flaky_test): + test_path = path_for_layout_test(flaky_test) + commit_infos = self._tool.checkout().recent_commit_infos_for_files([test_path]) + # This ignores authors which are not committers because we don't have their bugzilla_email. + return set([commit_info.author().bugzilla_email() for commit_info in commit_infos if commit_info.author()]) + + def _bugzilla_email(self): + # FIXME: This is kinda a funny way to get the bugzilla email, + # we could also just create a Credentials object directly + # but some of the Credentials logic is in bugzilla.py too... + self._tool.bugs.authenticate() + return self._tool.bugs.username + + # FIXME: This should move into common.config + _bot_emails = set([ + "commit-queue@webkit.org", # commit-queue + "eseidel@chromium.org", # old commit-queue + "webkit.review.bot@gmail.com", # style-queue, sheriff-bot, CrLx/Gtk EWS + "buildbot@hotmail.com", # Win EWS + # Mac EWS currently uses eric@webkit.org, but that's not normally a bot + ]) + + def _lookup_bug_for_flaky_test(self, flaky_test): + bugs = self._tool.bugs.queries.fetch_bugs_matching_search(search_string=flaky_test) + if not bugs: + return None + # Match any bugs which are from known bots or the email this bot is using. + allowed_emails = self._bot_emails | set([self._bugzilla_email]) + bugs = filter(lambda bug: bug.reporter_email() in allowed_emails, bugs) + if not bugs: + return None + if len(bugs) > 1: + # FIXME: There are probably heuristics we could use for finding + # the right bug instead of the first, like open vs. closed. + _log.warn("Found %s %s matching '%s' filed by a bot, using the first." % (pluralize('bug', len(bugs)), [bug.id() for bug in bugs], flaky_test)) + return bugs[0] + + def _view_source_url_for_test(self, test_path): + return urls.view_source_url("LayoutTests/%s" % test_path) + + def _create_bug_for_flaky_test(self, flaky_test, author_emails, latest_flake_message): + format_values = { + 'test': flaky_test, + 'authors': join_with_separators(sorted(author_emails)), + 'flake_message': latest_flake_message, + 'test_url': self._view_source_url_for_test(flaky_test), + 'bot_name': self._bot_name, + } + title = "Flaky Test: %(test)s" % format_values + description = """This is an automatically generated bug from the %(bot_name)s. +%(test)s has been flaky on the %(bot_name)s. + +%(test)s was authored by %(authors)s. +%(test_url)s + +%(flake_message)s + +The bots will update this with information from each new failure. + +If you believe this bug to be fixed or invalid, feel free to close. The bots will re-open if the flake re-occurs. + +If you would like to track this test fix with another bug, please close this bug as a duplicate. The bots will follow the duplicate chain when making future comments. +""" % format_values + + master_flake_bug = 50856 # MASTER: Flaky tests found by the commit-queue + return self._tool.bugs.create_bug(title, description, + component="Tools / Tests", + cc=",".join(author_emails), + blocked="50856") + + # This is over-engineered, but it makes for pretty bug messages. + def _optional_author_string(self, author_emails): + if not author_emails: + return "" + heading_string = plural('author') if len(author_emails) > 1 else 'author' + authors_string = join_with_separators(sorted(author_emails)) + return " (%s: %s)" % (heading_string, authors_string) + + def _latest_flake_message(self, flaky_result, patch): + failure_messages = [failure.message() for failure in flaky_result.failures] + flake_message = "The %s just saw %s flake (%s) while processing attachment %s on bug %s." % (self._bot_name, flaky_result.test_name, ", ".join(failure_messages), patch.id(), patch.bug_id()) + return "%s\n%s" % (flake_message, self._bot_info.summary_text()) + + def _results_diff_path_for_test(self, test_path): + # FIXME: This is a big hack. We should get this path from results.json + # except that old-run-webkit-tests doesn't produce a results.json + # so we just guess at the file path. + (test_path_root, _) = os.path.splitext(test_path) + return "%s-diffs.txt" % test_path_root + + def _follow_duplicate_chain(self, bug): + while bug.is_closed() and bug.duplicate_of(): + bug = self._tool.bugs.fetch_bug(bug.duplicate_of()) + return bug + + # Maybe this logic should move into Bugzilla? a reopen=True arg to post_comment? + def _update_bug_for_flaky_test(self, bug, latest_flake_message): + if bug.is_closed(): + self._tool.bugs.reopen_bug(bug.id(), latest_flake_message) + else: + self._tool.bugs.post_comment_to_bug(bug.id(), latest_flake_message) + + # This method is needed because our archive paths include a leading tmp/layout-test-results + def _find_in_archive(self, path, archive): + for archived_path in archive.namelist(): + # Archives are currently created with full paths. + if archived_path.endswith(path): + return archived_path + return None + + def _attach_failure_diff(self, flake_bug_id, flaky_test, results_archive_zip): + results_diff_path = self._results_diff_path_for_test(flaky_test) + # Check to make sure that the path makes sense. + # Since we're not actually getting this path from the results.html + # there is a chance it's wrong. + bot_id = self._tool.status_server.bot_id or "bot" + archive_path = self._find_in_archive(results_diff_path, results_archive_zip) + if archive_path: + results_diff = results_archive_zip.read(archive_path) + description = "Failure diff from %s" % bot_id + self._tool.bugs.add_attachment_to_bug(flake_bug_id, results_diff, description, filename="failure.diff") + else: + _log.warn("%s does not exist in results archive, uploading entire archive." % results_diff_path) + description = "Archive of layout-test-results from %s" % bot_id + # results_archive is a ZipFile object, grab the File object (.fp) to pass to Mechanize for uploading. + results_archive_file = results_archive_zip.fp + # Rewind the file object to start (since Mechanize won't do that automatically) + # See https://bugs.webkit.org/show_bug.cgi?id=54593 + results_archive_file.seek(0) + self._tool.bugs.add_attachment_to_bug(flake_bug_id, results_archive_file, description, filename="layout-test-results.zip") + + def report_flaky_tests(self, patch, flaky_test_results, results_archive): + message = "The %s encountered the following flaky tests while processing attachment %s:\n\n" % (self._bot_name, patch.id()) + for flaky_result in flaky_test_results: + flaky_test = flaky_result.test_name + bug = self._lookup_bug_for_flaky_test(flaky_test) + latest_flake_message = self._latest_flake_message(flaky_result, patch) + author_emails = self._author_emails_for_test(flaky_test) + if not bug: + _log.info("Bug does not already exist for %s, creating." % flaky_test) + flake_bug_id = self._create_bug_for_flaky_test(flaky_test, author_emails, latest_flake_message) + else: + bug = self._follow_duplicate_chain(bug) + # FIXME: Ideally we'd only make one comment per flake, not two. But that's not possible + # in all cases (e.g. when reopening), so for now file attachment and comment are separate. + self._update_bug_for_flaky_test(bug, latest_flake_message) + flake_bug_id = bug.id() + + self._attach_failure_diff(flake_bug_id, flaky_test, results_archive) + message += "%s bug %s%s\n" % (flaky_test, flake_bug_id, self._optional_author_string(author_emails)) + + message += "The %s is continuing to process your patch." % self._bot_name + self._tool.bugs.post_comment_to_bug(patch.bug_id(), message) diff --git a/Tools/Scripts/webkitpy/tool/bot/flakytestreporter_unittest.py b/Tools/Scripts/webkitpy/tool/bot/flakytestreporter_unittest.py new file mode 100644 index 000000000..961b83dfe --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/flakytestreporter_unittest.py @@ -0,0 +1,168 @@ +# Copyright (c) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import unittest + +from webkitpy.common.config.committers import Committer +from webkitpy.common.system.filesystem_mock import MockFileSystem +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.layout_tests.models import test_results +from webkitpy.layout_tests.models import test_failures +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.bot.flakytestreporter import FlakyTestReporter +from webkitpy.tool.mocktool import MockTool +from webkitpy.common.net.statusserver_mock import MockStatusServer + + +# Creating fake CommitInfos is a pain, so we use a mock one here. +class MockCommitInfo(object): + def __init__(self, author_email): + self._author_email = author_email + + def author(self): + # It's definitely possible to have commits with authors who + # are not in our committers.py list. + if not self._author_email: + return None + return Committer("Mock Committer", self._author_email) + + +class FlakyTestReporterTest(unittest.TestCase): + def _mock_test_result(self, testname): + return test_results.TestResult(testname, [test_failures.FailureTextMismatch()]) + + def _assert_emails_for_test(self, emails): + tool = MockTool() + reporter = FlakyTestReporter(tool, 'dummy-queue') + commit_infos = [MockCommitInfo(email) for email in emails] + tool.checkout().recent_commit_infos_for_files = lambda paths: set(commit_infos) + self.assertEqual(reporter._author_emails_for_test([]), set(emails)) + + def test_author_emails_for_test(self): + self._assert_emails_for_test([]) + self._assert_emails_for_test(["test1@test.com", "test1@test.com"]) + self._assert_emails_for_test(["test1@test.com", "test2@test.com"]) + + def test_create_bug_for_flaky_test(self): + reporter = FlakyTestReporter(MockTool(), 'dummy-queue') + expected_stderr = """MOCK create_bug +bug_title: Flaky Test: foo/bar.html +bug_description: This is an automatically generated bug from the dummy-queue. +foo/bar.html has been flaky on the dummy-queue. + +foo/bar.html was authored by test@test.com. +http://trac.webkit.org/browser/trunk/LayoutTests/foo/bar.html + +FLAKE_MESSAGE + +The bots will update this with information from each new failure. + +If you believe this bug to be fixed or invalid, feel free to close. The bots will re-open if the flake re-occurs. + +If you would like to track this test fix with another bug, please close this bug as a duplicate. The bots will follow the duplicate chain when making future comments. + +component: Tools / Tests +cc: test@test.com +blocked: 50856 +""" + OutputCapture().assert_outputs(self, reporter._create_bug_for_flaky_test, ['foo/bar.html', ['test@test.com'], 'FLAKE_MESSAGE'], expected_stderr=expected_stderr) + + def test_follow_duplicate_chain(self): + tool = MockTool() + reporter = FlakyTestReporter(tool, 'dummy-queue') + bug = tool.bugs.fetch_bug(50004) + self.assertEqual(reporter._follow_duplicate_chain(bug).id(), 50002) + + def test_report_flaky_tests_creating_bug(self): + tool = MockTool() + tool.filesystem = MockFileSystem({"/mock-results/foo/bar-diffs.txt": "mock"}) + tool.status_server = MockStatusServer(bot_id="mock-bot-id") + reporter = FlakyTestReporter(tool, 'dummy-queue') + reporter._lookup_bug_for_flaky_test = lambda bug_id: None + patch = tool.bugs.fetch_attachment(10000) + expected_stderr = """MOCK create_bug +bug_title: Flaky Test: foo/bar.html +bug_description: This is an automatically generated bug from the dummy-queue. +foo/bar.html has been flaky on the dummy-queue. + +foo/bar.html was authored by abarth@webkit.org. +http://trac.webkit.org/browser/trunk/LayoutTests/foo/bar.html + +The dummy-queue just saw foo/bar.html flake (Text diff mismatch) while processing attachment 10000 on bug 50000. +Bot: mock-bot-id Port: MockPort Platform: MockPlatform 1.0 + +The bots will update this with information from each new failure. + +If you believe this bug to be fixed or invalid, feel free to close. The bots will re-open if the flake re-occurs. + +If you would like to track this test fix with another bug, please close this bug as a duplicate. The bots will follow the duplicate chain when making future comments. + +component: Tools / Tests +cc: abarth@webkit.org +blocked: 50856 +MOCK add_attachment_to_bug: bug_id=50004, description=Failure diff from mock-bot-id filename=failure.diff +MOCK bug comment: bug_id=50000, cc=None +--- Begin comment --- +The dummy-queue encountered the following flaky tests while processing attachment 10000: + +foo/bar.html bug 50004 (author: abarth@webkit.org) +The dummy-queue is continuing to process your patch. +--- End comment --- + +""" + test_results = [self._mock_test_result('foo/bar.html')] + + class MockZipFile(object): + def read(self, path): + return "" + + def namelist(self): + return ['foo/bar-diffs.txt'] + + OutputCapture().assert_outputs(self, reporter.report_flaky_tests, [patch, test_results, MockZipFile()], expected_stderr=expected_stderr) + + def test_optional_author_string(self): + reporter = FlakyTestReporter(MockTool(), 'dummy-queue') + self.assertEqual(reporter._optional_author_string([]), "") + self.assertEqual(reporter._optional_author_string(["foo@bar.com"]), " (author: foo@bar.com)") + self.assertEqual(reporter._optional_author_string(["a@b.com", "b@b.com"]), " (authors: a@b.com and b@b.com)") + + def test_results_diff_path_for_test(self): + reporter = FlakyTestReporter(MockTool(), 'dummy-queue') + self.assertEqual(reporter._results_diff_path_for_test("test.html"), "test-diffs.txt") + + def test_find_in_archive(self): + reporter = FlakyTestReporter(MockTool(), 'dummy-queue') + + class MockZipFile(object): + def namelist(self): + return ["tmp/layout-test-results/foo/bar-diffs.txt"] + + reporter._find_in_archive("foo/bar-diffs.txt", MockZipFile()) + # This is not ideal, but its + reporter._find_in_archive("txt", MockZipFile()) diff --git a/Tools/Scripts/webkitpy/tool/bot/irc_command.py b/Tools/Scripts/webkitpy/tool/bot/irc_command.py new file mode 100644 index 000000000..fb2f42fd1 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/irc_command.py @@ -0,0 +1,270 @@ +# 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 itertools +import random +import re + +from webkitpy.common.config import irc as config_irc +from webkitpy.common.config import urls +from webkitpy.common.config.committers import CommitterList +from webkitpy.common.checkout.changelog import parse_bug_id +from webkitpy.common.system.executive import ScriptError +from webkitpy.tool.bot.queueengine import TerminateQueue +from webkitpy.tool.grammar import join_with_separators + + +def _post_error_and_check_for_bug_url(tool, nicks_string, exception): + tool.irc().post("%s" % exception) + bug_id = parse_bug_id(exception.output) + if bug_id: + bug_url = tool.bugs.bug_url_for_bug_id(bug_id) + tool.irc().post("%s: Ugg... Might have created %s" % (nicks_string, bug_url)) + + +# 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, + urls.view_revision_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 _extract_revisions(self, arg): + + revision_list = [] + possible_revisions = arg.split(",") + for revision in possible_revisions: + revision = revision.strip() + if not revision: + continue + revision = revision.lstrip("r") + # If one part of the arg isn't in the correct format, + # then none of the arg should be considered a revision. + if not revision.isdigit(): + return None + revision_list.append(int(revision)) + return revision_list + + def _parse_args(self, args): + if not args: + return (None, None) + + svn_revision_list = [] + remaining_args = args[:] + # First process all revisions. + while remaining_args: + new_revisions = self._extract_revisions(remaining_args[0]) + if not new_revisions: + break + svn_revision_list += new_revisions + remaining_args = remaining_args[1:] + + # Was there a revision number? + if not len(svn_revision_list): + return (None, None) + + # Everything left is the reason. + rollout_reason = " ".join(remaining_args) + return svn_revision_list, rollout_reason + + def _responsible_nicknames_from_revisions(self, tool, sheriff, svn_revision_list): + commit_infos = map(tool.checkout().commit_info_for_revision, svn_revision_list) + nickname_lists = map(sheriff.responsible_nicknames_from_commit_info, commit_infos) + return sorted(set(itertools.chain(*nickname_lists))) + + def _nicks_string(self, tool, sheriff, requester_nick, svn_revision_list): + # FIXME: _parse_args guarentees that our svn_revision_list is all numbers. + # However, it's possible our checkout will not include one of the revisions, + # so we may need to catch exceptions from commit_info_for_revision here. + target_nicks = [requester_nick] + self._responsible_nicknames_from_revisions(tool, sheriff, svn_revision_list) + return ", ".join(target_nicks) + + def _update_working_copy(self, tool): + tool.scm().ensure_clean_working_directory(force_clean=True) + tool.executive.run_and_throw_if_fail(tool.port().update_webkit_command(), quiet=True, cwd=tool.scm().checkout_root) + + def execute(self, nick, args, tool, sheriff): + svn_revision_list, rollout_reason = self._parse_args(args) + + if (not svn_revision_list or not rollout_reason): + # return is equivalent to an irc().post(), but makes for easier unit testing. + return "%s: Usage: rollout SVN_REVISION [SVN_REVISIONS] REASON" % nick + + revision_urls_string = join_with_separators([urls.view_revision_url(revision) for revision in svn_revision_list]) + tool.irc().post("%s: Preparing rollout for %s..." % (nick, revision_urls_string)) + + self._update_working_copy(tool) + + # FIXME: IRCCommand should bind to a tool and have a self._tool like Command objects do. + # Likewise we should probably have a self._sheriff. + nicks_string = self._nicks_string(tool, sheriff, nick, svn_revision_list) + + try: + complete_reason = "%s (Requested by %s on %s)." % ( + rollout_reason, nick, config_irc.channel) + bug_id = sheriff.post_rollout_patch(svn_revision_list, complete_reason) + bug_url = tool.bugs.bug_url_for_bug_id(bug_id) + tool.irc().post("%s: Created rollout: %s" % (nicks_string, bug_url)) + except ScriptError, e: + tool.irc().post("%s: Failed to create rollout patch:" % nicks_string) + _post_error_and_check_for_bug_url(tool, nicks_string, e) + + +class RollChromiumDEPS(IRCCommand): + def _parse_args(self, args): + if not args: + return + revision = args[0].lstrip("r") + if not revision.isdigit(): + return + return revision + + def execute(self, nick, args, tool, sheriff): + revision = self._parse_args(args) + + roll_target = "r%s" % revision if revision else "last-known good revision" + tool.irc().post("%s: Rolling Chromium DEPS to %s" % (nick, roll_target)) + + try: + bug_id = sheriff.post_chromium_deps_roll(revision, roll_target) + bug_url = tool.bugs.bug_url_for_bug_id(bug_id) + tool.irc().post("%s: Created DEPS roll: %s" % (nick, bug_url)) + except ScriptError, e: + match = re.search(r"Current Chromium DEPS revision \d+ is newer than \d+\.", e.output) + if match: + tool.irc().post("%s: %s" % (nick, match.group(0))) + return + tool.irc().post("%s: Failed to create DEPS roll:" % nick) + _post_error_and_check_for_bug_url(tool, nick, e) + + +class Help(IRCCommand): + def execute(self, nick, args, tool, sheriff): + return "%s: Available commands: %s" % (nick, ", ".join(sorted(visible_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 Whois(IRCCommand): + def _nick_or_full_record(self, contributor): + if contributor.irc_nicknames: + return ', '.join(contributor.irc_nicknames) + return unicode(contributor) + + def execute(self, nick, args, tool, sheriff): + if len(args) != 1: + return "%s: Usage: whois SEARCH_STRING" % nick + search_string = args[0] + # FIXME: We should get the ContributorList off the tool somewhere. + contributors = CommitterList().contributors_by_search_string(search_string) + if not contributors: + return "%s: Sorry, I don't know any contributors matching '%s'." % (nick, search_string) + if len(contributors) > 5: + return "%s: More than 5 contributors match '%s', could you be more specific?" % (nick, search_string) + if len(contributors) == 1: + contributor = contributors[0] + if not contributor.irc_nicknames: + return "%s: %s hasn't told me their nick. Boo hoo :-(" % (nick, contributor) + if contributor.emails and search_string.lower() not in map(lambda email: email.lower(), contributor.emails): + formattedEmails = ', '.join(contributor.emails) + return "%s: %s is %s (%s). Why do you ask?" % (nick, search_string, self._nick_or_full_record(contributor), formattedEmails) + else: + return "%s: %s is %s. Why do you ask?" % (nick, search_string, self._nick_or_full_record(contributor)) + contributor_nicks = map(self._nick_or_full_record, contributors) + contributors_string = join_with_separators(contributor_nicks, only_two_separator=" or ", last_separator=', or ') + return "%s: I'm not sure who you mean? %s could be '%s'." % (nick, contributors_string, search_string) + + +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))) + + +class CreateBug(IRCCommand): + def execute(self, nick, args, tool, sheriff): + if not args: + return "%s: Usage: create-bug BUG_TITLE" % nick + + bug_title = " ".join(args) + bug_description = "%s\nRequested by %s on %s." % (bug_title, nick, config_irc.channel) + + # There happens to be a committers list hung off of Bugzilla, so + # re-using that one makes things easiest for now. + requester = tool.bugs.committers.contributor_by_irc_nickname(nick) + requester_email = requester.bugzilla_email() if requester else None + + try: + bug_id = tool.bugs.create_bug(bug_title, bug_description, cc=requester_email, assignee=requester_email) + bug_url = tool.bugs.bug_url_for_bug_id(bug_id) + return "%s: Created bug: %s" % (nick, bug_url) + except Exception, e: + return "%s: Failed to create bug:\n%s" % (nick, e) + + +# FIXME: Lame. We should have an auto-registering CommandCenter. +visible_commands = { + "help": Help, + "hi": Hi, + "last-green-revision": LastGreenRevision, + "restart": Restart, + "rollout": Rollout, + "whois": Whois, + "create-bug": CreateBug, + "roll-chromium-deps": RollChromiumDEPS, +} + +# Add revert as an "easter egg" command. Why? +# revert is the same as rollout and it would be confusing to list both when +# they do the same thing. However, this command is a very natural thing for +# people to use and it seems silly to have them hunt around for "rollout" instead. +commands = visible_commands.copy() +commands["revert"] = Rollout diff --git a/Tools/Scripts/webkitpy/tool/bot/irc_command_unittest.py b/Tools/Scripts/webkitpy/tool/bot/irc_command_unittest.py new file mode 100644 index 000000000..096d16408 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/irc_command_unittest.py @@ -0,0 +1,122 @@ +# Copyright (c) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import unittest + +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.tool.bot.irc_command import * +from webkitpy.tool.mocktool import MockTool +from webkitpy.common.system.executive_mock import MockExecutive + + +class IRCCommandTest(unittest.TestCase): + def test_eliza(self): + eliza = Eliza() + eliza.execute("tom", "hi", None, None) + eliza.execute("tom", "bye", None, None) + + def test_whois(self): + whois = Whois() + self.assertEquals("tom: Usage: whois SEARCH_STRING", + whois.execute("tom", [], None, None)) + self.assertEquals("tom: Usage: whois SEARCH_STRING", + whois.execute("tom", ["Adam", "Barth"], None, None)) + self.assertEquals("tom: Sorry, I don't know any contributors matching 'unknown@example.com'.", + whois.execute("tom", ["unknown@example.com"], None, None)) + self.assertEquals("tom: tonyg@chromium.org is tonyg-cr. Why do you ask?", + whois.execute("tom", ["tonyg@chromium.org"], None, None)) + self.assertEquals("tom: TonyG@Chromium.org is tonyg-cr. Why do you ask?", + whois.execute("tom", ["TonyG@Chromium.org"], None, None)) + self.assertEquals("tom: rniwa is rniwa (rniwa@webkit.org). Why do you ask?", + whois.execute("tom", ["rniwa"], None, None)) + self.assertEquals("tom: lopez is xan (xan.lopez@gmail.com, xan@gnome.org, xan@webkit.org, xlopez@igalia.com). Why do you ask?", + whois.execute("tom", ["lopez"], None, None)) + self.assertEquals('tom: "Vicki Murley" <vicki@apple.com> hasn\'t told me their nick. Boo hoo :-(', + whois.execute("tom", ["vicki@apple.com"], None, None)) + self.assertEquals('tom: I\'m not sure who you mean? epenner, eroman, ericu, eric_carlson, or eseidel could be \'Eric\'.', + whois.execute("tom", ["Eric"], None, None)) + self.assertEquals('tom: More than 5 contributors match \'david\', could you be more specific?', + whois.execute("tom", ["david"], None, None)) + + def test_create_bug(self): + create_bug = CreateBug() + self.assertEquals("tom: Usage: create-bug BUG_TITLE", + create_bug.execute("tom", [], None, None)) + + example_args = ["sherrif-bot", "should", "have", "a", "create-bug", "command"] + tool = MockTool() + + # MockBugzilla has a create_bug, but it logs to stderr, this avoids any logging. + tool.bugs.create_bug = lambda a, b, cc=None, assignee=None: 50004 + self.assertEquals("tom: Created bug: http://example.com/50004", + create_bug.execute("tom", example_args, tool, None)) + + def mock_create_bug(title, description, cc=None, assignee=None): + raise Exception("Exception from bugzilla!") + tool.bugs.create_bug = mock_create_bug + self.assertEquals("tom: Failed to create bug:\nException from bugzilla!", + create_bug.execute("tom", example_args, tool, None)) + + def test_roll_chromium_deps(self): + roll = RollChromiumDEPS() + self.assertEquals(None, roll._parse_args([])) + self.assertEquals("1234", roll._parse_args(["1234"])) + + def test_rollout_updates_working_copy(self): + rollout = Rollout() + tool = MockTool() + tool.executive = MockExecutive(should_log=True) + expected_stderr = "MOCK run_and_throw_if_fail: ['mock-update-webkit'], cwd=/mock-checkout\n" + OutputCapture().assert_outputs(self, rollout._update_working_copy, [tool], expected_stderr=expected_stderr) + + def test_rollout(self): + rollout = Rollout() + self.assertEquals(([1234], "testing foo"), + rollout._parse_args(["1234", "testing", "foo"])) + + self.assertEquals(([554], "testing foo"), + rollout._parse_args(["r554", "testing", "foo"])) + + self.assertEquals(([556, 792], "testing foo"), + rollout._parse_args(["r556", "792", "testing", "foo"])) + + self.assertEquals(([128, 256], "testing foo"), + rollout._parse_args(["r128,r256", "testing", "foo"])) + + self.assertEquals(([512, 1024, 2048], "testing foo"), + rollout._parse_args(["512,", "1024,2048", "testing", "foo"])) + + # Test invalid argument parsing: + self.assertEquals((None, None), rollout._parse_args([])) + self.assertEquals((None, None), rollout._parse_args(["--bar", "1234"])) + + # Invalid arguments result in the USAGE message. + self.assertEquals("tom: Usage: rollout SVN_REVISION [SVN_REVISIONS] REASON", + rollout.execute("tom", [], None, None)) + + # FIXME: We need a better way to test IRCCommands which call tool.irc().post() diff --git a/Tools/Scripts/webkitpy/tool/bot/layouttestresultsreader.py b/Tools/Scripts/webkitpy/tool/bot/layouttestresultsreader.py new file mode 100644 index 000000000..1a0366e25 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/layouttestresultsreader.py @@ -0,0 +1,88 @@ +# Copyright (c) 2011 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.layouttestresults import LayoutTestResults +from webkitpy.common.system.deprecated_logging import error, log +from webkitpy.tool.steps.runtests import RunTests + + +class LayoutTestResultsReader(object): + def __init__(self, tool, archive_directory): + self._tool = tool + self._archive_directory = archive_directory + + # FIXME: This exists for mocking, but should instead be mocked via + # tool.filesystem.read_text_file. They have different error handling at the moment. + def _read_file_contents(self, path): + try: + return self._tool.filesystem.read_text_file(path) + except IOError, e: # File does not exist or can't be read. + return None + + # FIXME: This logic should move to the port object. + def _create_layout_test_results(self): + results_path = self._tool.port().layout_tests_results_path() + results_html = self._read_file_contents(results_path) + if not results_html: + return None + return LayoutTestResults.results_from_string(results_html) + + def results(self): + results = self._create_layout_test_results() + # FIXME: We should not have to set failure_limit_count, but we + # do until run-webkit-tests can be updated save off the value + # of --exit-after-N-failures in results.html/results.json. + # https://bugs.webkit.org/show_bug.cgi?id=58481 + if results: + results.set_failure_limit_count(RunTests.NON_INTERACTIVE_FAILURE_LIMIT_COUNT) + return results + + def _results_directory(self): + results_path = self._tool.port().layout_tests_results_path() + # FIXME: This is wrong in two ways: + # 1. It assumes that results.html is at the top level of the results tree. + # 2. This uses the "old" ports.py infrastructure instead of the new layout_tests/port + # which will not support Chromium. However the new arch doesn't work with old-run-webkit-tests + # so we have to use this for now. + return self._tool.filesystem.dirname(results_path) + + def archive(self, patch): + results_directory = self._results_directory() + results_name, _ = self._tool.filesystem.splitext(self._tool.filesystem.basename(results_directory)) + # Note: We name the zip with the bug_id instead of patch_id to match work_item_log_path(). + zip_path = self._tool.workspace.find_unused_filename(self._archive_directory, "%s-%s" % (patch.bug_id(), results_name), "zip") + if not zip_path: + return None + if not self._tool.filesystem.isdir(results_directory): + log("%s does not exist, not archiving." % results_directory) + return None + archive = self._tool.workspace.create_zip(zip_path, results_directory) + # Remove the results directory to prevent http logs, etc. from getting huge between runs. + # We could have create_zip remove the original, but this is more explicit. + self._tool.filesystem.rmtree(results_directory) + return archive diff --git a/Tools/Scripts/webkitpy/tool/bot/layouttestresultsreader_unittest.py b/Tools/Scripts/webkitpy/tool/bot/layouttestresultsreader_unittest.py new file mode 100644 index 000000000..8567d0f7c --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/layouttestresultsreader_unittest.py @@ -0,0 +1,77 @@ +# Copyright (c) 2011 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.filesystem_mock import MockFileSystem +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.common.net.layouttestresults import LayoutTestResults +from webkitpy.tool.bot.layouttestresultsreader import * +from webkitpy.tool.mocktool import MockTool + + +class LayoutTestResultsReaderTest(unittest.TestCase): + def test_missing_layout_test_results(self): + tool = MockTool() + reader = LayoutTestResultsReader(tool, "/var/logs") + results_path = '/mock-results/results.html' + tool.filesystem = MockFileSystem({results_path: None}) + # Make sure that our filesystem mock functions as we expect. + self.assertRaises(IOError, tool.filesystem.read_text_file, results_path) + # layout_test_results shouldn't raise even if the results.html file is missing. + self.assertEquals(reader.results(), None) + + def test_layout_test_results(self): + reader = LayoutTestResultsReader(MockTool(), "/var/logs") + reader._read_file_contents = lambda path: None + self.assertEquals(reader.results(), None) + reader._read_file_contents = lambda path: "" + self.assertEquals(reader.results(), None) + reader._create_layout_test_results = lambda: LayoutTestResults([]) + results = reader.results() + self.assertNotEquals(results, None) + self.assertEquals(results.failure_limit_count(), 20) # This value matches RunTests.NON_INTERACTIVE_FAILURE_LIMIT_COUNT + + def test_archive_last_layout_test_results(self): + tool = MockTool() + reader = LayoutTestResultsReader(tool, "/var/logs") + patch = tool.bugs.fetch_attachment(10001) + tool.filesystem = MockFileSystem() + # Should fail because the results_directory does not exist. + expected_stderr = "/mock-results does not exist, not archiving.\n" + archive = OutputCapture().assert_outputs(self, reader.archive, [patch], expected_stderr=expected_stderr) + self.assertEqual(archive, None) + + results_directory = "/mock-results" + # Sanity check what we assume our mock results directory is. + self.assertEqual(reader._results_directory(), results_directory) + tool.filesystem.maybe_make_directory(results_directory) + self.assertTrue(tool.filesystem.exists(results_directory)) + + self.assertNotEqual(reader.archive(patch), None) + self.assertFalse(tool.filesystem.exists(results_directory)) diff --git a/Tools/Scripts/webkitpy/tool/bot/patchanalysistask.py b/Tools/Scripts/webkitpy/tool/bot/patchanalysistask.py new file mode 100644 index 000000000..47e23a1c4 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/patchanalysistask.py @@ -0,0 +1,252 @@ +# 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 PatchAnalysisTaskDelegate(object): + def parent_command(self): + raise NotImplementedError("subclasses must implement") + + 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 expected_failures(self): + raise NotImplementedError("subclasses must implement") + + def layout_test_results(self): + raise NotImplementedError("subclasses must implement") + + def archive_last_layout_test_results(self, patch): + raise NotImplementedError("subclasses must implement") + + def build_style(self): + raise NotImplementedError("subclasses must implement") + + # We could make results_archive optional, but for now it's required. + def report_flaky_tests(self, patch, flaky_tests, results_archive): + raise NotImplementedError("subclasses must implement") + + +class PatchAnalysisTask(object): + def __init__(self, delegate, patch): + self._delegate = delegate + self._patch = patch + self._script_error = None + self._results_archive_from_patch_test_run = None + self._results_from_patch_test_run = None + self._expected_failures = delegate.expected_failures() + assert(self._expected_failures) + + 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 _clean(self): + return self._run_command([ + "clean", + ], + "Cleaned working directory", + "Unable to clean working directory") + + def _update(self): + # FIXME: Ideally the status server log message should include which revision we updated to. + return self._run_command([ + "update", + ], + "Updated working directory", + "Unable to update working directory") + + def _apply(self): + return self._run_command([ + "apply-attachment", + "--no-update", + "--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=%s" % self._delegate.build_style(), + ], + "Built patch", + "Patch does not build") + + def _build_without_patch(self): + return self._run_command([ + "build", + "--force-clean", + "--no-update", + "--build-style=%s" % self._delegate.build_style(), + ], + "Able to build without patch", + "Unable to build without patch") + + def _test(self): + success = 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") + + self._expected_failures.shrink_expected_failures(self._delegate.layout_test_results(), success) + return success + + def _build_and_test_without_patch(self): + success = 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?)") + + self._expected_failures.shrink_expected_failures(self._delegate.layout_test_results(), success) + return success + + 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", + "--non-interactive", + "--parent-command=" + self._delegate.parent_command(), + self._patch.id(), + ], + "Landed patch", + "Unable to land patch") + + def _report_flaky_tests(self, flaky_test_results, results_archive): + self._delegate.report_flaky_tests(self._patch, flaky_test_results, results_archive) + + def _results_failed_different_tests(self, first, second): + first_failing_tests = [] if not first else first.failing_tests() + second_failing_tests = [] if not second else second.failing_tests() + return first_failing_tests != second_failing_tests + + def _test_patch(self): + if self._test(): + return True + + # Note: archive_last_layout_test_results deletes the results directory, making these calls order-sensitve. + # We could remove this dependency by building the layout_test_results from the archive. + first_results = self._delegate.layout_test_results() + first_results_archive = self._delegate.archive_last_layout_test_results(self._patch) + first_script_error = self._script_error + + if self._expected_failures.failures_were_expected(first_results): + return True + + if self._test(): + # Only report flaky tests if we were successful at parsing results.html and archiving results. + if first_results and first_results_archive: + self._report_flaky_tests(first_results.failing_test_results(), first_results_archive) + return True + + second_results = self._delegate.layout_test_results() + if self._results_failed_different_tests(first_results, second_results): + # We could report flaky tests here, but we would need to be careful + # to use similar checks to ExpectedFailures._can_trust_results + # to make sure we don't report constant failures as flakes when + # we happen to hit the --exit-after-N-failures limit. + # See https://bugs.webkit.org/show_bug.cgi?id=51272 + return False + + # Archive (and remove) second results so layout_test_results() after + # build_and_test_without_patch won't use second results instead of the clean-tree results. + second_results_archive = self._delegate.archive_last_layout_test_results(self._patch) + + if self._build_and_test_without_patch(): + # The error from the previous ._test() run is real, report it. + return self.report_failure(first_results_archive, first_results, first_script_error) + + clean_tree_results = self._delegate.layout_test_results() + self._expected_failures.grow_expected_failures(clean_tree_results) + + # Re-check if the original results are now to be expected to avoid a full re-try. + if self._expected_failures.failures_were_expected(first_results): + return True + + # Now that we have updated information about failing tests with a clean checkout, we can + # tell if our original failures were unexpected and fail the patch if necessary. + if self._expected_failures.unexpected_failures_observed(first_results): + return self.report_failure(first_results_archive, first_results, first_script_error) + + # We don't know what's going on. The tree is likely very red (beyond our layout-test-results + # failure limit), just keep retrying the patch. until someone fixes the tree. + return False + + def results_archive_from_patch_test_run(self, patch): + assert(self._patch.id() == patch.id()) # PatchAnalysisTask is not currently re-useable. + return self._results_archive_from_patch_test_run + + def results_from_patch_test_run(self, patch): + assert(self._patch.id() == patch.id()) # PatchAnalysisTask is not currently re-useable. + return self._results_from_patch_test_run + + def report_failure(self, results_archive=None, results=None, script_error=None): + if not self.validate(): + return False + self._results_archive_from_patch_test_run = results_archive + self._results_from_patch_test_run = results + raise script_error or self._script_error + + def validate(self): + raise NotImplementedError("subclasses must implement") + + def run(self): + raise NotImplementedError("subclasses must implement") diff --git a/Tools/Scripts/webkitpy/tool/bot/queueengine.py b/Tools/Scripts/webkitpy/tool/bot/queueengine.py new file mode 100644 index 000000000..2f087bfcd --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/queueengine.py @@ -0,0 +1,167 @@ +# 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 + + +# FIXME: This will be caught by "except Exception:" blocks, we should consider +# making this inherit from SystemExit instead (or BaseException, except that's not recommended). +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/Tools/Scripts/webkitpy/tool/bot/queueengine_unittest.py b/Tools/Scripts/webkitpy/tool/bot/queueengine_unittest.py new file mode 100644 index 000000000..d860c6c73 --- /dev/null +++ b/Tools/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': 50000} + 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/Tools/Scripts/webkitpy/tool/bot/sheriff.py b/Tools/Scripts/webkitpy/tool/bot/sheriff.py new file mode 100644 index 000000000..df2686803 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/sheriff.py @@ -0,0 +1,116 @@ +# 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 import urls +from webkitpy.common.checkout.changelog 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 responsible_nicknames_from_commit_info(self, commit_info): + nestedList = [party.irc_nicknames for party in commit_info.responsible_parties() if party.irc_nicknames] + return reduce(lambda list, childList: list + childList, nestedList) + + def post_irc_warning(self, commit_info, builders): + irc_nicknames = sorted(self.responsible_nicknames_from_commit_info(commit_info)) + irc_prefix = ": " if irc_nicknames else "" + irc_message = "%s%s%s might have broken %s" % ( + ", ".join(irc_nicknames), + irc_prefix, + urls.view_revision_url(commit_info.revision()), + join_with_separators([builder.name() for builder in builders])) + + self._tool.irc().post(irc_message) + + def post_irc_summary(self, failure_map): + failing_tests = failure_map.failing_tests() + if not failing_tests: + return + test_list_limit = 5 + irc_message = "New failures: %s" % ", ".join(sorted(failing_tests)[:test_list_limit]) + failure_count = len(failing_tests) + if failure_count > test_list_limit: + irc_message += " (and %s more...)" % (failure_count - test_list_limit) + self._tool.irc().post(irc_message) + + def post_rollout_patch(self, svn_revision_list, rollout_reason): + # Ensure that svn revisions are numbers (and not options to + # create-rollout). + try: + svn_revisions = " ".join([str(int(revision)) for revision in svn_revision_list]) + except: + raise ScriptError(message="Invalid svn revision number \"%s\"." + % " ".join(svn_revision_list)) + + 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_revisions, + rollout_reason, + ]) + return parse_bug_id(output) + + def post_chromium_deps_roll(self, revision, revision_name): + args = [ + "post-chromium-deps-roll", + "--force-clean", + "--non-interactive", + "--parent-command=sheriff-bot", + ] + # revision can be None, but revision_name is always something meaningful. + args += [revision, revision_name] + output = self._sheriffbot.run_webkit_patch(args) + 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" % ( + urls.view_revision_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/Tools/Scripts/webkitpy/tool/bot/sheriff_unittest.py b/Tools/Scripts/webkitpy/tool/bot/sheriff_unittest.py new file mode 100644 index 000000000..690af1ffc --- /dev/null +++ b/Tools/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/Tools/Scripts/webkitpy/tool/bot/sheriffircbot.py b/Tools/Scripts/webkitpy/tool/bot/sheriffircbot.py new file mode 100644 index 000000000..7269c2ec5 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/sheriffircbot.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.tool.bot import irc_command +from webkitpy.tool.bot.queueengine import TerminateQueue +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 _parse_command_and_args(self, request): + tokenized_request = request.strip().split(" ") + 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 + return (command, args) + + def process_message(self, requester_nick, request): + command, args = self._parse_command_and_args(request) + try: + response = command().execute(requester_nick, args, self._tool, self._sheriff) + if response: + self._tool.irc().post(response) + except TerminateQueue: + raise + # This will catch everything else. SystemExit and KeyboardInterrupt are not subclasses of Exception, so we won't catch those. + except Exception, e: + self._tool.irc().post("Exception executing command: %s" % e) + + def process_pending_messages(self): + (messages, is_running) = self._message_queue.take_all() + for message in messages: + (nick, request) = message + self.process_message(nick, request) diff --git a/Tools/Scripts/webkitpy/tool/bot/sheriffircbot_unittest.py b/Tools/Scripts/webkitpy/tool/bot/sheriffircbot_unittest.py new file mode 100644 index 000000000..d9303808c --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/bot/sheriffircbot_unittest.py @@ -0,0 +1,161 @@ +# 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 import irc_command +from webkitpy.tool.bot.queueengine import TerminateQueue +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_parse_command_and_args(self): + tool = MockTool() + bot = SheriffIRCBot(tool, Sheriff(tool, MockSheriffBot())) + self.assertEqual(bot._parse_command_and_args(""), (irc_command.Eliza, [""])) + self.assertEqual(bot._parse_command_and_args(" "), (irc_command.Eliza, [""])) + self.assertEqual(bot._parse_command_and_args(" hi "), (irc_command.Hi, [])) + self.assertEqual(bot._parse_command_and_args(" hi there "), (irc_command.Hi, ["there"])) + + def test_exception_during_command(self): + tool = MockTool() + tool.ensure_irc_connected(None) + bot = SheriffIRCBot(tool, Sheriff(tool, MockSheriffBot())) + + class CommandWithException(object): + def execute(self, nick, args, tool, sheriff): + raise Exception("mock_exception") + + bot._parse_command_and_args = lambda request: (CommandWithException, []) + expected_stderr = 'MOCK: irc.post: Exception executing command: mock_exception\n' + OutputCapture().assert_outputs(self, bot.process_message, args=["mock_nick", "ignored message"], expected_stderr=expected_stderr) + + class CommandWithException(object): + def execute(self, nick, args, tool, sheriff): + raise KeyboardInterrupt() + + bot._parse_command_and_args = lambda request: (CommandWithException, []) + # KeyboardInterrupt and SystemExit are not subclasses of Exception and thus correctly will not be caught. + OutputCapture().assert_outputs(self, bot.process_message, args=["mock_nick", "ignored message"], expected_exception=KeyboardInterrupt) + + 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: create-bug, help, hi, last-green-revision, restart, roll-chromium-deps, rollout, whois\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_restart(self): + expected_stderr = "MOCK: irc.post: Restarting...\n" + OutputCapture().assert_outputs(self, run, args=["restart"], expected_stderr=expected_stderr, expected_exception=TerminateQueue) + + def test_rollout(self): + expected_stderr = "MOCK: irc.post: mock_nick: Preparing rollout for http://trac.webkit.org/changeset/21654...\nMOCK: irc.post: mock_nick, abarth, darin, eseidel: 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_revert(self): + expected_stderr = "MOCK: irc.post: mock_nick: Preparing rollout for http://trac.webkit.org/changeset/21654...\nMOCK: irc.post: mock_nick, abarth, darin, eseidel: Created rollout: http://example.com/36936\n" + OutputCapture().assert_outputs(self, run, args=["revert 21654 This patch broke the world"], expected_stderr=expected_stderr) + + def test_roll_chromium_deps(self): + expected_stderr = "MOCK: irc.post: mock_nick: Rolling Chromium DEPS to r21654\nMOCK: irc.post: mock_nick: Created DEPS roll: http://example.com/36936\n" + OutputCapture().assert_outputs(self, run, args=["roll-chromium-deps 21654"], expected_stderr=expected_stderr) + + def test_roll_chromium_deps_to_lkgr(self): + expected_stderr = "MOCK: irc.post: mock_nick: Rolling Chromium DEPS to last-known good revision\nMOCK: irc.post: mock_nick: Created DEPS roll: http://example.com/36936\n" + OutputCapture().assert_outputs(self, run, args=["roll-chromium-deps"], expected_stderr=expected_stderr) + + def test_multi_rollout(self): + expected_stderr = "MOCK: irc.post: mock_nick: Preparing rollout for http://trac.webkit.org/changeset/21654, http://trac.webkit.org/changeset/21655, and http://trac.webkit.org/changeset/21656...\nMOCK: irc.post: mock_nick, abarth, darin, eseidel: Created rollout: http://example.com/36936\n" + OutputCapture().assert_outputs(self, run, args=["rollout 21654 21655 21656 This 21654 patch broke the world"], expected_stderr=expected_stderr) + + def test_rollout_with_r_in_svn_revision(self): + expected_stderr = "MOCK: irc.post: mock_nick: Preparing rollout for http://trac.webkit.org/changeset/21654...\nMOCK: irc.post: mock_nick, abarth, darin, eseidel: 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_multi_rollout_with_r_in_svn_revision(self): + expected_stderr = "MOCK: irc.post: mock_nick: Preparing rollout for http://trac.webkit.org/changeset/21654, http://trac.webkit.org/changeset/21655, and http://trac.webkit.org/changeset/21656...\nMOCK: irc.post: mock_nick, abarth, darin, eseidel: Created rollout: http://example.com/36936\n" + OutputCapture().assert_outputs(self, run, args=["rollout r21654 21655 r21656 This r21654 patch broke the world"], expected_stderr=expected_stderr) + + def test_rollout_bananas(self): + expected_stderr = "MOCK: irc.post: mock_nick: Usage: rollout SVN_REVISION [SVN_REVISIONS] REASON\n" + OutputCapture().assert_outputs(self, run, args=["rollout bananas"], expected_stderr=expected_stderr) + + def test_rollout_invalidate_revision(self): + # When folks pass junk arguments, we should just spit the usage back at them. + expected_stderr = "MOCK: irc.post: mock_nick: Usage: rollout SVN_REVISION [SVN_REVISIONS] REASON\n" + OutputCapture().assert_outputs(self, run, + args=["rollout --component=Tools 21654"], + expected_stderr=expected_stderr) + + def test_rollout_invalidate_reason(self): + # FIXME: I'm slightly confused as to why this doesn't return the USAGE message. + expected_stderr = """MOCK: irc.post: mock_nick: Preparing rollout for http://trac.webkit.org/changeset/21654... +MOCK: irc.post: mock_nick, abarth, darin, eseidel: Failed to create rollout patch: +MOCK: irc.post: The rollout reason may not begin with - (\"-bad (Requested by mock_nick on #webkit).\"). +""" + OutputCapture().assert_outputs(self, run, + args=["rollout 21654 -bad"], + expected_stderr=expected_stderr) + + def test_multi_rollout_invalidate_reason(self): + expected_stderr = """MOCK: irc.post: mock_nick: Preparing rollout for http://trac.webkit.org/changeset/21654, http://trac.webkit.org/changeset/21655, and http://trac.webkit.org/changeset/21656... +MOCK: irc.post: mock_nick, abarth, darin, eseidel: Failed to create rollout patch: +MOCK: irc.post: The rollout reason may not begin with - (\"-bad (Requested by mock_nick on #webkit).\"). +""" + OutputCapture().assert_outputs(self, run, + args=["rollout " + "21654 21655 r21656 -bad"], + expected_stderr=expected_stderr) + + def test_rollout_no_reason(self): + expected_stderr = "MOCK: irc.post: mock_nick: Usage: rollout SVN_REVISION [SVN_REVISIONS] REASON\n" + OutputCapture().assert_outputs(self, run, args=["rollout 21654"], expected_stderr=expected_stderr) + + def test_multi_rollout_no_reason(self): + expected_stderr = "MOCK: irc.post: mock_nick: Usage: rollout SVN_REVISION [SVN_REVISIONS] REASON\n" + OutputCapture().assert_outputs(self, run, args=["rollout 21654 21655 r21656"], expected_stderr=expected_stderr) diff --git a/Tools/Scripts/webkitpy/tool/commands/__init__.py b/Tools/Scripts/webkitpy/tool/commands/__init__.py new file mode 100644 index 000000000..ef05b5ac4 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/__init__.py @@ -0,0 +1,22 @@ +# Required for Python to search this directory for module files + +from webkitpy.tool.commands.adduserstogroups import AddUsersToGroups +from webkitpy.tool.commands.analyzechangelog import AnalyzeChangeLog +from webkitpy.tool.commands.applywatchlistlocal import ApplyWatchListLocal +from webkitpy.tool.commands.bugfortest import BugForTest +from webkitpy.tool.commands.bugsearch import BugSearch +from webkitpy.tool.commands.download import * +from webkitpy.tool.commands.earlywarningsystem import * +from webkitpy.tool.commands.expectations import OptimizeExpectations +from webkitpy.tool.commands.findusers import FindUsers +from webkitpy.tool.commands.gardenomatic import GardenOMatic +from webkitpy.tool.commands.openbugs import OpenBugs +from webkitpy.tool.commands.prettydiff import PrettyDiff +from webkitpy.tool.commands.queries import * +from webkitpy.tool.commands.queues import * +from webkitpy.tool.commands.rebaseline import Rebaseline +from webkitpy.tool.commands.rebaselineserver import RebaselineServer +from webkitpy.tool.commands.roll import * +from webkitpy.tool.commands.sheriffbot import * +from webkitpy.tool.commands.upload import * +from webkitpy.tool.commands.suggestnominations import * diff --git a/Tools/Scripts/webkitpy/tool/commands/abstractlocalservercommand.py b/Tools/Scripts/webkitpy/tool/commands/abstractlocalservercommand.py new file mode 100644 index 000000000..269cf25cf --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/abstractlocalservercommand.py @@ -0,0 +1,55 @@ +# Copyright (C) 2011 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: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# 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 +import threading + +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +class AbstractLocalServerCommand(AbstractDeclarativeCommand): + server = None + launch_path = "/" + + def __init__(self): + options = [ + make_option("--httpd-port", action="store", type="int", default=8127, help="Port to use for the HTTP server"), + ] + AbstractDeclarativeCommand.__init__(self, options=options) + + def _prepare_config(self, options, args, tool): + return None + + def execute(self, options, args, tool): + config = self._prepare_config(options, args, tool) + + server_url = "http://localhost:%d%s" % (options.httpd_port, self.launch_path) + print "Starting server at %s" % server_url + print "Use the 'Exit' link in the UI, %squitquitquit or Ctrl-C to stop" % server_url + + # FIXME: This seems racy. + threading.Timer(0.1, lambda: self._tool.user.open_url(server_url)).start() + + httpd = self.server(httpd_port=options.httpd_port, config=config) + httpd.serve_forever() diff --git a/Tools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py b/Tools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py new file mode 100644 index 000000000..fd1089056 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py @@ -0,0 +1,51 @@ +# 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.system.deprecated_logging import log +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): + try: + state = self._prepare_state(options, args, tool) + except ScriptError, e: + log(e.message_with_output()) + exit(e.exit_code or 2) + + self._sequence.run_and_handle_errors(tool, options, state) diff --git a/Tools/Scripts/webkitpy/tool/commands/adduserstogroups.py b/Tools/Scripts/webkitpy/tool/commands/adduserstogroups.py new file mode 100644 index 000000000..22869584d --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/adduserstogroups.py @@ -0,0 +1,65 @@ +# Copyright (c) 2011 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.multicommandtool import AbstractDeclarativeCommand + + +class AddUsersToGroups(AbstractDeclarativeCommand): + name = "add-users-to-groups" + help_text = "Add users matching subtring to specified groups" + + # This probably belongs in bugzilla.py + known_groups = ['canconfirm', 'editbugs'] + + def execute(self, options, args, tool): + search_string = args[0] + # FIXME: We could allow users to specify groups on the command line. + list_title = 'Add users matching "%s" which groups?' % search_string + # FIXME: Need a way to specify that "none" is not allowed. + # FIXME: We could lookup what groups the current user is able to grant from bugzilla. + groups = tool.user.prompt_with_list(list_title, self.known_groups, can_choose_multiple=True) + if not groups: + print "No groups specified." + return + + login_userid_pairs = tool.bugs.queries.fetch_login_userid_pairs_matching_substring(search_string) + if not login_userid_pairs: + print "No users found matching '%s'" % search_string + return + + print "Found %s users matching %s:" % (len(login_userid_pairs), search_string) + for (login, user_id) in login_userid_pairs: + print "%s (%s)" % (login, user_id) + + confirm_message = "Are you sure you want add %s users to groups %s? (This action cannot be undone using webkit-patch.)" % (len(login_userid_pairs), groups) + if not tool.user.confirm(confirm_message): + return + + for (login, user_id) in login_userid_pairs: + print "Adding %s to %s" % (login, groups) + tool.bugs.add_user_to_groups(user_id, groups) diff --git a/Tools/Scripts/webkitpy/tool/commands/analyzechangelog.py b/Tools/Scripts/webkitpy/tool/commands/analyzechangelog.py new file mode 100644 index 000000000..2fe34ade5 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/analyzechangelog.py @@ -0,0 +1,208 @@ +# Copyright (c) 2011 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 __future__ import with_statement + +import json +import re +import time + +from webkitpy.common.checkout.scm.detection import SCMDetector +from webkitpy.common.checkout.changelog import ChangeLog +from webkitpy.common.config.contributionareas import ContributionAreas +from webkitpy.common.system.filesystem import FileSystem +from webkitpy.common.system.executive import Executive +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand +from webkitpy.tool import steps + + +class AnalyzeChangeLog(AbstractDeclarativeCommand): + name = "analyze-changelog" + help_text = "Experimental command for analyzing change logs." + long_help = "This command parses changelogs in a specified directory and summarizes the result as JSON files." + + def __init__(self): + options = [ + steps.Options.changelog_count, + ] + AbstractDeclarativeCommand.__init__(self, options=options) + + @staticmethod + def _enumerate_changelogs(filesystem, dirname, changelog_count): + changelogs = [filesystem.join(dirname, filename) for filename in filesystem.listdir(dirname) if re.match('^ChangeLog(-(\d{4}-\d{2}-\d{2}))?$', filename)] + # Make sure ChangeLog shows up before ChangeLog-2011-01-01 + changelogs = sorted(changelogs, key=lambda filename: filename + 'X', reverse=True) + return changelogs[:changelog_count] + + @staticmethod + def _generate_jsons(filesystem, jsons, output_dir): + for filename in jsons: + print ' Generating', filename + filesystem.write_text_file(filesystem.join(output_dir, filename), json.dumps(jsons[filename], indent=2)) + + def execute(self, options, args, tool): + filesystem = self._tool.filesystem + if len(args) < 1 or not filesystem.exists(args[0]): + print "Need the directory name to look for changelog as the first argument" + return + changelog_dir = filesystem.abspath(args[0]) + + if len(args) < 2 or not filesystem.exists(args[1]): + print "Need the output directory name as the second argument" + return + output_dir = args[1] + + startTime = time.time() + + print 'Enumerating ChangeLog files...' + changelogs = AnalyzeChangeLog._enumerate_changelogs(filesystem, changelog_dir, options.changelog_count) + + analyzer = ChangeLogAnalyzer(tool, changelogs) + analyzer.analyze() + + print 'Generating json files...' + json_files = { + 'summary.json': analyzer.summary(), + 'contributors.json': analyzer.contributors_statistics(), + 'areas.json': analyzer.areas_statistics(), + } + AnalyzeChangeLog._generate_jsons(filesystem, json_files, output_dir) + commands_dir = filesystem.dirname(filesystem.path_to_module(self.__module__)) + print commands_dir + filesystem.copyfile(filesystem.join(commands_dir, 'data/summary.html'), filesystem.join(output_dir, 'summary.html')) + + tick = time.time() - startTime + print 'Finished in %02dm:%02ds' % (int(tick / 60), int(tick % 60)) + + +class ChangeLogAnalyzer(object): + def __init__(self, host, changelog_paths): + self._changelog_paths = changelog_paths + self._filesystem = host.filesystem + self._contribution_areas = ContributionAreas(host.filesystem) + self._scm = host.scm() + self._parsed_revisions = {} + + self._contributors_statistics = {} + self._areas_statistics = dict([(area, {'reviewed': 0, 'unreviewed': 0, 'contributors': {}}) for area in self._contribution_areas.names()]) + self._summary = {'reviewed': 0, 'unreviewed': 0} + + self._longest_filename = max([len(path) - len(self._scm.checkout_root) for path in changelog_paths]) + self._filename = '' + self._length_of_previous_output = 0 + + def contributors_statistics(self): + return self._contributors_statistics + + def areas_statistics(self): + return self._areas_statistics + + def summary(self): + return self._summary + + def _print_status(self, status): + if self._length_of_previous_output: + print "\r" + " " * self._length_of_previous_output, + new_output = ('%' + str(self._longest_filename) + 's: %s') % (self._filename, status) + print "\r" + new_output, + self._length_of_previous_output = len(new_output) + + def _set_filename(self, filename): + if self._filename: + print + self._filename = filename + + def analyze(self): + for path in self._changelog_paths: + self._set_filename(self._filesystem.relpath(path, self._scm.checkout_root)) + with self._filesystem.open_text_file_for_reading(path) as changelog: + self._print_status('Parsing entries...') + number_of_parsed_entries = self._analyze_entries(ChangeLog.parse_entries_from_file(changelog), path) + self._print_status('Done (%d entries)' % number_of_parsed_entries) + print + self._summary['contributors'] = len(self._contributors_statistics) + self._summary['contributors_with_reviews'] = sum([1 for contributor in self._contributors_statistics.values() if contributor['reviews']['total']]) + self._summary['contributors_without_reviews'] = self._summary['contributors'] - self._summary['contributors_with_reviews'] + + def _collect_statistics_for_contributor_area(self, area, contributor, contribution_type, reviewed): + area_contributors = self._areas_statistics[area]['contributors'] + if contributor not in area_contributors: + area_contributors[contributor] = {'reviews': 0, 'reviewed': 0, 'unreviewed': 0} + if contribution_type == 'patches': + contribution_type = 'reviewed' if reviewed else 'unreviewed' + area_contributors[contributor][contribution_type] += 1 + + def _collect_statistics_for_contributor(self, contributor, contribution_type, areas, touched_files, reviewed): + if contributor not in self._contributors_statistics: + self._contributors_statistics[contributor] = { + 'reviews': {'total': 0, 'areas': {}, 'files': {}}, + 'patches': {'reviewed': 0, 'unreviewed': 0, 'areas': {}, 'files': {}}} + statistics = self._contributors_statistics[contributor][contribution_type] + + if contribution_type == 'reviews': + statistics['total'] += 1 + elif reviewed: + statistics['reviewed'] += 1 + else: + statistics['unreviewed'] += 1 + + for area in areas: + self._increment_dictionary_value(statistics['areas'], area) + self._collect_statistics_for_contributor_area(area, contributor, contribution_type, reviewed) + for touchedfile in touched_files: + self._increment_dictionary_value(statistics['files'], touchedfile) + + def _increment_dictionary_value(self, dictionary, key): + dictionary[key] = dictionary.get(key, 0) + 1 + + def _analyze_entries(self, entries, changelog_path): + dirname = self._filesystem.dirname(changelog_path) + for i, entry in enumerate(entries): + self._print_status('(%s) entries' % i) + assert(entry.authors()) + + touchedfiles_for_entry = [self._filesystem.relpath(self._filesystem.join(dirname, name), self._scm.checkout_root) for name in entry.touched_files()] + areas_for_entry = self._contribution_areas.areas_for_touched_files(touchedfiles_for_entry) + authors_for_entry = entry.authors() + reviewers_for_entry = entry.reviewers() + + for reviewer in reviewers_for_entry: + self._collect_statistics_for_contributor(reviewer.full_name, 'reviews', areas_for_entry, touchedfiles_for_entry, reviewed=True) + + for author in authors_for_entry: + self._collect_statistics_for_contributor(author['name'], 'patches', areas_for_entry, touchedfiles_for_entry, + reviewed=bool(reviewers_for_entry)) + + for area in areas_for_entry: + self._areas_statistics[area]['reviewed' if reviewers_for_entry else 'unreviewed'] += 1 + + self._summary['reviewed' if reviewers_for_entry else 'unreviewed'] += 1 + + i += 1 + self._print_status('(%s) entries' % i) + return i diff --git a/Tools/Scripts/webkitpy/tool/commands/analyzechangelog_unittest.py b/Tools/Scripts/webkitpy/tool/commands/analyzechangelog_unittest.py new file mode 100644 index 000000000..661d2d85f --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/analyzechangelog_unittest.py @@ -0,0 +1,185 @@ +# Cpyright (c) 2011 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 json +import sys +from webkitpy.common.config.contributionareas import ContributionAreas +from webkitpy.common.host_mock import MockHost +from webkitpy.common.system.filesystem_mock import MockFileSystem +from webkitpy.common.system.executive import Executive +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.tool.commands.analyzechangelog import AnalyzeChangeLog +from webkitpy.tool.commands.analyzechangelog import ChangeLogAnalyzer +from webkitpy.tool.commands.commandtest import CommandsTest +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +class AnalyzeChangeLogTest(CommandsTest): + def test_enumerate_enumerate_changelogs(self): + filesystem = MockFileSystem({ + 'foo/ChangeLog': '', + 'foo/ChangeLog-2010-06-23': '', + 'foo/ChangeLog-2010-12-31': '', + 'foo/ChangeLog-x': '', + 'foo/ChangeLog-2011-01-01': '', + }) + changelogs = AnalyzeChangeLog._enumerate_changelogs(filesystem, 'foo/', None) + self.assertEqual(changelogs, ['foo/ChangeLog', 'foo/ChangeLog-2011-01-01', 'foo/ChangeLog-2010-12-31', 'foo/ChangeLog-2010-06-23']) + + changelogs = AnalyzeChangeLog._enumerate_changelogs(filesystem, 'foo/', 2) + self.assertEqual(changelogs, ['foo/ChangeLog', 'foo/ChangeLog-2011-01-01']) + + def test_generate_jsons(self): + filesystem = MockFileSystem() + test_json = {'array.json': [1, 2, 3, {'key': 'value'}], 'dictionary.json': {'somekey': 'somevalue', 'array': [4, 5]}} + + capture = OutputCapture() + capture.capture_output() + + AnalyzeChangeLog._generate_jsons(filesystem, test_json, 'bar') + self.assertEqual(set(filesystem.files.keys()), set(['bar/array.json', 'bar/dictionary.json'])) + + capture.restore_output() + + self.assertEqual(json.loads(filesystem.files['bar/array.json']), test_json['array.json']) + self.assertEqual(json.loads(filesystem.files['bar/dictionary.json']), test_json['dictionary.json']) + + +class ChangeLogAnalyzerTest(CommandsTest): + def test_analyze_one_changelog(self): + host = MockHost() + host.filesystem.files['mock-checkout/foo/ChangeLog'] = u"""2011-11-17 Mark Rowe <mrowe@apple.com> + + <http://webkit.org/b/72646> Disable deprecation warnings around code where we cannot easily + switch away from the deprecated APIs. + + Reviewed by Sam Weinig. + + * platform/mac/WebCoreNSStringExtras.mm: + * platform/network/cf/SocketStreamHandleCFNet.cpp: + (WebCore::SocketStreamHandle::reportErrorToClient): + +2011-11-19 Kevin Ollivier <kevino@theolliviers.com> + + [wx] C++ bindings build fix for move of array classes to WTF. + + * bindings/scripts/CodeGeneratorCPP.pm: + (GetCPPTypeGetter): + (GetNamespaceForClass): + (GenerateHeader): + (GenerateImplementation): + +2011-10-27 Philippe Normand <pnormand@igalia.com> and Zan Dobersek <zandobersek@gmail.com> + + [GStreamer] WebAudio AudioFileReader implementation + https://bugs.webkit.org/show_bug.cgi?id=69834 + + Reviewed by Martin Robinson. + + Basic FileReader implementation, supporting one or 2 audio + channels. An empty AudioDestination is also provided, its complete + implementation is handled in bug 69835. + + * GNUmakefile.am: + * GNUmakefile.list.am: + * platform/audio/gstreamer/AudioDestinationGStreamer.cpp: Added. + (WebCore::AudioDestination::create): + (WebCore::AudioDestination::hardwareSampleRate): + (WebCore::AudioDestinationGStreamer::AudioDestinationGStreamer): + (WebCore::AudioDestinationGStreamer::~AudioDestinationGStreamer): + (WebCore::AudioDestinationGStreamer::start): + (WebCore::AudioDestinationGStreamer::stop): + * platform/audio/gstreamer/AudioDestinationGStreamer.h: Added. + (WebCore::AudioDestinationGStreamer::isPlaying): + (WebCore::AudioDestinationGStreamer::sampleRate): + (WebCore::AudioDestinationGStreamer::sourceProvider): + * platform/audio/gstreamer/AudioFileReaderGStreamer.cpp: Added. + (WebCore::getGStreamerAudioCaps): + (WebCore::getFloatFromByteReader): + (WebCore::copyGstreamerBuffersToAudioChannel): + (WebCore::onAppsinkNewBufferCallback): + (WebCore::messageCallback): + (WebCore::onGStreamerDeinterleavePadAddedCallback): + (WebCore::onGStreamerDeinterleaveReadyCallback): + (WebCore::onGStreamerDecodebinPadAddedCallback): + (WebCore::AudioFileReader::AudioFileReader): + (WebCore::AudioFileReader::~AudioFileReader): + (WebCore::AudioFileReader::handleBuffer): + (WebCore::AudioFileReader::handleMessage): + (WebCore::AudioFileReader::handleNewDeinterleavePad): + (WebCore::AudioFileReader::deinterleavePadsConfigured): + (WebCore::AudioFileReader::plugDeinterleave): + (WebCore::AudioFileReader::createBus): + (WebCore::createBusFromAudioFile): + (WebCore::createBusFromInMemoryAudioFile): + * platform/audio/gtk/AudioBusGtk.cpp: Added. + (WebCore::AudioBus::loadPlatformResource): +""" + + capture = OutputCapture() + capture.capture_output() + + analyzer = ChangeLogAnalyzer(host, ['mock-checkout/foo/ChangeLog']) + analyzer.analyze() + + capture.restore_output() + + self.assertEqual(analyzer.summary(), + {'reviewed': 2, 'unreviewed': 1, 'contributors': 6, 'contributors_with_reviews': 2, 'contributors_without_reviews': 4}) + + self.assertEqual(set(analyzer.contributors_statistics().keys()), + set(['Sam Weinig', u'Mark Rowe', u'Kevin Ollivier', 'Martin Robinson', u'Philippe Normand', u'Zan Dobersek'])) + + self.assertEqual(analyzer.contributors_statistics()['Sam Weinig'], + {'reviews': {'files': {u'foo/platform/mac/WebCoreNSStringExtras.mm': 1, u'foo/platform/network/cf/SocketStreamHandleCFNet.cpp': 1}, + 'total': 1, 'areas': {'Network': 1}}, 'patches': {'files': {}, 'areas': {}, 'unreviewed': 0, 'reviewed': 0}}) + self.assertEqual(analyzer.contributors_statistics()[u'Mark Rowe'], + {'reviews': {'files': {}, 'total': 0, 'areas': {}}, 'patches': {'files': {u'foo/platform/mac/WebCoreNSStringExtras.mm': 1, + u'foo/platform/network/cf/SocketStreamHandleCFNet.cpp': 1}, 'areas': {'Network': 1}, 'unreviewed': 0, 'reviewed': 1}}) + self.assertEqual(analyzer.contributors_statistics()[u'Kevin Ollivier'], + {'reviews': {'files': {}, 'total': 0, 'areas': {}}, 'patches': {'files': {u'foo/bindings/scripts/CodeGeneratorCPP.pm': 1}, + 'areas': {'Bindings': 1}, 'unreviewed': 1, 'reviewed': 0}}) + + files_for_audio_patch = {u'foo/GNUmakefile.am': 1, u'foo/GNUmakefile.list.am': 1, 'foo/platform/audio/gstreamer/AudioDestinationGStreamer.cpp': 1, + 'foo/platform/audio/gstreamer/AudioDestinationGStreamer.h': 1, 'foo/platform/audio/gstreamer/AudioFileReaderGStreamer.cpp': 1, + 'foo/platform/audio/gtk/AudioBusGtk.cpp': 1} + author_expectation_for_audio_patch = {'reviews': {'files': {}, 'total': 0, 'areas': {}}, + 'patches': {'files': files_for_audio_patch, 'areas': {'The WebKitGTK+ Port': 1}, 'unreviewed': 0, 'reviewed': 1}} + self.assertEqual(analyzer.contributors_statistics()[u'Martin Robinson'], + {'reviews': {'files': files_for_audio_patch, 'total': 1, 'areas': {'The WebKitGTK+ Port': 1}}, + 'patches': {'files': {}, 'areas': {}, 'unreviewed': 0, 'reviewed': 0}}) + self.assertEqual(analyzer.contributors_statistics()[u'Philippe Normand'], author_expectation_for_audio_patch) + self.assertEqual(analyzer.contributors_statistics()[u'Zan Dobersek'], author_expectation_for_audio_patch) + + areas_statistics = analyzer.areas_statistics() + areas_with_patches = [area for area in areas_statistics if areas_statistics[area]['reviewed'] or areas_statistics[area]['unreviewed']] + self.assertEqual(set(areas_with_patches), set(['Bindings', 'Network', 'The WebKitGTK+ Port'])) + self.assertEqual(areas_statistics['Bindings'], {'unreviewed': 1, 'reviewed': 0, 'contributors': + {u'Kevin Ollivier': {'reviews': 0, 'unreviewed': 1, 'reviewed': 0}}}) + self.assertEqual(areas_statistics['Network'], {'unreviewed': 0, 'reviewed': 1, 'contributors': + {'Sam Weinig': {'reviews': 1, 'unreviewed': 0, 'reviewed': 0}, u'Mark Rowe': {'reviews': 0, 'unreviewed': 0, 'reviewed': 1}}}) diff --git a/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal.py b/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal.py new file mode 100644 index 000000000..6735d4842 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal.py @@ -0,0 +1,50 @@ +# Copyright (c) 2011 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 +from webkitpy.tool import steps + + +class ApplyWatchListLocal(AbstractSequencedCommand): + name = "apply-watchlist-local" + help_text = "Applies the watchlist to local changes" + argument_names = "[BUGID]" + steps = [ + steps.ApplyWatchList, + ] + long_help = """"Applies the watchlist to local changes. +The results is logged if a bug is no given. This may be used to try out a watchlist change.""" + + def _prepare_state(self, options, args, tool): + if len(args) > 1: + raise Exception("Too many arguments given: %s" % (' '.join(args))) + if not args: + return {} + return { + "bug_id": args[0], + } diff --git a/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal_unittest.py b/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal_unittest.py new file mode 100644 index 000000000..91818d1c2 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/applywatchlistlocal_unittest.py @@ -0,0 +1,50 @@ +# Copyright (c) 2011 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.applywatchlistlocal import ApplyWatchListLocal + + +class ApplyWatchListLocalTest(CommandsTest): + def test_args_parsing(self): + expected_stderr = 'MockWatchList: determine_cc_and_messages\n' + self.assert_execute_outputs(ApplyWatchListLocal(), [''], expected_stderr=expected_stderr) + + def test_args_parsing_with_bug(self): + expected_stderr = """MockWatchList: determine_cc_and_messages +MOCK bug comment: bug_id=50002, cc=set(['eric@webkit.org', 'levin@chromium.org', 'abarth@webkit.org']) +--- Begin comment --- +Message1. + +Message2. +--- End comment ---\n\n""" + self.assert_execute_outputs(ApplyWatchListLocal(), ['50002'], expected_stderr=expected_stderr) + + def test_args_parsing_with_two_bugs(self): + self._assertRaisesRegexp(Exception, 'Too many arguments given: 1234 5678', self.assert_execute_outputs, ApplyWatchListLocal(), ['1234', '5678']) diff --git a/Tools/Scripts/webkitpy/tool/commands/bugfortest.py b/Tools/Scripts/webkitpy/tool/commands/bugfortest.py new file mode 100644 index 000000000..36aa6b5f1 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/bugfortest.py @@ -0,0 +1,48 @@ +# 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.multicommandtool import AbstractDeclarativeCommand +from webkitpy.tool.bot.flakytestreporter import FlakyTestReporter + + +# This is mostly a command for testing FlakyTestReporter, however +# it could be easily expanded to auto-create bugs, etc. if another +# command outside of webkitpy wanted to use it. +class BugForTest(AbstractDeclarativeCommand): + name = "bug-for-test" + help_text = "Finds the bugzilla bug for a given test" + + def execute(self, options, args, tool): + reporter = FlakyTestReporter(tool, "webkitpy") + search_string = args[0] + bug = reporter._lookup_bug_for_flaky_test(search_string) + if bug: + bug = reporter._follow_duplicate_chain(bug) + print "%5s %s" % (bug.id(), bug.title()) + else: + print "No bugs found matching '%s'" % search_string diff --git a/Tools/Scripts/webkitpy/tool/commands/bugsearch.py b/Tools/Scripts/webkitpy/tool/commands/bugsearch.py new file mode 100644 index 000000000..5cbc1a044 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/bugsearch.py @@ -0,0 +1,42 @@ +# 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.multicommandtool import AbstractDeclarativeCommand + + +class BugSearch(AbstractDeclarativeCommand): + name = "bug-search" + help_text = "List bugs matching a query" + + def execute(self, options, args, tool): + search_string = args[0] + bugs = tool.bugs.queries.fetch_bugs_matching_quicksearch(search_string) + for bug in bugs: + print "%5s %s" % (bug.id(), bug.title()) + if not bugs: + print "No bugs found matching '%s'" % search_string diff --git a/Tools/Scripts/webkitpy/tool/commands/commandtest.py b/Tools/Scripts/webkitpy/tool/commands/commandtest.py new file mode 100644 index 000000000..eea0a6156 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/commandtest.py @@ -0,0 +1,48 @@ +# 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.system.outputcapture import OutputCapture +from webkitpy.common.webkitunittest import TestCase +from webkitpy.tool.mocktool import MockOptions, MockTool + + +class CommandsTest(TestCase): + def assert_execute_outputs(self, command, args=[], expected_stdout="", expected_stderr="", expected_exception=None, options=MockOptions(), tool=MockTool()): + options.blocks = None + options.cc = 'MOCK cc' + options.component = 'MOCK component' + options.confirm = True + options.email = 'MOCK email' + options.git_commit = 'MOCK git commit' + options.obsolete_patches = True + options.open_bug = True + options.port = 'MOCK port' + options.quiet = True + options.reviewer = 'MOCK reviewer' + command.bind_to_tool(tool) + OutputCapture().assert_outputs(self, command.execute, [options, args, tool], expected_stdout=expected_stdout, expected_stderr=expected_stderr, expected_exception=expected_exception) diff --git a/Tools/Scripts/webkitpy/tool/commands/data/summary.html b/Tools/Scripts/webkitpy/tool/commands/data/summary.html new file mode 100644 index 000000000..abf80d84f --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/data/summary.html @@ -0,0 +1,455 @@ +<!DOCTYPE html> +<html> +<head> +<title>ChangeLog Analysis</title> +<style type="text/css"> + +body { + font-family: 'Helvetica' 'Segoe UI Light' sans-serif; + font-weight: 200; + padding: 20px; + min-width: 1200px; +} + +* { + padding: 0px; + margin: 0px; + border: 0px; +} + +h1, h2, h3 { + font-weight: 200; +} + +h1 { + margin: 0 0 1em 0; +} + +h2 { + font-size: 1.2em; + text-align: center; + margin-bottom: 1em; +} + +h3 { + font-size: 1em; +} + +.view { + margin: 0px; + width: 600px; + float: left; +} + +.graph-container p { + width: 200px; + text-align: right; + margin: 20px 0 20px 0; + padding: 5px; + border-right: solid 1px black; +} + +.graph-container table { + width: 100%; +} + +.graph-container table, .graph-container td { + border-collapse: collapse; + border: none; +} + +.graph-container td { + padding: 5px; + vertical-align: center; +} + +.graph-container td:first-child { + width: 200px; + text-align: right; + border-right: solid 1px black; +} + +.graph-container .selected { + background: #eee; +} + +#reviewers .selected td:first-child { + border-radius: 10px 0px 0px 10px; +} + +#areas .selected td:last-child { + border-radius: 0px 10px 10px 0px; +} + +.graph-container .bar { + display: inline-block; + min-height: 1em; + background: #9f6; + margin-right: 0.4ex; +} + +.graph-container .reviewed-patches { + background: #3cf; + margin-right: 1px; +} + +.graph-container .unreviewed-patches { + background: #f99; +} + +.constrained { + background: #eee; + border-radius: 10px; +} + +.constrained .vertical-bar { + border-right: solid 1px #eee; +} + +#header { + border-spacing: 5px; +} + +#header section { + display: table-cell; + width: 200px; + vertical-align: top; + border: solid 2px #ccc; + border-collapse: collapse; + padding: 5px; + font-size: 0.8em; +} + +#header dt { + float: left; +} + +#header dt:after { + content: ': '; +} + +#header .legend { + width: 600px; +} + +.legend .bar { + width: 15ex; + padding: 2px; +} + +.legend .reviews { + width: 25ex; +} + +.legend td:first-child { + width: 18ex; +} + +</style> +</head> +<body> +<h1>ChangeLog Analysis</h1> + +<section id="header"> +<section id="summary"> +<h2>Summary</h2> +</section> + +<section class="legend"> +<h2>Legend</h2> +<div class="graph-container"> +<table> +<tbody> +<tr><td>Contributor's name</td> +<td><span class="bar reviews">Reviews</span> <span class="value-container">(# of reviews)</span><br> +<span class="bar reviewed-patches">Reviewed</span><span class="bar unreviewed-patches">Unreviewed</span> +<span class="value-container">(# of reviewed):(# of unreviewed)</span></td></tr> +</tbody> +</table> +</div> +</section> +</section> + +<section id="contributors" class="view"> +<h2 id="contributors-title">Contributors</h2> +<div class="graph-container"></div> +</section> + +<section id="areas" class="view"> +<h2 id="areas-title">Areas of contributions</h2> +<div class="graph-container"></div> +</section> + +<script> + +// Naive implementation of element extensions discussed on public-webapps + +if (!Element.prototype.append) { + Element.prototype.append = function () { + for (var i = 0; i < arguments.length; i++) { + // FIXME: Take care of other node types + if (arguments[i] instanceof Element || arguments[i] instanceof CharacterData) + this.appendChild(arguments[i]); + else + this.appendChild(document.createTextNode(arguments[i])); + } + return this; + } +} + +if (!Node.prototype.remove) { + Node.prototype.remove = function () { + this.parentNode.removeChild(this); + return this; + } +} + +if (!Element.create) { + Element.create = function () { + if (arguments.length < 1) + return null; + var element = document.createElement(arguments[0]); + if (arguments.length == 1) + return element; + + // FIXME: the second argument can be content or IDL attributes + var attributes = arguments[1]; + for (attribute in attributes) + element.setAttribute(attribute, attributes[attribute]); + + if (arguments.length >= 3) + element.append.apply(element, arguments[2]); + + return element; + } +} + +if (!Node.prototype.removeAllChildren) { + Node.prototype.removeAllChildren = function () { + while (this.firstChild) + this.firstChild.remove(); + return this; + } +} + +Element.prototype.removeClassNameFromAllElements = function (className) { + var elements = this.getElementsByClassName(className); + for (var i = 0; i < elements.length; i++) + elements[i].classList.remove(className); +} + +function getJSON(url, callback) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.onreadystatechange = function () { + if (this.readyState == 4) + callback(JSON.parse(xhr.responseText)); + } + xhr.send(); +} + +function GraphView(container) { + this._container = container; + this._defaultData = null; +} + +GraphView.prototype.setData = function(data, constrained) { + if (constrained) + this._container.classList.add('constrained'); + else + this._container.classList.remove('constrained'); + this._clearGraph(); + this._constructGraph(data); +} + +GraphView.prototype.setDefaultData = function(data) { + this._defaultData = data; + this.setData(data); +} + +GraphView.prototype.reset = function () { + this.setMarginTop(); + this.setData(this._defaultData); +} + +GraphView.prototype.isConstrained = function () { return this._container.classList.contains('constrained'); } + +GraphView.prototype.targetRow = function (node) { + var target = null; + + while (node && node != this._container) { + if (node.localName == 'tr') + target = node; + node = node.parentNode; + } + + return node && target; +} + +GraphView.prototype.selectRow = function (row) { + this._container.removeClassNameFromAllElements('selected'); + row.classList.add('selected'); +} + +GraphView.prototype.setMarginTop = function (y) { this._container.style.marginTop = y ? y + 'px' : null; } +GraphView.prototype._graphContainer = function () { return this._container.getElementsByClassName('graph-container')[0]; } +GraphView.prototype._clearGraph = function () { return this._graphContainer().removeAllChildren(); } + +GraphView.prototype._numberOfPatches = function (dataItem) { + return dataItem.numberOfReviewedPatches + (dataItem.numberOfUnreviewedPatches !== undefined ? dataItem.numberOfUnreviewedPatches : 0); +} + +GraphView.prototype._maximumValue = function (labels, data) { + var numberOfPatches = this._numberOfPatches; + return Math.max.apply(null, labels.map(function (label) { + return Math.max(numberOfPatches(data[label]), data[label].numberOfReviews !== undefined ? data[label].numberOfReviews : 0); + })); +} + +GraphView.prototype._sortLabelsByNumberOfReviwsAndReviewedPatches = function(data) { + var labels = Object.keys(data); + if (!labels.length) + return null; + var numberOfPatches = this._numberOfPatches; + var computeValue = function (dataItem) { + return numberOfPatches(dataItem) + (dataItem.numberOfReviews !== undefined ? dataItem.numberOfReviews : 0); + } + labels.sort(function (a, b) { return computeValue(data[b]) - computeValue(data[a]); }); + return labels; +} + +GraphView.prototype._constructGraph = function (data) { + var element = this._graphContainer(); + var labels = this._sortLabelsByNumberOfReviwsAndReviewedPatches(data); + if (!labels) { + element.append(Element.create('p', {}, ['None'])); + return; + } + + var maxValue = this._maximumValue(labels, data); + var computeStyleForBar = function (value) { return 'width:' + (value * 85.0 / maxValue) + '%' } + + var table = Element.create('table', {}, [Element.create('tbody')]); + for (var i = 0; i < labels.length; i++) { + var label = labels[i]; + var item = data[label]; + var row = Element.create('tr', {}, [Element.create('td', {}, [label]), Element.create('td', {})]); + var valueCell = row.lastChild; + + if (item.numberOfReviews != undefined) { + valueCell.append( + Element.create('span', {'class': 'bar reviews', 'style': computeStyleForBar(item.numberOfReviews) }), + Element.create('span', {'class': 'value-container'}, [item.numberOfReviews]), + Element.create('br') + ); + } + + valueCell.append(Element.create('span', {'class': 'bar reviewed-patches', 'style': computeStyleForBar(item.numberOfReviewedPatches) })); + if (item.numberOfUnreviewedPatches !== undefined) + valueCell.append(Element.create('span', {'class': 'bar unreviewed-patches', 'style': computeStyleForBar(item.numberOfUnreviewedPatches) })); + + valueCell.append(Element.create('span', {'class': 'value-container'}, + [item.numberOfReviewedPatches + (item.numberOfUnreviewedPatches !== undefined ? ':' + item.numberOfUnreviewedPatches : '')])); + + table.firstChild.append(row); + row.label = label; + row.data = item; + } + element.append(table); +} + +var contributorsView = new GraphView(document.querySelector('#contributors')); +var areasView = new GraphView(document.querySelector('#areas')); + +getJSON('summary.json', + function (summary) { + var summaryContainer = document.querySelector('#summary'); + summaryContainer.append(Element.create('dl', {}, [ + Element.create('dt', {}, ['Total entries (reviewed)']), + Element.create('dd', {}, [(summary['reviewed'] + summary['unreviewed']) + ' (' + summary['reviewed'] + ')']), + Element.create('dt', {}, ['Total contributors']), + Element.create('dd', {}, [summary['contributors']]), + Element.create('dt', {}, ['Contributors who reviewed']), + Element.create('dd', {}, [summary['contributors_with_reviews']]), + ])); + }); + +getJSON('contributors.json', + function (contributors) { + for (var contributor in contributors) { + contributor = contributors[contributor]; + contributor.numberOfReviews = contributor.reviews ? contributor.reviews.total : 0; + contributor.numberOfReviewedPatches = contributor.patches ? contributor.patches.reviewed : 0; + contributor.numberOfUnreviewedPatches = contributor.patches ? contributor.patches.unreviewed : 0; + } + contributorsView.setDefaultData(contributors); + }); + +getJSON('areas.json', + function (areas) { + for (var area in areas) { + areas[area].numberOfReviewedPatches = areas[area].reviewed; + areas[area].numberOfUnreviewedPatches = areas[area].unreviewed; + } + areasView.setDefaultData(areas); + }); + +function contributorAreas(contributorData) { + var areas = new Object; + for (var area in contributorData.reviews.areas) { + if (!areas[area]) + areas[area] = {'numberOfReviewedPatches': 0}; + areas[area].numberOfReviews = contributorData.reviews.areas[area]; + } + for (var area in contributorData.patches.areas) { + if (!areas[area]) + areas[area] = {'numberOfReviews': 0}; + areas[area].numberOfReviewedPatches = contributorData.patches.areas[area]; + } + return areas; +} + +function areaContributors(areaData) { + var contributors = areaData['contributors']; + for (var contributor in contributors) { + contributor = contributors[contributor]; + contributor.numberOfReviews = contributor.reviews; + contributor.numberOfReviewedPatches = contributor.reviewed; + contributor.numberOfUnreviewedPatches = contributor.unreviewed; + } + return contributors; +} + +var mouseTimer = 0; +window.onmouseover = function (event) { + clearTimeout(mouseTimer); + + var row = contributorsView.targetRow(event.target); + if (row) { + if (!contributorsView.isConstrained()) { + contributorsView.selectRow(row); + areasView.setMarginTop(row.firstChild.offsetTop); + areasView.setData(contributorAreas(row.data), 'constrained'); + } + return; + } + + row = areasView.targetRow(event.target); + if (row) { + if (!areasView.isConstrained()) { + areasView.selectRow(row); + contributorsView.setMarginTop(row.firstChild.offsetTop); + contributorsView.setData(areaContributors(row.data), 'constrained'); + } + return; + } + + mouseTimer = setTimeout(function () { + contributorsView.reset(); + areasView.reset(); + }, 500); +} + +</script> +</body> +</html> diff --git a/Tools/Scripts/webkitpy/tool/commands/download.py b/Tools/Scripts/webkitpy/tool/commands/download.py new file mode 100644 index 000000000..2ba4986e2 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/download.py @@ -0,0 +1,434 @@ +# Copyright (c) 2009, 2011 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 webkitpy.tool import steps + +from webkitpy.common.checkout.changelog import ChangeLog +from webkitpy.common.config import urls +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 Clean(AbstractSequencedCommand): + name = "clean" + help_text = "Clean the working copy" + steps = [ + steps.CleanWorkingDirectory, + ] + + def _prepare_state(self, options, args, tool): + options.force_clean = True + + +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, + ] + + def _prepare_state(self, options, args, tool): + options.build = True + + +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.UpdateChangeLogsWithReviewer, + steps.ValidateReviewer, + steps.ValidateChangeLogs, # We do this after UpdateChangeLogsWithReviewer to avoid not having to cache the diff twice. + 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): + changed_files = self._tool.scm().changed_files(options.git_commit) + return { + "changed_files": changed_files, + "bug_id": (args and args[0]) or tool.checkout().bug_id_for_this_commit(options.git_commit, changed_files), + } + + +class LandCowboy(AbstractSequencedCommand): + name = "land-cowboy" + help_text = "Prepares a ChangeLog and lands the current working directory diff." + steps = [ + steps.PrepareChangeLog, + steps.EditChangeLog, + steps.CheckStyle, + steps.ConfirmDiff, + steps.Build, + steps.RunTests, + steps.Commit, + steps.CloseBugForLandDiff, + ] + + def _prepare_state(self, options, args, tool): + options.check_style_filter = "-changelog" + + +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 BuildAndTestAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin): + name = "build-and-test-attachment" + help_text = "Apply, build, and test patches from bugzilla" + argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]" + main_steps = [ + steps.CleanWorkingDirectory, + steps.Update, + steps.ApplyPatch, + steps.Build, + steps.RunTests, + ] + + +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 ApplyWatchList(AbstractPatchSequencingCommand, ProcessAttachmentsMixin): + name = "apply-watchlist" + help_text = "Applies the watchlist to the specified attachments" + argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]" + main_steps = [ + steps.CleanWorkingDirectory, + steps.Update, + steps.ApplyPatch, + steps.ApplyWatchList, + ] + long_help = """"Applies the watchlist to the specified attachments. +Downloads the attachment, applies it locally, runs the watchlist against it, and updates the bug with the result.""" + + +class AbstractPatchLandingCommand(AbstractPatchSequencingCommand): + main_steps = [ + steps.CleanWorkingDirectory, + steps.Update, + steps.ApplyPatch, + steps.ValidateChangeLogs, + 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 ValidateChangelog(AbstractSequencedCommand): + name = "validate-changelog" + help_text = "Validate that the ChangeLogs and reviewers look reasonable" + long_help = """Examines the current diff to see whether the ChangeLogs +and the reviewers listed in the ChangeLogs look reasonable. +""" + steps = [ + steps.ValidateChangeLogs, + steps.ValidateReviewer, + ] + + +class AbstractRolloutPrepCommand(AbstractSequencedCommand): + argument_names = "REVISION [REVISIONS] 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_list = [] + for revision in str(args[0]).split(): + if revision.isdigit(): + revision_list.append(int(revision)) + else: + raise ScriptError(message="Invalid svn revision number: " + revision) + revision_list.sort() + + # We use the earliest revision for the bug info + earliest_revision = revision_list[0] + commit_info = self._commit_info(earliest_revision) + cc_list = sorted([party.bugzilla_email() + for party in commit_info.responsible_parties() + if party.bugzilla_email()]) + return { + "revision": earliest_revision, + "revision_list": revision_list, + "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(s) in the working copy and prepare ChangeLogs with revert reason" + long_help = """Updates the working copy. +Applies the inverse diff for the provided revision(s). +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 the broken SVN revision(s) 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" % (urls.view_revision_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(s) 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/Tools/Scripts/webkitpy/tool/commands/download_unittest.py b/Tools/Scripts/webkitpy/tool/commands/download_unittest.py new file mode 100644 index 000000000..8b63dfcb6 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/download_unittest.py @@ -0,0 +1,252 @@ +# Copyright (C) 2009, 2011 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 +from webkitpy.common.checkout.checkout_mock import MockCheckout + + +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 50000.\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) + + def test_prepare_state(self): + command = AbstractRolloutPrepCommand() + mock_commit_info = MockCheckout().commit_info_for_revision(123) + command._commit_info = lambda revision: mock_commit_info + + state = command._prepare_state(None, ["124 123 125", "Reason"], None) + self.assertEqual(123, state["revision"]) + self.assertEqual([123, 124, 125], state["revision_list"]) + + self.assertRaises(ScriptError, command._prepare_state, options=None, args=["125 r122 123", "Reason"], tool=None) + self.assertRaises(ScriptError, command._prepare_state, options=None, args=["125 foo 123", "Reason"], tool=None) + + +class DownloadCommandsTest(CommandsTest): + def _default_options(self): + options = MockOptions() + options.build = True + options.build_style = True + options.check_style = True + options.check_style_filter = None + options.clean = True + options.close_bug = True + options.force_clean = False + options.force_patch = True + options.non_interactive = False + options.parent_command = 'MOCK parent command' + options.quiet = False + options.test = True + options.update = 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 10000 from bug 50000.\n" + self.assert_execute_outputs(ApplyAttachment(), [10000], 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 50000.\nProcessing 2 patches from 1 bug.\nProcessing patch 10000 from bug 50000.\nProcessing patch 10001 from bug 50000.\n" + self.assert_execute_outputs(ApplyFromBug(), [50000], options=options, expected_stderr=expected_stderr) + + def test_apply_watch_list(self): + expected_stderr = """Processing 1 patch from 1 bug. +Updating working directory +MOCK run_and_throw_if_fail: ['mock-update-webkit'], cwd=/mock-checkout\nProcessing patch 10000 from bug 50000. +MockWatchList: determine_cc_and_messages +""" + self.assert_execute_outputs(ApplyWatchList(), [10000], options=self._default_options(), expected_stderr=expected_stderr, tool=MockTool(log_executive=True)) + + def test_land(self): + expected_stderr = "Building WebKit\nRunning Python unit tests\nRunning Perl unit tests\nRunning JavaScriptCore tests\nRunning run-webkit-tests\nCommitted r49824: <http://trac.webkit.org/changeset/49824>\nUpdating bug 50000\n" + mock_tool = MockTool() + mock_tool.scm().create_patch = Mock(return_value="Patch1\nMockPatch\n") + mock_tool.checkout().modified_changelogs = Mock(return_value=[]) + self.assert_execute_outputs(Land(), [50000], 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_cowboy(self): + expected_stderr = """MOCK run_and_throw_if_fail: ['mock-prepare-ChangeLog', '--email=MOCK email', '--merge-base=None', 'MockFile1'], cwd=/mock-checkout +MOCK run_and_throw_if_fail: ['mock-check-webkit-style', '--git-commit', 'MOCK git commit', '--diff-files', 'MockFile1', '--filter', '-changelog'], cwd=/mock-checkout +MOCK run_command: ['ruby', '-I', '/mock-checkout/Websites/bugs.webkit.org/PrettyPatch', '/mock-checkout/Websites/bugs.webkit.org/PrettyPatch/prettify.rb'], cwd=None +MOCK: user.open_url: file://... +Was that diff correct? +Building WebKit +MOCK run_and_throw_if_fail: ['mock-build-webkit'], cwd=/mock-checkout, env={'LC_ALL': 'C', 'MOCK_ENVIRON_COPY': '1'} +Running Python unit tests +MOCK run_and_throw_if_fail: ['mock-test-webkitpy'], cwd=/mock-checkout +Running Perl unit tests +MOCK run_and_throw_if_fail: ['mock-test-webkitperl'], cwd=/mock-checkout +Running JavaScriptCore tests +MOCK run_and_throw_if_fail: ['mock-run-javacriptcore-tests'], cwd=/mock-checkout +Running run-webkit-tests +MOCK run_and_throw_if_fail: ['mock-run-webkit-tests', '--quiet'], cwd=/mock-checkout +Committed r49824: <http://trac.webkit.org/changeset/49824> +Committed r49824: <http://trac.webkit.org/changeset/49824> +No bug id provided. +""" + mock_tool = MockTool(log_executive=True) + self.assert_execute_outputs(LandCowboy(), [50000], options=self._default_options(), expected_stderr=expected_stderr, tool=mock_tool) + + def test_land_red_builders(self): + expected_stderr = 'Building WebKit\nRunning Python unit tests\nRunning Perl unit tests\nRunning JavaScriptCore tests\nRunning run-webkit-tests\nCommitted r49824: <http://trac.webkit.org/changeset/49824>\nUpdating bug 50000\n' + mock_tool = MockTool() + mock_tool.buildbot.light_tree_on_fire() + self.assert_execute_outputs(Land(), [50000], options=self._default_options(), expected_stderr=expected_stderr, tool=mock_tool) + + def test_check_style(self): + expected_stderr = """Processing 1 patch from 1 bug. +Updating working directory +MOCK run_and_throw_if_fail: ['mock-update-webkit'], cwd=/mock-checkout +Processing patch 10000 from bug 50000. +MOCK run_and_throw_if_fail: ['mock-check-webkit-style', '--git-commit', 'MOCK git commit', '--diff-files', 'MockFile1'], cwd=/mock-checkout +""" + self.assert_execute_outputs(CheckStyle(), [10000], options=self._default_options(), expected_stderr=expected_stderr, tool=MockTool(log_executive=True)) + + def test_build_attachment(self): + expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 10000 from bug 50000.\nBuilding WebKit\n" + self.assert_execute_outputs(BuildAttachment(), [10000], 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 10000 from bug 50000. +Building WebKit +Running Python unit tests +Running Perl unit tests +Running JavaScriptCore tests +Running run-webkit-tests +Committed r49824: <http://trac.webkit.org/changeset/49824> +Not closing bug 50000 as attachment 10000 has review=+. Assuming there are more patches to land from this bug. +""" + self.assert_execute_outputs(LandAttachment(), [10000], 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 50000. +Processing 2 patches from 1 bug. +Updating working directory +Processing patch 10000 from bug 50000. +Building WebKit +Running Python unit tests +Running Perl unit tests +Running JavaScriptCore tests +Running run-webkit-tests +Committed r49824: <http://trac.webkit.org/changeset/49824> +Not closing bug 50000 as attachment 10000 has review=+. Assuming there are more patches to land from this bug. +Updating working directory +Processing patch 10001 from bug 50000. +Building WebKit +Running Python unit tests +Running Perl unit tests +Running JavaScriptCore tests +Running run-webkit-tests +Committed r49824: <http://trac.webkit.org/changeset/49824> +Not closing bug 50000 as attachment 10000 has review=+. Assuming there are more patches to land from this bug. +""" + self.assert_execute_outputs(LandFromBug(), [50000], options=self._default_options(), expected_stderr=expected_stderr) + + def test_prepare_rollout(self): + expected_stderr = "Preparing rollout for bug 50000.\nUpdating working directory\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 50000. +Updating working directory +MOCK create_bug +bug_title: REGRESSION(r852): Reason +bug_description: http://trac.webkit.org/changeset/852 broke the build: +Reason +component: MOCK component +cc: MOCK cc +blocked: 50000 +MOCK add_patch_to_bug: bug_id=50004, 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 + +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) + self.assert_execute_outputs(CreateRollout(), ["855 852 854", "Reason"], options=self._default_options(), expected_stderr=expected_stderr) + + def test_rollout(self): + expected_stderr = """Preparing rollout for bug 50000. +Updating working directory +MOCK: user.open_url: file://... +Was that diff correct? +Building WebKit +Committed r49824: <http://trac.webkit.org/changeset/49824> +MOCK reopen_bug 50000 with comment 'Reverted r852 for reason: + +Reason + +Committed r49824: <http://trac.webkit.org/changeset/49824>' +""" + self.assert_execute_outputs(Rollout(), [852, "Reason"], options=self._default_options(), expected_stderr=expected_stderr) + diff --git a/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem.py b/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem.py new file mode 100644 index 000000000..338149b6d --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem.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. + +from webkitpy.common.config.committers import CommitterList +from webkitpy.common.config.ports import WebKitPort +from webkitpy.common.system.deprecated_logging import error, log +from webkitpy.common.system.executive import ScriptError +from webkitpy.tool.bot.expectedfailures import ExpectedFailures +from webkitpy.tool.bot.layouttestresultsreader import LayoutTestResultsReader +from webkitpy.tool.bot.queueengine import QueueEngine +from webkitpy.tool.bot.earlywarningsystemtask import EarlyWarningSystemTask, EarlyWarningSystemTaskDelegate, UnableToApplyPatch +from webkitpy.tool.commands.queues import AbstractReviewQueue + + +class AbstractEarlyWarningSystem(AbstractReviewQueue, EarlyWarningSystemTaskDelegate): + _build_style = "release" + # FIXME: Switch _run_tests from opt-in to opt-out once more bots are ready to run tests. + _run_tests = False + + def __init__(self): + AbstractReviewQueue.__init__(self) + self.port = WebKitPort.port(self.port_name) + + def should_proceed_with_work_item(self, patch): + return True + + def begin_work_queue(self): + # FIXME: This violates abstraction + self._tool._deprecated_port = self.port + AbstractReviewQueue.begin_work_queue(self) + self._expected_failures = ExpectedFailures() + self._layout_test_results_reader = LayoutTestResultsReader(self._tool, self._log_directory()) + + def _failing_tests_message(self, task, patch): + results = task.results_from_patch_test_run(patch) + unexpected_failures = self._expected_failures.unexpected_failures_observed(results) + if not unexpected_failures: + return None + return "New failing tests:\n%s" % "\n".join(unexpected_failures) + + def _post_reject_message_on_bug(self, tool, patch, status_id, extra_message_text=None): + results_link = tool.status_server.results_url_for_status(status_id) + message = "Attachment %s did not pass %s (%s):\nOutput: %s" % (patch.id(), self.name, self.port_name, results_link) + # FIXME: We might want to add some text about rejecting from the commit-queue in + # the case where patch.commit_queue() isn't already set to '-'. + if self.watchers: + tool.bugs.add_cc_to_bug(patch.bug_id(), self.watchers) + tool.bugs.set_flag_on_attachment(patch.id(), "commit-queue", "-", message, extra_message_text) + + def review_patch(self, patch): + task = EarlyWarningSystemTask(self, patch, self._run_tests) + if not task.validate(): + self._did_error(patch, "%s did not process patch." % self.name) + return False + try: + return task.run() + except UnableToApplyPatch, e: + self._did_error(patch, "%s unable to apply patch." % self.name) + return False + except ScriptError, e: + self._post_reject_message_on_bug(self._tool, patch, task.failure_status_id, self._failing_tests_message(task, patch)) + results_archive = task.results_archive_from_patch_test_run(patch) + if results_archive: + self._upload_results_archive_for_patch(patch, results_archive) + self._did_fail(patch) + # FIXME: We're supposed to be able to raise e again here and have + # one of our base classes mark the patch as fail, but there seems + # to be an issue with the exit_code. + return False + + # EarlyWarningSystemDelegate methods + + def parent_command(self): + return self.name + + def run_command(self, command): + self.run_webkit_patch(command + [self.port.flag()]) + + def command_passed(self, message, patch): + pass + + def command_failed(self, message, script_error, patch): + failure_log = self._log_from_script_error_for_upload(script_error) + return self._update_status(message, patch=patch, results_file=failure_log) + + def expected_failures(self): + return self._expected_failures + + def layout_test_results(self): + return self._layout_test_results_reader.results() + + def archive_last_layout_test_results(self, patch): + return self._layout_test_results_reader.archive(patch) + + def build_style(self): + return self._build_style + + def refetch_patch(self, patch): + return self._tool.bugs.fetch_attachment(patch.id()) + + def report_flaky_tests(self, patch, flaky_test_results, results_archive): + pass + + # StepSequenceErrorHandler methods + + @classmethod + def handle_script_error(cls, tool, state, script_error): + # FIXME: Why does this not exit(1) like the superclass does? + log(script_error.message_with_output()) + + +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", + "gyuyoung.kim@samsung.com", + ] + + +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" + port_name = "chromium-xvfb" + _run_tests = True + + +class ChromiumWindowsEWS(AbstractChromiumEWS): + name = "cr-win-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 process_work_item(self, patch): + if not patch.attacher() or not patch.attacher().can_commit: + self._did_error(patch, "%s cannot process patches from non-committers :(" % self.name) + return False + return AbstractEarlyWarningSystem.process_work_item(self, patch) + + +# FIXME: Inheriting from AbstractCommitterOnlyEWS is kinda a hack, but it +# happens to work because AbstractChromiumEWS and AbstractCommitterOnlyEWS +# provide disjoint sets of functionality, and Python is otherwise smart +# enough to handle the diamond inheritance. +class ChromiumMacEWS(AbstractChromiumEWS, AbstractCommitterOnlyEWS): + name = "cr-mac-ews" + + +class MacEWS(AbstractCommitterOnlyEWS): + name = "mac-ews" + port_name = "mac" diff --git a/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py b/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py new file mode 100644 index 000000000..44143f72d --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py @@ -0,0 +1,99 @@ +# 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.common.system.outputcapture import OutputCapture +from webkitpy.tool.bot.queueengine import QueueEngine +from webkitpy.tool.commands.earlywarningsystem import * +from webkitpy.tool.commands.queuestest import QueuesTest +from webkitpy.tool.mocktool import MockTool, MockOptions + + +class AbstractEarlyWarningSystemTest(QueuesTest): + def test_failing_tests_message(self): + # Needed to define port_name, used in AbstractEarlyWarningSystem.__init__ + class TestEWS(AbstractEarlyWarningSystem): + port_name = "win" # Needs to be a port which port/factory understands. + + ews = TestEWS() + ews.bind_to_tool(MockTool()) + ews._options = MockOptions(port=None, confirm=False) + OutputCapture().assert_outputs(self, ews.begin_work_queue, expected_stderr=self._default_begin_work_queue_stderr(ews.name)) + ews._expected_failures.unexpected_failures_observed = lambda results: set(["foo.html", "bar.html"]) + task = Mock() + patch = ews._tool.bugs.fetch_attachment(10000) + self.assertEqual(ews._failing_tests_message(task, patch), "New failing tests:\nbar.html\nfoo.html") + + +class EarlyWarningSytemTest(QueuesTest): + def _default_expected_stderr(self, ews): + string_replacemnts = { + "name": ews.name, + "port": ews.port_name, + } + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr(ews.name), + "handle_unexpected_error": "Mock error message\n", + "next_work_item": "", + "process_work_item": "MOCK: update_status: %(name)s Pass\nMOCK: release_work_item: %(name)s 10000\n" % string_replacemnts, + "handle_script_error": "ScriptError error message\n", + } + return expected_stderr + + def _test_builder_ews(self, ews): + ews.bind_to_tool(MockTool()) + self.assert_queue_outputs(ews, expected_stderr=self._default_expected_stderr(ews)) + + def _test_committer_only_ews(self, ews): + ews.bind_to_tool(MockTool()) + expected_stderr = self._default_expected_stderr(ews) + string_replacemnts = {"name": ews.name} + expected_stderr["process_work_item"] = "MOCK: update_status: %(name)s Error: %(name)s cannot process patches from non-committers :(\nMOCK: release_work_item: %(name)s 10000\n" % string_replacemnts + self.assert_queue_outputs(ews, expected_stderr=expected_stderr) + + def _test_testing_ews(self, ews): + ews.layout_test_results = lambda: None + ews.bind_to_tool(MockTool()) + expected_stderr = self._default_expected_stderr(ews) + expected_stderr["handle_script_error"] = "ScriptError error message\n" + self.assert_queue_outputs(ews, expected_stderr=expected_stderr) + + def test_committer_only_ewses(self): + self._test_committer_only_ews(MacEWS()) + self._test_committer_only_ews(ChromiumMacEWS()) + + def test_builder_ewses(self): + self._test_builder_ews(ChromiumWindowsEWS()) + self._test_builder_ews(QtEWS()) + self._test_builder_ews(GtkEWS()) + self._test_builder_ews(EflEWS()) + + def test_testing_ewses(self): + self._test_testing_ews(ChromiumLinuxEWS()) diff --git a/Tools/Scripts/webkitpy/tool/commands/expectations.py b/Tools/Scripts/webkitpy/tool/commands/expectations.py new file mode 100644 index 000000000..575e80ce8 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/expectations.py @@ -0,0 +1,45 @@ +# Copyright (c) 2011 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.layout_tests.models.test_configuration import TestConfigurationConverter +from webkitpy.layout_tests.models.test_expectations import TestExpectationParser, TestExpectationSerializer +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +class OptimizeExpectations(AbstractDeclarativeCommand): + name = "optimize-expectations" + help_text = "Fixes simple style issues in test_expectations file. (Currently works only for chromium port.)" + + def execute(self, options, args, tool): + port = tool.port_factory.get("chromium-win-win7") # FIXME: This should be selectable. + expectation_lines = TestExpectationParser.tokenize_list(port.test_expectations()) + parser = TestExpectationParser(port, [], allow_rebaseline_modifier=False) + for expectation_line in expectation_lines: + parser.parse(expectation_line) + converter = TestConfigurationConverter(port.all_test_configurations(), port.configuration_specifier_macros()) + tool.filesystem.write_text_file(port.path_to_test_expectations_file(), TestExpectationSerializer.list_to_string(expectation_lines, converter)) diff --git a/Tools/Scripts/webkitpy/tool/commands/findusers.py b/Tools/Scripts/webkitpy/tool/commands/findusers.py new file mode 100644 index 000000000..4363c8cf2 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/findusers.py @@ -0,0 +1,44 @@ +# Copyright (c) 2011 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.multicommandtool import AbstractDeclarativeCommand + + +class FindUsers(AbstractDeclarativeCommand): + name = "find-users" + help_text = "Find users matching substring" + + def execute(self, options, args, tool): + search_string = args[0] + login_userid_pairs = tool.bugs.queries.fetch_login_userid_pairs_matching_substring(search_string) + for (login, user_id) in login_userid_pairs: + user = tool.bugs.fetch_user(user_id) + groups_string = ", ".join(user['groups']) if user['groups'] else "none" + print "%s <%s> (%s) (%s)" % (user['name'], user['login'], user_id, groups_string) + else: + print "No users found matching '%s'" % search_string diff --git a/Tools/Scripts/webkitpy/tool/commands/gardenomatic.py b/Tools/Scripts/webkitpy/tool/commands/gardenomatic.py new file mode 100644 index 000000000..7da96e4bc --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/gardenomatic.py @@ -0,0 +1,41 @@ +# Copyright (C) 2011 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: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# 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.multicommandtool import AbstractDeclarativeCommand +from webkitpy.tool.servers.gardeningserver import GardeningHTTPServer + + +class GardenOMatic(AbstractDeclarativeCommand): + name = "garden-o-matic" + help_text = "Experimental command for gardening the WebKit tree." + + def execute(self, options, args, tool): + print "This command runs a local HTTP server that changes your working copy" + print "based on the actions you take in the web-based UI." + + httpd = GardeningHTTPServer(httpd_port=8127, config={'tool': tool}) + self._tool.user.open_url(httpd.url()) + + print "Local HTTP server started." + httpd.serve_forever() diff --git a/Tools/Scripts/webkitpy/tool/commands/openbugs.py b/Tools/Scripts/webkitpy/tool/commands/openbugs.py new file mode 100644 index 000000000..1b51c9ff6 --- /dev/null +++ b/Tools/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/Tools/Scripts/webkitpy/tool/commands/openbugs_unittest.py b/Tools/Scripts/webkitpy/tool/commands/openbugs_unittest.py new file mode 100644 index 000000000..40a6e1b2e --- /dev/null +++ b/Tools/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/Tools/Scripts/webkitpy/tool/commands/prettydiff.py b/Tools/Scripts/webkitpy/tool/commands/prettydiff.py new file mode 100644 index 000000000..66a06a69e --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/prettydiff.py @@ -0,0 +1,39 @@ +# 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 +from webkitpy.tool import steps + + +class PrettyDiff(AbstractSequencedCommand): + name = "pretty-diff" + help_text = "Shows the pretty diff in the default browser" + show_in_main_help = True + steps = [ + steps.ConfirmDiff, + ] diff --git a/Tools/Scripts/webkitpy/tool/commands/queries.py b/Tools/Scripts/webkitpy/tool/commands/queries.py new file mode 100644 index 000000000..7d23717a8 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/queries.py @@ -0,0 +1,407 @@ +# 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.tool import steps + +from webkitpy.common.checkout.commitinfo import CommitInfo +from webkitpy.common.config.committers import CommitterList +import webkitpy.common.config.urls as config_urls +from webkitpy.common.net.buildbot import BuildBot +from webkitpy.common.net.regressionwindow import RegressionWindow +from webkitpy.common.system.crashlogs import CrashLogs +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 +from webkitpy.layout_tests import port + + +class SuggestReviewers(AbstractDeclarativeCommand): + name = "suggest-reviewers" + help_text = "Suggest reviewers for a patch based on recent changes to the modified files." + + def __init__(self): + options = [ + steps.Options.git_commit, + ] + AbstractDeclarativeCommand.__init__(self, options=options) + + def execute(self, options, args, tool): + reviewers = tool.checkout().suggested_reviewers(options.git_commit) + print "\n".join([reviewer.full_name for reviewer in reviewers]) + + +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" % config_urls.buildbot_url + + def _print_builder_line(self, builder_name, max_name_width, status_message): + print "%s : %s" % (builder_name.ljust(max_name_width), status_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"]) + regression_window = builder.find_regression_window(red_build) + if not regression_window.failing_build(): + self._print_builder_line(builder.name(), name_width, "FAIL (error loading build information)") + return + if not regression_window.build_before_failure(): + self._print_builder_line(builder.name(), name_width, "FAIL (blame-list: sometime before %s?)" % regression_window.failing_build().revision()) + return + + revisions = regression_window.revisions() + first_failure_message = "" + if (regression_window.failing_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)" % (revisions, first_failure_message)) + for revision in 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 ResultsFor(AbstractDeclarativeCommand): + name = "results-for" + help_text = "Print a list of failures for the passed revision from bots on %s" % config_urls.buildbot_url + 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" % config_urls.buildbot_url + + def _blame_line_for_revision(self, revision): + try: + commit_info = self._tool.checkout().commit_info_for_revision(revision) + except Exception, e: + return "FAILED to fetch CommitInfo for r%s, exception: %s" % (revision, e) + if not commit_info: + return "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision + return commit_info.blame_string(self._tool.bugs) + + def _print_blame_information_for_transition(self, regression_window, failing_tests): + red_build = regression_window.failing_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 regression_window.revisions(): + print self._blame_line_for_revision(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 from %s; can't continue. (start revision = r%s)" % (build.results_url(), 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 + regression_window = RegressionWindow(build, last_build_with_results) + self._print_blame_information_for_transition(regression_window, 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 = self._tool.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 FindFlakyTests(AbstractDeclarativeCommand): + name = "find-flaky-tests" + help_text = "Lists tests that often fail for a single build at %s" % config_urls.buildbot_url + + def _find_failures(self, builder, revision): + build = builder.build_for_revision(revision, allow_failed_lookups=True) + if not build: + print "No build for %s" % revision + return (None, None) + results = build.layout_test_results() + if not results: + print "No results build %s (r%s)" % (build._number, build.revision()) + return (None, None) + failures = set(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()) + return (None, None) + return (build, failures) + + def _increment_statistics(self, flaky_tests, flaky_test_statistics): + for test in flaky_tests: + count = flaky_test_statistics.get(test, 0) + flaky_test_statistics[test] = count + 1 + + def _print_statistics(self, statistics): + print "=== Results ===" + print "Occurances Test name" + for value, key in sorted([(value, key) for key, value in statistics.items()]): + print "%10d %s" % (value, key) + + def _walk_backwards_from(self, builder, start_revision, limit): + flaky_test_statistics = {} + all_previous_failures = set([]) + one_time_previous_failures = set([]) + previous_build = None + for i in range(limit): + revision = start_revision - i + print "Analyzing %s ... " % revision, + (build, failures) = self._find_failures(builder, revision) + if failures == None: + # Notice that we don't loop on the empty set! + continue + print "has %s failures" % len(failures) + flaky_tests = one_time_previous_failures - failures + if flaky_tests: + print "Flaky tests: %s %s" % (sorted(flaky_tests), + previous_build.results_url()) + self._increment_statistics(flaky_tests, flaky_test_statistics) + one_time_previous_failures = failures - all_previous_failures + all_previous_failures = failures + previous_build = build + self._print_statistics(flaky_test_statistics) + + def _builder_to_analyze(self): + statuses = self._tool.buildbot.builder_statuses() + choices = [status["name"] for status in statuses] + chosen_name = self._tool.user.prompt_with_list("Which builder to analyze:", choices) + for status in 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_analyze() + limit = self._tool.user.prompt("How many revisions to look through? [10000] ") or 10000 + return self._walk_backwards_from(builder, latest_revision, limit=int(limit)) + + +class TreeStatus(AbstractDeclarativeCommand): + name = "tree-status" + help_text = "Print the status of the %s buildbots" % config_urls.buildbot_url + 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"]) + + +class CrashLog(AbstractDeclarativeCommand): + name = "crash-log" + help_text = "Print the newest crash log for the given process" + long_help = """Finds the newest crash log matching the given process name +and PID and prints it to stdout.""" + argument_names = "PROCESS_NAME [PID]" + + def execute(self, options, args, tool): + crash_logs = CrashLogs(tool.filesystem) + pid = None + if len(args) > 1: + pid = int(args[1]) + print crash_logs.find_newest_log(args[0], pid) + + +class SkippedPorts(AbstractDeclarativeCommand): + name = "skipped-ports" + help_text = "Print the list of ports skipping the given layout test(s)" + long_help = """Scans the the Skipped file of each port and figure +out what ports are skipping the test(s). Categories are taken in account too.""" + argument_names = "TEST_NAME" + + def execute(self, options, args, tool): + results = dict([(test_name, []) for test_name in args]) + for port_name in tool.port_factory.all_port_names(): + port_object = tool.port_factory.get(port_name) + for test_name in args: + if port_object.skips_layout_test(test_name): + results[test_name].append(port_name) + + for test_name, ports in results.iteritems(): + if ports: + print "Ports skipping test %r: %s" % (test_name, ', '.join(ports)) + else: + print "Test %r is not skipped by any port." % test_name diff --git a/Tools/Scripts/webkitpy/tool/commands/queries_unittest.py b/Tools/Scripts/webkitpy/tool/commands/queries_unittest.py new file mode 100644 index 000000000..fe13a4c54 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/queries_unittest.py @@ -0,0 +1,117 @@ +# 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 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 MockTestPort1(object): + def skips_layout_test(self, test_name): + return test_name in ["media/foo/bar.html", "foo"] + + +class MockTestPort2(object): + def skips_layout_test(self, test_name): + return test_name == "media/foo/bar.html" + + +class MockPortFactory(object): + def __init__(self): + self._all_ports = { + "test_port1": MockTestPort1(), + "test_port2": MockTestPort2(), + } + + def all_port_names(self, options=None): + return self._all_ports.keys() + + def get(self, port_name): + return self._all_ports.get(port_name) + + +class QueryCommandsTest(CommandsTest): + def test_bugs_to_commit(self): + expected_stderr = "Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com)\n" + self.assert_execute_outputs(BugsToCommit(), None, "50000\n50003\n", expected_stderr) + + def test_patches_in_commit_queue(self): + expected_stdout = "http://example.com/10000\nhttp://example.com/10002\n" + expected_stderr = "Warning, attachment 10001 on bug 50000 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/10003&action=edit\n" + expected_stderr = "10000 already has cq=+\n10001 already has cq=+\n10004 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/50003\n" + options.bugs = True + self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options) + + def test_patches_to_review(self): + expected_stdout = "10002\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) + + def test_skipped_ports(self): + tool = MockTool() + tool.port_factory = MockPortFactory() + + expected_stdout = "Ports skipping test 'media/foo/bar.html': test_port1, test_port2\n" + self.assert_execute_outputs(SkippedPorts(), ("media/foo/bar.html",), expected_stdout, tool=tool) + + expected_stdout = "Ports skipping test 'foo': test_port1\n" + self.assert_execute_outputs(SkippedPorts(), ("foo",), expected_stdout, tool=tool) + + expected_stdout = "Test 'media' is not skipped by any port.\n" + self.assert_execute_outputs(SkippedPorts(), ("media",), expected_stdout, tool=tool) + + +class FailureReasonTest(unittest.TestCase): + def test_blame_line_for_revision(self): + tool = MockTool() + command = FailureReason() + command.bind_to_tool(tool) + # This is an artificial example, mostly to test the CommitInfo lookup failure case. + self.assertEquals(command._blame_line_for_revision(None), "FAILED to fetch CommitInfo for rNone, likely missing ChangeLog") + + def raising_mock(self): + raise Exception("MESSAGE") + tool.checkout().commit_info_for_revision = raising_mock + self.assertEquals(command._blame_line_for_revision(None), "FAILED to fetch CommitInfo for rNone, exception: MESSAGE") diff --git a/Tools/Scripts/webkitpy/tool/commands/queues.py b/Tools/Scripts/webkitpy/tool/commands/queues.py new file mode 100644 index 000000000..f61a63991 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/queues.py @@ -0,0 +1,443 @@ +# 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 __future__ import with_statement + +import codecs +import time +import traceback +import os + +from datetime import datetime +from optparse import make_option +from StringIO import StringIO + +from webkitpy.common.config.committervalidator import CommitterValidator +from webkitpy.common.net.bugzilla import Attachment +from webkitpy.common.net.statusserver import StatusServer +from webkitpy.common.system.deprecated_logging import error, log +from webkitpy.common.system.executive import ScriptError +from webkitpy.tool.bot.botinfo import BotInfo +from webkitpy.tool.bot.commitqueuetask import CommitQueueTask, CommitQueueTaskDelegate +from webkitpy.tool.bot.expectedfailures import ExpectedFailures +from webkitpy.tool.bot.feeders import CommitQueueFeeder, EWSFeeder +from webkitpy.tool.bot.layouttestresultsreader import LayoutTestResultsReader +from webkitpy.tool.bot.queueengine import QueueEngine, QueueEngineDelegate +from webkitpy.tool.bot.flakytestreporter import FlakyTestReporter +from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler +from webkitpy.tool.multicommandtool import Command, TryAgain + + +class AbstractQueue(Command, QueueEngineDelegate): + watchers = [ + ] + + _pass_status = "Pass" + _fail_status = "Fail" + _retry_status = "Retry" + _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] + if self._tool.status_server.bot_id: + webkit_patch_args += ["--bot-id=%s" % self._tool.status_server.bot_id] + if self._options.port: + webkit_patch_args += ["--port=%s" % self._options.port] + webkit_patch_args.extend(args) + # FIXME: There is probably no reason to use run_and_throw_if_fail anymore. + # run_and_throw_if_fail was invented to support tee'd output + # (where we write both to a log file and to the console at once), + # but the queues don't need live-progress, a dump-of-output at the + # end should be sufficient. + return self._tool.executive.run_and_throw_if_fail(webkit_patch_args, cwd=self._tool.scm().checkout_root) + + def _log_directory(self): + return os.path.join("..", "%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) + self._tool.status_server.update_status(self.name, "Starting Queue") + + def stop_work_queue(self, reason): + self._tool.status_server.update_status(self.name, "Stopping Queue, reason: %s" % reason) + + 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 # FIXME: This code is wrong. Command.options is a list, this assumes an Options element! + self._tool = tool # FIXME: This code is wrong too! Command.bind_to_tool handles this! + 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 FeederQueue(AbstractQueue): + name = "feeder-queue" + + _sleep_duration = 30 # seconds + + # AbstractQueue methods + + def begin_work_queue(self): + AbstractQueue.begin_work_queue(self) + self.feeders = [ + CommitQueueFeeder(self._tool), + EWSFeeder(self._tool), + ] + + def next_work_item(self): + # This really show inherit from some more basic class that doesn't + # understand work items, but the base class in the heirarchy currently + # understands work items. + return "synthetic-work-item" + + def should_proceed_with_work_item(self, work_item): + return True + + def process_work_item(self, work_item): + for feeder in self.feeders: + feeder.feed() + time.sleep(self._sleep_duration) + return True + + def work_item_log_path(self, work_item): + return None + + def handle_unexpected_error(self, work_item, message): + log(message) + + +class AbstractPatchQueue(AbstractQueue): + def _update_status(self, message, patch=None, results_file=None): + return self._tool.status_server.update_status(self.name, message, patch, results_file) + + def _next_patch(self): + patch_id = self._tool.status_server.next_work_item(self.name) + if not patch_id: + return None + patch = self._tool.bugs.fetch_attachment(patch_id) + if not patch: + # FIXME: Using a fake patch because release_work_item has the wrong API. + # We also don't really need to release the lock (although that's fine), + # mostly we just need to remove this bogus patch from our queue. + # If for some reason bugzilla is just down, then it will be re-fed later. + patch = Attachment({'id': patch_id}, None) + self._release_work_item(patch) + return None + return patch + + def _release_work_item(self, patch): + self._tool.status_server.release_work_item(self.name, patch) + + def _did_pass(self, patch): + self._update_status(self._pass_status, patch) + self._release_work_item(patch) + + def _did_fail(self, patch): + self._update_status(self._fail_status, patch) + self._release_work_item(patch) + + def _did_retry(self, patch): + self._update_status(self._retry_status, patch) + self._release_work_item(patch) + + def _did_error(self, patch, reason): + message = "%s: %s" % (self._error_status, reason) + self._update_status(message, patch) + self._release_work_item(patch) + + # FIXME: This probably belongs at a layer below AbstractPatchQueue, but shared by CommitQueue and the EarlyWarningSystem. + def _upload_results_archive_for_patch(self, patch, results_archive_zip): + bot_id = self._tool.status_server.bot_id or "bot" + description = "Archive of layout-test-results from %s" % bot_id + # results_archive is a ZipFile object, grab the File object (.fp) to pass to Mechanize for uploading. + results_archive_file = results_archive_zip.fp + # Rewind the file object to start (since Mechanize won't do that automatically) + # See https://bugs.webkit.org/show_bug.cgi?id=54593 + results_archive_file.seek(0) + # FIXME: This is a small lie to always say run-webkit-tests since Chromium uses new-run-webkit-tests. + # We could make this code look up the test script name off the port. + comment_text = "The attached test failures were seen while running run-webkit-tests on the %s.\n" % (self.name) + # FIXME: We could easily list the test failures from the archive here, + # currently callers do that separately. + comment_text += BotInfo(self._tool).summary_text() + self._tool.bugs.add_attachment_to_bug(patch.bug_id(), results_archive_file, description, filename="layout-test-results.zip", comment_text=comment_text) + + def work_item_log_path(self, patch): + return os.path.join(self._log_directory(), "%s.log" % patch.bug_id()) + + +class CommitQueue(AbstractPatchQueue, StepSequenceErrorHandler, CommitQueueTaskDelegate): + name = "commit-queue" + + # AbstractPatchQueue methods + + def begin_work_queue(self): + AbstractPatchQueue.begin_work_queue(self) + self.committer_validator = CommitterValidator(self._tool) + self._expected_failures = ExpectedFailures() + self._layout_test_results_reader = LayoutTestResultsReader(self._tool, self._log_directory()) + + def next_work_item(self): + return self._next_patch() + + def should_proceed_with_work_item(self, patch): + patch_text = "rollout patch" if patch.is_rollout() else "patch" + self._update_status("Processing %s" % patch_text, patch) + return True + + def process_work_item(self, patch): + self._cc_watchers(patch.bug_id()) + task = CommitQueueTask(self, patch) + try: + if task.run(): + self._did_pass(patch) + return True + self._did_retry(patch) + except ScriptError, e: + validator = CommitterValidator(self._tool) + validator.reject_patch_from_commit_queue(patch.id(), self._error_message_for_bug(task, patch, e)) + results_archive = task.results_archive_from_patch_test_run(patch) + if results_archive: + self._upload_results_archive_for_patch(patch, results_archive) + self._did_fail(patch) + + def _failing_tests_message(self, task, patch): + results = task.results_from_patch_test_run(patch) + unexpected_failures = self._expected_failures.unexpected_failures_observed(results) + if not unexpected_failures: + return None + return "New failing tests:\n%s" % "\n".join(unexpected_failures) + + def _error_message_for_bug(self, task, patch, script_error): + message = self._failing_tests_message(task, patch) + if not message: + message = script_error.message_with_output() + results_link = self._tool.status_server.results_url_for_status(task.failure_status_id) + return "%s\nFull output: %s" % (message, results_link) + + def handle_unexpected_error(self, patch, message): + self.committer_validator.reject_patch_from_commit_queue(patch.id(), message) + + # CommitQueueTaskDelegate methods + + def run_command(self, command): + self.run_webkit_patch(command) + + def command_passed(self, message, patch): + self._update_status(message, patch=patch) + + def command_failed(self, message, script_error, patch): + failure_log = self._log_from_script_error_for_upload(script_error) + return self._update_status(message, patch=patch, results_file=failure_log) + + def expected_failures(self): + return self._expected_failures + + def layout_test_results(self): + return self._layout_test_results_reader.results() + + def archive_last_layout_test_results(self, patch): + return self._layout_test_results_reader.archive(patch) + + def build_style(self): + return "both" + + def refetch_patch(self, patch): + return self._tool.bugs.fetch_attachment(patch.id()) + + def report_flaky_tests(self, patch, flaky_test_results, results_archive=None): + reporter = FlakyTestReporter(self._tool, self.name) + reporter.report_flaky_tests(patch, flaky_test_results, results_archive) + + # StepSequenceErrorHandler methods + + def handle_script_error(cls, tool, state, script_error): + # Hitting this error handler should be pretty rare. It does occur, + # however, when a patch no longer applies to top-of-tree in the final + # land step. + log(script_error.message_with_output()) + + @classmethod + def handle_checkout_needs_update(cls, tool, state, options, error): + message = "Tests passed, but commit failed (checkout out of date). Updating, then landing without building or re-running tests." + tool.status_server.update_status(cls.name, message, state["patch"]) + # 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 AbstractReviewQueue(AbstractPatchQueue, StepSequenceErrorHandler): + """This is the base-class for the EWS queues and the style-queue.""" + def __init__(self, options=None): + AbstractPatchQueue.__init__(self, options) + + def review_patch(self, patch): + raise NotImplementedError("subclasses must implement") + + # AbstractPatchQueue methods + + def begin_work_queue(self): + AbstractPatchQueue.begin_work_queue(self) + + def next_work_item(self): + return self._next_patch() + + 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) + else: + # The subprocess handled the error, but won't have released the patch, so we do. + # FIXME: We need to simplify the rules by which _release_work_item is called. + self._release_work_item(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): + try: + # Run the style checks. + self.run_webkit_patch(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch.id()]) + finally: + # Apply the watch list. + try: + self.run_webkit_patch(["apply-watchlist-local", patch.bug_id()]) + except ScriptError, e: + # Don't turn the style bot block red due to watchlist errors. + pass + + 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/Tools/Scripts/webkitpy/tool/commands/queues_unittest.py b/Tools/Scripts/webkitpy/tool/commands/queues_unittest.py new file mode 100644 index 000000000..ae3bffee7 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/queues_unittest.py @@ -0,0 +1,486 @@ +# Copyright (C) 2009 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import os +import StringIO + +from webkitpy.common.checkout.scm import CheckoutNeedsUpdate +from webkitpy.common.checkout.scm.scm_mock import MockSCM +from webkitpy.common.net.bugzilla import Attachment +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.layout_tests.models import test_results +from webkitpy.layout_tests.models import test_failures +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.commands.stepsequence import StepSequence +from webkitpy.common.net.statusserver_mock import MockStatusServer +from webkitpy.tool.mocktool import MockTool, MockOptions + + +class TestCommitQueue(CommitQueue): + def __init__(self, tool=None): + CommitQueue.__init__(self) + if tool: + self.bind_to_tool(tool) + self._options = MockOptions(confirm=False, parent_command="commit-queue") + + def begin_work_queue(self): + output_capture = OutputCapture() + output_capture.capture_output() + CommitQueue.begin_work_queue(self) + output_capture.restore_output() + + +class TestQueue(AbstractPatchQueue): + name = "test-queue" + + +class TestReviewQueue(AbstractReviewQueue): + name = "test-review-queue" + + +class TestFeederQueue(FeederQueue): + _sleep_duration = 0 + + +class AbstractQueueTest(CommandsTest): + def test_log_directory(self): + self.assertEquals(TestQueue()._log_directory(), os.path.join("..", "test-queue-logs")) + + def _assert_run_webkit_patch(self, run_args, port=None): + queue = TestQueue() + tool = MockTool() + tool.status_server.bot_id = "gort" + tool.executive = Mock() + queue.bind_to_tool(tool) + queue._options = Mock() + queue._options.port = port + + queue.run_webkit_patch(run_args) + expected_run_args = ["echo", "--status-host=example.com", "--bot-id=gort"] + if port: + expected_run_args.append("--port=%s" % port) + expected_run_args.extend(run_args) + tool.executive.run_and_throw_if_fail.assert_called_with(expected_run_args, cwd='/mock-checkout') + + def test_run_webkit_patch(self): + self._assert_run_webkit_patch([1]) + self._assert_run_webkit_patch(["one", 2]) + self._assert_run_webkit_patch([1], port="mockport") + + 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 FeederQueueTest(QueuesTest): + def test_feeder_queue(self): + queue = TestFeederQueue() + tool = MockTool(log_executive=True) + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("feeder-queue"), + "should_proceed_with_work_item": "", + "next_work_item": "", + "process_work_item": """Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com) +Warning, attachment 10001 on bug 50000 has invalid committer (non-committer@example.com) +MOCK setting flag 'commit-queue' to '-' on attachment '10001' with comment 'Rejecting attachment 10001 from commit-queue.' and additional comment 'non-committer@example.com does not have committer permissions according to http://trac.webkit.org/browser/trunk/Tools/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 Tools/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 [10005, 10000] +Feeding commit-queue items [10005, 10000] +Feeding EWS (1 r? patch, 1 new) +MOCK: submit_to_ews: 10002 +""", + "handle_unexpected_error": "Mock error message\n", + } + self.assert_queue_outputs(queue, tool=tool, expected_stderr=expected_stderr) + + +class AbstractPatchQueueTest(CommandsTest): + def test_next_patch(self): + queue = AbstractPatchQueue() + tool = MockTool() + queue.bind_to_tool(tool) + queue._options = Mock() + queue._options.port = None + self.assertEquals(queue._next_patch(), None) + tool.status_server = MockStatusServer(work_items=[2, 10000]) + expected_stdout = "MOCK: fetch_attachment: 2 is not a known attachment id\n" # A mock-only message to prevent us from making mistakes. + expected_stderr = "MOCK: release_work_item: None 2\n" + patch_id = OutputCapture().assert_outputs(self, queue._next_patch, expected_stdout=expected_stdout, expected_stderr=expected_stderr) + self.assertEquals(patch_id, None) # 2 is an invalid patch id + self.assertEquals(queue._next_patch().id(), 10000) + + def test_upload_results_archive_for_patch(self): + queue = AbstractPatchQueue() + queue.name = "mock-queue" + tool = MockTool() + queue.bind_to_tool(tool) + queue._options = Mock() + queue._options.port = None + patch = queue._tool.bugs.fetch_attachment(10001) + expected_stderr = """MOCK add_attachment_to_bug: bug_id=50000, description=Archive of layout-test-results from bot filename=layout-test-results.zip +-- Begin comment -- +The attached test failures were seen while running run-webkit-tests on the mock-queue. +Port: MockPort Platform: MockPlatform 1.0 +-- End comment -- +""" + OutputCapture().assert_outputs(self, queue._upload_results_archive_for_patch, [patch, Mock()], expected_stderr=expected_stderr) + + +class NeedsUpdateSequence(StepSequence): + def _run(self, tool, options, state): + raise CheckoutNeedsUpdate([], 1, "", None) + + +class AlwaysCommitQueueTool(object): + def __init__(self): + self.status_server = MockStatusServer() + + def command_by_name(self, name): + return CommitQueue + + +class SecondThoughtsCommitQueue(TestCommitQueue): + def __init__(self, tool=None): + self._reject_patch = False + TestCommitQueue.__init__(self, tool) + + def run_command(self, command): + # We want to reject the patch after the first validation, + # so wait to reject it until after some other command has run. + self._reject_patch = True + return CommitQueue.run_command(self, command) + + def refetch_patch(self, patch): + if not self._reject_patch: + return self._tool.bugs.fetch_attachment(patch.id()) + + attachment_dictionary = { + "id": patch.id(), + "bug_id": patch.bug_id(), + "name": "Rejected", + "is_obsolete": True, + "is_patch": False, + "review": "-", + "reviewer_email": "foo@bar.com", + "commit-queue": "-", + "committer_email": "foo@bar.com", + "attacher_email": "Contributer1", + } + return Attachment(attachment_dictionary, None) + + +class CommitQueueTest(QueuesTest): + def _mock_test_result(self, testname): + return test_results.TestResult(testname, [test_failures.FailureTextMismatch()]) + + def test_commit_queue(self): + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue"), + "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing patch\n", + "next_work_item": "", + "process_work_item": """MOCK: update_status: commit-queue Cleaned working directory +MOCK: update_status: commit-queue Updated working directory +MOCK: update_status: commit-queue Applied patch +MOCK: update_status: commit-queue ChangeLog validated +MOCK: update_status: commit-queue Built patch +MOCK: update_status: commit-queue Passed tests +MOCK: update_status: commit-queue Landed patch +MOCK: update_status: commit-queue Pass +MOCK: release_work_item: commit-queue 10000 +""", + "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'Mock error message'\n", + "handle_script_error": "ScriptError error message\n", + } + self.assert_queue_outputs(CommitQueue(), expected_stderr=expected_stderr) + + def test_commit_queue_failure(self): + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue"), + "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing patch\n", + "next_work_item": "", + "process_work_item": """MOCK: update_status: commit-queue Cleaned working directory +MOCK: update_status: commit-queue Updated working directory +MOCK: update_status: commit-queue Patch does not apply +MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'MOCK script error +Full output: http://dummy_url' +MOCK: update_status: commit-queue Fail +MOCK: release_work_item: commit-queue 10000 +""", + "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'Mock error message'\n", + "handle_script_error": "ScriptError error message\n", + } + queue = CommitQueue() + + def mock_run_webkit_patch(command): + if command == ['clean'] or command == ['update']: + # We want cleaning to succeed so we can error out on a step + # that causes the commit-queue to reject the patch. + return + raise ScriptError('MOCK script error') + + queue.run_webkit_patch = mock_run_webkit_patch + self.assert_queue_outputs(queue, expected_stderr=expected_stderr) + + def test_commit_queue_failure_with_failing_tests(self): + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue"), + "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing patch\n", + "next_work_item": "", + "process_work_item": """MOCK: update_status: commit-queue Cleaned working directory +MOCK: update_status: commit-queue Updated working directory +MOCK: update_status: commit-queue Patch does not apply +MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'New failing tests: +mock_test_name.html +another_test_name.html +Full output: http://dummy_url' +MOCK: update_status: commit-queue Fail +MOCK: release_work_item: commit-queue 10000 +""", + "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'Mock error message'\n", + "handle_script_error": "ScriptError error message\n", + } + queue = CommitQueue() + + def mock_run_webkit_patch(command): + if command == ['clean'] or command == ['update']: + # We want cleaning to succeed so we can error out on a step + # that causes the commit-queue to reject the patch. + return + queue._expected_failures.unexpected_failures_observed = lambda results: ["mock_test_name.html", "another_test_name.html"] + raise ScriptError('MOCK script error') + + queue.run_webkit_patch = mock_run_webkit_patch + self.assert_queue_outputs(queue, expected_stderr=expected_stderr) + + def test_rollout(self): + tool = MockTool(log_executive=True) + tool.filesystem.write_text_file('/mock-results/results.html', '') # Otherwise the commit-queue will hit a KeyError trying to read the results from the MockFileSystem. + tool.buildbot.light_tree_on_fire() + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue"), + "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing patch\n", + "next_work_item": "", + "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'clean'], cwd=/mock-checkout +MOCK: update_status: commit-queue Cleaned working directory +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'update'], cwd=/mock-checkout +MOCK: update_status: commit-queue Updated working directory +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-attachment', '--no-update', '--non-interactive', 10000], cwd=/mock-checkout +MOCK: update_status: commit-queue Applied patch +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'validate-changelog', '--non-interactive', 10000], cwd=/mock-checkout +MOCK: update_status: commit-queue ChangeLog validated +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build', '--no-clean', '--no-update', '--build-style=both'], cwd=/mock-checkout +MOCK: update_status: commit-queue Built patch +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build-and-test', '--no-clean', '--no-update', '--test', '--non-interactive'], cwd=/mock-checkout +MOCK: update_status: commit-queue Passed tests +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10000], cwd=/mock-checkout +MOCK: update_status: commit-queue Landed patch +MOCK: update_status: commit-queue Pass +MOCK: release_work_item: commit-queue 10000 +""", + "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '10000' with comment 'Rejecting attachment 10000 from commit-queue.' and additional comment 'Mock error message'\n", + "handle_script_error": "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 = tool.bugs.fetch_attachment(10005) # _patch6, a rollout patch. + assert(rollout_patch.is_rollout()) + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue"), + "should_proceed_with_work_item": "MOCK: update_status: commit-queue Processing rollout patch\n", + "next_work_item": "", + "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'clean'], cwd=/mock-checkout +MOCK: update_status: commit-queue Cleaned working directory +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'update'], cwd=/mock-checkout +MOCK: update_status: commit-queue Updated working directory +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-attachment', '--no-update', '--non-interactive', 10005], cwd=/mock-checkout +MOCK: update_status: commit-queue Applied patch +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'validate-changelog', '--non-interactive', 10005], cwd=/mock-checkout +MOCK: update_status: commit-queue ChangeLog validated +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--non-interactive', '--parent-command=commit-queue', 10005], cwd=/mock-checkout +MOCK: update_status: commit-queue Landed patch +MOCK: update_status: commit-queue Pass +MOCK: release_work_item: commit-queue 10005 +""", + "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '10005' with comment 'Rejecting attachment 10005 from commit-queue.' and additional comment 'Mock error message'\n", + "handle_script_error": "ScriptError error message\n", + } + self.assert_queue_outputs(CommitQueue(), tool=tool, work_item=rollout_patch, expected_stderr=expected_stderr) + + def test_auto_retry(self): + queue = CommitQueue() + options = Mock() + options.parent_command = "commit-queue" + tool = AlwaysCommitQueueTool() + sequence = NeedsUpdateSequence(None) + + expected_stderr = "Commit failed because the checkout is out of date. Please update and try again.\nMOCK: update_status: commit-queue Tests passed, but commit failed (checkout out of date). Updating, then landing without building or re-running tests.\n" + state = {'patch': None} + OutputCapture().assert_outputs(self, sequence.run_and_handle_errors, [tool, options, state], expected_exception=TryAgain, expected_stderr=expected_stderr) + + self.assertEquals(options.update, True) + self.assertEquals(options.build, False) + self.assertEquals(options.test, False) + + def test_manual_reject_during_processing(self): + queue = SecondThoughtsCommitQueue(MockTool()) + queue.begin_work_queue() + queue._tool.filesystem.write_text_file('/mock-results/results.html', '') # Otherwise the commit-queue will hit a KeyError trying to read the results from the MockFileSystem. + queue._options = Mock() + queue._options.port = None + expected_stderr = """MOCK: update_status: commit-queue Cleaned working directory +MOCK: update_status: commit-queue Updated working directory +MOCK: update_status: commit-queue Applied patch +MOCK: update_status: commit-queue ChangeLog validated +MOCK: update_status: commit-queue Built patch +MOCK: update_status: commit-queue Passed tests +MOCK: update_status: commit-queue Retry +MOCK: release_work_item: commit-queue 10000 +""" + OutputCapture().assert_outputs(self, queue.process_work_item, [QueuesTest.mock_work_item], expected_stderr=expected_stderr) + + def test_report_flaky_tests(self): + queue = TestCommitQueue(MockTool()) + expected_stderr = """MOCK bug comment: bug_id=50002, cc=None +--- Begin comment --- +The commit-queue just saw foo/bar.html flake (Text diff mismatch) while processing attachment 10000 on bug 50000. +Port: MockPort Platform: MockPlatform 1.0 +--- End comment --- + +MOCK add_attachment_to_bug: bug_id=50002, description=Failure diff from bot filename=failure.diff +MOCK bug comment: bug_id=50002, cc=None +--- Begin comment --- +The commit-queue just saw bar/baz.html flake (Text diff mismatch) while processing attachment 10000 on bug 50000. +Port: MockPort Platform: MockPlatform 1.0 +--- End comment --- + +MOCK add_attachment_to_bug: bug_id=50002, description=Archive of layout-test-results from bot filename=layout-test-results.zip +MOCK bug comment: bug_id=50000, cc=None +--- Begin comment --- +The commit-queue encountered the following flaky tests while processing attachment 10000: + +foo/bar.html bug 50002 (author: abarth@webkit.org) +bar/baz.html bug 50002 (author: abarth@webkit.org) +The commit-queue is continuing to process your patch. +--- End comment --- + +""" + test_names = ["foo/bar.html", "bar/baz.html"] + test_results = [self._mock_test_result(name) for name in test_names] + + class MockZipFile(object): + def __init__(self): + self.fp = StringIO() + + def read(self, path): + return "" + + def namelist(self): + # This is intentionally missing one diffs.txt to exercise the "upload the whole zip" codepath. + return ['foo/bar-diffs.txt'] + + OutputCapture().assert_outputs(self, queue.report_flaky_tests, [QueuesTest.mock_work_item, test_results, MockZipFile()], expected_stderr=expected_stderr) + + +class StyleQueueTest(QueuesTest): + def test_style_queue_with_style_exception(self): + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("style-queue"), + "next_work_item": "", + "should_proceed_with_work_item": "MOCK: update_status: style-queue Checking style\n", + "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'check-style', '--force-clean', '--non-interactive', '--parent-command=style-queue', 10000], cwd=/mock-checkout +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-watchlist-local', 50000], cwd=/mock-checkout +MOCK: update_status: style-queue Fail +MOCK: release_work_item: style-queue 10000\n""", + "handle_unexpected_error": "Mock error message\n", + "handle_script_error": "MOCK: update_status: style-queue ScriptError error message\nMOCK bug comment: bug_id=50000, cc=[]\n--- Begin comment ---\nAttachment 10000 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 = { + "process_work_item": ScriptError, + "handle_script_error": SystemExit, + } + tool = MockTool(log_executive=True, executive_throws_when_run=set(['check-style'])) + self.assert_queue_outputs(StyleQueue(), expected_stderr=expected_stderr, expected_exceptions=expected_exceptions, tool=tool) + + def test_style_queue_with_watch_list_exception(self): + expected_stderr = { + "begin_work_queue": self._default_begin_work_queue_stderr("style-queue"), + "next_work_item": "", + "should_proceed_with_work_item": "MOCK: update_status: style-queue Checking style\n", + "process_work_item": """MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'check-style', '--force-clean', '--non-interactive', '--parent-command=style-queue', 10000], cwd=/mock-checkout +MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'apply-watchlist-local', 50000], cwd=/mock-checkout +MOCK: update_status: style-queue Pass +MOCK: release_work_item: style-queue 10000\n""", + "handle_unexpected_error": "Mock error message\n", + "handle_script_error": "MOCK: update_status: style-queue ScriptError error message\nMOCK bug comment: bug_id=50000, cc=[]\n--- Begin comment ---\nAttachment 10000 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, + } + tool = MockTool(log_executive=True, executive_throws_when_run=set(['apply-watchlist-local'])) + self.assert_queue_outputs(StyleQueue(), expected_stderr=expected_stderr, expected_exceptions=expected_exceptions, tool=tool) diff --git a/Tools/Scripts/webkitpy/tool/commands/queuestest.py b/Tools/Scripts/webkitpy/tool/commands/queuestest.py new file mode 100644 index 000000000..cb16b530f --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/queuestest.py @@ -0,0 +1,99 @@ +# 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.commands.stepsequence import StepSequenceErrorHandler +from webkitpy.tool.mocktool import MockTool + + +class MockQueueEngine(object): + def __init__(self, name, queue, wakeup_event): + pass + + def run(self): + pass + + +class QueuesTest(unittest.TestCase): + # This is _patch1 in mocktool.py + mock_work_item = MockTool().bugs.fetch_attachment(10000) + + 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 _default_begin_work_queue_stderr(self, name): + checkout_dir = '/mock-checkout' + string_replacements = {"name": name, 'checkout_dir': checkout_dir} + return "CAUTION: %(name)s will discard all local changes in \"%(checkout_dir)s\"\nRunning WebKit %(name)s.\nMOCK: update_status: %(name)s Starting Queue\n" % string_replacements + + def assert_queue_outputs(self, queue, args=None, work_item=None, expected_stdout=None, expected_stderr=None, expected_exceptions=None, options=None, tool=None): + if not tool: + tool = MockTool() + # This is a hack to make it easy for callers to not have to setup a custom MockFileSystem just to test the commit-queue + # the cq tries to read the layout test results, and will hit a KeyError in MockFileSystem if we don't do this. + tool.filesystem.write_text_file('/mock-results/results.html', "") + if not expected_stdout: + expected_stdout = {} + if not expected_stderr: + expected_stderr = {} + if not args: + args = [] + if not options: + options = Mock() + options.port = None + 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) + # Should we have a different function for testing StepSequenceErrorHandlers? + if isinstance(queue, StepSequenceErrorHandler): + self.assert_outputs(queue.handle_script_error, "handle_script_error", [tool, {"patch": self.mock_work_item}, ScriptError(message="ScriptError error message", script_args="MockErrorCommand")], expected_stdout, expected_stderr, expected_exceptions) diff --git a/Tools/Scripts/webkitpy/tool/commands/rebaseline.py b/Tools/Scripts/webkitpy/tool/commands/rebaseline.py new file mode 100644 index 000000000..515ff7dfa --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/rebaseline.py @@ -0,0 +1,241 @@ +# 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 + +import webkitpy.common.config.urls as config_urls +from webkitpy.common.checkout.baselineoptimizer import BaselineOptimizer +from webkitpy.common.net.buildbot import BuildBot +from webkitpy.common.net.layouttestresults import LayoutTestResults +from webkitpy.common.system.executive import ScriptError +from webkitpy.common.system.user import User +from webkitpy.layout_tests.controllers.test_result_writer import TestResultWriter +from webkitpy.layout_tests.models import test_failures +from webkitpy.layout_tests.models.test_expectations import TestExpectations +from webkitpy.layout_tests.port import builders +from webkitpy.tool.grammar import pluralize +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +_baseline_suffix_list = ['png', 'txt'] + + +# FIXME: Should TestResultWriter know how to compute this string? +def _baseline_name(fs, test_name, suffix): + return fs.splitext(test_name)[0] + TestResultWriter.FILENAME_SUFFIX_EXPECTED + "." + suffix + + +class RebaselineTest(AbstractDeclarativeCommand): + name = "rebaseline-test" + help_text = "Rebaseline a single test from a buildbot. (Currently works only with build.chromium.org buildbots.)" + argument_names = "BUILDER_NAME TEST_NAME" + + def _results_url(self, builder_name): + # FIXME: Generalize this command to work with non-build.chromium.org builders. + builder = self._tool.chromium_buildbot().builder_with_name(builder_name) + return builder.accumulated_results_url() + + def _baseline_directory(self, builder_name): + port = self._tool.port_factory.get_from_builder_name(builder_name) + return port.baseline_path() + + def _save_baseline(self, data, target_baseline): + if not data: + return + filesystem = self._tool.filesystem + filesystem.maybe_make_directory(filesystem.dirname(target_baseline)) + filesystem.write_binary_file(target_baseline, data) + if not self._tool.scm().exists(target_baseline): + self._tool.scm().add(target_baseline) + + def _test_root(self, test_name): + return os.path.splitext(test_name)[0] + + def _file_name_for_actual_result(self, test_name, suffix): + return "%s-actual.%s" % (self._test_root(test_name), suffix) + + def _file_name_for_expected_result(self, test_name, suffix): + return "%s-expected.%s" % (self._test_root(test_name), suffix) + + def _rebaseline_test(self, builder_name, test_name, suffix): + results_url = self._results_url(builder_name) + baseline_directory = self._baseline_directory(builder_name) + + source_baseline = "%s/%s" % (results_url, self._file_name_for_actual_result(test_name, suffix)) + target_baseline = os.path.join(baseline_directory, self._file_name_for_expected_result(test_name, suffix)) + + print "Retrieving %s." % source_baseline + self._save_baseline(self._tool.web.get_binary(source_baseline, convert_404_to_None=True), target_baseline) + + def execute(self, options, args, tool): + for suffix in _baseline_suffix_list: + self._rebaseline_test(args[0], args[1], suffix) + + +class OptimizeBaselines(AbstractDeclarativeCommand): + name = "optimize-baselines" + help_text = "Reshuffles the baselines for the given tests to use as litte space on disk as possible." + argument_names = "TEST_NAMES" + + def _optimize_baseline(self, test_name): + for suffix in _baseline_suffix_list: + baseline_name = _baseline_name(self._tool.filesystem, test_name, suffix) + if not self._baseline_optimizer.optimize(baseline_name): + print "Hueristics failed to optimize %s" % baseline_name + + def execute(self, options, args, tool): + self._baseline_optimizer = BaselineOptimizer(tool) + self._port = tool.port_factory.get("chromium-win-win7") # FIXME: This should be selectable. + + for test_name in self._port.tests(args): + print "Optimizing %s." % test_name + self._optimize_baseline(test_name) + + +class AnalyzeBaselines(AbstractDeclarativeCommand): + name = "analyze-baselines" + help_text = "Analyzes the baselines for the given tests and prints results that are identical." + argument_names = "TEST_NAMES" + + def _print(self, baseline_name, directories_by_result): + for result, directories in directories_by_result.items(): + if len(directories) <= 1: + continue + results_names = [self._tool.filesystem.join(directory, baseline_name) for directory in directories] + print ' '.join(results_names) + + def _analyze_baseline(self, test_name): + for suffix in _baseline_suffix_list: + baseline_name = _baseline_name(self._tool.filesystem, test_name, suffix) + directories_by_result = self._baseline_optimizer.directories_by_result(baseline_name) + self._print(baseline_name, directories_by_result) + + def execute(self, options, args, tool): + self._baseline_optimizer = BaselineOptimizer(tool) + self._port = tool.port_factory.get("chromium-win-win7") # FIXME: This should be selectable. + + for test_name in self._port.tests(args): + self._analyze_baseline(test_name) + + +class RebaselineExpectations(AbstractDeclarativeCommand): + name = "rebaseline-expectations" + help_text = "Rebaselines the tests indicated in test_expectations.txt." + + def _run_webkit_patch(self, args): + try: + self._tool.executive.run_command([self._tool.path()] + args, cwd=self._tool.scm().checkout_root) + except ScriptError, e: + pass + + def _is_supported_port(self, port_name): + # FIXME: Support non-Chromium ports. + return port_name.startswith('chromium-') + + def _expectations(self, port): + return TestExpectations(port, None, port.test_expectations(), port.test_configuration()) + + def _update_expectations_file(self, port_name): + if not self._is_supported_port(port_name): + return + port = self._tool.port_factory.get(port_name) + expectations = self._expectations(port) + path = port.path_to_test_expectations_file() + self._tool.filesystem.write_text_file(path, expectations.remove_rebaselined_tests(expectations.get_rebaselining_failures())) + + def _tests_to_rebaseline(self, port): + return self._expectations(port).get_rebaselining_failures() + + def _rebaseline_port(self, port_name): + if not self._is_supported_port(port_name): + return + builder_name = builders.builder_name_for_port_name(port_name) + if not builder_name: + return + print "Retrieving results for %s from %s." % (port_name, builder_name) + for test_name in self._tests_to_rebaseline(self._tool.port_factory.get(port_name)): + self._touched_test_names.add(test_name) + print " %s" % test_name + self._run_webkit_patch(['rebaseline-test', builder_name, test_name]) + + def execute(self, options, args, tool): + self._touched_test_names = set([]) + for port_name in tool.port_factory.all_port_names(): + self._rebaseline_port(port_name) + for port_name in tool.port_factory.all_port_names(): + self._update_expectations_file(port_name) + for test_name in self._touched_test_names: + print "Optimizing baselines for %s." % test_name + self._run_webkit_patch(['optimize-baselines', test_name]) + + +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): + failing_tests = build.layout_test_results().tests_matching_failure_types([test_failures.FailureTextMismatch]) + return self._tool.user.prompt_with_list("Which test(s) to rebaseline:", failing_tests, can_choose_multiple=True) + + 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 = tool.port_factory.get_from_builder_name(builder.name()) + + for test in self._tests_to_update(build): + results_url = self._results_url_for_test(build, test) + # Port operates with absolute paths. + expected_file = port.expected_filename(test, '.txt') + print test + self._replace_expectation_with_remote_result(expected_file, results_url) + + # FIXME: We should handle new results too. diff --git a/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py b/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py new file mode 100644 index 000000000..bcb0921aa --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py @@ -0,0 +1,120 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import unittest + +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.commands.rebaseline import * +from webkitpy.tool.mocktool import MockTool +from webkitpy.common.system.executive_mock import MockExecutive + + +class TestRebaseline(unittest.TestCase): + def test_tests_to_update(self): + command = Rebaseline() + command.bind_to_tool(MockTool()) + build = Mock() + OutputCapture().assert_outputs(self, command._tests_to_update, [build]) + + def test_rebaseline_test(self): + command = RebaselineTest() + command.bind_to_tool(MockTool()) + expected_stdout = "Retrieving http://example.com/f/builders/Webkit Linux/results/layout-test-results/userscripts/another-test-actual.txt.\n" + OutputCapture().assert_outputs(self, command._rebaseline_test, ["Webkit Linux", "userscripts/another-test.html", "txt"], expected_stdout=expected_stdout) + + def test_rebaseline_expectations(self): + command = RebaselineExpectations() + tool = MockTool() + command.bind_to_tool(tool) + + for port_name in tool.port_factory.all_port_names(): + port = tool.port_factory.get(port_name) + tool.filesystem.write_text_file(port.path_to_test_expectations_file(), '') + + # Don't enable logging until after we create the mock expectation files as some Port.__init__'s run subcommands. + tool.executive = MockExecutive(should_log=True) + + expected_stdout = """Retrieving results for chromium-cg-mac-leopard from Webkit Mac10.5 (CG). + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-cg-mac-snowleopard from Webkit Mac10.6 (CG). + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-gpu-cg-mac-leopard from Webkit Mac10.5 (CG) - GPU. +Retrieving results for chromium-gpu-cg-mac-snowleopard from Webkit Mac10.6 (CG) - GPU. +Retrieving results for chromium-gpu-mac-snowleopard from Webkit Mac10.6 - GPU. +Retrieving results for chromium-gpu-win-win7 from Webkit Win7 - GPU. +Retrieving results for chromium-gpu-win-xp from Webkit Win - GPU. +Retrieving results for chromium-linux-x86 from Webkit Linux 32. + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-linux-x86_64 from Webkit Linux. + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-mac-leopard from Webkit Mac10.5. + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-mac-snowleopard from Webkit Mac10.6. + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-win-vista from Webkit Vista. + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-win-win7 from Webkit Win7. + userscripts/another-test.html + userscripts/images.svg +Retrieving results for chromium-win-xp from Webkit Win. + userscripts/another-test.html + userscripts/images.svg +Optimizing baselines for userscripts/another-test.html. +Optimizing baselines for userscripts/images.svg. +""" + expected_stderr = """MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.5 (CG)', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.5 (CG)', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.6 (CG)', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.6 (CG)', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Linux 32', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Linux 32', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Linux', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Linux', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.5', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.5', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.6', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.6', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Vista', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Vista', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Win7', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Win7', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Win', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Win', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'optimize-baselines', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'optimize-baselines', 'userscripts/images.svg'], cwd=/mock-checkout +""" + command._tests_to_rebaseline = lambda port: [] if not port.name().find('-gpu-') == -1 else ['userscripts/another-test.html', 'userscripts/images.svg'] + OutputCapture().assert_outputs(self, command.execute, [None, [], tool], expected_stdout=expected_stdout, expected_stderr=expected_stderr) diff --git a/Tools/Scripts/webkitpy/tool/commands/rebaselineserver.py b/Tools/Scripts/webkitpy/tool/commands/rebaselineserver.py new file mode 100644 index 000000000..e7ff86a14 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/rebaselineserver.py @@ -0,0 +1,101 @@ +# 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. + +"""Starts a local HTTP server which displays layout test failures (given a test +results directory), provides comparisons of expected and actual results (both +images and text) and allows one-click rebaselining of tests.""" + +import os +import os.path + +from webkitpy.common import system +from webkitpy.common.net.resultsjsonparser import for_each_test, JSONTestResult +from webkitpy.layout_tests.layout_package import json_results_generator +from webkitpy.tool.commands.abstractlocalservercommand import AbstractLocalServerCommand +from webkitpy.tool.servers.rebaselineserver import get_test_baselines, RebaselineHTTPServer, STATE_NEEDS_REBASELINE + + +class TestConfig(object): + def __init__(self, test_port, layout_tests_directory, results_directory, platforms, filesystem, scm): + self.test_port = test_port + self.layout_tests_directory = layout_tests_directory + self.results_directory = results_directory + self.platforms = platforms + self.filesystem = filesystem + self.scm = scm + + +class RebaselineServer(AbstractLocalServerCommand): + name = "rebaseline-server" + help_text = __doc__ + argument_names = "/path/to/results/directory" + + server = RebaselineHTTPServer + + def _gather_baselines(self, results_json): + # Rebaseline server and it's associated JavaScript expected the tests subtree to + # be key-value pairs instead of hierarchical. + # FIXME: make the rebaseline server use the hierarchical tree. + new_tests_subtree = {} + + def gather_baselines_for_test(test_name, result_dict): + result = JSONTestResult(test_name, result_dict) + if result.did_pass_or_run_as_expected(): + return + result_dict['state'] = STATE_NEEDS_REBASELINE + result_dict['baselines'] = get_test_baselines(test_name, self._test_config) + new_tests_subtree[test_name] = result_dict + + for_each_test(results_json['tests'], gather_baselines_for_test) + results_json['tests'] = new_tests_subtree + + def _prepare_config(self, options, args, tool): + results_directory = args[0] + filesystem = system.filesystem.FileSystem() + scm = self._tool.scm() + + print 'Parsing full_results.json...' + results_json_path = filesystem.join(results_directory, 'full_results.json') + results_json = json_results_generator.load_json(filesystem, results_json_path) + + port = tool.port_factory.get() + layout_tests_directory = port.layout_tests_dir() + platforms = filesystem.listdir(filesystem.join(layout_tests_directory, 'platform')) + self._test_config = TestConfig(port, layout_tests_directory, results_directory, platforms, filesystem, scm) + + print 'Gathering current baselines...' + self._gather_baselines(results_json) + + return { + 'test_config': self._test_config, + "results_json": results_json, + "platforms_json": { + 'platforms': platforms, + 'defaultPlatform': port.name(), + }, + } diff --git a/Tools/Scripts/webkitpy/tool/commands/roll.py b/Tools/Scripts/webkitpy/tool/commands/roll.py new file mode 100644 index 000000000..37481b2b8 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/roll.py @@ -0,0 +1,74 @@ +# Copyright (c) 2011 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 + +from webkitpy.tool import steps + + +class RollChromiumDEPS(AbstractSequencedCommand): + name = "roll-chromium-deps" + help_text = "Updates Chromium DEPS (defaults to the last-known good revision of Chromium)" + argument_names = "[CHROMIUM_REVISION]" + steps = [ + steps.UpdateChromiumDEPS, + steps.PrepareChangeLogForDEPSRoll, + steps.ConfirmDiff, + steps.Commit, + ] + + def _prepare_state(self, options, args, tool): + return { + "chromium_revision": (args and args[0]), + } + + +class PostChromiumDEPSRoll(AbstractSequencedCommand): + name = "post-chromium-deps-roll" + help_text = "Posts a patch to update Chromium DEPS (revision defaults to the last-known good revision of Chromium)" + argument_names = "CHROMIUM_REVISION CHROMIUM_REVISION_NAME" + steps = [ + steps.CleanWorkingDirectory, + steps.Update, + steps.UpdateChromiumDEPS, + steps.PrepareChangeLogForDEPSRoll, + steps.CreateBug, + steps.PostDiff, + ] + + def _prepare_state(self, options, args, tool): + options.review = False + options.request_commit = True + + chromium_revision = args[0] + chromium_revision_name = args[1] + return { + "chromium_revision": chromium_revision, + "bug_title": "Roll Chromium DEPS to %s" % chromium_revision_name, + "bug_description": "A DEPS roll a day keeps the build break away.", + } diff --git a/Tools/Scripts/webkitpy/tool/commands/roll_unittest.py b/Tools/Scripts/webkitpy/tool/commands/roll_unittest.py new file mode 100644 index 000000000..800bc5b96 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/roll_unittest.py @@ -0,0 +1,63 @@ +# Copyright (C) 2011 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.roll import * +from webkitpy.tool.mocktool import MockOptions, MockTool + + +class RollCommandsTest(CommandsTest): + def test_update_chromium_deps(self): + expected_stderr = """Updating Chromium DEPS to 6764 +MOCK: MockDEPS.write_variable(chromium_rev, 6764) +MOCK: user.open_url: file://... +Was that diff correct? +Committed r49824: <http://trac.webkit.org/changeset/49824> +""" + self.assert_execute_outputs(RollChromiumDEPS(), [6764], expected_stderr=expected_stderr) + + def test_update_chromium_deps_older_revision(self): + options = MockOptions(non_interactive=False) + expected_stderr = """Current Chromium DEPS revision 6564 is newer than 5764. +ERROR: Unable to update Chromium DEPS +""" + self.assert_execute_outputs(RollChromiumDEPS(), [5764], options=options, expected_stderr=expected_stderr, expected_exception=SystemExit) + + +class PostRollCommandsTest(CommandsTest): + def test_prepare_state(self): + postroll = PostChromiumDEPSRoll() + options = MockOptions() + tool = MockTool() + lkgr_state = postroll._prepare_state(options, [None, "last-known good revision"], tool) + self.assertEquals(None, lkgr_state["chromium_revision"]) + self.assertEquals("Roll Chromium DEPS to last-known good revision", lkgr_state["bug_title"]) + revision_state = postroll._prepare_state(options, ["1234", "r1234"], tool) + self.assertEquals("1234", revision_state["chromium_revision"]) + self.assertEquals("Roll Chromium DEPS to r1234", revision_state["bug_title"]) diff --git a/Tools/Scripts/webkitpy/tool/commands/sheriffbot.py b/Tools/Scripts/webkitpy/tool/commands/sheriffbot.py new file mode 100644 index 000000000..547309e88 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/sheriffbot.py @@ -0,0 +1,80 @@ +# 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", + ] + + # 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, failure_map): + return None + + def _is_old_failure(self, revision): + return self._tool.status_server.svn_revision(revision) + + def next_work_item(self): + self._irc_bot.process_pending_messages() + return + + def should_proceed_with_work_item(self, failure_map): + # Currently, we don't have any reasons not to proceed with work items. + return True + + def process_work_item(self, failure_map): + return True + + def handle_unexpected_error(self, failure_map, 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/Tools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py b/Tools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py new file mode 100644 index 000000000..735ccab69 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py @@ -0,0 +1,37 @@ +# 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 * + + +class SheriffBotTest(QueuesTest): + pass # No unittests as the moment. diff --git a/Tools/Scripts/webkitpy/tool/commands/stepsequence.py b/Tools/Scripts/webkitpy/tool/commands/stepsequence.py new file mode 100644 index 000000000..b66655446 --- /dev/null +++ b/Tools/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. + +from webkitpy.tool import steps + +from webkitpy.common.checkout.scm import CheckoutNeedsUpdate +from webkitpy.common.system.deprecated_logging import log +from webkitpy.common.system.executive import ScriptError +from webkitpy.tool.bot.queueengine import QueueEngine + + +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/Tools/Scripts/webkitpy/tool/commands/suggestnominations.py b/Tools/Scripts/webkitpy/tool/commands/suggestnominations.py new file mode 100644 index 000000000..c197a1116 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/suggestnominations.py @@ -0,0 +1,247 @@ +# Copyright (c) 2011 Google Inc. All rights reserved. +# Copyright (c) 2011 Code Aurora Forum. 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 +import re + +from webkitpy.common.checkout.changelog import ChangeLogEntry +from webkitpy.common.config.committers import CommitterList +from webkitpy.tool import steps +from webkitpy.tool.grammar import join_with_separators +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +class SuggestNominations(AbstractDeclarativeCommand): + name = "suggest-nominations" + help_text = "Suggest contributors for committer/reviewer nominations" + + def __init__(self): + options = [ + make_option("--committer-minimum", action="store", dest="committer_minimum", type="int", default=10, help="Specify minimum patch count for Committer nominations."), + make_option("--reviewer-minimum", action="store", dest="reviewer_minimum", type="int", default=80, help="Specify minimum patch count for Reviewer nominations."), + make_option("--max-commit-age", action="store", dest="max_commit_age", type="int", default=9, help="Specify max commit age to consider for nominations (in months)."), + make_option("--show-commits", action="store_true", dest="show_commits", default=False, help="Show commit history with nomination suggestions."), + ] + + AbstractDeclarativeCommand.__init__(self, options=options) + # FIXME: This should probably be on the tool somewhere. + self._committer_list = CommitterList() + + _counters_by_name = {} + _counters_by_email = {} + + def _init_options(self, options): + self.committer_minimum = options.committer_minimum + self.reviewer_minimum = options.reviewer_minimum + self.max_commit_age = options.max_commit_age + self.show_commits = options.show_commits + self.verbose = options.verbose + + # FIXME: This should move to scm.py + def _recent_commit_messages(self): + git_log = self._tool.executive.run_command(['git', 'log', '--since="%s months ago"' % self.max_commit_age]) + match_git_svn_id = re.compile(r"\n\n git-svn-id:.*\n", re.MULTILINE) + match_get_log_lines = re.compile(r"^\S.*\n", re.MULTILINE) + match_leading_indent = re.compile(r"^[ ]{4}", re.MULTILINE) + + messages = re.split(r"commit \w{40}", git_log)[1:] # Ignore the first message which will be empty. + for message in messages: + # Remove any lines from git and unindent all the lines + (message, _) = match_git_svn_id.subn("", message) + (message, _) = match_get_log_lines.subn("", message) + (message, _) = match_leading_indent.subn("", message) + yield message.lstrip() # Remove any leading newlines from the log message. + + # e.g. Patch by Eric Seidel <eric@webkit.org> on 2011-09-15 + patch_by_regexp = r'^Patch by (?P<name>.+?)\s+<(?P<email>[^<>]+)> on (?P<date>\d{4}-\d{2}-\d{2})$' + + def _count_recent_patches(self): + # This entire block could be written as a map/reduce over the messages. + for message in self._recent_commit_messages(): + # FIXME: This should use ChangeLogEntry to do the entire parse instead + # of grabbing at its regexps. + dateline_match = re.match(ChangeLogEntry.date_line_regexp, message, re.MULTILINE) + if not dateline_match: + # Modern commit messages don't just dump the ChangeLog entry, but rather + # have a special Patch by line for non-committers. + dateline_match = re.search(self.patch_by_regexp, message, re.MULTILINE) + if not dateline_match: + continue + + author_email = dateline_match.group("email") + if not author_email: + continue + + # We only care about reviewed patches, so make sure it has a valid reviewer line. + reviewer_match = re.search(ChangeLogEntry.reviewed_by_regexp, message, re.MULTILINE) + # We might also want to validate the reviewer name against the committer list. + if not reviewer_match or not reviewer_match.group("reviewer"): + continue + + author_name = dateline_match.group("name") + if not author_name: + continue + + if re.search("([^a-zA-Z]and[^a-zA-Z])|(,)|(@)", author_name): + # This entry seems to have multiple reviewers, or invalid characters, so reject it. + continue + + svn_id_match = re.search(ChangeLogEntry.svn_id_regexp, message, re.MULTILINE) + if svn_id_match: + svn_id = svn_id_match.group("svnid") + if not svn_id_match or not svn_id: + svn_id = "unknown" + commit_date = dateline_match.group("date") + + # See if we already have a contributor with this name or email + counter_by_name = self._counters_by_name.get(author_name) + counter_by_email = self._counters_by_email.get(author_email) + if counter_by_name: + if counter_by_email: + if counter_by_name != counter_by_email: + # Merge these two counters This is for the case where we had + # John Smith (jsmith@gmail.com) and Jonathan Smith (jsmith@apple.com) + # and just found a John Smith (jsmith@apple.com). Now we know the + # two names are the same person + counter_by_name['names'] |= counter_by_email['names'] + counter_by_name['emails'] |= counter_by_email['emails'] + counter_by_name['count'] += counter_by_email.get('count', 0) + self._counters_by_email[author_email] = counter_by_name + else: + # Add email to the existing counter + self._counters_by_email[author_email] = counter_by_name + counter_by_name['emails'] |= set([author_email]) + else: + if counter_by_email: + # Add name to the existing counter + self._counters_by_name[author_name] = counter_by_email + counter_by_email['names'] |= set([author_name]) + else: + # Create new counter + new_counter = {'names': set([author_name]), 'emails': set([author_email]), 'latest_name': author_name, 'latest_email': author_email, 'commits': ""} + self._counters_by_name[author_name] = new_counter + self._counters_by_email[author_email] = new_counter + + assert(self._counters_by_name[author_name] == self._counters_by_email[author_email]) + counter = self._counters_by_name[author_name] + counter['count'] = counter.get('count', 0) + 1 + + if svn_id.isdigit(): + svn_id = "http://trac.webkit.org/changeset/" + svn_id + counter['commits'] += " commit: %s on %s by %s (%s)\n" % (svn_id, commit_date, author_name, author_email) + + return self._counters_by_email + + def _collect_nominations(self, counters_by_email): + nominations = [] + for author_email, counter in counters_by_email.items(): + if author_email != counter['latest_email']: + continue + roles = [] + + contributor = self._committer_list.contributor_by_email(author_email) + + author_name = counter['latest_name'] + patch_count = counter['count'] + + if patch_count >= self.committer_minimum and (not contributor or not contributor.can_commit): + roles.append("committer") + if patch_count >= self.reviewer_minimum and (not contributor or not contributor.can_review): + roles.append("reviewer") + if roles: + nominations.append({ + 'roles': roles, + 'author_name': author_name, + 'author_email': author_email, + 'patch_count': patch_count, + }) + return nominations + + def _print_nominations(self, nominations): + def nomination_cmp(a_nomination, b_nomination): + roles_result = cmp(a_nomination['roles'], b_nomination['roles']) + if roles_result: + return -roles_result + count_result = cmp(a_nomination['patch_count'], b_nomination['patch_count']) + if count_result: + return -count_result + return cmp(a_nomination['author_name'], b_nomination['author_name']) + + for nomination in sorted(nominations, nomination_cmp): + # This is a little bit of a hack, but its convienent to just pass the nomination dictionary to the formating operator. + nomination['roles_string'] = join_with_separators(nomination['roles']).upper() + print "%(roles_string)s: %(author_name)s (%(author_email)s) has %(patch_count)s reviewed patches" % nomination + counter = self._counters_by_email[nomination['author_email']] + + if self.show_commits: + print counter['commits'] + + def _print_counts(self, counters_by_email): + def counter_cmp(a_tuple, b_tuple): + # split the tuples + # the second element is the "counter" structure + _, a_counter = a_tuple + _, b_counter = b_tuple + + count_result = cmp(a_counter['count'], b_counter['count']) + if count_result: + return -count_result + return cmp(a_counter['latest_name'].lower(), b_counter['latest_name'].lower()) + + for author_email, counter in sorted(counters_by_email.items(), counter_cmp): + if author_email != counter['latest_email']: + continue + contributor = self._committer_list.contributor_by_email(author_email) + author_name = counter['latest_name'] + patch_count = counter['count'] + counter['names'] = counter['names'] - set([author_name]) + counter['emails'] = counter['emails'] - set([author_email]) + + alias_list = [] + for alias in counter['names']: + alias_list.append(alias) + for alias in counter['emails']: + alias_list.append(alias) + if alias_list: + print "CONTRIBUTOR: %s (%s) has %d reviewed patches %s" % (author_name, author_email, patch_count, "(aliases: " + ", ".join(alias_list) + ")") + else: + print "CONTRIBUTOR: %s (%s) has %d reviewed patches" % (author_name, author_email, patch_count) + return + + def execute(self, options, args, tool): + self._init_options(options) + patch_counts = self._count_recent_patches() + nominations = self._collect_nominations(patch_counts) + self._print_nominations(nominations) + if self.verbose: + self._print_counts(patch_counts) + + +if __name__ == "__main__": + SuggestNominations() diff --git a/Tools/Scripts/webkitpy/tool/commands/suggestnominations_unittest.py b/Tools/Scripts/webkitpy/tool/commands/suggestnominations_unittest.py new file mode 100644 index 000000000..88be25303 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/suggestnominations_unittest.py @@ -0,0 +1,77 @@ +# Copyright (C) 2011 Google Inc. All rights reserved. +# Copyright (C) 2011 Code Aurora Forum. 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.suggestnominations import SuggestNominations +from webkitpy.tool.mocktool import MockOptions, MockTool + + +class SuggestNominationsTest(CommandsTest): + + mock_git_output = """commit 60831dde5beb22f35aef305a87fca7b5f284c698 +Author: fpizlo@apple.com <fpizlo@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc> +Date: Thu Sep 15 19:56:21 2011 +0000 + + Value profiles collect no information for global variables + https://bugs.webkit.org/show_bug.cgi?id=68143 + + Reviewed by Geoffrey Garen. + + git-svn-id: http://svn.webkit.org/repository/webkit/trunk@95219 268f45cc-cd09-0410-ab3c-d52691b4dbfc +""" + mock_same_author_commit_message = """Value profiles collect no information for global variables +https://bugs.webkit.org/show_bug.cgi?id=68143 + +Reviewed by Geoffrey Garen.""" + + def test_recent_commit_messages(self): + tool = MockTool() + suggest_nominations = SuggestNominations() + suggest_nominations._init_options(options=MockOptions(reviewer_minimum=80, committer_minimum=10, max_commit_age=9, show_commits=False, verbose=False)) + suggest_nominations.bind_to_tool(tool) + + tool.executive.run_command = lambda command: self.mock_git_output + self.assertEqual(list(suggest_nominations._recent_commit_messages()), [self.mock_same_author_commit_message]) + + mock_non_committer_commit_message = """Let TestWebKitAPI work for chromium +https://bugs.webkit.org/show_bug.cgi?id=67756 + +Patch by Xianzhu Wang <wangxianzhu@chromium.org> on 2011-09-15 +Reviewed by Sam Weinig. + +Source/WebKit/chromium: + +* WebKit.gyp:""" + + def test_basic(self): + expected_stdout = "REVIEWER: Xianzhu Wang (wangxianzhu@chromium.org) has 88 reviewed patches\n" + suggest_nominations = SuggestNominations() + suggest_nominations._init_options(options=MockOptions(reviewer_minimum=80, committer_minimum=10, max_commit_age=9, show_commits=False, verbose=False)) + suggest_nominations._recent_commit_messages = lambda: [self.mock_non_committer_commit_message for _ in range(88)] + self.assert_execute_outputs(suggest_nominations, [], expected_stdout=expected_stdout, options=MockOptions(reviewer_minimum=80, committer_minimum=10, max_commit_age=9, show_commits=False, verbose=False)) diff --git a/Tools/Scripts/webkitpy/tool/commands/upload.py b/Tools/Scripts/webkitpy/tool/commands/upload.py new file mode 100644 index 000000000..1436a9378 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/upload.py @@ -0,0 +1,504 @@ +#!/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 + +from webkitpy.tool import steps + +from webkitpy.common.checkout.changelog import parse_bug_id_from_changelog +from webkitpy.common.config.committers import CommitterList +from webkitpy.common.system.deprecated_logging import error, log +from webkitpy.common.system.user import User +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand +from webkitpy.tool.comments import bug_comment_from_svn_revision +from webkitpy.tool.grammar import pluralize, join_with_separators +from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand + + +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) + + +# FIXME: This should be share more logic with AssignToCommitter and CleanPendingCommit +class CleanReviewQueue(AbstractDeclarativeCommand): + name = "clean-review-queue" + help_text = "Clear r? on obsolete patches so they do not appear in the pending-review list." + + def execute(self, options, args, tool): + queue_url = "http://webkit.org/pending-review" + # We do this inefficient dance to be more like webkit.org/pending-review + # bugs.queries.fetch_bug_ids_from_review_queue() doesn't return + # closed bugs, but folks using /pending-review will see them. :( + for patch_id in tool.bugs.queries.fetch_attachment_ids_from_review_queue(): + patch = self._tool.bugs.fetch_attachment(patch_id) + if not patch.review() == "?": + continue + attachment_obsolete_modifier = "" + if patch.is_obsolete(): + attachment_obsolete_modifier = "obsolete " + elif patch.bug().is_closed(): + bug_closed_explanation = " If you would like this patch reviewed, please attach it to a new bug (or re-open this bug before marking it for review again)." + else: + # Neither the patch was obsolete or the bug was closed, next patch... + continue + message = "Cleared review? from %sattachment %s so that this bug does not appear in %s.%s" % (attachment_obsolete_modifier, patch.id(), queue_url, bug_closed_explanation) + 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 AttachToBug(AbstractSequencedCommand): + name = "attach-to-bug" + help_text = "Attach the the file to the bug" + argument_names = "BUGID FILEPATH" + steps = [ + steps.AttachToBug, + ] + + def _prepare_state(self, options, args, tool): + state = {} + state["bug_id"] = args[0] + state["filepath"] = args[1] + return state + + +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: + changed_files = self._tool.scm().changed_files(options.git_commit) + state["changed_files"] = changed_files + bug_id = tool.checkout().bug_id_for_this_commit(options.git_commit, changed_files) + 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.ValidateChangeLogs, + steps.CheckStyle, + steps.ConfirmDiff, + steps.ObsoletePatches, + steps.SuggestReviewers, + steps.EnsureBugIsOpenAndAssigned, + 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.ValidateChangeLogs, + steps.ObsoletePatches, + steps.EnsureBugIsOpenAndAssigned, + 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.ValidateChangeLogs, + steps.CheckStyle, + steps.PromptForBugOrTitle, + steps.CreateBug, + steps.PrepareChangeLog, + steps.EditChangeLog, + steps.ConfirmDiff, + steps.ObsoletePatches, + steps.SuggestReviewers, + steps.EnsureBugIsOpenAndAssigned, + 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_from_changelog(commit_message.message()) or parse_bug_id_from_changelog(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_from_changelog(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/Tools/Scripts/webkitpy/tool/commands/upload_unittest.py b/Tools/Scripts/webkitpy/tool/commands/upload_unittest.py new file mode 100644 index 000000000..0ab0ede8f --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/commands/upload_unittest.py @@ -0,0 +1,149 @@ +# 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 10001 on bug 50000 has invalid committer (non-committer@example.com) +MOCK reassign_bug: bug_id=50000, assignee=eric@webkit.org +-- Begin comment -- +Attachment 10001 was posted by a committer and has review+, assigning to Eric Seidel for commit. +-- End comment -- +Bug 50003 is already assigned to foo@foo.com (None). +Bug 50002 has no non-obsolete patches, ignoring. +""" + self.assert_execute_outputs(AssignToCommitter(), [], expected_stderr=expected_stderr, tool=tool) + + def test_obsolete_attachments(self): + expected_stderr = "Obsoleting 2 old patches on bug 50000\n" + self.assert_execute_outputs(ObsoleteAttachments(), [50000], expected_stderr=expected_stderr) + + def test_post(self): + options = MockOptions() + options.cc = None + options.check_style = True + options.check_style_filter = None + options.comment = None + options.description = "MOCK description" + options.request_commit = False + options.review = True + options.suggest_reviewers = False + expected_stderr = """MOCK: user.open_url: file://... +Was that diff correct? +Obsoleting 2 old patches on bug 50000 +MOCK reassign_bug: bug_id=50000, assignee=None +MOCK add_patch_to_bug: bug_id=50000, description=MOCK description, mark_for_review=True, mark_for_commit_queue=False, mark_for_landing=False +MOCK: user.open_url: http://example.com/50000 +""" + self.assert_execute_outputs(Post(), [50000], options=options, expected_stderr=expected_stderr) + + def test_attach_to_bug(self): + options = MockOptions() + options.comment = "extra comment" + options.description = "file description" + expected_stderr = """MOCK add_attachment_to_bug: bug_id=50000, description=file description filename=None +-- Begin comment -- +extra comment +-- End comment -- +""" + self.assert_execute_outputs(AttachToBug(), [50000, "path/to/file.txt", "file description"], options=options, expected_stderr=expected_stderr) + + def test_attach_to_bug_no_description_or_comment(self): + options = MockOptions() + options.comment = None + options.description = None + expected_stderr = """MOCK add_attachment_to_bug: bug_id=50000, description=file.txt filename=None +""" + self.assert_execute_outputs(AttachToBug(), [50000, "path/to/file.txt"], options=options, expected_stderr=expected_stderr) + + def test_land_safely(self): + expected_stderr = "Obsoleting 2 old patches on bug 50000\nMOCK reassign_bug: bug_id=50000, assignee=None\nMOCK add_patch_to_bug: bug_id=50000, description=Patch for landing, mark_for_review=False, mark_for_commit_queue=False, mark_for_landing=True\n" + self.assert_execute_outputs(LandSafely(), [50000], expected_stderr=expected_stderr) + + def test_prepare_diff_with_arg(self): + self.assert_execute_outputs(Prepare(), [50000]) + + def test_prepare(self): + expected_stderr = "MOCK create_bug\nbug_title: Mock user response\nbug_description: Mock user response\ncomponent: MOCK component\ncc: MOCK cc\n" + self.assert_execute_outputs(Prepare(), [], expected_stderr=expected_stderr) + + def test_upload(self): + options = MockOptions() + options.cc = None + options.check_style = True + options.check_style_filter = None + options.comment = None + options.description = "MOCK description" + options.request_commit = False + options.review = True + options.suggest_reviewers = False + expected_stderr = """MOCK: user.open_url: file://... +Was that diff correct? +Obsoleting 2 old patches on bug 50000 +MOCK reassign_bug: bug_id=50000, assignee=None +MOCK add_patch_to_bug: bug_id=50000, description=MOCK description, mark_for_review=True, mark_for_commit_queue=False, mark_for_landing=False +MOCK: user.open_url: http://example.com/50000 +""" + self.assert_execute_outputs(Upload(), [50000], 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 = 50000 + options.comment = "MOCK comment" + expected_stderr = """Bug: <http://example.com/50000> Bug with two r+'d and cq+'d patches, one of which has an invalid commit-queue setter. +Revision: 9876 +MOCK: user.open_url: http://example.com/50000 +Is this correct? +Adding comment to Bug 50000. +MOCK bug comment: bug_id=50000, cc=None +--- Begin comment --- +MOCK comment + +Committed r9876: <http://trac.webkit.org/changeset/9876> +--- End comment --- + +""" + self.assert_execute_outputs(MarkBugFixed(), [], expected_stderr=expected_stderr, tool=tool, options=options) + + def test_edit_changelog(self): + self.assert_execute_outputs(EditChangeLogs(), []) diff --git a/Tools/Scripts/webkitpy/tool/comments.py b/Tools/Scripts/webkitpy/tool/comments.py new file mode 100755 index 000000000..771953e69 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/comments.py @@ -0,0 +1,42 @@ +# Copyright (c) 2009 Google Inc. All rights reserved. +# Copyright (c) 2009 Apple Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# A tool for automating dealing with bugzilla, posting patches, committing +# patches, etc. + +from webkitpy.common.config import urls + + +def bug_comment_from_svn_revision(svn_revision): + return "Committed r%s: <%s>" % (svn_revision, urls.view_revision_url(svn_revision)) + + +def bug_comment_from_commit_text(scm, commit_text): + svn_revision = scm.svn_revision_from_commit_text(commit_text) + return bug_comment_from_svn_revision(svn_revision) diff --git a/Tools/Scripts/webkitpy/tool/grammar.py b/Tools/Scripts/webkitpy/tool/grammar.py new file mode 100644 index 000000000..8db9826f8 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/grammar.py @@ -0,0 +1,54 @@ +# 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 re + + +def plural(noun): + # This is a dumb plural() implementation that is just enough for our uses. + if re.search("h$", noun): + return noun + "es" + else: + return noun + "s" + + +def pluralize(noun, count): + if count != 1: + noun = plural(noun) + return "%d %s" % (count, noun) + + +def join_with_separators(list_of_strings, separator=', ', only_two_separator=" and ", last_separator=', and '): + if not list_of_strings: + return "" + if len(list_of_strings) == 1: + return list_of_strings[0] + if len(list_of_strings) == 2: + return only_two_separator.join(list_of_strings) + return "%s%s%s" % (separator.join(list_of_strings[:-1]), last_separator, list_of_strings[-1]) diff --git a/Tools/Scripts/webkitpy/tool/grammar_unittest.py b/Tools/Scripts/webkitpy/tool/grammar_unittest.py new file mode 100644 index 000000000..cab71db01 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/grammar_unittest.py @@ -0,0 +1,41 @@ +# 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.grammar import join_with_separators + +class GrammarTest(unittest.TestCase): + + def test_join_with_separators(self): + self.assertEqual(join_with_separators(["one"]), "one") + self.assertEqual(join_with_separators(["one", "two"]), "one and two") + self.assertEqual(join_with_separators(["one", "two", "three"]), "one, two, and three") + +if __name__ == '__main__': + unittest.main() diff --git a/Tools/Scripts/webkitpy/tool/main.py b/Tools/Scripts/webkitpy/tool/main.py new file mode 100755 index 000000000..962379af3 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/main.py @@ -0,0 +1,111 @@ +# Copyright (c) 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. +# +# A tool for automating dealing with bugzilla, posting patches, committing patches, etc. + +from optparse import make_option +import os +import threading + +from webkitpy.common.config.ports import WebKitPort +from webkitpy.common.host import Host +from webkitpy.common.net.irc import ircproxy +from webkitpy.common.net.statusserver import StatusServer +from webkitpy.tool.multicommandtool import MultiCommandTool +from webkitpy.tool import commands + + +class WebKitPatch(MultiCommandTool, Host): + global_options = [ + make_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="enable all logging"), + make_option("-d", "--directory", action="append", dest="patch_directories", default=[], help="Directory to look at for changed files"), + make_option("--dry-run", action="store_true", dest="dry_run", default=False, help="do not touch remote servers"), + make_option("--status-host", action="store", dest="status_host", type="string", help="Hostname (e.g. localhost or commit.webkit.org) where status updates should be posted."), + make_option("--bot-id", action="store", dest="bot_id", type="string", help="Identifier for this bot (if multiple bots are running for a queue)"), + make_option("--irc-password", action="store", dest="irc_password", type="string", help="Password to use when communicating via IRC."), + make_option("--port", action="store", dest="port", default=None, help="Specify a port (e.g., mac, qt, gtk, ...)."), + ] + + def __init__(self, path): + MultiCommandTool.__init__(self) + Host.__init__(self) + self._path = path + self.status_server = StatusServer() + + self.wakeup_event = threading.Event() + self._irc = None + self._deprecated_port = None + + def port(self): + return self._deprecated_port + + def path(self): + return self._path + + def ensure_irc_connected(self, irc_delegate): + if not self._irc: + self._irc = ircproxy.IRCProxy(irc_delegate) + + def irc(self): + # We don't automatically construct IRCProxy here because constructing + # IRCProxy actually connects to IRC. We want clients to explicitly + # connect to IRC. + return self._irc + + def command_completed(self): + if self._irc: + self._irc.disconnect() + + def should_show_in_main_help(self, command): + if not command.show_in_main_help: + return False + if command.requires_local_commits: + return self.scm().supports_local_commits() + return True + + # FIXME: This may be unnecessary since we pass global options to all commands during execute() as well. + def handle_global_options(self, options): + self._initialize_scm(options.patch_directories) + if options.dry_run: + self.scm().dryrun = True + self.bugs.dryrun = True + if options.status_host: + self.status_server.set_host(options.status_host) + if options.bot_id: + self.status_server.set_bot_id(options.bot_id) + if options.irc_password: + self.irc_password = options.irc_password + # If options.port is None, we'll get the default port for this platform. + self._deprecated_port = WebKitPort.port(options.port) + + def should_execute_command(self, command): + if command.requires_local_commits and not self.scm().supports_local_commits(): + failure_reason = "%s requires local commits using %s in %s." % (command.name, self.scm().display_name(), self.scm().checkout_root) + return (False, failure_reason) + return (True, None) diff --git a/Tools/Scripts/webkitpy/tool/mocktool.py b/Tools/Scripts/webkitpy/tool/mocktool.py new file mode 100644 index 000000000..25f82698a --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/mocktool.py @@ -0,0 +1,77 @@ +# Copyright (C) 2011 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 threading + +from webkitpy.common.host_mock import MockHost +from webkitpy.common.net.statusserver_mock import MockStatusServer +from webkitpy.common.net.irc.irc_mock import MockIRC + +# FIXME: Old-style "Ports" need to die and be replaced by modern layout_tests.port which needs to move to common. +from webkitpy.common.config.ports_mock import MockPort + + +# FIXME: This should be moved somewhere in common and renamed +# something without Mock in the name. +class MockOptions(object): + """Mock implementation of optparse.Values.""" + + def __init__(self, **kwargs): + # The caller can set option values using keyword arguments. We don't + # set any values by default because we don't know how this + # object will be used. Generally speaking unit tests should + # subclass this or provider wrapper functions that set a common + # set of options. + for key, value in kwargs.items(): + self.__dict__[key] = value + + +# FIXME: This should be renamed MockWebKitPatch. +class MockTool(MockHost): + def __init__(self, *args, **kwargs): + MockHost.__init__(self, *args, **kwargs) + + self._deprecated_port = MockPort() + self.status_server = MockStatusServer() + + self._irc = None + self.irc_password = "MOCK irc password" + self.wakeup_event = threading.Event() + + def port(self): + return self._deprecated_port + + def path(self): + return "echo" + + def ensure_irc_connected(self, delegate): + if not self._irc: + self._irc = MockIRC() + + def irc(self): + return self._irc diff --git a/Tools/Scripts/webkitpy/tool/mocktool_unittest.py b/Tools/Scripts/webkitpy/tool/mocktool_unittest.py new file mode 100644 index 000000000..cceaa2e0a --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/mocktool_unittest.py @@ -0,0 +1,59 @@ +# 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 mocktool import MockOptions + + +class MockOptionsTest(unittest.TestCase): + # MockOptions() should implement the same semantics that + # optparse.Values does. + + def test_get__set(self): + # Test that we can still set options after we construct the + # object. + options = MockOptions() + options.foo = 'bar' + self.assertEqual(options.foo, 'bar') + + def test_get__unset(self): + # Test that unset options raise an exception (regular Mock + # objects return an object and hence are different from + # optparse.Values()). + options = MockOptions() + self.assertRaises(AttributeError, lambda: options.foo) + + def test_kwarg__set(self): + # Test that keyword arguments work in the constructor. + options = MockOptions(foo='bar') + self.assertEqual(options.foo, 'bar') + + +if __name__ == '__main__': + unittest.main() diff --git a/Tools/Scripts/webkitpy/tool/multicommandtool.py b/Tools/Scripts/webkitpy/tool/multicommandtool.py new file mode 100644 index 000000000..4848ae532 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/multicommandtool.py @@ -0,0 +1,314 @@ +# Copyright (c) 2009 Google Inc. All rights reserved. +# Copyright (c) 2009 Apple Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# MultiCommandTool provides a framework for writing svn-like/git-like tools +# which are called with the following format: +# tool-name [global options] command-name [command options] + +import sys + +from optparse import OptionParser, IndentedHelpFormatter, SUPPRESS_USAGE, make_option + +from webkitpy.tool.grammar import pluralize +from webkitpy.common.system.deprecated_logging import log + + +class TryAgain(Exception): + pass + + +class Command(object): + name = None + show_in_main_help = False + def __init__(self, help_text, argument_names=None, options=None, long_help=None, requires_local_commits=False): + self.help_text = help_text + self.long_help = long_help + self.argument_names = argument_names + self.required_arguments = self._parse_required_arguments(argument_names) + self.options = options + self.requires_local_commits = requires_local_commits + self._tool = None + # option_parser can be overriden by the tool using set_option_parser + # This default parser will be used for standalone_help printing. + self.option_parser = HelpPrintingOptionParser(usage=SUPPRESS_USAGE, add_help_option=False, option_list=self.options) + + # This design is slightly awkward, but we need the + # the tool to be able to create and modify the option_parser + # before it knows what Command to run. + def set_option_parser(self, option_parser): + self.option_parser = option_parser + self._add_options_to_parser() + + def _add_options_to_parser(self): + options = self.options or [] + for option in options: + self.option_parser.add_option(option) + + # The tool calls bind_to_tool on each Command after adding it to its list. + def bind_to_tool(self, tool): + # Command instances can only be bound to one tool at a time. + if self._tool and tool != self._tool: + raise Exception("Command already bound to tool!") + self._tool = tool + + @staticmethod + def _parse_required_arguments(argument_names): + required_args = [] + if not argument_names: + return required_args + split_args = argument_names.split(" ") + for argument in split_args: + if argument[0] == '[': + # For now our parser is rather dumb. Do some minimal validation that + # we haven't confused it. + if argument[-1] != ']': + raise Exception("Failure to parse argument string %s. Argument %s is missing ending ]" % (argument_names, argument)) + else: + required_args.append(argument) + return required_args + + def name_with_arguments(self): + usage_string = self.name + if self.options: + usage_string += " [options]" + if self.argument_names: + usage_string += " " + self.argument_names + return usage_string + + def parse_args(self, args): + return self.option_parser.parse_args(args) + + def check_arguments_and_execute(self, options, args, tool=None): + if len(args) < len(self.required_arguments): + log("%s required, %s provided. Provided: %s Required: %s\nSee '%s help %s' for usage." % ( + pluralize("argument", len(self.required_arguments)), + pluralize("argument", len(args)), + "'%s'" % " ".join(args), + " ".join(self.required_arguments), + tool.name(), + self.name)) + return 1 + return self.execute(options, args, tool) or 0 + + def standalone_help(self): + help_text = self.name_with_arguments().ljust(len(self.name_with_arguments()) + 3) + self.help_text + "\n\n" + if self.long_help: + help_text += "%s\n\n" % self.long_help + help_text += self.option_parser.format_option_help(IndentedHelpFormatter()) + return help_text + + def execute(self, options, args, tool): + raise NotImplementedError, "subclasses must implement" + + # main() exists so that Commands can be turned into stand-alone scripts. + # Other parts of the code will likely require modification to work stand-alone. + def main(self, args=sys.argv): + (options, args) = self.parse_args(args) + # Some commands might require a dummy tool + return self.check_arguments_and_execute(options, args) + + +# FIXME: This should just be rolled into Command. help_text and argument_names do not need to be instance variables. +class AbstractDeclarativeCommand(Command): + help_text = None + argument_names = None + long_help = None + def __init__(self, options=None, **kwargs): + Command.__init__(self, self.help_text, self.argument_names, options=options, long_help=self.long_help, **kwargs) + + +class HelpPrintingOptionParser(OptionParser): + def __init__(self, epilog_method=None, *args, **kwargs): + self.epilog_method = epilog_method + OptionParser.__init__(self, *args, **kwargs) + + def error(self, msg): + self.print_usage(sys.stderr) + error_message = "%s: error: %s\n" % (self.get_prog_name(), msg) + # This method is overriden to add this one line to the output: + error_message += "\nType \"%s --help\" to see usage.\n" % self.get_prog_name() + self.exit(1, error_message) + + # We override format_epilog to avoid the default formatting which would paragraph-wrap the epilog + # and also to allow us to compute the epilog lazily instead of in the constructor (allowing it to be context sensitive). + def format_epilog(self, epilog): + if self.epilog_method: + return "\n%s\n" % self.epilog_method() + return "" + + +class HelpCommand(AbstractDeclarativeCommand): + name = "help" + help_text = "Display information about this program or its subcommands" + argument_names = "[COMMAND]" + + def __init__(self): + options = [ + make_option("-a", "--all-commands", action="store_true", dest="show_all_commands", help="Print all available commands"), + ] + AbstractDeclarativeCommand.__init__(self, options) + self.show_all_commands = False # A hack used to pass --all-commands to _help_epilog even though it's called by the OptionParser. + + def _help_epilog(self): + # Only show commands which are relevant to this checkout's SCM system. Might this be confusing to some users? + if self.show_all_commands: + epilog = "All %prog commands:\n" + relevant_commands = self._tool.commands[:] + else: + epilog = "Common %prog commands:\n" + relevant_commands = filter(self._tool.should_show_in_main_help, self._tool.commands) + longest_name_length = max(map(lambda command: len(command.name), relevant_commands)) + relevant_commands.sort(lambda a, b: cmp(a.name, b.name)) + command_help_texts = map(lambda command: " %s %s\n" % (command.name.ljust(longest_name_length), command.help_text), relevant_commands) + epilog += "%s\n" % "".join(command_help_texts) + epilog += "See '%prog help --all-commands' to list all commands.\n" + epilog += "See '%prog help COMMAND' for more information on a specific command.\n" + return epilog.replace("%prog", self._tool.name()) # Use of %prog here mimics OptionParser.expand_prog_name(). + + # FIXME: This is a hack so that we don't show --all-commands as a global option: + def _remove_help_options(self): + for option in self.options: + self.option_parser.remove_option(option.get_opt_string()) + + def execute(self, options, args, tool): + if args: + command = self._tool.command_by_name(args[0]) + if command: + print command.standalone_help() + return 0 + + self.show_all_commands = options.show_all_commands + self._remove_help_options() + self.option_parser.print_help() + return 0 + + +class MultiCommandTool(object): + global_options = None + + def __init__(self, name=None, commands=None): + self._name = name or OptionParser(prog=name).get_prog_name() # OptionParser has nice logic for fetching the name. + # Allow the unit tests to disable command auto-discovery. + self.commands = commands or [cls() for cls in self._find_all_commands() if cls.name] + self.help_command = self.command_by_name(HelpCommand.name) + # Require a help command, even if the manual test list doesn't include one. + if not self.help_command: + self.help_command = HelpCommand() + self.commands.append(self.help_command) + for command in self.commands: + command.bind_to_tool(self) + + @classmethod + def _add_all_subclasses(cls, class_to_crawl, seen_classes): + for subclass in class_to_crawl.__subclasses__(): + if subclass not in seen_classes: + seen_classes.add(subclass) + cls._add_all_subclasses(subclass, seen_classes) + + @classmethod + def _find_all_commands(cls): + commands = set() + cls._add_all_subclasses(Command, commands) + return sorted(commands) + + def name(self): + return self._name + + def _create_option_parser(self): + usage = "Usage: %prog [options] COMMAND [ARGS]" + return HelpPrintingOptionParser(epilog_method=self.help_command._help_epilog, prog=self.name(), usage=usage) + + @staticmethod + def _split_command_name_from_args(args): + # Assume the first argument which doesn't start with "-" is the command name. + command_index = 0 + for arg in args: + if arg[0] != "-": + break + command_index += 1 + else: + return (None, args[:]) + + command = args[command_index] + return (command, args[:command_index] + args[command_index + 1:]) + + def command_by_name(self, command_name): + for command in self.commands: + if command_name == command.name: + return command + return None + + def path(self): + raise NotImplementedError, "subclasses must implement" + + def command_completed(self): + pass + + def should_show_in_main_help(self, command): + return command.show_in_main_help + + def should_execute_command(self, command): + return True + + def _add_global_options(self, option_parser): + global_options = self.global_options or [] + for option in global_options: + option_parser.add_option(option) + + def handle_global_options(self, options): + pass + + def main(self, argv=sys.argv): + (command_name, args) = self._split_command_name_from_args(argv[1:]) + + option_parser = self._create_option_parser() + self._add_global_options(option_parser) + + command = self.command_by_name(command_name) or self.help_command + if not command: + option_parser.error("%s is not a recognized command" % command_name) + + command.set_option_parser(option_parser) + (options, args) = command.parse_args(args) + self.handle_global_options(options) + + (should_execute, failure_reason) = self.should_execute_command(command) + if not should_execute: + log(failure_reason) + return 0 # FIXME: Should this really be 0? + + while True: + try: + result = command.check_arguments_and_execute(options, args, self) + break + except TryAgain, e: + pass + + self.command_completed() + return result diff --git a/Tools/Scripts/webkitpy/tool/multicommandtool_unittest.py b/Tools/Scripts/webkitpy/tool/multicommandtool_unittest.py new file mode 100644 index 000000000..c19095c3e --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/multicommandtool_unittest.py @@ -0,0 +1,177 @@ +# Copyright (c) 2009 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import sys +import unittest + +from optparse import make_option + +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.tool.multicommandtool import MultiCommandTool, Command, TryAgain + + +class TrivialCommand(Command): + name = "trivial" + show_in_main_help = True + def __init__(self, **kwargs): + Command.__init__(self, "help text", **kwargs) + + def execute(self, options, args, tool): + pass + + +class UncommonCommand(TrivialCommand): + name = "uncommon" + show_in_main_help = False + + +class LikesToRetry(Command): + name = "likes-to-retry" + show_in_main_help = True + + def __init__(self, **kwargs): + Command.__init__(self, "help text", **kwargs) + self.execute_count = 0 + + def execute(self, options, args, tool): + self.execute_count += 1 + if self.execute_count < 2: + raise TryAgain() + + +class CommandTest(unittest.TestCase): + def test_name_with_arguments(self): + command_with_args = TrivialCommand(argument_names="ARG1 ARG2") + self.assertEqual(command_with_args.name_with_arguments(), "trivial ARG1 ARG2") + + command_with_args = TrivialCommand(options=[make_option("--my_option")]) + self.assertEqual(command_with_args.name_with_arguments(), "trivial [options]") + + def test_parse_required_arguments(self): + self.assertEqual(Command._parse_required_arguments("ARG1 ARG2"), ["ARG1", "ARG2"]) + self.assertEqual(Command._parse_required_arguments("[ARG1] [ARG2]"), []) + self.assertEqual(Command._parse_required_arguments("[ARG1] ARG2"), ["ARG2"]) + # Note: We might make our arg parsing smarter in the future and allow this type of arguments string. + self.assertRaises(Exception, Command._parse_required_arguments, "[ARG1 ARG2]") + + def test_required_arguments(self): + two_required_arguments = TrivialCommand(argument_names="ARG1 ARG2 [ARG3]") + expected_missing_args_error = "2 arguments required, 1 argument provided. Provided: 'foo' Required: ARG1 ARG2\nSee 'trivial-tool help trivial' for usage.\n" + exit_code = OutputCapture().assert_outputs(self, two_required_arguments.check_arguments_and_execute, [None, ["foo"], TrivialTool()], expected_stderr=expected_missing_args_error) + self.assertEqual(exit_code, 1) + + +class TrivialTool(MultiCommandTool): + def __init__(self, commands=None): + MultiCommandTool.__init__(self, name="trivial-tool", commands=commands) + + def path(self): + return __file__ + + def should_execute_command(self, command): + return (True, None) + + +class MultiCommandToolTest(unittest.TestCase): + def _assert_split(self, args, expected_split): + self.assertEqual(MultiCommandTool._split_command_name_from_args(args), expected_split) + + def test_split_args(self): + # MultiCommandToolTest._split_command_name_from_args returns: (command, args) + full_args = ["--global-option", "command", "--option", "arg"] + full_args_expected = ("command", ["--global-option", "--option", "arg"]) + self._assert_split(full_args, full_args_expected) + + full_args = [] + full_args_expected = (None, []) + self._assert_split(full_args, full_args_expected) + + full_args = ["command", "arg"] + full_args_expected = ("command", ["arg"]) + self._assert_split(full_args, full_args_expected) + + def test_command_by_name(self): + # This also tests Command auto-discovery. + tool = TrivialTool() + self.assertEqual(tool.command_by_name("trivial").name, "trivial") + self.assertEqual(tool.command_by_name("bar"), None) + + def _assert_tool_main_outputs(self, tool, main_args, expected_stdout, expected_stderr = "", expected_exit_code=0): + exit_code = OutputCapture().assert_outputs(self, tool.main, [main_args], expected_stdout=expected_stdout, expected_stderr=expected_stderr) + self.assertEqual(exit_code, expected_exit_code) + + def test_retry(self): + likes_to_retry = LikesToRetry() + tool = TrivialTool(commands=[likes_to_retry]) + tool.main(["tool", "likes-to-retry"]) + self.assertEqual(likes_to_retry.execute_count, 2) + + def test_global_help(self): + tool = TrivialTool(commands=[TrivialCommand(), UncommonCommand()]) + expected_common_commands_help = """Usage: trivial-tool [options] COMMAND [ARGS] + +Options: + -h, --help show this help message and exit + +Common trivial-tool commands: + trivial help text + +See 'trivial-tool help --all-commands' to list all commands. +See 'trivial-tool help COMMAND' for more information on a specific command. + +""" + self._assert_tool_main_outputs(tool, ["tool"], expected_common_commands_help) + self._assert_tool_main_outputs(tool, ["tool", "help"], expected_common_commands_help) + expected_all_commands_help = """Usage: trivial-tool [options] COMMAND [ARGS] + +Options: + -h, --help show this help message and exit + +All trivial-tool commands: + help Display information about this program or its subcommands + trivial help text + uncommon help text + +See 'trivial-tool help --all-commands' to list all commands. +See 'trivial-tool help COMMAND' for more information on a specific command. + +""" + self._assert_tool_main_outputs(tool, ["tool", "help", "--all-commands"], expected_all_commands_help) + # Test that arguments can be passed before commands as well + self._assert_tool_main_outputs(tool, ["tool", "--all-commands", "help"], expected_all_commands_help) + + + def test_command_help(self): + command_with_options = TrivialCommand(options=[make_option("--my_option")], long_help="LONG HELP") + tool = TrivialTool(commands=[command_with_options]) + expected_subcommand_help = "trivial [options] help text\n\nLONG HELP\n\nOptions:\n --my_option=MY_OPTION\n\n" + self._assert_tool_main_outputs(tool, ["tool", "help", "trivial"], expected_subcommand_help) + + +if __name__ == "__main__": + unittest.main() diff --git a/Tools/Scripts/webkitpy/tool/servers/__init__.py b/Tools/Scripts/webkitpy/tool/servers/__init__.py new file mode 100644 index 000000000..ef65bee5b --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/servers/__init__.py @@ -0,0 +1 @@ +# Required for Python to search this directory for module files diff --git a/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/index.html b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/index.html new file mode 100644 index 000000000..f40a34d5b --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/index.html @@ -0,0 +1,182 @@ +<!DOCTYPE html> +<!-- + 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. +--> +<html> +<head> + <title>Layout Test Rebaseline Server</title> + <link rel="stylesheet" href="/main.css" type="text/css"> + <script src="/util.js"></script> + <script src="/loupe.js"></script> + <script src="/main.js"></script> + <script src="/queue.js"></script> +</head> +<body class="loading"> + +<pre id="log" style="display: none"></pre> +<div id="queue" style="display: none"> + Queue: + <select id="queue-select" size="10"></select> + <button id="remove-queue-selection">Remove selection</button> + <button id="rebaseline-queue">Rebaseline queue</button> +</div> + +<div id="header"> + <div id="controls"> + <!-- Add a dummy <select> node so that this lines up with the text on the left --> + <select style="visibility: hidden"></select> + <span id="toggle-sort" class="link">Sort tests by metric</span> + <span class="divider">|</span> + <span id="toggle-log" class="link">Log</span> + <span class="divider">|</span> + <a href="/quitquitquit">Exit</a> + </div> + + <span id="selectors"> + <label> + Failure type: + <select id="failure-type-selector"></select> + </label> + + <label> + Directory: + <select id="directory-selector"></select> + </label> + + <label> + Test: + <select id="test-selector"></select> + </label> + </span> + + <a id="test-link" target="_blank">View test</a> + + <span id="nav-buttons"> + <button id="previous-test">«</button> + <span id="test-index"></span> of <span id="test-count"></span> + <button id="next-test">»</button> + </span> +</div> + +<table id="test-output"> + <thead id="labels"> + <tr> + <th>Expected</th> + <th>Actual</th> + <th>Diff</th> + </tr> + </thead> + <tbody id="image-outputs" style="display: none"> + <tr> + <td colspan="3"><h2>Image</h2></td> + </tr> + <tr> + <td><img id="expected-image"></td> + <td><img id="actual-image"></td> + <td> + <canvas id="diff-canvas" width="800" height="600"></canvas> + <div id="diff-checksum" style="display: none"> + <h3>Checksum mismatch</h3> + Expected: <span id="expected-checksum"></span><br> + Actual: <span id="actual-checksum"></span> + </div> + </td> + </tr> + </tbody> + <tbody id="text-outputs" style="display: none"> + <tr> + <td colspan="3"><h2>Text</h2></td> + </tr> + <tr> + <td><pre id="expected-text" class="text-output"></pre></td> + <td><pre id="actual-text" class="text-output"></pre></td> + <td><div id="diff-text-pretty" class="text-output"></div></td> + </tr> + </tbody> +</table> + +<div id="footer"> + <label>State: <span id="state"></span></label> + <label>Existing baselines: <span id="current-baselines"></span></label> + <label> + Baseline target: + <select id="baseline-target"></select> + </label> + <label> + Move current baselines to: + <select id="baseline-move-to"> + <option value="none">Nowhere (replace)</option> + </select> + </label> + + <!-- Add a dummy <button> node so that this lines up with the text on the right --> + <button style="visibility: hidden; padding-left: 0; padding-right: 0;"></button> + + <div id="action-buttons"> + <span id="toggle-queue" class="link">Queue</span> + <button id="add-to-rebaseline-queue">Add to rebaseline queue</button> + </div> +</div> + +<table id="loupe" style="display: none"> + <tr> + <td colspan="3" id="loupe-info"> + <span id="loupe-close" class="link">Close</span> + <label>Coordinate: <span id="loupe-coordinate"></span></label> + </td> + </tr> + <tr> + <td> + <div class="loupe-container"> + <canvas id="expected-loupe" width="210" height="210"></canvas> + <div class="center-highlight"></div> + </div> + </td> + <td> + <div class="loupe-container"> + <canvas id="actual-loupe" width="210" height="210"></canvas> + <div class="center-highlight"></div> + </div> + </td> + <td> + <div class="loupe-container"> + <canvas id="diff-loupe" width="210" height="210"></canvas> + <div class="center-highlight"></div> + </div> + </td> + </tr> + <tr id="loupe-colors"> + <td><label>Exp. color: <span id="expected-loupe-color"></span></label></td> + <td><label>Actual color: <span id="actual-loupe-color"></span></label></td> + <td><label>Diff color: <span id="diff-loupe-color"></span></label></td> + </tr> +</table> + +</body> +</html> diff --git a/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/loupe.js b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/loupe.js new file mode 100644 index 000000000..41f977ac8 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/loupe.js @@ -0,0 +1,144 @@ +/* + * 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. + */ + +var LOUPE_MAGNIFICATION_FACTOR = 10; + +function Loupe() +{ + this._node = $('loupe'); + this._currentCornerX = -1; + this._currentCornerY = -1; + + var self = this; + + function handleOutputClick(event) { self._handleOutputClick(event); } + $('expected-image').addEventListener('click', handleOutputClick); + $('actual-image').addEventListener('click', handleOutputClick); + $('diff-canvas').addEventListener('click', handleOutputClick); + + function handleLoupeClick(event) { self._handleLoupeClick(event); } + $('expected-loupe').addEventListener('click', handleLoupeClick); + $('actual-loupe').addEventListener('click', handleLoupeClick); + $('diff-loupe').addEventListener('click', handleLoupeClick); + + function hide(event) { self.hide(); } + $('loupe-close').addEventListener('click', hide); +} + +Loupe.prototype._handleOutputClick = function(event) +{ + // The -1 compensates for the border around the image/canvas. + this._showFor(event.offsetX - 1, event.offsetY - 1); +}; + +Loupe.prototype._handleLoupeClick = function(event) +{ + var deltaX = Math.floor(event.offsetX/LOUPE_MAGNIFICATION_FACTOR); + var deltaY = Math.floor(event.offsetY/LOUPE_MAGNIFICATION_FACTOR); + + this._showFor( + this._currentCornerX + deltaX, this._currentCornerY + deltaY); +} + +Loupe.prototype.hide = function() +{ + this._node.style.display = 'none'; +}; + +Loupe.prototype._showFor = function(x, y) +{ + this._fillFromImage(x, y, 'expected', $('expected-image')); + this._fillFromImage(x, y, 'actual', $('actual-image')); + this._fillFromCanvas(x, y, 'diff', $('diff-canvas')); + + this._node.style.display = ''; +}; + +Loupe.prototype._fillFromImage = function(x, y, type, sourceImage) +{ + var tempCanvas = document.createElement('canvas'); + tempCanvas.width = sourceImage.width; + tempCanvas.height = sourceImage.height; + var tempContext = tempCanvas.getContext('2d'); + + tempContext.drawImage(sourceImage, 0, 0); + + this._fillFromCanvas(x, y, type, tempCanvas); +}; + +Loupe.prototype._fillFromCanvas = function(x, y, type, canvas) +{ + var context = canvas.getContext('2d'); + var sourceImageData = + context.getImageData(0, 0, canvas.width, canvas.height); + + var targetCanvas = $(type + '-loupe'); + var targetContext = targetCanvas.getContext('2d'); + targetContext.fillStyle = 'rgba(255, 255, 255, 1)'; + targetContext.fillRect(0, 0, targetCanvas.width, targetCanvas.height); + + var sourceXOffset = (targetCanvas.width/LOUPE_MAGNIFICATION_FACTOR - 1)/2; + var sourceYOffset = (targetCanvas.height/LOUPE_MAGNIFICATION_FACTOR - 1)/2; + + function readPixelComponent(x, y, component) { + var offset = (y * sourceImageData.width + x) * 4 + component; + return sourceImageData.data[offset]; + } + + for (var i = -sourceXOffset; i <= sourceXOffset; i++) { + for (var j = -sourceYOffset; j <= sourceYOffset; j++) { + var sourceX = x + i; + var sourceY = y + j; + + var sourceR = readPixelComponent(sourceX, sourceY, 0); + var sourceG = readPixelComponent(sourceX, sourceY, 1); + var sourceB = readPixelComponent(sourceX, sourceY, 2); + var sourceA = readPixelComponent(sourceX, sourceY, 3)/255; + sourceA = Math.round(sourceA * 10)/10; + + var targetX = (i + sourceXOffset) * LOUPE_MAGNIFICATION_FACTOR; + var targetY = (j + sourceYOffset) * LOUPE_MAGNIFICATION_FACTOR; + var colorString = + sourceR + ', ' + sourceG + ', ' + sourceB + ', ' + sourceA; + targetContext.fillStyle = 'rgba(' + colorString + ')'; + targetContext.fillRect( + targetX, targetY, + LOUPE_MAGNIFICATION_FACTOR, LOUPE_MAGNIFICATION_FACTOR); + + if (i == 0 && j == 0) { + $('loupe-coordinate').textContent = sourceX + ', ' + sourceY; + $(type + '-loupe-color').textContent = colorString; + } + } + } + + this._currentCornerX = x - sourceXOffset; + this._currentCornerY = y - sourceYOffset; +}; diff --git a/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/main.css b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/main.css new file mode 100644 index 000000000..280c3b2f9 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/main.css @@ -0,0 +1,313 @@ +/* + * 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. + */ + +body { + font-size: 12px; + font-family: Helvetica, Arial, sans-serif; + padding: 0; + margin: 0; +} + +.loading { + opacity: 0.5; +} + +div { + margin: 0; +} + +a, .link { + color: #aaf; + text-decoration: underline; + cursor: pointer; +} + +.link.selected { + color: #fff; + font-weight: bold; + text-decoration: none; +} + +#log, +#queue { + padding: .25em 0 0 .25em; + position: absolute; + right: 0; + height: 200px; + overflow: auto; + background: #fff; + -webkit-box-shadow: 1px 1px 5px rgba(0, 0, 0, .5); +} + +#log { + top: 2em; + width: 500px; +} + +#queue { + bottom: 3em; + width: 400px; +} + +#queue-select { + display: block; + width: 390px; +} + +#header, +#footer { + padding: .5em 1em; + background: #333; + color: #fff; + -webkit-box-shadow: 0 1px 5px rgba(0, 0, 0, 0.5); +} + +#header { + margin-bottom: 1em; +} + +#header .divider, +#footer .divider { + opacity: .3; + padding: 0 .5em; +} + +#header label, +#footer label { + padding-right: 1em; + color: #ccc; +} + +#test-link { + margin-right: 1em; +} + +#header label span, +#footer label span { + color: #fff; + font-weight: bold; +} + +#nav-buttons { + white-space: nowrap; +} + +#nav-buttons button { + background: #fff; + border: 0; + border-radius: 10px; +} + +#nav-buttons button:active { + -webkit-box-shadow: 0 0 5px #33f inset; + background: #aaa; +} + +#nav-buttons button[disabled] { + opacity: .5; +} + +#controls { + float: right; +} + +.disabled-control { + color: #888; +} + +#test-output { + border-spacing: 0; + border-collapse: collapse; + margin: 0 auto; + width: 100%; +} + +#test-output td, +#test-output th { + padding: 0; + vertical-align: top; +} + +#image-outputs img, +#image-outputs canvas, +#image-outputs #diff-checksum { + width: 800px; + height: 600px; + border: solid 1px #ddd; + -webkit-user-select: none; + -webkit-user-drag: none; +} + +#image-outputs img, +#image-outputs canvas { + cursor: crosshair; +} + +#image-outputs img.loading, +#image-outputs canvas.loading { + opacity: .5; +} + +#image-outputs #actual-image { + margin: 0 1em; +} + +#test-output #labels th { + text-align: center; + color: #666; +} + +#text-outputs .text-output { + height: 600px; + width: 800px; + overflow: auto; +} + +#test-output h2 { + border-bottom: solid 1px #ccc; + font-weight: bold; + margin: 0; + background: #eee; +} + +#footer { + position: absolute; + bottom: 0; + left: 0; + right: 0; + margin-top: 1em; +} + +#state.needs_rebaseline { + color: yellow; +} + +#state.rebaseline_failed { + color: red; +} + +#state.rebaseline_succeeded { + color: green; +} + +#state.in_queue { + color: gray; +} + +#current-baselines { + font-weight: normal !important; +} + +#current-baselines .platform { + font-weight: bold; +} + +#current-baselines a { + color: #ddf; +} + +#current-baselines .was-used-for-test { + color: #aaf; + font-weight: bold; +} + +#action-buttons { + float: right; +} + +#action-buttons .link { + margin-right: 1em; +} + +#footer button { + padding: 1em; +} + +#loupe { + -webkit-box-shadow: 2px 2px 5px rgba(0, 0, 0, .5); + position: absolute; + width: 634px; + top: 50%; + left: 50%; + margin-left: -151px; + margin-top: -50px; + background: #fff; + border-spacing: 0; + border-collapse: collapse; +} + +#loupe td { + padding: 0; + border: solid 1px #ccc; +} + +#loupe label { + color: #999; + padding-right: 1em; +} + +#loupe span { + color: #000; + font-weight: bold; +} + +#loupe canvas { + cursor: crosshair; +} + +#loupe #loupe-close { + float: right; +} + +#loupe #loupe-info { + background: #eee; + padding: .3em .5em; +} + +#loupe #loupe-colors td { + text-align: center; +} + +#loupe .loupe-container { + position: relative; + width: 210px; + height: 210px; +} + +#loupe .center-highlight { + position: absolute; + width: 10px; + height: 10px; + top: 50%; + left: 50%; + margin-left: -5px; + margin-top: -5px; + outline: solid 1px #999; +} diff --git a/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/main.js b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/main.js new file mode 100644 index 000000000..5e1fa52c1 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/main.js @@ -0,0 +1,577 @@ +/* + * 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. + */ + +var ALL_DIRECTORY_PATH = '[all]'; + +var STATE_NEEDS_REBASELINE = 'needs_rebaseline'; +var STATE_REBASELINE_FAILED = 'rebaseline_failed'; +var STATE_REBASELINE_SUCCEEDED = 'rebaseline_succeeded'; +var STATE_IN_QUEUE = 'in_queue'; +var STATE_TO_DISPLAY_STATE = {}; +STATE_TO_DISPLAY_STATE[STATE_NEEDS_REBASELINE] = 'Needs rebaseline'; +STATE_TO_DISPLAY_STATE[STATE_REBASELINE_FAILED] = 'Rebaseline failed'; +STATE_TO_DISPLAY_STATE[STATE_REBASELINE_SUCCEEDED] = 'Rebaseline succeeded'; +STATE_TO_DISPLAY_STATE[STATE_IN_QUEUE] = 'In queue'; + +var results; +var testsByFailureType = {}; +var testsByDirectory = {}; +var selectedTests = []; +var loupe; +var queue; +var shouldSortTestsByMetric = false; + +function main() +{ + $('failure-type-selector').addEventListener('change', selectFailureType); + $('directory-selector').addEventListener('change', selectDirectory); + $('test-selector').addEventListener('change', selectTest); + $('next-test').addEventListener('click', nextTest); + $('previous-test').addEventListener('click', previousTest); + + $('toggle-log').addEventListener('click', function() { toggle('log'); }); + disableSorting(); + + loupe = new Loupe(); + queue = new RebaselineQueue(); + + document.addEventListener('keydown', function(event) { + if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { + return; + } + + switch (event.keyIdentifier) { + case 'Left': + event.preventDefault(); + previousTest(); + break; + case 'Right': + event.preventDefault(); + nextTest(); + break; + case 'U+0051': // q + queue.addCurrentTest(); + break; + case 'U+0058': // x + queue.removeCurrentTest(); + break; + case 'U+0052': // r + queue.rebaseline(); + break; + } + }); + + loadText('/platforms.json', function(text) { + var platforms = JSON.parse(text); + platforms.platforms.forEach(function(platform) { + var platformOption = document.createElement('option'); + platformOption.value = platform; + platformOption.textContent = platform; + + var targetOption = platformOption.cloneNode(true); + targetOption.selected = platform == platforms.defaultPlatform; + $('baseline-target').appendChild(targetOption); + $('baseline-move-to').appendChild(platformOption.cloneNode(true)); + }); + }); + + loadText('/results.json', function(text) { + results = JSON.parse(text); + displayResults(); + }); +} + +/** + * Groups test results by failure type. + */ +function displayResults() +{ + var failureTypeSelector = $('failure-type-selector'); + var failureTypes = []; + + for (var testName in results.tests) { + var test = results.tests[testName]; + if (test.actual == 'PASS') { + continue; + } + var failureType = test.actual + ' (expected ' + test.expected + ')'; + if (!(failureType in testsByFailureType)) { + testsByFailureType[failureType] = []; + failureTypes.push(failureType); + } + testsByFailureType[failureType].push(testName); + } + + // Sort by number of failures + failureTypes.sort(function(a, b) { + return testsByFailureType[b].length - testsByFailureType[a].length; + }); + + for (var i = 0, failureType; failureType = failureTypes[i]; i++) { + var failureTypeOption = document.createElement('option'); + failureTypeOption.value = failureType; + failureTypeOption.textContent = failureType + ' - ' + testsByFailureType[failureType].length + ' tests'; + failureTypeSelector.appendChild(failureTypeOption); + } + + selectFailureType(); + + document.body.className = ''; +} + +function enableSorting() +{ + $('toggle-sort').onclick = function() { + shouldSortTestsByMetric = !shouldSortTestsByMetric; + // Regenerates the list of tests; this alphabetizes, and + // then re-sorts if we turned sorting on. + selectDirectory(); + } + $('toggle-sort').classList.remove('disabled-control'); +} + +function disableSorting() +{ + $('toggle-sort').onclick = function() { return false; } + $('toggle-sort').classList.add('disabled-control'); +} + +/** + * For a given failure type, gets all the tests and groups them by directory + * (populating the directory selector with them). + */ +function selectFailureType() +{ + var selectedFailureType = getSelectValue('failure-type-selector'); + var tests = testsByFailureType[selectedFailureType]; + + testsByDirectory = {} + var displayDirectoryNamesByDirectory = {}; + var directories = []; + + // Include a special option for all tests + testsByDirectory[ALL_DIRECTORY_PATH] = tests; + displayDirectoryNamesByDirectory[ALL_DIRECTORY_PATH] = 'all'; + directories.push(ALL_DIRECTORY_PATH); + + // Roll up tests by ancestor directories + tests.forEach(function(test) { + var pathPieces = test.split('/'); + var pathDirectories = pathPieces.slice(0, pathPieces.length -1); + var ancestorDirectory = ''; + + pathDirectories.forEach(function(pathDirectory, index) { + ancestorDirectory += pathDirectory + '/'; + if (!(ancestorDirectory in testsByDirectory)) { + testsByDirectory[ancestorDirectory] = []; + var displayDirectoryName = new Array(index * 6).join(' ') + pathDirectory; + displayDirectoryNamesByDirectory[ancestorDirectory] = displayDirectoryName; + directories.push(ancestorDirectory); + } + + testsByDirectory[ancestorDirectory].push(test); + }); + }); + + directories.sort(); + + var directorySelector = $('directory-selector'); + directorySelector.innerHTML = ''; + + directories.forEach(function(directory) { + var directoryOption = document.createElement('option'); + directoryOption.value = directory; + directoryOption.innerHTML = + displayDirectoryNamesByDirectory[directory] + ' - ' + + testsByDirectory[directory].length + ' tests'; + directorySelector.appendChild(directoryOption); + }); + + selectDirectory(); +} + +/** + * For a given failure type and directory and failure type, gets all the tests + * in that directory and populatest the test selector with them. + */ +function selectDirectory() +{ + var previouslySelectedTest = getSelectedTest(); + + var selectedDirectory = getSelectValue('directory-selector'); + selectedTests = testsByDirectory[selectedDirectory]; + selectedTests.sort(); + + var testsByState = {}; + selectedTests.forEach(function(testName) { + var state = results.tests[testName].state; + if (state == STATE_IN_QUEUE) { + state = STATE_NEEDS_REBASELINE; + } + if (!(state in testsByState)) { + testsByState[state] = []; + } + testsByState[state].push(testName); + }); + + var optionIndexByTest = {}; + + var testSelector = $('test-selector'); + testSelector.innerHTML = ''; + + var selectedFailureType = getSelectValue('failure-type-selector'); + var sampleSelectedTest = testsByFailureType[selectedFailureType][0]; + var selectedTypeIsSortable = 'metric' in results.tests[sampleSelectedTest]; + if (selectedTypeIsSortable) { + enableSorting(); + if (shouldSortTestsByMetric) { + for (var state in testsByState) { + testsByState[state].sort(function(a, b) { + return results.tests[b].metric - results.tests[a].metric + }) + } + } + } else + disableSorting(); + + for (var state in testsByState) { + var stateOption = document.createElement('option'); + stateOption.textContent = STATE_TO_DISPLAY_STATE[state]; + stateOption.disabled = true; + testSelector.appendChild(stateOption); + + testsByState[state].forEach(function(testName) { + var testOption = document.createElement('option'); + testOption.value = testName; + var testDisplayName = testName; + if (testName.lastIndexOf(selectedDirectory) == 0) { + testDisplayName = testName.substring(selectedDirectory.length); + } + testOption.innerHTML = ' ' + testDisplayName; + optionIndexByTest[testName] = testSelector.options.length; + testSelector.appendChild(testOption); + }); + } + + if (previouslySelectedTest in optionIndexByTest) { + testSelector.selectedIndex = optionIndexByTest[previouslySelectedTest]; + } else if (STATE_NEEDS_REBASELINE in testsByState) { + testSelector.selectedIndex = + optionIndexByTest[testsByState[STATE_NEEDS_REBASELINE][0]]; + selectTest(); + } else { + testSelector.selectedIndex = 1; + selectTest(); + } + + selectTest(); +} + +function getSelectedTest() +{ + return getSelectValue('test-selector'); +} + +function selectTest() +{ + var selectedTest = getSelectedTest(); + + if (results.tests[selectedTest].actual.indexOf('IMAGE') != -1) { + $('image-outputs').style.display = ''; + displayImageResults(selectedTest); + } else { + $('image-outputs').style.display = 'none'; + } + + if (results.tests[selectedTest].actual.indexOf('TEXT') != -1) { + $('text-outputs').style.display = ''; + displayTextResults(selectedTest); + } else { + $('text-outputs').style.display = 'none'; + } + + var currentBaselines = $('current-baselines'); + currentBaselines.textContent = ''; + var baselines = results.tests[selectedTest].baselines; + var testName = selectedTest.split('.').slice(0, -1).join('.'); + getSortedKeys(baselines).forEach(function(platform, i) { + if (i != 0) { + currentBaselines.appendChild(document.createTextNode('; ')); + } + var platformName = document.createElement('span'); + platformName.className = 'platform'; + platformName.textContent = platform; + currentBaselines.appendChild(platformName); + currentBaselines.appendChild(document.createTextNode(' (')); + getSortedKeys(baselines[platform]).forEach(function(extension, j) { + if (j != 0) { + currentBaselines.appendChild(document.createTextNode(', ')); + } + var link = document.createElement('a'); + var baselinePath = ''; + if (platform != 'base') { + baselinePath += 'platform/' + platform + '/'; + } + baselinePath += testName + '-expected' + extension; + link.href = getTracUrl(baselinePath); + if (extension == '.checksum') { + link.textContent = 'chk'; + } else { + link.textContent = extension.substring(1); + } + link.target = '_blank'; + if (baselines[platform][extension]) { + link.className = 'was-used-for-test'; + } + currentBaselines.appendChild(link); + }); + currentBaselines.appendChild(document.createTextNode(')')); + }); + + updateState(); + loupe.hide(); + + prefetchNextImageTest(); +} + +function prefetchNextImageTest() +{ + var testSelector = $('test-selector'); + if (testSelector.selectedIndex == testSelector.options.length - 1) { + return; + } + var nextTest = testSelector.options[testSelector.selectedIndex + 1].value; + if (results.tests[nextTest].actual.indexOf('IMAGE') != -1) { + new Image().src = getTestResultUrl(nextTest, 'expected-image'); + new Image().src = getTestResultUrl(nextTest, 'actual-image'); + } +} + +function updateState() +{ + var testName = getSelectedTest(); + var testIndex = selectedTests.indexOf(testName); + var testCount = selectedTests.length + $('test-index').textContent = testIndex + 1; + $('test-count').textContent = testCount; + + $('next-test').disabled = testIndex == testCount - 1; + $('previous-test').disabled = testIndex == 0; + + $('test-link').href = getTracUrl(testName); + + var state = results.tests[testName].state; + $('state').className = state; + $('state').innerHTML = STATE_TO_DISPLAY_STATE[state]; + + queue.updateState(); +} + +function getTestResultUrl(testName, mode) +{ + return '/test_result?test=' + testName + '&mode=' + mode; +} + +var currentExpectedImageTest; +var currentActualImageTest; + +function displayImageResults(testName) +{ + if (currentExpectedImageTest == currentActualImageTest + && currentExpectedImageTest == testName) { + return; + } + + function displayImageResult(mode, callback) { + var image = $(mode); + image.className = 'loading'; + image.src = getTestResultUrl(testName, mode); + image.onload = function() { + image.className = ''; + callback(); + updateImageDiff(); + }; + } + + displayImageResult( + 'expected-image', + function() { currentExpectedImageTest = testName; }); + displayImageResult( + 'actual-image', + function() { currentActualImageTest = testName; }); + + $('diff-canvas').className = 'loading'; + $('diff-canvas').style.display = ''; + $('diff-checksum').style.display = 'none'; +} + +/** + * Computes a graphical a diff between the expected and actual images by + * rendering each to a canvas, getting the image data, and comparing the RGBA + * components of each pixel. The output is put into the diff canvas, with + * identical pixels appearing at 12.5% opacity and different pixels being + * highlighted in red. + */ +function updateImageDiff() { + if (currentExpectedImageTest != currentActualImageTest) + return; + + var expectedImage = $('expected-image'); + var actualImage = $('actual-image'); + + function getImageData(image) { + var imageCanvas = document.createElement('canvas'); + imageCanvas.width = image.width; + imageCanvas.height = image.height; + imageCanvasContext = imageCanvas.getContext('2d'); + + imageCanvasContext.fillStyle = 'rgba(255, 255, 255, 1)'; + imageCanvasContext.fillRect( + 0, 0, image.width, image.height); + + imageCanvasContext.drawImage(image, 0, 0); + return imageCanvasContext.getImageData( + 0, 0, image.width, image.height); + } + + var expectedImageData = getImageData(expectedImage); + var actualImageData = getImageData(actualImage); + + var diffCanvas = $('diff-canvas'); + var diffCanvasContext = diffCanvas.getContext('2d'); + var diffImageData = + diffCanvasContext.createImageData(diffCanvas.width, diffCanvas.height); + + // Avoiding property lookups for all these during the per-pixel loop below + // provides a significant performance benefit. + var expectedWidth = expectedImage.width; + var expectedHeight = expectedImage.height; + var expected = expectedImageData.data; + + var actualWidth = actualImage.width; + var actual = actualImageData.data; + + var diffWidth = diffImageData.width; + var diff = diffImageData.data; + + var hadDiff = false; + for (var x = 0; x < expectedWidth; x++) { + for (var y = 0; y < expectedHeight; y++) { + var expectedOffset = (y * expectedWidth + x) * 4; + var actualOffset = (y * actualWidth + x) * 4; + var diffOffset = (y * diffWidth + x) * 4; + if (expected[expectedOffset] != actual[actualOffset] || + expected[expectedOffset + 1] != actual[actualOffset + 1] || + expected[expectedOffset + 2] != actual[actualOffset + 2] || + expected[expectedOffset + 3] != actual[actualOffset + 3]) { + hadDiff = true; + diff[diffOffset] = 255; + diff[diffOffset + 1] = 0; + diff[diffOffset + 2] = 0; + diff[diffOffset + 3] = 255; + } else { + diff[diffOffset] = expected[expectedOffset]; + diff[diffOffset + 1] = expected[expectedOffset + 1]; + diff[diffOffset + 2] = expected[expectedOffset + 2]; + diff[diffOffset + 3] = 32; + } + } + } + + diffCanvasContext.putImageData( + diffImageData, + 0, 0, + 0, 0, + diffImageData.width, diffImageData.height); + diffCanvas.className = ''; + + if (!hadDiff) { + diffCanvas.style.display = 'none'; + $('diff-checksum').style.display = ''; + loadTextResult(currentExpectedImageTest, 'expected-checksum'); + loadTextResult(currentExpectedImageTest, 'actual-checksum'); + } +} + +function loadTextResult(testName, mode, responseIsHtml) +{ + loadText(getTestResultUrl(testName, mode), function(text) { + if (responseIsHtml) { + $(mode).innerHTML = text; + } else { + $(mode).textContent = text; + } + }); +} + +function displayTextResults(testName) +{ + loadTextResult(testName, 'expected-text'); + loadTextResult(testName, 'actual-text'); + loadTextResult(testName, 'diff-text-pretty', true); +} + +function nextTest() +{ + var testSelector = $('test-selector'); + var nextTestIndex = testSelector.selectedIndex + 1; + while (true) { + if (nextTestIndex == testSelector.options.length) { + return; + } + if (testSelector.options[nextTestIndex].disabled) { + nextTestIndex++; + } else { + testSelector.selectedIndex = nextTestIndex; + selectTest(); + return; + } + } +} + +function previousTest() +{ + var testSelector = $('test-selector'); + var previousTestIndex = testSelector.selectedIndex - 1; + while (true) { + if (previousTestIndex == -1) { + return; + } + if (testSelector.options[previousTestIndex].disabled) { + previousTestIndex--; + } else { + testSelector.selectedIndex = previousTestIndex; + selectTest(); + return + } + } +} + +window.addEventListener('DOMContentLoaded', main); diff --git a/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/queue.js b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/queue.js new file mode 100644 index 000000000..338e28f80 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/queue.js @@ -0,0 +1,186 @@ +/* + * 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. + */ + +function RebaselineQueue() +{ + this._selectNode = $('queue-select'); + this._rebaselineButtonNode = $('rebaseline-queue'); + this._toggleNode = $('toggle-queue'); + this._removeSelectionButtonNode = $('remove-queue-selection'); + + this._inProgressRebaselineCount = 0; + + var self = this; + $('add-to-rebaseline-queue').addEventListener( + 'click', function() { self.addCurrentTest(); }); + this._selectNode.addEventListener('change', updateState); + this._removeSelectionButtonNode.addEventListener( + 'click', function() { self._removeSelection(); }); + this._rebaselineButtonNode.addEventListener( + 'click', function() { self.rebaseline(); }); + this._toggleNode.addEventListener( + 'click', function() { toggle('queue'); }); +} + +RebaselineQueue.prototype.updateState = function() +{ + var testName = getSelectedTest(); + + var state = results.tests[testName].state; + $('add-to-rebaseline-queue').disabled = state != STATE_NEEDS_REBASELINE; + + var queueLength = this._selectNode.options.length; + if (this._inProgressRebaselineCount > 0) { + this._rebaselineButtonNode.disabled = true; + this._rebaselineButtonNode.textContent = + 'Rebaseline in progress (' + this._inProgressRebaselineCount + + ' tests left)'; + } else if (queueLength == 0) { + this._rebaselineButtonNode.disabled = true; + this._rebaselineButtonNode.textContent = 'Rebaseline queue'; + this._toggleNode.textContent = 'Queue'; + } else { + this._rebaselineButtonNode.disabled = false; + this._rebaselineButtonNode.textContent = + 'Rebaseline queue (' + queueLength + ' tests)'; + this._toggleNode.textContent = 'Queue (' + queueLength + ' tests)'; + } + this._removeSelectionButtonNode.disabled = + this._selectNode.selectedIndex == -1; +}; + +RebaselineQueue.prototype.addCurrentTest = function() +{ + var testName = getSelectedTest(); + var test = results.tests[testName]; + + if (test.state != STATE_NEEDS_REBASELINE) { + log('Cannot add test with state "' + test.state + '" to queue.', + log.WARNING); + return; + } + + var queueOption = document.createElement('option'); + queueOption.value = testName; + queueOption.textContent = testName; + this._selectNode.appendChild(queueOption); + test.state = STATE_IN_QUEUE; + updateState(); +}; + +RebaselineQueue.prototype.removeCurrentTest = function() +{ + this._removeTest(getSelectedTest()); +}; + +RebaselineQueue.prototype._removeSelection = function() +{ + if (this._selectNode.selectedIndex == -1) + return; + + this._removeTest( + this._selectNode.options[this._selectNode.selectedIndex].value); +}; + +RebaselineQueue.prototype._removeTest = function(testName) +{ + var queueOption = this._selectNode.firstChild; + + while (queueOption && queueOption.value != testName) { + queueOption = queueOption.nextSibling; + } + + if (!queueOption) + return; + + this._selectNode.removeChild(queueOption); + var test = results.tests[testName]; + test.state = STATE_NEEDS_REBASELINE; + updateState(); +}; + +RebaselineQueue.prototype.rebaseline = function() +{ + var testNames = []; + for (var queueOption = this._selectNode.firstChild; + queueOption; + queueOption = queueOption.nextSibling) { + testNames.push(queueOption.value); + } + + this._inProgressRebaselineCount = testNames.length; + updateState(); + + testNames.forEach(this._rebaselineTest, this); +}; + +RebaselineQueue.prototype._rebaselineTest = function(testName) +{ + var baselineTarget = getSelectValue('baseline-target'); + var baselineMoveTo = getSelectValue('baseline-move-to'); + + var xhr = new XMLHttpRequest(); + xhr.open('POST', + '/rebaseline?test=' + encodeURIComponent(testName) + + '&baseline-target=' + encodeURIComponent(baselineTarget) + + '&baseline-move-to=' + encodeURIComponent(baselineMoveTo)); + + var self = this; + function handleResponse(logType, newState) { + log(xhr.responseText, logType); + self._removeTest(testName); + self._inProgressRebaselineCount--; + results.tests[testName].state = newState; + updateState(); + // If we're done with a set of rebaselines, regenerate the test menu + // (which is grouped by state) since test states have changed. + if (self._inProgressRebaselineCount == 0) { + selectDirectory(); + } + } + + function handleSuccess() { + handleResponse(log.SUCCESS, STATE_REBASELINE_SUCCEEDED); + } + function handleFailure() { + handleResponse(log.ERROR, STATE_REBASELINE_FAILED); + } + + xhr.addEventListener('load', function() { + if (xhr.status < 400) { + handleSuccess(); + } else { + handleFailure(); + } + }); + xhr.addEventListener('error', handleFailure); + + xhr.send(); +}; diff --git a/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/util.js b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/util.js new file mode 100644 index 000000000..5ad761231 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/servers/data/rebaselineserver/util.js @@ -0,0 +1,104 @@ +/* + * 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. + */ + +var results; +var testsByFailureType = {}; +var testsByDirectory = {}; +var selectedTests = []; + +function $(id) +{ + return document.getElementById(id); +} + +function getSelectValue(id) +{ + var select = $(id); + if (select.selectedIndex == -1) { + return null; + } else { + return select.options[select.selectedIndex].value; + } +} + +function loadText(url, callback) +{ + var xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.addEventListener('load', function() { callback(xhr.responseText); }); + xhr.send(); +} + +function log(text, type) +{ + var node = $('log'); + + if (type) { + var typeNode = document.createElement('span'); + typeNode.textContent = type.text; + typeNode.style.color = type.color; + node.appendChild(typeNode); + } + + node.appendChild(document.createTextNode(text + '\n')); + node.scrollTop = node.scrollHeight; +} + +log.WARNING = {text: 'Warning: ', color: '#aa3'}; +log.SUCCESS = {text: 'Success: ', color: 'green'}; +log.ERROR = {text: 'Error: ', color: 'red'}; + +function toggle(id) +{ + var element = $(id); + var toggler = $('toggle-' + id); + if (element.style.display == 'none') { + element.style.display = ''; + toggler.className = 'link selected'; + } else { + element.style.display = 'none'; + toggler.className = 'link'; + } +} + +function getTracUrl(layoutTestPath) +{ + return 'http://trac.webkit.org/browser/trunk/LayoutTests/' + layoutTestPath; +} + +function getSortedKeys(obj) +{ + var keys = []; + for (var key in obj) { + keys.push(key); + } + keys.sort(); + return keys; +}
\ No newline at end of file diff --git a/Tools/Scripts/webkitpy/tool/servers/gardeningserver.py b/Tools/Scripts/webkitpy/tool/servers/gardeningserver.py new file mode 100644 index 000000000..425d1cea7 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/servers/gardeningserver.py @@ -0,0 +1,156 @@ +# Copyright (C) 2011 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: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# 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 BaseHTTPServer +import os + +from webkitpy.common.memoized import memoized +from webkitpy.tool.servers.reflectionhandler import ReflectionHandler +from webkitpy.layout_tests.controllers.test_expectations_editor import BugManager, TestExpectationsEditor +from webkitpy.layout_tests.models.test_expectations import TestExpectationParser, TestExpectations, TestExpectationSerializer +from webkitpy.layout_tests.models.test_configuration import TestConfigurationConverter +from webkitpy.layout_tests.port import builders + + +class BuildCoverageExtrapolator(object): + def __init__(self, test_configuration_converter): + self._test_configuration_converter = test_configuration_converter + + @memoized + def _covered_test_configurations_for_builder_name(self): + coverage = {} + for builder_name in builders.all_builder_names(): + coverage[builder_name] = self._test_configuration_converter.to_config_set(builders.coverage_specifiers_for_builder_name(builder_name)) + return coverage + + def extrapolate_test_configurations(self, builder_name): + return self._covered_test_configurations_for_builder_name()[builder_name] + + +class GardeningExpectationsUpdater(BugManager): + def __init__(self, tool, port): + self._converter = TestConfigurationConverter(port.all_test_configurations(), port.configuration_specifier_macros()) + self._extrapolator = BuildCoverageExtrapolator(self._converter) + self._parser = TestExpectationParser(port, [], allow_rebaseline_modifier=False) + self._path_to_test_expectations_file = port.path_to_test_expectations_file() + self._tool = tool + + def close_bug(self, bug_id, reference_bug_id=None): + # FIXME: Implement this properly. + pass + + def create_bug(self): + return "BUG_NEW" + + def update_expectations(self, failure_info_list): + expectation_lines = TestExpectationParser.tokenize_list(self._tool.filesystem.read_text_file(self._path_to_test_expectations_file)) + for expectation_line in expectation_lines: + self._parser.parse(expectation_line) + editor = TestExpectationsEditor(expectation_lines, self) + updated_expectation_lines = [] + # FIXME: Group failures by testName+failureTypeList. + for failure_info in failure_info_list: + expectation_set = set(filter(lambda expectation: expectation is not None, + map(TestExpectations.expectation_from_string, failure_info['failureTypeList']))) + assert(expectation_set) + test_name = failure_info['testName'] + assert(test_name) + builder_name = failure_info['builderName'] + affected_test_configuration_set = self._extrapolator.extrapolate_test_configurations(builder_name) + updated_expectation_lines.extend(editor.update_expectation(test_name, affected_test_configuration_set, expectation_set)) + self._tool.filesystem.write_text_file(self._path_to_test_expectations_file, TestExpectationSerializer.list_to_string(expectation_lines, self._converter, reconstitute_only_these=updated_expectation_lines)) + + +class GardeningHTTPServer(BaseHTTPServer.HTTPServer): + def __init__(self, httpd_port, config): + server_name = '' + self.tool = config['tool'] + BaseHTTPServer.HTTPServer.__init__(self, (server_name, httpd_port), GardeningHTTPRequestHandler) + + def url(self): + return 'file://' + os.path.join(GardeningHTTPRequestHandler.STATIC_FILE_DIRECTORY, 'garden-o-matic.html') + + +class GardeningHTTPRequestHandler(ReflectionHandler): + STATIC_FILE_NAMES = frozenset() + + STATIC_FILE_DIRECTORY = os.path.join( + os.path.dirname(__file__), + '..', + '..', + '..', + '..', + 'BuildSlaveSupport', + 'build.webkit.org-config', + 'public_html', + 'TestFailures') + + allow_cross_origin_requests = True + + def _run_webkit_patch(self, args): + return self.server.tool.executive.run_command([self.server.tool.path()] + args, cwd=self.server.tool.scm().checkout_root) + + @memoized + def _expectations_updater(self): + # FIXME: Should split failure_info_list into lists per port, then edit each expectations file separately. + # For now, assume Chromium port. + port = self.server.tool.get("chromium-win-win7") + return GardeningExpectationsUpdater(self.server.tool, port) + + def rollout(self): + revision = self.query['revision'][0] + reason = self.query['reason'][0] + self._run_webkit_patch([ + 'rollout', + '--force-clean', + '--non-interactive', + revision, + reason, + ]) + self._serve_text('success') + + def ping(self): + self._serve_text('pong') + + def updateexpectations(self): + self._expectations_updater().update_expectations(self._read_entity_body_as_json()) + self._serve_text('success') + + def rebaseline(self): + builder = self.query['builder'][0] + test = self.query['test'][0] + self._run_webkit_patch([ + 'rebaseline-test', + builder, + test, + ]) + self._serve_text('success') + + def optimizebaselines(self): + test = self.query['test'][0] + self._run_webkit_patch([ + 'optimize-baselines', + test, + ]) + self._serve_text('success') diff --git a/Tools/Scripts/webkitpy/tool/servers/gardeningserver_unittest.py b/Tools/Scripts/webkitpy/tool/servers/gardeningserver_unittest.py new file mode 100644 index 000000000..34486a378 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/servers/gardeningserver_unittest.py @@ -0,0 +1,202 @@ +# Copyright (C) 2011 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. + +try: + import json +except ImportError: + # python 2.5 compatibility + import webkitpy.thirdparty.simplejson as json + +import unittest + +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.layout_tests.models.test_configuration import * +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.mocktool import MockTool +from webkitpy.common.system.executive_mock import MockExecutive +from webkitpy.common.host_mock import MockHost +from webkitpy.tool.servers.gardeningserver import * + + +class TestPortFactory(object): + # FIXME: Why is this a class method? + @classmethod + def create(cls): + host = MockHost() + return host.port_factory.get("test-win-xp") + + @classmethod + def path_to_test_expectations_file(cls): + return cls.create().path_to_test_expectations_file() + + +class MockServer(object): + def __init__(self): + self.tool = MockTool() + self.tool.executive = MockExecutive(should_log=True) + self.tool.filesystem.files[TestPortFactory.path_to_test_expectations_file()] = "" + + +# The real GardeningHTTPRequestHandler has a constructor that's too hard to +# call in a unit test, so we create a subclass that's easier to constrcut. +class TestGardeningHTTPRequestHandler(GardeningHTTPRequestHandler): + def __init__(self, server): + self.server = server + self.body = None + + def _expectations_updater(self): + return GardeningExpectationsUpdater(self.server.tool, TestPortFactory.create()) + + def _read_entity_body(self): + return self.body if self.body else '' + + def _serve_text(self, text): + print "== Begin Response ==" + print text + print "== End Response ==" + + def _serve_json(self, json_object): + print "== Begin JSON Response ==" + print json.dumps(json_object) + print "== End JSON Response ==" + + +class BuildCoverageExtrapolatorTest(unittest.TestCase): + def test_extrapolate(self): + # FIXME: Make this test not rely on actual (not mock) port objects. + host = MockHost() + port = host.port_factory.get('chromium-win-win7', None) + converter = TestConfigurationConverter(port.all_test_configurations(), port.configuration_specifier_macros()) + extrapolator = BuildCoverageExtrapolator(converter) + self.assertEquals(extrapolator.extrapolate_test_configurations("Webkit Win"), set([TestConfiguration(version='xp', architecture='x86', build_type='release', graphics_type='cpu')])) + self.assertEquals(extrapolator.extrapolate_test_configurations("Webkit Vista"), set([ + TestConfiguration(version='vista', architecture='x86', build_type='debug', graphics_type='cpu'), + TestConfiguration(version='vista', architecture='x86', build_type='debug', graphics_type='gpu'), + TestConfiguration(version='vista', architecture='x86', build_type='release', graphics_type='gpu'), + TestConfiguration(version='vista', architecture='x86', build_type='release', graphics_type='cpu')])) + self.assertRaises(KeyError, extrapolator.extrapolate_test_configurations, "Potato") + + +class GardeningExpectationsUpdaterTest(unittest.TestCase): + def __init__(self, testFunc): + self.tool = MockTool() + self.tool.executive = MockExecutive(should_log=True) + self.tool.filesystem.files[TestPortFactory.path_to_test_expectations_file()] = "" + unittest.TestCase.__init__(self, testFunc) + + def assert_update(self, failure_info_list, expectations_before=None, expectations_after=None, expected_exception=None): + updater = GardeningExpectationsUpdater(self.tool, TestPortFactory.create()) + path_to_test_expectations_file = TestPortFactory.path_to_test_expectations_file() + self.tool.filesystem.files[path_to_test_expectations_file] = expectations_before or "" + if expected_exception: + self.assertRaises(expected_exception, updater.update_expectations, (failure_info_list)) + else: + updater.update_expectations(failure_info_list) + self.assertEquals(self.tool.filesystem.files[path_to_test_expectations_file], expectations_after) + + def test_empty_expectations(self): + failure_info_list = [] + expectations_before = "" + expectations_after = "" + self.assert_update(failure_info_list, expectations_before=expectations_before, expectations_after=expectations_after) + + def test_unknown_builder(self): + failure_info_list = [{"testName": "failures/expected/image.html", "builderName": "Bob", "failureTypeList": ["IMAGE"]}] + self.assert_update(failure_info_list, expected_exception=KeyError) + + def test_empty_failure_type_list(self): + failure_info_list = [{"testName": "failures/expected/image.html", "builderName": "Webkit Win", "failureTypeList": []}] + self.assert_update(failure_info_list, expected_exception=AssertionError) + + def test_empty_test_name(self): + failure_info_list = [{"testName": "", "builderName": "Webkit Win", "failureTypeList": ["TEXT"]}] + self.assert_update(failure_info_list, expected_exception=AssertionError) + + def test_unknown_failure_type(self): + failure_info_list = [{"testName": "failures/expected/image.html", "builderName": "Webkit Win", "failureTypeList": ["IMAGE", "EXPLODE"]}] + expectations_before = "" + expectations_after = "\nBUG_NEW XP RELEASE CPU : failures/expected/image.html = IMAGE" + self.assert_update(failure_info_list, expectations_before=expectations_before, expectations_after=expectations_after) + + def test_add_new_expectation(self): + failure_info_list = [{"testName": "failures/expected/image.html", "builderName": "Webkit Win", "failureTypeList": ["IMAGE"]}] + expectations_before = "" + expectations_after = "\nBUG_NEW XP RELEASE CPU : failures/expected/image.html = IMAGE" + self.assert_update(failure_info_list, expectations_before=expectations_before, expectations_after=expectations_after) + + def test_replace_old_expectation(self): + failure_info_list = [{"testName": "failures/expected/image.html", "builderName": "Webkit Win", "failureTypeList": ["IMAGE"]}] + expectations_before = "BUG_OLD XP RELEASE CPU : failures/expected/image.html = TEXT" + expectations_after = "BUG_NEW XP RELEASE CPU : failures/expected/image.html = IMAGE" + self.assert_update(failure_info_list, expectations_before=expectations_before, expectations_after=expectations_after) + + def test_pass_expectation(self): + failure_info_list = [{"testName": "failures/expected/image.html", "builderName": "Webkit Win", "failureTypeList": ["PASS"]}] + expectations_before = "BUG_OLD XP RELEASE CPU : failures/expected/image.html = TEXT" + expectations_after = "" + self.assert_update(failure_info_list, expectations_before=expectations_before, expectations_after=expectations_after) + + def test_supplement_old_expectation(self): + failure_info_list = [{"testName": "failures/expected/image.html", "builderName": "Webkit Win", "failureTypeList": ["IMAGE"]}] + expectations_before = "BUG_OLD XP RELEASE : failures/expected/image.html = TEXT" + expectations_after = "BUG_OLD XP RELEASE GPU : failures/expected/image.html = TEXT\nBUG_NEW XP RELEASE CPU : failures/expected/image.html = IMAGE" + self.assert_update(failure_info_list, expectations_before=expectations_before, expectations_after=expectations_after) + + def test_spurious_updates(self): + failure_info_list = [{"testName": "failures/expected/image.html", "builderName": "Webkit Win", "failureTypeList": ["IMAGE"]}] + expectations_before = "BUG_OLDER MAC LINUX : failures/expected/image.html = IMAGE+TEXT\nBUG_OLD XP RELEASE CPU : failures/expected/image.html = TEXT" + expectations_after = "BUG_OLDER MAC LINUX : failures/expected/image.html = IMAGE+TEXT\nBUG_NEW XP RELEASE CPU : failures/expected/image.html = IMAGE" + self.assert_update(failure_info_list, expectations_before=expectations_before, expectations_after=expectations_after) + + +class GardeningServerTest(unittest.TestCase): + def _post_to_path(self, path, body=None, expected_stderr=None, expected_stdout=None): + handler = TestGardeningHTTPRequestHandler(MockServer()) + handler.path = path + handler.body = body + OutputCapture().assert_outputs(self, handler.do_POST, expected_stderr=expected_stderr, expected_stdout=expected_stdout) + + def test_rollout(self): + expected_stderr = "MOCK run_command: ['echo', 'rollout', '--force-clean', '--non-interactive', '2314', 'MOCK rollout reason'], cwd=/mock-checkout\n" + expected_stdout = "== Begin Response ==\nsuccess\n== End Response ==\n" + self._post_to_path("/rollout?revision=2314&reason=MOCK+rollout+reason", expected_stderr=expected_stderr, expected_stdout=expected_stdout) + + def test_rebaseline(self): + expected_stderr = "MOCK run_command: ['echo', 'rebaseline-test', 'MOCK builder', 'user-scripts/another-test.html'], cwd=/mock-checkout\n" + expected_stdout = "== Begin Response ==\nsuccess\n== End Response ==\n" + self._post_to_path("/rebaseline?builder=MOCK+builder&test=user-scripts/another-test.html", expected_stderr=expected_stderr, expected_stdout=expected_stdout) + + def test_optimizebaselines(self): + expected_stderr = "MOCK run_command: ['echo', 'optimize-baselines', 'user-scripts/another-test.html'], cwd=/mock-checkout\n" + expected_stdout = "== Begin Response ==\nsuccess\n== End Response ==\n" + self._post_to_path("/optimizebaselines?test=user-scripts/another-test.html", expected_stderr=expected_stderr, expected_stdout=expected_stdout) + + def test_updateexpectations(self): + expected_stderr = "" + expected_stdout = "== Begin Response ==\nsuccess\n== End Response ==\n" + self._post_to_path("/updateexpectations", body="[]", expected_stderr=expected_stderr, expected_stdout=expected_stdout) diff --git a/Tools/Scripts/webkitpy/tool/servers/rebaselineserver.py b/Tools/Scripts/webkitpy/tool/servers/rebaselineserver.py new file mode 100644 index 000000000..39c5b9e55 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/servers/rebaselineserver.py @@ -0,0 +1,288 @@ +# 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 fnmatch +import os +import os.path +import BaseHTTPServer + +from webkitpy.common.host import Host # FIXME: This should not be needed! +from webkitpy.layout_tests.port.webkit import WebKitPort +from webkitpy.tool.servers.reflectionhandler import ReflectionHandler + + +STATE_NEEDS_REBASELINE = 'needs_rebaseline' +STATE_REBASELINE_FAILED = 'rebaseline_failed' +STATE_REBASELINE_SUCCEEDED = 'rebaseline_succeeded' + + +def _get_actual_result_files(test_file, test_config): + test_name, _ = os.path.splitext(test_file) + test_directory = os.path.dirname(test_file) + + test_results_directory = test_config.filesystem.join( + test_config.results_directory, test_directory) + actual_pattern = os.path.basename(test_name) + '-actual.*' + actual_files = [] + for filename in test_config.filesystem.listdir(test_results_directory): + if fnmatch.fnmatch(filename, actual_pattern): + actual_files.append(filename) + actual_files.sort() + return tuple(actual_files) + + +def _rebaseline_test(test_file, baseline_target, baseline_move_to, test_config, log): + test_name, _ = os.path.splitext(test_file) + test_directory = os.path.dirname(test_name) + + log('Rebaselining %s...' % test_name) + + actual_result_files = _get_actual_result_files(test_file, test_config) + filesystem = test_config.filesystem + scm = test_config.scm + layout_tests_directory = test_config.layout_tests_directory + results_directory = test_config.results_directory + target_expectations_directory = filesystem.join( + layout_tests_directory, 'platform', baseline_target, test_directory) + test_results_directory = test_config.filesystem.join( + test_config.results_directory, test_directory) + + # If requested, move current baselines out + current_baselines = get_test_baselines(test_file, test_config) + if baseline_target in current_baselines and baseline_move_to != 'none': + log(' Moving current %s baselines to %s' % + (baseline_target, baseline_move_to)) + + # See which ones we need to move (only those that are about to be + # updated), and make sure we're not clobbering any files in the + # destination. + current_extensions = set(current_baselines[baseline_target].keys()) + actual_result_extensions = [ + os.path.splitext(f)[1] for f in actual_result_files] + extensions_to_move = current_extensions.intersection( + actual_result_extensions) + + if extensions_to_move.intersection( + current_baselines.get(baseline_move_to, {}).keys()): + log(' Already had baselines in %s, could not move existing ' + '%s ones' % (baseline_move_to, baseline_target)) + return False + + # Do the actual move. + if extensions_to_move: + if not _move_test_baselines( + test_file, + list(extensions_to_move), + baseline_target, + baseline_move_to, + test_config, + log): + return False + else: + log(' No current baselines to move') + + log(' Updating baselines for %s' % baseline_target) + filesystem.maybe_make_directory(target_expectations_directory) + for source_file in actual_result_files: + source_path = filesystem.join(test_results_directory, source_file) + destination_file = source_file.replace('-actual', '-expected') + destination_path = filesystem.join( + target_expectations_directory, destination_file) + filesystem.copyfile(source_path, destination_path) + exit_code = scm.add(destination_path, return_exit_code=True) + if exit_code: + log(' Could not update %s in SCM, exit code %d' % + (destination_file, exit_code)) + return False + else: + log(' Updated %s' % destination_file) + + return True + + +def _move_test_baselines(test_file, extensions_to_move, source_platform, destination_platform, test_config, log): + test_file_name = os.path.splitext(os.path.basename(test_file))[0] + test_directory = os.path.dirname(test_file) + filesystem = test_config.filesystem + + # Want predictable output order for unit tests. + extensions_to_move.sort() + + source_directory = os.path.join( + test_config.layout_tests_directory, + 'platform', + source_platform, + test_directory) + destination_directory = os.path.join( + test_config.layout_tests_directory, + 'platform', + destination_platform, + test_directory) + filesystem.maybe_make_directory(destination_directory) + + for extension in extensions_to_move: + file_name = test_file_name + '-expected' + extension + source_path = filesystem.join(source_directory, file_name) + destination_path = filesystem.join(destination_directory, file_name) + filesystem.copyfile(source_path, destination_path) + exit_code = test_config.scm.add(destination_path, return_exit_code=True) + if exit_code: + log(' Could not update %s in SCM, exit code %d' % + (file_name, exit_code)) + return False + else: + log(' Moved %s' % file_name) + + return True + + +def get_test_baselines(test_file, test_config): + # FIXME: This seems like a hack. This only seems used to access the Port.expected_baselines logic. + class AllPlatformsPort(WebKitPort): + def __init__(self, host): + WebKitPort.__init__(self, host) + self._platforms_by_directory = dict([(self._webkit_baseline_path(p), p) for p in test_config.platforms]) + + def baseline_search_path(self): + return self._platforms_by_directory.keys() + + def platform_from_directory(self, directory): + return self._platforms_by_directory[directory] + + test_path = test_config.filesystem.join(test_config.layout_tests_directory, test_file) + + # FIXME: This should get the Host from the test_config to be mockable! + host = Host() + host._initialize_scm() + host.filesystem = test_config.filesystem + all_platforms_port = AllPlatformsPort(host) + + all_test_baselines = {} + for baseline_extension in ('.txt', '.checksum', '.png'): + test_baselines = test_config.test_port.expected_baselines(test_file, baseline_extension) + baselines = all_platforms_port.expected_baselines(test_file, baseline_extension, all_baselines=True) + for platform_directory, expected_filename in baselines: + if not platform_directory: + continue + if platform_directory == test_config.layout_tests_directory: + platform = 'base' + else: + platform = all_platforms_port.platform_from_directory(platform_directory) + platform_baselines = all_test_baselines.setdefault(platform, {}) + was_used_for_test = (platform_directory, expected_filename) in test_baselines + platform_baselines[baseline_extension] = was_used_for_test + + return all_test_baselines + + +class RebaselineHTTPServer(BaseHTTPServer.HTTPServer): + def __init__(self, httpd_port, config): + server_name = "" + BaseHTTPServer.HTTPServer.__init__(self, (server_name, httpd_port), RebaselineHTTPRequestHandler) + self.test_config = config['test_config'] + self.results_json = config['results_json'] + self.platforms_json = config['platforms_json'] + + +class RebaselineHTTPRequestHandler(ReflectionHandler): + STATIC_FILE_NAMES = frozenset([ + "index.html", + "loupe.js", + "main.js", + "main.css", + "queue.js", + "util.js", + ]) + + STATIC_FILE_DIRECTORY = os.path.join(os.path.dirname(__file__), "data", "rebaselineserver") + + def results_json(self): + self._serve_json(self.server.results_json) + + def test_config(self): + self._serve_json(self.server.test_config) + + def platforms_json(self): + self._serve_json(self.server.platforms_json) + + def rebaseline(self): + test = self.query['test'][0] + baseline_target = self.query['baseline-target'][0] + baseline_move_to = self.query['baseline-move-to'][0] + test_json = self.server.results_json['tests'][test] + + if test_json['state'] != STATE_NEEDS_REBASELINE: + self.send_error(400, "Test %s is in unexpected state: %s" % (test, test_json["state"])) + return + + log = [] + success = _rebaseline_test( + test, + baseline_target, + baseline_move_to, + self.server.test_config, + log=lambda l: log.append(l)) + + if success: + test_json['state'] = STATE_REBASELINE_SUCCEEDED + self.send_response(200) + else: + test_json['state'] = STATE_REBASELINE_FAILED + self.send_response(500) + + self.send_header('Content-type', 'text/plain') + self.end_headers() + self.wfile.write('\n'.join(log)) + + def test_result(self): + test_name, _ = os.path.splitext(self.query['test'][0]) + mode = self.query['mode'][0] + if mode == 'expected-image': + file_name = test_name + '-expected.png' + elif mode == 'actual-image': + file_name = test_name + '-actual.png' + if mode == 'expected-checksum': + file_name = test_name + '-expected.checksum' + elif mode == 'actual-checksum': + file_name = test_name + '-actual.checksum' + elif mode == 'diff-image': + file_name = test_name + '-diff.png' + if mode == 'expected-text': + file_name = test_name + '-expected.txt' + elif mode == 'actual-text': + file_name = test_name + '-actual.txt' + elif mode == 'diff-text': + file_name = test_name + '-diff.txt' + elif mode == 'diff-text-pretty': + file_name = test_name + '-pretty-diff.html' + + file_path = os.path.join(self.server.test_config.results_directory, file_name) + + # Let results be cached for 60 seconds, so that they can be pre-fetched + # by the UI + self._serve_file(file_path, cacheable_seconds=60) diff --git a/Tools/Scripts/webkitpy/tool/servers/rebaselineserver_unittest.py b/Tools/Scripts/webkitpy/tool/servers/rebaselineserver_unittest.py new file mode 100644 index 000000000..b02d1d357 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/servers/rebaselineserver_unittest.py @@ -0,0 +1,319 @@ +# 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 + +try: + import json +except ImportError: + # python 2.5 compatibility + import webkitpy.thirdparty.simplejson as json + + +from webkitpy.common.net import resultsjsonparser_unittest +from webkitpy.common.host_mock import MockHost +from webkitpy.layout_tests.layout_package.json_results_generator import strip_json_wrapper +from webkitpy.layout_tests.port.webkit import WebKitPort +from webkitpy.tool.commands.rebaselineserver import TestConfig, RebaselineServer +from webkitpy.tool.servers import rebaselineserver + + +class RebaselineTestTest(unittest.TestCase): + def test_text_rebaseline_update(self): + self._assertRebaseline( + test_files=( + 'fast/text-expected.txt', + 'platform/mac/fast/text-expected.txt', + ), + results_files=( + 'fast/text-actual.txt', + ), + test_name='fast/text.html', + baseline_target='mac', + baseline_move_to='none', + expected_success=True, + expected_log=[ + 'Rebaselining fast/text...', + ' Updating baselines for mac', + ' Updated text-expected.txt', + ]) + + def test_text_rebaseline_new(self): + self._assertRebaseline( + test_files=( + 'fast/text-expected.txt', + ), + results_files=( + 'fast/text-actual.txt', + ), + test_name='fast/text.html', + baseline_target='mac', + baseline_move_to='none', + expected_success=True, + expected_log=[ + 'Rebaselining fast/text...', + ' Updating baselines for mac', + ' Updated text-expected.txt', + ]) + + def test_text_rebaseline_move_no_op_1(self): + self._assertRebaseline( + test_files=( + 'fast/text-expected.txt', + 'platform/win/fast/text-expected.txt', + ), + results_files=( + 'fast/text-actual.txt', + ), + test_name='fast/text.html', + baseline_target='mac', + baseline_move_to='mac-leopard', + expected_success=True, + expected_log=[ + 'Rebaselining fast/text...', + ' Updating baselines for mac', + ' Updated text-expected.txt', + ]) + + def test_text_rebaseline_move_no_op_2(self): + self._assertRebaseline( + test_files=( + 'fast/text-expected.txt', + 'platform/mac/fast/text-expected.checksum', + ), + results_files=( + 'fast/text-actual.txt', + ), + test_name='fast/text.html', + baseline_target='mac', + baseline_move_to='mac-leopard', + expected_success=True, + expected_log=[ + 'Rebaselining fast/text...', + ' Moving current mac baselines to mac-leopard', + ' No current baselines to move', + ' Updating baselines for mac', + ' Updated text-expected.txt', + ]) + + def test_text_rebaseline_move(self): + self._assertRebaseline( + test_files=( + 'fast/text-expected.txt', + 'platform/mac/fast/text-expected.txt', + ), + results_files=( + 'fast/text-actual.txt', + ), + test_name='fast/text.html', + baseline_target='mac', + baseline_move_to='mac-leopard', + expected_success=True, + expected_log=[ + 'Rebaselining fast/text...', + ' Moving current mac baselines to mac-leopard', + ' Moved text-expected.txt', + ' Updating baselines for mac', + ' Updated text-expected.txt', + ]) + + def test_text_rebaseline_move_only_images(self): + self._assertRebaseline( + test_files=( + 'fast/image-expected.txt', + 'platform/mac/fast/image-expected.txt', + 'platform/mac/fast/image-expected.png', + 'platform/mac/fast/image-expected.checksum', + ), + results_files=( + 'fast/image-actual.png', + 'fast/image-actual.checksum', + ), + test_name='fast/image.html', + baseline_target='mac', + baseline_move_to='mac-leopard', + expected_success=True, + expected_log=[ + 'Rebaselining fast/image...', + ' Moving current mac baselines to mac-leopard', + ' Moved image-expected.checksum', + ' Moved image-expected.png', + ' Updating baselines for mac', + ' Updated image-expected.checksum', + ' Updated image-expected.png', + ]) + + def test_text_rebaseline_move_already_exist(self): + self._assertRebaseline( + test_files=( + 'fast/text-expected.txt', + 'platform/mac-leopard/fast/text-expected.txt', + 'platform/mac/fast/text-expected.txt', + ), + results_files=( + 'fast/text-actual.txt', + ), + test_name='fast/text.html', + baseline_target='mac', + baseline_move_to='mac-leopard', + expected_success=False, + expected_log=[ + 'Rebaselining fast/text...', + ' Moving current mac baselines to mac-leopard', + ' Already had baselines in mac-leopard, could not move existing mac ones', + ]) + + def test_image_rebaseline(self): + self._assertRebaseline( + test_files=( + 'fast/image-expected.txt', + 'platform/mac/fast/image-expected.png', + 'platform/mac/fast/image-expected.checksum', + ), + results_files=( + 'fast/image-actual.png', + 'fast/image-actual.checksum', + ), + test_name='fast/image.html', + baseline_target='mac', + baseline_move_to='none', + expected_success=True, + expected_log=[ + 'Rebaselining fast/image...', + ' Updating baselines for mac', + ' Updated image-expected.checksum', + ' Updated image-expected.png', + ]) + + def test_gather_baselines(self): + example_json = resultsjsonparser_unittest.ResultsJSONParserTest._example_full_results_json + results_json = json.loads(strip_json_wrapper(example_json)) + server = RebaselineServer() + server._test_config = get_test_config() + server._gather_baselines(results_json) + self.assertEqual(results_json['tests']['svg/dynamic-updates/SVGFEDropShadowElement-dom-stdDeviation-attr.html']['state'], 'needs_rebaseline') + self.assertFalse('prototype-chocolate.html' in results_json['tests']) + + def _assertRebaseline(self, test_files, results_files, test_name, baseline_target, baseline_move_to, expected_success, expected_log): + log = [] + test_config = get_test_config(test_files, results_files) + success = rebaselineserver._rebaseline_test( + test_name, + baseline_target, + baseline_move_to, + test_config, + log=lambda l: log.append(l)) + self.assertEqual(expected_log, log) + self.assertEqual(expected_success, success) + + +class GetActualResultFilesTest(unittest.TestCase): + def test(self): + test_config = get_test_config(result_files=( + 'fast/text-actual.txt', + 'fast2/text-actual.txt', + 'fast/text2-actual.txt', + 'fast/text-notactual.txt', + )) + self.assertEqual( + ('text-actual.txt',), + rebaselineserver._get_actual_result_files( + 'fast/text.html', test_config)) + + +class GetBaselinesTest(unittest.TestCase): + def test_no_baselines(self): + self._assertBaselines( + test_files=(), + test_name='fast/missing.html', + expected_baselines={}) + + def test_text_baselines(self): + self._assertBaselines( + test_files=( + 'fast/text-expected.txt', + 'platform/mac/fast/text-expected.txt', + ), + test_name='fast/text.html', + expected_baselines={ + 'mac': {'.txt': True}, + 'base': {'.txt': False}, + }) + + def test_image_and_text_baselines(self): + self._assertBaselines( + test_files=( + 'fast/image-expected.txt', + 'platform/mac/fast/image-expected.png', + 'platform/mac/fast/image-expected.checksum', + 'platform/win/fast/image-expected.png', + 'platform/win/fast/image-expected.checksum', + ), + test_name='fast/image.html', + expected_baselines={ + 'base': {'.txt': True}, + 'mac': {'.checksum': True, '.png': True}, + 'win': {'.checksum': False, '.png': False}, + }) + + def test_extra_baselines(self): + self._assertBaselines( + test_files=( + 'fast/text-expected.txt', + 'platform/nosuchplatform/fast/text-expected.txt', + ), + test_name='fast/text.html', + expected_baselines={'base': {'.txt': True}}) + + def _assertBaselines(self, test_files, test_name, expected_baselines): + actual_baselines = rebaselineserver.get_test_baselines(test_name, get_test_config(test_files)) + self.assertEqual(expected_baselines, actual_baselines) + + +def get_test_config(test_files=[], result_files=[]): + # We could grab this from port.layout_tests_dir(), but instantiating a fully mocked port is a pain. + layout_tests_directory = "/mock-checkout/LayoutTests" + results_directory = '/WebKitBuild/Debug/layout-test-results' + host = MockHost() + for file in test_files: + file_path = host.filesystem.join(layout_tests_directory, file) + host.filesystem.files[file_path] = '' + for file in result_files: + file_path = host.filesystem.join(results_directory, file) + host.filesystem.files[file_path] = '' + + class TestMacPort(WebKitPort): + port_name = "mac" + + return TestConfig( + TestMacPort(host), + layout_tests_directory, + results_directory, + ('mac', 'mac-leopard', 'win', 'linux'), + host.filesystem, + host.scm()) diff --git a/Tools/Scripts/webkitpy/tool/servers/reflectionhandler.py b/Tools/Scripts/webkitpy/tool/servers/reflectionhandler.py new file mode 100644 index 000000000..db118afa6 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/servers/reflectionhandler.py @@ -0,0 +1,146 @@ +# Copyright (c) 2011 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 __future__ import with_statement + +try: + import json +except ImportError: + # python 2.5 compatibility + import webkitpy.thirdparty.simplejson as json + +import BaseHTTPServer + +import cgi +import codecs +import datetime +import fnmatch +import mimetypes +import os +import os.path +import shutil +import threading +import time +import urlparse +import wsgiref.handlers +import BaseHTTPServer + +class ReflectionHandler(BaseHTTPServer.BaseHTTPRequestHandler): + # Subclasses should override. + STATIC_FILE_NAMES = None + STATIC_FILE_DIRECTORY = None + + # Setting this flag to True causes the server to send + # Access-Control-Allow-Origin: * + # with every response. + allow_cross_origin_requests = False + + def do_GET(self): + self._handle_request() + + def do_POST(self): + self._handle_request() + + def _read_entity_body(self): + length = int(self.headers.getheader('content-length')) + return self.rfile.read(length) + + def _read_entity_body_as_json(self): + return json.loads(self._read_entity_body()) + + def _handle_request(self): + if "?" in self.path: + path, query_string = self.path.split("?", 1) + self.query = cgi.parse_qs(query_string) + else: + path = self.path + self.query = {} + function_or_file_name = path[1:] or "index.html" + + if function_or_file_name in self.STATIC_FILE_NAMES: + self._serve_static_file(function_or_file_name) + return + + function_name = function_or_file_name.replace(".", "_") + if not hasattr(self, function_name): + self.send_error(404, "Unknown function %s" % function_name) + return + if function_name[0] == "_": + self.send_error(401, "Not allowed to invoke private or protected methods") + return + function = getattr(self, function_name) + function() + + def _serve_static_file(self, static_path): + self._serve_file(os.path.join(self.STATIC_FILE_DIRECTORY, static_path)) + + def quitquitquit(self): + self._serve_text("Server quit.\n") + # Shutdown has to happen on another thread from the server's thread, + # otherwise there's a deadlock + threading.Thread(target=lambda: self.server.shutdown()).start() + + def _send_access_control_header(self): + if self.allow_cross_origin_requests: + self.send_header('Access-Control-Allow-Origin', '*') + + def _serve_text(self, text): + self.send_response(200) + self._send_access_control_header() + self.send_header("Content-type", "text/plain") + self.end_headers() + self.wfile.write(text) + + def _serve_json(self, json_object): + self.send_response(200) + self._send_access_control_header() + self.send_header('Content-type', 'application/json') + self.end_headers() + json.dump(json_object, self.wfile) + + def _serve_file(self, file_path, cacheable_seconds=0): + if not os.path.exists(file_path): + self.send_error(404, "File not found") + return + with codecs.open(file_path, "rb") as static_file: + self.send_response(200) + self._send_access_control_header() + self.send_header("Content-Length", os.path.getsize(file_path)) + mime_type, encoding = mimetypes.guess_type(file_path) + if mime_type: + self.send_header("Content-type", mime_type) + + if cacheable_seconds: + expires_time = (datetime.datetime.now() + + datetime.timedelta(0, cacheable_seconds)) + expires_formatted = wsgiref.handlers.format_date_time( + time.mktime(expires_time.timetuple())) + self.send_header("Expires", expires_formatted) + self.end_headers() + + shutil.copyfileobj(static_file, self.wfile) diff --git a/Tools/Scripts/webkitpy/tool/steps/__init__.py b/Tools/Scripts/webkitpy/tool/steps/__init__.py new file mode 100644 index 000000000..aca9706f5 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/__init__.py @@ -0,0 +1,64 @@ +# 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. + +# FIXME: Is this the right way to do this? +from webkitpy.tool.steps.applypatch import ApplyPatch +from webkitpy.tool.steps.applypatchwithlocalcommit import ApplyPatchWithLocalCommit +from webkitpy.tool.steps.applywatchlist import ApplyWatchList +from webkitpy.tool.steps.attachtobug import AttachToBug +from webkitpy.tool.steps.build import Build +from webkitpy.tool.steps.checkstyle import CheckStyle +from webkitpy.tool.steps.cleanworkingdirectory import CleanWorkingDirectory +from webkitpy.tool.steps.cleanworkingdirectorywithlocalcommits import CleanWorkingDirectoryWithLocalCommits +from webkitpy.tool.steps.closebug import CloseBug +from webkitpy.tool.steps.closebugforlanddiff import CloseBugForLandDiff +from webkitpy.tool.steps.closepatch import ClosePatch +from webkitpy.tool.steps.commit import Commit +from webkitpy.tool.steps.confirmdiff import ConfirmDiff +from webkitpy.tool.steps.createbug import CreateBug +from webkitpy.tool.steps.editchangelog import EditChangeLog +from webkitpy.tool.steps.ensurebugisopenandassigned import EnsureBugIsOpenAndAssigned +from webkitpy.tool.steps.ensurelocalcommitifneeded import EnsureLocalCommitIfNeeded +from webkitpy.tool.steps.obsoletepatches import ObsoletePatches +from webkitpy.tool.steps.options import Options +from webkitpy.tool.steps.postdiff import PostDiff +from webkitpy.tool.steps.postdiffforcommit import PostDiffForCommit +from webkitpy.tool.steps.postdiffforrevert import PostDiffForRevert +from webkitpy.tool.steps.preparechangelog import PrepareChangeLog +from webkitpy.tool.steps.preparechangelogfordepsroll import PrepareChangeLogForDEPSRoll +from webkitpy.tool.steps.preparechangelogforrevert import PrepareChangeLogForRevert +from webkitpy.tool.steps.promptforbugortitle import PromptForBugOrTitle +from webkitpy.tool.steps.reopenbugafterrollout import ReopenBugAfterRollout +from webkitpy.tool.steps.revertrevision import RevertRevision +from webkitpy.tool.steps.runtests import RunTests +from webkitpy.tool.steps.suggestreviewers import SuggestReviewers +from webkitpy.tool.steps.update import Update +from webkitpy.tool.steps.updatechangelogswithreviewer import UpdateChangeLogsWithReviewer +from webkitpy.tool.steps.updatechromiumdeps import UpdateChromiumDEPS +from webkitpy.tool.steps.validatechangelogs import ValidateChangeLogs +from webkitpy.tool.steps.validatereviewer import ValidateReviewer diff --git a/Tools/Scripts/webkitpy/tool/steps/abstractstep.py b/Tools/Scripts/webkitpy/tool/steps/abstractstep.py new file mode 100644 index 000000000..5ac976f9e --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/abstractstep.py @@ -0,0 +1,74 @@ +# 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.config.ports import WebKitPort +from webkitpy.tool.steps.options import Options + + +class AbstractStep(object): + def __init__(self, tool, options): + self._tool = tool + self._options = options + + def _changed_files(self, state): + return self.cached_lookup(state, "changed_files") + + _well_known_keys = { + # FIXME: Should this use state.get('bug_id') or state.get('patch').bug_id() like UpdateChangeLogsWithReviewer does? + "bug": lambda self, state: self._tool.bugs.fetch_bug(state["bug_id"]), + # bug_title can either be a new title given by the user, or one from an existing bug. + "bug_title": lambda self, state: self.cached_lookup(state, 'bug').title(), + "changed_files": lambda self, state: self._tool.scm().changed_files(self._options.git_commit), + "diff": lambda self, state: self._tool.scm().create_patch(self._options.git_commit, changed_files=self._changed_files(state)), + # Absolute path to ChangeLog files. + "changelogs": lambda self, state: self._tool.checkout().modified_changelogs(self._options.git_commit, changed_files=self._changed_files(state)), + } + + def cached_lookup(self, state, key, promise=None): + if state.get(key): + return state[key] + if not promise: + promise = self._well_known_keys.get(key) + state[key] = promise(self, state) + return state[key] + + def did_modify_checkout(self, state): + state["diff"] = None + state["changelogs"] = None + state["changed_files"] = None + + @classmethod + def options(cls): + return [ + # We need this option here because cached_lookup uses it. :( + Options.git_commit, + ] + + def run(self, state): + raise NotImplementedError, "subclasses must implement" diff --git a/Tools/Scripts/webkitpy/tool/steps/applypatch.py b/Tools/Scripts/webkitpy/tool/steps/applypatch.py new file mode 100644 index 000000000..327ac09c1 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/applypatch.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.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options +from webkitpy.common.system.deprecated_logging import log + +class ApplyPatch(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.non_interactive, + Options.force_patch, + ] + + def run(self, state): + log("Processing patch %s from bug %s." % (state["patch"].id(), state["patch"].bug_id())) + self._tool.checkout().apply_patch(state["patch"], force=self._options.non_interactive or self._options.force_patch) diff --git a/Tools/Scripts/webkitpy/tool/steps/applypatchwithlocalcommit.py b/Tools/Scripts/webkitpy/tool/steps/applypatchwithlocalcommit.py new file mode 100644 index 000000000..3dcd8d931 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/applypatchwithlocalcommit.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.steps.applypatch import ApplyPatch +from webkitpy.tool.steps.options import Options + +class ApplyPatchWithLocalCommit(ApplyPatch): + @classmethod + def options(cls): + return ApplyPatch.options() + [ + Options.local_commit, + ] + + def run(self, state): + ApplyPatch.run(self, state) + if self._options.local_commit: + commit_message = self._tool.checkout().commit_message_for_this_commit(git_commit=None) + self._tool.scm().commit_locally_with_message(commit_message.message() or state["patch"].name()) diff --git a/Tools/Scripts/webkitpy/tool/steps/applywatchlist.py b/Tools/Scripts/webkitpy/tool/steps/applywatchlist.py new file mode 100644 index 000000000..a4bb89174 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/applywatchlist.py @@ -0,0 +1,67 @@ +# Copyright (C) 2011 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 import logutils +from webkitpy.tool.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options + + +_log = logutils.get_logger(__file__) + + +class ApplyWatchList(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.git_commit, + ] + + def run(self, state): + diff = self.cached_lookup(state, 'diff') + bug_id = state.get('bug_id') + + cc_and_messages = self._tool.watch_list().determine_cc_and_messages(diff) + cc_emails = cc_and_messages['cc_list'] + messages = cc_and_messages['messages'] + if bug_id: + # Remove emails and cc's which are already in the bug or the reporter. + bug = self._tool.bugs.fetch_bug(bug_id) + + messages = filter(lambda message: not bug.is_in_comments(message), messages) + cc_emails = set(cc_emails).difference(bug.cc_emails()) + cc_emails.discard(bug.reporter_email()) + + comment_text = '\n\n'.join(messages) + if bug_id: + if cc_emails or comment_text: + self._tool.bugs.post_comment_to_bug(bug_id, comment_text, cc_emails) + log_result = _log.debug + else: + _log.info('No bug was updated because no id was given.') + log_result = _log.info + log_result('Result of watchlist: cc "%s" messages "%s"' % (', '.join(cc_emails), comment_text)) diff --git a/Tools/Scripts/webkitpy/tool/steps/applywatchlist_unittest.py b/Tools/Scripts/webkitpy/tool/steps/applywatchlist_unittest.py new file mode 100644 index 000000000..bdaaf758a --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/applywatchlist_unittest.py @@ -0,0 +1,51 @@ +# Copyright (C) 2011 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 +from webkitpy.tool.steps.applywatchlist import ApplyWatchList + + +class ApplyWatchListTest(unittest.TestCase): + def test_apply_watch_list_local(self): + capture = OutputCapture() + step = ApplyWatchList(MockTool(log_executive=True), MockOptions()) + state = { + 'bug_id': '50001', + 'diff': 'The diff', + } + expected_stderr = """MockWatchList: determine_cc_and_messages +MOCK bug comment: bug_id=50001, cc=set(['levin@chromium.org']) +--- Begin comment --- +Message2. +--- End comment --- + +""" + capture.assert_outputs(self, step.run, [state], expected_stderr=expected_stderr) diff --git a/Tools/Scripts/webkitpy/tool/steps/attachtobug.py b/Tools/Scripts/webkitpy/tool/steps/attachtobug.py new file mode 100644 index 000000000..4885fcb72 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/attachtobug.py @@ -0,0 +1,51 @@ +# Copyright (C) 2011 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.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options + + +class AttachToBug(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.comment, + Options.description, + ] + + def run(self, state): + filepath = state["filepath"] + bug_id = state["bug_id"] + description = self._options.description or filepath.split(os.sep)[-1] + comment_text = self._options.comment + + # add_attachment_to_bug fills in the filename from the file path. + filename = None + self._tool.bugs.add_attachment_to_bug(bug_id, filepath, description, filename, comment_text) diff --git a/Tools/Scripts/webkitpy/tool/steps/build.py b/Tools/Scripts/webkitpy/tool/steps/build.py new file mode 100644 index 000000000..7f7dd9f36 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/build.py @@ -0,0 +1,60 @@ +# 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.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options +from webkitpy.common.system.deprecated_logging import log + + +class Build(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.build, + Options.quiet, + Options.build_style, + ] + + def build(self, build_style): + environment = self._tool.copy_current_environment() + environment.disable_gcc_smartquotes() + env = environment.to_dictionary() + + build_webkit_command = self._tool.port().build_webkit_command(build_style=build_style) + self._tool.executive.run_and_throw_if_fail(build_webkit_command, self._options.quiet, + cwd=self._tool.scm().checkout_root, env=env) + + def run(self, state): + if not self._options.build: + return + log("Building WebKit") + if self._options.build_style == "both": + self.build("debug") + self.build("release") + else: + self.build(self._options.build_style) diff --git a/Tools/Scripts/webkitpy/tool/steps/checkstyle.py b/Tools/Scripts/webkitpy/tool/steps/checkstyle.py new file mode 100644 index 000000000..a1a318134 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/checkstyle.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. + +import os + +from webkitpy.common.system.executive import ScriptError +from webkitpy.tool.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options +from webkitpy.common.system.deprecated_logging import error + +class CheckStyle(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.non_interactive, + Options.check_style, + Options.check_style_filter, + Options.git_commit, + ] + + def run(self, state): + if not self._options.check_style: + return + + args = [] + if self._options.git_commit: + args.append("--git-commit") + args.append(self._options.git_commit) + + args.append("--diff-files") + args.extend(self._changed_files(state)) + + if self._options.check_style_filter: + args.append("--filter") + args.append(self._options.check_style_filter) + + try: + self._tool.executive.run_and_throw_if_fail(self._tool.port().check_webkit_style_command() + args, cwd=self._tool.scm().checkout_root) + except ScriptError, e: + if self._options.non_interactive: + # We need to re-raise the exception here to have the + # style-queue do the right thing. + raise e + if not self._tool.user.confirm("Are you sure you want to continue?"): + exit(1) diff --git a/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectory.py b/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectory.py new file mode 100644 index 000000000..27c536361 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectory.py @@ -0,0 +1,52 @@ +# 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.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options + + +class CleanWorkingDirectory(AbstractStep): + def __init__(self, tool, options, allow_local_commits=False): + AbstractStep.__init__(self, tool, options) + self._allow_local_commits = allow_local_commits + + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.force_clean, + Options.clean, + ] + + def run(self, state): + if not self._options.clean: + return + if not self._allow_local_commits: + self._tool.scm().ensure_no_local_commits(self._options.force_clean) + self._tool.scm().ensure_clean_working_directory(force_clean=self._options.force_clean) diff --git a/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectory_unittest.py b/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectory_unittest.py new file mode 100644 index 000000000..15a8850a5 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectory_unittest.py @@ -0,0 +1,52 @@ +# 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.thirdparty.mock import Mock +from webkitpy.tool.mocktool import MockOptions, MockTool +from webkitpy.tool.steps.cleanworkingdirectory import CleanWorkingDirectory + + +class CleanWorkingDirectoryTest(unittest.TestCase): + def test_run(self): + tool = MockTool() + tool._scm = Mock() + tool._scm.checkout_root = '/mock-checkout' + step = CleanWorkingDirectory(tool, MockOptions(clean=True, force_clean=False)) + step.run({}) + self.assertEqual(tool._scm.ensure_no_local_commits.call_count, 1) + self.assertEqual(tool._scm.ensure_clean_working_directory.call_count, 1) + + def test_no_clean(self): + tool = MockTool() + tool._scm = Mock() + step = CleanWorkingDirectory(tool, MockOptions(clean=False)) + step.run({}) + self.assertEqual(tool._scm.ensure_no_local_commits.call_count, 0) + self.assertEqual(tool._scm.ensure_clean_working_directory.call_count, 0) diff --git a/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectorywithlocalcommits.py b/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectorywithlocalcommits.py new file mode 100644 index 000000000..f06f94ef4 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/cleanworkingdirectorywithlocalcommits.py @@ -0,0 +1,34 @@ +# 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.steps.cleanworkingdirectory import CleanWorkingDirectory + +class CleanWorkingDirectoryWithLocalCommits(CleanWorkingDirectory): + def __init__(self, tool, options): + # FIXME: This a bit of a hack. Consider doing this more cleanly. + CleanWorkingDirectory.__init__(self, tool, options, allow_local_commits=True) diff --git a/Tools/Scripts/webkitpy/tool/steps/closebug.py b/Tools/Scripts/webkitpy/tool/steps/closebug.py new file mode 100644 index 000000000..b33e373bf --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/closebug.py @@ -0,0 +1,53 @@ +# 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.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options +from webkitpy.common.system.deprecated_logging import log + + +class CloseBug(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.close_bug, + ] + + def run(self, state): + if not self._options.close_bug: + return + # Check to make sure there are no r? or r+ patches on the bug before closing. + # Assume that r- patches are just previous patches someone forgot to obsolete. + # FIXME: Should this use self.cached_lookup('bug')? It's unclear if + # state["patch"].bug_id() always equals state['bug_id']. + patches = self._tool.bugs.fetch_bug(state["patch"].bug_id()).patches() + for patch in patches: + if patch.review() == "?" or patch.review() == "+": + log("Not closing bug %s as attachment %s has review=%s. Assuming there are more patches to land from this bug." % (patch.bug_id(), patch.id(), patch.review())) + return + self._tool.bugs.close_bug_as_fixed(state["patch"].bug_id(), "All reviewed patches have been landed. Closing bug.") diff --git a/Tools/Scripts/webkitpy/tool/steps/closebugforlanddiff.py b/Tools/Scripts/webkitpy/tool/steps/closebugforlanddiff.py new file mode 100644 index 000000000..e5a68dbf1 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/closebugforlanddiff.py @@ -0,0 +1,58 @@ +# 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.comments import bug_comment_from_commit_text +from webkitpy.tool.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options +from webkitpy.common.system.deprecated_logging import log + + +class CloseBugForLandDiff(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.close_bug, + ] + + def run(self, state): + comment_text = bug_comment_from_commit_text(self._tool.scm(), state["commit_text"]) + bug_id = state.get("bug_id") + if not bug_id and state.get("patch"): + bug_id = state.get("patch").bug_id() + + if bug_id: + log("Updating bug %s" % bug_id) + if self._options.close_bug: + self._tool.bugs.close_bug_as_fixed(bug_id, comment_text) + else: + # FIXME: We should a smart way to figure out if the patch is attached + # to the bug, and if so obsolete it. + self._tool.bugs.post_comment_to_bug(bug_id, comment_text) + else: + log(comment_text) + log("No bug id provided.") diff --git a/Tools/Scripts/webkitpy/tool/steps/closebugforlanddiff_unittest.py b/Tools/Scripts/webkitpy/tool/steps/closebugforlanddiff_unittest.py new file mode 100644 index 000000000..0a56564dd --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/closebugforlanddiff_unittest.py @@ -0,0 +1,40 @@ +# 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 +from webkitpy.tool.steps.closebugforlanddiff import CloseBugForLandDiff + +class CloseBugForLandDiffTest(unittest.TestCase): + def test_empty_state(self): + capture = OutputCapture() + step = CloseBugForLandDiff(MockTool(), MockOptions()) + expected_stderr = "Committed r49824: <http://trac.webkit.org/changeset/49824>\nNo bug id provided.\n" + capture.assert_outputs(self, step.run, [{"commit_text" : "Mock commit text"}], expected_stderr=expected_stderr) diff --git a/Tools/Scripts/webkitpy/tool/steps/closepatch.py b/Tools/Scripts/webkitpy/tool/steps/closepatch.py new file mode 100644 index 000000000..ff94df8b4 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/closepatch.py @@ -0,0 +1,36 @@ +# 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.comments import bug_comment_from_commit_text +from webkitpy.tool.steps.abstractstep import AbstractStep + + +class ClosePatch(AbstractStep): + def run(self, state): + comment_text = bug_comment_from_commit_text(self._tool.scm(), state["commit_text"]) + self._tool.bugs.clear_attachment_flags(state["patch"].id(), comment_text) diff --git a/Tools/Scripts/webkitpy/tool/steps/commit.py b/Tools/Scripts/webkitpy/tool/steps/commit.py new file mode 100644 index 000000000..08a7310bf --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/commit.py @@ -0,0 +1,88 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from webkitpy.common.checkout.scm import AuthenticationError, AmbiguousCommitError +from webkitpy.common.config import urls +from webkitpy.common.system.deprecated_logging import log +from webkitpy.common.system.executive import ScriptError +from webkitpy.common.system.user import User +from webkitpy.tool.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options + + +class Commit(AbstractStep): + # FIXME: This option exists only to make sure we don't break scripts which include --ignore-builders + # You can safely delete this option any time after 11/01/11. + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.check_builders, + ] + + def _commit_warning(self, error): + working_directory_message = "" if error.working_directory_is_clean else " and working copy changes" + return ('There are %s local commits%s. Everything will be committed as a single commit. ' + 'To avoid this prompt, set "git config webkit-patch.commit-should-always-squash true".' % ( + error.num_local_commits, working_directory_message)) + + def run(self, state): + self._commit_message = self._tool.checkout().commit_message_for_this_commit(self._options.git_commit).message() + if len(self._commit_message) < 10: + raise Exception("Attempted to commit with a commit message shorter than 10 characters. Either your patch is missing a ChangeLog or webkit-patch may have a bug.") + + self._state = state + + username = None + password = None + force_squash = False + + num_tries = 0 + while num_tries < 3: + num_tries += 1 + + try: + scm = self._tool.scm() + commit_text = scm.commit_with_message(self._commit_message, git_commit=self._options.git_commit, username=username, password=password, force_squash=force_squash, changed_files=self._changed_files(state)) + svn_revision = scm.svn_revision_from_commit_text(commit_text) + log("Committed r%s: <%s>" % (svn_revision, urls.view_revision_url(svn_revision))) + self._state["commit_text"] = commit_text + break; + except AmbiguousCommitError, e: + if self._tool.user.confirm(self._commit_warning(e)): + force_squash = True + else: + # This will correctly interrupt the rest of the commit process. + raise ScriptError(message="Did not commit") + except AuthenticationError, e: + username = self._tool.user.prompt("%s login: " % e.server_host, repeat=5) + if not username: + raise ScriptError("You need to specify the username on %s to perform the commit as." % e.server_host) + if e.prompt_for_password: + password = self._tool.user.prompt_password("%s password for %s: " % (e.server_host, username), repeat=5) + if not password: + raise ScriptError("You need to specify the password for %s on %s to perform the commit." % (username, e.server_host)) diff --git a/Tools/Scripts/webkitpy/tool/steps/confirmdiff.py b/Tools/Scripts/webkitpy/tool/steps/confirmdiff.py new file mode 100644 index 000000000..7e8e34898 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/confirmdiff.py @@ -0,0 +1,77 @@ +# 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 urllib + +from webkitpy.tool.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options +from webkitpy.common.prettypatch import PrettyPatch +from webkitpy.common.system import logutils +from webkitpy.common.system.executive import ScriptError + + +_log = logutils.get_logger(__file__) + + +class ConfirmDiff(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.confirm, + ] + + def _show_pretty_diff(self, diff): + if not self._tool.user.can_open_url(): + return None + + try: + pretty_patch = PrettyPatch(self._tool.executive, + self._tool.scm().checkout_root) + pretty_diff_file = pretty_patch.pretty_diff_file(diff) + url = "file://%s" % urllib.quote(pretty_diff_file.name) + self._tool.user.open_url(url) + # We return the pretty_diff_file here because we need to keep the + # file alive until the user has had a chance to confirm the diff. + return pretty_diff_file + except ScriptError, e: + _log.warning("PrettyPatch failed. :(") + except OSError, e: + _log.warning("PrettyPatch unavailable.") + + def run(self, state): + if not self._options.confirm: + return + diff = self.cached_lookup(state, "diff") + pretty_diff_file = self._show_pretty_diff(diff) + if not pretty_diff_file: + self._tool.user.page(diff) + diff_correct = self._tool.user.confirm("Was that diff correct?") + if pretty_diff_file: + pretty_diff_file.close() + if not diff_correct: + exit(1) diff --git a/Tools/Scripts/webkitpy/tool/steps/createbug.py b/Tools/Scripts/webkitpy/tool/steps/createbug.py new file mode 100644 index 000000000..0ab6f68a2 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/createbug.py @@ -0,0 +1,52 @@ +# 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.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options + + +class CreateBug(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.cc, + Options.component, + Options.blocks, + ] + + def run(self, state): + # No need to create a bug if we already have one. + if state.get("bug_id"): + return + cc = self._options.cc + if not cc: + cc = state.get("bug_cc") + blocks = self._options.blocks + if not blocks: + blocks = state.get("bug_blocked") + state["bug_id"] = self._tool.bugs.create_bug(state["bug_title"], state["bug_description"], blocked=blocks, component=self._options.component, cc=cc) diff --git a/Tools/Scripts/webkitpy/tool/steps/editchangelog.py b/Tools/Scripts/webkitpy/tool/steps/editchangelog.py new file mode 100644 index 000000000..2c5764722 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/editchangelog.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 os + +from webkitpy.tool.steps.abstractstep import AbstractStep + + +class EditChangeLog(AbstractStep): + def run(self, state): + absolute_paths = map(self._tool.scm().absolute_path, self.cached_lookup(state, "changelogs")) + self._tool.user.edit_changelog(absolute_paths) + self.did_modify_checkout(state) diff --git a/Tools/Scripts/webkitpy/tool/steps/ensurebugisopenandassigned.py b/Tools/Scripts/webkitpy/tool/steps/ensurebugisopenandassigned.py new file mode 100644 index 000000000..54f90b6d7 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/ensurebugisopenandassigned.py @@ -0,0 +1,41 @@ +# 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.steps.abstractstep import AbstractStep + + +class EnsureBugIsOpenAndAssigned(AbstractStep): + def run(self, state): + bug = self.cached_lookup(state, "bug") + if bug.is_unassigned(): + self._tool.bugs.reassign_bug(bug.id()) + + if bug.is_closed(): + # FIXME: We should probably pass this message in somehow? + # Right now this step is only used before PostDiff steps, so this is OK. + self._tool.bugs.reopen_bug(bug.id(), "Reopening to attach new patch.") diff --git a/Tools/Scripts/webkitpy/tool/steps/ensurelocalcommitifneeded.py b/Tools/Scripts/webkitpy/tool/steps/ensurelocalcommitifneeded.py new file mode 100644 index 000000000..2167351e2 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/ensurelocalcommitifneeded.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.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options +from webkitpy.common.system.deprecated_logging import error + + +class EnsureLocalCommitIfNeeded(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.local_commit, + ] + + def run(self, state): + if self._options.local_commit and not self._tool.scm().supports_local_commits(): + error("--local-commit passed, but %s does not support local commits" % self._tool.scm().display_name()) diff --git a/Tools/Scripts/webkitpy/tool/steps/metastep.py b/Tools/Scripts/webkitpy/tool/steps/metastep.py new file mode 100644 index 000000000..7cbd1c55c --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/metastep.py @@ -0,0 +1,54 @@ +# 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.steps.abstractstep import AbstractStep + + +# FIXME: Unify with StepSequence? I'm not sure yet which is the better design. +class MetaStep(AbstractStep): + substeps = [] # Override in subclasses + def __init__(self, tool, options): + AbstractStep.__init__(self, tool, options) + self._step_instances = [] + for step_class in self.substeps: + self._step_instances.append(step_class(tool, options)) + + @staticmethod + def _collect_options_from_steps(steps): + collected_options = [] + for step in steps: + collected_options = collected_options + step.options() + return collected_options + + @classmethod + def options(cls): + return cls._collect_options_from_steps(cls.substeps) + + def run(self, state): + for step in self._step_instances: + step.run(state) diff --git a/Tools/Scripts/webkitpy/tool/steps/obsoletepatches.py b/Tools/Scripts/webkitpy/tool/steps/obsoletepatches.py new file mode 100644 index 000000000..de508c6cc --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/obsoletepatches.py @@ -0,0 +1,51 @@ +# 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.grammar import pluralize +from webkitpy.tool.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options +from webkitpy.common.system.deprecated_logging import log + + +class ObsoletePatches(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.obsolete_patches, + ] + + def run(self, state): + if not self._options.obsolete_patches: + return + bug_id = state["bug_id"] + patches = self._tool.bugs.fetch_bug(bug_id).patches() + if not patches: + return + log("Obsoleting %s on bug %s" % (pluralize("old patch", len(patches)), bug_id)) + for patch in patches: + self._tool.bugs.obsolete_attachment(patch.id()) diff --git a/Tools/Scripts/webkitpy/tool/steps/options.py b/Tools/Scripts/webkitpy/tool/steps/options.py new file mode 100644 index 000000000..fc781a6cc --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/options.py @@ -0,0 +1,61 @@ +# 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 optparse import make_option + +class Options(object): + blocks = make_option("--blocks", action="store", type="string", dest="blocks", default=None, help="Bug number which the created bug blocks.") + build = make_option("--build", action="store_true", dest="build", default=False, help="Build and run run-webkit-tests before committing.") + build_style = make_option("--build-style", action="store", dest="build_style", default=None, help="Whether to build debug, release, or both.") + cc = make_option("--cc", action="store", type="string", dest="cc", help="Comma-separated list of email addresses to carbon-copy.") + check_builders = make_option("--ignore-builders", action="store_false", dest="check_builders", default=True, help="DEPRECATED: Will be removed any time after 11/01/11.") + check_style = make_option("--ignore-style", action="store_false", dest="check_style", default=True, help="Don't check to see if the patch has proper style before uploading.") + check_style_filter = make_option("--check-style-filter", action="store", type="string", dest="check_style_filter", default=None, help="Filter style-checker rules (see check-webkit-style --help).") + clean = make_option("--no-clean", action="store_false", dest="clean", default=True, help="Don't check if the working directory is clean before applying patches") + close_bug = make_option("--no-close", action="store_false", dest="close_bug", default=True, help="Leave bug open after landing.") + comment = make_option("--comment", action="store", type="string", dest="comment", help="Comment to post to bug.") + component = make_option("--component", action="store", type="string", dest="component", help="Component for the new bug.") + confirm = make_option("--no-confirm", action="store_false", dest="confirm", default=True, help="Skip confirmation steps.") + description = make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment") + email = make_option("--email", action="store", type="string", dest="email", help="Email address to use in ChangeLogs.") + force_clean = make_option("--force-clean", action="store_true", dest="force_clean", default=False, help="Clean working directory before applying patches (removes local changes and commits)") + force_patch = make_option("--force-patch", action="store_true", dest="force_patch", default=False, help="Forcefully applies the patch, continuing past errors.") + git_commit = make_option("-g", "--git-commit", action="store", dest="git_commit", help="Operate on a local commit. If a range, the commits are squashed into one. HEAD.. operates on working copy changes only.") + local_commit = make_option("--local-commit", action="store_true", dest="local_commit", default=False, help="Make a local commit for each applied patch") + non_interactive = make_option("--non-interactive", action="store_true", dest="non_interactive", default=False, help="Never prompt the user, fail as fast as possible.") + obsolete_patches = make_option("--no-obsolete", action="store_false", dest="obsolete_patches", default=True, help="Do not obsolete old patches before posting this one.") + open_bug = make_option("--open-bug", action="store_true", dest="open_bug", default=False, help="Opens the associated bug in a browser.") + parent_command = make_option("--parent-command", action="store", dest="parent_command", default=None, help="(Internal) The command that spawned this instance.") + quiet = make_option("--quiet", action="store_true", dest="quiet", default=False, help="Produce less console output.") + request_commit = make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review.") + review = make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review.") + reviewer = make_option("-r", "--reviewer", action="store", type="string", dest="reviewer", help="Update ChangeLogs to say Reviewed by REVIEWER.") + suggest_reviewers = make_option("--suggest-reviewers", action="store_true", default=False, help="Offer to CC appropriate reviewers.") + test = make_option("--test", action="store_true", dest="test", default=False, help="Run run-webkit-tests before committing.") + update = make_option("--no-update", action="store_false", dest="update", default=True, help="Don't update the working directory.") + changelog_count = make_option("--changelog-count", action="store", type="int", dest="changelog_count", help="Number of changelogs to parse.") diff --git a/Tools/Scripts/webkitpy/tool/steps/postdiff.py b/Tools/Scripts/webkitpy/tool/steps/postdiff.py new file mode 100644 index 000000000..6913cab88 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/postdiff.py @@ -0,0 +1,52 @@ +# 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.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options + + +class PostDiff(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.description, + Options.comment, + Options.review, + Options.request_commit, + Options.open_bug, + ] + + def run(self, state): + diff = self.cached_lookup(state, "diff") + description = self._options.description or "Patch" + comment_text = self._options.comment + bug_id = state["bug_id"] + + self._tool.bugs.add_patch_to_bug(bug_id, diff, description, comment_text=comment_text, mark_for_review=self._options.review, mark_for_commit_queue=self._options.request_commit) + if self._options.open_bug: + self._tool.user.open_url(self._tool.bugs.bug_url_for_bug_id(bug_id)) diff --git a/Tools/Scripts/webkitpy/tool/steps/postdiffforcommit.py b/Tools/Scripts/webkitpy/tool/steps/postdiffforcommit.py new file mode 100644 index 000000000..13bc00c4f --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/postdiffforcommit.py @@ -0,0 +1,39 @@ +# 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.steps.abstractstep import AbstractStep + + +class PostDiffForCommit(AbstractStep): + def run(self, state): + self._tool.bugs.add_patch_to_bug( + state["bug_id"], + self.cached_lookup(state, "diff"), + "Patch for landing", + mark_for_review=False, + mark_for_landing=True) diff --git a/Tools/Scripts/webkitpy/tool/steps/postdiffforrevert.py b/Tools/Scripts/webkitpy/tool/steps/postdiffforrevert.py new file mode 100644 index 000000000..2900eb39f --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/postdiffforrevert.py @@ -0,0 +1,49 @@ +# 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.net.bugzilla import Attachment +from webkitpy.tool.steps.abstractstep import AbstractStep + + +class PostDiffForRevert(AbstractStep): + def run(self, state): + comment_text = "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.\n\n\ +If you would like to land the rollout faster, you can use the \ +following command:\n\n\ + webkit-patch land-attachment ATTACHMENT_ID\n\n\ +where ATTACHMENT_ID is the ID of this attachment." + self._tool.bugs.add_patch_to_bug( + state["bug_id"], + self.cached_lookup(state, "diff"), + "%s%s" % (Attachment.rollout_preamble, state["revision"]), + comment_text=comment_text, + mark_for_review=False, + mark_for_commit_queue=True) diff --git a/Tools/Scripts/webkitpy/tool/steps/preparechangelog.py b/Tools/Scripts/webkitpy/tool/steps/preparechangelog.py new file mode 100644 index 000000000..caaafa2d4 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/preparechangelog.py @@ -0,0 +1,79 @@ +# 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.common.checkout.changelog import ChangeLog +from webkitpy.common.system.executive import ScriptError +from webkitpy.tool.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options +from webkitpy.common.system.deprecated_logging import error + + +class PrepareChangeLog(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.quiet, + Options.email, + Options.git_commit, + ] + + def _ensure_bug_url(self, state): + if not state.get("bug_id"): + return + bug_id = state.get("bug_id") + changelogs = self.cached_lookup(state, "changelogs") + for changelog_path in changelogs: + changelog = ChangeLog(changelog_path) + if not changelog.latest_entry().bug_id(): + changelog.set_short_description_and_bug_url( + self.cached_lookup(state, "bug_title"), + self._tool.bugs.bug_url_for_bug_id(bug_id)) + + def run(self, state): + if self.cached_lookup(state, "changelogs"): + self._ensure_bug_url(state) + return + args = self._tool.port().prepare_changelog_command() + if state.get("bug_id"): + args.append("--bug=%s" % state["bug_id"]) + args.append("--description=%s" % self.cached_lookup(state, 'bug_title')) + if self._options.email: + args.append("--email=%s" % self._options.email) + + if self._tool.scm().supports_local_commits(): + args.append("--merge-base=%s" % self._tool.scm().merge_base(self._options.git_commit)) + + args.extend(self._changed_files(state)) + + try: + self._tool.executive.run_and_throw_if_fail(args, self._options.quiet, cwd=self._tool.scm().checkout_root) + except ScriptError, e: + error("Unable to prepare ChangeLogs.") + self.did_modify_checkout(state) diff --git a/Tools/Scripts/webkitpy/tool/steps/preparechangelog_unittest.py b/Tools/Scripts/webkitpy/tool/steps/preparechangelog_unittest.py new file mode 100644 index 000000000..71ae51afd --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/preparechangelog_unittest.py @@ -0,0 +1,54 @@ +# 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.checkout.changelog_unittest import ChangeLogTest +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.tool.mocktool import MockOptions, MockTool +from webkitpy.tool.steps.preparechangelog import PrepareChangeLog + + +class PrepareChangeLogTest(ChangeLogTest): + def test_ensure_bug_url(self): + capture = OutputCapture() + step = PrepareChangeLog(MockTool(), MockOptions()) + changelog_contents = u"%s\n%s" % (self._new_entry_boilerplate, self._example_changelog) + changelog_path = self._write_tmp_file_with_contents(changelog_contents.encode("utf-8")) + state = { + "bug_title": "Example title", + "bug_id": 1234, + "changelogs": [changelog_path], + } + capture.assert_outputs(self, step.run, [state]) + actual_contents = self._read_file_contents(changelog_path, "utf-8") + expected_message = "Example title\n http://example.com/1234" + expected_contents = changelog_contents.replace("Need a short description and bug URL (OOPS!)", expected_message) + os.remove(changelog_path) + self.assertEquals(actual_contents.splitlines(), expected_contents.splitlines()) diff --git a/Tools/Scripts/webkitpy/tool/steps/preparechangelogfordepsroll.py b/Tools/Scripts/webkitpy/tool/steps/preparechangelogfordepsroll.py new file mode 100644 index 000000000..3387b2483 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/preparechangelogfordepsroll.py @@ -0,0 +1,40 @@ +# Copyright (C) 2011 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.checkout.changelog import ChangeLog +from webkitpy.tool.steps.abstractstep import AbstractStep + + +class PrepareChangeLogForDEPSRoll(AbstractStep): + def run(self, state): + self._tool.executive.run_and_throw_if_fail(self._tool.port().prepare_changelog_command()) + changelog_paths = self._tool.checkout().modified_changelogs(git_commit=None) + for changelog_path in changelog_paths: + ChangeLog(changelog_path).update_with_unreviewed_message("Unreviewed. Rolled DEPS.\n\n") diff --git a/Tools/Scripts/webkitpy/tool/steps/preparechangelogforrevert.py b/Tools/Scripts/webkitpy/tool/steps/preparechangelogforrevert.py new file mode 100644 index 000000000..84796d2ce --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/preparechangelogforrevert.py @@ -0,0 +1,60 @@ +# 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.common.checkout.changelog import ChangeLog +from webkitpy.common.config import urls +from webkitpy.tool.grammar import join_with_separators +from webkitpy.tool.steps.abstractstep import AbstractStep + + +class PrepareChangeLogForRevert(AbstractStep): + @classmethod + def _message_for_revert(cls, revision_list, reason, bug_url=None): + message = "Unreviewed, rolling out %s.\n" % join_with_separators(['r' + str(revision) for revision in revision_list]) + for revision in revision_list: + message += "%s\n" % urls.view_revision_url(revision) + if bug_url: + message += "%s\n" % bug_url + # Add an extra new line after the rollout links, before any reason. + message += "\n" + if reason: + message += "%s\n\n" % reason + return message + + def run(self, state): + # This could move to prepare-ChangeLog by adding a --revert= option. + self._tool.executive.run_and_throw_if_fail(self._tool.port().prepare_changelog_command(), cwd=self._tool.scm().checkout_root) + changelog_paths = self._tool.checkout().modified_changelogs(git_commit=None) + bug_url = self._tool.bugs.bug_url_for_bug_id(state["bug_id"]) if state["bug_id"] else None + message = self._message_for_revert(state["revision_list"], state["reason"], bug_url) + for changelog_path in changelog_paths: + # FIXME: Seems we should prepare the message outside of changelogs.py and then just pass in + # text that we want to use to replace the reviewed by line. + ChangeLog(changelog_path).update_with_unreviewed_message(message) diff --git a/Tools/Scripts/webkitpy/tool/steps/preparechangelogforrevert_unittest.py b/Tools/Scripts/webkitpy/tool/steps/preparechangelogforrevert_unittest.py new file mode 100644 index 000000000..aa9d5e981 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/preparechangelogforrevert_unittest.py @@ -0,0 +1,130 @@ +# Copyright (C) 2011 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 __future__ import with_statement + +import codecs +import os +import tempfile +import unittest + +from webkitpy.common.checkout.changelog import ChangeLog +from webkitpy.common.checkout.changelog_unittest import ChangeLogTest +from webkitpy.tool.steps.preparechangelogforrevert import * + + +class UpdateChangeLogsForRevertTest(unittest.TestCase): + @staticmethod + def _write_tmp_file_with_contents(byte_array): + assert(isinstance(byte_array, str)) + (file_descriptor, file_path) = tempfile.mkstemp() # NamedTemporaryFile always deletes the file on close in python < 2.6 + with os.fdopen(file_descriptor, "w") as file: + file.write(byte_array) + return file_path + + _revert_entry_with_bug_url = '''2009-08-19 Eric Seidel <eric@webkit.org> + + Unreviewed, rolling out r12345. + http://trac.webkit.org/changeset/12345 + http://example.com/123 + + Reason + + * Scripts/bugzilla-tool: +''' + + _revert_entry_without_bug_url = '''2009-08-19 Eric Seidel <eric@webkit.org> + + Unreviewed, rolling out r12345. + http://trac.webkit.org/changeset/12345 + + Reason + + * Scripts/bugzilla-tool: +''' + + _multiple_revert_entry_with_bug_url = '''2009-08-19 Eric Seidel <eric@webkit.org> + + Unreviewed, rolling out r12345, r12346, and r12347. + http://trac.webkit.org/changeset/12345 + http://trac.webkit.org/changeset/12346 + http://trac.webkit.org/changeset/12347 + http://example.com/123 + + Reason + + * Scripts/bugzilla-tool: +''' + + _multiple_revert_entry_without_bug_url = '''2009-08-19 Eric Seidel <eric@webkit.org> + + Unreviewed, rolling out r12345, r12346, and r12347. + http://trac.webkit.org/changeset/12345 + http://trac.webkit.org/changeset/12346 + http://trac.webkit.org/changeset/12347 + + Reason + + * Scripts/bugzilla-tool: +''' + + _revert_with_log_reason = """2009-08-19 Eric Seidel <eric@webkit.org> + + Unreviewed, rolling out r12345. + http://trac.webkit.org/changeset/12345 + http://example.com/123 + + This is a very long reason which should be long enough so that + _message_for_revert will need to wrap it. We'll also include + a + https://veryveryveryveryverylongbugurl.com/reallylongbugthingy.cgi?bug_id=12354 + link so that we can make sure we wrap that right too. + + * Scripts/bugzilla-tool: +""" + + def _assert_message_for_revert_output(self, args, expected_entry): + changelog_contents = u"%s\n%s" % (ChangeLogTest._new_entry_boilerplate, ChangeLogTest._example_changelog) + changelog_path = self._write_tmp_file_with_contents(changelog_contents.encode("utf-8")) + changelog = ChangeLog(changelog_path) + changelog.update_with_unreviewed_message(PrepareChangeLogForRevert._message_for_revert(*args)) + actual_entry = changelog.latest_entry() + os.remove(changelog_path) + self.assertEquals(actual_entry.contents(), expected_entry) + self.assertEquals(actual_entry.reviewer_text(), None) + # These checks could be removed to allow this to work on other entries: + self.assertEquals(actual_entry.author_name(), "Eric Seidel") + self.assertEquals(actual_entry.author_email(), "eric@webkit.org") + + def test_message_for_revert(self): + self._assert_message_for_revert_output([[12345], "Reason"], self._revert_entry_without_bug_url) + self._assert_message_for_revert_output([[12345], "Reason", "http://example.com/123"], self._revert_entry_with_bug_url) + self._assert_message_for_revert_output([[12345, 12346, 12347], "Reason"], self._multiple_revert_entry_without_bug_url) + self._assert_message_for_revert_output([[12345, 12346, 12347], "Reason", "http://example.com/123"], self._multiple_revert_entry_with_bug_url) + long_reason = "This is a very long reason which should be long enough so that _message_for_revert will need to wrap it. We'll also include a https://veryveryveryveryverylongbugurl.com/reallylongbugthingy.cgi?bug_id=12354 link so that we can make sure we wrap that right too." + self._assert_message_for_revert_output([[12345], long_reason, "http://example.com/123"], self._revert_with_log_reason) diff --git a/Tools/Scripts/webkitpy/tool/steps/promptforbugortitle.py b/Tools/Scripts/webkitpy/tool/steps/promptforbugortitle.py new file mode 100644 index 000000000..31c913cb0 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/promptforbugortitle.py @@ -0,0 +1,45 @@ +# 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.steps.abstractstep import AbstractStep + + +class PromptForBugOrTitle(AbstractStep): + def run(self, state): + # No need to prompt if we alrady have the bug_id. + if state.get("bug_id"): + return + user_response = self._tool.user.prompt("Please enter a bug number or a title for a new bug:\n") + # If the user responds with a number, we assume it's bug number. + # Otherwise we assume it's a bug subject. + try: + state["bug_id"] = int(user_response) + except ValueError, TypeError: + state["bug_title"] = user_response + # FIXME: This is kind of a lame description. + state["bug_description"] = user_response diff --git a/Tools/Scripts/webkitpy/tool/steps/reopenbugafterrollout.py b/Tools/Scripts/webkitpy/tool/steps/reopenbugafterrollout.py new file mode 100644 index 000000000..f369ca925 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/reopenbugafterrollout.py @@ -0,0 +1,44 @@ +# 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.comments import bug_comment_from_commit_text +from webkitpy.tool.steps.abstractstep import AbstractStep +from webkitpy.common.system.deprecated_logging import log + + +class ReopenBugAfterRollout(AbstractStep): + def run(self, state): + commit_comment = bug_comment_from_commit_text(self._tool.scm(), state["commit_text"]) + comment_text = "Reverted r%s for reason:\n\n%s\n\n%s" % (state["revision"], state["reason"], commit_comment) + + bug_id = state["bug_id"] + if not bug_id: + log(comment_text) + log("No bugs were updated.") + return + self._tool.bugs.reopen_bug(bug_id, comment_text) diff --git a/Tools/Scripts/webkitpy/tool/steps/revertrevision.py b/Tools/Scripts/webkitpy/tool/steps/revertrevision.py new file mode 100644 index 000000000..8016be5b9 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/revertrevision.py @@ -0,0 +1,35 @@ +# 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.steps.abstractstep import AbstractStep + + +class RevertRevision(AbstractStep): + def run(self, state): + self._tool.checkout().apply_reverse_diffs(state["revision_list"]) + self.did_modify_checkout(state) diff --git a/Tools/Scripts/webkitpy/tool/steps/runtests.py b/Tools/Scripts/webkitpy/tool/steps/runtests.py new file mode 100644 index 000000000..fa4273e8d --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/runtests.py @@ -0,0 +1,74 @@ +# 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.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options +from webkitpy.common.system.deprecated_logging import log + +class RunTests(AbstractStep): + # FIXME: This knowledge really belongs in the commit-queue. + NON_INTERACTIVE_FAILURE_LIMIT_COUNT = 20 + + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.test, + Options.non_interactive, + Options.quiet, + ] + + def run(self, state): + if not self._options.test: + return + + python_unittests_command = self._tool.port().run_python_unittests_command() + if python_unittests_command: + log("Running Python unit tests") + self._tool.executive.run_and_throw_if_fail(python_unittests_command, cwd=self._tool.scm().checkout_root) + + perl_unittests_command = self._tool.port().run_perl_unittests_command() + if perl_unittests_command: + log("Running Perl unit tests") + self._tool.executive.run_and_throw_if_fail(perl_unittests_command, cwd=self._tool.scm().checkout_root) + + javascriptcore_tests_command = self._tool.port().run_javascriptcore_tests_command() + if javascriptcore_tests_command: + log("Running JavaScriptCore tests") + self._tool.executive.run_and_throw_if_fail(javascriptcore_tests_command, quiet=True, cwd=self._tool.scm().checkout_root) + + log("Running run-webkit-tests") + args = self._tool.port().run_webkit_tests_command() + if self._options.non_interactive: + args.append("--no-new-test-results") + args.append("--no-launch-safari") + args.append("--exit-after-n-failures=%s" % self.NON_INTERACTIVE_FAILURE_LIMIT_COUNT) + + if self._options.quiet: + args.append("--quiet") + self._tool.executive.run_and_throw_if_fail(args, cwd=self._tool.scm().checkout_root) + diff --git a/Tools/Scripts/webkitpy/tool/steps/runtests_unittest.py b/Tools/Scripts/webkitpy/tool/steps/runtests_unittest.py new file mode 100644 index 000000000..fd59315b6 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/runtests_unittest.py @@ -0,0 +1,44 @@ +# Copyright (C) 2011 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 +from webkitpy.tool.steps.runtests import RunTests + +class RunTestsTest(unittest.TestCase): + def test_no_unit_tests(self): + tool = MockTool() + tool._deprecated_port.run_python_unittests_command = lambda: None + tool._deprecated_port.run_perl_unittests_command = lambda: None + step = RunTests(tool, MockOptions(test=True, non_interactive=True, quiet=False)) + expected_stderr = """Running JavaScriptCore tests +Running run-webkit-tests +""" + OutputCapture().assert_outputs(self, step.run, [{}], expected_stderr=expected_stderr) diff --git a/Tools/Scripts/webkitpy/tool/steps/steps_unittest.py b/Tools/Scripts/webkitpy/tool/steps/steps_unittest.py new file mode 100644 index 000000000..c464ec46b --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/steps_unittest.py @@ -0,0 +1,116 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import unittest + +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.common.config.ports import WebKitPort +from webkitpy.tool.mocktool import MockOptions, MockTool + +from webkitpy.tool import steps + +class StepsTest(unittest.TestCase): + def _step_options(self): + options = MockOptions() + options.non_interactive = True + options.port = 'MOCK port' + options.quiet = True + options.test = True + return options + + def _run_step(self, step, tool=None, options=None, state=None): + if not tool: + tool = MockTool() + if not options: + options = self._step_options() + if not state: + state = {} + step(tool, options).run(state) + + def test_update_step(self): + tool = MockTool() + options = self._step_options() + options.update = True + expected_stderr = "Updating working directory\n" + OutputCapture().assert_outputs(self, self._run_step, [steps.Update, tool, options], expected_stderr=expected_stderr) + + def test_prompt_for_bug_or_title_step(self): + tool = MockTool() + tool.user.prompt = lambda message: 50000 + self._run_step(steps.PromptForBugOrTitle, tool=tool) + + def _post_diff_options(self): + options = self._step_options() + options.git_commit = None + options.description = None + options.comment = None + options.review = True + options.request_commit = False + options.open_bug = True + return options + + def _assert_step_output_with_bug(self, step, bug_id, expected_stderr, options=None): + state = {'bug_id': bug_id} + OutputCapture().assert_outputs(self, self._run_step, [step, MockTool(), options, state], expected_stderr=expected_stderr) + + def _assert_post_diff_output_for_bug(self, step, bug_id, expected_stderr): + self._assert_step_output_with_bug(step, bug_id, expected_stderr, self._post_diff_options()) + + def test_post_diff(self): + expected_stderr = "MOCK add_patch_to_bug: bug_id=78, description=Patch, mark_for_review=True, mark_for_commit_queue=False, mark_for_landing=False\nMOCK: user.open_url: http://example.com/78\n" + self._assert_post_diff_output_for_bug(steps.PostDiff, 78, expected_stderr) + + def test_post_diff_for_commit(self): + expected_stderr = "MOCK add_patch_to_bug: bug_id=78, description=Patch for landing, mark_for_review=False, mark_for_commit_queue=False, mark_for_landing=True\n" + self._assert_post_diff_output_for_bug(steps.PostDiffForCommit, 78, expected_stderr) + + def test_ensure_bug_is_open_and_assigned(self): + expected_stderr = "MOCK reopen_bug 50004 with comment 'Reopening to attach new patch.'\n" + self._assert_step_output_with_bug(steps.EnsureBugIsOpenAndAssigned, 50004, expected_stderr) + expected_stderr = "MOCK reassign_bug: bug_id=50002, assignee=None\n" + self._assert_step_output_with_bug(steps.EnsureBugIsOpenAndAssigned, 50002, expected_stderr) + + def test_runtests_args(self): + mock_options = self._step_options() + step = steps.RunTests(MockTool(log_executive=True), mock_options) + # FIXME: We shouldn't use a real port-object here, but there is too much to mock at the moment. + mock_port = WebKitPort() + mock_port.name = lambda: "Mac" + tool = MockTool(log_executive=True) + tool.port = lambda: mock_port + step = steps.RunTests(tool, mock_options) + expected_stderr = """Running Python unit tests +MOCK run_and_throw_if_fail: ['Tools/Scripts/test-webkitpy'], cwd=/mock-checkout +Running Perl unit tests +MOCK run_and_throw_if_fail: ['Tools/Scripts/test-webkitperl'], cwd=/mock-checkout +Running JavaScriptCore tests +MOCK run_and_throw_if_fail: ['Tools/Scripts/run-javascriptcore-tests'], cwd=/mock-checkout +Running run-webkit-tests +MOCK run_and_throw_if_fail: ['Tools/Scripts/run-webkit-tests', '--no-new-test-results', '--no-launch-safari', '--exit-after-n-failures=20', '--quiet'], cwd=/mock-checkout +""" + OutputCapture().assert_outputs(self, step.run, [{}], expected_stderr=expected_stderr) diff --git a/Tools/Scripts/webkitpy/tool/steps/suggestreviewers.py b/Tools/Scripts/webkitpy/tool/steps/suggestreviewers.py new file mode 100644 index 000000000..76bef35ac --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/suggestreviewers.py @@ -0,0 +1,51 @@ +# 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.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options + + +class SuggestReviewers(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.git_commit, + Options.suggest_reviewers, + ] + + def run(self, state): + if not self._options.suggest_reviewers: + return + + reviewers = self._tool.checkout().suggested_reviewers(self._options.git_commit, self._changed_files(state)) + print "The following reviewers have recently modified files in your patch:" + print "\n".join([reviewer.full_name for reviewer in reviewers]) + if not self._tool.user.confirm("Would you like to CC them?"): + return + reviewer_emails = [reviewer.bugzilla_email() for reviewer in reviewers] + self._tool.bugs.add_cc_to_bug(state['bug_id'], reviewer_emails) diff --git a/Tools/Scripts/webkitpy/tool/steps/suggestreviewers_unittest.py b/Tools/Scripts/webkitpy/tool/steps/suggestreviewers_unittest.py new file mode 100644 index 000000000..e99566326 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/suggestreviewers_unittest.py @@ -0,0 +1,46 @@ +# 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 +from webkitpy.tool.steps.suggestreviewers import SuggestReviewers + + +class SuggestReviewersTest(unittest.TestCase): + def test_disabled(self): + step = SuggestReviewers(MockTool(), MockOptions(suggest_reviewers=False)) + OutputCapture().assert_outputs(self, step.run, [{}]) + + def test_basic(self): + capture = OutputCapture() + step = SuggestReviewers(MockTool(), MockOptions(suggest_reviewers=True, git_commit=None)) + expected_stdout = "The following reviewers have recently modified files in your patch:\nFoo Bar\n" + expected_stderr = "Would you like to CC them?\n" + capture.assert_outputs(self, step.run, [{"bug_id": "123"}], expected_stdout=expected_stdout, expected_stderr=expected_stderr) diff --git a/Tools/Scripts/webkitpy/tool/steps/update.py b/Tools/Scripts/webkitpy/tool/steps/update.py new file mode 100644 index 000000000..cae2bbd8d --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/update.py @@ -0,0 +1,51 @@ +# 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.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options +from webkitpy.common.system.deprecated_logging import log + + +class Update(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.non_interactive, + Options.update, + Options.quiet, + ] + + def run(self, state): + if not self._options.update: + return + log("Updating working directory") + self._tool.executive.run_and_throw_if_fail(self._update_command(), quiet=self._options.quiet, cwd=self._tool.scm().checkout_root) + + def _update_command(self): + update_command = self._tool.port().update_webkit_command(self._options.non_interactive) + return update_command diff --git a/Tools/Scripts/webkitpy/tool/steps/update_unittest.py b/Tools/Scripts/webkitpy/tool/steps/update_unittest.py new file mode 100644 index 000000000..19ef949da --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/update_unittest.py @@ -0,0 +1,60 @@ +# Copyright (C) 2011 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.config.ports import ChromiumPort, ChromiumXVFBPort +from webkitpy.tool.mocktool import MockOptions, MockTool +from webkitpy.tool.steps.update import Update + + +class UpdateTest(unittest.TestCase): + + def test_update_command_non_interactive(self): + tool = MockTool() + options = MockOptions(non_interactive=True) + step = Update(tool, options) + self.assertEqual(["mock-update-webkit"], step._update_command()) + + tool._deprecated_port = ChromiumPort() + self.assertEqual(["Tools/Scripts/update-webkit", "--chromium", "--force-update"], step._update_command()) + + tool._deprecated_port = ChromiumXVFBPort() + self.assertEqual(["Tools/Scripts/update-webkit", "--chromium", "--force-update"], step._update_command()) + + def test_update_command_interactive(self): + tool = MockTool() + options = MockOptions(non_interactive=False) + step = Update(tool, options) + self.assertEqual(["mock-update-webkit"], step._update_command()) + + tool._deprecated_port = ChromiumPort() + self.assertEqual(["Tools/Scripts/update-webkit", "--chromium"], step._update_command()) + + tool._deprecated_port = ChromiumXVFBPort() + self.assertEqual(["Tools/Scripts/update-webkit", "--chromium"], step._update_command()) diff --git a/Tools/Scripts/webkitpy/tool/steps/updatechangelogswithreview_unittest.py b/Tools/Scripts/webkitpy/tool/steps/updatechangelogswithreview_unittest.py new file mode 100644 index 000000000..8ec8891f9 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/updatechangelogswithreview_unittest.py @@ -0,0 +1,54 @@ +# 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 +from webkitpy.tool.steps.updatechangelogswithreviewer import UpdateChangeLogsWithReviewer + +class UpdateChangeLogsWithReviewerTest(unittest.TestCase): + def test_guess_reviewer_from_bug(self): + capture = OutputCapture() + step = UpdateChangeLogsWithReviewer(MockTool(), MockOptions()) + expected_stderr = "No reviewed patches on bug 50001, cannot infer reviewer.\n" + capture.assert_outputs(self, step._guess_reviewer_from_bug, [50001], expected_stderr=expected_stderr) + + def test_guess_reviewer_from_multipatch_bug(self): + capture = OutputCapture() + step = UpdateChangeLogsWithReviewer(MockTool(), MockOptions()) + expected_stderr = "Guessing \"Reviewer2\" as reviewer from attachment 10001 on bug 50000.\n" + capture.assert_outputs(self, step._guess_reviewer_from_bug, [50000], expected_stderr=expected_stderr) + + def test_empty_state(self): + capture = OutputCapture() + options = MockOptions() + options.reviewer = 'MOCK reviewer' + options.git_commit = 'MOCK git commit' + step = UpdateChangeLogsWithReviewer(MockTool(), options) + capture.assert_outputs(self, step.run, [{}]) diff --git a/Tools/Scripts/webkitpy/tool/steps/updatechangelogswithreviewer.py b/Tools/Scripts/webkitpy/tool/steps/updatechangelogswithreviewer.py new file mode 100644 index 000000000..e8881a3aa --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/updatechangelogswithreviewer.py @@ -0,0 +1,77 @@ +# 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.common.checkout.changelog import ChangeLog +from webkitpy.tool.grammar import pluralize +from webkitpy.tool.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options +from webkitpy.common.system.deprecated_logging import log, error + +class UpdateChangeLogsWithReviewer(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.git_commit, + Options.reviewer, + ] + + def _guess_reviewer_from_bug(self, bug_id): + # FIXME: It's unclear if it would be safe to use self.cached_lookup(state, 'bug') + # here as we don't currently have a way to invalidate a bug after making changes (like ObsoletePatches does). + patches = self._tool.bugs.fetch_bug(bug_id).reviewed_patches() + if not patches: + log("%s on bug %s, cannot infer reviewer." % ("No reviewed patches", bug_id)) + return None + patch = patches[-1] + log("Guessing \"%s\" as reviewer from attachment %s on bug %s." % (patch.reviewer().full_name, patch.id(), bug_id)) + return patch.reviewer().full_name + + def run(self, state): + bug_id = state.get("bug_id") + if not bug_id and state.get("patch"): + bug_id = state.get("patch").bug_id() + + reviewer = self._options.reviewer + if not reviewer: + if not bug_id: + log("No bug id provided and --reviewer= not provided. Not updating ChangeLogs with reviewer.") + return + reviewer = self._guess_reviewer_from_bug(bug_id) + + if not reviewer: + log("Failed to guess reviewer from bug %s and --reviewer= not provided. Not updating ChangeLogs with reviewer." % bug_id) + return + + # cached_lookup("changelogs") is always absolute paths. + for changelog_path in self.cached_lookup(state, "changelogs"): + ChangeLog(changelog_path).set_reviewer(reviewer) + + # Tell the world that we just changed something on disk so that the cached diff is invalidated. + self.did_modify_checkout(state) diff --git a/Tools/Scripts/webkitpy/tool/steps/updatechromiumdeps.py b/Tools/Scripts/webkitpy/tool/steps/updatechromiumdeps.py new file mode 100644 index 000000000..c9fc63179 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/updatechromiumdeps.py @@ -0,0 +1,73 @@ +# Copyright (C) 2011 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 urllib2 + +from webkitpy.tool.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options +from webkitpy.common.config import urls +from webkitpy.common.system.deprecated_logging import log, error + + +class UpdateChromiumDEPS(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.non_interactive, + ] + + # Notice that this method throws lots of exciting exceptions! + def _fetch_last_known_good_revision(self): + return int(urllib2.urlopen(urls.chromium_lkgr_url).read()) + + def _validate_revisions(self, current_chromium_revision, new_chromium_revision): + if new_chromium_revision < current_chromium_revision: + message = "Current Chromium DEPS revision %s is newer than %s." % (current_chromium_revision, new_chromium_revision) + if self._options.non_interactive: + error(message) # Causes the process to terminate. + log(message) + new_chromium_revision = self._tool.user.prompt("Enter new chromium revision (enter nothing to cancel):\n") + try: + new_chromium_revision = int(new_chromium_revision) + except ValueError, TypeError: + new_chromium_revision = None + if not new_chromium_revision: + error("Unable to update Chromium DEPS") + + + def run(self, state): + # Note that state["chromium_revision"] must be defined, but can be None. + new_chromium_revision = state["chromium_revision"] + if not new_chromium_revision: + new_chromium_revision = self._fetch_last_known_good_revision() + + deps = self._tool.checkout().chromium_deps() + current_chromium_revision = deps.read_variable("chromium_rev") + self._validate_revisions(current_chromium_revision, new_chromium_revision) + log("Updating Chromium DEPS to %s" % new_chromium_revision) + deps.write_variable("chromium_rev", new_chromium_revision) diff --git a/Tools/Scripts/webkitpy/tool/steps/validatechangelogs.py b/Tools/Scripts/webkitpy/tool/steps/validatechangelogs.py new file mode 100644 index 000000000..b6b33c0b6 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/validatechangelogs.py @@ -0,0 +1,76 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from webkitpy.tool.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options +from webkitpy.common.checkout.diff_parser import DiffParser +from webkitpy.common.system.deprecated_logging import error, log + + +# This is closely related to the ValidateReviewer step and the CommitterValidator class. +# We may want to unify more of this code in one place. +class ValidateChangeLogs(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.non_interactive, + ] + + def _check_changelog_diff(self, diff_file): + if not self._tool.checkout().is_path_to_changelog(diff_file.filename): + return True + # Each line is a tuple, the first value is the deleted line number + # Date, reviewer, bug title, bug url, and empty lines could all be + # identical in the most recent entries. If the diff starts any + # later than that, assume that the entry is wrong. + if diff_file.lines[0][0] < 8: + return True + if self._options.non_interactive: + return False + + log("The diff to %s looks wrong. Are you sure your ChangeLog entry is at the top of the file?" % (diff_file.filename)) + # FIXME: Do we need to make the file path absolute? + self._tool.scm().diff_for_file(diff_file.filename) + if self._tool.user.confirm("OK to continue?", default='n'): + return True + return False + + def run(self, state): + changed_files = self.cached_lookup(state, "changed_files") + for filename in changed_files: + if not self._tool.checkout().is_path_to_changelog(filename): + continue + # Diff ChangeLogs directly because svn-create-patch will move + # ChangeLog entries to the # top automatically, defeating our + # validation here. + # FIXME: Should we diff all the ChangeLogs at once? + diff = self._tool.scm().diff_for_file(filename) + parsed_diff = DiffParser(diff.splitlines()) + for filename, diff_file in parsed_diff.files.items(): + if not self._check_changelog_diff(diff_file): + error("ChangeLog entry in %s is not at the top of the file." % diff_file.filename) diff --git a/Tools/Scripts/webkitpy/tool/steps/validatechangelogs_unittest.py b/Tools/Scripts/webkitpy/tool/steps/validatechangelogs_unittest.py new file mode 100644 index 000000000..96bae9fa8 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/validatechangelogs_unittest.py @@ -0,0 +1,58 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import unittest + +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.thirdparty.mock import Mock +from webkitpy.tool.mocktool import MockOptions, MockTool +from webkitpy.tool.steps.validatechangelogs import ValidateChangeLogs + + +class ValidateChangeLogsTest(unittest.TestCase): + + def _assert_start_line_produces_output(self, start_line, should_fail=False, non_interactive=False): + tool = MockTool() + tool._checkout.is_path_to_changelog = lambda path: True + step = ValidateChangeLogs(tool, MockOptions(git_commit=None, non_interactive=non_interactive)) + diff_file = Mock() + diff_file.filename = "mock/ChangeLog" + diff_file.lines = [(start_line, start_line, "foo")] + expected_stdout = expected_stderr = "" + if should_fail and not non_interactive: + expected_stderr = "The diff to mock/ChangeLog looks wrong. Are you sure your ChangeLog entry is at the top of the file?\nOK to continue?\n" + result = OutputCapture().assert_outputs(self, step._check_changelog_diff, [diff_file], expected_stderr=expected_stderr) + self.assertEqual(not result, should_fail) + + def test_check_changelog_diff(self): + self._assert_start_line_produces_output(1) + self._assert_start_line_produces_output(7) + self._assert_start_line_produces_output(8, should_fail=True) + + self._assert_start_line_produces_output(1, non_interactive=False) + self._assert_start_line_produces_output(8, non_interactive=True, should_fail=True) diff --git a/Tools/Scripts/webkitpy/tool/steps/validatereviewer.py b/Tools/Scripts/webkitpy/tool/steps/validatereviewer.py new file mode 100644 index 000000000..1d4e92569 --- /dev/null +++ b/Tools/Scripts/webkitpy/tool/steps/validatereviewer.py @@ -0,0 +1,58 @@ +# 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 re + +from webkitpy.common.checkout.changelog import ChangeLog +from webkitpy.tool.steps.abstractstep import AbstractStep +from webkitpy.tool.steps.options import Options +from webkitpy.common.system.deprecated_logging import error, log + + +# FIXME: Some of this logic should probably be unified with CommitterValidator? +class ValidateReviewer(AbstractStep): + @classmethod + def options(cls): + return AbstractStep.options() + [ + Options.non_interactive, + ] + + def run(self, state): + # FIXME: For now we disable this check when a user is driving the script + # this check is too draconian (and too poorly tested) to foist upon users. + if not self._options.non_interactive: + return + for changelog_path in self.cached_lookup(state, "changelogs"): + changelog_entry = ChangeLog(changelog_path).latest_entry() + if changelog_entry.has_valid_reviewer(): + continue + reviewer_text = changelog_entry.reviewer_text() + if reviewer_text: + log("%s found in %s does not appear to be a valid reviewer according to committers.py." % (reviewer_text, changelog_path)) + error('%s neither lists a valid reviewer nor contains the string "Unreviewed" or "Rubber stamp" (case insensitive).' % changelog_path) |
