New flakiness dashboard should support showing the failing tests per builder
[WebKit-https.git] / Websites / test-results / index.html
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();
 });