New flakiness dashboard should support showing the failing tests per builder
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 19 Oct 2013 01:47:40 +0000 (01:47 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 19 Oct 2013 01:47:40 +0000 (01:47 +0000)
https://bugs.webkit.org/show_bug.cgi?id=123011

Reviewed by Timothy Hatcher.

Added the feature. Also did some refactoring to add this feature.

* ChangeLog: Added.
* api/failing-tests.php: Added.
* api/manifest.php: Removed the code to make maps by id. The work is now done in index.html.
* api/results.php:
* common.css: Added. Extracted from index.html.
* include/test-results.php: Extracted parse_revisions_array and format_result_rows from results.php.
* index.html:
* main.css: Added.
(TestResultsView.setAvailableTests): Added.
(TestResultsView.showTooltip): Fixed the code to compute x and y coordinates of the tooltip to take
scrolled positions into account.
(TestResultsView._createTestResultRow): Extracted from _populateTestPane.
(TestResultsView.fetchTest): Added the code to show "Loading..." in the pane while loading the JSON.
(TestResultsView.fetchTests): Respect the doNotUpdateHash flag.
(TestResultsView._populateBuilderPane): Added.
(TestResultsView.fetchFailingTestsForBuilder): Added.
(TestResultsView.updateLocationHash): Serialize builder & builderDays.
(TestResultsView.locationHashChanged): Don't delete existing test panes since that's now done in
loadTestsFromLocationHash.
(TestResultsView.loadTestsFromLocationHash): Take care of both 'tests' and 'builder' components.
(fetchManifest): Setup the UI to select a builder.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@157659 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Websites/test-results/ChangeLog [new file with mode: 0644]
Websites/test-results/api/failing-tests.php [new file with mode: 0644]
Websites/test-results/api/manifest.php
Websites/test-results/api/results.php
Websites/test-results/common.css [new file with mode: 0644]
Websites/test-results/include/test-results.php
Websites/test-results/index.html
Websites/test-results/main.css [new file with mode: 0644]

diff --git a/Websites/test-results/ChangeLog b/Websites/test-results/ChangeLog
new file mode 100644 (file)
index 0000000..8172a02
--- /dev/null
@@ -0,0 +1,31 @@
+2013-10-18  Ryosuke Niwa  <rniwa@webkit.org>
+
+        New flakiness dashboard should support showing the failing tests per builder
+        https://bugs.webkit.org/show_bug.cgi?id=123011
+
+        Reviewed by Timothy Hatcher.
+
+        Added the feature. Also did some refactoring to add this feature.
+
+        * ChangeLog: Added.
+        * api/failing-tests.php: Added.
+        * api/manifest.php: Removed the code to make maps by id. The work is now done in index.html.
+        * api/results.php:
+        * common.css: Added. Extracted from index.html.
+        * include/test-results.php: Extracted parse_revisions_array and format_result_rows from results.php.
+        * index.html:
+        * main.css: Added.
+        (TestResultsView.setAvailableTests): Added.
+        (TestResultsView.showTooltip): Fixed the code to compute x and y coordinates of the tooltip to take
+        scrolled positions into account.
+        (TestResultsView._createTestResultRow): Extracted from _populateTestPane.
+        (TestResultsView.fetchTest): Added the code to show "Loading..." in the pane while loading the JSON.
+        (TestResultsView.fetchTests): Respect the doNotUpdateHash flag.
+        (TestResultsView._populateBuilderPane): Added.
+        (TestResultsView.fetchFailingTestsForBuilder): Added.
+        (TestResultsView.updateLocationHash): Serialize builder & builderDays.
+        (TestResultsView.locationHashChanged): Don't delete existing test panes since that's now done in
+        loadTestsFromLocationHash.
+        (TestResultsView.loadTestsFromLocationHash): Take care of both 'tests' and 'builder' components.
+        (fetchManifest): Setup the UI to select a builder.
+
diff --git a/Websites/test-results/api/failing-tests.php b/Websites/test-results/api/failing-tests.php
new file mode 100644 (file)
index 0000000..8cf2b5b
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+require_once('../include/json-shared.php');
+require_once('../include/test-results.php');
+
+$db = connect();
+
+require_existence_of($_GET, array('builder' => '/^[A-Za-z0-9 \(\)\-_]+$/'));
+$builder_name = $_GET['builder'];
+$number_of_days = array_get($_GET, 'days');
+if ($number_of_days) {
+    require_format('number_of_days', $number_of_days, '/^[0-9]+$/');
+    $number_of_days = intval($number_of_days);
+} else
+    $number_of_days = 3;
+
+$builder_row = $db->select_first_row('builders', NULL, array('name' => $builder_name));
+if (!$builder_row)
+    exit_with_error('BuilderNotFound');
+$builder_id = $builder_row['id'];
+
+$result_rows = $db->query_and_fetch_all(
+'SELECT results.*, builds.*, array_agg((build_revisions.repository, build_revisions.value, build_revisions.time)) AS revisions
+    FROM results, builds, build_revisions
+    WHERE build_revisions.build = builds.id AND results.build = builds.id AND builds.builder = $1
+    AND results.actual != $2 AND builds.start_time > now() - interval \'' . $number_of_days . ' days\'
+    GROUP BY results.id, builds.id', array($builder_id, 'PASS'));
+if (!$result_rows)
+    exit_with_error('ResultsNotFound');
+
+exit_with_success(format_result_rows($result_rows));
+
+?>
index 672b78a..19cf6f9 100644 (file)
@@ -3,25 +3,10 @@
 require_once('../include/json-shared.php');
 
 $db = connect();
-$tests = $db->fetch_table('tests');
 
-function fetch_table_and_create_map_by_id($table_name) {
-    global $db;
-
-    $rows = $db->fetch_table($table_name);
-    if (!$rows)
-        return array();
-
-    $results = array();
-    foreach ($rows as $row) {
-        $results[$row['id']] = $row;
-    }
-    return $results;
-}
-
-exit_with_success(array('tests' => $tests,
-    'builders' => fetch_table_and_create_map_by_id('builders'),
-    'slaves' => fetch_table_and_create_map_by_id('slaves'),
-    'repositories' => fetch_table_and_create_map_by_id('repositories')));
+exit_with_success(array('tests' => $db->fetch_table('tests'),
+    'builders' => $db->fetch_table('builders'),
+    'slaves' => $db->fetch_table('slaves'),
+    'repositories' => $db->fetch_table('repositories')));
 
 ?>
index 0dd6a27..1883ea0 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 require_once('../include/json-shared.php');
+require_once('../include/test-results.php');
 
 $db = connect();
 
@@ -13,36 +14,11 @@ if (!$test)
 $result_rows = $db->query_and_fetch_all(
 'SELECT results.*, builds.*, array_agg((build_revisions.repository, build_revisions.value, build_revisions.time)) AS revisions
     FROM results, builds, build_revisions
-    WHERE build_revisions.build = builds.id AND results.test = $1 AND results.build = builds.id
+    WHERE build_revisions.build = builds.id AND results.build = builds.id AND results.test = $1
     GROUP BY results.id, builds.id', array($test['id']));
 if (!$result_rows)
     exit_with_error('ResultsNotFound');
 
-date_default_timezone_set('UTC');
-function parse_revisions_array($postgres_array) {
-    // e.g. {"(WebKit,131456,\"2012-10-16 14:53:00\")","(Safari,162004,)"}
-    $outer_array = json_decode('[' . trim($postgres_array, '{}') . ']');
-    $revisions = array();
-    foreach ($outer_array as $item) {
-        $name_and_revision = explode(',', trim($item, '()'));
-        $time = strtotime(trim($name_and_revision[2], '"')) * 1000;
-        $revisions[trim($name_and_revision[0], '"')] = array(trim($name_and_revision[1], '"'), $time);
-    }
-    return $revisions;
-}
-
-$builders = array();
-foreach ($result_rows as $result) {
-    array_push(array_ensure_item_has_array($builders, $result['builder']),
-        array('buildTime' => strtotime($result['start_time']) * 1000,
-        'revisions' => parse_revisions_array($result['revisions']),
-        'builder' => $result['builder'],
-        'slave' => $result['slave'],
-        'buildNumber' => $result['number'],
-        'actual' => $result['actual'],
-        'expected' => $result['expected']));
-}
-
-exit_with_success(array('builders' => $builders));
+exit_with_success(format_result_rows($result_rows));
 
 ?>
diff --git a/Websites/test-results/common.css b/Websites/test-results/common.css
new file mode 100644 (file)
index 0000000..dcb6a24
--- /dev/null
@@ -0,0 +1,65 @@
+
+html, body {
+    margin: 0;
+    padding: 0;
+}
+
+body {
+    background-repeat: repeat-x;
+    font-family: sans-serif;
+    padding: 10px;
+}
+
+#title {
+    background-image: linear-gradient(bottom, rgb(240,240,240) 31%, rgb(255,255,255) 90%);
+    background-image: -o-linear-gradient(bottom, rgb(240,240,240) 31%, rgb(255,255,255) 90%);
+    background-image: -moz-linear-gradient(bottom, rgb(240,240,240) 31%, rgb(255,255,255) 90%);
+    background-image: -webkit-linear-gradient(bottom, rgb(240,240,240) 31%, rgb(255,255,255) 90%);
+    background-image: -ms-linear-gradient(bottom, rgb(240,240,240) 31%, rgb(255,255,255) 90%);
+    -moz-box-shadow:    1px 1px 3px 1px #ccc;
+    -webkit-box-shadow: 1px 1px 3px 1px #ccc;
+    box-shadow:         1px 1px 3px 1px #ccc;
+    padding: 5px 10px;
+    margin: 0 0 20px 0;
+    border-radius: 5px;
+    position: relative;
+}
+
+#title h1 {
+    font-weight: normal;
+    text-shadow: #bbb 1px 1px 2px;
+    margin: 0;
+    padding: 0;
+    font-size: 2em;
+}
+#title li, #title ul {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+}
+
+#title li {
+    display: inline;
+}
+
+#title li:after {
+    content: ' | ';
+}
+
+#title li:last-child:after {
+    content: '';
+}
+
+#title ul {
+    position: absolute;
+    vertical-align: middle;
+    top: 5px;
+    right: 20px;
+    padding-top: 0.6em;
+}
+
+a {
+    text-decoration: none;
+    color: #000;
+    text-shadow: #bbb 1px 1px 2px;
+}
index d77e05f..af92be1 100644 (file)
@@ -66,4 +66,31 @@ function recursively_add_test_results($db, $build_id, $tests, $full_name) {
         'expected' => $tests['expected'], 'actual' => $tests['actual']));
 }
 
