// Copyright (C) 2012 Google 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: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * 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. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT // OWNER OR 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. ////////////////////////////////////////////////////////////////////////////// // CONSTANTS ////////////////////////////////////////////////////////////////////////////// var FORWARD = 'forward'; var BACKWARD = 'backward'; var TEST_URL_BASE_PATH_FOR_BROWSING = 'http://src.chromium.org/viewvc/blink/trunk/LayoutTests/'; var TEST_URL_BASE_PATH_FOR_XHR = 'http://src.chromium.org/blink/trunk/LayoutTests/'; var TEST_RESULTS_BASE_PATH = 'https://storage.googleapis.com/chromium-layout-test-archives/'; var GPU_RESULTS_BASE_PATH = 'http://chromium-browser-gpu-tests.commondatastorage.googleapis.com/runs/' var RELEASE_TIMEOUT = 6; var DEBUG_TIMEOUT = 12; var SLOW_MULTIPLIER = 5; // FIXME: Figure out how to make this not be hard-coded. // Probably just include in the results.json files and get it from there. var VIRTUAL_SUITES = { 'virtual/gpu/fast/canvas': 'fast/canvas', 'virtual/gpu/canvas/philip': 'canvas/philip', 'virtual/threaded/compositing/visibility': 'compositing/visibility', 'virtual/threaded/compositing/webgl': 'compositing/webgl', 'virtual/gpu/fast/hidpi': 'fast/hidpi', 'virtual/softwarecompositing': 'compositing', 'virtual/deferred/fast/images': 'fast/images', 'virtual/gpu/compositedscrolling/overflow': 'compositing/overflow', 'virtual/gpu/compositedscrolling/scrollbars': 'scrollbars', }; var ACTUAL_RESULT_SUFFIXES = ['expected.txt', 'expected.png', 'actual.txt', 'actual.png', 'diff.txt', 'diff.png', 'wdiff.html', 'crash-log.txt']; var EXPECTATIONS_ORDER = ACTUAL_RESULT_SUFFIXES.filter(function(suffix) { return !string.endsWith(suffix, 'png'); }).map(function(suffix) { return suffix.split('.')[0] }); var resourceLoader; function generatePage(historyInstance) { if (historyInstance.crossDashboardState.useTestData) return; document.body.innerHTML = '
LOADING...
'; resourceLoader.showErrors(); // tests expands to all tests that match the CSV list. // result expands to all tests that ever have the given result if (historyInstance.dashboardSpecificState.tests || historyInstance.dashboardSpecificState.result) generatePageForIndividualTests(individualTests()); else generatePageForBuilder(historyInstance.dashboardSpecificState.builder || currentBuilderGroup().defaultBuilder()); for (var builder in currentBuilders()) processTestResultsForBuilderAsync(builder); postHeightChangedMessage(); } function handleValidHashParameter(historyInstance, key, value) { switch(key) { case 'result': case 'tests': history.validateParameter(historyInstance.dashboardSpecificState, key, value, function() { return string.isValidName(value); }); return true; case 'builder': history.validateParameter(historyInstance.dashboardSpecificState, key, value, function() { return value in currentBuilders(); }); return true; case 'sortColumn': history.validateParameter(historyInstance.dashboardSpecificState, key, value, function() { // Get all possible headers since the actual used set of headers // depends on the values in historyInstance.dashboardSpecificState, which are currently being set. var getAllTableHeaders = true; var headers = tableHeaders(getAllTableHeaders); for (var i = 0; i < headers.length; i++) { if (value == sortColumnFromTableHeader(headers[i])) return true; } return value == 'test' || value == 'builder'; }); return true; case 'sortOrder': history.validateParameter(historyInstance.dashboardSpecificState, key, value, function() { return value == FORWARD || value == BACKWARD; }); return true; case 'resultsHeight': case 'revision': history.validateParameter(historyInstance.dashboardSpecificState, key, Number(value), function() { return value.match(/^\d+$/); }); return true; case 'showChrome': case 'showExpectations': case 'showFlaky': case 'showLargeExpectations': case 'showNonFlaky': case 'showSlow': case 'showSkip': case 'showUnexpectedPasses': case 'showWontFix': historyInstance.dashboardSpecificState[key] = value == 'true'; return true; default: return false; } } // @param {Object} params New or modified query parameters as key: value. function handleQueryParameterChange(historyInstance, params) { for (key in params) { if (key == 'tests') { // Entering cross-builder view, only keep valid keys for that view. for (var currentKey in historyInstance.dashboardSpecificState) { if (isInvalidKeyForCrossBuilderView(currentKey)) { delete historyInstance.dashboardSpecificState[currentKey]; } } } else if (isInvalidKeyForCrossBuilderView(key)) { delete historyInstance.dashboardSpecificState.tests; delete historyInstance.dashboardSpecificState.result; } } return true; } var defaultDashboardSpecificStateValues = { sortOrder: BACKWARD, sortColumn: 'flakiness', showExpectations: false, // FIXME: Show flaky tests by default if you have a builder picked. // Ideally, we'd fix the dashboard to not pick a default builder and have // you pick one. In the interim, this is a good way to make the default // page load faster since we don't need to generate/layout a large table. showFlaky: false, showLargeExpectations: false, showChrome: true, showWontFix: false, showNonFlaky: false, showSkip: false, showUnexpectedPasses: false, resultsHeight: 300, revision: null, tests: '', result: '', builder: null }; var DB_SPECIFIC_INVALIDATING_PARAMETERS = { 'tests' : 'builder', 'testType': 'builder', 'group': 'builder' }; var flakinessConfig = { defaultStateValues: defaultDashboardSpecificStateValues, generatePage: generatePage, handleValidHashParameter: handleValidHashParameter, handleQueryParameterChange: handleQueryParameterChange, invalidatingHashParameters: DB_SPECIFIC_INVALIDATING_PARAMETERS }; // FIXME(jparent): Eventually remove all usage of global history object. var g_history = new history.History(flakinessConfig); g_history.parseCrossDashboardParameters(); ////////////////////////////////////////////////////////////////////////////// // GLOBALS ////////////////////////////////////////////////////////////////////////////// var g_perBuilderFailures = {}; // Maps test path to an array of {builder, testResults} objects. var g_testToResultsMap = {}; function createResultsObjectForTest(test, builder) { return { test: test, builder: builder, // HTML for display of the results in the flakiness column html: '', flipCount: 0, slowestTime: 0, isFlaky: false, bugs: [], expectations : '', rawResults: '', // List of all the results the test actually has. actualResults: [] }; } var TestTrie = function(builders, resultsByBuilder) { this._trie = {}; for (var builder in builders) { if (!resultsByBuilder[builder]) { console.warn("No results for builder: ", builder) continue; } var testsForBuilder = resultsByBuilder[builder].tests; for (var test in testsForBuilder) this._addTest(test.split('/'), this._trie); } } TestTrie.prototype.forEach = function(callback, startingTriePath) { var testsTrie = this._trie; if (startingTriePath) { var splitPath = startingTriePath.split('/'); while (splitPath.length && testsTrie) testsTrie = testsTrie[splitPath.shift()]; } if (!testsTrie) return; function traverse(trie, triePath) { if (trie == true) callback(triePath); else { for (var member in trie) traverse(trie[member], triePath ? triePath + '/' + member : member); } } traverse(testsTrie, startingTriePath); } TestTrie.prototype._addTest = function(test, trie) { var rootComponent = test.shift(); if (!test.length) { if (!trie[rootComponent]) trie[rootComponent] = true; return; } if (!trie[rootComponent] || trie[rootComponent] == true) trie[rootComponent] = {}; this._addTest(test, trie[rootComponent]); } // Map of all tests to true values. This is just so we can have the list of // all tests across all the builders. var g_allTestsTrie; function getAllTestsTrie() { if (!g_allTestsTrie) g_allTestsTrie = new TestTrie(currentBuilders(), g_resultsByBuilder); return g_allTestsTrie; } // Returns an array of tests to be displayed in the individual tests view. // Note that a directory can be listed as a test, so we expand that into all // tests in the directory. function individualTests() { if (g_history.dashboardSpecificState.result) return allTestsWithResult(g_history.dashboardSpecificState.result); if (!g_history.dashboardSpecificState.tests) return []; return individualTestsForSubstringList(); } function splitTestList() { // Convert windows slashes to unix slashes and spaces/newlines to commas. var tests = g_history.dashboardSpecificState.tests.replace(/\\/g, '/').replace('\n', ' ').replace(/\s+/g, ','); return tests.split(','); } function individualTestsForSubstringList() { var testList = splitTestList(); // If listing a lot of tests, assume you've passed in an explicit list of tests // instead of patterns to match against. The matching code below is super slow. // // Also, when showChrome is false, we're embedding the dashboard elsewhere and // an explicit test list is passed in. In that case, we don't want // a search for compositing/foo.html to also show virtual/softwarecompositing/foo.html. if (testList.length > 10 || !g_history.dashboardSpecificState.showChrome) return testList; // Put the tests into an object first and then move them into an array // as a way of deduping. var testsMap = {}; for (var i = 0; i < testList.length; i++) { var path = testList[i]; // Ignore whitespace entries as they'd match every test. if (path.match(/^\s*$/)) continue; var hasAnyMatches = false; getAllTestsTrie().forEach(function(triePath) { if (string.caseInsensitiveContains(triePath, path)) { testsMap[triePath] = 1; hasAnyMatches = true; } }); // If a path doesn't match any tests, then assume it's a full path // to a test that passes on all builders. if (!hasAnyMatches) testsMap[path] = 1; } var testsArray = []; for (var test in testsMap) testsArray.push(test); return testsArray; } function allTestsWithResult(result) { processTestRunsForAllBuilders(); var retVal = []; getAllTestsTrie().forEach(function(triePath) { for (var i = 0; i < g_testToResultsMap[triePath].length; i++) { if (g_testToResultsMap[triePath][i].actualResults.indexOf(result.toUpperCase()) != -1) { retVal.push(triePath); break; } } }); return retVal; } function processTestResultsForBuilderAsync(builder) { setTimeout(function() { processTestRunsForBuilder(builder); }, 0); } function processTestRunsForAllBuilders() { for (var builder in currentBuilders()) processTestRunsForBuilder(builder); } function processTestRunsForBuilder(builderName) { if (g_perBuilderFailures[builderName]) return; if (!g_resultsByBuilder[builderName]) { console.error('No tests found for ' + builderName); g_perBuilderFailures[builderName] = []; return; } var failures = []; var allTestsForThisBuilder = g_resultsByBuilder[builderName].tests; for (var test in allTestsForThisBuilder) { var resultsForTest = createResultsObjectForTest(test, builderName); var rawTest = g_resultsByBuilder[builderName].tests[test]; resultsForTest.rawTimes = rawTest.times; var rawResults = rawTest.results; resultsForTest.rawResults = rawResults; if (rawTest.expected) resultsForTest.expectations = rawTest.expected; if (rawTest.bugs) resultsForTest.bugs = rawTest.bugs; var failureMap = g_resultsByBuilder[builderName][results.FAILURE_MAP]; // FIXME: Switch to resultsByBuild var times = resultsForTest.rawTimes; var numTimesSeen = 0; var numResultsSeen = 0; var resultsIndex = 0; var resultsMap = {} for (var i = 0; i < times.length; i++) { numTimesSeen += times[i][results.RLE.LENGTH]; while (rawResults[resultsIndex] && numTimesSeen > (numResultsSeen + rawResults[resultsIndex][results.RLE.LENGTH])) { numResultsSeen += rawResults[resultsIndex][results.RLE.LENGTH]; resultsIndex++; } if (rawResults && rawResults[resultsIndex]) { var result = rawResults[resultsIndex][results.RLE.VALUE]; resultsMap[failureMap[result]] = true; } resultsForTest.slowestTime = Math.max(resultsForTest.slowestTime, times[i][results.RLE.VALUE]); } resultsForTest.actualResults = Object.keys(resultsMap); results.determineFlakiness(failureMap, rawResults, resultsForTest); failures.push(resultsForTest); if (!g_testToResultsMap[test]) g_testToResultsMap[test] = []; g_testToResultsMap[test].push(resultsForTest); } g_perBuilderFailures[builderName] = failures; } function linkHTMLToOpenWindow(url, text) { return '' + text + ''; } // Returns whether the result for index'th result for testName on builder was // a failure. function isFailure(builder, testName, index) { var currentIndex = 0; var rawResults = g_resultsByBuilder[builder].tests[testName].results; var failureMap = g_resultsByBuilder[builder][results.FAILURE_MAP]; for (var i = 0; i < rawResults.length; i++) { currentIndex += rawResults[i][results.RLE.LENGTH]; if (currentIndex > index) return results.isFailingResult(failureMap, rawResults[i][results.RLE.VALUE]); } console.error('Index exceeds number of results: ' + index); } // Returns an array of indexes for all builds where this test failed. function indexesForFailures(builder, testName) { var rawResults = g_resultsByBuilder[builder].tests[testName].results; var buildNumbers = g_resultsByBuilder[builder].buildNumbers; var failureMap = g_resultsByBuilder[builder][results.FAILURE_MAP]; var index = 0; var failures = []; for (var i = 0; i < rawResults.length; i++) { var numResults = rawResults[i][results.RLE.LENGTH]; if (results.isFailingResult(failureMap, rawResults[i][results.RLE.VALUE])) { for (var j = 0; j < numResults; j++) failures.push(index + j); } index += numResults; } return failures; } // Returns the path to the failure log for this non-webkit test. function pathToFailureLog(testName) { return '/steps/' + g_history.crossDashboardState.testType + '/logs/' + testName.split('.')[1] } function showPopupForBuild(e, builder, index, opt_testName) { var html = ''; var time = g_resultsByBuilder[builder].secondsSinceEpoch[index]; if (time) { var date = new Date(time * 1000); html += date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); } var buildNumber = g_resultsByBuilder[builder].buildNumbers[index]; var master = builders.master(builder); var buildBasePath = master.logPath(builder, buildNumber); html += ''; ui.popup.show(e.target, html); } function classNameForFailureString(failure) { return failure.replace(/(\+|\ )/, ''); } function htmlForTestResults(test) { var html = ''; var testResults = test.rawResults.concat(); var times = test.rawTimes.concat(); var builder = test.builder; var master = builders.master(builder); var buildNumbers = g_resultsByBuilder[builder].buildNumbers; var indexToReplaceCurrentResult = -1; var indexToReplaceCurrentTime = -1; for (var i = 0; i < buildNumbers.length; i++) { var currentResultArray, currentTimeArray, innerHTML, resultString; if (i > indexToReplaceCurrentResult) { currentResultArray = testResults.shift(); if (currentResultArray) { resultString = g_resultsByBuilder[builder][results.FAILURE_MAP][currentResultArray[results.RLE.VALUE]]; indexToReplaceCurrentResult += currentResultArray[results.RLE.LENGTH]; } else { resultString = results.NO_DATA; indexToReplaceCurrentResult += buildNumbers.length; } } if (i > indexToReplaceCurrentTime) { currentTimeArray = times.shift(); var currentTime = 0; if (currentResultArray) { currentTime = currentTimeArray[results.RLE.VALUE]; indexToReplaceCurrentTime += currentTimeArray[results.RLE.LENGTH]; } else indexToReplaceCurrentTime += buildNumbers.length; innerHTML = currentTime || ' '; } html += '' + innerHTML; } return html; } function shouldShowTest(testResult) { if (!g_history.isLayoutTestResults()) return true; if (testResult.expectations == 'WONTFIX') return g_history.dashboardSpecificState.showWontFix; if (testResult.expectations == results.SKIP) return g_history.dashboardSpecificState.showSkip; if (testResult.isFlaky) return g_history.dashboardSpecificState.showFlaky; return g_history.dashboardSpecificState.showNonFlaky; } function createBugHTML(test) { var symptom = test.isFlaky ? 'flaky' : 'failing'; var title = encodeURIComponent('Layout Test ' + test.test + ' is ' + symptom); var description = encodeURIComponent('The following layout test is ' + symptom + ' on ' + '[insert platform]\n\n' + test.test + '\n\nProbable cause:\n\n' + '[insert probable cause]'); url = 'https://code.google.com/p/chromium/issues/entry?template=Layout%20Test%20Failure&summary=' + title + '&comment=' + description; return 'File new bug'; } function isCrossBuilderView() { return g_history.dashboardSpecificState.tests || g_history.dashboardSpecificState.result; } function tableHeaders(opt_getAll) { var headers = []; if (isCrossBuilderView() || opt_getAll) headers.push('builder'); if (!isCrossBuilderView() || opt_getAll) headers.push('test'); if (g_history.isLayoutTestResults() || opt_getAll) headers.push('bugs', 'expectations'); headers.push('slowest run', 'flakiness (numbers are runtimes in seconds)'); return headers; } function linkifyBugs(bugs) { var html = ''; bugs.forEach(function(bug) { var bugHtml; if (string.startsWith(bug, 'Bug(')) bugHtml = bug; else bugHtml = '' + bug + ''; html += '
' + bugHtml + '
' }); return html; } function htmlForSingleTestRow(test, showBuilderNames) { var headers = tableHeaders(); var html = ''; for (var i = 0; i < headers.length; i++) { var header = headers[i]; if (string.startsWith(header, 'test') || string.startsWith(header, 'builder')) { var testCellClassName = 'test-link' + (showBuilderNames ? ' builder-name' : ''); var testCellHTML = showBuilderNames ? test.builder : '' + test.test + ''; html += '' + testCellHTML; } else if (string.startsWith(header, 'bugs')) // FIXME: linkify bugs. html += '' + (linkifyBugs(test.bugs) || createBugHTML(test)); else if (string.startsWith(header, 'expectations')) html += '' + test.expectations; else if (string.startsWith(header, 'slowest')) html += '' + (test.slowestTime ? test.slowestTime + 's' : ''); else if (string.startsWith(header, 'flakiness')) html += htmlForTestResults(test); } return html; } function sortColumnFromTableHeader(headerText) { return headerText.split(' ', 1)[0]; } function htmlForTableColumnHeader(headerName, opt_fillColSpan) { // Use the first word of the header title as the sortkey var thisSortValue = sortColumnFromTableHeader(headerName); var arrowHTML = thisSortValue == g_history.dashboardSpecificState.sortColumn ? '' + (g_history.dashboardSpecificState.sortOrder == FORWARD ? '↑' : '↓' ) + '' : ''; return '
' + arrowHTML + '' + headerName + '' + arrowHTML + '
'; } function htmlForTestTable(rowsHTML, opt_excludeHeaders) { var html = ''; if (!opt_excludeHeaders) { html += ''; var headers = tableHeaders(); for (var i = 0; i < headers.length; i++) html += htmlForTableColumnHeader(headers[i], i == headers.length - 1); html += ''; } return html + '' + rowsHTML + '
'; } function appendHTML(html) { // InnerHTML to a div that's not in the document. This is // ~300ms faster in Safari 4 and Chrome 4 on mac. var div = document.createElement('div'); div.innerHTML = html; document.body.appendChild(div); postHeightChangedMessage(); } function alphanumericCompare(column, reverse) { return reversibleCompareFunction(function(a, b) { // Put null entries at the bottom var a = a[column] ? String(a[column]) : 'z'; var b = b[column] ? String(b[column]) : 'z'; if (a < b) return -1; else if (a == b) return 0; else return 1; }, reverse); } function numericSort(column, reverse) { return reversibleCompareFunction(function(a, b) { a = parseFloat(a[column]); b = parseFloat(b[column]); return a - b; }, reverse); } function reversibleCompareFunction(compare, reverse) { return function(a, b) { return compare(reverse ? b : a, reverse ? a : b); }; } function changeSort(e) { var target = e.currentTarget; e.preventDefault(); var sortValue = target.getAttribute('sortValue'); while (target && target.tagName != 'TABLE') target = target.parentNode; var sort = 'sortColumn'; var orderKey = 'sortOrder'; if (sortValue == g_history.dashboardSpecificState[sort] && g_history.dashboardSpecificState[orderKey] == FORWARD) order = BACKWARD; else order = FORWARD; g_history.setQueryParameter(sort, sortValue, orderKey, order); } function sortTests(tests, column, order) { var resultsProperty, sortFunctionGetter; if (column == 'flakiness') { sortFunctionGetter = numericSort; resultsProperty = 'flipCount'; } else if (column == 'slowest') { sortFunctionGetter = numericSort; resultsProperty = 'slowestTime'; } else { sortFunctionGetter = alphanumericCompare; resultsProperty = column; } tests.sort(sortFunctionGetter(resultsProperty, order == BACKWARD)); } function htmlForIndividualTestOnAllBuilders(test) { processTestRunsForAllBuilders(); var testResults = g_testToResultsMap[test]; if (!testResults) return '
Test not found. Either it does not exist, is skipped or passes on all recorded runs.
'; var html = ''; var shownBuilders = []; for (var j = 0; j < testResults.length; j++) { shownBuilders.push(testResults[j].builder); var showBuilderNames = true; html += htmlForSingleTestRow(testResults[j], showBuilderNames); } var skippedBuilders = [] for (builder in currentBuilders()) { if (shownBuilders.indexOf(builder) == -1) skippedBuilders.push(builder); } var skippedBuildersHtml = ''; if (skippedBuilders.length) { skippedBuildersHtml = '
The following builders either don\'t run this test (e.g. it\'s skipped) or all recorded runs passed:
' + '
' + skippedBuilders.join('
') + '
'; } return htmlForTestTable(html) + skippedBuildersHtml; } function htmlForIndividualTestOnAllBuildersWithResultsLinks(test) { processTestRunsForAllBuilders(); var testResults = g_testToResultsMap[test]; var html = ''; html += htmlForIndividualTestOnAllBuilders(test); html += '
' + linkHTMLToToggleState('showExpectations', 'results') if (g_history.isLayoutTestResults() || g_history.isGPUTestResults()) { if (g_history.isLayoutTestResults()) html += ' | ' + linkHTMLToToggleState('showLargeExpectations', 'large thumbnails'); html += ' | Only shows actual results/diffs from the most recent *failure* on each bot.'; } else { html += ' | Results height:px'; } html += '
'; return html; } function maybeAddPngChecksum(expectationDiv, pngUrl) { // pngUrl gets served from the browser cache since we just loaded it in an // tag. loader.request(pngUrl, function(xhr) { // Convert the first 2k of the response to a byte string. var bytes = xhr.responseText.substring(0, 2048); for (var position = 0; position < bytes.length; ++position) bytes[position] = bytes[position] & 0xff; // Look for the comment. var commentKey = 'tEXtchecksum\x00'; var checksumPosition = bytes.indexOf(commentKey); if (checksumPosition == -1) return; var checksum = bytes.substring(checksumPosition + commentKey.length, checksumPosition + commentKey.length + 32); var checksumContainer = document.createElement('span'); checksumContainer.innerText = 'Embedded checksum: ' + checksum; checksumContainer.setAttribute('class', 'pngchecksum'); expectationDiv.parentNode.appendChild(checksumContainer); }, function(xhr) {}, true); } function getOrCreate(className, parent) { var element = parent.querySelector('.' + className); if (!element) { element = document.createElement('div'); element.className = className; parent.appendChild(element); } return element; } function handleExpectationsItemLoad(title, item, itemType, parent) { item.className = 'expectation'; if (g_history.dashboardSpecificState.showLargeExpectations) item.className += ' large'; var titleContainer = document.createElement('h3'); titleContainer.className = 'expectations-title'; titleContainer.textContent = title; var itemContainer = document.createElement('span'); itemContainer.appendChild(titleContainer); itemContainer.className = 'expectations-item ' + title; itemContainer.appendChild(item); // Separate text and image results into separate divs.. var typeContainer = getOrCreate(itemType, parent); // Insert results in a consistent order. var index = EXPECTATIONS_ORDER.indexOf(title); while (index < EXPECTATIONS_ORDER.length) { index++; var elementAfter = typeContainer.querySelector('.' + EXPECTATIONS_ORDER[index]); if (elementAfter) { typeContainer.insertBefore(itemContainer, elementAfter); break; } } if (!itemContainer.parentNode) typeContainer.appendChild(itemContainer); handleFinishedLoadingExpectations(parent); } function addExpectationItem(expectationsContainers, parentContainer, url, opt_builder) { // Group expectations by builder, putting test and reference files first. var builder = opt_builder || "Test and reference files"; var container = expectationsContainers[builder]; if (!container) { container = document.createElement('div'); container.className = 'expectations-container'; container.setAttribute('data-builder', builder); parentContainer.appendChild(container); expectationsContainers[builder] = container; } var numUnloaded = container.getAttribute('data-unloaded') || 0; container.setAttribute('data-unloaded', ++numUnloaded); var isImage = url.match(/\.png$/); var appendExpectationsItem = function(item) { var itemType = isImage ? 'image' : 'text'; handleExpectationsItemLoad(expectationsTitle(url), item, itemType, container); }; var handleLoadError = function() { handleFinishedLoadingExpectations(container); }; if (isImage) { var dummyNode = document.createElement('img'); dummyNode.onload = function() { var item = dummyNode; maybeAddPngChecksum(item, url); appendExpectationsItem(item); } dummyNode.onerror = handleLoadError; dummyNode.src = url; } else { loader.request(url, function(xhr) { var item = document.createElement('pre'); if (string.endsWith(url, '-wdiff.html')) item.innerHTML = xhr.responseText; else item.textContent = xhr.responseText; appendExpectationsItem(item); }, handleLoadError); } } function handleFinishedLoadingExpectations(container) { var numUnloaded = container.getAttribute('data-unloaded') - 1; container.setAttribute('data-unloaded', numUnloaded); if (numUnloaded) return; if (!container.firstChild) { container.remove(); return; } var builder = container.getAttribute('data-builder'); if (!builder) return; var header = document.createElement('h2'); header.textContent = builder; container.insertBefore(header, container.firstChild); } function expectationsTitle(url) { var matchingSuffixes = ACTUAL_RESULT_SUFFIXES.filter(function(suffix) { return string.endsWith(url, suffix); }); if (matchingSuffixes.length) return matchingSuffixes[0].split('.')[0]; var parts = url.split('/'); return parts[parts.length - 1]; } function loadExpectations(expectationsContainer) { var test = expectationsContainer.getAttribute('test'); if (g_history.isLayoutTestResults()) loadExpectationsLayoutTests(test, expectationsContainer); else { var testResults = g_testToResultsMap[test]; for (var i = 0; i < testResults.length; i++) if (g_history.isGPUTestResults()) loadGPUResultsForBuilder(testResults[i].builder, test, expectationsContainer); else loadNonWebKitResultsForBuilder(testResults[i].builder, test, expectationsContainer); } } function gpuResultsPath(chromeRevision, builder) { return chromeRevision + '_' + builder.replace(/[^A-Za-z0-9]+/g, '_'); } function loadGPUResultsForBuilder(builder, test, expectationsContainer) { var container = document.createElement('div'); container.className = 'expectations-container'; container.innerHTML = '
' + builder + '
'; expectationsContainer.appendChild(container); var failureIndex = indexesForFailures(builder, test)[0]; var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndex]; var pathToLog = builders.master(builder).logPath(builder, buildNumber) + pathToFailureLog(test); var chromeRevision = g_resultsByBuilder[builder].chromeRevision[failureIndex]; var resultsUrl = GPU_RESULTS_BASE_PATH + gpuResultsPath(chromeRevision, builder); var filename = test.split(/\./)[1] + '.png'; appendNonWebKitResults(container, pathToLog, 'non-webkit-results'); appendNonWebKitResults(container, resultsUrl + '/gen/' + filename, 'gpu-test-results', 'Generated'); appendNonWebKitResults(container, resultsUrl + '/ref/' + filename, 'gpu-test-results', 'Reference'); appendNonWebKitResults(container, resultsUrl + '/diff/' + filename, 'gpu-test-results', 'Diff'); } function loadNonWebKitResultsForBuilder(builder, test, expectationsContainer) { var failureIndexes = indexesForFailures(builder, test); var container = document.createElement('div'); container.innerHTML = '
' + builder + '
'; expectationsContainer.appendChild(container); for (var i = 0; i < failureIndexes.length; i++) { // FIXME: This doesn't seem to work anymore. Did the paths change? // Once that's resolved, see if we need to try each gtest modifier prefix as well. var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndexes[i]]; var pathToLog = builders.master(builder).logPath(builder, buildNumber) + pathToFailureLog(test); appendNonWebKitResults(container, pathToLog, 'non-webkit-results'); } } function appendNonWebKitResults(container, url, itemClassName, opt_title) { // Use a script tag to detect whether the URL 404s. // Need to use a script tag since the URL is cross-domain. var dummyNode = document.createElement('script'); dummyNode.src = url; dummyNode.onload = function() { var item = document.createElement('iframe'); item.src = dummyNode.src; item.className = itemClassName; item.style.height = g_history.dashboardSpecificState.resultsHeight + 'px'; if (opt_title) { var childContainer = document.createElement('div'); childContainer.style.display = 'inline-block'; var title = document.createElement('div'); title.textContent = opt_title; childContainer.appendChild(title); childContainer.appendChild(item); container.replaceChild(childContainer, dummyNode); } else container.replaceChild(item, dummyNode); } dummyNode.onerror = function() { container.removeChild(dummyNode); } container.appendChild(dummyNode); } function lookupVirtualTestSuite(test) { for (var suite in VIRTUAL_SUITES) { if (test.indexOf(suite) != -1) return suite; } return ''; } function baseTest(test, suite) { base = VIRTUAL_SUITES[suite]; return base ? test.replace(suite, base) : test; } function loadTestAndReferenceFiles(expectationsContainers, expectationsContainer, test) { var testWithoutSuffix = test.substring(0, test.lastIndexOf('.')); var reftest_html_file = testWithoutSuffix + "-expected.html"; var reftest_mismatch_html_file = testWithoutSuffix + "-expected-mismatch.html"; var suite = lookupVirtualTestSuite(test); if (suite) { loadTestAndReferenceFiles(expectationsContainers, expectationsContainer, baseTest(test, suite)); return; } addExpectationItem(expectationsContainers, expectationsContainer, TEST_URL_BASE_PATH_FOR_XHR + test); addExpectationItem(expectationsContainers, expectationsContainer, TEST_URL_BASE_PATH_FOR_XHR + reftest_html_file); addExpectationItem(expectationsContainers, expectationsContainer, TEST_URL_BASE_PATH_FOR_XHR + reftest_mismatch_html_file); } function loadExpectationsLayoutTests(test, expectationsContainer) { // Map from file extension to container div for expectations of that type. var expectationsContainers = {}; loadTestAndReferenceFiles(expectationsContainers, expectationsContainer, test); var testWithoutSuffix = test.substring(0, test.lastIndexOf('.')); for (var builder in currentBuilders()) { var actualResultsBase = TEST_RESULTS_BASE_PATH + currentBuilders()[builder] + '/results/layout-test-results/'; ACTUAL_RESULT_SUFFIXES.forEach(function(suffix) {{ addExpectationItem(expectationsContainers, expectationsContainer, actualResultsBase + testWithoutSuffix + '-' + suffix, builder); }}) } // Add a clearing element so floated elements don't bleed out of their // containing block. var br = document.createElement('br'); br.style.clear = 'both'; expectationsContainer.appendChild(br); } function appendExpectations() { var expectations = g_history.dashboardSpecificState.showExpectations ? document.getElementsByClassName('expectations') : []; g_chunkedActionState = { items: expectations, index: 0 } performChunkedAction(function(expectation) { loadExpectations(expectation); postHeightChangedMessage(); }, hideLoadingUI, expectations); } function hideLoadingUI() { var loadingDiv = $('loading-ui'); if (loadingDiv) loadingDiv.style.display = 'none'; postHeightChangedMessage(); } function generatePageForIndividualTests(tests) { console.log('Number of tests: ' + tests.length); if (g_history.dashboardSpecificState.showChrome) appendHTML(htmlForNavBar()); performChunkedAction(function(test) { appendHTML(htmlForIndividualTest(test)); }, appendExpectations, tests); if (g_history.dashboardSpecificState.showChrome) { $('tests-input').value = g_history.dashboardSpecificState.tests; $('result-input').value = g_history.dashboardSpecificState.result; } } var g_chunkedActionRequestId; function performChunkedAction(action, onComplete, items, opt_index) { if (g_chunkedActionRequestId) cancelAnimationFrame(g_chunkedActionRequestId); var index = opt_index || 0; g_chunkedActionRequestId = requestAnimationFrame(function() { if (index < items.length) { action(items[index]); performChunkedAction(action, onComplete, items, ++index); } else { onComplete(); } }); } function htmlForIndividualTest(test) { var testNameHtml = ''; if (g_history.dashboardSpecificState.showChrome) { if (g_history.isLayoutTestResults()) { var suite = lookupVirtualTestSuite(test); var base = suite ? baseTest(test, suite) : test; var versionControlUrl = TEST_URL_BASE_PATH_FOR_BROWSING + base; testNameHtml += '

