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>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>
36 <div id="builderFailingTestsView"></div>
39 <div id="tooltipContainer"></div>
43 var TestResultsView = new (function () {
44 var tooltipContainer = document.getElementById('tooltipContainer');
46 tooltipContainer.onclick = function (event) { event.insideTooltip = true; }
47 document.addEventListener('click', function (event) {
48 if (!event.insideTooltip)
49 tooltipContainer.style.display = 'none';
52 window.addEventListener('hashchange', function (event) {
53 TestResultsView.locationHashChanged();
56 this._tooltipContainer = tooltipContainer;
58 this._currentBuilder = null;
59 this._currentBuilderFailureType = null;
60 this._currentBuilderDays = null;
64 this._repositories = {};
65 this._testCategories = {};
68 TestResultsView.setAvailableTests = function (availableTests) {
69 this._availableTests = availableTests;
72 TestResultsView.setBuilders = function (builders) {
73 this._builders = builders;
76 TestResultsView.setSlaves = function (slaves) {
77 this._slaves = slaves;
80 TestResultsView.setRepositories = function (repositories) {
81 this._repositories = repositories;
84 TestResultsView.setTestCategories = function (testCategories) {
85 this._testCategories = testCategories;
88 TestResultsView.showTooltip = function (anchor, contentElement) {
89 var tooltipContainer = this._tooltipContainer;
90 tooltipContainer.style.display = null;
92 while (tooltipContainer.firstChild)
93 tooltipContainer.removeChild(tooltipContainer.firstChild);
94 tooltipContainer.appendChild(contentElement);
96 var position = {x: 0, y: 0};
97 var currentNode = anchor;
99 position.x += currentNode.offsetLeft;
100 position.y += currentNode.offsetTop;
101 currentNode = currentNode.offsetParent;
103 tooltipContainer.style.left = (position.x - contentElement.offsetWidth / 2 + anchor.offsetWidth / 2) + 'px';
104 tooltipContainer.style.top = (position.y - contentElement.clientHeight - 5) + 'px';
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 : '';
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;
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")';
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(', ');
135 revisionDescription.push(element('a', {'href': revision.url}, [revision.label]));
137 revisionDescription.push(revision.label);
140 TestResultsView.showTooltip(anchor, element('div', {'class': 'tooltip'}, [
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]),
150 var cell = element('span', {
151 'class': actual + ' resultCell',
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)])]);
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);
171 buildTimes.sort(function (a, b) { return b - a; });
173 var repositories = [];
174 for (var repositoryId in this._repositories)
175 repositories.push(this._repositories[repositoryId]);
177 table.appendChild(this._createTestResultHeader('Builder', repositories));
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));
185 table.appendChild(tbody);
186 section.appendChild(table);
187 $(table).tablesorter();
190 TestResultsView._urlFromTest = function (test) {
191 var category = this._testCategories[test.category];
194 return category.url.replace(/\$testName/g, test.name);
197 TestResultsView._linkifiedTestName = function (test) {
198 var url = this._urlFromTest(test);
199 return url ? element('a', {'href': url}, [test.name]) : test.name;
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')]))]);
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;
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();
226 function addEmptyCell() { cells.push(element('span', {'class': 'resultCell'}, [element('a')])); }
228 var buildTimeIndex = 0;
229 for (var i = 0; i < sortedResults.length; i++) {
230 var result = sortedResults[i];
232 while (buildTimes[buildTimeIndex] > result.build.time()) {
236 if (buildTimes[buildTimeIndex] == result.build.time())
239 cells.push(this._createResultCell(builder.master, builder.name, result, sortedResults[i - 1]));
242 while (buildTimeIndex < buildTimes.length) {
248 var seenBugLink = false;
249 var formattedModifiers = sortedResults[0].modifiers.split(' ').map(function (modifier) {
250 if (modifier.indexOf('/') > 0) {
252 return element('a', {'href': (modifier.indexOf('http') == 0 ? '' : 'http://') + modifier}, [modifier]);
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'},
264 var slowestTime = Math.max.apply(Math, results.map(function (result) { return result.roundedTime; }));
265 if (slowestTime >= 1)
270 var formattedRevisionCells = [];
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]));
278 formattedRevisionCells.push(revisionInfo.label);
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)]));
291 TestResultsView.fetchTest = function (testName) {
292 if (this._tests.indexOf(testName) >= 0)
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();
306 var section = element('section', {'id': testName, 'class': 'testResults'}, [closeButton, 'Loading...']);
308 document.getElementById('testView').appendChild(section);
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']));
319 self._populateTestPane(testName, response, section);
322 this._tests.push(testName);
325 TestResultsView._removeTest = function (testName) {
326 var index = this._tests.indexOf(testName);
329 this._tests.splice(index, 1);
330 this.updateLocationHash();
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();
340 TestResultsView._matchesFailureType = function (results, failureType, tn) {
344 var latestActualResult = results[0].actual;
345 var latestExpectedResult = results[0].expected;
346 switch (failureType) {
348 return results[0].actual != 'PASS';
350 var offOneChangeCount = 0;
351 for (var i = 1; i + 1 < results.length; i++) {
352 var previousActual = results[i - 1].actual;
353 var nextActual = results[i + 1].actual;
354 if (previousActual == nextActual && results[i].actual != previousActual)
357 return offOneChangeCount; // Heuristics.
358 case 'wrongexpectations':
359 if (latestExpectedResult == latestActualResult)
361 var expectedTokens = latestExpectedResult.split(' ');
362 if (expectedTokens.indexOf(latestActualResult) >= 0)
364 if (latestActualResult == 'TEXT' || latestActualResult == 'TEXT+IMAGE' && expectedTokens.indexOf('FAIL') >= 0)
372 TestResultsView._populateBuilderPane = function(builderName, failureType, results, section) {
373 var table = element('table', {'class': 'resultsTable tablesorter'}, [element('caption', [builderName])]);
374 var resultsByBuilder = results['builders'];
377 for (var currentBuilderId in resultsByBuilder) {
378 builderId = currentBuilderId;
379 resultsByTests = resultsByBuilder[builderId];
384 table.appendChild(this._createTestResultHeader('Test'));
386 var tbody = element('tbody');
387 var builder = this._builders[builderId];
389 for (var testId in resultsByTests) {
390 var results = resultsByTests[testId];
391 if (!results.length || !this._matchesFailureType(results, failureType, this._availableTests[testId].name))
393 this._createBuildsAndComputeSlownessOfResults(builderId, resultsByTests[testId]);
394 var test = this._availableTests[testId];
395 var externalTestLink = element('a', {'class': 'externalTestLink', 'href': this._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(this._createTestResultRow(element('span', [testName, externalTestLink]), resultsByTests[testId], 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 xhr.open("GET", 'api/failing-tests.php?builder=' + escape(builderName) + '&days=' + numberOfDays + '&failureType=' + failureType, true);
421 xhr.onload = function(event) {
422 var response = JSON.parse(xhr.response);
423 section.innerHTML = '';
424 if (response['status'] != 'OK') {
425 section.appendChild(text('Failed to load results for ' + builderName + ': ' + response['status']));
428 // FIXME: Normalize failureType.
429 self._currentBuilder = builderName;
430 self._currentBuilderFailureType = failureType;
431 self._currentBuilderDays = numberOfDays;
432 self._populateBuilderPane(builderName, failureType, response, section);
433 if (!doNotUpdateHash)
434 self.updateLocationHash();
439 TestResultsView._createLocationHash = function (tests) {
441 'tests': tests.join(','),
442 'builder': this._currentBuilder,
443 'builderFailureType': this._currentBuilderFailureType,
444 'builderDays': this._currentBuilderDays,
447 for (var key in params) {
448 var value = params[key];
449 if (value === null || value === undefined)
451 hash += '&' + decodeURIComponent(key) + '=' + decodeURIComponent(value);
456 TestResultsView.updateLocationHash = function () {
457 location.hash = this._createLocationHash(this._tests);
458 this._oldHash = location.hash;
461 TestResultsView.locationHashChanged = function () {
462 var newHash = location.hash;
463 if (newHash == this._oldHash)
465 this._oldHash = newHash;
467 this.loadTestsFromLocationHash();
470 TestResultsView.loadTestsFromLocationHash = function () {
472 location.hash.substr(1).split('&').forEach(function (component) {
473 var equalPosition = component.indexOf('=');
474 if (equalPosition < 0)
476 var name = decodeURIComponent(component.substr(0, equalPosition));
477 parsed[name] = decodeURIComponent(component.substr(equalPosition + 1));
479 var doNotUpdateHash = true;
480 if (parsed['tests']) {
481 document.getElementById('testView').innerHTML = '';
482 this.fetchTests(parsed['tests'].split(','), doNotUpdateHash);
484 if (parsed['builder']) {
485 this.fetchFailingTestsForBuilder(
487 parseInt(parsed['builderDays']) || 5,
488 parsed['builderFailureType'] || 'failing', doNotUpdateHash);
493 function fetchManifest(callback) {
494 var xhr = new XMLHttpRequest();
495 xhr.open("GET", 'api/manifest.php', true);
496 xhr.onload = function(event) {
497 var response = JSON.parse(xhr.response);
498 if (response['status'] != 'OK') {
499 alert('Failed to load manifest:' + response['status']);
500 console.log(response);
508 fetchManifest(function (response) {
509 var testNames = response['tests'].map(function (test) { return test['name']; })
510 var input = document.getElementById('testName');
511 input.autocompleter = new Autocompleter(input, testNames);
513 var builderListView = document.getElementById('builderListView');
514 for (var builderId in response['builders'])
515 builderListView.appendChild(element('option', [text(response['builders'][builderId].name)]));
517 var builderDaysView = document.getElementById('builderDaysView');
518 var builderFailureTypeView = document.getElementById('builderFailureTypeView');
520 function updateBuilderView() {
521 if (builderListView.value)
522 TestResultsView.fetchFailingTestsForBuilder(builderListView.value, builderDaysView.value, builderFailureTypeView.value);
525 builderListView.addEventListener('change', updateBuilderView);
526 builderDaysView.addEventListener('change', updateBuilderView);
527 builderFailureTypeView.addEventListener('change', updateBuilderView);
529 function mapById(items) {
531 for (var i = 0; i < items.length; i++)
532 results[items[i].id] = items[i];
536 TestResultsView.setAvailableTests(mapById(response['tests']));
537 TestResultsView.setBuilders(mapById(response['builders']));
538 TestResultsView.setSlaves(mapById(response['slaves']));
539 TestResultsView.setRepositories(mapById(response['repositories']));
540 TestResultsView.setTestCategories(response['testCategories']);
541 // FIXME: Updating location.href shouldn't be TestResultsView's responsibility.
542 var parsedStates = TestResultsView.loadTestsFromLocationHash();
543 if (parsedStates['builder']) {
544 builderListView.value = parsedStates['builder'];
545 builderDaysView.value = parsedStates['builderDays'];
546 builderFailureTypeView.value = parsedStates['builderFailureType'];
550 function pasteHelper(input, event) {
551 function removeJunkFromNRWTStdout(input) {
552 return input.replace(/(\[[\w ]+\])|(.+\:.+)/g, '').replace(/^[ \t]+|[ \t]+$/gm, '');
555 function removeJunkFromResultsPage(input) {
556 return input.replace(/(^[^\/]+$)|(^\+)/gm, '').replace(/\([^)]+\)/g, '').replace(/\s+[A-Za-z]+(\s+[A-Za-z]+)*\s*$/gm, '');
559 var text = event.clipboardData.getData('text/plain');
560 if (text.indexOf('\n') < 0)
563 var urls = removeJunkFromResultsPage(removeJunkFromNRWTStdout(text)).split('\n');
564 TestResultsView.fetchTests(urls.filter(function (url) { return url.length; }));
565 event.preventDefault();