+date_default_timezone_set('UTC');
+function parse_revisions_array($postgres_array) {
+    // e.g. {"(WebKit,131456,\"2012-10-16 14:53:00\")","(Safari,162004,)"}
+    $outer_array = json_decode('[' . trim($postgres_array, '{}') . ']');
+    $revisions = array();
+    foreach ($outer_array as $item) {
+        $name_and_revision = explode(',', trim($item, '()'));
+        $time = strtotime(trim($name_and_revision[2], '"')) * 1000;
+        $revisions[trim($name_and_revision[0], '"')] = array(trim($name_and_revision[1], '"'), $time);
+    }
+    return $revisions;
+}
+
+function format_result_rows($result_rows) {
+    $builders = array();
+    foreach ($result_rows as $result) {
+        array_push(array_ensure_item_has_array(array_ensure_item_has_array($builders, $result['builder']), $result['test']),
+            array('buildTime' => strtotime($result['start_time']) * 1000,
+            'revisions' => parse_revisions_array($result['revisions']),
+            'slave' => $result['slave'],
+            'buildNumber' => $result['number'],
+            'actual' => $result['actual'],
+            'expected' => $result['expected']));
+    }
+    return array('builders' => $builders);
+}
+
 ?>
index 367375c..01f1ab6 100644 (file)
@@ -2,7 +2,8 @@
 <html>
 <head>
 <title>WebKit Test Results</title>
