summaryrefslogtreecommitdiffstats
path: root/Tools/Scripts/webkitpy/tool/commands
diff options
context:
space:
mode:
authorBen Murdoch <benm@google.com>2011-05-05 14:36:32 +0100
committerBen Murdoch <benm@google.com>2011-05-10 15:38:30 +0100
commitf05b935882198ccf7d81675736e3aeb089c5113a (patch)
tree4ea0ca838d9ef1b15cf17ddb3928efb427c7e5a1 /Tools/Scripts/webkitpy/tool/commands
parent60fbdcc62bced8db2cb1fd233cc4d1e4ea17db1b (diff)
downloadexternal_webkit-f05b935882198ccf7d81675736e3aeb089c5113a.zip
external_webkit-f05b935882198ccf7d81675736e3aeb089c5113a.tar.gz
external_webkit-f05b935882198ccf7d81675736e3aeb089c5113a.tar.bz2
Merge WebKit at r74534: Initial merge by git.
Change-Id: I6ccd1154fa1b19c2ec2a66878eb675738735f1eb
Diffstat (limited to 'Tools/Scripts/webkitpy/tool/commands')
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/__init__.py14
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py51
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/bugfortest.py48
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/bugsearch.py42
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/commandtest.py48
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/index.html180
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/loupe.js144
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/main.css309
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/main.js543
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/queue.js186
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/util.js104
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/download.py405
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/download_unittest.py206
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/earlywarningsystem.py182
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py132
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/openbugs.py63
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/openbugs_unittest.py50
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/prettydiff.py38
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/queries.py389
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/queries_unittest.py90
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/queues.py406
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/queues_unittest.py380
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/queuestest.py95
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/rebaseline.py112
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py38
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/rebaselineserver.py457
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/rebaselineserver_unittest.py304
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/sheriffbot.py106
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py57
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/stepsequence.py83
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/upload.py483
-rw-r--r--Tools/Scripts/webkitpy/tool/commands/upload_unittest.py122
32 files changed, 5867 insertions, 0 deletions
diff --git a/Tools/Scripts/webkitpy/tool/commands/__init__.py b/Tools/Scripts/webkitpy/tool/commands/__init__.py
new file mode 100644
index 0000000..a974b67
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/__init__.py
@@ -0,0 +1,14 @@
+# Required for Python to search this directory for module files
+
+from webkitpy.tool.commands.bugsearch import BugSearch
+from webkitpy.tool.commands.bugfortest import BugForTest
+from webkitpy.tool.commands.download import *
+from webkitpy.tool.commands.earlywarningsystem import *
+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.sheriffbot import *
+from webkitpy.tool.commands.upload import *
diff --git a/Tools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py b/Tools/Scripts/webkitpy/tool/commands/abstractsequencedcommand.py
new file mode 100644
index 0000000..fd10890
--- /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/bugfortest.py b/Tools/Scripts/webkitpy/tool/commands/bugfortest.py
new file mode 100644
index 0000000..36aa6b5
--- /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 0000000..5cbc1a0
--- /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 0000000..c0efa50
--- /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.
+
+import unittest
+
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.tool.mocktool import MockOptions, MockTool
+
+class CommandsTest(unittest.TestCase):
+ def assert_execute_outputs(self, command, args, expected_stdout="", expected_stderr="", options=MockOptions(), tool=MockTool()):
+ 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)
diff --git a/Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/index.html b/Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/index.html
new file mode 100644
index 0000000..8bdf7c2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/index.html
@@ -0,0 +1,180 @@
+<!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-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">&laquo;</button>
+ <span id="test-index"></span> of <span id="test-count"></span>
+ <button id="next-test">&raquo;</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/commands/data/rebaselineserver/loupe.js b/Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/loupe.js
new file mode 100644
index 0000000..41f977a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/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/commands/data/rebaselineserver/main.css b/Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/main.css
new file mode 100644
index 0000000..76643c5
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/main.css
@@ -0,0 +1,309 @@
+/*
+ * 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;
+}
+
+#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/commands/data/rebaselineserver/main.js b/Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/main.js
new file mode 100644
index 0000000..aeaac04
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/main.js
@@ -0,0 +1,543 @@
+/*
+ * 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;
+
+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'); });
+
+ 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 = '';
+}
+
+/**
+ * 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('&nbsp;') + 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 = '';
+
+ 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 = '&nbsp;&nbsp;' + 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/commands/data/rebaselineserver/queue.js b/Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/queue.js
new file mode 100644
index 0000000..338e28f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/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/commands/data/rebaselineserver/util.js b/Tools/Scripts/webkitpy/tool/commands/data/rebaselineserver/util.js
new file mode 100644
index 0000000..5ad7612
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/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/commands/download.py b/Tools/Scripts/webkitpy/tool/commands/download.py
new file mode 100644
index 0000000..020f339
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/download.py
@@ -0,0 +1,405 @@
+# 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 webkitpy.tool.steps as 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.EnsureBuildersAreGreen,
+ steps.UpdateChangeLogsWithReviewer,
+ steps.ValidateReviewer,
+ steps.Build,
+ steps.RunTests,
+ steps.Commit,
+ steps.CloseBugForLandDiff,
+ ]
+ long_help = """land commits the current working copy diff (just as svn or git commit would).
+land will NOT build and run the tests before committing, but you can use the --build option for that.
+If a bug id is provided, or one can be found in the ChangeLog land will update the bug after committing."""
+
+ def _prepare_state(self, options, args, tool):
+ 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.ConfirmDiff,
+ steps.Build,
+ steps.RunTests,
+ steps.Commit,
+ ]
+
+
+class AbstractPatchProcessingCommand(AbstractDeclarativeCommand):
+ # Subclasses must implement the methods below. We don't declare them here
+ # because we want to be able to implement them with mix-ins.
+ #
+ # def _fetch_list_of_patches_to_process(self, options, args, tool):
+ # def _prepare_to_process(self, options, args, tool):
+
+ @staticmethod
+ def _collect_patches_by_bug(patches):
+ bugs_to_patches = {}
+ for patch in patches:
+ bugs_to_patches[patch.bug_id()] = bugs_to_patches.get(patch.bug_id(), []) + [patch]
+ return bugs_to_patches
+
+ def execute(self, options, args, tool):
+ self._prepare_to_process(options, args, tool)
+ patches = self._fetch_list_of_patches_to_process(options, args, tool)
+
+ # It's nice to print out total statistics.
+ bugs_to_patches = self._collect_patches_by_bug(patches)
+ log("Processing %s from %s." % (pluralize("patch", len(patches)), pluralize("bug", len(bugs_to_patches))))
+
+ for patch in patches:
+ self._process_patch(patch, options, args, tool)
+
+
+class AbstractPatchSequencingCommand(AbstractPatchProcessingCommand):
+ prepare_steps = None
+ main_steps = None
+
+ def __init__(self):
+ options = []
+ self._prepare_sequence = StepSequence(self.prepare_steps)
+ self._main_sequence = StepSequence(self.main_steps)
+ options = sorted(set(self._prepare_sequence.options() + self._main_sequence.options()))
+ AbstractPatchProcessingCommand.__init__(self, options)
+
+ def _prepare_to_process(self, options, args, tool):
+ self._prepare_sequence.run_and_handle_errors(tool, options)
+
+ def _process_patch(self, patch, options, args, tool):
+ state = { "patch" : patch }
+ self._main_sequence.run_and_handle_errors(tool, options, state)
+
+
+class ProcessAttachmentsMixin(object):
+ def _fetch_list_of_patches_to_process(self, options, args, tool):
+ return map(lambda patch_id: tool.bugs.fetch_attachment(patch_id), args)
+
+
+class ProcessBugsMixin(object):
+ def _fetch_list_of_patches_to_process(self, options, args, tool):
+ all_patches = []
+ for bug_id in args:
+ patches = tool.bugs.fetch_bug(bug_id).reviewed_patches()
+ log("%s found on bug %s." % (pluralize("reviewed patch", len(patches)), bug_id))
+ all_patches += patches
+ return all_patches
+
+
+class CheckStyle(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
+ name = "check-style"
+ help_text = "Run check-webkit-style on the specified attachments"
+ argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+ main_steps = [
+ steps.CleanWorkingDirectory,
+ steps.Update,
+ steps.ApplyPatch,
+ steps.CheckStyle,
+ ]
+
+
+class BuildAttachment(AbstractPatchSequencingCommand, ProcessAttachmentsMixin):
+ name = "build-attachment"
+ help_text = "Apply and build patches from bugzilla"
+ argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+ main_steps = [
+ steps.CleanWorkingDirectory,
+ steps.Update,
+ steps.ApplyPatch,
+ steps.Build,
+ ]
+
+
+class 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 AbstractPatchLandingCommand(AbstractPatchSequencingCommand):
+ prepare_steps = [
+ steps.EnsureBuildersAreGreen,
+ ]
+ main_steps = [
+ steps.CleanWorkingDirectory,
+ steps.Update,
+ steps.ApplyPatch,
+ steps.ValidateReviewer,
+ steps.Build,
+ steps.RunTests,
+ steps.Commit,
+ steps.ClosePatch,
+ steps.CloseBug,
+ ]
+ long_help = """Checks to make sure builders are green.
+Updates the working copy.
+Applies the patch.
+Builds.
+Runs the layout tests.
+Commits the patch.
+Clears the flags on the patch.
+Closes the bug if no patches are marked for review."""
+
+
+class LandAttachment(AbstractPatchLandingCommand, ProcessAttachmentsMixin):
+ name = "land-attachment"
+ help_text = "Land patches from bugzilla, optionally building and testing them first"
+ argument_names = "ATTACHMENT_ID [ATTACHMENT_IDS]"
+ show_in_main_help = True
+
+
+class LandFromBug(AbstractPatchLandingCommand, ProcessBugsMixin):
+ name = "land-from-bug"
+ help_text = "Land all patches on the given bugs, optionally building and testing them first"
+ argument_names = "BUGID [BUGIDS]"
+ show_in_main_help = True
+
+
+class AbstractRolloutPrepCommand(AbstractSequencedCommand):
+ argument_names = "REVISION [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 0000000..3748a8f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/download_unittest.py
@@ -0,0 +1,206 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.download import *
+from webkitpy.tool.mocktool import MockCheckout, MockOptions, MockTool
+
+
+class AbstractRolloutPrepCommandTest(unittest.TestCase):
+ def test_commit_info(self):
+ command = AbstractRolloutPrepCommand()
+ tool = MockTool()
+ command.bind_to_tool(tool)
+ output = OutputCapture()
+
+ expected_stderr = "Preparing rollout for bug 42.\n"
+ commit_info = output.assert_outputs(self, command._commit_info, [1234], expected_stderr=expected_stderr)
+ self.assertTrue(commit_info)
+
+ mock_commit_info = Mock()
+ mock_commit_info.bug_id = lambda: None
+ tool._checkout.commit_info_for_revision = lambda revision: mock_commit_info
+ expected_stderr = "Unable to parse bug number from diff.\n"
+ commit_info = output.assert_outputs(self, command._commit_info, [1234], expected_stderr=expected_stderr)
+ self.assertEqual(commit_info, mock_commit_info)
+
+ 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_builders = True
+ options.check_style = True
+ 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 197 from bug 42.\n"
+ self.assert_execute_outputs(ApplyAttachment(), [197], options=options, expected_stderr=expected_stderr)
+
+ def test_apply_patches(self):
+ options = self._default_options()
+ options.update = True
+ options.local_commit = True
+ expected_stderr = "Updating working directory\n2 reviewed patches found on bug 42.\nProcessing 2 patches from 1 bug.\nProcessing patch 197 from bug 42.\nProcessing patch 128 from bug 42.\n"
+ self.assert_execute_outputs(ApplyFromBug(), [42], options=options, expected_stderr=expected_stderr)
+
+ def test_land_diff(self):
+ expected_stderr = "Building WebKit\nRunning Python unit tests\nRunning Perl unit tests\nRunning JavaScriptCore tests\nRunning run-webkit-tests\nCommitted r49824: <http://trac.webkit.org/changeset/49824>\nUpdating bug 42\n"
+ mock_tool = MockTool()
+ mock_tool.scm().create_patch = Mock()
+ mock_tool.checkout().modified_changelogs = Mock(return_value=[])
+ self.assert_execute_outputs(Land(), [42], options=self._default_options(), expected_stderr=expected_stderr, tool=mock_tool)
+ # Make sure we're not calling expensive calls too often.
+ self.assertEqual(mock_tool.scm().create_patch.call_count, 0)
+ self.assertEqual(mock_tool.checkout().modified_changelogs.call_count, 1)
+
+ def test_land_red_builders(self):
+ expected_stderr = '\nWARNING: Builders ["Builder2"] are red, please watch your commit carefully.\nSee http://dummy_buildbot_host/console?category=core\n\nBuilding WebKit\nRunning Python unit tests\nRunning Perl unit tests\nRunning JavaScriptCore tests\nRunning run-webkit-tests\nCommitted r49824: <http://trac.webkit.org/changeset/49824>\nUpdating bug 42\n'
+ mock_tool = MockTool()
+ mock_tool.buildbot.light_tree_on_fire()
+ self.assert_execute_outputs(Land(), [42], options=self._default_options(), expected_stderr=expected_stderr, tool=mock_tool)
+
+ def test_check_style(self):
+ expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nRunning check-webkit-style\n"
+ self.assert_execute_outputs(CheckStyle(), [197], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_build_attachment(self):
+ expected_stderr = "Processing 1 patch from 1 bug.\nUpdating working directory\nProcessing patch 197 from bug 42.\nBuilding WebKit\n"
+ self.assert_execute_outputs(BuildAttachment(), [197], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_land_attachment(self):
+ # FIXME: This expected result is imperfect, notice how it's seeing the same patch as still there after it thought it would have cleared the flags.
+ expected_stderr = """Processing 1 patch from 1 bug.
+Updating working directory
+Processing patch 197 from bug 42.
+Building WebKit
+Running Python unit tests
+Running Perl unit tests
+Running JavaScriptCore tests
+Running run-webkit-tests
+Committed r49824: <http://trac.webkit.org/changeset/49824>
+Not closing bug 42 as attachment 197 has review=+. Assuming there are more patches to land from this bug.
+"""
+ self.assert_execute_outputs(LandAttachment(), [197], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_land_patches(self):
+ # FIXME: This expected result is imperfect, notice how it's seeing the same patch as still there after it thought it would have cleared the flags.
+ expected_stderr = """2 reviewed patches found on bug 42.
+Processing 2 patches from 1 bug.
+Updating working directory
+Processing patch 197 from bug 42.
+Building WebKit
+Running Python unit tests
+Running Perl unit tests
+Running JavaScriptCore tests
+Running run-webkit-tests
+Committed r49824: <http://trac.webkit.org/changeset/49824>
+Not closing bug 42 as attachment 197 has review=+. Assuming there are more patches to land from this bug.
+Updating working directory
+Processing patch 128 from bug 42.
+Building WebKit
+Running Python unit tests
+Running Perl unit tests
+Running JavaScriptCore tests
+Running run-webkit-tests
+Committed r49824: <http://trac.webkit.org/changeset/49824>
+Not closing bug 42 as attachment 197 has review=+. Assuming there are more patches to land from this bug.
+"""
+ self.assert_execute_outputs(LandFromBug(), [42], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_prepare_rollout(self):
+ expected_stderr = "Preparing rollout for bug 42.\nUpdating working directory\nRunning prepare-ChangeLog\n"
+ self.assert_execute_outputs(PrepareRollout(), [852, "Reason"], options=self._default_options(), expected_stderr=expected_stderr)
+
+ def test_create_rollout(self):
+ expected_stderr = """Preparing rollout for bug 42.
+Updating working directory
+MOCK create_bug
+bug_title: REGRESSION(r852): Reason
+bug_description: http://trac.webkit.org/changeset/852 broke the build:
+Reason
+component: MOCK component
+cc: MOCK cc
+blocked: 42
+Running prepare-ChangeLog
+MOCK add_patch_to_bug: bug_id=78, description=ROLLOUT of r852, mark_for_review=False, mark_for_commit_queue=True, mark_for_landing=False
+-- Begin comment --
+Any committer can land this patch automatically by marking it commit-queue+. The commit-queue will build and test the patch before landing to ensure that the rollout will be successful. This process takes approximately 15 minutes.
+
+If you would like to land the rollout faster, you can use the following command:
+
+ webkit-patch land-attachment ATTACHMENT_ID --ignore-builders
+
+where ATTACHMENT_ID is the ID of this attachment.
+-- End comment --
+"""
+ self.assert_execute_outputs(CreateRollout(), [852, "Reason"], options=self._default_options(), expected_stderr=expected_stderr)
+ 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 42.\nUpdating working directory\nRunning prepare-ChangeLog\nMOCK: user.open_url: file://...\nBuilding WebKit\nCommitted r49824: <http://trac.webkit.org/changeset/49824>\n"
+ expected_stdout = "Was that diff correct?\n"
+ self.assert_execute_outputs(Rollout(), [852, "Reason"], options=self._default_options(), expected_stdout=expected_stdout, 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 0000000..3b53d1a
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem.py
@@ -0,0 +1,182 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.commands.queues import AbstractReviewQueue
+from webkitpy.common.config.committers import CommitterList
+from webkitpy.common.config.ports import WebKitPort
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.tool.bot.queueengine import QueueEngine
+
+
+class AbstractEarlyWarningSystem(AbstractReviewQueue):
+ _build_style = "release"
+
+ def __init__(self):
+ AbstractReviewQueue.__init__(self)
+ self.port = WebKitPort.port(self.port_name)
+
+ def should_proceed_with_work_item(self, patch):
+ return True
+
+ def _can_build(self):
+ try:
+ self.run_webkit_patch([
+ "build",
+ self.port.flag(),
+ "--build-style=%s" % self._build_style,
+ "--force-clean",
+ "--no-update"])
+ return True
+ except ScriptError, e:
+ failure_log = self._log_from_script_error_for_upload(e)
+ self._update_status("Unable to perform a build", results_file=failure_log)
+ return False
+
+ def _build(self, patch, first_run=False):
+ try:
+ args = [
+ "build-attachment",
+ self.port.flag(),
+ "--build",
+ "--build-style=%s" % self._build_style,
+ "--force-clean",
+ "--quiet",
+ "--non-interactive",
+ patch.id()]
+ if not first_run:
+ # See commit-queue for an explanation of what we're doing here.
+ args.append("--no-update")
+ args.append("--parent-command=%s" % self.name)
+ self.run_webkit_patch(args)
+ return True
+ except ScriptError, e:
+ if first_run:
+ return False
+ raise
+
+ def review_patch(self, patch):
+ if patch.is_obsolete():
+ self._did_error(patch, "%s does not process obsolete patches." % self.name)
+ return False
+
+ if patch.bug().is_closed():
+ self._did_error(patch, "%s does not process patches on closed bugs." % self.name)
+ return False
+
+ if not self._build(patch, first_run=True):
+ if not self._can_build():
+ return False
+ self._build(patch)
+ return True
+
+ @classmethod
+ def handle_script_error(cls, tool, state, script_error):
+ is_svn_apply = script_error.command_name() == "svn-apply"
+ status_id = cls._update_status_for_script_error(tool, state, script_error, is_error=is_svn_apply)
+ if is_svn_apply:
+ QueueEngine.exit_after_handled_error(script_error)
+ results_link = tool.status_server.results_url_for_status(status_id)
+ message = "Attachment %s did not build on %s:\nBuild output: %s" % (state["patch"].id(), cls.port_name, results_link)
+ tool.bugs.post_comment_to_bug(state["patch"].bug_id(), message, cc=cls.watchers)
+ exit(1)
+
+
+class GtkEWS(AbstractEarlyWarningSystem):
+ name = "gtk-ews"
+ port_name = "gtk"
+ watchers = AbstractEarlyWarningSystem.watchers + [
+ "gns@gnome.org",
+ "xan.lopez@gmail.com",
+ ]
+
+
+class EflEWS(AbstractEarlyWarningSystem):
+ name = "efl-ews"
+ port_name = "efl"
+ watchers = AbstractEarlyWarningSystem.watchers + [
+ "leandro@profusion.mobi",
+ "antognolli@profusion.mobi",
+ "lucas.demarchi@profusion.mobi",
+ ]
+
+
+class QtEWS(AbstractEarlyWarningSystem):
+ name = "qt-ews"
+ port_name = "qt"
+
+
+class WinEWS(AbstractEarlyWarningSystem):
+ name = "win-ews"
+ port_name = "win"
+ # Use debug, the Apple Win port fails to link Release on 32-bit Windows.
+ # https://bugs.webkit.org/show_bug.cgi?id=39197
+ _build_style = "debug"
+
+
+class AbstractChromiumEWS(AbstractEarlyWarningSystem):
+ port_name = "chromium"
+ watchers = AbstractEarlyWarningSystem.watchers + [
+ "dglazkov@chromium.org",
+ ]
+
+
+class ChromiumLinuxEWS(AbstractChromiumEWS):
+ # FIXME: We should rename this command to cr-linux-ews, but that requires
+ # a database migration. :(
+ name = "chromium-ews"
+
+
+class ChromiumWindowsEWS(AbstractChromiumEWS):
+ name = "cr-win-ews"
+
+
+# For platforms that we can't run inside a VM (like Mac OS X), we require
+# patches to be uploaded by committers, who are generally trustworthy folk. :)
+class AbstractCommitterOnlyEWS(AbstractEarlyWarningSystem):
+ def __init__(self, committers=CommitterList()):
+ AbstractEarlyWarningSystem.__init__(self)
+ self._committers = committers
+
+ def process_work_item(self, patch):
+ if not self._committers.committer_by_email(patch.attacher_email()):
+ self._did_error(patch, "%s cannot process patches from non-committers :(" % self.name)
+ return False
+ return AbstractEarlyWarningSystem.process_work_item(self, patch)
+
+
+# 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 0000000..830e11c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/earlywarningsystem_unittest.py
@@ -0,0 +1,132 @@
+# 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_can_build(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.
+
+ queue = TestEWS()
+ queue.bind_to_tool(MockTool(log_executive=True))
+ queue._options = MockOptions(port=None)
+ expected_stderr = "MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build', '--port=win', '--build-style=release', '--force-clean', '--no-update']\n"
+ OutputCapture().assert_outputs(self, queue._can_build, [], expected_stderr=expected_stderr)
+
+ def mock_run_webkit_patch(args):
+ raise ScriptError("MOCK script error")
+
+ queue.run_webkit_patch = mock_run_webkit_patch
+ expected_stderr = "MOCK: update_status: None Unable to perform a build\n"
+ OutputCapture().assert_outputs(self, queue._can_build, [], expected_stderr=expected_stderr)
+
+ # FIXME: This belongs on an AbstractReviewQueueTest object in queues_unittest.py
+ def test_subprocess_handled_error(self):
+ queue = AbstractReviewQueue()
+ queue.bind_to_tool(MockTool())
+
+ def mock_review_patch(patch):
+ raise ScriptError('MOCK script error', exit_code=QueueEngine.handled_error_code)
+
+ queue.review_patch = mock_review_patch
+ mock_patch = queue._tool.bugs.fetch_attachment(197)
+ expected_stderr = "MOCK: release_work_item: None 197\n"
+ OutputCapture().assert_outputs(self, queue.process_work_item, [mock_patch], expected_stderr=expected_stderr, expected_exception=ScriptError)
+
+
+class EarlyWarningSytemTest(QueuesTest):
+ def test_failed_builds(self):
+ ews = ChromiumLinuxEWS()
+ ews.bind_to_tool(MockTool())
+ ews._build = lambda patch, first_run=False: False
+ ews._can_build = lambda: True
+ mock_patch = ews._tool.bugs.fetch_attachment(197)
+ ews.review_patch(mock_patch)
+
+ def _default_expected_stderr(self, ews):
+ string_replacemnts = {
+ "name": ews.name,
+ "port": ews.port_name,
+ "watchers": ews.watchers,
+ }
+ expected_stderr = {
+ "begin_work_queue": self._default_begin_work_queue_stderr(ews.name, ews._tool.scm().checkout_root),
+ "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 197\n" % string_replacemnts,
+ "handle_script_error": "MOCK: update_status: %(name)s ScriptError error message\nMOCK bug comment: bug_id=42, cc=%(watchers)s\n--- Begin comment ---\nAttachment 197 did not build on %(port)s:\nBuild output: http://dummy_url\n--- End comment ---\n\n" % string_replacemnts,
+ }
+ return expected_stderr
+
+ def _test_ews(self, ews):
+ ews.bind_to_tool(MockTool())
+ expected_exceptions = {
+ "handle_script_error": SystemExit,
+ }
+ self.assert_queue_outputs(ews, expected_stderr=self._default_expected_stderr(ews), expected_exceptions=expected_exceptions)
+
+ 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 197\n" % string_replacemnts
+ expected_exceptions = {"handle_script_error": SystemExit}
+ self.assert_queue_outputs(ews, expected_stderr=expected_stderr, expected_exceptions=expected_exceptions)
+
+ # FIXME: If all EWSes are going to output the same text, we
+ # could test them all in one method with a for loop over an array.
+ def test_chromium_linux_ews(self):
+ self._test_ews(ChromiumLinuxEWS())
+
+ def test_chromium_windows_ews(self):
+ self._test_ews(ChromiumWindowsEWS())
+
+ def test_qt_ews(self):
+ self._test_ews(QtEWS())
+
+ def test_gtk_ews(self):
+ self._test_ews(GtkEWS())
+
+ def test_efl_ews(self):
+ self._test_ews(EflEWS())
+
+ def test_mac_ews(self):
+ self._test_committer_only_ews(MacEWS())
+
+ def test_chromium_mac_ews(self):
+ self._test_committer_only_ews(ChromiumMacEWS())
diff --git a/Tools/Scripts/webkitpy/tool/commands/openbugs.py b/Tools/Scripts/webkitpy/tool/commands/openbugs.py
new file mode 100644
index 0000000..1b51c9f
--- /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 0000000..40a6e1b
--- /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 0000000..e3fc00c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/prettydiff.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand
+import webkitpy.tool.steps as steps
+
+
+class PrettyDiff(AbstractSequencedCommand):
+ name = "pretty-diff"
+ help_text = "Shows the pretty diff in the default browser"
+ steps = [
+ steps.ConfirmDiff,
+ ]
diff --git a/Tools/Scripts/webkitpy/tool/commands/queries.py b/Tools/Scripts/webkitpy/tool/commands/queries.py
new file mode 100644
index 0000000..f04f384
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/queries.py
@@ -0,0 +1,389 @@
+# 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
+
+import webkitpy.tool.steps as steps
+
+from webkitpy.common.checkout.commitinfo import CommitInfo
+from webkitpy.common.config.committers import CommitterList
+from webkitpy.common.net.buildbot import BuildBot
+from webkitpy.common.net.regressionwindow import RegressionWindow
+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" % BuildBot.default_host
+
+ def _print_builder_line(self, builder_name, max_name_width, status_message):
+ print "%s : %s" % (builder_name.ljust(max_name_width), status_message)
+
+ 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" % BuildBot.default_host
+ argument_names = "REVISION"
+
+ def _print_layout_test_results(self, results):
+ if not results:
+ print " No results."
+ return
+ for title, files in results.parsed_results().items():
+ print " %s" % title
+ for filename in files:
+ print " %s" % filename
+
+ def execute(self, options, args, tool):
+ builders = self._tool.buildbot.builders()
+ for builder in builders:
+ print "%s:" % builder.name()
+ build = builder.build_for_revision(args[0], allow_failed_lookups=True)
+ self._print_layout_test_results(build.layout_test_results())
+
+
+class FailureReason(AbstractDeclarativeCommand):
+ name = "failure-reason"
+ help_text = "Lists revisions where individual test failures started at %s" % BuildBot.default_host
+
+ def _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; can't continue. (start revision = r%s)" % start_revision
+ return 1
+
+ results_to_explain = set(layout_test_results.failing_tests())
+ last_build_with_results = build
+ print "Starting at %s" % revision_to_test
+ while results_to_explain:
+ revision_to_test -= 1
+ new_build = builder.build_for_revision(revision_to_test, allow_failed_lookups=True)
+ if not new_build:
+ print "No build for %s" % revision_to_test
+ continue
+ build = new_build
+ latest_results = build.layout_test_results()
+ if not latest_results:
+ print "No results build %s (r%s)" % (build._number, build.revision())
+ continue
+ failures = set(latest_results.failing_tests())
+ if len(failures) >= 20:
+ # FIXME: We may need to move this logic into the LayoutTestResults class.
+ # The buildbot stops runs after 20 failures so we don't have full results to work with here.
+ print "Too many failures in build %s (r%s), ignoring." % (build._number, build.revision())
+ continue
+ fixed_results = results_to_explain - failures
+ if not fixed_results:
+ print "No change in build %s (r%s), %s unexplained failures (%s in this build)" % (build._number, build.revision(), len(results_to_explain), len(failures))
+ last_build_with_results = build
+ continue
+ 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 = 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" % BuildBot.default_host
+
+ 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 = 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" % BuildBot.default_host
+ long_help = """Fetches build status from http://build.webkit.org/one_box_per_builder
+and displayes the status of each builder."""
+
+ def execute(self, options, args, tool):
+ for builder in tool.buildbot.builder_statuses():
+ status_string = "ok" if builder["is_green"] else "FAIL"
+ print "%s : %s" % (status_string.ljust(4), builder["name"])
+
+
+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, port_object in tool.port_factory.get_all().iteritems():
+ 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 0000000..05a4a5c
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/queries_unittest.py
@@ -0,0 +1,90 @@
+# 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 QueryCommandsTest(CommandsTest):
+ def test_bugs_to_commit(self):
+ expected_stderr = "Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)\n"
+ self.assert_execute_outputs(BugsToCommit(), None, "42\n77\n", expected_stderr)
+
+ def test_patches_in_commit_queue(self):
+ expected_stdout = "http://example.com/197\nhttp://example.com/103\n"
+ expected_stderr = "Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)\nPatches in commit queue:\n"
+ self.assert_execute_outputs(PatchesInCommitQueue(), None, expected_stdout, expected_stderr)
+
+ def test_patches_to_commit_queue(self):
+ expected_stdout = "http://example.com/104&action=edit\n"
+ expected_stderr = "197 already has cq=+\n128 already has cq=+\n105 committer = \"Eric Seidel\" <eric@webkit.org>\n"
+ options = Mock()
+ options.bugs = False
+ self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options)
+
+ expected_stdout = "http://example.com/77\n"
+ options.bugs = True
+ self.assert_execute_outputs(PatchesToCommitQueue(), None, expected_stdout, expected_stderr, options=options)
+
+ def test_patches_to_review(self):
+ expected_stdout = "103\n"
+ expected_stderr = "Patches pending review:\n"
+ self.assert_execute_outputs(PatchesToReview(), None, expected_stdout, expected_stderr)
+
+ def test_tree_status(self):
+ expected_stdout = "ok : Builder1\nok : Builder2\n"
+ self.assert_execute_outputs(TreeStatus(), None, expected_stdout)
+
+ def test_skipped_ports(self):
+ 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)
+
+ expected_stdout = "Ports skipping test 'foo': test_port1\n"
+ self.assert_execute_outputs(SkippedPorts(), ("foo",), expected_stdout)
+
+ expected_stdout = "Test 'media' is not skipped by any port.\n"
+ self.assert_execute_outputs(SkippedPorts(), ("media",), expected_stdout)
+
+
+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 0000000..e15555f
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/queues.py
@@ -0,0 +1,406 @@
+# 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.layouttestresults import LayoutTestResults
+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.commitqueuetask import CommitQueueTask, CommitQueueTaskDelegate
+from webkitpy.tool.bot.feeders import CommitQueueFeeder, EWSFeeder
+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)
+
+ def _log_directory(self):
+ return "%s-logs" % self.name
+
+ # QueueEngineDelegate methods
+
+ def queue_log_path(self):
+ return os.path.join(self._log_directory(), "%s.log" % self.name)
+
+ def work_item_log_path(self, work_item):
+ raise NotImplementedError, "subclasses must implement"
+
+ def begin_work_queue(self):
+ log("CAUTION: %s will discard all local changes in \"%s\"" % (self.name, self._tool.scm().checkout_root))
+ if self._options.confirm:
+ response = self._tool.user.prompt("Are you sure? Type \"yes\" to continue: ")
+ if (response != "yes"):
+ error("User declined.")
+ log("Running WebKit %s." % self.name)
+ 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
+
+ # AbstractPatchQueue 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)
+
+ 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.bugs)
+
+ 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.bugs)
+ validator.reject_patch_from_commit_queue(patch.id(), self._error_message_for_bug(task.failure_status_id, e))
+ self._did_fail(patch)
+
+ def _error_message_for_bug(self, status_id, script_error):
+ if not script_error.output:
+ return script_error.message_with_output()
+ results_link = self._tool.status_server.results_url_for_status(status_id)
+ return "%s\nFull output: %s" % (script_error.message_with_output(), 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)
+
+ # 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:
+ with codecs.open(path, "r", "utf-8") as open_file:
+ return open_file.read()
+ except OSError, e: # File does not exist or can't be read.
+ return None
+
+ # FIXME: This may belong on the Port object.
+ def 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 refetch_patch(self, patch):
+ return self._tool.bugs.fetch_attachment(patch.id())
+
+ def report_flaky_tests(self, patch, flaky_tests):
+ reporter = FlakyTestReporter(self._tool, self.name)
+ reporter.report_flaky_tests(flaky_tests, patch)
+
+ # 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):
+ self.run_webkit_patch(["check-style", "--force-clean", "--non-interactive", "--parent-command=style-queue", patch.id()])
+ return True
+
+ @classmethod
+ def handle_script_error(cls, tool, state, script_error):
+ is_svn_apply = script_error.command_name() == "svn-apply"
+ status_id = cls._update_status_for_script_error(tool, state, script_error, is_error=is_svn_apply)
+ if is_svn_apply:
+ QueueEngine.exit_after_handled_error(script_error)
+ message = "Attachment %s did not pass %s:\n\n%s\n\nIf any of these errors are false positives, please file a bug against check-webkit-style." % (state["patch"].id(), cls.name, script_error.message_with_output(output_limit=3*1024))
+ tool.bugs.post_comment_to_bug(state["patch"].bug_id(), message, cc=cls.watchers)
+ exit(1)
diff --git a/Tools/Scripts/webkitpy/tool/commands/queues_unittest.py b/Tools/Scripts/webkitpy/tool/commands/queues_unittest.py
new file mode 100644
index 0000000..d793213
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/queues_unittest.py
@@ -0,0 +1,380 @@
+# 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.checkout.scm import CheckoutNeedsUpdate
+from webkitpy.common.net.bugzilla import Attachment
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.queues import *
+from webkitpy.tool.commands.queuestest import QueuesTest
+from webkitpy.tool.commands.stepsequence import StepSequence
+from webkitpy.tool.mocktool import MockTool, MockSCM, MockStatusServer
+
+
+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(), "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)
+
+ 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", MockSCM.fake_checkout_root),
+ "should_proceed_with_work_item": "",
+ "next_work_item": "",
+ "process_work_item": """Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)
+Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)
+MOCK setting flag 'commit-queue' to '-' on attachment '128' with comment 'Rejecting attachment 128 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 [106, 197]
+Feeding commit-queue items [106, 197]
+Feeding EWS (1 r? patch, 1 new)
+MOCK: submit_to_ews: 103
+""",
+ "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, 197])
+ 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(), 197)
+
+
+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(CommitQueue):
+ def __init__(self):
+ self._reject_patch = False
+ CommitQueue.__init__(self)
+
+ 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 test_commit_queue(self):
+ expected_stderr = {
+ "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue", MockSCM.fake_checkout_root),
+ "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 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 197
+""",
+ "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting attachment 197 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", MockSCM.fake_checkout_root),
+ "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 '197' with comment 'Rejecting attachment 197 from commit-queue.' and additional comment 'MOCK script error'
+MOCK: update_status: commit-queue Fail
+MOCK: release_work_item: commit-queue 197
+""",
+ "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting attachment 197 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_rollout(self):
+ tool = MockTool(log_executive=True)
+ tool.buildbot.light_tree_on_fire()
+ expected_stderr = {
+ "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue", MockSCM.fake_checkout_root),
+ "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']
+MOCK: update_status: commit-queue Cleaned working directory
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'update']
+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', 197]
+MOCK: update_status: commit-queue Applied patch
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build', '--no-clean', '--no-update', '--build-style=both']
+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']
+MOCK: update_status: commit-queue Passed tests
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--ignore-builders', '--non-interactive', '--parent-command=commit-queue', 197]
+MOCK: update_status: commit-queue Landed patch
+MOCK: update_status: commit-queue Pass
+MOCK: release_work_item: commit-queue 197
+""",
+ "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '197' with comment 'Rejecting attachment 197 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(106) # _patch6, a rollout patch.
+ assert(rollout_patch.is_rollout())
+ expected_stderr = {
+ "begin_work_queue": self._default_begin_work_queue_stderr("commit-queue", MockSCM.fake_checkout_root),
+ "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']
+MOCK: update_status: commit-queue Cleaned working directory
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'update']
+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', 106]
+MOCK: update_status: commit-queue Applied patch
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'build', '--no-clean', '--no-update', '--build-style=both']
+MOCK: update_status: commit-queue Built patch
+MOCK run_and_throw_if_fail: ['echo', '--status-host=example.com', 'land-attachment', '--force-clean', '--ignore-builders', '--non-interactive', '--parent-command=commit-queue', 106]
+MOCK: update_status: commit-queue Landed patch
+MOCK: update_status: commit-queue Pass
+MOCK: release_work_item: commit-queue 106
+""",
+ "handle_unexpected_error": "MOCK setting flag 'commit-queue' to '-' on attachment '106' with comment 'Rejecting attachment 106 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()
+ queue.bind_to_tool(MockTool())
+ 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 Built patch
+MOCK: update_status: commit-queue Passed tests
+MOCK: update_status: commit-queue Retry
+MOCK: release_work_item: commit-queue 197
+"""
+ OutputCapture().assert_outputs(self, queue.process_work_item, [QueuesTest.mock_work_item], expected_stderr=expected_stderr)
+
+ def test_report_flaky_tests(self):
+ queue = CommitQueue()
+ queue.bind_to_tool(MockTool())
+ expected_stderr = """MOCK bug comment: bug_id=76, cc=None
+--- Begin comment ---
+The commit-queue just saw foo/bar.html flake while processing attachment 197 on bug 42.
+Port: MockPort Platform: MockPlatform 1.0
+--- End comment ---
+
+MOCK bug comment: bug_id=76, cc=None
+--- Begin comment ---
+The commit-queue just saw bar/baz.html flake while processing attachment 197 on bug 42.
+Port: MockPort Platform: MockPlatform 1.0
+--- End comment ---
+
+MOCK bug comment: bug_id=42, cc=None
+--- Begin comment ---
+The commit-queue encountered the following flaky tests while processing attachment 197:
+
+foo/bar.html bug 76 (author: abarth@webkit.org)
+bar/baz.html bug 76 (author: abarth@webkit.org)
+The commit-queue is continuing to process your patch.
+--- End comment ---
+
+"""
+ OutputCapture().assert_outputs(self, queue.report_flaky_tests, [QueuesTest.mock_work_item, ["foo/bar.html", "bar/baz.html"]], expected_stderr=expected_stderr)
+
+ def test_layout_test_results(self):
+ queue = CommitQueue()
+ queue.bind_to_tool(MockTool())
+ queue._read_file_contents = lambda path: None
+ self.assertEquals(queue.layout_test_results(), None)
+ queue._read_file_contents = lambda path: ""
+ self.assertEquals(queue.layout_test_results(), None)
+
+
+class StyleQueueTest(QueuesTest):
+ def test_style_queue(self):
+ expected_stderr = {
+ "begin_work_queue": self._default_begin_work_queue_stderr("style-queue", MockSCM.fake_checkout_root),
+ "next_work_item": "",
+ "should_proceed_with_work_item": "MOCK: update_status: style-queue Checking style\n",
+ "process_work_item": "MOCK: update_status: style-queue Pass\nMOCK: release_work_item: style-queue 197\n",
+ "handle_unexpected_error": "Mock error message\n",
+ "handle_script_error": "MOCK: update_status: style-queue ScriptError error message\nMOCK bug comment: bug_id=42, cc=[]\n--- Begin comment ---\nAttachment 197 did not pass style-queue:\n\nScriptError error message\n\nIf any of these errors are false positives, please file a bug against check-webkit-style.\n--- End comment ---\n\n",
+ }
+ expected_exceptions = {
+ "handle_script_error": SystemExit,
+ }
+ self.assert_queue_outputs(StyleQueue(), expected_stderr=expected_stderr, expected_exceptions=expected_exceptions)
diff --git a/Tools/Scripts/webkitpy/tool/commands/queuestest.py b/Tools/Scripts/webkitpy/tool/commands/queuestest.py
new file mode 100644
index 0000000..6455617
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/queuestest.py
@@ -0,0 +1,95 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.common.net.bugzilla import Attachment
+from webkitpy.common.system.outputcapture import OutputCapture
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.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(197)
+
+ 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):
+ 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()
+ 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 0000000..8c4b997
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/rebaseline.py
@@ -0,0 +1,112 @@
+# Copyright (c) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os.path
+import re
+import shutil
+import urllib
+
+from webkitpy.common.net.buildbot import BuildBot
+from webkitpy.common.net.layouttestresults import LayoutTestResults
+from webkitpy.common.system.user import User
+from webkitpy.layout_tests.port import factory
+from webkitpy.tool.grammar import pluralize
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+
+
+# FIXME: I'm not sure where this logic should go in the end.
+# For now it's here, until we have a second need for it.
+class BuilderToPort(object):
+ _builder_name_to_port_name = {
+ r"SnowLeopard": "mac-snowleopard",
+ r"Leopard": "mac-leopard",
+ r"Tiger": "mac-tiger",
+ r"Windows": "win",
+ r"GTK": "gtk",
+ r"Qt": "qt",
+ r"Chromium Mac": "chromium-mac",
+ r"Chromium Linux": "chromium-linux",
+ r"Chromium Win": "chromium-win",
+ }
+
+ def _port_name_for_builder_name(self, builder_name):
+ for regexp, port_name in self._builder_name_to_port_name.items():
+ if re.match(regexp, builder_name):
+ return port_name
+
+ def port_for_builder(self, builder_name):
+ port_name = self._port_name_for_builder_name(builder_name)
+ assert(port_name) # Need to update _builder_name_to_port_name
+ port = factory.get(port_name)
+ assert(port) # Need to update _builder_name_to_port_name
+ return port
+
+
+class Rebaseline(AbstractDeclarativeCommand):
+ name = "rebaseline"
+ help_text = "Replaces local expected.txt files with new results from build bots"
+
+ # FIXME: This should share more code with FailureReason._builder_to_explain
+ def _builder_to_pull_from(self):
+ builder_statuses = self._tool.buildbot.builder_statuses()
+ red_statuses = [status for status in builder_statuses if not status["is_green"]]
+ print "%s failing" % (pluralize("builder", len(red_statuses)))
+ builder_choices = [status["name"] for status in red_statuses]
+ chosen_name = self._tool.user.prompt_with_list("Which builder to pull results from:", builder_choices)
+ # FIXME: prompt_with_list should really take a set of objects and a set of names and then return the object.
+ for status in red_statuses:
+ if status["name"] == chosen_name:
+ return (self._tool.buildbot.builder_with_name(chosen_name), status["build_number"])
+
+ def _replace_expectation_with_remote_result(self, local_file, remote_file):
+ (downloaded_file, headers) = urllib.urlretrieve(remote_file)
+ shutil.move(downloaded_file, local_file)
+
+ def _tests_to_update(self, build):
+ failing_tests = build.layout_test_results().results_matching_keys([LayoutTestResults.fail_key])
+ 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 = BuilderToPort().port_for_builder(builder.name())
+
+ for test in self._tests_to_update(build):
+ results_url = self._results_url_for_test(build, test)
+ # Port operates with absolute paths.
+ absolute_path = os.path.join(port.layout_tests_dir(), test)
+ expected_file = port.expected_filename(absolute_path, ".txt")
+ print test
+ self._replace_expectation_with_remote_result(expected_file, results_url)
+
+ # FIXME: We should handle new results too.
diff --git a/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py b/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py
new file mode 100644
index 0000000..d6582a7
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py
@@ -0,0 +1,38 @@
+# Copyright (C) 2010 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from webkitpy.tool.commands.rebaseline import BuilderToPort
+
+
+class BuilderToPortTest(unittest.TestCase):
+ def test_port_for_builder(self):
+ converter = BuilderToPort()
+ port = converter.port_for_builder("Leopard Intel Debug (Tests)")
+ self.assertEqual(port.name(), "mac-leopard")
diff --git a/Tools/Scripts/webkitpy/tool/commands/rebaselineserver.py b/Tools/Scripts/webkitpy/tool/commands/rebaselineserver.py
new file mode 100644
index 0000000..56780b5
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/rebaselineserver.py
@@ -0,0 +1,457 @@
+# 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."""
+from __future__ import with_statement
+
+import codecs
+import datetime
+import fnmatch
+import mimetypes
+import os
+import os.path
+import shutil
+import threading
+import time
+import urlparse
+import BaseHTTPServer
+
+from optparse import make_option
+from wsgiref.handlers import format_date_time
+
+from webkitpy.common import system
+from webkitpy.layout_tests.port import factory
+from webkitpy.layout_tests.port.webkit import WebKitPort
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+from webkitpy.thirdparty import simplejson
+
+STATE_NEEDS_REBASELINE = 'needs_rebaseline'
+STATE_REBASELINE_FAILED = 'rebaseline_failed'
+STATE_REBASELINE_SUCCEEDED = 'rebaseline_succeeded'
+
+class RebaselineHTTPServer(BaseHTTPServer.HTTPServer):
+ def __init__(self, httpd_port, test_config, results_json, platforms_json):
+ BaseHTTPServer.HTTPServer.__init__(self, ("", httpd_port), RebaselineHTTPRequestHandler)
+ self.test_config = test_config
+ self.results_json = results_json
+ self.platforms_json = platforms_json
+
+
+class RebaselineHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ 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 do_GET(self):
+ self._handle_request()
+
+ def do_POST(self):
+ self._handle_request()
+
+ def _handle_request(self):
+ # Parse input.
+ if "?" in self.path:
+ path, query_string = self.path.split("?", 1)
+ self.query = urlparse.parse_qs(query_string)
+ else:
+ path = self.path
+ self.query = {}
+ function_or_file_name = path[1:] or "index.html"
+
+ # See if a static file matches.
+ if function_or_file_name in RebaselineHTTPRequestHandler.STATIC_FILE_NAMES:
+ self._serve_static_file(function_or_file_name)
+ return
+
+ # See if a class method matches.
+ 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(
+ RebaselineHTTPRequestHandler.STATIC_FILE_DIRECTORY, static_path))
+
+ 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 quitquitquit(self):
+ self.send_response(200)
+ self.send_header("Content-type", "text/plain")
+ self.end_headers()
+ self.wfile.write("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 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)
+
+ def results_json(self):
+ self._serve_json(self.server.results_json)
+
+ def platforms_json(self):
+ self._serve_json(self.server.platforms_json)
+
+ def _serve_json(self, json):
+ self.send_response(200)
+ self.send_header('Content-type', 'application/json')
+ self.end_headers()
+ simplejson.dump(json, 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_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 = format_date_time(
+ time.mktime(expires_time.timetuple()))
+ self.send_header("Expires", expires_formatted)
+ self.end_headers()
+
+ shutil.copyfileobj(static_file, self.wfile)
+
+
+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
+
+
+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):
+ class AllPlatformsPort(WebKitPort):
+ def __init__(self):
+ WebKitPort.__init__(self, filesystem=test_config.filesystem)
+ 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)
+
+ all_platforms_port = AllPlatformsPort()
+
+ all_test_baselines = {}
+ for baseline_extension in ('.txt', '.checksum', '.png'):
+ test_baselines = test_config.test_port.expected_baselines(
+ test_path, baseline_extension)
+ baselines = all_platforms_port.expected_baselines(
+ test_path, 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 RebaselineServer(AbstractDeclarativeCommand):
+ name = "rebaseline-server"
+ help_text = __doc__
+ argument_names = "/path/to/results/directory"
+
+ def __init__(self):
+ options = [
+ make_option("--httpd-port", action="store", type="int", default=8127, help="Port to use for the the rebaseline HTTP server"),
+ ]
+ AbstractDeclarativeCommand.__init__(self, options=options)
+
+ def execute(self, options, args, tool):
+ results_directory = args[0]
+ filesystem = system.filesystem.FileSystem()
+ scm = self._tool.scm()
+
+ if options.dry_run:
+
+ def no_op_copyfile(src, dest):
+ pass
+
+ def no_op_add(path, return_exit_code=False):
+ if return_exit_code:
+ return 0
+
+ filesystem.copyfile = no_op_copyfile
+ scm.add = no_op_add
+
+ print 'Parsing unexpected_results.json...'
+ results_json_path = filesystem.join(
+ results_directory, 'unexpected_results.json')
+ with codecs.open(results_json_path, "r") as results_json_file:
+ results_json_file = file(results_json_path)
+ results_json = simplejson.load(results_json_file)
+
+ port = factory.get()
+ layout_tests_directory = port.layout_tests_dir()
+ platforms = filesystem.listdir(
+ filesystem.join(layout_tests_directory, 'platform'))
+ test_config = TestConfig(
+ port,
+ layout_tests_directory,
+ results_directory,
+ platforms,
+ filesystem,
+ scm)
+
+ print 'Gathering current baselines...'
+ for test_file, test_json in results_json['tests'].items():
+ test_json['state'] = STATE_NEEDS_REBASELINE
+ test_path = filesystem.join(layout_tests_directory, test_file)
+ test_json['baselines'] = _get_test_baselines(test_file, test_config)
+
+ server_url = "http://localhost:%d/" % options.httpd_port
+ print "Starting server at %s" % server_url
+ print ("Use the 'Exit' link in the UI, %squitquitquit "
+ "or Ctrl-C to stop") % server_url
+
+ threading.Timer(
+ .1, lambda: self._tool.user.open_url(server_url)).start()
+
+ httpd = RebaselineHTTPServer(
+ httpd_port=options.httpd_port,
+ test_config=test_config,
+ results_json=results_json,
+ platforms_json={
+ 'platforms': platforms,
+ 'defaultPlatform': port.name(),
+ })
+ httpd.serve_forever()
diff --git a/Tools/Scripts/webkitpy/tool/commands/rebaselineserver_unittest.py b/Tools/Scripts/webkitpy/tool/commands/rebaselineserver_unittest.py
new file mode 100644
index 0000000..f4371f4
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/rebaselineserver_unittest.py
@@ -0,0 +1,304 @@
+# 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 import filesystem_mock
+from webkitpy.layout_tests.port import base
+from webkitpy.layout_tests.port.webkit import WebKitPort
+from webkitpy.tool.commands import rebaselineserver
+from webkitpy.tool.mocktool import MockSCM
+
+
+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 _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=[]):
+ layout_tests_directory = base.Port().layout_tests_dir()
+ results_directory = '/WebKitBuild/Debug/layout-test-results'
+ mock_filesystem = filesystem_mock.MockFileSystem()
+ for file in test_files:
+ file_path = mock_filesystem.join(layout_tests_directory, file)
+ mock_filesystem.files[file_path] = ''
+ for file in result_files:
+ file_path = mock_filesystem.join(results_directory, file)
+ mock_filesystem.files[file_path] = ''
+
+ class TestMacPort(WebKitPort):
+ def __init__(self):
+ WebKitPort.__init__(self, filesystem=mock_filesystem)
+ self._name = 'mac'
+
+ return rebaselineserver.TestConfig(
+ TestMacPort(),
+ layout_tests_directory,
+ results_directory,
+ ('mac', 'mac-leopard', 'win', 'linux'),
+ mock_filesystem,
+ MockSCM())
diff --git a/Tools/Scripts/webkitpy/tool/commands/sheriffbot.py b/Tools/Scripts/webkitpy/tool/commands/sheriffbot.py
new file mode 100644
index 0000000..145f485
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/sheriffbot.py
@@ -0,0 +1,106 @@
+# Copyright (c) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+
+from webkitpy.common.system.deprecated_logging import log
+from webkitpy.common.config.ports import WebKitPort
+from webkitpy.tool.bot.sheriff import Sheriff
+from webkitpy.tool.bot.sheriffircbot import SheriffIRCBot
+from webkitpy.tool.commands.queues import AbstractQueue
+from webkitpy.tool.commands.stepsequence import StepSequenceErrorHandler
+
+
+class SheriffBot(AbstractQueue, StepSequenceErrorHandler):
+ name = "sheriff-bot"
+ watchers = AbstractQueue.watchers + [
+ "abarth@webkit.org",
+ "eric@webkit.org",
+ ]
+
+ def _update(self):
+ self.run_webkit_patch(["update", "--force-clean", "--quiet"])
+
+ # AbstractQueue methods
+
+ def begin_work_queue(self):
+ AbstractQueue.begin_work_queue(self)
+ self._sheriff = Sheriff(self._tool, self)
+ self._irc_bot = SheriffIRCBot(self._tool, self._sheriff)
+ self._tool.ensure_irc_connected(self._irc_bot.irc_delegate())
+
+ def work_item_log_path(self, 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()
+ self._update()
+
+ # FIXME: We need to figure out how to provoke_flaky_builders.
+
+ failure_map = self._tool.buildbot.failure_map()
+ failure_map.filter_out_old_failures(self._is_old_failure)
+ if failure_map.is_empty():
+ return None
+ return failure_map
+
+ 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):
+ failing_revisions = failure_map.failing_revisions()
+ for revision in failing_revisions:
+ builders = failure_map.builders_failing_for(revision)
+ tests = failure_map.tests_failing_for(revision)
+ try:
+ commit_info = self._tool.checkout().commit_info_for_revision(revision)
+ if not commit_info:
+ print "FAILED to fetch CommitInfo for r%s, likely missing ChangeLog" % revision
+ continue
+ self._sheriff.post_irc_warning(commit_info, builders)
+ self._sheriff.post_blame_comment_on_bug(commit_info, builders, tests)
+
+ finally:
+ for builder in builders:
+ self._tool.status_server.update_svn_revision(revision, builder.name())
+ 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 0000000..4db463e
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/sheriffbot_unittest.py
@@ -0,0 +1,57 @@
+# 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):
+ builder1 = MockBuilder("Builder1")
+ builder2 = MockBuilder("Builder2")
+
+ def test_sheriff_bot(self):
+ tool = MockTool()
+ mock_work_item = MockFailureMap(tool.buildbot)
+ expected_stderr = {
+ "begin_work_queue": self._default_begin_work_queue_stderr("sheriff-bot", tool.scm().checkout_root),
+ "next_work_item": "",
+ "process_work_item": """MOCK: irc.post: abarth, darin, eseidel: http://trac.webkit.org/changeset/29837 might have broken Builder1
+MOCK bug comment: bug_id=42, cc=['abarth@webkit.org', 'eric@webkit.org']
+--- Begin comment ---
+http://trac.webkit.org/changeset/29837 might have broken Builder1
+The following tests are not passing:
+mock-test-1
+--- End comment ---
+
+""",
+ "handle_unexpected_error": "Mock error message\n"
+ }
+ self.assert_queue_outputs(SheriffBot(), work_item=mock_work_item, expected_stderr=expected_stderr)
diff --git a/Tools/Scripts/webkitpy/tool/commands/stepsequence.py b/Tools/Scripts/webkitpy/tool/commands/stepsequence.py
new file mode 100644
index 0000000..be2ed4c
--- /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.
+
+import webkitpy.tool.steps as steps
+
+from webkitpy.common.system.executive import ScriptError
+from webkitpy.common.checkout.scm import CheckoutNeedsUpdate
+from webkitpy.tool.bot.queueengine import QueueEngine
+from webkitpy.common.system.deprecated_logging import log
+
+
+class StepSequenceErrorHandler():
+ @classmethod
+ def handle_script_error(cls, tool, patch, script_error):
+ raise NotImplementedError, "subclasses must implement"
+
+ @classmethod
+ def handle_checkout_needs_update(cls, tool, state, options, error):
+ raise NotImplementedError, "subclasses must implement"
+
+
+class StepSequence(object):
+ def __init__(self, steps):
+ self._steps = steps or []
+
+ def options(self):
+ collected_options = [
+ steps.Options.parent_command,
+ steps.Options.quiet,
+ ]
+ for step in self._steps:
+ collected_options = collected_options + step.options()
+ # Remove duplicates.
+ collected_options = sorted(set(collected_options))
+ return collected_options
+
+ def _run(self, tool, options, state):
+ for step in self._steps:
+ step(tool, options).run(state)
+
+ def run_and_handle_errors(self, tool, options, state=None):
+ if not state:
+ state = {}
+ try:
+ self._run(tool, options, state)
+ except CheckoutNeedsUpdate, e:
+ log("Commit failed because the checkout is out of date. Please update and try again.")
+ if options.parent_command:
+ command = tool.command_by_name(options.parent_command)
+ command.handle_checkout_needs_update(tool, state, options, e)
+ QueueEngine.exit_after_handled_error(e)
+ except ScriptError, e:
+ if not options.quiet:
+ log(e.message_with_output())
+ if options.parent_command:
+ command = tool.command_by_name(options.parent_command)
+ command.handle_script_error(tool, state, e)
+ QueueEngine.exit_after_handled_error(e)
diff --git a/Tools/Scripts/webkitpy/tool/commands/upload.py b/Tools/Scripts/webkitpy/tool/commands/upload.py
new file mode 100644
index 0000000..e12c8e2
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/upload.py
@@ -0,0 +1,483 @@
+#!/usr/bin/env python
+# Copyright (c) 2009, 2010 Google Inc. All rights reserved.
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import re
+import sys
+
+from optparse import make_option
+
+import webkitpy.tool.steps as steps
+
+from webkitpy.common.config.committers import CommitterList
+from webkitpy.common.net.bugzilla import parse_bug_id
+from webkitpy.common.system.user import User
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.abstractsequencedcommand import AbstractSequencedCommand
+from webkitpy.tool.grammar import pluralize, join_with_separators
+from webkitpy.tool.comments import bug_comment_from_svn_revision
+from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
+from webkitpy.common.system.deprecated_logging import error, log
+
+
+class CommitMessageForCurrentDiff(AbstractDeclarativeCommand):
+ name = "commit-message"
+ help_text = "Print a commit message suitable for the uncommitted changes"
+
+ def __init__(self):
+ options = [
+ steps.Options.git_commit,
+ ]
+ AbstractDeclarativeCommand.__init__(self, options=options)
+
+ def execute(self, options, args, tool):
+ # This command is a useful test to make sure commit_message_for_this_commit
+ # always returns the right value regardless of the current working directory.
+ print "%s" % tool.checkout().commit_message_for_this_commit(options.git_commit).message()
+
+
+class CleanPendingCommit(AbstractDeclarativeCommand):
+ name = "clean-pending-commit"
+ help_text = "Clear r+ on obsolete patches so they do not appear in the pending-commit list."
+
+ # NOTE: This was designed to be generic, but right now we're only processing patches from the pending-commit list, so only r+ matters.
+ def _flags_to_clear_on_patch(self, patch):
+ if not patch.is_obsolete():
+ return None
+ what_was_cleared = []
+ if patch.review() == "+":
+ if patch.reviewer():
+ what_was_cleared.append("%s's review+" % patch.reviewer().full_name)
+ else:
+ what_was_cleared.append("review+")
+ return join_with_separators(what_was_cleared)
+
+ def execute(self, options, args, tool):
+ committers = CommitterList()
+ for bug_id in tool.bugs.queries.fetch_bug_ids_from_pending_commit_list():
+ bug = self._tool.bugs.fetch_bug(bug_id)
+ patches = bug.patches(include_obsolete=True)
+ for patch in patches:
+ flags_to_clear = self._flags_to_clear_on_patch(patch)
+ if not flags_to_clear:
+ continue
+ message = "Cleared %s from obsolete attachment %s so that this bug does not appear in http://webkit.org/pending-commit." % (flags_to_clear, patch.id())
+ self._tool.bugs.obsolete_attachment(patch.id(), message)
+
+
+# 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-commit 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 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.CheckStyle,
+ steps.ConfirmDiff,
+ steps.ObsoletePatches,
+ steps.SuggestReviewers,
+ steps.PostDiff,
+ ]
+
+
+class LandSafely(AbstractPatchUploadingCommand):
+ name = "land-safely"
+ help_text = "Land the current diff via the commit-queue"
+ argument_names = "[BUGID]"
+ long_help = """land-safely updates the ChangeLog with the reviewer listed
+ in bugs.webkit.org for BUGID (or the bug ID detected from the ChangeLog).
+ The command then uploads the current diff to the bug and marks it for
+ commit by the commit-queue."""
+ show_in_main_help = True
+ steps = [
+ steps.UpdateChangeLogsWithReviewer,
+ steps.ObsoletePatches,
+ steps.PostDiffForCommit,
+ ]
+
+
+class Prepare(AbstractSequencedCommand):
+ name = "prepare"
+ help_text = "Creates a bug (or prompts for an existing bug) and prepares the ChangeLogs"
+ argument_names = "[BUGID]"
+ steps = [
+ steps.PromptForBugOrTitle,
+ steps.CreateBug,
+ steps.PrepareChangeLog,
+ ]
+
+ def _prepare_state(self, options, args, tool):
+ bug_id = args and args[0]
+ return { "bug_id" : bug_id }
+
+
+class Upload(AbstractPatchUploadingCommand):
+ name = "upload"
+ help_text = "Automates the process of uploading a patch for review"
+ argument_names = "[BUGID]"
+ show_in_main_help = True
+ steps = [
+ steps.CheckStyle,
+ steps.PromptForBugOrTitle,
+ steps.CreateBug,
+ steps.PrepareChangeLog,
+ steps.EditChangeLog,
+ steps.ConfirmDiff,
+ steps.ObsoletePatches,
+ steps.SuggestReviewers,
+ steps.PostDiff,
+ ]
+ long_help = """upload uploads the current diff to bugs.webkit.org.
+ If no bug id is provided, upload will create a bug.
+ If the current diff does not have a ChangeLog, upload
+ will prepare a ChangeLog. Once a patch is read, upload
+ will open the ChangeLogs for editing using the command in the
+ EDITOR environment variable and will display the diff using the
+ command in the PAGER environment variable."""
+
+ def _prepare_state(self, options, args, tool):
+ state = {}
+ state["bug_id"] = self._bug_id(options, args, tool, state)
+ return state
+
+
+class EditChangeLogs(AbstractSequencedCommand):
+ name = "edit-changelogs"
+ help_text = "Opens modified ChangeLogs in $EDITOR"
+ show_in_main_help = True
+ steps = [
+ steps.EditChangeLog,
+ ]
+
+
+class PostCommits(AbstractDeclarativeCommand):
+ name = "post-commits"
+ help_text = "Attach a range of local commits to bugs as patch files"
+ argument_names = "COMMITISH"
+
+ def __init__(self):
+ options = [
+ make_option("-b", "--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
+ make_option("--add-log-as-comment", action="store_true", dest="add_log_as_comment", default=False, help="Add commit log message as a comment when uploading the patch."),
+ make_option("-m", "--description", action="store", type="string", dest="description", help="Description string for the attachment (default: description from commit message)"),
+ steps.Options.obsolete_patches,
+ steps.Options.review,
+ steps.Options.request_commit,
+ ]
+ AbstractDeclarativeCommand.__init__(self, options=options, requires_local_commits=True)
+
+ def _comment_text_for_commit(self, options, commit_message, tool, commit_id):
+ comment_text = None
+ if (options.add_log_as_comment):
+ comment_text = commit_message.body(lstrip=True)
+ comment_text += "---\n"
+ comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
+ return comment_text
+
+ def execute(self, options, args, tool):
+ commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
+ if len(commit_ids) > 10: # We could lower this limit, 10 is too many for one bug as-is.
+ error("webkit-patch does not support attaching %s at once. Are you sure you passed the right commit range?" % (pluralize("patch", len(commit_ids))))
+
+ have_obsoleted_patches = set()
+ for commit_id in commit_ids:
+ commit_message = tool.scm().commit_message_for_local_commit(commit_id)
+
+ # Prefer --bug-id=, then a bug url in the commit message, then a bug url in the entire commit diff (i.e. ChangeLogs).
+ bug_id = options.bug_id or parse_bug_id(commit_message.message()) or parse_bug_id(tool.scm().create_patch(git_commit=commit_id))
+ if not bug_id:
+ log("Skipping %s: No bug id found in commit or specified with --bug-id." % commit_id)
+ continue
+
+ if options.obsolete_patches and bug_id not in have_obsoleted_patches:
+ state = { "bug_id": bug_id }
+ steps.ObsoletePatches(tool, options).run(state)
+ have_obsoleted_patches.add(bug_id)
+
+ diff = tool.scm().create_patch(git_commit=commit_id)
+ description = options.description or commit_message.description(lstrip=True, strip_url=True)
+ comment_text = self._comment_text_for_commit(options, commit_message, tool, commit_id)
+ tool.bugs.add_patch_to_bug(bug_id, diff, description, comment_text, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
+
+
+# FIXME: This command needs to be brought into the modern age with steps and CommitInfo.
+class MarkBugFixed(AbstractDeclarativeCommand):
+ name = "mark-bug-fixed"
+ help_text = "Mark the specified bug as fixed"
+ argument_names = "[SVN_REVISION]"
+ def __init__(self):
+ options = [
+ make_option("--bug-id", action="store", type="string", dest="bug_id", help="Specify bug id if no URL is provided in the commit log."),
+ make_option("--comment", action="store", type="string", dest="comment", help="Text to include in bug comment."),
+ make_option("--open", action="store_true", default=False, dest="open_bug", help="Open bug in default web browser (Mac only)."),
+ make_option("--update-only", action="store_true", default=False, dest="update_only", help="Add comment to the bug, but do not close it."),
+ ]
+ AbstractDeclarativeCommand.__init__(self, options=options)
+
+ # FIXME: We should be using checkout().changelog_entries_for_revision(...) instead here.
+ def _fetch_commit_log(self, tool, svn_revision):
+ if not svn_revision:
+ return tool.scm().last_svn_commit_log()
+ return tool.scm().svn_commit_log(svn_revision)
+
+ def _determine_bug_id_and_svn_revision(self, tool, bug_id, svn_revision):
+ commit_log = self._fetch_commit_log(tool, svn_revision)
+
+ if not bug_id:
+ bug_id = parse_bug_id(commit_log)
+
+ if not svn_revision:
+ match = re.search("^r(?P<svn_revision>\d+) \|", commit_log, re.MULTILINE)
+ if match:
+ svn_revision = match.group('svn_revision')
+
+ if not bug_id or not svn_revision:
+ not_found = []
+ if not bug_id:
+ not_found.append("bug id")
+ if not svn_revision:
+ not_found.append("svn revision")
+ error("Could not find %s on command-line or in %s."
+ % (" or ".join(not_found), "r%s" % svn_revision if svn_revision else "last commit"))
+
+ return (bug_id, svn_revision)
+
+ def execute(self, options, args, tool):
+ bug_id = options.bug_id
+
+ svn_revision = args and args[0]
+ if svn_revision:
+ if re.match("^r[0-9]+$", svn_revision, re.IGNORECASE):
+ svn_revision = svn_revision[1:]
+ if not re.match("^[0-9]+$", svn_revision):
+ error("Invalid svn revision: '%s'" % svn_revision)
+
+ needs_prompt = False
+ if not bug_id or not svn_revision:
+ needs_prompt = True
+ (bug_id, svn_revision) = self._determine_bug_id_and_svn_revision(tool, bug_id, svn_revision)
+
+ log("Bug: <%s> %s" % (tool.bugs.bug_url_for_bug_id(bug_id), tool.bugs.fetch_bug_dictionary(bug_id)["title"]))
+ log("Revision: %s" % svn_revision)
+
+ if options.open_bug:
+ tool.user.open_url(tool.bugs.bug_url_for_bug_id(bug_id))
+
+ if needs_prompt:
+ if not tool.user.confirm("Is this correct?"):
+ exit(1)
+
+ bug_comment = bug_comment_from_svn_revision(svn_revision)
+ if options.comment:
+ bug_comment = "%s\n\n%s" % (options.comment, bug_comment)
+
+ if options.update_only:
+ log("Adding comment to Bug %s." % bug_id)
+ tool.bugs.post_comment_to_bug(bug_id, bug_comment)
+ else:
+ log("Adding comment to Bug %s and marking as Resolved/Fixed." % bug_id)
+ tool.bugs.close_bug_as_fixed(bug_id, bug_comment)
+
+
+# FIXME: Requires unit test. Blocking issue: too complex for now.
+class CreateBug(AbstractDeclarativeCommand):
+ name = "create-bug"
+ help_text = "Create a bug from local changes or local commits"
+ argument_names = "[COMMITISH]"
+
+ def __init__(self):
+ options = [
+ steps.Options.cc,
+ steps.Options.component,
+ make_option("--no-prompt", action="store_false", dest="prompt", default=True, help="Do not prompt for bug title and comment; use commit log instead."),
+ make_option("--no-review", action="store_false", dest="review", default=True, help="Do not mark the patch for review."),
+ make_option("--request-commit", action="store_true", dest="request_commit", default=False, help="Mark the patch as needing auto-commit after review."),
+ ]
+ AbstractDeclarativeCommand.__init__(self, options=options)
+
+ def create_bug_from_commit(self, options, args, tool):
+ commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
+ if len(commit_ids) > 3:
+ error("Are you sure you want to create one bug with %s patches?" % len(commit_ids))
+
+ commit_id = commit_ids[0]
+
+ bug_title = ""
+ comment_text = ""
+ if options.prompt:
+ (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
+ else:
+ commit_message = tool.scm().commit_message_for_local_commit(commit_id)
+ bug_title = commit_message.description(lstrip=True, strip_url=True)
+ comment_text = commit_message.body(lstrip=True)
+ comment_text += "---\n"
+ comment_text += tool.scm().files_changed_summary_for_commit(commit_id)
+
+ diff = tool.scm().create_patch(git_commit=commit_id)
+ bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
+
+ if bug_id and len(commit_ids) > 1:
+ options.bug_id = bug_id
+ options.obsolete_patches = False
+ # FIXME: We should pass through --no-comment switch as well.
+ PostCommits.execute(self, options, commit_ids[1:], tool)
+
+ def create_bug_from_patch(self, options, args, tool):
+ bug_title = ""
+ comment_text = ""
+ if options.prompt:
+ (bug_title, comment_text) = self.prompt_for_bug_title_and_comment()
+ else:
+ commit_message = tool.checkout().commit_message_for_this_commit(options.git_commit)
+ bug_title = commit_message.description(lstrip=True, strip_url=True)
+ comment_text = commit_message.body(lstrip=True)
+
+ diff = tool.scm().create_patch(options.git_commit)
+ bug_id = tool.bugs.create_bug(bug_title, comment_text, options.component, diff, "Patch", cc=options.cc, mark_for_review=options.review, mark_for_commit_queue=options.request_commit)
+
+ def prompt_for_bug_title_and_comment(self):
+ bug_title = User.prompt("Bug title: ")
+ print "Bug comment (hit ^D on blank line to end):"
+ lines = sys.stdin.readlines()
+ try:
+ sys.stdin.seek(0, os.SEEK_END)
+ except IOError:
+ # Cygwin raises an Illegal Seek (errno 29) exception when the above
+ # seek() call is made. Ignoring it seems to cause no harm.
+ # FIXME: Figure out a way to get avoid the exception in the first
+ # place.
+ pass
+ comment_text = "".join(lines)
+ return (bug_title, comment_text)
+
+ def execute(self, options, args, tool):
+ if len(args):
+ if (not tool.scm().supports_local_commits()):
+ error("Extra arguments not supported; patch is taken from working directory.")
+ self.create_bug_from_commit(options, args, tool)
+ else:
+ self.create_bug_from_patch(options, args, tool)
diff --git a/Tools/Scripts/webkitpy/tool/commands/upload_unittest.py b/Tools/Scripts/webkitpy/tool/commands/upload_unittest.py
new file mode 100644
index 0000000..a347b00
--- /dev/null
+++ b/Tools/Scripts/webkitpy/tool/commands/upload_unittest.py
@@ -0,0 +1,122 @@
+# Copyright (C) 2009 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from webkitpy.thirdparty.mock import Mock
+from webkitpy.tool.commands.commandtest import CommandsTest
+from webkitpy.tool.commands.upload import *
+from webkitpy.tool.mocktool import MockOptions, MockTool
+
+class UploadCommandsTest(CommandsTest):
+ def test_commit_message_for_current_diff(self):
+ tool = MockTool()
+ expected_stdout = "This is a fake commit message that is at least 50 characters.\n"
+ self.assert_execute_outputs(CommitMessageForCurrentDiff(), [], expected_stdout=expected_stdout, tool=tool)
+
+ def test_clean_pending_commit(self):
+ self.assert_execute_outputs(CleanPendingCommit(), [])
+
+ def test_assign_to_committer(self):
+ tool = MockTool()
+ expected_stderr = "Warning, attachment 128 on bug 42 has invalid committer (non-committer@example.com)\nBug 77 is already assigned to foo@foo.com (None).\nBug 76 has no non-obsolete patches, ignoring.\n"
+ self.assert_execute_outputs(AssignToCommitter(), [], expected_stderr=expected_stderr, tool=tool)
+ tool.bugs.reassign_bug.assert_called_with(42, "eric@webkit.org", "Attachment 128 was posted by a committer and has review+, assigning to Eric Seidel for commit.")
+
+ def test_obsolete_attachments(self):
+ expected_stderr = "Obsoleting 2 old patches on bug 42\n"
+ self.assert_execute_outputs(ObsoleteAttachments(), [42], expected_stderr=expected_stderr)
+
+ def test_post(self):
+ options = MockOptions()
+ options.cc = None
+ options.check_style = True
+ options.comment = None
+ options.description = "MOCK description"
+ options.request_commit = False
+ options.review = True
+ options.suggest_reviewers = False
+ expected_stderr = """Running check-webkit-style
+MOCK: user.open_url: file://...
+Obsoleting 2 old patches on bug 42
+MOCK add_patch_to_bug: bug_id=42, description=MOCK description, mark_for_review=True, mark_for_commit_queue=False, mark_for_landing=False
+MOCK: user.open_url: http://example.com/42
+"""
+ expected_stdout = "Was that diff correct?\n"
+ self.assert_execute_outputs(Post(), [42], options=options, expected_stdout=expected_stdout, expected_stderr=expected_stderr)
+
+ def test_land_safely(self):
+ expected_stderr = "Obsoleting 2 old patches on bug 42\nMOCK add_patch_to_bug: bug_id=42, description=Patch for landing, mark_for_review=False, mark_for_commit_queue=False, mark_for_landing=True\n"
+ self.assert_execute_outputs(LandSafely(), [42], expected_stderr=expected_stderr)
+
+ def test_prepare_diff_with_arg(self):
+ self.assert_execute_outputs(Prepare(), [42])
+
+ def test_prepare(self):
+ expected_stderr = "MOCK create_bug\nbug_title: Mock user response\nbug_description: Mock user response\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.comment = None
+ options.description = "MOCK description"
+ options.request_commit = False
+ options.review = True
+ options.suggest_reviewers = False
+ expected_stderr = """Running check-webkit-style
+MOCK: user.open_url: file://...
+Obsoleting 2 old patches on bug 42
+MOCK add_patch_to_bug: bug_id=42, description=MOCK description, mark_for_review=True, mark_for_commit_queue=False, mark_for_landing=False
+MOCK: user.open_url: http://example.com/42
+"""
+ expected_stdout = "Was that diff correct?\n"
+ self.assert_execute_outputs(Upload(), [42], options=options, expected_stdout=expected_stdout, expected_stderr=expected_stderr)
+
+ def test_mark_bug_fixed(self):
+ tool = MockTool()
+ tool._scm.last_svn_commit_log = lambda: "r9876 |"
+ options = Mock()
+ options.bug_id = 42
+ options.comment = "MOCK comment"
+ expected_stderr = """Bug: <http://example.com/42> Bug with two r+'d and cq+'d patches, one of which has an invalid commit-queue setter.
+Revision: 9876
+MOCK: user.open_url: http://example.com/42
+Adding comment to Bug 42.
+MOCK bug comment: bug_id=42, cc=None
+--- Begin comment ---
+MOCK comment
+
+Committed r9876: <http://trac.webkit.org/changeset/9876>
+--- End comment ---
+
+"""
+ expected_stdout = "Is this correct?\n"
+ self.assert_execute_outputs(MarkBugFixed(), [], expected_stdout=expected_stdout, expected_stderr=expected_stderr, tool=tool, options=options)
+
+ def test_edit_changelog(self):
+ self.assert_execute_outputs(EditChangeLogs(), [])