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