blob: 1ab763d16f13b8e5d75386957dc1199484fb6e6c [file] [log] [blame]
/*
* Copyright (C) 2011 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
function ViewController(buildbot) {
this._buildbot = buildbot;
this._navigationID = 0;
var self = this;
addEventListener('load', function() { self.loaded() }, false);
addEventListener('hashchange', function() { self.parseHash(location.hash) }, false);
}
ViewController.prototype = {
loaded: function() {
this._header = document.createElement('h1');
document.body.appendChild(this._header);
this._mainContentElement = document.createElement('div');
document.body.appendChild(this._mainContentElement);
document.body.appendChild(this._domForAuxiliaryUIElements());
this.parseHash(location.hash);
},
parseHash: function(hash) {
++this._navigationID;
var match = /#\/(.*)/.exec(hash);
if (match)
this._displayBuilder(this._buildbot.builderNamed(decodeURIComponent(match[1])));
else
this._displayTesters();
},
_displayBuilder: function(builder) {
this._setTitle(builder.name);
this._mainContentElement.removeAllChildren();
var navigationID = this._navigationID;
var self = this;
(new LayoutTestHistoryAnalyzer(builder)).start(function(data, stillFetchingData) {
if (self._navigationID !== navigationID) {
// The user has navigated somewhere else. Stop loading data about this tester.
return false;
}
var list = document.createElement('ol');
list.id = 'failure-history';
var buildNames = Object.keys(data.history)
buildNames.forEach(function(buildName, buildIndex, buildNameArray) {
var failingTestNames = Object.keys(data.history[buildName].tests);
if (!failingTestNames.length)
return;
var item = document.createElement('li');
list.appendChild(item);
var testList = document.createElement('ol');
item.appendChild(testList);
testList.className = 'test-list';
for (var testName in data.history[buildName].tests) {
var testItem = document.createElement('li');
testItem.appendChild(self._domForFailedTest(builder, buildName, testName, data.history[buildName].tests[testName]));
testList.appendChild(testItem);
}
if (data.history[buildName].tooManyFailures) {
var p = document.createElement('p');
p.className = 'info';
p.appendChild(document.createTextNode('run-webkit-tests exited early due to too many failures/crashes/timeouts'));
item.appendChild(p);
}
var passingBuildName;
if (buildIndex + 1 < buildNameArray.length)
passingBuildName = buildNameArray[buildIndex + 1];
item.appendChild(self._domForRegressionRange(builder, buildName, passingBuildName, failingTestNames));
if (passingBuildName || !stillFetchingData) {
var bugForm = new FailingTestsBugForm(builder, buildName, passingBuildName, failingTestNames);
item.appendChild(self._domForNewAndExistingBugs(builder, failingTestNames, bugForm))
}
});
self._mainContentElement.removeAllChildren();
self._mainContentElement.appendChild(list);
self._mainContentElement.appendChild(self._domForPossiblyFlakyTests(builder, data.possiblyFlaky, buildNames));
if (!stillFetchingData)
PersistentCache.prune();
return true;
});
},
_displayTesters: function() {
this._setTitle('Testers');
this._mainContentElement.removeAllChildren();
var list = document.createElement('ul');
this._mainContentElement.appendChild(list);
var latestBuildInfos = [];
var navigationID = this._navigationID;
function updateList() {
latestBuildInfos.sort(function(a, b) { return a.tester.name.localeCompare(b.tester.name) });
list.removeAllChildren();
latestBuildInfos.forEach(function(buildInfo) {
var link = base.createLinkNode('#/' + buildInfo.tester.name, buildInfo.tester.name)
var item = document.createElement('li');
item.appendChild(link);
if (buildInfo.tooManyFailures)
item.appendChild(document.createTextNode(' (too many failures/crashes/timeouts)'));
else
item.appendChild(document.createTextNode(' (' + buildInfo.failureCount + ' failing test' + (buildInfo.failureCount > 1 ? 's' : '') + ')'));
list.appendChild(item);
});
}
var self = this;
this._buildbot.getTesters(function(testers) {
if (self._navigationID !== navigationID) {
// The user has navigated somewhere else.
return;
}
testers.forEach(function(tester) {
tester.getMostRecentCompletedBuildNumber(function(buildNumber) {
if (self._navigationID !== navigationID)
return;
if (buildNumber < 0)
return;
tester.getNumberOfFailingTests(buildNumber, function(failureCount, tooManyFailures) {
if (self._navigationID !== navigationID)
return;
if (failureCount <= 0)
return;
latestBuildInfos.push({ tester: tester, failureCount: failureCount, tooManyFailures: tooManyFailures });
updateList();
});
});
});
});
},
_domForRegressionRange: function(builder, failingBuildName, passingBuildName, failingTestNames) {
var result = document.createDocumentFragment();
var dlItems = [
[document.createTextNode('Failed'), this._domForBuildName(builder, failingBuildName)],
];
if (passingBuildName)
dlItems.push([document.createTextNode('Passed'), this._domForBuildName(builder, passingBuildName)]);
result.appendChild(createDescriptionList(dlItems));
if (!passingBuildName)
return result;
var firstSuspectRevision = this._buildbot.parseBuildName(passingBuildName).revision + 1;
var lastSuspectRevision = this._buildbot.parseBuildName(failingBuildName).revision;
if (firstSuspectRevision === lastSuspectRevision)
return result;
var suspectsContainer = document.createElement('div');
result.appendChild(suspectsContainer);
var link = base.createLinkNode(trac.logURL('trunk', firstSuspectRevision, lastSuspectRevision, true), 'View regression range in Trac');
result.appendChild(link);
suspectsContainer.appendChild(document.createTextNode('Searching for suspect revisions\u2026'));
// FIXME: Maybe some of this code should go in LayoutTestHistoryAnalyzer, or some other class?
var self = this;
trac.commitDataForRevisionRange('trunk', firstSuspectRevision, lastSuspectRevision, function(commits) {
var failingTestNamesWithoutExtensions = failingTestNames.map(removePathExtension);
var suspectCommits = commits.filter(function(commit) {
return failingTestNamesWithoutExtensions.some(function(testName) {
return commit.message.contains(testName);
});
});
suspectsContainer.removeAllChildren();
if (!suspectCommits.length)
return;
var title = 'Suspect revision' + (suspectCommits.length > 1 ? 's' : '') + ':';
suspectsContainer.appendChild(document.createTextNode(title));
var list = document.createElement('ul');
suspectsContainer.appendChild(list);
list.className = 'suspect-revisions-list';
function compareCommits(a, b) {
return b.revision - a.revision;
}
list.appendChildren(sorted(suspectCommits, compareCommits).map(function(commit) {
var item = document.createElement('li');
item.appendChild(base.createLinkNode(trac.changesetURL(commit.revision), commit.title));
return item;
}));
});
return result;
},
_domForAuxiliaryUIElements: function() {
var aside = document.createElement('aside');
aside.appendChild(document.createTextNode('Something not working? Have an idea to improve this page? '));
var queryParameters = {
product: 'WebKit',
component: 'Tools / Tests',
version: '528+ (Nightly build)',
bug_file_loc: location.href,
cc: 'aroben@apple.com',
short_desc: 'TestFailures page needs more unicorns!',
};
aside.appendChild(base.createLinkNode(addQueryParametersToURL(config.kBugzillaURL + '/enter_bug.cgi', queryParameters), 'File a bug!', '_blank'));
return aside;
},
_domForBuildName: function(builder, buildName) {
var parsed = this._buildbot.parseBuildName(buildName);
var sourceLink = base.createLinkNode('https://trac.webkit.org/changeset/' + parsed.revision, 'r' + parsed.revision);
var buildLink = base.createLinkNode(builder.buildURL(parsed.buildNumber), parsed.buildNumber);
var resultsLink = base.createLinkNode(builder.resultsPageURL(buildName), 'results.html');
var result = document.createDocumentFragment();
result.appendChild(sourceLink);
result.appendChild(document.createTextNode(' ('));
result.appendChild(buildLink);
result.appendChild(document.createTextNode(') ('));
result.appendChild(resultsLink);
result.appendChild(document.createTextNode(')'));
return result;
},
_domForFailedTest: function(builder, buildName, testName, testResult) {
var result = document.createDocumentFragment();
result.appendChild(document.createTextNode(testName + ': '));
result.appendChild(this._domForFailureDiagnosis(builder, buildName, testName, testResult));
return result;
},
_domForFailureDiagnosis: function(builder, buildName, testName, testResult) {
var diagnosticInfo = builder.failureDiagnosisTextAndURL(buildName, testName, testResult);
if (!diagnosticInfo)
return document.createTextNode(testResult.failureType);
var textAndCrashingSymbol = document.createDocumentFragment();
textAndCrashingSymbol.appendChild(document.createTextNode(diagnosticInfo.text));
if (testResult.crashingSymbol) {
var code = document.createElement('code');
code.appendChild(document.createTextNode(testResult.crashingSymbol));
textAndCrashingSymbol.appendChild(document.createTextNode(' ('));
textAndCrashingSymbol.appendChild(code);
textAndCrashingSymbol.appendChild(document.createTextNode(')'));
}
if (!('url' in diagnosticInfo))
return textAndCrashingSymbol;
return base.createLinkNode(diagnosticInfo.url, textAndCrashingSymbol);
},
_domForNewAndExistingBugs: function(tester, failingTests, bugForm) {
var result = document.createDocumentFragment();
var container = document.createElement('p');
result.appendChild(container);
container.className = 'existing-and-new-bugs';
var bugsContainer = document.createElement('div');
container.appendChild(bugsContainer);
bugsContainer.appendChild(document.createTextNode('Searching for bugs related to ' + (failingTests.length > 1 ? 'these tests' : 'this test') + '\u2026'));
bugzilla.quickSearch('ALL ' + failingTests.join('|'), function(bugs) {
if (!bugs.length) {
bugsContainer.parentNode.removeChild(bugsContainer);
return;
}
while (bugsContainer.firstChild)
bugsContainer.removeChild(bugsContainer.firstChild);
bugsContainer.appendChild(document.createTextNode('Existing bugs related to ' + (failingTests.length > 1 ? 'these tests' : 'this test') + ':'));
var list = document.createElement('ul');
bugsContainer.appendChild(list);
list.className = 'existing-bugs-list';
function bugToListItem(bug) {
var link = base.createLinkNode(bug.url, bug.title);
var item = document.createElement('li');
item.appendChild(link);
return item;
}
var openBugs = bugs.filter(function(bug) { return bugzilla.isOpenStatus(bug.status) });
var closedBugs = bugs.filter(function(bug) { return !bugzilla.isOpenStatus(bug.status) });
list.appendChildren(openBugs.map(bugToListItem));
if (!closedBugs.length)
return;
var item = document.createElement('li');
list.appendChild(item);
item.appendChild(document.createTextNode('Closed bugs:'));
var closedList = document.createElement('ul');
item.appendChild(closedList);
closedList.appendChildren(closedBugs.map(bugToListItem));
});
var form = bugForm.domElement();
result.appendChild(form);
var linkText = 'File bug for ' + (failingTests.length > 1 ? 'these failures' : 'this failure');
var link = base.createLinkNode('#', linkText);
link.addEventListener('click', function(event) { form.submit(); event.preventDefault(); });
container.appendChild(link);
return result;
},
_domForPossiblyFlakyTests: function(builder, possiblyFlakyTestData, allBuilds) {
var result = document.createDocumentFragment();
var flakyTests = Object.keys(possiblyFlakyTestData);
if (!flakyTests.length)
return result;
var flakyHeader = document.createElement('h2');
result.appendChild(flakyHeader);
flakyHeader.appendChild(document.createTextNode('Possibly Flaky Tests'));
var flakyList = document.createElement('ol');
result.appendChild(flakyList);
flakyList.id = 'possibly-flaky-tests';
var self = this;
flakyList.appendChildren(sorted(flakyTests).map(function(testName) {
var item = document.createElement('li');
var disclosureTriangle = document.createElement('span');
item.appendChild(disclosureTriangle);
disclosureTriangle.className = 'disclosure-triangle';
const blackRightPointingSmallTriangle = '\u25b8';
disclosureTriangle.appendChild(document.createTextNode(blackRightPointingSmallTriangle));
var failures = possiblyFlakyTestData[testName];
item.appendChild(document.createTextNode(testName + ' (failed ' + failures.length + ' out of ' + allBuilds.length + ' times)'));
var container = document.createElement('div');
item.appendChild(container);
container.className = 'expandable';
disclosureTriangle.addEventListener('click', function() {
item.toggleStyleClass('expanded');
if (!item.hasStyleClass('expanded'))
return;
if (!container.firstChild) {
var failureList = document.createElement('ol');
container.appendChild(failureList);
failureList.className = 'flakiness-examples-list';
failureList.appendChildren(failures.map(function(historyItem) {
var item = document.createElement('li');
item.appendChild(self._domForBuildName(builder, historyItem.build));
item.appendChild(document.createTextNode(': '));
item.appendChild(self._domForFailureDiagnosis(builder, historyItem.build, testName, historyItem.result));
return item;
}));
var failingBuildNames = failures.map(function(historyItem) { return historyItem.build });
var bugForm = new FlakyTestBugForm(builder, failingBuildNames, testName, allBuilds.last(), allBuilds[0], allBuilds.length);
container.appendChild(self._domForNewAndExistingBugs(builder, [testName], bugForm));
}
});
return item;
}));
return result;
},
_setTitle: function(title) {
document.title = title;
this._header.textContent = title;
},
};