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