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>
31 <label>tests <select id="builderFailureTypeView"><option>failing</option><option>flaky</option><option value="wrongexpectations">has wrong expectations</option></select></label>
32 <label>on <select id="builderListView"><option value="">Select builder</option></select></label>
33 <label for="builderDaysView">in the last <select id="builderDaysView"><option>5</option><option>15</option><option>30</option></select> days</label>
35 <div id="builderFailingTestsView"></div>
37 <div id="tooltipContainer"></div>
41 var TestResultsView = new (function () {
42 var tooltipContainer = document.getElementById('tooltipContainer');
44 tooltipContainer.onclick = function (event) { event.insideTooltip = true; }
45 document.addEventListener('click', function (event) {
46 if (!event.insideTooltip)
47 tooltipContainer.style.display = 'none';
50 window.addEventListener('hashchange', function (event) {
51 TestResultsView.locationHashChanged();
54 this._tooltipContainer = tooltipContainer;
56 this._currentBuilder = null;
57 this._currentBuilderFailureType = null;
58 this._currentBuilderDays = null;
62 this._repositories = {};
63 this._testCategories = {};
66 TestResultsView.setAvailableTests = function (availableTests) {
67 this._availableTests = availableTests;
70 TestResultsView.setBuilders = function (builders) {
71 this._builders = builders;
74 TestResultsView.setSlaves = function (slaves) {
75 this._slaves = slaves;
78 TestResultsView.setRepositories = function (repositories) {
79 this._repositories = repositories;
82 TestResultsView.setTestCategories = function (testCategories) {
83 this._testCategories = testCategories;
86 TestResultsView.showTooltip = function (anchor, contentElement) {
87 var tooltipContainer = this._tooltipContainer;
88 tooltipContainer.style.display = null;
90 while (tooltipContainer.firstChild)
91 tooltipContainer.removeChild(tooltipContainer.firstChild);
92 tooltipContainer.appendChild(contentElement);
94 var position = {x: 0, y: 0};
95 var currentNode = anchor;
97 position.x += currentNode.offsetLeft;
98 position.y += currentNode.offsetTop;
99 currentNode = currentNode.offsetParent;
101 tooltipContainer.style.left = (position.x - contentElement.offsetWidth / 2 + anchor.offsetWidth / 2) + 'px';
102 tooltipContainer.style.top = (position.y - contentElement.clientHeight - 5) + 'px';
105 TestResultsView._createResultCell = function (master, builder, result, previousResult) {
106 var buildTime = result['buildTime'];
107 var revisions = result['revisions'];
108 var slave = result['slave'];
109 var buildNumber = result['buildNumber'];
110 var actual = result['actual'];
111 var expected = result['expected'];
112 var timeIfSlow = result.isSlow ? result.roundedTime : '';
114 // FIXME: We shouldn't be hard-coding WebKit revisions here.
115 var webkitRepositoryId;
116 for (var repositoryId in TestResultsView._repositories) {
117 if (TestResultsView._repositories[repositoryId].name == 'WebKit')
118 webkitRepositoryId = repositoryId;
120 var webkitRevision = result.build.revision(webkitRepositoryId);
121 var resultsPage = webkitRevision ? "http://" + master + "/results/" + builder + "/r" + webkitRevision + "%20(" + buildNumber + ")/results.html"
122 : 'javascript:alert("Could no resolve WebKit revision")';
124 var anchor = element('a', {'href': resultsPage }, [timeIfSlow]);
125 anchor.onmouseenter = function () {
126 var formattedRevisions = result.build.formattedRevisions(previousResult ? previousResult.build : null);
127 var revisionDescription = [];
128 for (var repositoryName in formattedRevisions) {
129 var revision = formattedRevisions[repositoryName];
130 if (revisionDescription.length)
131 revisionDescription.push(', ');
133 revisionDescription.push(element('a', {'href': revision.url}, [revision.label]));
135 revisionDescription.push(revision.label);
138 TestResultsView.showTooltip(anchor, element('div', {'class': 'tooltip'}, [
140 element('li', ['Build Time: ' + result.build.formattedBuildTime()]),
141 element('li', ['Revision: '].concat(revisionDescription)),
142 element('li', ['Build: ', element('a', {'href': result.build.buildUrl()}, [buildNumber])]),
143 element('li', ['Actual: ' + actual]),
144 element('li', ['Expected: ' + expected]),
148 var cell = element('span', {
149 'class': actual + ' resultCell',
154 TestResultsView._populateTestPane = function(testName, results, section) {
155 var test = {name: testName, category: 'LayoutTest'}; // FIXME: Use the real test object.
156 var table = element('table', {'class': 'resultsTable tablesorter'}, [element('caption', [this._linkifiedTestName(test)])]);
158 var resultsByBuilder = results['builders'];
159 var buildTimes = new Array();
160 for (var builderId in resultsByBuilder) {
161 var results = resultsByBuilder[builderId];
162 this._createBuildsAndComputeSlownessOfResults(builderId, results);
163 for (var i = 0; i < results.length; i++) {
164 var time = results[i].build.time();
165 if (buildTimes.indexOf(time) < 0)
166 buildTimes.push(time);
169 buildTimes.sort(function (a, b) { return b - a; });
171 var repositories = [];
172 for (var repositoryId in this._repositories)
173 repositories.push(this._repositories[repositoryId]);
175 table.appendChild(this._createTestResultHeader('Builder', repositories));
177 var tbody = element('tbody');
178 for (var builderId in resultsByBuilder) {
179 var builder = this._builders[builderId];
180 // FIXME: Add a master name if there is more than one.
181 tbody.appendChild(this._createTestResultRow(builder.name, resultsByBuilder[builderId], builder, buildTimes, repositories));
183 table.appendChild(tbody);
184 section.appendChild(table);
185 $(table).tablesorter();
188 TestResultsView._linkifiedTestName = function (test) {
189 var category = this._testCategories[test.category];
193 return element('a', {'href': category.url.replace(/\$testName/g, test.name)}, [test.name]);
196 TestResultsView._createTestResultHeader = function (labelForFirstColumn, repositories) {
197 return element('thead', [element('tr', [
198 element('th', [labelForFirstColumn]),
199 element('th', ['Bug']),
200 element('th', ['Expectations']),
201 element('th', ['Slowest'])]
202 .concat(repositories ? repositories.map(function (repository) { return element('th', [repository.name]); }) : [])
203 .concat([element('th')]))]);
206 TestResultsView._createBuildsAndComputeSlownessOfResults = function (builderId, results) {
207 for (var i = 0; i < results.length; i++) {
208 var result = results[i];
209 result.builder = builderId;
210 result.build = new TestBuild(this._repositories, this._builders, result);
211 result.roundedTime = result.time > 10000 ? Math.round(result.time / 1000) : Math.round(result.time / 100) / 10;
212 result.isSlow = result.time > 1000;
216 TestResultsView._createTestResultRow = function (title, results, builder, buildTimes, repositories) {
217 var sortedResults = results.sort(function (result1, result2) { return result2.build.time() - result1.build.time(); });
218 var cells = new Array();
220 function addEmptyCell() { cells.push(element('span', {'class': 'resultCell'}, [element('a')])); }
222 var buildTimeIndex = 0;
223 for (var i = 0; i < sortedResults.length; i++) {
224 var result = sortedResults[i];
226 while (buildTimes[buildTimeIndex] > result.build.time()) {
230 if (buildTimes[buildTimeIndex] == result.build.time())
233 cells.push(this._createResultCell(builder.master, builder.name, result, sortedResults[i - 1]));
236 while (buildTimeIndex < buildTimes.length) {
242 var seenBugLink = false;
243 var formattedModifiers = sortedResults[0].modifiers.split(' ').map(function (modifier) {
244 if (modifier.indexOf('/') > 0) {
246 return element('a', {'href': (modifier.indexOf('http') == 0 ? '' : 'http://') + modifier}, [modifier]);
252 // FIXME: Make bug tracker configurable.
253 formattedModifiers.push(element('a',
254 {'href': 'https://bugs.webkit.org/enter_bug.cgi?product=WebKit&component=Tools%20/%20Tests&form_name=enter_bug&keywords=LayoutTestFailure'},
258 var slowestTime = Math.max.apply(Math, results.map(function (result) { return result.roundedTime; }));
259 if (slowestTime >= 1)
264 var formattedRevisionCells = [];
266 var build = sortedResults[0].build;
267 for (var i = 0; i < repositories.length; i++) {
268 var revisionInfo = build.formattedRevision(repositories[i].id);
269 if (revisionInfo.url)
270 formattedRevisionCells.push(element('a', {'href': revisionInfo.url}, [revisionInfo.label]));
272 formattedRevisionCells.push(revisionInfo.label);
276 return element('tr', [
277 element('th', [title]),
278 element('td', {'class': 'modifiers'}, formattedModifiers),
279 element('td', {'class': 'expected'}, [sortedResults[0].expected]),
280 element('td', {'class': 'slowestTime'}, [slowestTime])]
281 .concat(formattedRevisionCells)
282 .concat([element('td', cells)]));
285 TestResultsView.fetchTest = function (testName) {
286 if (this._tests.indexOf(testName) >= 0)
291 var closeButton = element('div', {'class': 'closeButton'});
292 closeButton.innerHTML = '<svg viewBox="0 0 100 100"><g stroke-width="10">'
293 + '<circle cx="50" cy="50" r="45" fill="transparent"></circle><polygon points="30,30 70,70"></polygon>'
294 + '<polygon points="30,70 70,30"></polygon></g></svg>';
295 closeButton.addEventListener('click', function (event) {
296 self._removeTest(testName);
297 section.parentNode.removeChild(section);
298 event.preventDefault();
300 var section = element('section', {'id': testName, 'class': 'testResults'}, [closeButton, 'Loading...']);
302 document.getElementById('testView').appendChild(section);
304 var xhr = new XMLHttpRequest();
305 xhr.open("GET", 'api/results.php?test=' + testName, true);
306 xhr.onload = function(event) {
307 var response = JSON.parse(xhr.response);
308 section.removeChild(section.lastChild); // Remove Loading...
309 if (response['status'] != 'OK') {
310 section.appendChild(text('Failed to load results for ' + testName + ': ' + response['status']));
313 self._populateTestPane(testName, response, section);
316 this._tests.push(testName);
319 TestResultsView._removeTest = function (testName) {
320 var index = this._tests.indexOf(testName);
323 this._tests.splice(index, 1);
324 this.updateLocationHash();
327 TestResultsView.fetchTests = function (testNames, doNotUpdateHash) {
328 for (var i = 0; i < testNames.length; i++)
329 this.fetchTest(testNames[i], doNotUpdateHash);
330 if (!doNotUpdateHash)
331 this.updateLocationHash();
334 TestResultsView._matchesFailureType = function (results, failureType, tn) {
337 var latestActualResult = results[0].actual;
338 var latestExpectedResult = results[0].expected;
339 switch (failureType) {
341 return results[0].actual != 'PASS';
343 var offOneChangeCount = 0;
344 for (var i = 1; i + 1 < results.length; i++) {
345 var previousActual = results[i - 1].actual;
346 var nextActual = results[i + 1].actual;
347 if (previousActual == nextActual && results[i].actual != previousActual)
350 return offOneChangeCount; // Heuristics.
351 case 'wrongexpectations':
352 if (latestExpectedResult == latestActualResult)
354 var expectedTokens = latestExpectedResult.split(' ');
355 if (expectedTokens.indexOf(latestActualResult) >= 0)
357 if (latestActualResult == 'TEXT' || latestActualResult == 'TEXT+IMAGE' && expectedTokens.indexOf('FAIL') >= 0)
365 TestResultsView._populateBuilderPane = function(builderName, failureType, results, section) {
366 var table = element('table', {'class': 'resultsTable tablesorter'}, [element('caption', [builderName])]);
367 var resultsByBuilder = results['builders'];
370 for (var currentBuilderId in resultsByBuilder) {
371 builderId = currentBuilderId;
372 resultsByTests = resultsByBuilder[builderId];
377 table.appendChild(this._createTestResultHeader('Test'));
379 var tbody = element('tbody');
380 var builder = this._builders[builderId];
381 for (var testId in resultsByTests) {
382 var results = resultsByTests[testId];
383 if (!results.length || !this._matchesFailureType(results, failureType, this._availableTests[testId].name))
385 this._createBuildsAndComputeSlownessOfResults(builderId, resultsByTests[testId]);
386 tbody.appendChild(this._createTestResultRow(this._linkifiedTestName(this._availableTests[testId]), resultsByTests[testId], builder));
389 table.appendChild(tbody);
390 section.appendChild(table);
392 $(table).tablesorter();
395 TestResultsView.fetchFailingTestsForBuilder = function (builderName, numberOfDays, failureType, doNotUpdateHash) {
396 var section = element('section', {'class': 'testResults'}, ['Loading...']);
398 var container = document.getElementById('builderFailingTestsView');
399 container.innerHTML = '';
400 container.appendChild(section);
403 var xhr = new XMLHttpRequest();
404 xhr.open("GET", 'api/failing-tests.php?builder=' + escape(builderName) + '&days=' + numberOfDays, true);
405 xhr.onload = function(event) {
406 var response = JSON.parse(xhr.response);
407 section.innerHTML = '';
408 if (response['status'] != 'OK') {
409 section.appendChild(text('Failed to load results for ' + builderName + ': ' + response['status']));
412 // FIXME: Normalize failureType.
413 self._currentBuilder = builderName;
414 self._currentBuilderFailureType = failureType;
415 self._currentBuilderDays = numberOfDays;
416 self._populateBuilderPane(builderName, failureType, response, section);
417 if (!doNotUpdateHash)
418 self.updateLocationHash();
423 TestResultsView.updateLocationHash = function () {
425 'tests': this._tests.join(','),
426 'builder': this._currentBuilder,
427 'builderFailureType': this._currentBuilderFailureType,
428 'builderDays': this._currentBuilderDays,
431 for (var key in params) {
432 var value = params[key];
433 if (value === null || value === undefined)
435 hash += '&' + decodeURIComponent(key) + '=' + decodeURIComponent(value);
437 location.hash = hash;
438 this._oldHash = location.hash;
441 TestResultsView.locationHashChanged = function () {
442 var newHash = location.hash;
443 if (newHash == this._oldHash)
445 this._oldHash = newHash;
447 this.loadTestsFromLocationHash();
450 TestResultsView.loadTestsFromLocationHash = function () {
452 location.hash.substr(1).split('&').forEach(function (component) {
453 var equalPosition = component.indexOf('=');
454 if (equalPosition < 0)
456 var name = decodeURIComponent(component.substr(0, equalPosition));
457 parsed[name] = decodeURIComponent(component.substr(equalPosition + 1));
459 var doNotUpdateHash = true;
460 if (parsed['tests']) {
461 document.getElementById('testView').innerHTML = '';
462 this.fetchTests(parsed['tests'].split(','), doNotUpdateHash);
464 if (parsed['builder']) {
465 this.fetchFailingTestsForBuilder(
467 parseInt(parsed['builderDays']) || 5,
468 parsed['builderFailureType'] || 'failing', doNotUpdateHash);
473 function fetchManifest(callback) {
474 var xhr = new XMLHttpRequest();
475 xhr.open("GET", 'api/manifest.php', true);
476 xhr.onload = function(event) {
477 var response = JSON.parse(xhr.response);
478 if (response['status'] != 'OK') {
479 alert('Failed to load manifest:' + response['status']);
480 console.log(response);
488 fetchManifest(function (response) {
489 var testNames = response['tests'].map(function (test) { return test['name']; })
490 var input = document.getElementById('testName');
491 input.autocompleter = new Autocompleter(input, testNames);
493 var builderListView = document.getElementById('builderListView');
494 for (var builderId in response['builders'])
495 builderListView.appendChild(element('option', [text(response['builders'][builderId].name)]));
497 var builderDaysView = document.getElementById('builderDaysView');
498 var builderFailureTypeView = document.getElementById('builderFailureTypeView');
500 function updateBuilderView() {
501 if (builderListView.value)
502 TestResultsView.fetchFailingTestsForBuilder(builderListView.value, builderDaysView.value, builderFailureTypeView.value);
505 builderListView.addEventListener('change', updateBuilderView);
506 builderDaysView.addEventListener('change', updateBuilderView);
507 builderFailureTypeView.addEventListener('change', updateBuilderView);
509 function mapById(items) {
511 for (var i = 0; i < items.length; i++)
512 results[items[i].id] = items[i];
516 TestResultsView.setAvailableTests(mapById(response['tests']));
517 TestResultsView.setBuilders(mapById(response['builders']));
518 TestResultsView.setSlaves(mapById(response['slaves']));
519 TestResultsView.setRepositories(mapById(response['repositories']));
520 TestResultsView.setTestCategories(response['testCategories']);
521 // FIXME: Updating location.href shouldn't be TestResultsView's responsibility.
522 var parsedStates = TestResultsView.loadTestsFromLocationHash();
523 if (parsedStates['builder']) {
524 builderListView.value = parsedStates['builder'];
525 builderDaysView.value = parsedStates['builderDays'];
526 builderFailureTypeView.value = parsedStates['builderFailureType'];
530 function pasteHelper(input, event) {
531 function removeJunkFromNRWTStdout(input) {
532 return input.replace(/(\[[\w ]+\])|(.+\:.+)/g, '').replace(/^[ \t]+|[ \t]+$/gm, '');
535 function removeJunkFromResultsPage(input) {
536 return input.replace(/(^[^\/]+$)|(^\+)/gm, '').replace(/\([^)]+\)/g, '').replace(/\s+[A-Za-z]+(\s+[A-Za-z]+)*\s*$/gm, '');
539 var text = event.clipboardData.getData('text/plain');
540 if (text.indexOf('\n') < 0)
543 var urls = removeJunkFromResultsPage(removeJunkFromNRWTStdout(text)).split('\n');
544 TestResultsView.fetchTests(urls.filter(function (url) { return url.length; }));
545 event.preventDefault();