' + linkHTMLToOpenWindow(versionControlUrl, test) + '

'; } else testNameHtml += '

' + test + '

'; } return testNameHtml + htmlForIndividualTestOnAllBuildersWithResultsLinks(test); } function setTestsParameter(input) { g_history.setQueryParameter('tests', input.value); } function htmlForNavBar() { var extraHTML = ''; var html = ui.html.testTypeSwitcher(false, extraHTML, isCrossBuilderView()); html += '
Show all tests with result: ' + '' + '
Show tests on all platforms: ' + // Use a textarea to avoid the 32k limit on the length of inputs. '' + 'Show legend [type ?]
'; return html; } function checkBoxToToggleState(key, text) { var stateEnabled = g_history.dashboardSpecificState[key]; return ' '; } function linkHTMLToToggleState(key, linkText) { var stateEnabled = g_history.dashboardSpecificState[key]; return '' + (stateEnabled ? 'Hide' : 'Show') + ' ' + linkText + ''; } function headerForTestTableHtml() { return '

Failing tests

' + checkBoxToToggleState('showFlaky', 'Show flaky') + checkBoxToToggleState('showNonFlaky', 'Show non-flaky') + checkBoxToToggleState('showSkip', 'Show Skip') + checkBoxToToggleState('showWontFix', 'Show WontFix'); } function generatePageForBuilder(builderName) { processTestRunsForBuilder(builderName); var filteredResults = g_perBuilderFailures[builderName].filter(shouldShowTest); sortTests(filteredResults, g_history.dashboardSpecificState.sortColumn, g_history.dashboardSpecificState.sortOrder); var testsHTML = ''; if (filteredResults.length) { var tableRowsHTML = ''; var showBuilderNames = false; for (var i = 0; i < filteredResults.length; i++) tableRowsHTML += htmlForSingleTestRow(filteredResults[i], showBuilderNames) testsHTML = htmlForTestTable(tableRowsHTML); } else { if (g_history.isLayoutTestResults()) testsHTML += '
Fill in one of the text inputs or checkboxes above to show failures.
'; else testsHTML += '
No tests have failed!
'; } var html = htmlForNavBar(); if (g_history.isLayoutTestResults()) html += headerForTestTableHtml(); html += '
' + testsHTML; appendHTML(html); var ths = document.getElementsByTagName('th'); for (var i = 0; i < ths.length; i++) { ths[i].addEventListener('click', changeSort, false); ths[i].className = "sortable"; } hideLoadingUI(); } var VALID_KEYS_FOR_CROSS_BUILDER_VIEW = { tests: 1, result: 1, showChrome: 1, showExpectations: 1, showLargeExpectations: 1, resultsHeight: 1, revision: 1 }; function isInvalidKeyForCrossBuilderView(key) { return !(key in VALID_KEYS_FOR_CROSS_BUILDER_VIEW) && !(key in history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES); } function hideLegend() { var legend = $('legend'); if (legend) legend.parentNode.removeChild(legend); } function showLegend() { var legend = $('legend'); if (!legend) { legend = document.createElement('div'); legend.id = 'legend'; document.body.appendChild(legend); } var html = '
Hide ' + 'legend [type esc]
'; // Just grab the first failureMap. Technically, different builders can have different maps if they // haven't all cycled after the map was changed, but meh. var failureMap = g_resultsByBuilder[Object.keys(g_resultsByBuilder)[0]][results.FAILURE_MAP]; for (var expectation in failureMap) { var failureString = failureMap[expectation]; html += '
' + failureString + '
'; } if (g_history.isLayoutTestResults()) { html += '

' + ''; html += '
RELEASE TIMEOUTS:
' + htmlForSlowTimes(RELEASE_TIMEOUT) + '
DEBUG TIMEOUTS:
' + htmlForSlowTimes(DEBUG_TIMEOUT); } legend.innerHTML = html; } function htmlForSlowTimes(minTime) { return ''; } function postHeightChangedMessage() { if (window == parent) return; var root = document.documentElement; var height = root.offsetHeight; if (root.offsetWidth < root.scrollWidth) { // We have a horizontal scrollbar. Include it in the height. var dummyNode = document.createElement('div'); dummyNode.style.overflow = 'scroll'; document.body.appendChild(dummyNode); var scrollbarWidth = dummyNode.offsetHeight - dummyNode.clientHeight; document.body.removeChild(dummyNode); height += scrollbarWidth; } parent.postMessage({command: 'heightChanged', height: height}, '*') } if (window != parent) window.addEventListener('blur', ui.popup.hide); document.addEventListener('keydown', function(e) { if (e.keyIdentifier == 'U+003F' || e.keyIdentifier == 'U+00BF') { // WebKit MAC retursn 3F. WebKit WIN returns BF. This is a bug! // ? key showLegend(); } else if (e.keyIdentifier == 'U+001B') { // escape key hideLegend(); ui.popup.hide(); } }, false); window.addEventListener('load', function() { resourceLoader = new loader.Loader(); resourceLoader.load(); }, false);