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>
16 <h1><a href="/">WebKit Test Results</a></h1>
18 <li><a href="http://build.webkit.org/waterfall">Waterfall</a></li>
22 <form onsubmit="TestResultsView.fetchTest(this['testName'].value);
23 TestResultsView.updateLocationHash();
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>
29 <div id="buildersView">
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>
37 <div id="builderFailingTestsView"></div>
40 <div id="tooltipContainer"></div>
44 var TestResultsView = new (function () {
45 var tooltipContainer = document.getElementById('tooltipContainer');
47 tooltipContainer.onclick = function (event) { event.insideTooltip = true; }
48 document.addEventListener('click', function (event) {
49 if (!event.insideTooltip)
50 tooltipContainer.style.display = 'none';
53 window.addEventListener('hashchange', function (event) {
54 TestResultsView.locationHashChanged();
57 this._tooltipContainer = tooltipContainer;
59 this._currentBuilder = null;
60 this._currentBuilderFailureType = null;
61 this._currentBuilderDays = null;
64 this._builderByName = {};
66 this._repositories = {};
67 this._testCategories = {};
70 TestResultsView.setAvailableTests = function (availableTests) {
71 this._availableTests = availableTests;
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;
82 TestResultsView.setSlaves = function (slaves) {
83 this._slaves = slaves;
86 TestResultsView.setRepositories = function (repositories) {
87 this._repositories = repositories;
90 TestResultsView.setTestCategories = function (testCategories) {
91 this._testCategories = testCategories;
94 TestResultsView.showTooltip = function (anchor, contentElement) {
95 var tooltipContainer = this._tooltipContainer;
96 tooltipContainer.style.display = null;
98 while (tooltipContainer.firstChild)
99 tooltipContainer.removeChild(tooltipContainer.firstChild);
100 tooltipContainer.appendChild(contentElement);
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;
109 tooltipContainer.style.left = (position.x - contentElement.offsetWidth / 2 + anchor.offsetWidth / 2) + 'px';
110 tooltipContainer.style.top = (position.y - contentElement.clientHeight - 5) + 'px';
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 : '';
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;
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")';
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(', ');
141 revisionDescription.push(element('a', {'href': revision.url}, [revision.label]));
143 revisionDescription.push(revision.label);
146 TestResultsView.showTooltip(anchor, element('div', {'class': 'tooltip'}, [
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]),
157 var cell = element('span', {
158 'class': actual + ' resultCell',
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)])]);
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);
178 buildTimes.sort(function (a, b) { return b - a; });
180 var repositories = [];
181 for (var repositoryId in this._repositories)
182 repositories.push(this._repositories[repositoryId]);
184 table.appendChild(this._createTestResultHeader('Builder', repositories));
186 var tbody = element('tbody');
188 for (var builderId in resultsByBuilder)
189 builders.push(this._builders[builderId]);
192 this._sortObjectsByName(builders).forEach(function (builder) {
193 tbody.appendChild(self._createTestResultRow(builder.name, resultsByBuilder[builder.id], builder, buildTimes, repositories));
196 table.appendChild(tbody);
197 section.appendChild(table);
198 $(table).tablesorter();
201 TestResultsView._sortObjectsByName = function (list) {
202 return list.sort(function (a, b) {
211 TestResultsView._urlFromTest = function (test) {
212 var category = this._testCategories[test.category];
215 return category.url.replace(/\$testName/g, test.name);
218 TestResultsView._linkifiedTestName = function (test) {
219 var url = this._urlFromTest(test);
220 return url ? element('a', {'href': url}, [test.name]) : test.name;
223 TestResultsView._createTestResultHeader = function (labelForFirstColumn, repositories) {
224 var latestResultsLabel = '';
226 latestResultsLabel = ['Latest results', element('br'),
227 repositories.map(function (repository) { return repository.name; }).join(' / ')];
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),
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;
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();
253 function addEmptyCell() { cells.push(element('span', {'class': 'resultCell'}, [element('a')])); }
255 var buildTimeIndex = 0;
256 for (var i = 0; i < sortedResults.length; i++) {
257 var result = sortedResults[i];
259 while (buildTimes[buildTimeIndex] > result.build.time()) {
263 if (buildTimes[buildTimeIndex] == result.build.time())
266 cells.push(this._createResultCell(builder.master, builder.name, result, sortedResults[i - 1]));
269 while (buildTimeIndex < buildTimes.length) {
275 var seenBugLink = false;
276 var formattedModifiers = sortedResults[0].modifiers.split(' ').map(function (modifier) {
277 if (modifier.indexOf('/') > 0) {
279 return element('a', {'href': (modifier.indexOf('http') == 0 ? '' : 'http://') + modifier}, [modifier]);
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'},
291 var slowestTime = Math.max.apply(Math, results.map(function (result) { return result.roundedTime; }));
292 if (slowestTime >= 1)
297 var latestRevisions = [];
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]));
307 latestRevisions.push(revisionInfo.label);
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)]));
320 TestResultsView.fetchTest = function (testName) {
321 if (this._tests.indexOf(testName) >= 0)
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();
335 var section = element('section', {'id': testName, 'class': 'testResults'}, [closeButton, 'Loading...']);
337 document.getElementById('testView').appendChild(section);
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']));
348 self._populateTestPane(testName, response, section);
351 this._tests.push(testName);
354 TestResultsView._removeTest = function (testName) {
355 var index = this._tests.indexOf(testName);
358 this._tests.splice(index, 1);
359 this.updateLocationHash();
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();
369 TestResultsView._populateBuilderPane = function(builderName, failureType, results, section) {
370 var table = element('table', {'class': 'resultsTable tablesorter'}, [element('caption', [builderName])]);
371 var resultsByBuilder = results['builders'];
374 for (var currentBuilderId in resultsByBuilder) {
375 builderId = currentBuilderId;
376 resultsByTests = resultsByBuilder[builderId];
381 table.appendChild(this._createTestResultHeader('Test'));
384 for (var testId in resultsByTests)
385 tests.push(this._availableTests[testId]);
387 var tbody = element('tbody');
388 var builder = this._builders[builderId];
390 this._sortObjectsByName(tests).forEach(function (test) {
391 var results = resultsByTests[test.id];
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();
402 tbody.appendChild(self._createTestResultRow(element('span', [testName, externalTestLink]), results, builder));
405 table.appendChild(tbody);
406 section.appendChild(table);
408 $(table).tablesorter();
411 TestResultsView.fetchFailingTestsForBuilder = function (builderName, numberOfDays, failureType, doNotUpdateHash) {
412 var section = element('section', {'class': 'testResults'}, ['Loading...']);
414 var container = document.getElementById('builderFailingTestsView');
415 container.innerHTML = '';
416 container.appendChild(section);
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));
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']));
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();
444 TestResultsView._createLocationHash = function (tests) {
446 'tests': tests.join(','),
447 'builder': this._currentBuilder,
448 'builderFailureType': this._currentBuilderFailureType,
449 'builderDays': this._currentBuilderDays,
452 for (var key in params) {
453 var value = params[key];
454 if (value === null || value === undefined)
456 hash += '&' + decodeURIComponent(key) + '=' + decodeURIComponent(value);
461 TestResultsView.updateLocationHash = function () {
462 location.hash = this._createLocationHash(this._tests);
463 this._oldHash = location.hash;
466 TestResultsView.locationHashChanged = function () {
467 var newHash = location.hash;
468 if (newHash == this._oldHash)
470 this._oldHash = newHash;
472 this.loadTestsFromLocationHash();
475 TestResultsView.loadTestsFromLocationHash = function () {
477 location.hash.substr(1).split('&').forEach(function (component) {
478 var equalPosition = component.indexOf('=');
479 if (equalPosition < 0)
481 var name = decodeURIComponent(component.substr(0, equalPosition));
482 parsed[name] = decodeURIComponent(component.substr(equalPosition + 1));
484 var doNotUpdateHash = true;
485 if (parsed['tests']) {
486 document.getElementById('testView').innerHTML = '';
487 this.fetchTests(parsed['tests'].split(','), doNotUpdateHash);
489 if (parsed['builder']) {
490 this.fetchFailingTestsForBuilder(
492 parseInt(parsed['builderDays']) || 5,
493 parsed['builderFailureType'] || 'failing', doNotUpdateHash);
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);
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);
518 var builderListView = document.getElementById('builderListView');
519 TestResultsView._sortObjectsByName(response['builders']).forEach(function (builder) {
520 builderListView.appendChild(element('option', [builder.name]));
523 var builderDaysView = document.getElementById('builderDaysView');
524 var builderFailureTypeView = document.getElementById('builderFailureTypeView');
526 function updateBuilderView() {
527 if (builderListView.value)
528 TestResultsView.fetchFailingTestsForBuilder(builderListView.value, builderDaysView.value, builderFailureTypeView.value);
531 builderListView.addEventListener('change', updateBuilderView);
532 builderDaysView.addEventListener('change', updateBuilderView);
533 builderFailureTypeView.addEventListener('change', updateBuilderView);
535 function mapById(items) {
537 for (var i = 0; i < items.length; i++)
538 results[items[i].id] = items[i];
542 TestResultsView.setAvailableTests(mapById(response['tests']));
543 TestResultsView.setBuilders(mapById(response['builders']));
544 TestResultsView.setSlaves(mapById(response['slaves']));
545 TestResultsView.setRepositories(mapById(response['repositories']));
546 TestResultsView.setTestCategories(response['testCategories']);
547 // FIXME: Updating location.href shouldn't be TestResultsView's responsibility.
548 var parsedStates = TestResultsView.loadTestsFromLocationHash();
549 if (parsedStates['builder']) {
550 builderListView.value = parsedStates['builder'];
551 builderDaysView.value = parsedStates['builderDays'];
552 builderFailureTypeView.value = parsedStates['builderFailureType'];
556 function pasteHelper(input, event) {
557 function removeJunkFromNRWTStdout(input) {
558 return input.replace(/(\[[\w ]+\])|(.+\:.+)/g, '').replace(/^[ \t]+|[ \t]+$/gm, '');
561 function removeJunkFromResultsPage(input) {
562 return input.replace(/(^[^\/]+$)|(^\+)/gm, '').replace(/\([^)]+\)/g, '').replace(/\s+[A-Za-z]+(\s+[A-Za-z]+)*\s*$/gm, '');
565 var text = event.clipboardData.getData('text/plain');
566 if (text.indexOf('\n') < 0)
569 var urls = removeJunkFromResultsPage(removeJunkFromNRWTStdout(text)).split('\n');
570 TestResultsView.fetchTests(urls.filter(function (url) { return url.length; }));
571 event.preventDefault();