| // 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 = '<div id="loading-ui">LOADING...</div>'; |
| 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: '', |
| flips: 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 '<a href="' + url + '" target="_blank">' + text + '</a>'; |
| } |
| |
| // 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 += '<ul><li>' + linkHTMLToOpenWindow(buildBasePath, 'Build log'); |
| |
| if (g_resultsByBuilder[builder][results.BLINK_REVISIONS]) |
| html += '</li><li>Blink: ' + ui.html.blinkRevisionLink(g_resultsByBuilder[builder], index) + '</li>'; |
| |
| html += '</li><li>Chromium: ' + ui.html.chromiumRevisionLink(g_resultsByBuilder[builder], index) + '</li>'; |
| |
| var chromeRevision = g_resultsByBuilder[builder].chromeRevision[index]; |
| if (chromeRevision && g_history.isLayoutTestResults()) { |
| html += '<li><a href="' + TEST_RESULTS_BASE_PATH + currentBuilders()[builder] + |
| '/' + buildNumber + '/layout-test-results.zip">layout-test-results.zip</a></li>'; |
| } |
| |
| if (!g_history.isLayoutTestResults() && opt_testName && isFailure(builder, opt_testName, index)) |
| html += '<li>' + linkHTMLToOpenWindow(buildBasePath + pathToFailureLog(opt_testName), 'Failure log') + '</li>'; |
| |
| html += '</ul>'; |
| 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 += '<td title="' + resultString + '. Click for more info." class="results ' + classNameForFailureString(resultString) + |
| '" onclick=\'showPopupForBuild(event, "' + builder + '",' + i + ',"' + test.test + '")\'>' + 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 '<a href="' + url + '">File new bug</a>'; |
| } |
| |
| 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 = '<a href="http://' + bug + '">' + bug + '</a>'; |
| html += '<div>' + bugHtml + '</div>' |
| }); |
| 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 : '<span class="link" onclick="g_history.setQueryParameter(\'tests\',\'' + test.test +'\');">' + test.test + '</span>'; |
| html += '<tr><td class="' + testCellClassName + '">' + testCellHTML; |
| } else if (string.startsWith(header, 'bugs')) |
| // FIXME: linkify bugs. |
| html += '<td class=options-container>' + (linkifyBugs(test.bugs) || createBugHTML(test)); |
| else if (string.startsWith(header, 'expectations')) |
| html += '<td class=options-container>' + test.expectations; |
| else if (string.startsWith(header, 'slowest')) |
| html += '<td>' + (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 ? |
| '<span class=' + g_history.dashboardSpecificState.sortOrder + '>' + (g_history.dashboardSpecificState.sortOrder == FORWARD ? '↑' : '↓' ) + '</span>' : ''; |
| return '<th sortValue=' + thisSortValue + |
| // Extend last th through all the rest of the columns. |
| (opt_fillColSpan ? ' colspan=10000' : '') + |
| // Extra span here is so flex boxing actually centers. |
| // There's probably a better way to do this with CSS only though. |
| '><div class=table-header-content><span></span>' + arrowHTML + |
| '<span class=header-text>' + headerName + '</span>' + arrowHTML + '</div></th>'; |
| } |
| |
| function htmlForTestTable(rowsHTML, opt_excludeHeaders) |
| { |
| var html = '<table class=test-table>'; |
| if (!opt_excludeHeaders) { |
| html += '<thead><tr>'; |
| var headers = tableHeaders(); |
| for (var i = 0; i < headers.length; i++) |
| html += htmlForTableColumnHeader(headers[i], i == headers.length - 1); |
| html += '</tr></thead>'; |
| } |
| return html + '<tbody>' + rowsHTML + '</tbody></table>'; |
| } |
| |
| 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 = 'flips'; |
| } 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 '<div class="not-found">Test not found. Either it does not exist, is skipped or passes on all recorded runs.</div>'; |
| |
| 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 = '<div>The following builders either don\'t run this test (e.g. it\'s skipped) or all recorded runs passed:</div>' + |
| '<div class=skipped-builder-list><div class=skipped-builder>' + skippedBuilders.join('</div><div class=skipped-builder>') + '</div></div>'; |
| } |
| |
| return htmlForTestTable(html) + skippedBuildersHtml; |
| } |
| |
| function htmlForIndividualTestOnAllBuildersWithResultsLinks(test) |
| { |
| processTestRunsForAllBuilders(); |
| |
| var testResults = g_testToResultsMap[test]; |
| var html = ''; |
| html += htmlForIndividualTestOnAllBuilders(test); |
| |
| html += '<div class=expectations test=' + test + '><div>' + |
| linkHTMLToToggleState('showExpectations', 'results') |
| |
| if (g_history.isLayoutTestResults() || g_history.isGPUTestResults()) { |
| if (g_history.isLayoutTestResults()) |
| html += ' | ' + linkHTMLToToggleState('showLargeExpectations', 'large thumbnails'); |
| html += ' | <b>Only shows actual results/diffs from the most recent *failure* on each bot.</b>'; |
| } else { |
| html += ' | <span>Results height:<input ' + |
| 'onchange="g_history.setQueryParameter(\'resultsHeight\',this.value)" value="' + |
| g_history.dashboardSpecificState.resultsHeight + '" style="width:2.5em">px</span>'; |
| } |
| html += '</div></div>'; |
| return html; |
| } |
| |
| function maybeAddPngChecksum(expectationDiv, pngUrl) |
| { |
| // pngUrl gets served from the browser cache since we just loaded it in an |
| // <img> 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 = '<div><b>' + builder + '</b></div>'; |
| 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 = '<div><b>' + builder + '</b></div>'; |
| 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 += '<h2>' + linkHTMLToOpenWindow(versionControlUrl, test) + '</h2>'; |
| } else |
| testNameHtml += '<h2>' + test + '</h2>'; |
| } |
| |
| 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 += '<div class=forms><form id=result-form ' + |
| 'onsubmit="g_history.setQueryParameter(\'result\', result.value);' + |
| 'return false;">Show all tests with result: ' + |
| '<input name=result placeholder="e.g. CRASH" id=result-input>' + |
| '</form><span>Show tests on all platforms: </span>' + |
| // Use a textarea to avoid the 32k limit on the length of inputs. |
| '<textarea name=tests ' + |
| 'placeholder="Comma or space-separated list of tests or partial ' + |
| 'paths to show test results across all builders, e.g., ' + |
| 'foo/bar.html,foo/baz,domstorage" id=tests-input onchange="setTestsParameter(this)" ' + |
| 'onkeydown="if (event.keyCode == 13) { setTestsParameter(this); return false; }"></textarea>' + |
| '<span class=link onclick="showLegend()">Show legend [type ?]</span></div>'; |
| return html; |
| } |
| |
| function checkBoxToToggleState(key, text) |
| { |
| var stateEnabled = g_history.dashboardSpecificState[key]; |
| return '<label><input type=checkbox ' + (stateEnabled ? 'checked ' : '') + 'onclick="g_history.setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + text + '</label> '; |
| } |
| |
| function linkHTMLToToggleState(key, linkText) |
| { |
| var stateEnabled = g_history.dashboardSpecificState[key]; |
| return '<span class=link onclick="g_history.setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + (stateEnabled ? 'Hide' : 'Show') + ' ' + linkText + '</span>'; |
| } |
| |
| function headerForTestTableHtml() |
| { |
| return '<h2 style="display:inline-block">Failing tests</h2>' + |
| 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 += '<div>Fill in one of the text inputs or checkboxes above to show failures.</div>'; |
| else |
| testsHTML += '<div>No tests have failed!</div>'; |
| } |
| |
| var html = htmlForNavBar(); |
| |
| if (g_history.isLayoutTestResults()) |
| html += headerForTestTableHtml(); |
| |
| html += '<br>' + 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 = '<div id=legend-toggle onclick="hideLegend()">Hide ' + |
| 'legend [type esc]</div><div id=legend-contents>'; |
| |
| // 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 += '<div class=' + classNameForFailureString(failureString) + '>' + failureString + '</div>'; |
| } |
| |
| if (g_history.isLayoutTestResults()) { |
| html += '</div><br style="clear:both">' + |
| '</div>'; |
| |
| html += '<div>RELEASE TIMEOUTS:</div>' + |
| htmlForSlowTimes(RELEASE_TIMEOUT) + |
| '<div>DEBUG TIMEOUTS:</div>' + |
| htmlForSlowTimes(DEBUG_TIMEOUT); |
| } |
| |
| legend.innerHTML = html; |
| } |
| |
| function htmlForSlowTimes(minTime) |
| { |
| return '<ul><li>' + minTime + ' seconds</li><li>' + |
| SLOW_MULTIPLIER * minTime + ' seconds if marked Slow in TestExpectations</li></ul>'; |
| } |
| |
| 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); |