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 var resultsByBuilder = results['builders'];
149 for (var builderId in resultsByBuilder) {
150 var resultsByTest = resultsByBuilder[builderId];
152 for (var testId in resultsByTest)
153 results = resultsByTest[testId];
157 var builder = this._builders[builderId];
158 // FIXME: Add a master name if there is more than one.
159 table.appendChild(this._createTestResultRow(builder.name, results, builder));
161 section.appendChild(table);
164 TestResultsView._createTestResultRow = function (title, results, builder) {
166 for (var i = 0; i < results.length; i++) {
167 var result = results[i];
168 result.build = new TestBuild(this._repositories, this._builders, result);
169 result.roundedTime = result.time > 10000 ? Math.round(result.time / 1000) : Math.round(result.time / 100) / 10;
170 result.isSlow = result.time > 1000;
172 slowestTime = Math.max(slowestTime, result.roundedTime);
179 var sortedResults = results.sort(function (result1, result2) { return result2.build.time() - result1.build.time(); });
180 var cells = new Array(sortedResults.length);
181 for (var i = 0; i < sortedResults.length; i++)
182 cells[i] = this._createResultCell(builder.master, builder.name, sortedResults[i], sortedResults[i - 1]);
184 var formattedModifiers = sortedResults[0].modifiers.split(' ').map(function (modifier) {
185 if (modifier.indexOf('/') > 0)
186 return element('a', {'href': (modifier.indexOf('http') == 0 ? '' : 'http://') + modifier}, [modifier]);
190 return element('tr', [
191 element('th', ['' + title]),
192 element('td', {'class': 'modifiers'}, formattedModifiers),
193 element('td', {'class': 'expected'}, [sortedResults[0].expected]),
194 element('td', {'class': 'slowestTime'}, [slowestTime]),
195 element('td', cells)]);
198 TestResultsView.fetchTest = function (testName) {
199 if (this._tests.indexOf(testName) >= 0)
204 var closeButton = element('div', {'class': 'closeButton'});
205 closeButton.innerHTML = '<svg viewBox="0 0 100 100"><g stroke-width="10">'
206 + '<circle cx="50" cy="50" r="45" fill="transparent"></circle><polygon points="30,30 70,70"></polygon>'
207 + '<polygon points="30,70 70,30"></polygon></g></svg>';
208 closeButton.addEventListener('click', function (event) {
209 self._removeTest(testName);
210 section.parentNode.removeChild(section);
211 event.preventDefault();
213 var section = element('section', {'id': testName, 'class': 'testResults'}, [closeButton, 'Loading...']);
215 document.getElementById('testView').appendChild(section);
217 var xhr = new XMLHttpRequest();
218 xhr.open("GET", 'api/results.php?test=' + testName, true);
219 xhr.onload = function(event) {
220 var response = JSON.parse(xhr.response);
221 section.removeChild(section.lastChild); // Remove Loading...
222 if (response['status'] != 'OK') {
223 section.appendChild(text('Failed to load results for ' + testName + ': ' + response['status']));
226 self._populateTestPane(testName, response, section);
229 this._tests.push(testName);
232 TestResultsView._removeTest = function (testName) {
233 var index = this._tests.indexOf(testName);
236 this._tests.splice(index, 1);
237 this.updateLocationHash();
240 TestResultsView.fetchTests = function (testNames, doNotUpdateHash) {
241 for (var i = 0; i < testNames.length; i++)
242 this.fetchTest(testNames[i], doNotUpdateHash);
243 if (!doNotUpdateHash)
244 this.updateLocationHash();
247 TestResultsView._matchesFailureType = function (results, failureType, tn) {
250 var latestActualResult = results[0].actual;
251 var latestExpectedResult = results[0].expected;
252 switch (failureType) {
254 return results[0].actual != 'PASS';
256 var offOneChangeCount = 0;
257 for (var i = 1; i + 1 < results.length; i++) {
258 var previousActual = results[i - 1].actual;
259 var nextActual = results[i + 1].actual;
260 if (previousActual == nextActual && results[i].actual != previousActual)
263 return offOneChangeCount; // Heuristics.
264 case 'wrongexpectations':
265 if (latestExpectedResult == latestActualResult)
267 var expectedTokens = latestExpectedResult.split(' ');
268 if (expectedTokens.indexOf(latestActualResult) >= 0)
270 if (latestActualResult == 'TEXT' || latestActualResult == 'TEXT+IMAGE' && expectedTokens.indexOf('FAIL') >= 0)
278 TestResultsView._populateBuilderPane = function(builderName, failureType, results, section) {
279 var table = element('table', {'class': 'resultsTable'}, [element('caption', [builderName])]);
280 var resultsByBuilder = results['builders'];
283 for (var currentBuilderId in resultsByBuilder) {
284 builderId = currentBuilderId;
285 resultsByTests = resultsByBuilder[builderId];
290 var builder = this._builders[builderId];
291 for (var testId in resultsByTests) {
292 var results = resultsByTests[testId];
293 if (!results.length || !this._matchesFailureType(results, failureType, this._availableTests[testId].name))
295 table.appendChild(this._createTestResultRow(this._availableTests[testId].name, resultsByTests[testId], builder));
297 section.appendChild(table);
300 TestResultsView.fetchFailingTestsForBuilder = function (builderName, numberOfDays, failureType, doNotUpdateHash) {
301 var section = element('section', {'class': 'testResults'}, ['Loading...']);
303 var container = document.getElementById('builderFailingTestsView');
304 container.innerHTML = '';
305 container.appendChild(section);
308 var xhr = new XMLHttpRequest();
309 xhr.open("GET", 'api/failing-tests.php?builder=' + escape(builderName) + '&days=' + numberOfDays, true);
310 xhr.onload = function(event) {
311 var response = JSON.parse(xhr.response);
312 section.innerHTML = '';
313 if (response['status'] != 'OK') {
314 section.appendChild(text('Failed to load results for ' + builderName + ': ' + response['status']));
317 // FIXME: Normalize failureType.
318 self._currentBuilder = builderName;
319 self._currentBuilderFailureType = failureType;
320 self._currentBuilderDays = numberOfDays;
321 self._populateBuilderPane(builderName, failureType, response, section);
322 if (!doNotUpdateHash)
323 self.updateLocationHash();
328 TestResultsView.updateLocationHash = function () {
330 'tests': this._tests.join(','),
331 'builder': this._currentBuilder,
332 'builderFailureType': this._currentBuilderFailureType,
333 'builderDays': this._currentBuilderDays,
336 for (var key in params) {
337 var value = params[key];
338 if (value === null || value === undefined)
340 hash += '&' + decodeURIComponent(key) + '=' + decodeURIComponent(value);
342 location.hash = hash;
343 this._oldHash = location.hash;
346 TestResultsView.locationHashChanged = function () {
347 var newHash = location.hash;
348 if (newHash == this._oldHash)
350 this._oldHash = newHash;
352 this.loadTestsFromLocationHash();
355 TestResultsView.loadTestsFromLocationHash = function () {
357 location.hash.substr(1).split('&').forEach(function (component) {
358 var equalPosition = component.indexOf('=');
359 if (equalPosition < 0)
361 var name = decodeURIComponent(component.substr(0, equalPosition));
362 parsed[name] = decodeURIComponent(component.substr(equalPosition + 1));
364 var doNotUpdateHash = true;
365 if (parsed['tests']) {
366 document.getElementById('testView').innerHTML = '';
367 this.fetchTests(parsed['tests'].split(','), doNotUpdateHash);
369 if (parsed['builder']) {
370 this.fetchFailingTestsForBuilder(
372 parseInt(parsed['builderDays']) || 5,
373 parsed['builderFailureType'] || 'failing', doNotUpdateHash);
378 function fetchManifest(callback) {
379 var xhr = new XMLHttpRequest();
380 xhr.open("GET", 'api/manifest.php', true);
381 xhr.onload = function(event) {
382 var response = JSON.parse(xhr.response);
383 if (response['status'] != 'OK') {
384 alert('Failed to load manifest:' + response['status']);
385 console.log(response);
393 fetchManifest(function (response) {
394 var testNames = response['tests'].map(function (test) { return test['name']; })
395 var input = document.getElementById('testName');
396 input.autocompleter = new Autocompleter(input, testNames);
398 var builderListView = document.getElementById('builderListView');
399 for (var builderId in response['builders'])
400 builderListView.appendChild(element('option', [text(response['builders'][builderId].name)]));
402 var builderDaysView = document.getElementById('builderDaysView');
403 var builderFailureTypeView = document.getElementById('builderFailureTypeView');
405 function updateBuilderView() {
406 if (builderListView.value)
407 TestResultsView.fetchFailingTestsForBuilder(builderListView.value, builderDaysView.value, builderFailureTypeView.value);
410 builderListView.addEventListener('change', updateBuilderView);
411 builderDaysView.addEventListener('change', updateBuilderView);
412 builderFailureTypeView.addEventListener('change', updateBuilderView);
414 function mapById(items) {
416 for (var i = 0; i < items.length; i++)
417 results[items[i].id] = items[i];
421 TestResultsView.setAvailableTests(mapById(response['tests']));
422 TestResultsView.setBuilders(mapById(response['builders']));
423 TestResultsView.setSlaves(mapById(response['slaves']));
424 TestResultsView.setRepositories(mapById(response['repositories']));
425 // FIXME: Updating location.href shouldn't be TestResultsView's responsibility.
426 var parsedStates = TestResultsView.loadTestsFromLocationHash();
427 if (parsedStates['builder']) {
428 builderListView.value = parsedStates['builder'];
429 builderDaysView.value = parsedStates['builderDays'];
430 builderFailureTypeView.value = parsedStates['builderFailureType'];
434 function pasteHelper(input, event) {
435 function removeJunkFromNRWTStdout(input) {
436 return input.replace(/(\[[\w ]+\])|(.+\:.+)/g, '').replace(/^[ \t]+|[ \t]+$/gm, '');
439 function removeJunkFromResultsPage(input) {
440 return input.replace(/(^[^\/]+$)|(^\+)/gm, '').replace(/\([^)]+\)/g, '').replace(/\s+[A-Za-z]+(\s+[A-Za-z]+)*\s*$/gm, '');
443 var text = event.clipboardData.getData('text/plain');
444 if (text.indexOf('\n') < 0)
447 var urls = removeJunkFromResultsPage(removeJunkFromNRWTStdout(text)).split('\n');
448 TestResultsView.fetchTests(urls.filter(function (url) { return url.length; }));
449 event.preventDefault();