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>
28 <label>Show failing tests for</label>
29 <select id="builderListView"><option value="">Select builder</option></select>
30 <select id="builderDaysView"><option>5</option><option>15</option><option>30</option></select>
32 <div id="builderFailingTestsView"></div>
34 <div id="tooltipContainer"></div>
38 var TestResultsView = new (function () {
39 var tooltipContainer = document.getElementById('tooltipContainer');
41 tooltipContainer.onclick = function (event) { event.insideTooltip = true; }
42 document.addEventListener('click', function (event) {
43 if (!event.insideTooltip)
44 tooltipContainer.style.display = 'none';
47 window.addEventListener('hashchange', function (event) {
48 TestResultsView.locationHashChanged();
51 this._tooltipContainer = tooltipContainer;
53 this._currentBuilder = null;
57 this._repositories = [];
60 TestResultsView.setAvailableTests = function (availableTests) {
61 this._availableTests = availableTests;
64 TestResultsView.setBuilders = function (builders) {
65 this._builders = builders;
68 TestResultsView.setSlaves = function (slaves) {
69 this._slaves = slaves;
72 TestResultsView.setRepositories = function (repositories) {
73 this._repositories = repositories;
76 TestResultsView.showTooltip = function (anchor, contentElement) {
77 var tooltipContainer = this._tooltipContainer;
78 tooltipContainer.style.display = null;
80 while (tooltipContainer.firstChild)
81 tooltipContainer.removeChild(tooltipContainer.firstChild);
82 tooltipContainer.appendChild(contentElement);
84 var position = {x: 0, y: 0};
85 var currentNode = anchor;
87 position.x += currentNode.offsetLeft;
88 position.y += currentNode.offsetTop;
89 currentNode = currentNode.offsetParent;
91 tooltipContainer.style.left = (position.x - contentElement.offsetWidth / 2 + anchor.offsetWidth / 2) + 'px';
92 tooltipContainer.style.top = (position.y - contentElement.clientHeight - 5) + 'px';
95 TestResultsView._urlFromBuilder = function (urlType, master, builder, revision, build) {
96 // FIXME: We should probably make this configurable or fetch from buildbot configuraration.
98 "build": "http://$master/builders/$builder/builds/$build",
99 "result": "http://$master/results/$builder/r$revision%20($build)/results.html"
100 }[urlType].replace(/\$master/g, master).replace(/\$builder/g, builder)
101 .replace(/\$revision/g, revision).replace(/\$build/g, build);
104 TestResultsView._createResultCell = function (master, builder, result, previousResult) {
105 var buildTime = result['buildTime'];
106 var revision = result['revision'];
107 var slave = result['slave'];
108 var build = result['buildNumber'];
109 var actual = result['actual'];
110 var expected = result['expected'];
111 var anchor = element('a', {'href': this._urlFromBuilder('result', master, builder, revision, build)});
112 anchor.onmouseenter = function () {
113 var repositoryById = TestResultsView._repositories;
114 var formattedRevisions = result.build.formattedRevisions(previousResult ? previousResult.build : null);
115 var revisionDescription = '';
116 for (var repositoryName in formattedRevisions) {
117 var revision = formattedRevisions[repositoryName];
118 if (revisionDescription)
119 revisionDescription += ', ';
121 revisionDescription += repositoryName + ': ' + revision.url;
123 revisionDescription += revision.label;
126 TestResultsView.showTooltip(anchor, element('div', {'class': 'tooltip'}, [
128 element('li', ['Build Time: ' + result.build.formattedBuildTime()]),
129 element('li', ['Revision: ' + revisionDescription]),
130 element('li', ['Build: ', element('a', {'href': TestResultsView._urlFromBuilder('build', master, builder, revision, build)}, [build])]),
131 element('li', ['Result: ' + actual]),
135 var cell = element('span', {
136 'class': actual + ' resultCell',
141 TestResultsView._populateTestPane = function(testName, results, section) {
142 var table = element('table', {'class': 'resultsTable'}, [element('caption', [testName])]);
143 var resultsByBuilder = results['builders'];
144 for (var builderId in resultsByBuilder) {
145 var resultsByTest = resultsByBuilder[builderId];
147 for (var testId in resultsByTest)
148 results = resultsByTest[testId];
152 var builder = this._builders[builderId];
153 // FIXME: Add a master name if there is more than one.
154 table.appendChild(this._createTestResultRow(builder.name, results, builder));
156 section.appendChild(table);
159 TestResultsView._createTestResultRow = function (title, results, builder) {
160 for (var i = 0; i < results.length; i++)
161 results[i].build = new TestBuild(this._repositories, this._builders, results[i]);
163 var sortedResults = results.sort(function (result1, result2) { return result1.build.time() - result2.build.time(); });
164 var cells = new Array(sortedResults.length);
165 for (var i = 0; i < sortedResults.length; i++)
166 cells[i] = this._createResultCell(builder.master, builder.name, sortedResults[i], sortedResults[i - 1]);
168 var passCount = cells.filter(function (cell) { return cell.className == 'PASS'; }).length;
169 var passingRate = Math.round(passCount / cells.length * 100) + '%';
170 return element('tr', [element('th', ['' + title]), element('td', {'class': 'passingRate'}, [passingRate]), element('td', cells)]);
173 TestResultsView.fetchTest = function (testName) {
174 if (this._tests.indexOf(testName) >= 0)
179 var closeButton = element('div', {'class': 'closeButton'});
180 closeButton.innerHTML = '<svg viewBox="0 0 100 100"><g stroke-width="10">'
181 + '<circle cx="50" cy="50" r="45" fill="transparent"></circle><polygon points="30,30 70,70"></polygon>'
182 + '<polygon points="30,70 70,30"></polygon></g></svg>';
183 closeButton.addEventListener('click', function (event) {
184 self._removeTest(testName);
185 section.parentNode.removeChild(section);
186 event.preventDefault();
188 var section = element('section', {'id': testName, 'class': 'testResults'}, [closeButton, 'Loading...']);
190 document.getElementById('testView').appendChild(section);
192 var xhr = new XMLHttpRequest();
193 xhr.open("GET", 'api/results.php?test=' + testName, true);
194 xhr.onload = function(event) {
195 var response = JSON.parse(xhr.response);
196 section.removeChild(section.lastChild); // Remove Loading...
197 if (response['status'] != 'OK') {
198 section.appendChild(text('Failed to load results for ' + testName + ': ' + response['status']));
201 self._populateTestPane(testName, response, section);
204 this._tests.push(testName);
207 TestResultsView._removeTest = function (testName) {
208 var index = this._tests.indexOf(testName);
211 this._tests.splice(index, 1);
212 this.updateLocationHash();
215 TestResultsView.fetchTests = function (testNames, doNotUpdateHash) {
216 for (var i = 0; i < testNames.length; i++)
217 this.fetchTest(testNames[i], doNotUpdateHash);
218 if (!doNotUpdateHash)
219 this.updateLocationHash();
222 TestResultsView._populateBuilderPane = function(builderName, results, section) {
223 var table = element('table', {'class': 'resultsTable'}, [element('caption', [builderName])]);
224 var resultsByBuilder = results['builders'];
227 for (var currentBuilderId in resultsByBuilder) {
228 builderId = currentBuilderId;
229 resultsByTests = resultsByBuilder[builderId];
234 var builder = this._builders[builderId];
235 for (var testId in resultsByTests)
236 table.appendChild(this._createTestResultRow(this._availableTests[testId].name, resultsByTests[testId], builder));
237 section.appendChild(table);
240 TestResultsView.fetchFailingTestsForBuilder = function (builderName, numberOfDays, doNotUpdateHash) {
241 var section = element('section', {'class': 'testResults'}, ['Loading...']);
243 var container = document.getElementById('builderFailingTestsView');
244 container.innerHTML = '';
245 container.appendChild(section);
248 var xhr = new XMLHttpRequest();
249 xhr.open("GET", 'api/failing-tests.php?builder=' + escape(builderName) + '&' + 'days=' + numberOfDays, true);
250 xhr.onload = function(event) {
251 var response = JSON.parse(xhr.response);
252 section.innerHTML = '';
253 if (response['status'] != 'OK') {
254 section.appendChild(text('Failed to load results for ' + builderName + ': ' + response['status']));
257 self._currentBuilder = builderName;
258 self._currentBuilderDays = numberOfDays;
259 self._populateBuilderPane(builderName, response, section);
260 if (!doNotUpdateHash)
261 self.updateLocationHash();
266 TestResultsView.updateLocationHash = function () {
268 'tests': this._tests.join(','),
269 'builder': this._currentBuilder,
270 'builderDays': this._currentBuilderDays,
273 for (var key in params) {
274 var value = params[key];
275 if (value === null || value === undefined)
277 hash += '&' + decodeURIComponent(key) + '=' + decodeURIComponent(value);
279 location.hash = hash;
280 this._oldHash = location.hash;
283 TestResultsView.locationHashChanged = function () {
284 var newHash = location.hash;
285 if (newHash == this._oldHash)
287 this._oldHash = newHash;
289 this.loadTestsFromLocationHash();
292 TestResultsView.loadTestsFromLocationHash = function () {
294 location.hash.substr(1).split('&').forEach(function (component) {
295 var equalPosition = component.indexOf('=');
296 if (equalPosition < 0)
298 var name = decodeURIComponent(component.substr(0, equalPosition));
299 parsed[name] = decodeURIComponent(component.substr(equalPosition + 1));
301 var doNotUpdateHash = true;
302 if (parsed['tests']) {
303 document.getElementById('testView').innerHTML = '';
304 this.fetchTests(parsed['tests'].split(','), doNotUpdateHash);
306 if (parsed['builder'])
307 this.fetchFailingTestsForBuilder(parsed['builder'], parsed['builderDays'] || 5, doNotUpdateHash);
310 function fetchManifest(callback) {
311 var xhr = new XMLHttpRequest();
312 xhr.open("GET", 'api/manifest.php', true);
313 xhr.onload = function(event) {
314 var response = JSON.parse(xhr.response);
315 if (response['status'] != 'OK') {
316 alert('Failed to load manifest:' + response['status']);
317 console.log(response);
325 fetchManifest(function (response) {
326 var testNames = response['tests'].map(function (test) { return test['name']; })
327 var input = document.getElementById('testName');
328 input.autocompleter = new Autocompleter(input, testNames);
330 var builderListView = document.getElementById('builderListView');
331 for (var builderId in response['builders'])
332 builderListView.appendChild(element('option', [text(response['builders'][builderId].name)]));
334 var builderDaysView = document.getElementById('builderDaysView');
336 function updateBuilderView() {
337 if (builderListView.value)
338 TestResultsView.fetchFailingTestsForBuilder(builderListView.value, builderDaysView.value);
341 builderListView.addEventListener('change', updateBuilderView);
342 builderDaysView.addEventListener('change', updateBuilderView);
344 function mapById(items) {
346 for (var i = 0; i < items.length; i++)
347 results[items[i].id] = items[i];
351 TestResultsView.setAvailableTests(mapById(response['tests']));
352 TestResultsView.setBuilders(mapById(response['builders']));
353 TestResultsView.setSlaves(mapById(response['slaves']));
354 TestResultsView.setRepositories(mapById(response['repositories']));
355 TestResultsView.loadTestsFromLocationHash();
358 function pasteHelper(input, event) {
359 function removeJunkFromNRWTStdout(input) {
360 return input.replace(/(\[[\w ]+\])|(.+\:.+)/g, '').replace(/^[ \t]+|[ \t]+$/gm, '');
363 function removeJunkFromResultsPage(input) {
364 return input.replace(/(^[^\/]+$)|(^\+)/gm, '').replace(/\([^)]+\)/g, '').replace(/\s+[A-Za-z]+(\s+[A-Za-z]+)*\s*$/gm, '');
367 var text = event.clipboardData.getData('text/plain');
368 if (text.indexOf('\n') < 0)
371 var urls = removeJunkFromResultsPage(removeJunkFromNRWTStdout(text)).split('\n');
372 TestResultsView.fetchTests(urls.filter(function (url) { return url.length; }));
373 event.preventDefault();