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