-<link rel="stylesheet" href="/common.css">
+<link rel="stylesheet" href="common.css">
+<link rel="stylesheet" href="main.css">
 <script src="js/autocompleter.js"></script>
 <script src="js/build.js"></script>
 <script src="js/dom.js"></script>
 </ul>
 </header>
 
-<form id="navigationBar" onsubmit="TestResultsView.fetchTest(this['testName'].value);
+<form onsubmit="TestResultsView.fetchTest(this['testName'].value);
     TestResultsView.updateLocationHash();
     return false;">
 <input id="testName" type="text" size="150" onpaste="pasteHelper(this, event)"
     placeholder="Type in a test name, or copy and paste test names on results.html or NRWT stdout (including junks)"></form>
+<div id="testView"></div>
 
-<div id="container"></div>
-<div id="tooltipContainer"></div>
-
-<style>
-
-#testName {
-    width: 99%;
-    font-size: 1em;
-    outline: none;
-    border: 1px solid #ccc;
-    border-radius: 5px;
-    padding: 5px;
-}
-
-.testResults {
-    border: 1px solid #ccc;
-    border-radius: 5px;
-    padding: 5px;
-    margin: 10px 0px;
-    position: relative;
-}
-
-.closeButton {
-    position: absolute;
-    right: 5px;
-    top: 5px;
-    width: 1em;
-    height: 1em;
-    stroke: #999;
-}
-
-.resultsTable {
-    font-size: small;
-    border-collapse: collapse;
-    border: 2px solid #fff;
-    padding: 0;
-    margin: 0;
-}
-
-.resultsTable caption {
-    font-size: large;
-    font-weight: normal;
-    text-align: left;
-    margin-bottom: 0.3em;
-    white-space: pre;
-}
-
-.resultsTable td,
-.resultsTable th {
-    border: 2px solid #fff;
-    padding: 0;
-    margin: 0;
-}
-
-.resultsTable th,
-.resultsTable .passingRate {
-    font-weight: normal;
-    padding-right: 10px;
-}
-
-.resultsTable th {
-    width: 15em;
-}
-
-.resultsTable .passingRate {
-    width: 3em;
-}
-
-.resultsTable .resultCell {
-    display: inline-block;
-    padding: 0.2em 0.2em;
-}
-
-.resultsTable a {
-    display: block;
-    width: 1em;
-    height: 1.5em;
-    border-radius: 3px;
-}
-
-.resultsTable .PASS a {
-    background-color: #0c3;
-}
-
-.resultsTable .TEXT a {
-    background-color: #c33;
-}
-
-.resultsTable .IMAGE a {
-    background-color: #3cf;
-}
-
-.resultsTable .TEXT.PASS a {
-    background-color: #cf3;
-}
-
-.resultsTable .CRASH a {
-    background-color: #f00;
-}
-
-.candidateWindow {
-    z-index: 999;
-    position: absolute;
-    background: white;
-    color: black;
-    border: 1px solid #ccc;
-    border-radius: 5px;
-    margin: 5px 0 0 0;
-    padding: 5px;
-    font-size: 1em;
-    list-style: none;
-}
-
-.candidateWindow em {
-    background-color: #ccc;
-    font-style: normal;
-}
-
-.candidateWindow .selected {
-    background-color: #0cf;
-    color: white;
-}
-
-#tooltipContainer {
-    position: absolute;
-}
-
-.tooltip {
-    position: relative;
-    border-radius: 5px;
-    padding: 5px;
-    opacity: 0.9;
-    background: #333;
-    color: #eee;
-    font-size: small;
-    line-height: 130%;
-}
-
-.tooltip:after {
-    position: absolute;
-    width: 0;
-    height: 0;
-    left: 50%;
-    margin-left: -9px;
-    bottom: -19px;
-    content: "";
-    display: block;
-    border-style: solid;
-    border-width: 10px;
-    border-color: #333 transparent transparent transparent;
-}
+<form>
+<label>Show failing tests for</label>
+<select id="builderListView"><option value="">Select builder</option></select>
+<select id="builderDaysView"><option>5</option><option>15</option><option>30</option></select>
+</form>
+<div id="builderFailingTestsView"></div>
 
