4 <title>WebKit Test Results</title>
5 <link rel="stylesheet" href="common.css">
6 <link rel="stylesheet" href="main.css">
7 <script src="js/autocompleter.js"></script>
8 <script src="js/build.js"></script>
9 <script src="js/dom.js"></script>
14 <h1><a href="/">WebKit Test Results</a></h1>
16 <li><a href="http://build.webkit.org/waterfall">Waterfall</a></li>
20 <form onsubmit="TestResultsView.fetchTest(this['testName'].value);
21 TestResultsView.updateLocationHash();
23 <input id="testName" type="text" size="150" onpaste="pasteHelper(this, event)"
24 placeholder="Type in a test name, or copy and paste test names on results.html or NRWT stdout (including junks)"></form>
25 <div id="testView"></div>
29 <label>tests <select id="builderFailureTypeView"><option>failing</option><option>flaky</option><option value="wrongexpectations">has wrong expectations</option></select></label>
30 <label>on <select id="builderListView"><option value="">Select builder</option></select></label>
31 <label for="builderDaysView">in the last <select id="builderDaysView"><option>5</option><option>15</option><option>30</option></select> days</label>
33 <div id="builderFailingTestsView"></div>
35 <div id="tooltipContainer"></div>
39 var TestResultsView = new (function () {
40 var tooltipContainer = document.getElementById('tooltipContainer');
42 tooltipContainer.onclick = function (event) { event.insideTooltip = true; }
43 document.addEventListener('click', function (event) {
44 if (!event.insideTooltip)
45 tooltipContainer.style.display = 'none';
48 window.addEventListener('hashchange', function (event) {
49 TestResultsView.locationHashChanged();
52 this._tooltipContainer = tooltipContainer;
54 this._currentBuilder = null;
55 this._currentBuilderFailureType = null;
56 this._currentBuilderDays = null;
60 this._repositories = [];
63 TestResultsView.setAvailableTests = function (availableTests) {
64 this._availableTests = availableTests;
67 TestResultsView.setBuilders = function (builders) {
68 this._builders = builders;
71 TestResultsView.setSlaves = function (slaves) {
72 this._slaves = slaves;
75 TestResultsView.setRepositories = function (repositories) {
76 this._repositories = repositories;
79 TestResultsView.showTooltip = function (anchor, contentElement) {
80 var tooltipContainer = this._tooltipContainer;
81 tooltipContainer.style.display = null;
83 while (tooltipContainer.firstChild)
84 tooltipContainer.removeChild(tooltipContainer.firstChild);
85 tooltipContainer.appendChild(contentElement);
87 var position = {x: 0, y: 0};
88 var currentNode = anchor;
90 position.x += currentNode.offsetLeft;
91 position.y += currentNode.offsetTop;
92 currentNode = currentNode.offsetParent;
94 tooltipContainer.style.left = (position.x - contentElement.offsetWidth / 2 + anchor.offsetWidth / 2) + 'px';
95 tooltipContainer.style.top = (position.y - contentElement.clientHeight - 5) + 'px';
98 TestResultsView._urlFromBuilder = function (urlType, master, builder, revision, build) {
99 // FIXME: We should probably make this configurable or fetch from buildbot configuraration.
101 "build": "http://$master/builders/$builder/builds/$build",
102 "result": "http://$master/results/$builder/r$revision%20($build)/results.html"
103 }[urlType].replace(/\$master/g, master).replace(/\$builder/g, builder)
104 .replace(/\$revision/g, revision).replace(/\$build/g, build);
107 TestResultsView._createResultCell = function (master, builder, result, previousResult) {
108 var buildTime = result['buildTime'];
109 var revision = result['revision'];
110 var slave = result['slave'];
111 var build = result['buildNumber'];
112 var actual = result['actual'];
113 var expected = result['expected'];
114 var timeIfSlow = result.isSlow ? result.roundedTime : '';
115 var anchor = element('a', {'href': this._urlFromBuilder('result', master, builder, revision, build)}, [timeIfSlow]);
116 anchor.onmouseenter = function () {
117 var repositoryById = TestResultsView._repositories;
118 var formattedRevisions = result.build.formattedRevisions(previousResult ? previousResult.build : null);
119 var revisionDescription = '';
120 for (var repositoryName in formattedRevisions) {
121 var revision = formattedRevisions[repositoryName];
122 if (revisionDescription)
123 revisionDescription += ', ';
125 revisionDescription += repositoryName + ': ' + revision.url;
127 revisionDescription += revision.label;
130 TestResultsView.showTooltip(anchor, element('div', {'class': 'tooltip'}, [
132 element('li', ['Build Time: ' + result.build.formattedBuildTime()]),
133 element('li', ['Revision: ' + revisionDescription]),
134 element('li', ['Build: ', element('a', {'href': TestResultsView._urlFromBuilder('build', master, builder, revision, build)}, [build])]),
135 element('li', ['Actual: ' + actual]),
136 element('li', ['Expected: ' + expected]),
140 var cell = element('span', {
141 'class': actual + ' resultCell',
146 TestResultsView._populateTestPane = function(testName, results, section) {
147 var table = element('table', {'class': 'resultsTable'}, [element('caption', [testName])]);
148 table.appendChild(this._createTestResultHeader('Builder'));
150 var resultsByBuilder = results['builders'];
151 var buildTimes = new Array();
152 for (var builderId in resultsByBuilder) {
153 var results = resultsByBuilder[builderId];
154 this._createBuildsAndComputeSlownessOfResults(results);
155 for (var i = 0; i < results.length; i++) {
156 var time = results[i].build.time();
157 if (buildTimes.indexOf(time) < 0)
158 buildTimes.push(time);
161 buildTimes.sort(function (a, b) { return b - a; });
163 for (var builderId in resultsByBuilder) {
164 var builder = this._builders[builderId];
165 // FIXME: Add a master name if there is more than one.
166 table.appendChild(this._createTestResultRow(builder.name, resultsByBuilder[builderId], builder, buildTimes));
168 section.appendChild(table);
171 TestResultsView._createTestResultHeader = function (labelForFirstColumn) {
172 return element('thead', [element('tr', [
173 element('th', [labelForFirstColumn]),
174 element('th', ['Bug']),
175 element('th', ['Expectations']),
176 element('th', ['Slowest'])])]);
179 TestResultsView._createBuildsAndComputeSlownessOfResults = function (results) {
180 for (var i = 0; i < results.length; i++) {
181 var result = results[i];
182 result.build = new TestBuild(this._repositories, this._builders, result);
183 result.roundedTime = result.time > 10000 ? Math.round(result.time / 1000) : Math.round(result.time / 100) / 10;
184 result.isSlow = result.time > 1000;
188 TestResultsView._createTestResultRow = function (title, results, builder, buildTimes) {
189 var sortedResults = results.sort(function (result1, result2) { return result2.build.time() - result1.build.time(); });
190 var cells = new Array();
192 function addEmptyCell() { cells.push(element('span', {'class': 'resultCell'}, [element('a')])); }
194 var buildTimeIndex = 0;
195 for (var i = 0; i < sortedResults.length; i++) {
196 var result = sortedResults[i];
198 while (buildTimes[buildTimeIndex] > result.build.time()) {
202 if (buildTimes[buildTimeIndex] == result.build.time())
205 cells.push(this._createResultCell(builder.master, builder.name, result, sortedResults[i - 1]));
208 while (buildTimeIndex < buildTimes.length) {
214 var formattedModifiers = sortedResults[0].modifiers.split(' ').map(function (modifier) {
215 if (modifier.indexOf('/') > 0)
216 return element('a', {'href': (modifier.indexOf('http') == 0 ? '' : 'http://') + modifier}, [modifier]);
220 var slowestTime = Math.max.apply(Math, results.map(function (result) { return result.roundedTime; }));
221 if (slowestTime >= 1)
226 return element('tr', [
227 element('th', ['' + title]),
228 element('td', {'class': 'modifiers'}, formattedModifiers),
229 element('td', {'class': 'expected'}, [sortedResults[0].expected]),
230 element('td', {'class': 'slowestTime'}, [slowestTime]),
231 element('td', cells)]);
234 TestResultsView.fetchTest = function (testName) {
235 if (this._tests.indexOf(testName) >= 0)
240 var closeButton = element('div', {'class': 'closeButton'});
241 closeButton.innerHTML = '<svg viewBox="0 0 100 100"><g stroke-width="10">'
242 + '<circle cx="50" cy="50" r="45" fill="transparent"></circle><polygon points="30,30 70,70"></polygon>'
243 + '<polygon points="30,70 70,30"></polygon></g></svg>';
244 closeButton.addEventListener('click', function (event) {
245 self._removeTest(testName);
246 section.parentNode.removeChild(section);
247 event.preventDefault();
249 var section = element('section', {'id': testName, 'class': 'testResults'}, [closeButton, 'Loading...']);
251 document.getElementById('testView').appendChild(section);
253 var xhr = new XMLHttpRequest();
254 xhr.open("GET", 'api/results.php?test=' + testName, true);
255 xhr.onload = function(event) {
256 var response = JSON.parse(xhr.response);
257 section.removeChild(section.lastChild); // Remove Loading...
258 if (response['status'] != 'OK') {
259 section.appendChild(text('Failed to load results for ' + testName + ': ' + response['status']));
262 self._populateTestPane(testName, response, section);
265 this._tests.push(testName);
268 TestResultsView._removeTest = function (testName) {
269 var index = this._tests.indexOf(testName);
272 this._tests.splice(index, 1);
273 this.updateLocationHash();
276 TestResultsView.fetchTests = function (testNames, doNotUpdateHash) {
277 for (var i = 0; i < testNames.length; i++)
278 this.fetchTest(testNames[i], doNotUpdateHash);
279 if (!doNotUpdateHash)
280 this.updateLocationHash();
283 TestResultsView._matchesFailureType = function (results, failureType, tn) {
286 var latestActualResult = results[0].actual;
287 var latestExpectedResult = results[0].expected;
288 switch (failureType) {
290 return results[0].actual != 'PASS';
292 var offOneChangeCount = 0;
293 for (var i = 1; i + 1 < results.length; i++) {
294 var previousActual = results[i - 1].actual;
295 var nextActual = results[i + 1].actual;
296 if (previousActual == nextActual && results[i].actual != previousActual)
299 return offOneChangeCount; // Heuristics.
300 case 'wrongexpectations':
301 if (latestExpectedResult == latestActualResult)
303 var expectedTokens = latestExpectedResult.split(' ');
304 if (expectedTokens.indexOf(latestActualResult) >= 0)
306 if (latestActualResult == 'TEXT' || latestActualResult == 'TEXT+IMAGE' && expectedTokens.indexOf('FAIL') >= 0)
314 TestResultsView._populateBuilderPane = function(builderName, failureType, results, section) {
315 var table = element('table', {'class': 'resultsTable'}, [element('caption', [builderName])]);
316 var resultsByBuilder = results['builders'];
319 for (var currentBuilderId in resultsByBuilder) {
320 builderId = currentBuilderId;
321 resultsByTests = resultsByBuilder[builderId];
326 table.appendChild(this._createTestResultHeader('Test'));
328 var builder = this._builders[builderId];
329 for (var testId in resultsByTests) {
330 var results = resultsByTests[testId];
331 if (!results.length || !this._matchesFailureType(results, failureType, this._availableTests[testId].name))
333 this._createBuildsAndComputeSlownessOfResults(resultsByTests[testId]);
334 table.appendChild(this._createTestResultRow(this._availableTests[testId].name, resultsByTests[testId], builder));
336 section.appendChild(table);
339 TestResultsView.fetchFailingTestsForBuilder = function (builderName, numberOfDays, failureType, doNotUpdateHash) {
340 var section = element('section', {'class': 'testResults'}, ['Loading...']);
342 var container = document.getElementById('builderFailingTestsView');
343 container.innerHTML = '';
344 container.appendChild(section);
347 var xhr = new XMLHttpRequest();
348 xhr.open("GET", 'api/failing-tests.php?builder=' + escape(builderName) + '&days=' + numberOfDays, true);
349 xhr.onload = function(event) {
350 var response = JSON.parse(xhr.response);
351 section.innerHTML = '';
352 if (response['status'] != 'OK') {
353 section.appendChild(text('Failed to load results for ' + builderName + ': ' + response['status']));
356 // FIXME: Normalize failureType.
357 self._currentBuilder = builderName;
358 self._currentBuilderFailureType = failureType;
359 self._currentBuilderDays = numberOfDays;
360 self._populateBuilderPane(builderName, failureType, response, section);
361 if (!doNotUpdateHash)
362 self.updateLocationHash();
367 TestResultsView.updateLocationHash = function () {
369 'tests': this._tests.join(','),
370 'builder': this._currentBuilder,
371 'builderFailureType': this._currentBuilderFailureType,
372 'builderDays': this._currentBuilderDays,
375 for (var key in params) {
376 var value = params[key];
377 if (value === null || value === undefined)
379 hash += '&' + decodeURIComponent(key) + '=' + decodeURIComponent(value);
381 location.hash = hash;
382 this._oldHash = location.hash;
385 TestResultsView.locationHashChanged = function () {
386 var newHash = location.hash;
387 if (newHash == this._oldHash)
389 this._oldHash = newHash;
391 this.loadTestsFromLocationHash();
394 TestResultsView.loadTestsFromLocationHash = function () {
396 location.hash.substr(1).split('&').forEach(function (component) {
397 var equalPosition = component.indexOf('=');
398 if (equalPosition < 0)
400 var name = decodeURIComponent(component.substr(0, equalPosition));
401 parsed[name] = decodeURIComponent(component.substr(equalPosition + 1));
403 var doNotUpdateHash = true;
404 if (parsed['tests']) {
405 document.getElementById('testView').innerHTML = '';
406 this.fetchTests(parsed['tests'].split(','), doNotUpdateHash);
408 if (parsed['builder']) {
409 this.fetchFailingTestsForBuilder(
411 parseInt(parsed['builderDays']) || 5,
412 parsed['builderFailureType'] || 'failing', doNotUpdateHash);
417 function fetchManifest(callback) {
418 var xhr = new XMLHttpRequest();
419 xhr.open("GET", 'api/manifest.php', true);
420 xhr.onload = function(event) {
421 var response = JSON.parse(xhr.response);
422 if (response['status'] != 'OK') {
423 alert('Failed to load manifest:' + response['status']);
424 console.log(response);
432 fetchManifest(function (response) {
433 var testNames = response['tests'].map(function (test) { return test['name']; })
434 var input = document.getElementById('testName');
435 input.autocompleter = new Autocompleter(input, testNames);
437 var builderListView = document.getElementById('builderListView');
438 for (var builderId in response['builders'])
439 builderListView.appendChild(element('option', [text(response['builders'][builderId].name)]));
441 var builderDaysView = document.getElementById('builderDaysView');
442 var builderFailureTypeView = document.getElementById('builderFailureTypeView');
444 function updateBuilderView() {
445 if (builderListView.value)
446 TestResultsView.fetchFailingTestsForBuilder(builderListView.value, builderDaysView.value, builderFailureTypeView.value);
449 builderListView.addEventListener('change', updateBuilderView);
450 builderDaysView.addEventListener('change', updateBuilderView);
451 builderFailureTypeView.addEventListener('change', updateBuilderView);
453 function mapById(items) {
455 for (var i = 0; i < items.length; i++)
456 results[items[i].id] = items[i];
460 TestResultsView.setAvailableTests(mapById(response['tests']));
461 TestResultsView.setBuilders(mapById(response['builders']));
462 TestResultsView.setSlaves(mapById(response['slaves']));
463 TestResultsView.setRepositories(mapById(response['repositories']));
464 // FIXME: Updating location.href shouldn't be TestResultsView's responsibility.
465 var parsedStates = TestResultsView.loadTestsFromLocationHash();
466 if (parsedStates['builder']) {
467 builderListView.value = parsedStates['builder'];
468 builderDaysView.value = parsedStates['builderDays'];
469 builderFailureTypeView.value = parsedStates['builderFailureType'];
473 function pasteHelper(input, event) {
474 function removeJunkFromNRWTStdout(input) {
475 return input.replace(/(\[[\w ]+\])|(.+\:.+)/g, '').replace(/^[ \t]+|[ \t]+$/gm, '');
478 function removeJunkFromResultsPage(input) {
479 return input.replace(/(^[^\/]+$)|(^\+)/gm, '').replace(/\([^)]+\)/g, '').replace(/\s+[A-Za-z]+(\s+[A-Za-z]+)*\s*$/gm, '');
482 var text = event.clipboardData.getData('text/plain');
483 if (text.indexOf('\n') < 0)
486 var urls = removeJunkFromResultsPage(removeJunkFromNRWTStdout(text)).split('\n');
487 TestResultsView.fetchTests(urls.filter(function (url) { return url.length; }));
488 event.preventDefault();