Make tables on the new flakiness dashboard sortable
[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="https://svn.webkit.org/repository/webkit/!svn/bc/128779/trunk/PerformanceTests/Dromaeo/resources/dromaeo/web/lib/jquery-1.6.4.js"></script>
8 <script src="https://svn.webkit.org/repository/webkit/!svn/bc/128779/trunk/PerformanceTests/resources/jquery.tablesorter.min.js"></script>
9 <script src="js/autocompleter.js"></script>
10 <script src="js/build.js"></script>
11 <script src="js/dom.js"></script>
12 </head>
13 <body>
14
15 <header id="title">
16 <h1><a href="/">WebKit Test Results</a></h1>
17 <ul>
18     <li><a href="http://build.webkit.org/waterfall">Waterfall</a></li>
19 </ul>
20 </header>
21
22 <form onsubmit="TestResultsView.fetchTest(this['testName'].value);
23     TestResultsView.updateLocationHash();
24     return false;">
25 <input id="testName" type="text" size="150" onpaste="pasteHelper(this, event)"
26     placeholder="Type in a test name, or copy and paste test names on results.html or NRWT stdout (including junks)"></form>
27 <div id="testView"></div>
28
29 <form>
30 Show
31 <label>tests <select id="builderFailureTypeView"><option>failing</option><option>flaky</option><option value="wrongexpectations">has wrong expectations</option></select></label>
32 <label>on <select id="builderListView"><option value="">Select builder</option></select></label>
33 <label for="builderDaysView">in the last <select id="builderDaysView"><option>5</option><option>15</option><option>30</option></select> days</label>
34 </form>
35 <div id="builderFailingTestsView"></div>
36
37 <div id="tooltipContainer"></div>
38
39 <script>
40
41 var TestResultsView = new (function () {
42     var tooltipContainer = document.getElementById('tooltipContainer');
43
44     tooltipContainer.onclick = function (event) { event.insideTooltip = true; }
45     document.addEventListener('click', function (event) {
46         if (!event.insideTooltip)
47             tooltipContainer.style.display = 'none';
48     });
49
50     window.addEventListener('hashchange', function (event) {
51         TestResultsView.locationHashChanged();
52     });
53
54     this._tooltipContainer = tooltipContainer;
55     this._tests = [];
56     this._currentBuilder = null;
57     this._currentBuilderFailureType = null;
58     this._currentBuilderDays = null;
59     this._oldHash = null;
60     this._builders = {};
61     this._slaves = {};
62     this._repositories = {};
63     this._testCategories = {};
64 });
65
66 TestResultsView.setAvailableTests = function (availableTests) {
67     this._availableTests = availableTests;
68 }
69
70 TestResultsView.setBuilders = function (builders) {
71     this._builders = builders;
72 }
73
74 TestResultsView.setSlaves = function (slaves) {
75     this._slaves = slaves;
76 }
77
78 TestResultsView.setRepositories = function (repositories) {
79     this._repositories = repositories;
80 }
81
82 TestResultsView.setTestCategories = function (testCategories) {
83     this._testCategories = testCategories;
84 }
85
86 TestResultsView.showTooltip = function (anchor, contentElement) {
87     var tooltipContainer = this._tooltipContainer;
88     tooltipContainer.style.display = null;
89
90     while (tooltipContainer.firstChild)
91         tooltipContainer.removeChild(tooltipContainer.firstChild);
92     tooltipContainer.appendChild(contentElement);
93
94     var position = {x: 0, y: 0};
95     var currentNode = anchor;
96     while (currentNode) {
97         position.x += currentNode.offsetLeft;
98         position.y += currentNode.offsetTop;
99         currentNode = currentNode.offsetParent;
100     }
101     tooltipContainer.style.left = (position.x - contentElement.offsetWidth / 2 + anchor.offsetWidth / 2) + 'px';
102     tooltipContainer.style.top = (position.y - contentElement.clientHeight - 5) + 'px';
103 }
104
105 TestResultsView._createResultCell = function (master, builder, result, previousResult) {
106     var buildTime = result['buildTime'];
107     var revisions = result['revisions'];
108     var slave = result['slave'];
109     var buildNumber = result['buildNumber'];
110     var actual = result['actual'];
111     var expected = result['expected'];
112     var timeIfSlow = result.isSlow ? result.roundedTime : '';
113
114     // FIXME: We shouldn't be hard-coding WebKit revisions here.
115     var webkitRepositoryId;
116     for (var repositoryId in TestResultsView._repositories) {
117         if (TestResultsView._repositories[repositoryId].name == 'WebKit')
118             webkitRepositoryId = repositoryId;
119     }
120     var webkitRevision = result.build.revision(webkitRepositoryId);
121     var resultsPage = webkitRevision ? "http://" + master + "/results/" + builder + "/r" + webkitRevision + "%20(" + buildNumber + ")/results.html"
122         : 'javascript:alert("Could no resolve WebKit revision")';
123
124     var anchor = element('a', {'href': resultsPage }, [timeIfSlow]);
125     anchor.onmouseenter = function () {
126         var formattedRevisions = result.build.formattedRevisions(previousResult ? previousResult.build : null);
127         var revisionDescription = [];
128         for (var repositoryName in formattedRevisions) {
129             var revision = formattedRevisions[repositoryName];
130             if (revisionDescription.length)
131                 revisionDescription.push(', ');
132             if (revision.url)
133                 revisionDescription.push(element('a', {'href': revision.url}, [revision.label]));
134             else
135                 revisionDescription.push(revision.label);
136         }
137
138         TestResultsView.showTooltip(anchor, element('div', {'class': 'tooltip'}, [
139             element('ul', [
140                 element('li', ['Build Time: ' + result.build.formattedBuildTime()]),
141                 element('li', ['Revision: '].concat(revisionDescription)),
142                 element('li', ['Build: ', element('a', {'href': result.build.buildUrl()}, [buildNumber])]),
143                 element('li', ['Actual: ' + actual]),
144                 element('li', ['Expected: ' + expected]),
145             ])
146         ]));
147     }
148     var cell = element('span', {
149         'class': actual + ' resultCell',
150     }, [anchor]);
151     return cell;
152 }
153
154 TestResultsView._populateTestPane = function(testName, results, section) {
155     var test = {name: testName, category: 'LayoutTest'}; // FIXME: Use the real test object.
156     var table = element('table', {'class': 'resultsTable tablesorter'}, [element('caption', [this._linkifiedTestName(test)])]);
157     table.appendChild(this._createTestResultHeader('Builder'));
158
159     var resultsByBuilder = results['builders'];
160     var buildTimes = new Array();
161     for (var builderId in resultsByBuilder) {
162         var results = resultsByBuilder[builderId];
163         this._createBuildsAndComputeSlownessOfResults(builderId, results);
164         for (var i = 0; i < results.length; i++) {
165             var time = results[i].build.time();
166             if (buildTimes.indexOf(time) < 0)
167                 buildTimes.push(time);
168         }
169     }
170     buildTimes.sort(function (a, b) { return b - a; });
171
172     var tbody = element('tbody');
173     for (var builderId in resultsByBuilder) {
174         var builder = this._builders[builderId];
175         // FIXME: Add a master name if there is more than one.
176         tbody.appendChild(this._createTestResultRow(builder.name, resultsByBuilder[builderId], builder, buildTimes));
177     }
178     table.appendChild(tbody);
179     section.appendChild(table);
180     $(table).tablesorter();
181 }
182
183 TestResultsView._linkifiedTestName = function (test) {
184     var category = this._testCategories[test.category];
185     if (!category)
186         return test.name;
187
188     return element('a', {'href': category.url.replace(/\$testName/g, test.name)}, [test.name]);
189 }
190
191 TestResultsView._createTestResultHeader = function (labelForFirstColumn) {
192     return element('thead', [element('tr', [
193         element('th', [labelForFirstColumn]),
194         element('th', ['Bug']),
195         element('th', ['Expectations']),
196         element('th', ['Slowest'])])]);
197 }
198
199 TestResultsView._createBuildsAndComputeSlownessOfResults = function (builderId, results) {
200     for (var i = 0; i < results.length; i++) {
201         var result = results[i];
202         result.builder = builderId;
203         result.build = new TestBuild(this._repositories, this._builders, result);
204         result.roundedTime = result.time > 10000 ? Math.round(result.time / 1000) : Math.round(result.time / 100) / 10;
205         result.isSlow = result.time > 1000;
206     }
207 }
208
209 TestResultsView._createTestResultRow = function (title, results, builder, buildTimes) {
210     var sortedResults = results.sort(function (result1, result2) { return result2.build.time() - result1.build.time(); });
211     var cells = new Array();
212
213     function addEmptyCell() { cells.push(element('span', {'class': 'resultCell'}, [element('a')])); }
214
215     var buildTimeIndex = 0;
216     for (var i = 0; i < sortedResults.length; i++) {
217         var result = sortedResults[i];
218         if (buildTimes) {
219             while (buildTimes[buildTimeIndex] > result.build.time()) {
220                 addEmptyCell();
221                 buildTimeIndex++;
222             }
223             if (buildTimes[buildTimeIndex] == result.build.time())
224                 buildTimeIndex++;
225         }
226         cells.push(this._createResultCell(builder.master, builder.name, result, sortedResults[i - 1]));
227     }
228     if (buildTimes) {
229         while (buildTimeIndex < buildTimes.length) {
230             addEmptyCell();
231             buildTimeIndex++;
232         }
233     }
234
235     var formattedModifiers = sortedResults[0].modifiers.split(' ').map(function (modifier) {
236         if (modifier.indexOf('/') > 0)
237             return element('a', {'href': (modifier.indexOf('http') == 0 ? '' : 'http://') + modifier}, [modifier]);
238         return modifier;
239     });
240
241     var slowestTime = Math.max.apply(Math, results.map(function (result) { return result.roundedTime; }));
242     if (slowestTime >= 1)
243         slowestTime += 's';
244     else
245         slowestTime = '';
246
247     return element('tr', [
248         element('th', [title]),
249         element('td', {'class': 'modifiers'}, formattedModifiers),
250         element('td', {'class': 'expected'}, [sortedResults[0].expected]),
251         element('td', {'class': 'slowestTime'}, [slowestTime]),
252         element('td', cells)]);
253 }
254
255 TestResultsView.fetchTest = function (testName) {
256     if (this._tests.indexOf(testName) >= 0)
257         return;
258
259     var self = this;
260
261     var closeButton = element('div', {'class': 'closeButton'});
262     closeButton.innerHTML = '<svg viewBox="0 0 100 100"><g stroke-width="10">'
263         + '<circle cx="50" cy="50" r="45" fill="transparent"></circle><polygon points="30,30 70,70"></polygon>'
264         + '<polygon points="30,70 70,30"></polygon></g></svg>';
265     closeButton.addEventListener('click', function (event) {
266             self._removeTest(testName);
267             section.parentNode.removeChild(section);
268             event.preventDefault();
269         });
270     var section = element('section', {'id': testName, 'class': 'testResults'}, [closeButton, 'Loading...']);
271
272     document.getElementById('testView').appendChild(section);
273
274     var xhr = new XMLHttpRequest();
275     xhr.open("GET", 'api/results.php?test=' + testName, true);  
276     xhr.onload = function(event) {
277         var response = JSON.parse(xhr.response);
278         section.removeChild(section.lastChild); // Remove Loading...
279         if (response['status'] != 'OK') {
280             section.appendChild(text('Failed to load results for ' + testName + ': ' + response['status']));
281             return;
282         }
283         self._populateTestPane(testName, response, section);
284     }
285     xhr.send();
286     this._tests.push(testName);
287 }
288
289 TestResultsView._removeTest = function (testName) {
290     var index = this._tests.indexOf(testName);
291     if (index < 0)
292         return;
293     this._tests.splice(index, 1);
294     this.updateLocationHash();
295 }
296
297 TestResultsView.fetchTests = function (testNames, doNotUpdateHash) {
298     for (var i = 0; i < testNames.length; i++)
299         this.fetchTest(testNames[i], doNotUpdateHash);
300     if (!doNotUpdateHash)
301         this.updateLocationHash();
302 }
303
304 TestResultsView._matchesFailureType = function (results, failureType, tn) {
305     if (!results.length)
306         return false;
307     var latestActualResult = results[0].actual;
308     var latestExpectedResult = results[0].expected;
309     switch (failureType) {
310     case 'failing':
311         return results[0].actual != 'PASS';
312     case 'flaky':
313         var offOneChangeCount = 0;
314         for (var i = 1; i + 1 < results.length; i++) {
315             var previousActual = results[i - 1].actual;
316             var nextActual = results[i + 1].actual;
317             if (previousActual == nextActual && results[i].actual != previousActual)
318                 offOneChangeCount++;
319         }
320         return offOneChangeCount; // Heuristics.
321     case 'wrongexpectations':
322         if (latestExpectedResult == latestActualResult)
323             return false;
324         var expectedTokens = latestExpectedResult.split(' ');
325         if (expectedTokens.indexOf(latestActualResult) >= 0)
326             return false;
327         if (latestActualResult == 'TEXT' || latestActualResult == 'TEXT+IMAGE' && expectedTokens.indexOf('FAIL') >= 0)
328             return false;
329         return true;
330     }
331
332     return false;
333 }
334
335 TestResultsView._populateBuilderPane = function(builderName, failureType, results, section) {
336     var table = element('table', {'class': 'resultsTable tablesorter'}, [element('caption', [builderName])]);
337     var resultsByBuilder = results['builders'];
338     var resultsByTests;
339     var builderId;
340     for (var currentBuilderId in resultsByBuilder) {
341         builderId = currentBuilderId;
342         resultsByTests = resultsByBuilder[builderId];
343     }
344     if (!resultsByTests)
345         return;
346
347     table.appendChild(this._createTestResultHeader('Test'));
348
349     var tbody = element('tbody');
350     var builder = this._builders[builderId];
351     for (var testId in resultsByTests) {
352         var results = resultsByTests[testId];
353         if (!results.length || !this._matchesFailureType(results, failureType, this._availableTests[testId].name))
354             continue;
355         this._createBuildsAndComputeSlownessOfResults(builderId, resultsByTests[testId]);
356         tbody.appendChild(this._createTestResultRow(this._linkifiedTestName(this._availableTests[testId]), resultsByTests[testId], builder));
357     }
358
359     table.appendChild(tbody);
360     section.appendChild(table);
361
362     $(table).tablesorter(); 
363 }
364
365 TestResultsView.fetchFailingTestsForBuilder = function (builderName, numberOfDays, failureType, doNotUpdateHash) {
366     var section = element('section', {'class': 'testResults'}, ['Loading...']);
367
368     var container = document.getElementById('builderFailingTestsView');
369     container.innerHTML = '';
370     container.appendChild(section);
371
372     var self = this;
373     var xhr = new XMLHttpRequest();
374     xhr.open("GET", 'api/failing-tests.php?builder=' + escape(builderName) + '&days=' + numberOfDays, true);  
375     xhr.onload = function(event) {
376         var response = JSON.parse(xhr.response);
377         section.innerHTML = '';
378         if (response['status'] != 'OK') {
379             section.appendChild(text('Failed to load results for ' + builderName + ': ' + response['status']));
380             return;
381         }
382         // FIXME: Normalize failureType.
383         self._currentBuilder = builderName;
384         self._currentBuilderFailureType = failureType;
385         self._currentBuilderDays = numberOfDays;
386         self._populateBuilderPane(builderName, failureType, response, section);
387         if (!doNotUpdateHash)
388             self.updateLocationHash();
389     }
390     xhr.send();
391 }
392
393 TestResultsView.updateLocationHash = function () {
394     var params = {
395         'tests': this._tests.join(','),
396         'builder': this._currentBuilder,
397         'builderFailureType': this._currentBuilderFailureType,
398         'builderDays': this._currentBuilderDays,
399     };
400     var hash = '';
401     for (var key in params) {
402         var value = params[key];
403         if (value === null || value === undefined)
404             continue;
405         hash += '&' + decodeURIComponent(key) + '=' + decodeURIComponent(value);
406     }
407     location.hash = hash;
408     this._oldHash = location.hash;
409 }
410
411 TestResultsView.locationHashChanged = function () {
412     var newHash = location.hash;
413     if (newHash == this._oldHash)
414         return;
415     this._oldHash = newHash;
416     this._tests = [];
417     this.loadTestsFromLocationHash();
418 }
419
420 TestResultsView.loadTestsFromLocationHash = function () {
421     var parsed = {};
422     location.hash.substr(1).split('&').forEach(function (component) {
423         var equalPosition = component.indexOf('=');
424         if (equalPosition < 0)
425             return;
426         var name = decodeURIComponent(component.substr(0, equalPosition));
427         parsed[name] = decodeURIComponent(component.substr(equalPosition + 1));
428     });
429     var doNotUpdateHash = true;
430     if (parsed['tests']) {
431         document.getElementById('testView').innerHTML = '';
432         this.fetchTests(parsed['tests'].split(','), doNotUpdateHash);
433     }
434     if (parsed['builder']) {
435         this.fetchFailingTestsForBuilder(
436             parsed['builder'],
437             parseInt(parsed['builderDays']) || 5,
438             parsed['builderFailureType'] || 'failing', doNotUpdateHash);
439     }
440     return parsed;
441 }
442
443 function fetchManifest(callback) {
444     var xhr = new XMLHttpRequest();
445     xhr.open("GET", 'api/manifest.php', true);  
446     xhr.onload = function(event) {
447         var response = JSON.parse(xhr.response);
448         if (response['status'] != 'OK') {
449             alert('Failed to load manifest:' + response['status']);
450             console.log(response);
451             return;
452         }
453         callback(response);
454     }  
455     xhr.send();
456 }
457
458 fetchManifest(function (response) {
459     var testNames = response['tests'].map(function (test) { return test['name']; })
460     var input = document.getElementById('testName');
461     input.autocompleter = new Autocompleter(input, testNames);
462
463     var builderListView = document.getElementById('builderListView');
464     for (var builderId in response['builders'])
465         builderListView.appendChild(element('option', [text(response['builders'][builderId].name)]));
466
467     var builderDaysView = document.getElementById('builderDaysView');
468     var builderFailureTypeView = document.getElementById('builderFailureTypeView');
469
470     function updateBuilderView() {
471         if (builderListView.value)
472             TestResultsView.fetchFailingTestsForBuilder(builderListView.value, builderDaysView.value, builderFailureTypeView.value);
473     }
474
475     builderListView.addEventListener('change', updateBuilderView);
476     builderDaysView.addEventListener('change', updateBuilderView);
477     builderFailureTypeView.addEventListener('change', updateBuilderView);
478
479     function mapById(items) {
480         var results = {};
481         for (var i = 0; i < items.length; i++)
482             results[items[i].id] = items[i];
483         return results;
484     }
485
486     TestResultsView.setAvailableTests(mapById(response['tests']));
487     TestResultsView.setBuilders(mapById(response['builders']));
488     TestResultsView.setSlaves(mapById(response['slaves']));
489     TestResultsView.setRepositories(mapById(response['repositories']));
490     TestResultsView.setTestCategories(response['testCategories']);
491     // FIXME: Updating location.href shouldn't be TestResultsView's responsibility.
492     var parsedStates = TestResultsView.loadTestsFromLocationHash();
493     if (parsedStates['builder']) {
494         builderListView.value = parsedStates['builder'];
495         builderDaysView.value = parsedStates['builderDays'];
496         builderFailureTypeView.value = parsedStates['builderFailureType'];
497     }
498 });
499
500 function pasteHelper(input, event) {
501     function removeJunkFromNRWTStdout(input) {
502         return input.replace(/(\[[\w ]+\])|(.+\:.+)/g, '').replace(/^[ \t]+|[ \t]+$/gm, '');
503     }
504
505     function removeJunkFromResultsPage(input) {
506         return input.replace(/(^[^\/]+$)|(^\+)/gm, '').replace(/\([^)]+\)/g, '').replace(/\s+[A-Za-z]+(\s+[A-Za-z]+)*\s*$/gm, '');
507     }
508
509     var text = event.clipboardData.getData('text/plain');
510     if (text.indexOf('\n') < 0)
511         return;
512
513     var urls = removeJunkFromResultsPage(removeJunkFromNRWTStdout(text)).split('\n');
514     TestResultsView.fetchTests(urls.filter(function (url) { return url.length; }));
515     event.preventDefault();
516 }
517
518 </script>
519 </body>
520 </html>