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