New flakiness dashboard should support showing the failing tests per builder
[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 <link rel="stylesheet" href="main.css">
7 <script src="js/autocompleter.js"></script>
8 <script src="js/build.js"></script>
9 <script src="js/dom.js"></script>
10 </head>
11 <body>
12
13 <header id="title">
14 <h1><a href="/">WebKit Test Results</a></h1>
15 <ul>
16     <li><a href="http://build.webkit.org/waterfall">Waterfall</a></li>
17 </ul>
18 </header>
19
20 <form onsubmit="TestResultsView.fetchTest(this['testName'].value);
21     TestResultsView.updateLocationHash();
22     return false;">
23 <input id="testName" type="text" size="150" onpaste="pasteHelper(this, event)"
24     placeholder="Type in a test name, or copy and paste test names on results.html or NRWT stdout (including junks)"></form>
25 <div id="testView"></div>
26
27 <form>
28 <label>Show failing tests for</label>
29 <select id="builderListView"><option value="">Select builder</option></select>
30 <select id="builderDaysView"><option>5</option><option>15</option><option>30</option></select>
31 </form>
32 <div id="builderFailingTestsView"></div>
33
34 <div id="tooltipContainer"></div>
35
36 <script>
37
38 var TestResultsView = new (function () {
39     var tooltipContainer = document.getElementById('tooltipContainer');
40
41     tooltipContainer.onclick = function (event) { event.insideTooltip = true; }
42     document.addEventListener('click', function (event) {
43         if (!event.insideTooltip)
44             tooltipContainer.style.display = 'none';
45     });
46
47     window.addEventListener('hashchange', function (event) {
48         TestResultsView.locationHashChanged();
49     });
50
51     this._tooltipContainer = tooltipContainer;
52     this._tests = [];
53     this._currentBuilder = null;
54     this._oldHash = null;
55     this._builders = [];
56     this._slaves = [];
57     this._repositories = [];
58 });
59
60 TestResultsView.setAvailableTests = function (availableTests) {
61     this._availableTests = availableTests;
62 }
63
64 TestResultsView.setBuilders = function (builders) {
65     this._builders = builders;
66 }
67
68 TestResultsView.setSlaves = function (slaves) {
69     this._slaves = slaves;
70 }
71
72 TestResultsView.setRepositories = function (repositories) {
73     this._repositories = repositories;
74 }
75
76 TestResultsView.showTooltip = function (anchor, contentElement) {
77     var tooltipContainer = this._tooltipContainer;
78     tooltipContainer.style.display = null;
79
80     while (tooltipContainer.firstChild)
81         tooltipContainer.removeChild(tooltipContainer.firstChild);
82     tooltipContainer.appendChild(contentElement);
83
84     var position = {x: 0, y: 0};
85     var currentNode = anchor;
86     while (currentNode) {
87         position.x += currentNode.offsetLeft;
88         position.y += currentNode.offsetTop;
89         currentNode = currentNode.offsetParent;
90     }
91     tooltipContainer.style.left = (position.x - contentElement.offsetWidth / 2 + anchor.offsetWidth / 2) + 'px';
92     tooltipContainer.style.top = (position.y - contentElement.clientHeight - 5) + 'px';
93 }
94
95 TestResultsView._urlFromBuilder = function (urlType, master, builder, revision, build) {
96     // FIXME: We should probably make this configurable or fetch from buildbot configuraration.
97     return {
98         "build": "http://$master/builders/$builder/builds/$build",
99         "result": "http://$master/results/$builder/r$revision%20($build)/results.html"
100     }[urlType].replace(/\$master/g, master).replace(/\$builder/g, builder)
101         .replace(/\$revision/g, revision).replace(/\$build/g, build);
102 }
103
104 TestResultsView._createResultCell = function (master, builder, result, previousResult) {
105     var buildTime = result['buildTime'];
106     var revision = result['revision'];
107     var slave = result['slave'];
108     var build = result['buildNumber'];
109     var actual = result['actual'];
110     var expected = result['expected'];
111     var anchor = element('a', {'href': this._urlFromBuilder('result', master, builder, revision, build)});
112     anchor.onmouseenter = function () {
113         var repositoryById = TestResultsView._repositories;
114         var formattedRevisions = result.build.formattedRevisions(previousResult ? previousResult.build : null);
115         var revisionDescription = '';
116         for (var repositoryName in formattedRevisions) {
117             var revision = formattedRevisions[repositoryName];
118             if (revisionDescription)
119                 revisionDescription += ', ';
120             if (revision.url)
121                 revisionDescription += repositoryName + ': ' + revision.url;
122             else
123                 revisionDescription += revision.label;
124         }
125
126         TestResultsView.showTooltip(anchor, element('div', {'class': 'tooltip'}, [
127             element('ul', [
128                 element('li', ['Build Time: ' + result.build.formattedBuildTime()]),
129                 element('li', ['Revision: ' +  revisionDescription]),
130                 element('li', ['Build: ', element('a', {'href': TestResultsView._urlFromBuilder('build', master, builder, revision, build)}, [build])]),
131                 element('li', ['Result: ' + actual]),
132             ])
133         ]));
134     }
135     var cell = element('span', {
136         'class': actual + ' resultCell',
137     }, [anchor]);
138     return cell;
139 }
140
141 TestResultsView._populateTestPane = function(testName, results, section) {
142     var table = element('table', {'class': 'resultsTable'}, [element('caption', [testName])]);
143     var resultsByBuilder = results['builders'];
144     for (var builderId in resultsByBuilder) {
145         var resultsByTest = resultsByBuilder[builderId];
146         var results;
147         for (var testId in resultsByTest)
148             results = resultsByTest[testId];
149         if (!results)
150             continue;
151
152         var builder = this._builders[builderId];
153         // FIXME: Add a master name if there is more than one.
154         table.appendChild(this._createTestResultRow(builder.name, results, builder));
155     }
156     section.appendChild(table);
157 }
158
159 TestResultsView._createTestResultRow = function (title, results, builder) {
160     for (var i = 0; i < results.length; i++)
161         results[i].build = new TestBuild(this._repositories, this._builders, results[i]);
162
163     var sortedResults = results.sort(function (result1, result2) { return result1.build.time() - result2.build.time(); });
164     var cells = new Array(sortedResults.length);
165     for (var i = 0; i < sortedResults.length; i++)
166         cells[i] = this._createResultCell(builder.master, builder.name, sortedResults[i], sortedResults[i - 1]);
167
168     var passCount = cells.filter(function (cell) { return cell.className == 'PASS'; }).length;
169     var passingRate = Math.round(passCount / cells.length * 100) + '%';
170     return element('tr', [element('th', ['' + title]), element('td', {'class': 'passingRate'}, [passingRate]), element('td', cells)]);
171 }
172
173 TestResultsView.fetchTest = function (testName) {
174     if (this._tests.indexOf(testName) >= 0)
175         return;
176
177     var self = this;
178
179     var closeButton = element('div', {'class': 'closeButton'});
180     closeButton.innerHTML = '<svg viewBox="0 0 100 100"><g stroke-width="10">'
181         + '<circle cx="50" cy="50" r="45" fill="transparent"></circle><polygon points="30,30 70,70"></polygon>'
182         + '<polygon points="30,70 70,30"></polygon></g></svg>';
183     closeButton.addEventListener('click', function (event) {
184             self._removeTest(testName);
185             section.parentNode.removeChild(section);
186             event.preventDefault();
187         });
188     var section = element('section', {'id': testName, 'class': 'testResults'}, [closeButton, 'Loading...']);
189
190     document.getElementById('testView').appendChild(section);
191
192     var xhr = new XMLHttpRequest();
193     xhr.open("GET", 'api/results.php?test=' + testName, true);  
194     xhr.onload = function(event) {
195         var response = JSON.parse(xhr.response);
196         section.removeChild(section.lastChild); // Remove Loading...
197         if (response['status'] != 'OK') {
198             section.appendChild(text('Failed to load results for ' + testName + ': ' + response['status']));
199             return;
200         }
201         self._populateTestPane(testName, response, section);
202     }
203     xhr.send();
204     this._tests.push(testName);
205 }
206
207 TestResultsView._removeTest = function (testName) {
208     var index = this._tests.indexOf(testName);
209     if (index < 0)
210         return;
211     this._tests.splice(index, 1);
212     this.updateLocationHash();
213 }
214
215 TestResultsView.fetchTests = function (testNames, doNotUpdateHash) {
216     for (var i = 0; i < testNames.length; i++)
217         this.fetchTest(testNames[i], doNotUpdateHash);
218     if (!doNotUpdateHash)
219         this.updateLocationHash();
220 }
221
222 TestResultsView._populateBuilderPane = function(builderName, results, section) {
223     var table = element('table', {'class': 'resultsTable'}, [element('caption', [builderName])]);
224     var resultsByBuilder = results['builders'];
225     var resultsByTests;
226     var builderId;
227     for (var currentBuilderId in resultsByBuilder) {
228         builderId = currentBuilderId;
229         resultsByTests = resultsByBuilder[builderId];
230     }
231     if (!resultsByTests)
232         return;
233
234     var builder = this._builders[builderId];
235     for (var testId in resultsByTests)
236         table.appendChild(this._createTestResultRow(this._availableTests[testId].name, resultsByTests[testId], builder));
237     section.appendChild(table);
238 }
239
240 TestResultsView.fetchFailingTestsForBuilder = function (builderName, numberOfDays, doNotUpdateHash) {
241     var section = element('section', {'class': 'testResults'}, ['Loading...']);
242
243     var container = document.getElementById('builderFailingTestsView');
244     container.innerHTML = '';
245     container.appendChild(section);
246
247     var self = this;
248     var xhr = new XMLHttpRequest();
249     xhr.open("GET", 'api/failing-tests.php?builder=' + escape(builderName) + '&' + 'days=' + numberOfDays, true);  
250     xhr.onload = function(event) {
251         var response = JSON.parse(xhr.response);
252         section.innerHTML = '';
253         if (response['status'] != 'OK') {
254             section.appendChild(text('Failed to load results for ' + builderName + ': ' + response['status']));
255             return;
256         }
257         self._currentBuilder = builderName;
258         self._currentBuilderDays = numberOfDays;
259         self._populateBuilderPane(builderName, response, section);
260         if (!doNotUpdateHash)
261             self.updateLocationHash();
262     }
263     xhr.send();
264 }
265
266 TestResultsView.updateLocationHash = function () {
267     var params = {
268         'tests': this._tests.join(','),
269         'builder': this._currentBuilder,
270         'builderDays': this._currentBuilderDays,
271     };
272     var hash = '';
273     for (var key in params) {
274         var value = params[key];
275         if (value === null || value === undefined)
276             continue;
277         hash += '&' + decodeURIComponent(key) + '=' + decodeURIComponent(value);
278     }
279     location.hash = hash;
280     this._oldHash = location.hash;
281 }
282
283 TestResultsView.locationHashChanged = function () {
284     var newHash = location.hash;
285     if (newHash == this._oldHash)
286         return;
287     this._oldHash = newHash;
288     this._tests = [];
289     this.loadTestsFromLocationHash();
290 }
291
292 TestResultsView.loadTestsFromLocationHash = function () {
293     var parsed = {};
294     location.hash.substr(1).split('&').forEach(function (component) {
295         var equalPosition = component.indexOf('=');
296         if (equalPosition < 0)
297             return;
298         var name = decodeURIComponent(component.substr(0, equalPosition));
299         parsed[name] = decodeURIComponent(component.substr(equalPosition + 1));
300     });
301     var doNotUpdateHash = true;
302     if (parsed['tests']) {
303         document.getElementById('testView').innerHTML = '';
304         this.fetchTests(parsed['tests'].split(','), doNotUpdateHash);
305     }
306     if (parsed['builder'])
307         this.fetchFailingTestsForBuilder(parsed['builder'], parsed['builderDays'] || 5, doNotUpdateHash);
308 }
309
310 function fetchManifest(callback) {
311     var xhr = new XMLHttpRequest();
312     xhr.open("GET", 'api/manifest.php', true);  
313     xhr.onload = function(event) {
314         var response = JSON.parse(xhr.response);
315         if (response['status'] != 'OK') {
316             alert('Failed to load manifest:' + response['status']);
317             console.log(response);
318             return;
319         }
320         callback(response);
321     }  
322     xhr.send();
323 }
324
325 fetchManifest(function (response) {
326     var testNames = response['tests'].map(function (test) { return test['name']; })
327     var input = document.getElementById('testName');
328     input.autocompleter = new Autocompleter(input, testNames);
329
330     var builderListView = document.getElementById('builderListView');
331     for (var builderId in response['builders'])
332         builderListView.appendChild(element('option', [text(response['builders'][builderId].name)]));
333
334     var builderDaysView = document.getElementById('builderDaysView');
335
336     function updateBuilderView() {
337         if (builderListView.value)
338             TestResultsView.fetchFailingTestsForBuilder(builderListView.value, builderDaysView.value);
339     }
340
341     builderListView.addEventListener('change', updateBuilderView);
342     builderDaysView.addEventListener('change', updateBuilderView);
343
344     function mapById(items) {
345         var results = {};
346         for (var i = 0; i < items.length; i++)
347             results[items[i].id] = items[i];
348         return results;
349     }
350
351     TestResultsView.setAvailableTests(mapById(response['tests']));
352     TestResultsView.setBuilders(mapById(response['builders']));
353     TestResultsView.setSlaves(mapById(response['slaves']));
354     TestResultsView.setRepositories(mapById(response['repositories']));
355     TestResultsView.loadTestsFromLocationHash();
356 });
357
358 function pasteHelper(input, event) {
359     function removeJunkFromNRWTStdout(input) {
360         return input.replace(/(\[[\w ]+\])|(.+\:.+)/g, '').replace(/^[ \t]+|[ \t]+$/gm, '');
361     }
362
363     function removeJunkFromResultsPage(input) {
364         return input.replace(/(^[^\/]+$)|(^\+)/gm, '').replace(/\([^)]+\)/g, '').replace(/\s+[A-Za-z]+(\s+[A-Za-z]+)*\s*$/gm, '');
365     }
366
367     var text = event.clipboardData.getData('text/plain');
368     if (text.indexOf('\n') < 0)
369         return;
370
371     var urls = removeJunkFromResultsPage(removeJunkFromNRWTStdout(text)).split('\n');
372     TestResultsView.fetchTests(urls.filter(function (url) { return url.length; }));
373     event.preventDefault();
374 }
375
376 </script>
377 </body>
378 </html>