-.tooltip ul,
-.tooltip li {
-    padding: 0;
-    margin: 0;
-    list-style: none;
-}
-
-.tooltip a {
-    color: white;
-    text-shadow: none;
-    text-decoration: underline;
-}
+<div id="tooltipContainer"></div>
 
-</style>
 <script>
 
 var TestResultsView = new (function () {
@@ -205,12 +50,17 @@ var TestResultsView = new (function () {
 
     this._tooltipContainer = tooltipContainer;
     this._tests = [];
+    this._currentBuilder = null;
     this._oldHash = null;
     this._builders = [];
     this._slaves = [];
     this._repositories = [];
 });
 
+TestResultsView.setAvailableTests = function (availableTests) {
+    this._availableTests = availableTests;
+}
+
 TestResultsView.setBuilders = function (builders) {
     this._builders = builders;
 }
@@ -231,9 +81,15 @@ TestResultsView.showTooltip = function (anchor, contentElement) {
         tooltipContainer.removeChild(tooltipContainer.firstChild);
     tooltipContainer.appendChild(contentElement);
 
-    var rect = anchor.getBoundingClientRect();
-    tooltipContainer.style.left = (rect.left - contentElement.offsetWidth / 2 + anchor.offsetWidth / 2) + 'px';
-    tooltipContainer.style.top = (rect.top - contentElement.clientHeight - 5) + 'px';
+    var position = {x: 0, y: 0};
+    var currentNode = anchor;
+    while (currentNode) {
+        position.x += currentNode.offsetLeft;
+        position.y += currentNode.offsetTop;
+        currentNode = currentNode.offsetParent;
+    }
+    tooltipContainer.style.left = (position.x - contentElement.offsetWidth / 2 + anchor.offsetWidth / 2) + 'px';
+    tooltipContainer.style.top = (position.y - contentElement.clientHeight - 5) + 'px';
 }
 
 TestResultsView._urlFromBuilder = function (urlType, master, builder, revision, build) {
@@ -282,30 +138,38 @@ TestResultsView._createResultCell = function (master, builder, result, previousR
     return cell;
 }
 
-TestResultsView._populatePane = function(testName, results, section) {
+TestResultsView._populateTestPane = function(testName, results, section) {
     var table = element('table', {'class': 'resultsTable'}, [element('caption', [testName])]);
     var resultsByBuilder = results['builders'];
     for (var builderId in resultsByBuilder) {
-        var results = resultsByBuilder[builderId];
-        for (var i = 0; i < results.length; i++) {
-            results[i].build = new TestBuild(this._repositories, this._builders, results[i]);
-        }
+        var resultsByTest = resultsByBuilder[builderId];
+        var results;
+        for (var testId in resultsByTest)
+            results = resultsByTest[testId];
+        if (!results)
+            continue;
 
-        var sortedResults = results.sort(function (result1, result2) { return result1.build.time() - result2.build.time(); });
-        var cells = new Array(sortedResults.length);
         var builder = this._builders[builderId];
-        for (var i = 0; i < sortedResults.length; i++)
-            cells[i] = this._createResultCell(builder.master, builder.name, sortedResults[i], sortedResults[i - 1]);
-
-        var passCount = cells.filter(function (cell) { return cell.className == 'PASS'; }).length;
-        var passingRate = Math.round(passCount / cells.length * 100) + '%';
-        table.appendChild(element('tr', [element('th', [builder.name]), element('td', {'class': 'passingRate'}, [passingRate]),
-            element('td', cells)]));
         // FIXME: Add a master name if there is more than one.
+        table.appendChild(this._createTestResultRow(builder.name, results, builder));
     }
     section.appendChild(table);
 }
 
+TestResultsView._createTestResultRow = function (title, results, builder) {
+    for (var i = 0; i < results.length; i++)
+        results[i].build = new TestBuild(this._repositories, this._builders, results[i]);
+
+    var sortedResults = results.sort(function (result1, result2) { return result1.build.time() - result2.build.time(); });
+    var cells = new Array(sortedResults.length);
+    for (var i = 0; i < sortedResults.length; i++)
+        cells[i] = this._createResultCell(builder.master, builder.name, sortedResults[i], sortedResults[i - 1]);
+
+    var passCount = cells.filter(function (cell) { return cell.className == 'PASS'; }).length;
+    var passingRate = Math.round(passCount / cells.length * 100) + '%';
+    return element('tr', [element('th', ['' + title]), element('td', {'class': 'passingRate'}, [passingRate]), element('td', cells)]);
+}
+
 TestResultsView.fetchTest = function (testName) {
     if (this._tests.indexOf(testName) >= 0)
         return;
@@ -321,20 +185,20 @@ TestResultsView.fetchTest = function (testName) {
             section.parentNode.removeChild(section);
             event.preventDefault();
         });
-    var section = element('section', {'id': testName, 'class': 'testResults'}, [closeButton]);
+    var section = element('section', {'id': testName, 'class': 'testResults'}, [closeButton, 'Loading...']);
 
-    document.getElementById('container').appendChild(section);
+    document.getElementById('testView').appendChild(section);
 
     var xhr = new XMLHttpRequest();
     xhr.open("GET", 'api/results.php?test=' + testName, true);  
     xhr.onload = function(event) {
         var response = JSON.parse(xhr.response);
+        section.removeChild(section.lastChild); // Remove Loading...
         if (response['status'] != 'OK') {
             section.appendChild(text('Failed to load results for ' + testName + ': ' + response['status']));
             return;
         }
-
-        self._populatePane(testName, response, section);
+        self._populateTestPane(testName, response, section);
     }
     xhr.send();
     this._tests.push(testName);
@@ -351,16 +215,67 @@ TestResultsView._removeTest = function (testName) {
 TestResultsView.fetchTests = function (testNames, doNotUpdateHash) {
     for (var i = 0; i < testNames.length; i++)
         this.fetchTest(testNames[i], doNotUpdateHash);
-    this.updateLocationHash();
+    if (!doNotUpdateHash)
+        this.updateLocationHash();
+}
+
+TestResultsView._populateBuilderPane = function(builderName, results, section) {
+    var table = element('table', {'class': 'resultsTable'}, [element('caption', [builderName])]);
+    var resultsByBuilder = results['builders'];
+    var resultsByTests;
+    var builderId;
+    for (var currentBuilderId in resultsByBuilder) {
+        builderId = currentBuilderId;
+        resultsByTests = resultsByBuilder[builderId];
+    }
+    if (!resultsByTests)
+        return;
+
+    var builder = this._builders[builderId];
+    for (var testId in resultsByTests)
+        table.appendChild(this._createTestResultRow(this._availableTests[testId].name, resultsByTests[testId], builder));
+    section.appendChild(table);
+}
+
+TestResultsView.fetchFailingTestsForBuilder = function (builderName, numberOfDays, doNotUpdateHash) {
+    var section = element('section', {'class': 'testResults'}, ['Loading...']);
+
+    var container = document.getElementById('builderFailingTestsView');
+    container.innerHTML = '';
+    container.appendChild(section);
+
+    var self = this;
+    var xhr = new XMLHttpRequest();
+    xhr.open("GET", 'api/failing-tests.php?builder=' + escape(builderName) + '&' + 'days=' + numberOfDays, true);  
+    xhr.onload = function(event) {
+        var response = JSON.parse(xhr.response);
+        section.innerHTML = '';
+        if (response['status'] != 'OK') {
+            section.appendChild(text('Failed to load results for ' + builderName + ': ' + response['status']));
+            return;
+        }
+        self._currentBuilder = builderName;
+        self._currentBuilderDays = numberOfDays;
+        self._populateBuilderPane(builderName, response, section);
+        if (!doNotUpdateHash)
+            self.updateLocationHash();
+    }
+    xhr.send();
 }
 
 TestResultsView.updateLocationHash = function () {
     var params = {
-        'tests': this._tests.join(',')
+        'tests': this._tests.join(','),
+        'builder': this._currentBuilder,
+        'builderDays': this._currentBuilderDays,
     };
     var hash = '';
-    for (var key in params)
-        hash += decodeURIComponent(key) + '=' + decodeURIComponent(params[key]);
+    for (var key in params) {
+        var value = params[key];
+        if (value === null || value === undefined)
+            continue;
+        hash += '&' + decodeURIComponent(key) + '=' + decodeURIComponent(value);
+    }
     location.hash = hash;
     this._oldHash = location.hash;
 }
@@ -371,7 +286,6 @@ TestResultsView.locationHashChanged = function () {
         return;
     this._oldHash = newHash;
     this._tests = [];
-    document.getElementById('container').innerHTML = '';
     this.loadTestsFromLocationHash();
 }
 
@@ -384,10 +298,13 @@ TestResultsView.loadTestsFromLocationHash = function () {
         var name = decodeURIComponent(component.substr(0, equalPosition));
         parsed[name] = decodeURIComponent(component.substr(equalPosition + 1));
     });
-    if (!parsed['tests'])
-        return;
     var doNotUpdateHash = true;
-    this.fetchTests(parsed['tests'].split(','), doNotUpdateHash);
+    if (parsed['tests']) {
+        document.getElementById('testView').innerHTML = '';
+        this.fetchTests(parsed['tests'].split(','), doNotUpdateHash);
+    }
+    if (parsed['builder'])
+        this.fetchFailingTestsForBuilder(parsed['builder'], parsed['builderDays'] || 5, doNotUpdateHash);
 }
 
 function fetchManifest(callback) {
@@ -410,9 +327,31 @@ fetchManifest(function (response) {
     var input = document.getElementById('testName');
     input.autocompleter = new Autocompleter(input, testNames);
 
-    TestResultsView.setBuilders(response['builders']);
-    TestResultsView.setSlaves(response['slaves']);
-    TestResultsView.setRepositories(response['repositories']);
+    var builderListView = document.getElementById('builderListView');
+    for (var builderId in response['builders'])
+        builderListView.appendChild(element('option', [text(response['builders'][builderId].name)]));
+
+    var builderDaysView = document.getElementById('builderDaysView');
+
+    function updateBuilderView() {
+        if (builderListView.value)
+            TestResultsView.fetchFailingTestsForBuilder(builderListView.value, builderDaysView.value);
+    }
+
+    builderListView.addEventListener('change', updateBuilderView);
+    builderDaysView.addEventListener('change', updateBuilderView);
+
+    function mapById(items) {
+        var results = {};
+        for (var i = 0; i < items.length; i++)
+            results[items[i].id] = items[i];
+        return results;
+    }
+
+    TestResultsView.setAvailableTests(mapById(response['tests']));
+    TestResultsView.setBuilders(mapById(response['builders']));
+    TestResultsView.setSlaves(mapById(response['slaves']));
+    TestResultsView.setRepositories(mapById(response['repositories']));
     TestResultsView.loadTestsFromLocationHash();
 });
 
diff --git a/Websites/test-results/main.css b/Websites/test-results/main.css
new file mode 100644 (file)
index 0000000..3ff1663
--- /dev/null
@@ -0,0 +1,163 @@
+#testName {
+    width: 99%;
+    font-size: 1em;
+    outline: none;
+    border: 1px solid #ccc;
+    border-radius: 5px;
+    padding: 5px;
+}
+
+.actionBar {
+
+}
+
+.testResults {
+    border: 1px solid #ccc;
+    border-radius: 5px;
+    padding: 5px;
+    margin: 10px 0px;
+    position: relative;
+}
+
+.closeButton {
+    position: absolute;
+    right: 5px;
+    top: 5px;
+    width: 1em;
+    height: 1em;
+    stroke: #999;
+}
+
+.resultsTable {
+    font-size: small;
+    border-collapse: collapse;
+    border: 2px solid #fff;
+    padding: 0;
+    margin: 0;
+}
+
+.resultsTable caption {
+    font-size: large;
+    font-weight: normal;
+    text-align: left;
+    margin-bottom: 0.3em;
+    white-space: pre;
+}
+
+.resultsTable td,
+.resultsTable th {
+    border: 2px solid #fff;
+    padding: 0;
+    margin: 0;
+}
+
+.resultsTable th,
+.resultsTable .passingRate {
+    font-weight: normal;
+    padding-right: 10px;
+}
+
+.resultsTable th {
+    width: 15em;
+}
+
+.resultsTable .passingRate {
+    width: 3em;
+}
+
+.resultsTable .resultCell {
+    display: inline-block;
+    padding: 0.2em 0.2em;
+}
+
+.resultsTable a {
+    display: block;
+    width: 1em;
+    height: 1.5em;
+    border-radius: 3px;
+}
+
+.resultsTable .PASS a {
+    background-color: #0c3;
+}
+
+.resultsTable .TEXT a {
+    background-color: #c33;
+}
+
+.resultsTable .IMAGE a {
+    background-color: #3cf;
+}
+
+.resultsTable .TEXT.PASS a {
+    background-color: #cf3;
+}
+
+.resultsTable .CRASH a {
+    background-color: #f00;
+}
+
+.candidateWindow {
+    z-index: 999;
+    position: absolute;
+    background: white;
+    color: black;
+    border: 1px solid #ccc;
+    border-radius: 5px;
+    margin: 5px 0 0 0;
+    padding: 5px;
+    font-size: 1em;
+    list-style: none;
+}
+
+.candidateWindow em {
+    background-color: #ccc;
+    font-style: normal;
+}
+
+.candidateWindow .selected {
+    background-color: #0cf;
+    color: white;
+}
+
+#tooltipContainer {
+    position: absolute;
+}
+
+.tooltip {
+    position: relative;
+    border-radius: 5px;
+    padding: 5px;
+    opacity: 0.9;
+    background: #333;
+    color: #eee;
+    font-size: small;
+    line-height: 130%;
+}
+
+.tooltip:after {
+    position: absolute;
+    width: 0;
+    height: 0;
+    left: 50%;
+    margin-left: -9px;
+    bottom: -19px;
+    content: "";
+    display: block;
+    border-style: solid;
+    border-width: 10px;
+    border-color: #333 transparent transparent transparent;
+}
+
+.tooltip ul,
+.tooltip li {
+    padding: 0;
+    margin: 0;
+    list-style: none;
+}
+
+.tooltip a {
+    color: white;
+    text-shadow: none;
+    text-decoration: underline;
+}