| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>WebKit Test Results</title> |
| <link rel="stylesheet" href="common.css"> |
| <link rel="stylesheet" href="main.css"> |
| <script src="https://svn.webkit.org/repository/webkit/!svn/bc/128779/trunk/PerformanceTests/Dromaeo/resources/dromaeo/web/lib/jquery-1.6.4.js"></script> |
| <script src="https://svn.webkit.org/repository/webkit/!svn/bc/128779/trunk/PerformanceTests/resources/jquery.tablesorter.min.js"></script> |
| <script src="js/autocompleter.js"></script> |
| <script src="js/build.js"></script> |
| <script src="js/dom.js"></script> |
| </head> |
| <body> |
| |
| <header id="title"> |
| <h1><a href="/">WebKit Test Results</a></h1> |
| <ul> |
| <li><a href="http://build.webkit.org/waterfall">Waterfall</a></li> |
| </ul> |
| </header> |
| |
| <form onsubmit="TestResultsView.fetchTest(this['testName'].value); |
| TestResultsView.updateLocationHash(); |
| return false;"> |
| <input id="testName" type="text" size="150" onpaste="pasteHelper(this, event)" |
| placeholder="Type in a test name, or copy and paste test names on results.html or NRWT stdout (including junks)"></form> |
| <div id="testView"></div> |
| |
| <div id="buildersView"> |
| <form> |
| Show |
| <label>all tests <select id="builderFailureTypeView"><option>failing</option><option>flaky</option><option value="wrongexpectations">has wrong expectations</option></select></label> |
| <label>on <select id="builderListView"><option value="">Select builder</option></select></label> |
| <label for="builderDaysView">for |
| <select id="builderDaysView" disabled><option>5</option><option>15</option><option selected>30</option></select> days</label> |
| </form> |
| <div id="builderFailingTestsView"></div> |
| </div> |
| |
| <div id="tooltipContainer"></div> |
| |
| <script> |
| |
| var TestResultsView = new (function () { |
| var tooltipContainer = document.getElementById('tooltipContainer'); |
| |
| tooltipContainer.onclick = function (event) { event.insideTooltip = true; } |
| document.addEventListener('click', function (event) { |
| if (!event.insideTooltip) |
| tooltipContainer.style.display = 'none'; |
| }); |
| |
| window.addEventListener('hashchange', function (event) { |
| TestResultsView.locationHashChanged(); |
| }); |
| |
| this._tooltipContainer = tooltipContainer; |
| this._tests = []; |
| this._currentBuilder = null; |
| this._currentBuilderFailureType = null; |
| this._currentBuilderDays = null; |
| this._oldHash = null; |
| this._builders = {}; |
| this._builderByName = {}; |
| this._slaves = {}; |
| this._repositories = {}; |
| this._testCategories = {}; |
| this._newBugLinks = []; |
| }); |
| |
| TestResultsView.setAvailableTests = function (availableTests) { |
| this._availableTests = availableTests; |
| } |
| |
| TestResultsView.setBuilders = function (builders) { |
| this._builders = builders; |
| for (var builderId in builders) { |
| var builder = builders[builderId]; |
| this._builderByName[builder.name] = builder; |
| } |
| } |
| |
| TestResultsView.setSlaves = function (slaves) { |
| this._slaves = slaves; |
| } |
| |
| TestResultsView.setRepositories = function (repositories) { |
| this._repositories = repositories; |
| } |
| |
| TestResultsView.setTestCategories = function (testCategories) { |
| this._testCategories = testCategories; |
| } |
| |
| TestResultsView.setNewBugLinks = function (newBugLinks) { |
| this._newBugLinks = newBugLinks; |
| } |
| |
| TestResultsView.showTooltip = function (anchor, contentElement) { |
| var tooltipContainer = this._tooltipContainer; |
| tooltipContainer.style.display = null; |
| |
| while (tooltipContainer.firstChild) |
| tooltipContainer.removeChild(tooltipContainer.firstChild); |
| tooltipContainer.appendChild(contentElement); |
| |
| var position = {x: 0, y: 0}; |
| var currentNode = anchor; |
| while (currentNode) { |
| position.x += currentNode.offsetLeft; |
| position.y += currentNode.offsetTop; |
| currentNode = currentNode.offsetParent; |
| } |
| tooltipContainer.style.left = (position.x - contentElement.offsetWidth / 2 + anchor.offsetWidth / 2) + 'px'; |
| tooltipContainer.style.top = (position.y - contentElement.clientHeight - 5) + 'px'; |
| } |
| |
| TestResultsView._createResultCell = function (master, builder, result, previousResult) { |
| var buildTime = result['buildTime']; |
| var revisions = result['revisions']; |
| var slave = result['slave']; |
| var buildNumber = result['buildNumber']; |
| var actual = result['actual']; |
| var expected = result['expected']; |
| var timeIfSlow = result.isSlow ? result.roundedTime : ''; |
| |
| // FIXME: We shouldn't be hard-coding WebKit revisions here. |
| var webkitRepositoryId; |
| for (var repositoryId in TestResultsView._repositories) { |
| if (TestResultsView._repositories[repositoryId].name == 'WebKit') |
| webkitRepositoryId = repositoryId; |
| } |
| var webkitRevision = result.build.revision(webkitRepositoryId); |
| var resultsPage = webkitRevision ? "http://" + master + "/results/" + builder + "/r" + webkitRevision + "%20(" + buildNumber + ")/results.html" |
| : 'javascript:alert("Could no resolve WebKit revision")'; |
| |
| var anchor = element('a', {'href': resultsPage }, [timeIfSlow]); |
| anchor.onmouseenter = function () { |
| var formattedRevisions = result.build.formattedRevisions(previousResult ? previousResult.build : null); |
| var revisionDescription = []; |
| for (var repositoryName in formattedRevisions) { |
| var revision = formattedRevisions[repositoryName]; |
| if (revisionDescription.length) |
| revisionDescription.push(', '); |
| if (revision.url) |
| revisionDescription.push(element('a', {'href': revision.url}, [revision.label])); |
| else |
| revisionDescription.push(revision.label); |
| } |
| |
| TestResultsView.showTooltip(anchor, element('div', {'class': 'tooltip'}, [ |
| element('ul', [ |
| element('li', ['Build Time: ' + result.build.formattedBuildTime()]), |
| element('li', ['Revision: '].concat(revisionDescription)), |
| element('li', ['Build: ', element('a', {'href': result.build.buildUrl()}, [buildNumber]), ' (', |
| element('a', {'href': resultsPage}, ['results']), ')']), |
| element('li', ['Actual: ' + actual]), |
| element('li', ['Expected: ' + expected]), |
| ]) |
| ])); |
| } |
| var cell = element('span', { |
| 'class': actual + ' resultCell', |
| }, [anchor]); |
| return cell; |
| } |
| |
| TestResultsView._populateTestPane = function(testName, results, section) { |
| var test = {name: testName, category: 'LayoutTest'}; // FIXME: Use the real test object. |
| var table = element('table', {'class': 'resultsTable tablesorter'}, [element('caption', [this._linkifiedTestName(test)])]); |
| |
| var resultsByBuilder = results['builders']; |
| var buildTimes = new Array(); |
| for (var builderId in resultsByBuilder) { |
| var results = resultsByBuilder[builderId]; |
| this._createBuildsAndComputeSlownessOfResults(builderId, results); |
| for (var i = 0; i < results.length; i++) { |
| var time = results[i].build.time(); |
| if (buildTimes.indexOf(time) < 0) |
| buildTimes.push(time); |
| } |
| } |
| buildTimes.sort(function (a, b) { return b - a; }); |
| |
| var repositories = []; |
| for (var repositoryId in this._repositories) |
| repositories.push(this._repositories[repositoryId]); |
| |
| table.appendChild(this._createTestResultHeader('Builder', repositories)); |
| |
| var tbody = element('tbody'); |
| var builders = []; |
| for (var builderId in resultsByBuilder) |
| builders.push(this._builders[builderId]); |
| |
| var self = this; |
| this._sortObjectsByName(builders).forEach(function (builder) { |
| tbody.appendChild(self._createTestResultRow(builder.name, resultsByBuilder[builder.id], test, builder, buildTimes, repositories)); |
| }) |
| |
| table.appendChild(tbody); |
| section.appendChild(table); |
| $(table).tablesorter(); |
| } |
| |
| TestResultsView._sortObjectsByName = function (list) { |
| return list.sort(function (a, b) { |
| if (a.name < b.name) |
| return -1; |
| if (a.name > b.name) |
| return 1; |
| return 0; |
| }); |
| } |
| |
| TestResultsView._urlFromTest = function (test) { |
| var category = this._testCategories[test.category]; |
| if (!category) |
| return null; |
| return category.url.replace(/\$testName/g, test.name); |
| } |
| |
| TestResultsView._linkifiedTestName = function (test) { |
| var url = this._urlFromTest(test); |
| return url ? element('a', {'href': url}, [test.name]) : test.name; |
| } |
| |
| TestResultsView._createTestResultHeader = function (labelForFirstColumn, repositories) { |
| var latestResultsLabel = ''; |
| if (repositories) { |
| latestResultsLabel = ['Latest results', element('br'), |
| repositories.map(function (repository) { return repository.name; }).join(' / ')]; |
| } |
| |
| return element('thead', [element('tr', [ |
| element('th', [labelForFirstColumn]), |
| element('th', ['Bug']), |
| element('th', ['Expectations']), |
| element('th', ['Slowest']), |
| element('th', latestResultsLabel), |
| element('th')])]); |
| } |
| |
| TestResultsView._createBuildsAndComputeSlownessOfResults = function (builderId, results) { |
| for (var i = 0; i < results.length; i++) { |
| var result = results[i]; |
| result.builder = builderId; |
| result.build = new TestBuild(this._repositories, this._builders, result); |
| result.roundedTime = result.time > 10000 ? Math.round(result.time / 1000) : Math.round(result.time / 100) / 10; |
| result.isSlow = result.time > 1000; |
| } |
| } |
| |
| TestResultsView._createTestResultRow = function (title, results, test, builder, buildTimes, repositories) { |
| var sortedResults = results.sort(function (result1, result2) { return result2.build.time() - result1.build.time(); }); |
| var cells = new Array(); |
| |
| function addEmptyCell() { cells.push(element('span', {'class': 'resultCell'}, [element('a')])); } |
| |
| var buildTimeIndex = 0; |
| for (var i = 0; i < sortedResults.length; i++) { |
| var result = sortedResults[i]; |
| if (buildTimes) { |
| while (buildTimes[buildTimeIndex] > result.build.time()) { |
| addEmptyCell(); |
| buildTimeIndex++; |
| } |
| if (buildTimes[buildTimeIndex] == result.build.time()) |
| buildTimeIndex++; |
| } |
| cells.push(this._createResultCell(builder.master, builder.name, result, sortedResults[i - 1])); |
| } |
| if (buildTimes) { |
| while (buildTimeIndex < buildTimes.length) { |
| addEmptyCell(); |
| buildTimeIndex++; |
| } |
| } |
| |
| var seenBugLink = false; |
| var formattedModifiers = sortedResults[0].modifiers.split(' ').map(function (modifier) { |
| if (modifier.indexOf('/') > 0) { |
| seenBugLink = true; |
| return element('a', {'href': (modifier.indexOf('http') == 0 ? '' : 'http://') + modifier}, [modifier]); |
| } |
| return modifier; |
| }); |
| |
| if (!seenBugLink) { |
| // FIXME: Make bug tracker configurable. |
| for (var i = 0; i < this._newBugLinks.length; i++) { |
| if (i) |
| formattedModifiers.push(' / '); |
| var link = this._newBugLinks[i]; |
| formattedModifiers.push(element('a', {'href': link.url.replace(/\$testName/g, test.name)}, link.label)); |
| } |
| } |
| |
| var slowestTime = Math.max.apply(Math, results.map(function (result) { return result.roundedTime; })); |
| if (slowestTime >= 1) |
| slowestTime += 's'; |
| else |
| slowestTime = ''; |
| |
| var latestRevisions = []; |
| if (repositories) { |
| var build = sortedResults[0].build; |
| for (var i = 0; i < repositories.length; i++) { |
| if (!build.revision(repositories[i].id)) |
| continue; |
| var revisionInfo = build.formattedRevision(repositories[i].id); |
| if (latestRevisions.length) |
| latestRevisions.push(' / '); |
| if (revisionInfo.url) |
| latestRevisions.push(element('a', {'href': revisionInfo.url}, [revisionInfo.label])); |
| else |
| latestRevisions.push(revisionInfo.label); |
| } |
| } |
| |
| return element('tr', [ |
| element('th', [title]), |
| element('td', {'class': 'modifiers'}, formattedModifiers), |
| element('td', {'class': 'expected'}, [sortedResults[0].expected]), |
| element('td', {'class': 'slowestTime'}, [slowestTime])] |
| .concat(element('td', latestRevisions)) |
| .concat([element('td', cells)])); |
| } |
| |
| TestResultsView.fetchTest = function (testName) { |
| if (this._tests.indexOf(testName) >= 0) |
| return; |
| |
| var self = this; |
| |
| var closeButton = element('div', {'class': 'closeButton'}); |
| closeButton.innerHTML = '<svg viewBox="0 0 100 100"><g stroke-width="10">' |
| + '<circle cx="50" cy="50" r="45" fill="transparent"></circle><polygon points="30,30 70,70"></polygon>' |
| + '<polygon points="30,70 70,30"></polygon></g></svg>'; |
| closeButton.addEventListener('click', function (event) { |
| self._removeTest(testName); |
| section.parentNode.removeChild(section); |
| event.preventDefault(); |
| }); |
| var section = element('section', {'id': testName, 'class': 'testResults'}, [closeButton, 'Loading...']); |
| |
| document.getElementById('testView').appendChild(section); |
| |
| var xhr = new XMLHttpRequest(); |
| xhr.open("GET", 'api/results.php?test=' + testName, true); |
| xhr.onload = function(event) { |
| var response = JSON.parse(xhr.response); |
| section.removeChild(section.lastChild); // Remove Loading... |
| if (response['status'] != 'OK') { |
| section.appendChild(text('Failed to load results for ' + testName + ': ' + response['status'])); |
| return; |
| } |
| self._populateTestPane(testName, response, section); |
| } |
| xhr.send(); |
| this._tests.push(testName); |
| } |
| |
| TestResultsView._removeTest = function (testName) { |
| var index = this._tests.indexOf(testName); |
| if (index < 0) |
| return; |
| this._tests.splice(index, 1); |
| this.updateLocationHash(); |
| } |
| |
| TestResultsView.fetchTests = function (testNames, doNotUpdateHash) { |
| for (var i = 0; i < testNames.length; i++) |
| this.fetchTest(testNames[i], doNotUpdateHash); |
| if (!doNotUpdateHash) |
| this.updateLocationHash(); |
| } |
| |
| TestResultsView._populateBuilderPane = function(builderName, failureType, results, section) { |
| var table = element('table', {'class': 'resultsTable tablesorter'}, [element('caption', [builderName])]); |
| var resultsByBuilder = results['builders']; |
| var resultsByTests; |
| var builderId; |
| for (var currentBuilderId in resultsByBuilder) { |
| builderId = currentBuilderId; |
| resultsByTests = resultsByBuilder[builderId]; |
| } |
| if (!resultsByTests) |
| return; |
| |
| table.appendChild(this._createTestResultHeader('Test')); |
| |
| var tests = []; |
| for (var testId in resultsByTests) |
| tests.push(this._availableTests[testId]); |
| |
| var tbody = element('tbody'); |
| var builder = this._builders[builderId]; |
| var self = this; |
| this._sortObjectsByName(tests).forEach(function (test) { |
| var results = resultsByTests[test.id]; |
| if (!results.length) |
| return; |
| self._createBuildsAndComputeSlownessOfResults(builderId, results); |
| var externalTestLink = element('a', {'class': 'externalTestLink', |
| 'href': self._urlFromTest(test), |
| 'title': 'View the source code of ' + test.name}); |
| var testName = element('a', {'href':'#', |
| 'title': 'Show results of ' + test.name + ' on all platforms'}, [test.name]); |
| testName.onclick = function (event) { |
| self.fetchTests([this.textContent]); |
| event.preventDefault(); |
| return false; |
| } |
| tbody.appendChild(self._createTestResultRow(element('span', [testName, externalTestLink]), results, test, builder)); |
| }); |
| |
| table.appendChild(tbody); |
| section.appendChild(table); |
| |
| $(table).tablesorter(); |
| } |
| |
| TestResultsView.fetchFailingTestsForBuilder = function (builderName, numberOfDays, failureType, doNotUpdateHash) { |
| var section = element('section', {'class': 'testResults'}, ['Loading...']); |
| |
| var container = document.getElementById('builderFailingTestsView'); |
| container.innerHTML = ''; |
| container.appendChild(section); |
| |
| var self = this; |
| var xhr = new XMLHttpRequest(); |
| var builderId = this._builderByName[builderName].id; |
| xhr.open('GET', 'data/' + builderId + '-' + failureType + '.json', true); |
| xhr.onload = function(event) { |
| if (xhr.status != 200) { |
| section.appendChild(text('Failed to load results for ' + builderName + ': ' + xhr.status)); |
| return; |
| } |
| var response = JSON.parse(xhr.response); |
| section.innerHTML = ''; |
| if (response['status'] != 'OK') { |
| section.appendChild(text('Failed to load results for ' + builderName + ': ' + response['status'])); |
| return; |
| } |
| // FIXME: Normalize failureType. |
| self._currentBuilder = builderName; |
| self._currentBuilderFailureType = failureType; |
| self._currentBuilderDays = numberOfDays; |
| self._populateBuilderPane(builderName, failureType, response, section); |
| if (!doNotUpdateHash) |
| self.updateLocationHash(); |
| } |
| xhr.send(); |
| } |
| |
| TestResultsView._createLocationHash = function (tests) { |
| var params = { |
| 'tests': tests.join(','), |
| 'builder': this._currentBuilder, |
| 'builderFailureType': this._currentBuilderFailureType, |
| 'builderDays': this._currentBuilderDays, |
| }; |
| var hash = ''; |
| for (var key in params) { |
| var value = params[key]; |
| if (value === null || value === undefined) |
| continue; |
| hash += '&' + decodeURIComponent(key) + '=' + decodeURIComponent(value); |
| } |
| return hash; |
| } |
| |
| TestResultsView.updateLocationHash = function () { |
| location.hash = this._createLocationHash(this._tests); |
| this._oldHash = location.hash; |
| } |
| |
| TestResultsView.locationHashChanged = function () { |
| var newHash = location.hash; |
| if (newHash == this._oldHash) |
| return; |
| this._oldHash = newHash; |
| this._tests = []; |
| this.loadTestsFromLocationHash(); |
| } |
| |
| TestResultsView.loadTestsFromLocationHash = function () { |
| var parsed = {}; |
| location.hash.substr(1).split('&').forEach(function (component) { |
| var equalPosition = component.indexOf('='); |
| if (equalPosition < 0) |
| return; |
| var name = decodeURIComponent(component.substr(0, equalPosition)); |
| parsed[name] = decodeURIComponent(component.substr(equalPosition + 1)); |
| }); |
| var doNotUpdateHash = true; |
| if (parsed['tests']) { |
| document.getElementById('testView').innerHTML = ''; |
| this.fetchTests(parsed['tests'].split(','), doNotUpdateHash); |
| } |
| if (parsed['builder']) { |
| this.fetchFailingTestsForBuilder( |
| parsed['builder'], |
| parseInt(parsed['builderDays']) || 5, |
| parsed['builderFailureType'] || 'failing', doNotUpdateHash); |
| } |
| return parsed; |
| } |
| |
| function fetchManifest(callback) { |
| var xhr = new XMLHttpRequest(); |
| xhr.open("GET", 'api/manifest.php', true); |
| xhr.onload = function(event) { |
| var response = JSON.parse(xhr.response); |
| if (response['status'] != 'OK') { |
| alert('Failed to load manifest:' + response['status']); |
| console.log(response); |
| return; |
| } |
| callback(response); |
| } |
| xhr.send(); |
| } |
| |
| fetchManifest(function (response) { |
| var testNames = response['tests'].map(function (test) { return test['name']; }) |
| var input = document.getElementById('testName'); |
| input.autocompleter = new Autocompleter(input, testNames); |
| |
| input.form.onsubmit = function (event) { |
| addTests(input.value); |
| event.preventDefault(); |
| return false; |
| } |
| |
| function addTests(filter) { |
| var candidates = input.autocompleter.filterCandidates(filter); |
| if (candidates.length > 15) { |
| if (!confirm('Are you sure you want to add ' + candidates.length + ' tests that match this substring?')) |
| return; |
| } |
| for (var i = 0; i < candidates.length; i++) |
| TestResultsView.fetchTest(candidates[i]); |
| TestResultsView.updateLocationHash(); |
| } |
| |
| var builderListView = document.getElementById('builderListView'); |
| TestResultsView._sortObjectsByName(response['builders']).forEach(function (builder) { |
| builderListView.appendChild(element('option', [builder.name])); |
| }); |
| |
| var builderDaysView = document.getElementById('builderDaysView'); |
| var builderFailureTypeView = document.getElementById('builderFailureTypeView'); |
| |
| function updateBuilderView() { |
| if (builderListView.value) |
| TestResultsView.fetchFailingTestsForBuilder(builderListView.value, builderDaysView.value, builderFailureTypeView.value); |
| } |
| |
| builderListView.addEventListener('change', updateBuilderView); |
| builderDaysView.addEventListener('change', updateBuilderView); |
| builderFailureTypeView.addEventListener('change', updateBuilderView); |
| |
| function mapById(items) { |
| var results = {}; |
| for (var i = 0; i < items.length; i++) |
| results[items[i].id] = items[i]; |
| return results; |
| } |
| |
| TestResultsView.setAvailableTests(mapById(response['tests'])); |
| TestResultsView.setBuilders(mapById(response['builders'])); |
| TestResultsView.setSlaves(mapById(response['slaves'])); |
| TestResultsView.setRepositories(mapById(response['repositories'])); |
| TestResultsView.setTestCategories(response['testCategories']); |
| TestResultsView.setNewBugLinks(response['newBugLinks']); |
| // FIXME: Updating location.href shouldn't be TestResultsView's responsibility. |
| var parsedStates = TestResultsView.loadTestsFromLocationHash(); |
| if (parsedStates['builder']) { |
| builderListView.value = parsedStates['builder']; |
| builderDaysView.value = parsedStates['builderDays']; |
| builderFailureTypeView.value = parsedStates['builderFailureType']; |
| } |
| }); |
| |
| function pasteHelper(input, event) { |
| function removeJunkFromNRWTStdout(input) { |
| return input.replace(/(\[[\w ]+\])|(.+\:.+)/g, '').replace(/^[ \t]+|[ \t]+$/gm, ''); |
| } |
| |
| function removeJunkFromResultsPage(input) { |
| return input.replace(/(^[^\/]+$)|(^\+)/gm, '').replace(/\([^)]+\)/g, '').replace(/\s+[A-Za-z]+(\s+[A-Za-z]+)*\s*$/gm, ''); |
| } |
| |
| var text = event.clipboardData.getData('text/plain'); |
| if (text.indexOf('\n') < 0) |
| return; |
| |
| var urls = removeJunkFromResultsPage(removeJunkFromNRWTStdout(text)).split('\n'); |
| TestResultsView.fetchTests(urls.filter(function (url) { return url.length; })); |
| event.preventDefault(); |
| } |
| |
| </script> |
| </body> |
| </html> |