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