summaryrefslogtreecommitdiffstats
path: root/Tools/RebaselineQueueServer
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/RebaselineQueueServer')
-rwxr-xr-xTools/RebaselineQueueServer/app.yaml11
-rw-r--r--Tools/RebaselineQueueServer/handlers/__init__.py1
-rw-r--r--Tools/RebaselineQueueServer/handlers/builderqueue.py95
-rw-r--r--Tools/RebaselineQueueServer/handlers/pages.py47
-rwxr-xr-xTools/RebaselineQueueServer/index.yaml11
-rwxr-xr-xTools/RebaselineQueueServer/main.py56
-rw-r--r--Tools/RebaselineQueueServer/model/__init__.py1
-rw-r--r--Tools/RebaselineQueueServer/model/queueentry.py63
-rw-r--r--Tools/RebaselineQueueServer/static/builder-frame-empty.html10
-rw-r--r--Tools/RebaselineQueueServer/static/styles.css71
-rw-r--r--Tools/RebaselineQueueServer/templates/builder-picker.html74
-rw-r--r--Tools/RebaselineQueueServer/templates/builder-queue-edit.html176
-rw-r--r--Tools/RebaselineQueueServer/templates/builder-queue-list.html23
-rw-r--r--Tools/RebaselineQueueServer/templates/home.html28
14 files changed, 667 insertions, 0 deletions
diff --git a/Tools/RebaselineQueueServer/app.yaml b/Tools/RebaselineQueueServer/app.yaml
new file mode 100755
index 0000000..c425cfd
--- /dev/null
+++ b/Tools/RebaselineQueueServer/app.yaml
@@ -0,0 +1,11 @@
+application: rebaseline-queue
+version: 1
+runtime: python
+api_version: 1
+
+handlers:
+- url: /static
+ static_dir: static
+
+- url: .*
+ script: main.py
diff --git a/Tools/RebaselineQueueServer/handlers/__init__.py b/Tools/RebaselineQueueServer/handlers/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/RebaselineQueueServer/handlers/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/RebaselineQueueServer/handlers/builderqueue.py b/Tools/RebaselineQueueServer/handlers/builderqueue.py
new file mode 100644
index 0000000..c84e07b
--- /dev/null
+++ b/Tools/RebaselineQueueServer/handlers/builderqueue.py
@@ -0,0 +1,95 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from urllib import unquote_plus
+
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp import template
+from django.utils import simplejson
+
+from model.queueentry import QueueEntry
+
+
+class QueueHandler(webapp.RequestHandler):
+ def get(self, builder_name):
+ self._get(unquote_plus(builder_name))
+
+ def post(self, builder_name):
+ self._post(unquote_plus(builder_name))
+
+ def _queued_test_names(self, builder_name):
+ return [entry.test for entry in QueueEntry.entries_for_builder(builder_name)]
+
+ def _queue_list_url(self, builder_name):
+ return '/builder/%s/queue' % builder_name
+
+
+class QueueEdit(QueueHandler):
+ def _get(self, builder_name):
+ test_names = self._queued_test_names(builder_name)
+ self.response.out.write(
+ template.render("templates/builder-queue-edit.html", {
+ 'builder_name': builder_name,
+ 'queued_test_names': simplejson.dumps(test_names),
+ }))
+
+
+class QueueAdd(QueueHandler):
+ def _post(self, builder_name):
+ current_tests = set(self._queued_test_names(builder_name))
+ tests = set(self.request.get_all('test')).difference(current_tests)
+
+ for test in tests:
+ QueueEntry.add(builder_name, test)
+
+ self.redirect(self._queue_list_url(builder_name))
+
+
+class QueueRemove(QueueHandler):
+ def _post(self, builder_name):
+ tests = self.request.get_all('test')
+
+ for test in tests:
+ QueueEntry.remove(builder_name, test)
+
+ self.redirect(self._queue_list_url(builder_name))
+
+
+class QueueHtml(QueueHandler):
+ def _get(self, builder_name):
+ self.response.out.write(
+ template.render("templates/builder-queue-list.html", {
+ 'builder_name': builder_name,
+ 'entries': QueueEntry.entries_for_builder(builder_name),
+ }))
+
+
+class QueueJson(QueueHandler):
+ def _get(self, builder_name):
+ queue_json = {'tests': self._queued_test_names(builder_name)}
+ self.response.out.write(simplejson.dumps(queue_json))
diff --git a/Tools/RebaselineQueueServer/handlers/pages.py b/Tools/RebaselineQueueServer/handlers/pages.py
new file mode 100644
index 0000000..8fcf2e3
--- /dev/null
+++ b/Tools/RebaselineQueueServer/handlers/pages.py
@@ -0,0 +1,47 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp import template
+
+from model.queueentry import QueueEntry
+
+
+class Home(webapp.RequestHandler):
+ def get(self):
+ builder_names = QueueEntry.builder_names()
+ self.response.out.write(
+ template.render("templates/home.html", {
+ 'builder_names': builder_names,
+ }))
+
+
+class BuilderPicker(webapp.RequestHandler):
+ def get(self):
+ self.response.out.write(
+ template.render("templates/builder-picker.html", {}))
diff --git a/Tools/RebaselineQueueServer/index.yaml b/Tools/RebaselineQueueServer/index.yaml
new file mode 100755
index 0000000..a3b9e05
--- /dev/null
+++ b/Tools/RebaselineQueueServer/index.yaml
@@ -0,0 +1,11 @@
+indexes:
+
+# AUTOGENERATED
+
+# This index.yaml is automatically updated whenever the dev_appserver
+# detects that a new type of query is run. If you want to manage the
+# index.yaml file manually, remove the above marker line (the line
+# saying "# AUTOGENERATED"). If you want to manage some indexes
+# manually, move them above the marker line. The index.yaml file is
+# automatically uploaded to the admin console when you next deploy
+# your application using appcfg.py.
diff --git a/Tools/RebaselineQueueServer/main.py b/Tools/RebaselineQueueServer/main.py
new file mode 100755
index 0000000..4497d63
--- /dev/null
+++ b/Tools/RebaselineQueueServer/main.py
@@ -0,0 +1,56 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Request a modern Django
+from google.appengine.dist import use_library
+use_library('django', '1.2')
+
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp import util
+
+from handlers import pages
+from handlers import builderqueue
+
+
+def main():
+ application = webapp.WSGIApplication([
+ ('/', pages.Home),
+ ('/builder/picker', pages.BuilderPicker),
+
+ # Queue CRUD operations
+ ('/builder/(.+)/queue/edit', builderqueue.QueueEdit),
+ ('/builder/(.+)/queue/add', builderqueue.QueueAdd),
+ ('/builder/(.+)/queue/remove', builderqueue.QueueRemove),
+ ('/builder/(.+)/queue', builderqueue.QueueHtml),
+ ('/builder/(.+)/queue/json', builderqueue.QueueJson),
+ ],
+ debug=True)
+ util.run_wsgi_app(application)
+
+if __name__ == '__main__':
+ main()
diff --git a/Tools/RebaselineQueueServer/model/__init__.py b/Tools/RebaselineQueueServer/model/__init__.py
new file mode 100644
index 0000000..ef65bee
--- /dev/null
+++ b/Tools/RebaselineQueueServer/model/__init__.py
@@ -0,0 +1 @@
+# Required for Python to search this directory for module files
diff --git a/Tools/RebaselineQueueServer/model/queueentry.py b/Tools/RebaselineQueueServer/model/queueentry.py
new file mode 100644
index 0000000..6570fc0
--- /dev/null
+++ b/Tools/RebaselineQueueServer/model/queueentry.py
@@ -0,0 +1,63 @@
+# Copyright (C) 2011 Google Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from google.appengine.ext import db
+
+
+class QueueEntry(db.Model):
+ test = db.StringProperty()
+ builder = db.StringProperty()
+
+ @staticmethod
+ def add(builder_name, test):
+ entry = QueueEntry()
+ entry.builder = builder_name
+ entry.test = test
+ entry.put()
+ return entry
+
+ @staticmethod
+ def remove(builder_name, test):
+ query = QueueEntry.all()
+ query = query.filter('builder =', builder_name).filter('test =', test)
+ for entry in query:
+ entry.delete()
+
+ @staticmethod
+ def entries_for_builder(builder_name):
+ query = QueueEntry.all()
+ query = query.filter('builder =', builder_name)
+ return query
+
+ @staticmethod
+ def builder_names():
+ query = QueueEntry.all()
+ builder_names = set()
+ for entry in query:
+ builder_names.add(entry.builder)
+ return builder_names
diff --git a/Tools/RebaselineQueueServer/static/builder-frame-empty.html b/Tools/RebaselineQueueServer/static/builder-frame-empty.html
new file mode 100644
index 0000000..31b91bb
--- /dev/null
+++ b/Tools/RebaselineQueueServer/static/builder-frame-empty.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Rebaseline Queue</title>
+ <link rel="stylesheet" href="/static/styles.css" type="text/css">
+</head>
+<body>
+Select a group and then a builder to see tests that are currently failing on it (if any).
+</body>
+</html>
diff --git a/Tools/RebaselineQueueServer/static/styles.css b/Tools/RebaselineQueueServer/static/styles.css
new file mode 100644
index 0000000..a36ff35
--- /dev/null
+++ b/Tools/RebaselineQueueServer/static/styles.css
@@ -0,0 +1,71 @@
+body {
+ font-family: Helvetica, Arial, sans-serif;
+ font-size: 12px;
+}
+
+h1 {
+ border-bottom: solid 1px #ccc;
+}
+
+#builder-picker body,
+#add-form body {
+ margin: 0;
+}
+
+#builder-picker,
+#builder-picker body {
+ height: 100%;
+}
+
+#builder-picker body {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+}
+
+#builder-picker-controls {
+ padding: 0.5em;
+ border-bottom: solid 1px black;
+}
+
+#builder-picker-controls select {
+ min-width: 10em;
+}
+
+#builder-frame {
+ border: 0;
+ -webkit-box-flex: 1;
+ display: block;
+}
+
+.status {
+ font-size: 16px;
+ text-align: center;
+ padding: 1em;
+}
+
+.test-table {
+ border-collapse: collapse;
+}
+
+.test-table caption {
+ font-size: 16px;
+ font-weight: bold;
+ background: #eee;
+ padding: .5em;
+}
+
+.test-table th {
+ text-align: left;
+ border-bottom: solid 1px #ccc;
+ background: #eee;
+ min-width: 8em;
+}
+
+.test-table tbody tr:hover {
+ background: #ffa;
+}
+
+.test-table .submit-row {
+ text-align: right;
+ padding: 1em 0;
+}
diff --git a/Tools/RebaselineQueueServer/templates/builder-picker.html b/Tools/RebaselineQueueServer/templates/builder-picker.html
new file mode 100644
index 0000000..1068c04
--- /dev/null
+++ b/Tools/RebaselineQueueServer/templates/builder-picker.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html id="builder-picker">
+<head>
+ <title>Rebaseline Queue: Builders</title>
+ <script src="http://test-results.appspot.com/dashboards/builders.js"></script>
+ <link rel="stylesheet" href="/static/styles.css" type="text/css">
+</head>
+<body>
+
+<div id="builder-picker-controls">
+ <label for="builder-group">Group:</label>
+ <select id="builder-group">
+ <option disabled></option>
+ </select>
+
+ <label for="builder">Builder:</label>
+ <select id="builder">
+ <option disabled></option>
+ </select>
+</div>
+
+<iframe src="/static/builder-frame-empty.html" id="builder-frame"></iframe>
+
+<script>
+function init()
+{
+ var builderGroupMenu = document.getElementById('builder-group');
+ builderGroupMenu.addEventListener(
+ 'change', handleBuilderGroupSelected, false);
+
+ var builderMenu = document.getElementById('builder');
+ builderMenu.addEventListener(
+ 'change', handleBuilderSelected, false);
+
+ for (var builderGroupName in LAYOUT_TESTS_BUILDER_GROUPS) {
+ var builderGroupOption = document.createElement('option');
+ builderGroupOption.textContent = builderGroupOption.value =
+ builderGroupName;
+ builderGroupMenu.appendChild(builderGroupOption);
+ }
+}
+
+function handleBuilderGroupSelected()
+{
+ var builderGroupMenu = document.getElementById('builder-group');
+ var builderGroupName =
+ builderGroupMenu.options[builderGroupMenu.selectedIndex].value;
+ var builderGroup = LAYOUT_TESTS_BUILDER_GROUPS[builderGroupName];
+
+ var builderMenu = document.getElementById('builder');
+ while (builderMenu.options[1]) {
+ builderMenu.removeChild(builderMenu.options[1]);
+ }
+
+ for (var builderName in builderGroup.builders) {
+ var builderOption = document.createElement('option');
+ builderOption.textContent = builderOption.value = builderName;
+ builderMenu.appendChild(builderOption);
+ }
+}
+
+function handleBuilderSelected()
+{
+ var builderMenu = document.getElementById('builder');
+ var builderName = builderMenu.options[builderMenu.selectedIndex].value;
+ document.getElementById('builder-frame').src =
+ '/builder/' + builderName + '/queue/edit';
+}
+
+init();
+</script>
+
+</body>
+</html>
diff --git a/Tools/RebaselineQueueServer/templates/builder-queue-edit.html b/Tools/RebaselineQueueServer/templates/builder-queue-edit.html
new file mode 100644
index 0000000..21a0f02
--- /dev/null
+++ b/Tools/RebaselineQueueServer/templates/builder-queue-edit.html
@@ -0,0 +1,176 @@
+<!DOCTYPE html>
+<html id="add-form">
+<head>
+ <title>Rebaseline Queue: Edit</title>
+ <script src="http://test-results.appspot.com/dashboards/builders.js"></script>
+ <link rel="stylesheet" href="/static/styles.css" type="text/css">
+</head>
+<body">
+
+<div id="loading-indicator" class="status">Loading...</div>
+
+<form method="POST" id="form-template" style="display: none">
+ <table class="test-table">
+ <caption></caption>
+ <thead>
+ <th>Test</th>
+ <th>Expected</th>
+ <th>Actual</th>
+ <th>Results</th>
+ </thead>
+ <tbody></tbody>
+ <tbody>
+ <tr>
+ <td colspan="4" class="submit-row">
+ <input type="submit" value="">
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</form>
+
+<script>
+var TEST_RESULTS_SERVER = 'http://test-results.appspot.com/';
+var BUILDER_TO_GROUP = {};
+for (var builderGroupName in LAYOUT_TESTS_BUILDER_GROUPS) {
+ for (var builderName in LAYOUT_TESTS_BUILDER_GROUPS[builderGroupName]) {
+ BUILDER_TO_GROUP[builderName] = builderGroupName;
+ }
+}
+
+// Extract template parameters
+var builderName = '{{ builder_name|escapejs }}';
+var queuedTestNames = {{ queued_test_names|safe }};
+
+function init()
+{
+ var builderMaster = BUILDER_TO_MASTER[builderName];
+ var resultsUrl = TEST_RESULTS_SERVER + 'testfile?builder=' + builderName +
+ '&master=' + builderMaster.name +
+ '&testtype=layout-tests&name=full_results.json';
+
+ var script = document.createElement('script');
+ script.src = resultsUrl;
+ document.getElementsByTagName('head')[0].appendChild(script);
+}
+
+function ADD_RESULTS(results)
+{
+ var builderGroupName = BUILDER_TO_GROUP[builderName];
+
+ var tests = results.tests;
+ var failingTests = [];
+ var queuedTests = [];
+ for (var test in tests) {
+ var testResults = tests[test];
+ if (testResults.actual == testResults.expected ||
+ testResults.expected.split(' ').indexOf(testResults.actual) != -1 ||
+ testResults.actual == 'SKIP' ||
+ testResults.actual.indexOf('PASS') != -1 ||
+ (testResults.actual != 'PASS' && testResults.expected.indexOf('FAIL') != -1)) {
+ continue;
+ }
+
+ testResults.name = test;
+
+ if (queuedTestNames.indexOf(test) != -1) {
+ queuedTests.push(testResults);
+ queuedTestNames.splice(queuedTestNames.indexOf(test), 1);
+ } else {
+ failingTests.push(testResults);
+ }
+ }
+
+ // If we have remaining queued tests that are currently not failing,
+ // synthesize results for them.
+ queuedTestNames.forEach(function(queuedTestName) {
+ queuedTests.push({
+ name: queuedTestName,
+ actual: 'UNKNOWN',
+ expected: 'UNKNOWN'
+ });
+ });
+
+ document.getElementById('loading-indicator').style.display = 'none';
+
+ renderTestResults(
+ failingTests,
+ 'add',
+ 'Failing tests',
+ 'Add to rebaseline queue',
+ 'No failing tests.');
+ renderTestResults(
+ queuedTests,
+ 'remove',
+ 'Queued tests',
+ 'Remove from rebaseline queue',
+ 'No queued tests.');
+}
+
+function renderTestResults(testResults, formAction, title, submitLabel, emptyMessage)
+{
+ if (testResults.length == 0) {
+ var emptyNode = document.createElement('div');
+ emptyNode.className = 'status';
+ emptyNode.textContent = emptyMessage;
+ document.body.appendChild(emptyNode);
+ return;
+ }
+
+ var form = document.getElementById('form-template').cloneNode(true);
+ form.action = '/builder/' + builderName + '/queue/' + formAction;
+ form.style.display = '';
+ document.body.appendChild(form);
+
+ var testsTable = form.querySelector('.test-table');
+ testsTable.querySelector('caption').textContent = title;
+ testsTable.querySelector('input[type=submit]').value = submitLabel;
+
+ testResults.sort(function(a, b) {
+ return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0);
+ });
+
+ testResults.forEach(function(result) {
+ var testRow = document.createElement('tr');
+
+ var testCell = document.createElement('td');
+ testRow.appendChild(testCell);
+ var testCheckbox = document.createElement('input');
+ testCheckbox.type = 'checkbox';
+ testCheckbox.name = 'test';
+ testCheckbox.value = result.name;
+ testCheckbox.id = result.name;
+ testCell.appendChild(testCheckbox);
+
+ var testName = document.createElement('label');
+ testName.textContent = result.name;
+ testName.setAttribute('for', result.name);
+ testCell.appendChild(testName);
+
+ var expectedCell = document.createElement('td');
+ testRow.appendChild(expectedCell);
+ expectedCell.textContent = result.expected;
+
+ var actualCell = document.createElement('td');
+ testRow.appendChild(actualCell);
+ actualCell.textContent = result.actual;
+
+ var resultsCell = document.createElement('td');
+ testRow.appendChild(resultsCell);
+ var resultsLink = document.createElement('a');
+ resultsLink.target = '_blank';
+ resultsLink.href = TEST_RESULTS_SERVER +
+ 'dashboards/flakiness_dashboard.html#tests=' + result.name +
+ '&group=' + builderGroupName;
+ resultsLink.textContent = 'Flakiness dashboard';
+ resultsCell.appendChild(resultsLink);
+
+ testsTable.tBodies[0].appendChild(testRow);
+ });
+}
+
+init();
+</script>
+
+</body>
+</html>
diff --git a/Tools/RebaselineQueueServer/templates/builder-queue-list.html b/Tools/RebaselineQueueServer/templates/builder-queue-list.html
new file mode 100644
index 0000000..79fa02a
--- /dev/null
+++ b/Tools/RebaselineQueueServer/templates/builder-queue-list.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>{{ builder_name|escape }} Queue</title>
+ <link rel="stylesheet" href="/static/styles.css" type="text/css">
+</head>
+<body>
+
+<h1>Queue: {{ builder_name|escape }}</h1>
+
+<ol>
+{% for entry in entries %}
+ <li>
+ {{ entry.test|escape }}
+ </li>
+{% empty %}
+ No tests found in queue.
+{% endfor %}
+</ol>
+
+<a href="/builder/{{ builder_name|escape }}/queue/edit">Edit queue</a>
+</body>
+</html>
diff --git a/Tools/RebaselineQueueServer/templates/home.html b/Tools/RebaselineQueueServer/templates/home.html
new file mode 100644
index 0000000..c6a16ff
--- /dev/null
+++ b/Tools/RebaselineQueueServer/templates/home.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Rebaseline Queue</title>
+ <link rel="stylesheet" href="/static/styles.css" type="text/css">
+</head>
+<body>
+
+<h1>Rebaseline Queue</h1>
+
+<ul>
+ <li><a href="/builder/picker">Browse and enqueue failing tests on builders</a></li>
+ <li>
+ Builders with enqueued tests:
+ <ul>
+{% for builder_name in builder_names %}
+ <li>
+ <a href="/builder/{{ builder_name|escape }}/queue">{{ builder_name|escape }}</a>
+ </li>
+{% empty %}
+ None
+{% endfor %}
+ </ul>
+ </li>
+</ul>
+
+</body>
+</html>