Add a new flakiness dashboard clone
[WebKit-https.git] / Websites / test-results / index.html
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <title>WebKit Test Results</title>
5 <link rel="stylesheet" href="/common.css">
6 <script src="js/autocompleter.js"></script>
7 <script src="js/build.js"></script>
8 <script src="js/dom.js"></script>
9 </head>
10 <body>
11
12 <header id="title">
13 <h1><a href="/">WebKit Test Results</a></h1>
14 <ul>
15     <li><a href="http://build.webkit.org/waterfall">Waterfall</a></li>
16 </ul>
17 </header>
18
19 <form id="navigationBar" onsubmit="TestResultsView.fetchTest(this['testName'].value);
20     TestResultsView.updateLocationHash();
21     return false;">
22 <input id="testName" type="text" size="150" onpaste="pasteHelper(this, event)"
23     placeholder="Type in a test name, or copy and paste test names on results.html or NRWT stdout (including junks)"></form>
24
25 <div id="container"></div>
26 <div id="tooltipContainer"></div>
27
28 <style>
29
30 #testName {
31     width: 99%;
32     font-size: 1em;
33     outline: none;
34     border: 1px solid #ccc;
35     border-radius: 5px;
36     padding: 5px;
37 }
38
39 .testResults {
40     border: 1px solid #ccc;
41     border-radius: 5px;
42     padding: 5px;
43     margin: 10px 0px;
44     position: relative;
45 }
46
47 .closeButton {
48     position: absolute;
49     right: 5px;
50     top: 5px;
51     width: 1em;
52     height: 1em;
53     stroke: #999;
54 }
55
56 .resultsTable {
57     font-size: small;
58     border-collapse: collapse;
59     border: 2px solid #fff;
60     padding: 0;
61     margin: 0;
62 }
63
64 .resultsTable caption {
65     font-size: large;
66     font-weight: normal;
67     text-align: left;
68     margin-bottom: 0.3em;
69     white-space: pre;
70 }
71
72 .resultsTable td,
73 .resultsTable th {
74     border: 2px solid #fff;
75     padding: 0;
76     margin: 0;
77 }
78
79 .resultsTable th,
80 .resultsTable .passingRate {
81     font-weight: normal;
82     padding-right: 10px;
83 }
84
85 .resultsTable th {
86     width: 15em;
87 }
88
89 .resultsTable .passingRate {
90     width: 3em;
91 }
92
93 .resultsTable .resultCell {
94     display: inline-block;
95     padding: 0.2em 0.2em;
96 }
97
98 .resultsTable a {
99     display: block;
100     width: 1em;
101     height: 1.5em;
102     border-radius: 3px;
103 }
104
105 .resultsTable .PASS a {
106     background-color: #0c3;
107 }
108
109 .resultsTable .TEXT a {
110     background-color: #c33;
111 }
112
113 .resultsTable .IMAGE a {
114     background-color: #3cf;
115 }
116
117 .resultsTable .TEXT.PASS a {
118     background-color: #cf3;
119 }
120
121 .resultsTable .CRASH a {
122     background-color: #f00;
123 }
124
125 .candidateWindow {
126     z-index: 999;
127     position: absolute;
128     background: white;
129     color: black;
130     border: 1px solid #ccc;
131     border-radius: 5px;
132     margin: 5px 0 0 0;
133     padding: 5px;
134     font-size: 1em;
135     list-style: none;
136 }
137
138 .candidateWindow em {
139     background-color: #ccc;
140     font-style: normal;
141 }
142
143 .candidateWindow .selected {
144     background-color: #0cf;
145     color: white;
146 }
147
148 #tooltipContainer {
149     position: absolute;
150 }
151
152 .tooltip {
153     position: relative;
154     border-radius: 5px;
155     padding: 5px;
156     opacity: 0.9;
157     background: #333;
158     color: #eee;
159     font-size: small;
160     line-height: 130%;
161 }
162
163 .tooltip:after {
164     position: absolute;
165     width: 0;
166     height: 0;
167     left: 50%;
168     margin-left: -9px;
169     bottom: -19px;
170     content: "";
171     display: block;
172     border-style: solid;
173     border-width: 10px;
174     border-color: #333 transparent transparent transparent;
175 }
176
177 .tooltip ul,
178 .tooltip li {
179     padding: 0;
180     margin: 0;
181     list-style: none;
182 }
183
184 .tooltip a {
185     color: white;
186     text-shadow: none;
187     text-decoration: underline;
188 }
189
190 </style>
191 <script>
192
193 var TestResultsView = new (function () {
194     var tooltipContainer = document.getElementById('tooltipContainer');
195
196     tooltipContainer.onclick = function (event) { event.insideTooltip = true; }
197     document.addEventListener('click', function (event) {
198         if (!event.insideTooltip)
199             tooltipContainer.style.display = 'none';
200     });
201
202     window.addEventListener('hashchange', function (event) {
203         TestResultsView.locationHashChanged();
204     });
205
206     this._tooltipContainer = tooltipContainer;
207     this._tests = [];
208     this._oldHash = null;
209     this._builders = [];
210     this._slaves = [];
211     this._repositories = [];
212 });
213
214 TestResultsView.setBuilders = function (builders) {
215     this._builders = builders;
216 }
217
218 TestResultsView.setSlaves = function (slaves) {
219     this._slaves = slaves;
220 }
221
222 TestResultsView.setRepositories = function (repositories) {
223     this._repositories = repositories;
224 }
225
226 TestResultsView.showTooltip = function (anchor, contentElement) {
227     var tooltipContainer = this._tooltipContainer;
228     tooltipContainer.style.display = null;
229
230     while (tooltipContainer.firstChild)
231         tooltipContainer.removeChild(tooltipContainer.firstChild);
232     tooltipContainer.appendChild(contentElement);
233
234     var rect = anchor.getBoundingClientRect();
235     tooltipContainer.style.left = (rect.left - contentElement.offsetWidth / 2 + anchor.offsetWidth / 2) + 'px';
236     tooltipContainer.style.top = (rect.top - contentElement.clientHeight - 5) + 'px';
237 }
238
239 TestResultsView._urlFromBuilder = function (urlType, master, builder, revision, build) {
240     // FIXME: We should probably make this configurable or fetch from buildbot configuraration.
241     return {
242         "build": "http://$master/builders/$builder/builds/$build",
243         "result": "http://$master/results/$builder/r$revision%20($build)/results.html"
244     }[urlType].replace(/\$master/g, master).replace(/\$builder/g, builder)
245         .replace(/\$revision/g, revision).replace(/\$build/g, build);
246 }
247
248 TestResultsView._createResultCell = function (master, builder, result, previousResult) {
249     var buildTime = result['buildTime'];
250     var revision = result['revision'];
251     var slave = result['slave'];
252     var build = result['buildNumber'];
253     var actual = result['actual'];
254     var expected = result['expected'];
255     var anchor = element('a', {'href': this._urlFromBuilder('result', master, builder, revision, build)});
256     anchor.onmouseenter = function () {
257         var repositoryById = TestResultsView._repositories;
258         var formattedRevisions = result.build.formattedRevisions(previousResult ? previousResult.build : null);
259         var revisionDescription = '';
260         for (var repositoryName in formattedRevisions) {
261             var revision = formattedRevisions[repositoryName];
262             if (revisionDescription)
263                 revisionDescription += ', ';
264             if (revision.url)
265                 revisionDescription += repositoryName + ': ' + revision.url;
266             else
267                 revisionDescription += revision.label;
268         }
269
270         TestResultsView.showTooltip(anchor, element('div', {'class': 'tooltip'}, [
271             element('ul', [
272                 element('li', ['Build Time: ' + result.build.formattedBuildTime()]),
273                 element('li', ['Revision: ' +  revisionDescription]),
274                 element('li', ['Build: ', element('a', {'href': TestResultsView._urlFromBuilder('build', master, builder, revision, build)}, [build])]),
275                 element('li', ['Result: ' + actual]),
276             ])
277         ]));
278     }
279     var cell = element('span', {
280         'class': actual + ' resultCell',
281     }, [anchor]);
282     return cell;
283 }
284
285 TestResultsView._populatePane = function(testName, results, section) {
286     var table = element('table', {'class': 'resultsTable'}, [element('caption', [testName])]);
287     var resultsByBuilder = results['builders'];
288     for (var builderId in resultsByBuilder) {
289         var results = resultsByBuilder[builderId];
290         for (var i = 0; i < results.length; i++) {
291             results[i].build = new TestBuild(this._repositories, this._builders, results[i]);
292         }
293
294         var sortedResults = results.sort(function (result1, result2) { return result1.build.time() - result2.build.time(); });
295         var cells = new Array(sortedResults.length);
296         var builder = this._builders[builderId];
297         for (var i = 0; i < sortedResults.length; i++)
298             cells[i] = this._createResultCell(builder.master, builder.name, sortedResults[i], sortedResults[i - 1]);
299
300         var passCount = cells.filter(function (cell) { return cell.className == 'PASS'; }).length;
301         var passingRate = Math.round(passCount / cells.length * 100) + '%';
302         table.appendChild(element('tr', [element('th', [builder.name]), element('td', {'class': 'passingRate'}, [passingRate]),
303             element('td', cells)]));
304         // FIXME: Add a master name if there is more than one.
305     }
306     section.appendChild(table);
307 }
308
309 TestResultsView.fetchTest = function (testName) {
310     if (this._tests.indexOf(testName) >= 0)
311         return;
312
313     var self = this;
314
315     var closeButton = element('div', {'class': 'closeButton'});
316     closeButton.innerHTML = '<svg viewBox="0 0 100 100"><g stroke-width="10">'
317         + '<circle cx="50" cy="50" r="45" fill="transparent"></circle><polygon points="30,30 70,70"></polygon>'
318         + '<polygon points="30,70 70,30"></polygon></g></svg>';
319     closeButton.addEventListener('click', function (event) {
320             self._removeTest(testName);
321             section.parentNode.removeChild(section);
322             event.preventDefault();
323         });
324     var section = element('section', {'id': testName, 'class': 'testResults'}, [closeButton]);
325
326     document.getElementById('container').appendChild(section);
327
328     var xhr = new XMLHttpRequest();
329     xhr.open("GET", 'api/results.php?test=' + testName, true);  
330     xhr.onload = function(event) {
331         var response = JSON.parse(xhr.response);
332         if (response['status'] != 'OK') {
333             section.appendChild(text('Failed to load results for ' + testName + ': ' + response['status']));
334             return;
335         }
336
337         self._populatePane(testName, response, section);
338     }
339     xhr.send();
340     this._tests.push(testName);
341 }
342
343 TestResultsView._removeTest = function (testName) {
344     var index = this._tests.indexOf(testName);
345     if (index < 0)
346         return;
347     this._tests.splice(index, 1);
348     this.updateLocationHash();
349 }
350
351 TestResultsView.fetchTests = function (testNames, doNotUpdateHash) {
352     for (var i = 0; i < testNames.length; i++)
353         this.fetchTest(testNames[i], doNotUpdateHash);
354     this.updateLocationHash();
355 }
356
357 TestResultsView.updateLocationHash = function () {
358     var params = {
359         'tests': this._tests.join(',')
360     };
361     var hash = '';
362     for (var key in params)
363         hash += decodeURIComponent(key) + '=' + decodeURIComponent(params[key]);
364     location.hash = hash;
365     this._oldHash = location.hash;
366 }
367
368 TestResultsView.locationHashChanged = function () {
369     var newHash = location.hash;
370     if (newHash == this._oldHash)
371         return;
372     this._oldHash = newHash;
373     this._tests = [];
374     document.getElementById('container').innerHTML = '';
375     this.loadTestsFromLocationHash();
376 }
377
378 TestResultsView.loadTestsFromLocationHash = function () {
379     var parsed = {};
380     location.hash.substr(1).split('&').forEach(function (component) {
381         var equalPosition = component.indexOf('=');
382         if (equalPosition < 0)
383             return;
384         var name = decodeURIComponent(component.substr(0, equalPosition));
385         parsed[name] = decodeURIComponent(component.substr(equalPosition + 1));
386     });
387     if (!parsed['tests'])
388         return;
389     var doNotUpdateHash = true;
390     this.fetchTests(parsed['tests'].split(','), doNotUpdateHash);
391 }
392
393 function fetchManifest(callback) {
394     var xhr = new XMLHttpRequest();
395     xhr.open("GET", 'api/manifest.php', true);  
396     xhr.onload = function(event) {
397         var response = JSON.parse(xhr.response);
398         if (response['status'] != 'OK') {
399             alert('Failed to load manifest:' + response['status']);
400             console.log(response);
401             return;
402         }
403         callback(response);
404     }  
405     xhr.send();
406 }
407
408 fetchManifest(function (response) {
409     var testNames = response['tests'].map(function (test) { return test['name']; })
410     var input = document.getElementById('testName');
411     input.autocompleter = new Autocompleter(input, testNames);
412
413     TestResultsView.setBuilders(response['builders']);
414     TestResultsView.setSlaves(response['slaves']);
415     TestResultsView.setRepositories(response['repositories']);
416     TestResultsView.loadTestsFromLocationHash();
417 });
418
419 function pasteHelper(input, event) {
420     function removeJunkFromNRWTStdout(input) {
421         return input.replace(/(\[[\w ]+\])|(.+\:.+)/g, '').replace(/^[ \t]+|[ \t]+$/gm, '');
422     }
423
424     function removeJunkFromResultsPage(input) {
425         return input.replace(/(^[^\/]+$)|(^\+)/gm, '').replace(/\([^)]+\)/g, '').replace(/\s+[A-Za-z]+(\s+[A-Za-z]+)*\s*$/gm, '');
426     }
427
428     var text = event.clipboardData.getData('text/plain');
429     if (text.indexOf('\n') < 0)
430         return;
431
432     var urls = removeJunkFromResultsPage(removeJunkFromNRWTStdout(text)).split('\n');
433     TestResultsView.fetchTests(urls.filter(function (url) { return url.length; }));
434     event.preventDefault();
435 }
436
437 </script>
438 </body>
439 </html>