2 * Copyright (C) 2011 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
26 function ViewController(buildbot) {
27 this._buildbot = buildbot;
28 this._navigationID = 0;
31 addEventListener('load', function() { self.loaded() }, false);
32 addEventListener('hashchange', function() { self.parseHash(location.hash) }, false);
35 ViewController.prototype = {
37 this._header = document.createElement('h1');
38 document.body.appendChild(this._header);
39 this._mainContentElement = document.createElement('div');
40 document.body.appendChild(this._mainContentElement);
41 document.body.appendChild(this._domForAuxiliaryUIElements());
43 this.parseHash(location.hash);
46 parseHash: function(hash) {
49 var match = /#\/(.*)/.exec(hash);
51 this._displayBuilder(this._buildbot.builderNamed(decodeURIComponent(match[1])));
53 this._displayTesters();
56 _displayBuilder: function(builder) {
57 this._setTitle(builder.name);
58 this._mainContentElement.removeAllChildren();
60 var navigationID = this._navigationID;
63 (new LayoutTestHistoryAnalyzer(builder)).start(function(data, stillFetchingData) {
64 if (self._navigationID !== navigationID) {
65 // The user has navigated somewhere else. Stop loading data about this tester.
69 var list = document.createElement('ol');
70 list.id = 'failure-history';
72 var buildNames = Object.keys(data.history)
73 buildNames.forEach(function(buildName, buildIndex, buildNameArray) {
74 var failingTestNames = Object.keys(data.history[buildName].tests);
75 if (!failingTestNames.length)
78 var item = document.createElement('li');
79 list.appendChild(item);
81 var testList = document.createElement('ol');
82 item.appendChild(testList);
84 testList.className = 'test-list';
85 for (var testName in data.history[buildName].tests) {
86 var testItem = document.createElement('li');
87 testItem.appendChild(self._domForFailedTest(builder, buildName, testName, data.history[buildName].tests[testName]));
88 testList.appendChild(testItem);
91 if (data.history[buildName].tooManyFailures) {
92 var p = document.createElement('p');
94 p.appendChild(document.createTextNode('run-webkit-tests exited early due to too many failures/crashes/timeouts'));
99 if (buildIndex + 1 < buildNameArray.length)
100 passingBuildName = buildNameArray[buildIndex + 1];
102 item.appendChild(self._domForRegressionRange(builder, buildName, passingBuildName, failingTestNames));
104 if (passingBuildName || !stillFetchingData) {
105 var bugForm = new FailingTestsBugForm(builder, buildName, passingBuildName, failingTestNames);
106 item.appendChild(self._domForNewAndExistingBugs(builder, failingTestNames, bugForm))
110 self._mainContentElement.removeAllChildren();
111 self._mainContentElement.appendChild(list);
112 self._mainContentElement.appendChild(self._domForPossiblyFlakyTests(builder, data.possiblyFlaky, buildNames));
114 if (!stillFetchingData)
115 PersistentCache.prune();
121 _displayTesters: function() {
122 this._setTitle('Testers');
123 this._mainContentElement.removeAllChildren();
125 var list = document.createElement('ul');
126 this._mainContentElement.appendChild(list);
128 var latestBuildInfos = [];
129 var navigationID = this._navigationID;
131 function updateList() {
132 latestBuildInfos.sort(function(a, b) { return a.tester.name.localeCompare(b.tester.name) });
133 list.removeAllChildren();
134 latestBuildInfos.forEach(function(buildInfo) {
135 var link = document.createElement('a');
136 link.href = '#/' + buildInfo.tester.name;
137 link.appendChild(document.createTextNode(buildInfo.tester.name));
139 var item = document.createElement('li');
140 item.appendChild(link);
141 if (buildInfo.tooManyFailures)
142 item.appendChild(document.createTextNode(' (too many failures/crashes/timeouts)'));
144 item.appendChild(document.createTextNode(' (' + buildInfo.failureCount + ' failing test' + (buildInfo.failureCount > 1 ? 's' : '') + ')'));
145 list.appendChild(item);
150 this._buildbot.getTesters(function(testers) {
151 if (self._navigationID !== navigationID) {
152 // The user has navigated somewhere else.
155 testers.forEach(function(tester) {
156 tester.getMostRecentCompletedBuildNumber(function(buildNumber) {
157 if (self._navigationID !== navigationID)
161 tester.getNumberOfFailingTests(buildNumber, function(failureCount, tooManyFailures) {
162 if (self._navigationID !== navigationID)
164 if (failureCount <= 0)
166 latestBuildInfos.push({ tester: tester, failureCount: failureCount, tooManyFailures: tooManyFailures });
174 _domForRegressionRange: function(builder, failingBuildName, passingBuildName, failingTestNames) {
175 var result = document.createDocumentFragment();
178 [document.createTextNode('Failed'), this._domForBuildName(builder, failingBuildName)],
180 if (passingBuildName)
181 dlItems.push([document.createTextNode('Passed'), this._domForBuildName(builder, passingBuildName)]);
182 result.appendChild(createDefinitionList(dlItems));
184 if (!passingBuildName)
187 var firstSuspectRevision = this._buildbot.parseBuildName(passingBuildName).revision + 1;
188 var lastSuspectRevision = this._buildbot.parseBuildName(failingBuildName).revision;
190 if (firstSuspectRevision === lastSuspectRevision)
193 var suspectsContainer = document.createElement('div');
194 result.appendChild(suspectsContainer);
196 var link = document.createElement('a');
197 result.appendChild(link);
199 link.href = trac.logURL('trunk', firstSuspectRevision, lastSuspectRevision, true);
200 link.appendChild(document.createTextNode('View regression range in Trac'));
202 suspectsContainer.appendChild(document.createTextNode('Searching for suspect revisions\u2026'));
204 // FIXME: Maybe some of this code should go in LayoutTestHistoryAnalyzer, or some other class?
206 trac.commitDataForRevisionRange('trunk', firstSuspectRevision, lastSuspectRevision, function(commits) {
207 var failingTestNamesWithoutExtensions = failingTestNames.map(removePathExtension);
208 var suspectCommits = commits.filter(function(commit) {
209 return failingTestNamesWithoutExtensions.some(function(testName) {
210 return commit.message.contains(testName);
214 suspectsContainer.removeAllChildren();
216 if (!suspectCommits.length)
219 var title = 'Suspect revision' + (suspectCommits.length > 1 ? 's' : '') + ':';
220 suspectsContainer.appendChild(document.createTextNode(title));
222 var list = document.createElement('ul');
223 suspectsContainer.appendChild(list);
224 list.className = 'suspect-revisions-list';
226 function compareCommits(a, b) {
227 return b.revision - a.revision;
230 list.appendChildren(sorted(suspectCommits, compareCommits).map(function(commit) {
231 var item = document.createElement('li');
232 var link = document.createElement('a');
233 item.appendChild(link);
235 link.href = trac.changesetURL(commit.revision);
236 link.appendChild(document.createTextNode(commit.title))
245 _domForAuxiliaryUIElements: function() {
246 var aside = document.createElement('aside');
247 aside.appendChild(document.createTextNode('Something not working? Have an idea to improve this page? '));
248 var link = document.createElement('a');
249 aside.appendChild(link);
251 link.appendChild(document.createTextNode('File a bug!'));
252 var queryParameters = {
254 component: 'Tools / Tests',
255 version: '528+ (Nightly build)',
256 bug_file_loc: location.href,
257 cc: 'aroben@apple.com',
258 short_desc: 'TestFailures page needs more unicorns!',
260 link.href = addQueryParametersToURL(config.kBugzillaURL + '/enter_bug.cgi', queryParameters);
261 link.target = '_blank';
266 _domForBuildName: function(builder, buildName) {
267 var parsed = this._buildbot.parseBuildName(buildName);
269 var sourceLink = document.createElement('a');
270 sourceLink.href = 'http://trac.webkit.org/changeset/' + parsed.revision;
271 sourceLink.appendChild(document.createTextNode('r' + parsed.revision));
273 var buildLink = document.createElement('a');
274 buildLink.href = builder.buildURL(parsed.buildNumber);
275 buildLink.appendChild(document.createTextNode(parsed.buildNumber));
277 var resultsLink = document.createElement('a');
278 resultsLink.href = builder.resultsPageURL(buildName);
279 resultsLink.appendChild(document.createTextNode('results.html'));
281 var result = document.createDocumentFragment();
282 result.appendChild(sourceLink);
283 result.appendChild(document.createTextNode(' ('));
284 result.appendChild(buildLink);
285 result.appendChild(document.createTextNode(') ('));
286 result.appendChild(resultsLink);
287 result.appendChild(document.createTextNode(')'));
292 _domForFailedTest: function(builder, buildName, testName, testResult) {
293 var result = document.createDocumentFragment();
294 result.appendChild(document.createTextNode(testName + ': '));
295 result.appendChild(this._domForFailureDiagnosis(builder, buildName, testName, testResult));
299 _domForFailureDiagnosis: function(builder, buildName, testName, testResult) {
300 var diagnosticInfo = builder.failureDiagnosisTextAndURL(buildName, testName, testResult);
302 return document.createTextNode(testResult.failureType);
304 var textAndCrashingSymbol = document.createDocumentFragment();
305 textAndCrashingSymbol.appendChild(document.createTextNode(diagnosticInfo.text));
306 if (testResult.crashingSymbol) {
307 var code = document.createElement('code');
308 code.appendChild(document.createTextNode(testResult.crashingSymbol));
309 textAndCrashingSymbol.appendChild(document.createTextNode(' ('));
310 textAndCrashingSymbol.appendChild(code);
311 textAndCrashingSymbol.appendChild(document.createTextNode(')'));
314 if (!('url' in diagnosticInfo))
315 return textAndCrashingSymbol;
317 var link = document.createElement('a');
318 link.href = diagnosticInfo.url;
319 link.appendChild(textAndCrashingSymbol);
323 _domForNewAndExistingBugs: function(tester, failingTests, bugForm) {
324 var result = document.createDocumentFragment();
326 var container = document.createElement('p');
327 result.appendChild(container);
329 container.className = 'existing-and-new-bugs';
331 var bugsContainer = document.createElement('div');
332 container.appendChild(bugsContainer);
334 bugsContainer.appendChild(document.createTextNode('Searching for bugs related to ' + (failingTests.length > 1 ? 'these tests' : 'this test') + '\u2026'));
336 bugzilla.quickSearch('ALL ' + failingTests.join('|'), function(bugs) {
338 bugsContainer.parentNode.removeChild(bugsContainer);
342 while (bugsContainer.firstChild)
343 bugsContainer.removeChild(bugsContainer.firstChild);
345 bugsContainer.appendChild(document.createTextNode('Existing bugs related to ' + (failingTests.length > 1 ? 'these tests' : 'this test') + ':'));
347 var list = document.createElement('ul');
348 bugsContainer.appendChild(list);
350 list.className = 'existing-bugs-list';
352 function bugToListItem(bug) {
353 var link = document.createElement('a');
355 link.appendChild(document.createTextNode(bug.title));
357 var item = document.createElement('li');
358 item.appendChild(link);
363 var openBugs = bugs.filter(function(bug) { return bugzilla.isOpenStatus(bug.status) });
364 var closedBugs = bugs.filter(function(bug) { return !bugzilla.isOpenStatus(bug.status) });
366 list.appendChildren(openBugs.map(bugToListItem));
368 if (!closedBugs.length)
371 var item = document.createElement('li');
372 list.appendChild(item);
374 item.appendChild(document.createTextNode('Closed bugs:'));
376 var closedList = document.createElement('ul');
377 item.appendChild(closedList);
379 closedList.appendChildren(closedBugs.map(bugToListItem));
382 var form = bugForm.domElement();
383 result.appendChild(form);
385 var link = document.createElement('a');
386 container.appendChild(link);
388 link.addEventListener('click', function(event) { form.submit(); event.preventDefault(); });
390 link.appendChild(document.createTextNode('File bug for ' + (failingTests.length > 1 ? 'these failures' : 'this failure')));
395 _domForPossiblyFlakyTests: function(builder, possiblyFlakyTestData, allBuilds) {
396 var result = document.createDocumentFragment();
397 var flakyTests = Object.keys(possiblyFlakyTestData);
398 if (!flakyTests.length)
401 var flakyHeader = document.createElement('h2');
402 result.appendChild(flakyHeader);
403 flakyHeader.appendChild(document.createTextNode('Possibly Flaky Tests'));
405 var flakyList = document.createElement('ol');
406 result.appendChild(flakyList);
408 flakyList.id = 'possibly-flaky-tests';
411 flakyList.appendChildren(sorted(flakyTests).map(function(testName) {
412 var item = document.createElement('li');
414 var disclosureTriangle = document.createElement('span');
415 item.appendChild(disclosureTriangle);
417 disclosureTriangle.className = 'disclosure-triangle';
418 const blackRightPointingSmallTriangle = '\u25b8';
419 disclosureTriangle.appendChild(document.createTextNode(blackRightPointingSmallTriangle));
421 var failures = possiblyFlakyTestData[testName];
423 item.appendChild(document.createTextNode(testName + ' (failed ' + failures.length + ' out of ' + allBuilds.length + ' times)'));
425 var container = document.createElement('div');
426 item.appendChild(container);
428 container.className = 'expandable';
430 disclosureTriangle.addEventListener('click', function() {
431 item.toggleStyleClass('expanded');
432 if (!item.hasStyleClass('expanded'))
435 if (!container.firstChild) {
436 var failureList = document.createElement('ol');
437 container.appendChild(failureList);
439 failureList.className = 'flakiness-examples-list';
441 failureList.appendChildren(failures.map(function(historyItem) {
442 var item = document.createElement('li');
443 item.appendChild(self._domForBuildName(builder, historyItem.build));
444 item.appendChild(document.createTextNode(': '));
445 item.appendChild(self._domForFailureDiagnosis(builder, historyItem.build, testName, historyItem.result));
449 var failingBuildNames = failures.map(function(historyItem) { return historyItem.build });
450 var bugForm = new FlakyTestBugForm(builder, failingBuildNames, testName, allBuilds.last(), allBuilds[0], allBuilds.length);
451 container.appendChild(self._domForNewAndExistingBugs(builder, [testName], bugForm));
461 _setTitle: function(title) {
462 document.title = title;
463 this._header.textContent = title;