| // 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 ALL = 'ALL'; |
| var FORWARD = 'forward'; |
| var BACKWARD = 'backward'; |
| var GTEST_MODIFIERS = ['FLAKY', 'FAILS', 'MAYBE', 'DISABLED']; |
| var TEST_URL_BASE_PATH_TRAC = 'https://trac.webkit.org/browser/trunk/LayoutTests/'; |
| var TEST_URL_BASE_PATH = "https://svn.webkit.org/repository/webkit/trunk/LayoutTests/"; |
| var EXPECTATIONS_URL_BASE_PATH = TEST_URL_BASE_PATH + "platform/"; |
| |
| var PLATFORMS = { |
| 'APPLE': { |
| subPlatforms: { |
| 'MAC': { |
| expectationsDirectory: 'mac', |
| subPlatforms: { |
| 'SIERRA': { |
| subPlatforms: { |
| 'WK1': { fallbackPlatforms: ['APPLE_MAC_SIERRA', 'APPLE_MAC'] }, |
| 'WK2': { fallbackPlatforms: ['APPLE_MAC_SIERRA', 'APPLE_MAC', 'WK2'], expectationsDirectory: 'mac-wk2'} |
| } |
| }, |
| 'HIGHSIERRA': { |
| subPlatforms: { |
| 'WK1': { fallbackPlatforms: ['APPLE_MAC_HIGHSIERRA', 'APPLE_MAC'] }, |
| 'WK2': { fallbackPlatforms: ['APPLE_MAC_HIGHSIERRA', 'APPLE_MAC', 'WK2'], expectationsDirectory: 'mac-wk2'} |
| } |
| }, |
| 'MOJAVE': { |
| subPlatforms: { |
| 'WK1': { fallbackPlatforms: ['APPLE_MAC_MOJAVE', 'APPLE_MAC'] }, |
| 'WK2': { fallbackPlatforms: ['APPLE_MAC_MOJAVE', 'APPLE_MAC', 'WK2'], expectationsDirectory: 'mac-wk2'} |
| } |
| }, |
| 'CATALINA': { |
| subPlatforms: { |
| 'WK1': { fallbackPlatforms: ['APPLE_MAC_CATALINA', 'APPLE_MAC'] }, |
| 'WK2': { fallbackPlatforms: ['APPLE_MAC_CATALINA', 'APPLE_MAC', 'WK2'], expectationsDirectory: 'mac-wk2'} |
| } |
| }, |
| } |
| }, |
| 'WIN': { |
| expectationsDirectory: 'win', |
| subPlatforms: { |
| 'XP': { fallbackPlatforms: ['APPLE_WIN'] }, |
| 'WIN7': { fallbackPlatforms: ['APPLE_WIN'] }, |
| 'WIN10': { fallbackPlatforms: ['APPLE_WIN'] } |
| } |
| } |
| } |
| }, |
| 'GTK': { |
| expectationsDirectory: 'gtk', |
| subPlatforms: { |
| 'LINUX': { fallbackPlatforms: ['GTK', 'WK2'] } |
| } |
| }, |
| 'WK2': { |
| basePlatform: true, |
| expectationsDirectory: 'wk2' |
| }, |
| 'WPE': { |
| expectationsDirectory: 'wpe', |
| subPlatforms: { |
| 'LINUX': { fallbackPlatforms: ['WPE', 'WK2'] } |
| } |
| }, |
| 'WINCAIRO': { |
| expectationsDirectory: 'wincairo', |
| } |
| }; |
| |
| var BUILD_TYPES = {'DEBUG': 'DBG', 'RELEASE': 'RELEASE'}; |
| var MIN_SECONDS_FOR_SLOW_TEST = 4; |
| var MIN_SECONDS_FOR_SLOW_TEST_DEBUG = 2 * MIN_SECONDS_FOR_SLOW_TEST; |
| var FAIL_RESULTS = ['IMAGE', 'IMAGE+TEXT', 'TEXT', 'MISSING']; |
| var CHUNK_SIZE = 25; |
| var MAX_RESULTS = 1500; |
| |
| 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 if (historyInstance.dashboardSpecificState.expectationsUpdate) |
| generatePageForExpectationsUpdate(); |
| else |
| generatePageForBuilder(historyInstance.dashboardSpecificState.builder || currentBuilderGroup().defaultBuilder()); |
| |
| for (var builder in currentBuilders()) |
| processTestResultsForBuilderAsync(builder); |
| |
| postHeightChangedMessage(); |
| } |
| |
| function handleValidHashParameter(historyInstance, key, value) |
| { |
| switch(key) { |
| case 'tests': |
| history.validateParameter(historyInstance.dashboardSpecificState, key, value, |
| function() { |
| return string.isValidName(value); |
| }); |
| return true; |
| |
| case 'result': |
| value = value.toUpperCase(); |
| history.validateParameter(historyInstance.dashboardSpecificState, key, value, |
| function() { |
| for (var result in LAYOUT_TEST_EXPECTATIONS_MAP_) { |
| if (value == LAYOUT_TEST_EXPECTATIONS_MAP_[result]) |
| return true; |
| } |
| return false; |
| }); |
| 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 headers = tableHeaders(true); |
| 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 'updateIndex': |
| case 'revision': |
| history.validateParameter(historyInstance.dashboardSpecificState, key, Number(value), |
| function() { |
| return value.match(/^\d+$/); |
| }); |
| return true; |
| |
| case 'showChrome': |
| case 'showCorrectExpectations': |
| case 'showWrongExpectations': |
| case 'showExpectations': |
| case 'showFlaky': |
| case 'showLargeExpectations': |
| case 'legacyExpectationsSemantics': |
| case 'showSkipped': |
| case 'showSlow': |
| case 'showUnexpectedPasses': |
| case 'showWontFixSkip': |
| case 'expectationsUpdate': |
| 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, |
| showFlaky: true, |
| showLargeExpectations: false, |
| legacyExpectationsSemantics: true, |
| showChrome: true, |
| showCorrectExpectations: false, |
| showWrongExpectations: false, |
| showWontFixSkip: false, |
| showSlow: false, |
| showSkipped: false, |
| showUnexpectedPasses: false, |
| expectationsUpdate: false, |
| updateIndex: 0, |
| 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_perBuilderPlatformAndBuildType = {}; |
| var g_perBuilderFailures = {}; |
| // Map of builder to arrays of tests that are listed in the expectations file |
| // but have for that builder. |
| var g_perBuilderWithExpectationsButNoFailures = {}; |
| // Map of builder to arrays of paths that are skipped. This shows the raw |
| // path used in TestExpectations rather than the test path since we |
| // don't actually have any data here for skipped tests. |
| var g_perBuilderSkippedPaths = {}; |
| // Maps test path to an array of {builder, testResults} objects. |
| var g_testToResultsMap = {}; |
| // Tests that the user wants to update expectations for. |
| var g_confirmedTests = {}; |
| |
| function traversePlatformsTree(callback) |
| { |
| function traverse(platformObject, parentPlatform) { |
| Object.keys(platformObject).forEach(function(platformName) { |
| var platform = platformObject[platformName]; |
| platformName = parentPlatform ? parentPlatform + platformName : platformName; |
| |
| if (platform.subPlatforms) |
| traverse(platform.subPlatforms, platformName + '_'); |
| else if (!platform.basePlatform) |
| callback(platform, platformName); |
| }); |
| } |
| traverse(PLATFORMS, null); |
| } |
| |
| function createResultsObjectForTest(test, builder) |
| { |
| return { |
| test: test, |
| builder: builder, |
| // HTML for display of the results in the flakiness column |
| html: '', |
| flips: 0, |
| slowestTime: 0, |
| slowestNonTimeoutCrashTime: 0, |
| meetsExpectations: true, |
| isWontFixSkip: false, |
| isFlaky: false, |
| // Sorted string of missing expectations |
| missing: '', |
| // String of extra expectations (i.e. expectations that never occur). |
| extra: '', |
| modifiers: '', |
| bugs: '', |
| expectations : '', |
| rawResults: '', |
| // List of all the results the test actually has. |
| actualResults: [] |
| }; |
| } |
| |
| function matchingElement(stringToMatch, elementsMap) |
| { |
| for (var element in elementsMap) { |
| if (string.contains(stringToMatch, elementsMap[element])) |
| return element; |
| } |
| } |
| |
| function determineWKPlatform(builderName, basePlatform) |
| { |
| var isWK2Builder = string.contains(builderName, 'WK2') || string.contains(builderName, 'WEBKIT2'); |
| return basePlatform + (isWK2Builder ? '_WK2' : '_WK1'); |
| } |
| |
| function determineBuilderPlatform(builderNameUpperCase) |
| { |
| if (string.contains(builderNameUpperCase, 'WIN 10')) |
| return 'APPLE_WIN_WIN10'; |
| if (string.contains(builderNameUpperCase, 'WIN 7')) |
| return 'APPLE_WIN_WIN7'; |
| if (string.contains(builderNameUpperCase, 'WIN XP')) |
| return 'APPLE_WIN_XP'; |
| |
| if (string.contains(builderNameUpperCase, 'GTK LINUX')) |
| return 'GTK_LINUX'; |
| if (string.contains(builderNameUpperCase, 'WPE LINUX')) |
| return 'WPE_LINUX'; |
| |
| if (string.contains(builderNameUpperCase, 'WINCAIRO')) |
| return 'WINCAIRO'; |
| |
| if (string.contains(builderNameUpperCase, 'CATALINA')) |
| return determineWKPlatform(builderNameUpperCase, 'APPLE_MAC_CATALINA'); |
| if (string.contains(builderNameUpperCase, 'MOJAVE')) |
| return determineWKPlatform(builderNameUpperCase, 'APPLE_MAC_MOJAVE'); |
| if (string.contains(builderNameUpperCase, 'HIGHSIERRA')) |
| return determineWKPlatform(builderNameUpperCase, 'APPLE_MAC_HIGHSIERRA'); |
| if (string.contains(builderNameUpperCase, 'SIERRA')) |
| return determineWKPlatform(builderNameUpperCase, 'APPLE_MAC_SIERRA'); |
| if (string.contains(builderNameUpperCase, 'EL CAPITAN')) |
| return determineWKPlatform(builderNameUpperCase, 'APPLE_MAC_ELCAPITAN'); |
| if (string.contains(builderNameUpperCase, 'MAVERICKS')) |
| return determineWKPlatform(builderNameUpperCase, 'APPLE_MAC_MAVERICKS'); |
| if (string.contains(builderNameUpperCase, 'LION')) |
| return determineWKPlatform(builderNameUpperCase, 'APPLE_MAC_LION'); |
| if (string.contains(builderNameUpperCase, ' IOS ') && string.contains(builderNameUpperCase, 'SIMULATOR')) |
| return determineWKPlatform(builderNameUpperCase, 'APPLE_IOS_SIMULATOR'); |
| } |
| |
| function platformAndBuildType(builderName) |
| { |
| if (!g_perBuilderPlatformAndBuildType[builderName]) { |
| var builderNameUpperCase = builderName.toUpperCase(); |
| var platform = determineBuilderPlatform(builderNameUpperCase); |
| if (!platform) |
| console.error('Could not resolve platform for builder: ' + builderName); |
| |
| var buildType = string.contains(builderNameUpperCase, 'DEBUG') ? 'DEBUG' : 'RELEASE'; |
| g_perBuilderPlatformAndBuildType[builderName] = {platform: platform, buildType: buildType}; |
| } |
| return g_perBuilderPlatformAndBuildType[builderName]; |
| } |
| |
| function isDebug(builderName) |
| { |
| return platformAndBuildType(builderName).buildType == 'DEBUG'; |
| } |
| |
| // Returns the expectation string for the given single character result. |
| // This string should match the expectations that are put into |
| // test_expectations.py. |
| // |
| // For example, if we start explicitly listing IMAGE result failures, |
| // this function should start returning 'IMAGE'. |
| function expectationsFileStringForResult(result) |
| { |
| // For the purposes of comparing against the expecations of a test, |
| // consider simplified diff failures as just text failures since |
| // the test_expectations file doesn't treat them specially. |
| if (result == 'S') |
| return 'TEXT'; |
| |
| if (result == 'N') |
| return ''; |
| |
| return expectationsMap()[result]; |
| } |
| |
| var TestTrie = function(builders, resultsByBuilder) |
| { |
| this._trie = {}; |
| |
| for (var builder in builders) { |
| 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 substringList() |
| { |
| // Convert windows slashes to unix slashes. |
| var tests = g_history.dashboardSpecificState.tests.replace(/\\/g, '/'); |
| var separator = string.contains(tests, ' ') ? ' ' : ','; |
| var testList = tests.split(separator); |
| |
| if (g_history.isLayoutTestResults()) |
| return testList; |
| |
| var testListWithoutModifiers = []; |
| testList.forEach(function(path) { |
| GTEST_MODIFIERS.forEach(function(modifier) { |
| path = path.replace('.' + modifier + '_', '.'); |
| }); |
| testListWithoutModifiers.push(path); |
| }); |
| return testListWithoutModifiers; |
| } |
| |
| function individualTestsForSubstringList() |
| { |
| var testList = substringList(); |
| |
| // 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; |
| } |
| |
| // Returns whether this test's slowest time is above the cutoff for |
| // being a slow test. |
| function isSlowTest(resultsForTest) |
| { |
| var maxTime = isDebug(resultsForTest.builder) ? MIN_SECONDS_FOR_SLOW_TEST_DEBUG : MIN_SECONDS_FOR_SLOW_TEST; |
| return resultsForTest.slowestNonTimeoutCrashTime > maxTime; |
| } |
| |
| // Returns whether this test's slowest time is *well* below the cutoff for |
| // being a slow test. |
| function isFastTest(resultsForTest) |
| { |
| var maxTime = isDebug(resultsForTest.builder) ? MIN_SECONDS_FOR_SLOW_TEST_DEBUG : MIN_SECONDS_FOR_SLOW_TEST; |
| return resultsForTest.slowestNonTimeoutCrashTime < maxTime / 2; |
| } |
| |
| 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) != -1) { |
| retVal.push(triePath); |
| break; |
| } |
| } |
| }); |
| |
| return retVal; |
| } |
| |
| |
| // Adds all the tests for the given builder to the testMapToPopulate. |
| function addTestsForBuilder(builder, testMapToPopulate) |
| { |
| var tests = g_resultsByBuilder[builder].tests; |
| for (var test in tests) { |
| testMapToPopulate[test] = true; |
| } |
| } |
| |
| // Map of all tests to true values by platform and build type. |
| // e.g. g_allTestsByPlatformAndBuildType['XP']['DEBUG'] will have the union |
| // of all tests run on the xp-debug builders. |
| var g_allTestsByPlatformAndBuildType = {}; |
| traversePlatformsTree(function(platform, platformName) { |
| g_allTestsByPlatformAndBuildType[platformName] = {}; |
| }); |
| |
| // Map of all tests to true values by platform and build type. |
| // e.g. g_allTestsByPlatformAndBuildType['WIN']['DEBUG'] will have the union |
| // of all tests run on the win-debug builders. |
| function allTestsWithSamePlatformAndBuildType(platform, buildType) |
| { |
| if (!g_allTestsByPlatformAndBuildType[platform]) |
| return {}; |
| |
| if (!g_allTestsByPlatformAndBuildType[platform][buildType]) { |
| var tests = {}; |
| for (var thisBuilder in currentBuilders()) { |
| var thisBuilderBuildInfo = platformAndBuildType(thisBuilder); |
| if (thisBuilderBuildInfo.buildType == buildType && thisBuilderBuildInfo.platform == platform) { |
| addTestsForBuilder(thisBuilder, tests); |
| } |
| } |
| g_allTestsByPlatformAndBuildType[platform][buildType] = tests; |
| } |
| |
| return g_allTestsByPlatformAndBuildType[platform][buildType]; |
| } |
| |
| function getExpectations(test, platform, buildType) |
| { |
| var testObject = g_allExpectations[test]; |
| if (!testObject) |
| return null; |
| |
| var platformObject = testObject[platform]; |
| if (!platformObject) |
| return null; |
| |
| return platformObject[buildType]; |
| } |
| |
| function filterBugs(modifiers) |
| { |
| var bugs = modifiers.match(/\b(Bug|webkit.org|crbug.com|code.google.com)\S*/g); |
| if (!bugs) |
| return {bugs: '', modifiers: modifiers}; |
| for (var j = 0; j < bugs.length; j++) |
| modifiers = modifiers.replace(bugs[j], ''); |
| return {bugs: bugs.join(' '), modifiers: string.collapseWhitespace(string.trimString(modifiers))}; |
| } |
| |
| function populateExpectationsData(resultsObject) |
| { |
| var buildInfo = platformAndBuildType(resultsObject.builder); |
| var expectations = getExpectations(resultsObject.test, buildInfo.platform, buildInfo.buildType); |
| if (!expectations) |
| return; |
| |
| resultsObject.expectations = expectations.expectations; |
| var filteredModifiers = filterBugs(expectations.modifiers); |
| resultsObject.modifiers = filteredModifiers.modifiers; |
| resultsObject.bugs = filteredModifiers.bugs; |
| resultsObject.isWontFixSkip = string.contains(expectations.modifiers, 'WONTFIX') || string.contains(expectations.modifiers, 'SKIP'); |
| } |
| |
| function platformObjectForName(platformName) |
| { |
| var platformsList = platformName.split("_"); |
| var platformObject = PLATFORMS[platformsList.shift()]; |
| platformsList.forEach(function(platformName) { |
| platformObject = platformObject.subPlatforms[platformName]; |
| }); |
| return platformObject; |
| } |
| |
| // Data structure to hold the processed expectations. |
| // g_allExpectations[testPath][platform][buildType] gets the object that has |
| // expectations and modifiers properties for this platform/buildType. |
| // |
| // platform and buildType both go through fallback sets of keys from most |
| // specific key to least specific. For example, on Windows XP, we first |
| // check the platform WIN-XP, if there's no such object, we check WIN, |
| // then finally we check ALL. For build types, we check the current |
| // buildType, then ALL. |
| var g_allExpectations; |
| |
| function getParsedExpectations(data) |
| { |
| var expectations = []; |
| var lines = data.split('\n'); |
| lines.forEach(function(line) { |
| line = string.trimString(line); |
| if (!line || string.startsWith(line, '#')) |
| return; |
| |
| // This code mimics _tokenize_line_using_new_format() in |
| // Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py |
| // |
| // FIXME: consider doing more error checking here. |
| // |
| // FIXME: Clean this all up once we've fully cut over to the new syntax. |
| var tokens = line.split(/\s+/) |
| var parsed_bugs = []; |
| var parsed_modifiers = []; |
| var parsed_path; |
| var parsed_expectations = []; |
| var state = 'start'; |
| |
| // This clones _configuration_tokens_list in test_expectations.py. |
| // FIXME: unify with the platforms constants at the top of the file. |
| var configuration_tokens = { |
| 'Release': 'RELEASE', |
| 'Debug': 'DEBUG', |
| 'Mac': 'MAC', |
| 'Win': 'WIN', |
| 'Linux': 'LINUX', |
| 'SnowLeopard': 'SNOWLEOPARD', |
| 'Lion': 'LION', |
| 'MountainLion': 'MOUNTAINLION', |
| 'Mavericks': 'MAVERICKS', |
| 'Yosemite': 'YOSEMITE', |
| 'ElCapitan': 'ELCAPITAN', |
| 'Sierra': 'SIERRA', |
| 'HighSierra': 'HIGHSIERRA', |
| 'Mojave': 'MOJAVE', |
| 'Catalina': 'CATALINA', |
| 'Win7': 'WIN7', |
| 'Win10': 'WIN10', |
| 'XP': 'XP', |
| 'Vista': 'VISTA', |
| 'Android': 'ANDROID', |
| }; |
| |
| var expectation_tokens = { |
| 'Crash': 'CRASH', |
| 'Failure': 'FAIL', |
| 'ImageOnlyFailure': 'IMAGE', |
| 'Missing': 'MISSING', |
| 'Pass': 'PASS', |
| 'Rebaseline': 'REBASELINE', |
| 'Skip': 'SKIP', |
| 'Slow': 'SLOW', |
| 'Timeout': 'TIMEOUT', |
| 'WontFix': 'WONTFIX', |
| }; |
| |
| |
| tokens.forEach(function(token) { |
| if (token.indexOf('Bug') != -1 || |
| token.indexOf('webkit.org') != -1 || |
| token.indexOf('crbug.com') != -1 || |
| token.indexOf('code.google.com') != -1) { |
| parsed_bugs.push(token); |
| } else if (token == '[') { |
| if (state == 'start') { |
| state = 'configuration'; |
| } else if (state == 'name_found') { |
| state = 'expectations'; |
| } |
| } else if (token == ']') { |
| if (state == 'configuration') { |
| state = 'name'; |
| } else if (state == 'expectations') { |
| state = 'done'; |
| } |
| } else if (state == 'configuration') { |
| parsed_modifiers.push(configuration_tokens[token]); |
| } else if (state == 'expectations') { |
| if (token == 'Rebaseline' || token == 'Skip' || token == 'Slow' || token == 'WontFix') { |
| parsed_modifiers.push(token.toUpperCase()); |
| } else { |
| parsed_expectations.push(expectation_tokens[token]); |
| } |
| } else if (token == '#') { |
| state = 'done'; |
| } else if (state == 'name' || state == 'start') { |
| parsed_path = token; |
| state = 'name_found'; |
| } |
| }); |
| |
| if (!parsed_expectations.length) { |
| if (parsed_modifiers.indexOf('Slow') == -1) { |
| parsed_modifiers.push('Skip'); |
| parsed_expectations = ['Pass']; |
| } |
| } |
| |
| // FIXME: Should we include line number and comment lines here? |
| expectations.push({ |
| modifiers: parsed_bugs.concat(parsed_modifiers).join(' '), |
| path: parsed_path, |
| expectations: parsed_expectations.join(' '), |
| }); |
| }); |
| return expectations; |
| } |
| |
| |
| function addTestToAllExpectationsForPlatform(test, platformName, expectations, modifiers) |
| { |
| if (!g_allExpectations[test]) |
| g_allExpectations[test] = {}; |
| |
| if (!g_allExpectations[test][platformName]) |
| g_allExpectations[test][platformName] = {}; |
| |
| var allBuildTypes = []; |
| modifiers.split(' ').forEach(function(modifier) { |
| if (modifier in BUILD_TYPES) { |
| allBuildTypes.push(modifier); |
| return; |
| } |
| }); |
| if (!allBuildTypes.length) |
| allBuildTypes = Object.keys(BUILD_TYPES); |
| |
| allBuildTypes.forEach(function(buildType) { |
| g_allExpectations[test][platformName][buildType] = {modifiers: modifiers, expectations: expectations}; |
| }); |
| } |
| |
| function processExpectationsForPlatform(platformObject, platformName, expectationsArray) |
| { |
| if (!g_allExpectations) |
| g_allExpectations = {}; |
| |
| if (!expectationsArray) |
| return; |
| |
| // Sort the array to hit more specific paths last. More specific |
| // paths (e.g. foo/bar/baz.html) override entries for less-specific ones (e.g. foo/bar). |
| expectationsArray.sort(alphanumericCompare('path')); |
| |
| for (var i = 0; i < expectationsArray.length; i++) { |
| var path = expectationsArray[i].path; |
| var modifiers = expectationsArray[i].modifiers; |
| var expectations = expectationsArray[i].expectations; |
| |
| getAllTestsTrie().forEach(function(triePath) { |
| addTestToAllExpectationsForPlatform(triePath, platformName, expectations, modifiers); |
| }, path); |
| } |
| } |
| |
| function processExpectations() |
| { |
| // FIXME: An expectations-by-platform object should be passed into this function rather than checking |
| // for a global object. That way this function can be better tested and meaningful errors can |
| // be reported when expectations for a given platform are not found in that object. |
| if (!g_expectationsByPlatform) |
| return; |
| |
| traversePlatformsTree(function(platform, platformName) { |
| if (platform.fallbackPlatforms) { |
| platform.fallbackPlatforms.forEach(function(fallbackPlatform) { |
| if (fallbackPlatform in g_expectationsByPlatform) |
| processExpectationsForPlatform(platform, platformName, g_expectationsByPlatform[fallbackPlatform]); |
| }); |
| } |
| |
| if (platformName in g_expectationsByPlatform) |
| processExpectationsForPlatform(platform, platformName, g_expectationsByPlatform[platformName]); |
| }); |
| |
| g_expectationsByPlatform = undefined; |
| } |
| |
| function processMissingTestsWithExpectations(builder, platform, buildType) |
| { |
| var noFailures = []; |
| var skipped = []; |
| |
| var allTestsForPlatformAndBuildType = allTestsWithSamePlatformAndBuildType(platform, buildType); |
| for (var test in g_allExpectations) { |
| var expectations = getExpectations(test, platform, buildType); |
| |
| if (!expectations) |
| continue; |
| |
| // Test has expectations, but no result in the builders results. |
| // This means it's either SKIP or passes on all builds. |
| if (!allTestsForPlatformAndBuildType[test] && !string.contains(expectations.modifiers, 'WONTFIX')) { |
| if (string.contains(expectations.modifiers, 'SKIP')) |
| skipped.push(test); |
| else if (!expectations.expectations.match(/^\s*PASS\s*$/)) { |
| // Don't show tests expected to always pass. This is used in ways like |
| // the following: |
| // foo/bar = FAIL |
| // foo/bar/baz.html = PASS |
| noFailures.push({test: test, expectations: expectations.expectations, modifiers: expectations.modifiers}); |
| } |
| } |
| } |
| |
| g_perBuilderSkippedPaths[builder] = skipped.sort(); |
| g_perBuilderWithExpectationsButNoFailures[builder] = noFailures.sort(); |
| } |
| |
| 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; |
| } |
| |
| processExpectations(); |
| |
| var buildInfo = platformAndBuildType(builderName); |
| var platform = buildInfo.platform; |
| var buildType = buildInfo.buildType; |
| if (!platform) |
| return; |
| |
| processMissingTestsWithExpectations(builderName, platform, buildType); |
| |
| var failures = []; |
| var allTestsForThisBuilder = g_resultsByBuilder[builderName].tests; |
| |
| for (var test in allTestsForThisBuilder) { |
| var resultsForTest = createResultsObjectForTest(test, builderName); |
| populateExpectationsData(resultsForTest); |
| |
| var rawTest = g_resultsByBuilder[builderName].tests[test]; |
| resultsForTest.rawTimes = rawTest.times; |
| var rawResults = rawTest.results; |
| resultsForTest.rawResults = rawResults; |
| |
| // FIXME: Switch to resultsByBuild |
| var times = resultsForTest.rawTimes; |
| var numTimesSeen = 0; |
| var numResultsSeen = 0; |
| var resultsIndex = 0; |
| var currentResult; |
| for (var i = 0; i < times.length; i++) { |
| numTimesSeen += times[i][RLE.LENGTH]; |
| |
| while (rawResults[resultsIndex] && numTimesSeen > (numResultsSeen + rawResults[resultsIndex][RLE.LENGTH])) { |
| numResultsSeen += rawResults[resultsIndex][RLE.LENGTH]; |
| resultsIndex++; |
| } |
| |
| if (rawResults && rawResults[resultsIndex]) |
| currentResult = rawResults[resultsIndex][RLE.VALUE]; |
| |
| var time = times[i][RLE.VALUE] |
| |
| // Ignore times for crashing/timeout runs for the sake of seeing if |
| // a test should be marked slow. |
| if (currentResult != 'C' && currentResult != 'T') |
| resultsForTest.slowestNonTimeoutCrashTime = Math.max(resultsForTest.slowestNonTimeoutCrashTime, time); |
| resultsForTest.slowestTime = Math.max(resultsForTest.slowestTime, time); |
| } |
| |
| processMissingAndExtraExpectations(resultsForTest); |
| failures.push(resultsForTest); |
| |
| if (!g_testToResultsMap[test]) |
| g_testToResultsMap[test] = []; |
| g_testToResultsMap[test].push(resultsForTest); |
| } |
| |
| g_perBuilderFailures[builderName] = failures; |
| } |
| |
| function processMissingAndExtraExpectations(resultsForTest) |
| { |
| // Heuristic for determining whether expectations apply to a given test: |
| // -If a test result happens < MIN_RUNS_FOR_FLAKE, then consider it a flaky |
| // result and include it in the list of expected results. |
| // -Otherwise, grab the first contiguous set of runs with the same result |
| // for >= MIN_RUNS_FOR_FLAKE and ignore all following runs >= |
| // MIN_RUNS_FOR_FLAKE. |
| // This lets us rule out common cases of a test changing expectations for |
| // a few runs, then being fixed or otherwise modified in a non-flaky way. |
| var rawResults = resultsForTest.rawResults; |
| |
| // If the first result is no-data that means the test is skipped or is |
| // being run on a different builder (e.g. moved from one shard to another). |
| // Ignore these results since we have no real data about what's going on. |
| if (rawResults[0][RLE.VALUE] == 'N') |
| return; |
| |
| // Only consider flake if it doesn't happen twice in a row. |
| var MIN_RUNS_FOR_FLAKE = 2; |
| var resultsMap = {} |
| var numResultsSeen = 0; |
| var haveSeenNonFlakeResult = false; |
| var numRealResults = 0; |
| |
| var seenResults = {}; |
| for (var i = 0; i < rawResults.length; i++) { |
| var numResults = rawResults[i][RLE.LENGTH]; |
| numResultsSeen += numResults; |
| |
| var result = rawResults[i][RLE.VALUE]; |
| |
| var hasMinRuns = numResults >= MIN_RUNS_FOR_FLAKE; |
| if (haveSeenNonFlakeResult && hasMinRuns) |
| continue; |
| else if (hasMinRuns) |
| haveSeenNonFlakeResult = true; |
| else if (!seenResults[result]) { |
| // Only consider a short-lived result if we've seen it more than once. |
| // Otherwise, we include lots of false-positives due to tests that fail |
| // for a couple runs and then start passing. |
| seenResults[result] = true; |
| continue; |
| } |
| |
| var expectation = expectationsFileStringForResult(result); |
| resultsMap[expectation] = true; |
| numRealResults++; |
| } |
| |
| resultsForTest.flips = i - 1; |
| resultsForTest.isFlaky = numRealResults > 1; |
| |
| var missingExpectations = []; |
| var extraExpectations = []; |
| |
| if (g_history.isLayoutTestResults()) { |
| var expectationsArray = resultsForTest.expectations ? resultsForTest.expectations.split(' ') : []; |
| extraExpectations = expectationsArray.filter( |
| function(element) { |
| // FIXME: Once all the FAIL lines are removed from |
| // TestExpectations, delete all the legacyExpectationsSemantics |
| // code. |
| if (g_history.dashboardSpecificState.legacyExpectationsSemantics) { |
| if (element == 'FAIL') { |
| for (var i = 0; i < FAIL_RESULTS.length; i++) { |
| if (resultsMap[FAIL_RESULTS[i]]) |
| return false; |
| } |
| return true; |
| } |
| } |
| |
| return element && !resultsMap[element] && !string.contains(element, 'BUG'); |
| }); |
| |
| for (var result in resultsMap) { |
| resultsForTest.actualResults.push(result); |
| var hasExpectation = false; |
| for (var i = 0; i < expectationsArray.length; i++) { |
| var expectation = expectationsArray[i]; |
| // FIXME: Once all the FAIL lines are removed from |
| // TestExpectations, delete all the legacyExpectationsSemantics |
| // code. |
| if (g_history.dashboardSpecificState.legacyExpectationsSemantics) { |
| if (expectation == 'FAIL') { |
| for (var j = 0; j < FAIL_RESULTS.length; j++) { |
| if (result == FAIL_RESULTS[j]) { |
| hasExpectation = true; |
| break; |
| } |
| } |
| } |
| } |
| |
| if (result == expectation) |
| hasExpectation = true; |
| |
| if (hasExpectation) |
| break; |
| } |
| // If we have no expectations for a test and it only passes, then don't |
| // list PASS as a missing expectation. We only want to list PASS if it |
| // flaky passes, so there would be other expectations. |
| if (!hasExpectation && !(!expectationsArray.length && result == 'PASS' && numRealResults == 1)) |
| missingExpectations.push(result); |
| } |
| |
| // Only highlight tests that take > 2 seconds as needing to be marked as |
| // slow. There are too many tests that take ~2 seconds every couple |
| // hundred runs. It's not worth the manual maintenance effort. |
| // Also, if a test times out, then it should not be marked as slow. |
| var minTimeForNeedsSlow = isDebug(resultsForTest.builder) ? 2 : 1; |
| if (isSlowTest(resultsForTest) && !resultsMap['TIMEOUT'] && (!resultsForTest.modifiers || !string.contains(resultsForTest.modifiers, 'SLOW'))) |
| missingExpectations.push('SLOW'); |
| else if (isFastTest(resultsForTest) && resultsForTest.modifiers && string.contains(resultsForTest.modifiers, 'SLOW')) |
| extraExpectations.push('SLOW'); |
| |
| // If there are no missing results or modifiers besides build |
| // type, platform, or bug and the expectations are all extra |
| // that is, extraExpectations - expectations = PASS, |
| // include PASS as extra, since that means this line in |
| // test_expectations can be deleted.. |
| if (!missingExpectations.length && !(resultsForTest.modifiers && realModifiers(resultsForTest.modifiers))) { |
| var extraPlusPass = extraExpectations.concat(['PASS']); |
| if (extraPlusPass.sort().toString() == expectationsArray.slice(0).sort().toString()) |
| extraExpectations.push('PASS'); |
| } |
| |
| } |
| |
| resultsForTest.meetsExpectations = !missingExpectations.length && !extraExpectations.length; |
| resultsForTest.missing = missingExpectations.sort().join(' '); |
| resultsForTest.extra = extraExpectations.sort().join(' '); |
| } |
| |
| |
| var BUG_URL_PREFIX = '<a href="https://'; |
| var BUG_URL_POSTFIX = '/$1">crbug.com/$1</a> '; |
| var WEBKIT_BUG_URL_POSTFIX = '/$1">webkit.org/b/$1</a> '; |
| var INTERNAL_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'b' + BUG_URL_POSTFIX; |
| var EXTERNAL_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'crbug.com' + BUG_URL_POSTFIX; |
| var WEBKIT_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'webkit.org/b' + WEBKIT_BUG_URL_POSTFIX; |
| |
| function htmlForBugs(bugs) |
| { |
| bugs = bugs.replace(/crbug.com\/(\d+)(\ |$)/g, EXTERNAL_BUG_REPLACE_VALUE); |
| bugs = bugs.replace(/webkit.org\/b\/(\d+)(\ |$)/g, WEBKIT_BUG_REPLACE_VALUE); |
| return bugs; |
| } |
| |
| function linkHTMLToOpenWindow(url, text) |
| { |
| return '<a href="' + url + '" target="_blank">' + text + '</a>'; |
| } |
| |
| // FIXME: replaced with ui.html.webKitRevisionLink |
| function createBlameListHTML(revisions, index, urlBase, separator, repo) |
| { |
| var thisRevision = revisions[index]; |
| if (!thisRevision) |
| return ''; |
| |
| var previousRevision = revisions[index + 1]; |
| if (previousRevision && previousRevision != thisRevision) { |
| previousRevision++; |
| return linkHTMLToOpenWindow(urlBase + thisRevision + separator + previousRevision, |
| repo + ' blamelist r' + previousRevision + ':r' + thisRevision); |
| } else |
| return 'At ' + repo + ' revision: ' + thisRevision; |
| } |
| |
| // 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; |
| for (var i = 0; i < rawResults.length; i++) { |
| currentIndex += rawResults[i][RLE.LENGTH]; |
| if (currentIndex > index) |
| return isFailingResult(rawResults[i][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 index = 0; |
| var failures = []; |
| for (var i = 0; i < rawResults.length; i++) { |
| var numResults = rawResults[i][RLE.LENGTH]; |
| if (isFailingResult(rawResults[i][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 = builderMaster(builder); |
| var buildBasePath = master.logPath(builder, buildNumber); |
| |
| html += '<ul><li>' + linkHTMLToOpenWindow(buildBasePath, 'Build log') + |
| '</li><li>' + |
| createBlameListHTML(g_resultsByBuilder[builder].webkitRevision, index, |
| 'https://trac.webkit.org/log/?verbose=on&rev=', '&stop_rev=', |
| 'WebKit') + |
| '</li>'; |
| |
| if (master.name == WEBKIT_BUILDER_MASTER) { |
| var revision = g_resultsByBuilder[builder].webkitRevision[index]; |
| html += '<li><span class=link onclick="g_history.setQueryParameter(\'revision\',' + |
| revision + ')">Show results for WebKit r' + revision + |
| '</span></li>'; |
| } else |
| console.error("Unexpected master name: " + master.name); |
| |
| 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 htmlForTestResults(test) |
| { |
| var html = ''; |
| var results = test.rawResults.concat(); |
| var times = test.rawTimes.concat(); |
| var builder = test.builder; |
| var master = builderMaster(builder); |
| var buildNumbers = g_resultsByBuilder[builder].buildNumbers; |
| |
| var indexToReplaceCurrentResult = -1; |
| var indexToReplaceCurrentTime = -1; |
| var currentResultArray, currentTimeArray, currentResult, innerHTML, resultString; |
| for (var i = 0; i < buildNumbers.length; i++) { |
| if (i > indexToReplaceCurrentResult) { |
| currentResultArray = results.shift(); |
| if (currentResultArray) { |
| currentResult = currentResultArray[RLE.VALUE]; |
| // Treat simplified diff failures as just text failures. |
| if (currentResult == 'S') |
| currentResult = 'F'; |
| indexToReplaceCurrentResult += currentResultArray[RLE.LENGTH]; |
| } else { |
| currentResult = 'N'; |
| indexToReplaceCurrentResult += buildNumbers.length; |
| } |
| resultString = expectationsFileStringForResult(currentResult); |
| } |
| |
| if (i > indexToReplaceCurrentTime) { |
| currentTimeArray = times.shift(); |
| var currentTime = 0; |
| if (currentResultArray) { |
| currentTime = currentTimeArray[RLE.VALUE]; |
| indexToReplaceCurrentTime += currentTimeArray[RLE.LENGTH]; |
| } else |
| indexToReplaceCurrentTime += buildNumbers.length; |
| |
| innerHTML = currentTime || ' '; |
| } |
| |
| var extraClassNames = ''; |
| var webkitRevision = g_resultsByBuilder[builder].webkitRevision; |
| var isWebkitMerge = webkitRevision[i + 1] && webkitRevision[i] != webkitRevision[i + 1]; |
| if (isWebkitMerge && master.name != WEBKIT_BUILDER_MASTER) |
| extraClassNames += ' merge'; |
| |
| html += '<td title="' + (resultString || 'NO DATA') + '. Click for more info." class="results ' + currentResult + |
| extraClassNames + '" onclick=\'showPopupForBuild(event, "' + builder + '",' + i + ',"' + test.test + '")\'>' + innerHTML; |
| } |
| return html; |
| } |
| |
| function htmlForTestsWithExpectationsButNoFailures(builder) |
| { |
| var tests = g_perBuilderWithExpectationsButNoFailures[builder]; |
| var skippedPaths = g_perBuilderSkippedPaths[builder]; |
| var showUnexpectedPassesLink = linkHTMLToToggleState('showUnexpectedPasses', 'tests that have not failed in last ' + g_resultsByBuilder[builder].buildNumbers.length + ' runs'); |
| var showSkippedLink = linkHTMLToToggleState('showSkipped', 'skipped tests in TestExpectations'); |
| |
| var html = ''; |
| if (g_history.isLayoutTestResults() && (tests.length || skippedPaths.length)) { |
| var buildInfo = platformAndBuildType(builder); |
| html += '<h2 style="display:inline-block">Expectations for ' + buildInfo.platform + '-' + buildInfo.buildType + '</h2> '; |
| if (!g_history.dashboardSpecificState.showUnexpectedPasses && tests.length) |
| html += showUnexpectedPassesLink; |
| html += ' '; |
| if (!g_history.dashboardSpecificState.showSkipped && skippedPaths.length) |
| html += showSkippedLink; |
| } |
| |
| var open = '<div onclick="selectContents(this)">'; |
| |
| if (g_history.dashboardSpecificState.showUnexpectedPasses && tests.length) { |
| html += '<div id="passing-tests">' + showUnexpectedPassesLink; |
| for (var i = 0; i < tests.length; i++) |
| html += open + tests[i].test + '</div>'; |
| html += '</div>'; |
| } |
| |
| if (g_history.dashboardSpecificState.showSkipped && skippedPaths.length) |
| html += '<div id="skipped-tests">' + showSkippedLink + open + skippedPaths.join('</div>' + open) + '</div></div>'; |
| return html + '<br>'; |
| } |
| |
| // Returns whether we should exclude test results from the test table. |
| function shouldHideTest(testResult) |
| { |
| // For non-layout tests, we always show everything. |
| if (!g_history.isLayoutTestResults()) |
| return false; |
| |
| if (testResult.isWontFixSkip) |
| return !g_history.dashboardSpecificState.showWontFixSkip; |
| |
| if (testResult.isFlaky) |
| return !g_history.dashboardSpecificState.showFlaky; |
| |
| if (isSlowTest(testResult)) |
| return !g_history.dashboardSpecificState.showSlow; |
| |
| if (testResult.meetsExpectations) |
| return !g_history.dashboardSpecificState.showCorrectExpectations; |
| |
| return !g_history.dashboardSpecificState.showWrongExpectations; |
| } |
| |
| // Sets the browser's selection to the element's contents. |
| function selectContents(element) |
| { |
| window.getSelection().selectAllChildren(element); |
| } |
| |
| function createBugHTML(test) |
| { |
| var symptom = test.isFlaky ? 'flaky' : 'failing'; |
| var title = encodeURIComponent('Layout Test ' + test.test + ' is ' + symptom); |
| var dashboardURL = 'https://webkit-test-results.webkit.org/dashboards/flakiness_dashboard.html#showAllRuns=true&tests=' + encodeURIComponent(test.test); |
| var description = encodeURIComponent('The following layout test is ' + symptom + ' on ' + |
| '[insert platform]\n\n' + test.test + '\n\nProbable cause:\n\n' + '[insert probable cause]' + |
| '\n\nFlakiness Dashboard:\n\n' + dashboardURL); |
| |
| var component = encodeURIComponent('Tools / Tests'); |
| url = 'https://bugs.webkit.org/enter_bug.cgi?assigned_to=webkit-unassigned%40lists.webkit.org&product=WebKit&form_name=enter_bug&component=' + component + '&short_desc=' + title + '&comment=' + description; |
| return '<a href="' + url + '" class="file-bug" target="_blank">File</a>'; |
| } |
| |
| function isCrossBuilderView() |
| { |
| return g_history.dashboardSpecificState.tests || g_history.dashboardSpecificState.result || g_history.dashboardSpecificState.expectationsUpdate; |
| } |
| |
| 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('Bug(s)', 'Modifiers', 'Expectation(s)'); |
| |
| headers.push('Slowest Run', 'Flakiness (Numbers are runtimes in seconds)'); |
| return headers; |
| } |
| |
| function htmlForSingleTestRow(test) |
| { |
| if (!isCrossBuilderView() && shouldHideTest(test)) { |
| // The innerHTML call is considerably faster if we exclude the rows for |
| // items we're not showing than if we hide them using display:none. |
| // For the crossBuilderView, we want to show all rows the user is |
| // explicitly listing tests to view. |
| return ''; |
| } |
| |
| 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')) { |
| // If isCrossBuilderView() is true, we're just viewing a single test |
| // with results for many builders, so the first column is builder names |
| // instead of test paths. |
| var testCellClassName = 'test-link' + (isCrossBuilderView() ? ' builder-name' : ''); |
| var testCellHTML = isCrossBuilderView() ? 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, 'Bug(s)')) |
| html += '<td class=options-container bugs>' + (test.bugs ? htmlForBugs(test.bugs) : createBugHTML(test)); |
| else if (string.startsWith(header, 'Modifiers')) |
| html += '<td class=options-container>' + test.modifiers; |
| else if (string.startsWith(header, 'Expectation(s)')) |
| html += '<td class=options-container>' + test.expectations.split(' ').join(' | '); |
| else if (string.startsWith(header, 'Slowest')) |
| html += '<td class=options-container>' + (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)); |
| } |
| |
| // Sorts a space separated expectations string in alphanumeric order. |
| // @param {string} str The expectations string. |
| // @return {string} The sorted string. |
| function sortExpectationsString(str) |
| { |
| return str.split(' ').sort().join(' '); |
| } |
| |
| function addUpdate(testsNeedingUpdate, test, builderName, missing, extra) |
| { |
| if (!testsNeedingUpdate[test]) |
| testsNeedingUpdate[test] = {}; |
| |
| var buildInfo = platformAndBuildType(builderName); |
| var builder = buildInfo.platform + ' ' + buildInfo.buildType; |
| if (!testsNeedingUpdate[test][builder]) |
| testsNeedingUpdate[test][builder] = {}; |
| |
| if (missing) |
| testsNeedingUpdate[test][builder].missing = sortExpectationsString(missing); |
| |
| if (extra) |
| testsNeedingUpdate[test][builder].extra = sortExpectationsString(extra); |
| } |
| |
| |
| // From a string of modifiers, returns a string of modifiers that |
| // are for real result changes, like SLOW, and excludes modifiers |
| // that specificy things like platform, build_type, bug. |
| // @param {string} modifierString String containing all modifiers. |
| // @return {string} String containing only modifiers that effect the results. |
| function realModifiers(modifierString) |
| { |
| var modifiers = modifierString.split(' ');; |
| return modifiers.filter(function(modifier) { |
| return !(modifier in BUILD_TYPES || string.startsWith(modifier, 'BUG')); |
| }).join(' '); |
| } |
| |
| function generatePageForExpectationsUpdate() |
| { |
| // Always show all runs when auto-updating expectations. |
| if (!g_history.crossDashboardState.showAllRuns) |
| g_history.setQueryParameter('showAllRuns', true); |
| |
| processTestRunsForAllBuilders(); |
| var testsNeedingUpdate = {}; |
| for (var test in g_testToResultsMap) { |
| var results = g_testToResultsMap[test]; |
| for (var i = 0; i < results.length; i++) { |
| var thisResult = results[i]; |
| |
| if (!thisResult.missing && !thisResult.extra) |
| continue; |
| |
| var allPassesOrNoDatas = thisResult.rawResults.filter(function (x) { return x[1] != "P" && x[1] != "N"; }).length == 0; |
| |
| if (allPassesOrNoDatas) |
| continue; |
| |
| addUpdate(testsNeedingUpdate, test, thisResult.builder, thisResult.missing, thisResult.extra); |
| } |
| } |
| |
| for (var builder in currentBuilders()) { |
| var tests = g_perBuilderWithExpectationsButNoFailures[builder] |
| for (var i = 0; i < tests.length; i++) { |
| // Anything extra in this case is what is listed in expectations |
| // plus modifiers other than bug, platform, build type. |
| var modifiers = realModifiers(tests[i].modifiers); |
| var extras = tests[i].expectations; |
| extras += modifiers ? ' ' + modifiers : ''; |
| addUpdate(testsNeedingUpdate, tests[i].test, builder, null, extras); |
| } |
| } |
| |
| // Get the keys in alphabetical order, so it is easy to process groups |
| // of tests. |
| var keys = Object.keys(testsNeedingUpdate).sort(); |
| showUpdateInfoForTest(testsNeedingUpdate, keys); |
| } |
| |
| // Show the test results and the json for differing expectations, and |
| // allow the user to include or exclude this update. |
| // |
| // @param {Object} testsNeedingUpdate Tests that need updating. |
| // @param {Array.<string>} keys Keys into the testNeedingUpdate object. |
| function showUpdateInfoForTest(testsNeedingUpdate, keys) |
| { |
| var test = keys[g_history.dashboardSpecificState.updateIndex]; |
| document.body.innerHTML = ''; |
| |
| // FIXME: Make this DOM creation less verbose. |
| var index = document.createElement('div'); |
| index.style.cssFloat = 'right'; |
| index.textContent = (g_history.dashboardSpecificState.updateIndex + 1) + ' of ' + keys.length + ' tests'; |
| document.body.appendChild(index); |
| |
| var buttonRegion = document.createElement('div'); |
| var includeBtn = document.createElement('input'); |
| includeBtn.type = 'button'; |
| includeBtn.value = 'include selected'; |
| includeBtn.addEventListener('click', partial(handleUpdate, testsNeedingUpdate, keys), false); |
| buttonRegion.appendChild(includeBtn); |
| |
| var previousBtn = document.createElement('input'); |
| previousBtn.type = 'button'; |
| previousBtn.value = 'previous'; |
| previousBtn.addEventListener('click', |
| function() { |
| setUpdateIndex(g_history.dashboardSpecificState.updateIndex - 1, testsNeedingUpdate, keys); |
| }, |
| false); |
| buttonRegion.appendChild(previousBtn); |
| |
| var nextBtn = document.createElement('input'); |
| nextBtn.type = 'button'; |
| nextBtn.value = 'next'; |
| nextBtn.addEventListener('click', partial(nextUpdate, testsNeedingUpdate, keys), false); |
| buttonRegion.appendChild(nextBtn); |
| |
| var doneBtn = document.createElement('input'); |
| doneBtn.type = 'button'; |
| doneBtn.value = 'done'; |
| doneBtn.addEventListener('click', finishUpdate, false); |
| buttonRegion.appendChild(doneBtn); |
| |
| document.body.appendChild(buttonRegion); |
| |
| var updates = testsNeedingUpdate[test]; |
| var checkboxes = document.createElement('div'); |
| for (var builder in updates) { |
| // Create a checkbox for each builder. |
| var checkboxRegion = document.createElement('div'); |
| var checkbox = document.createElement('input'); |
| checkbox.type = 'checkbox'; |
| checkbox.id = builder; |
| checkbox.checked = true; |
| checkboxRegion.appendChild(checkbox); |
| checkboxRegion.appendChild(document.createTextNode(builder + ' : ' + JSON.stringify(updates[builder]))); |
| checkboxes.appendChild(checkboxRegion); |
| } |
| document.body.appendChild(checkboxes); |
| |
| var div = document.createElement('div'); |
| div.innerHTML = htmlForIndividualTestOnAllBuildersWithResultsLinks(test); |
| document.body.appendChild(div); |
| appendExpectations(); |
| } |
| |
| |
| // When the user has finished selecting expectations to update, provide them |
| // with json to copy over. |
| function finishUpdate() |
| { |
| document.body.innerHTML = 'The next step is to copy the output below ' + |
| 'into a local file and save it. Then, run<br><code>python ' + |
| 'src/webkit/tools/layout_tests/webkitpy/layout_tests/update_expectat' + |
| 'ions_from_dashboard.py path/to/local/file</code><br>in order to ' + |
| 'update the expectations file.<br><textarea id="results" '+ |
| 'style="width:600px;height:600px;"> ' + |
| JSON.stringify(g_confirmedTests) + '</textarea>'; |
| results.focus(); |
| document.execCommand('SelectAll'); |
| } |
| |
| // Handle user click on "include selected" button. |
| // Includes the tests that are selected and exclude the rest. |
| // @param {Object} testsNeedingUpdate Tests that need updating. |
| // @param {Array.<string>} keys Keys into the testNeedingUpdate object. |
| function handleUpdate(testsNeedingUpdate, keys) |
| { |
| var test = keys[g_history.dashboardSpecificState.updateIndex]; |
| var updates = testsNeedingUpdate[test]; |
| for (var builder in updates) { |
| // Add included tests, and delete excluded tests if |
| // they were previously included. |
| if ($(builder).checked) { |
| if (!g_confirmedTests[test]) |
| g_confirmedTests[test] = {}; |
| g_confirmedTests[test][builder] = testsNeedingUpdate[test][builder]; |
| } else if (g_confirmedTests[test] && g_confirmedTests[test][builder]) { |
| delete g_confirmedTests[test][builder]; |
| if (!Object.keys(g_confirmedTests[test]).length) |
| delete g_confirmedTests[test]; |
| } |
| } |
| nextUpdate(testsNeedingUpdate, keys); |
| } |
| |
| |
| // Move to the next item to update. |
| // @param {Object} testsNeedingUpdate Tests that need updating. |
| // @param {Array.<string>} keys Keys into the testNeedingUpdate object. |
| function nextUpdate(testsNeedingUpdate, keys) |
| { |
| setUpdateIndex(g_history.dashboardSpecificState.updateIndex + 1, testsNeedingUpdate, keys); |
| } |
| |
| |
| // Advance the index we are updating at. If we walk over the end |
| // or beginning, just loop. |
| // @param {string} newIndex The index into the keys to move to. |
| // @param {Object} testsNeedingUpdate Tests that need updating. |
| // @param {Array.<string>} keys Keys into the testNeedingUpdate object. |
| function setUpdateIndex(newIndex, testsNeedingUpdate, keys) |
| { |
| if (newIndex == -1) |
| newIndex = keys.length - 1; |
| else if (newIndex == keys.length) |
| newIndex = 0; |
| g_history.setQueryParameter("updateIndex", newIndex); |
| showUpdateInfoForTest(testsNeedingUpdate, keys); |
| } |
| |
| 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 platforms.</div>'; |
| |
| var html = ''; |
| var shownBuilders = []; |
| for (var j = 0; j < testResults.length; j++) { |
| shownBuilders.push(testResults[j].builder); |
| html += htmlForSingleTestRow(testResults[j]); |
| } |
| |
| 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 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()) { |
| html += ' | ' + linkHTMLToToggleState('showLargeExpectations', 'large thumbnails'); |
| if (testResults && currentBuilderGroup().master().name == WEBKIT_BUILDER_MASTER) { |
| var revision = g_history.dashboardSpecificState.revision || ''; |
| html += '<form onsubmit="g_history.setQueryParameter(\'revision\', revision.value);' + |
| 'return false;">Show results for WebKit revision: ' + |
| '<input name=revision placeholder="e.g. 65540" value="' + revision + |
| '" id=revision-input></form>'; |
| } else |
| 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 getExpectationsContainer(expectationsContainers, parentContainer, expectationsType) |
| { |
| if (!expectationsContainers[expectationsType]) { |
| var container = document.createElement('div'); |
| container.className = 'expectations-container'; |
| parentContainer.appendChild(container); |
| expectationsContainers[expectationsType] = container; |
| } |
| return expectationsContainers[expectationsType]; |
| } |
| |
| function ensureTrailingSlash(path) |
| { |
| if (path.match(/\/$/)) |
| return path; |
| return path + '/'; |
| } |
| |
| 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); |
| } |
| |
| // Adds a specific expectation. If it's an image, it's only added on the |
| // image's onload handler. If it's a text file, then a script tag is appended |
| // as a hack to see if the file 404s (necessary since it's cross-domain). |
| // Once all the expectations for a specific type have loaded or errored |
| // (e.g. all the text results), then we go through and identify which platform |
| // uses which expectation. |
| // |
| // @param {Object} expectationsContainers Map from expectations type to |
| // container DIV. |
| // @param {Element} parentContainer Container element for |
| // expectationsContainer divs. |
| // @param {string} platform Platform string. Empty string for non-platform |
| // specific expectations. |
| // @param {string} path Relative path to the expectation. |
| // @param {string} base Base path for the expectation URL. |
| // @param {string} opt_builder Builder whose actual results this expectation |
| // points to. |
| function addExpectationItem(expectationsContainers, parentContainer, platform, path, base, opt_builder) |
| { |
| var parts = path.split('.') |
| var fileExtension = parts[parts.length - 1]; |
| if (fileExtension == 'html') |
| fileExtension = 'txt'; |
| |
| var container = getExpectationsContainer(expectationsContainers, parentContainer, fileExtension); |
| var isImage = path.match(/\.png$/); |
| |
| // FIXME: Stop using script tags once all the places we pull from support CORS. |
| var platformPart = platform ? ensureTrailingSlash(platform) : ''; |
| |
| var childContainer = document.createElement('span'); |
| childContainer.className = 'unloaded'; |
| |
| var appendExpectationsItem = function(item) { |
| childContainer.appendChild(expectationsTitle(platformPart, path, opt_builder)); |
| childContainer.className = 'expectations-item'; |
| item.className = 'expectation ' + fileExtension; |
| if (g_history.dashboardSpecificState.showLargeExpectations) |
| item.className += ' large'; |
| childContainer.appendChild(item); |
| }; |
| |
| var url = base + platformPart + path; |
| if (isImage || !string.startsWith(base, 'https://svn.webkit.org')) { |
| var dummyNode = document.createElement(isImage ? 'img' : 'script'); |
| dummyNode.src = url; |
| dummyNode.onload = function() { |
| var item; |
| if (isImage) { |
| item = dummyNode; |
| if (string.startsWith(base, 'https://svn.webkit.org')) |
| maybeAddPngChecksum(item, url); |
| } else { |
| item = document.createElement('iframe'); |
| item.src = url; |
| } |
| appendExpectationsItem(item); |
| } |
| dummyNode.onerror = function() { |
| childContainer.parentNode.removeChild(childContainer); |
| } |
| |
| // Append script elements now so that they load. Images load without being |
| // appended to the DOM. |
| if (!isImage) |
| childContainer.appendChild(dummyNode); |
| } else { |
| loader.request(url, |
| function(xhr) { |
| var item = document.createElement('pre'); |
| item.innerText = xhr.responseText; |
| appendExpectationsItem(item); |
| }, |
| function(xhr) {/* Do nothing on errors since they're expected */}); |
| } |
| |
| container.appendChild(childContainer); |
| } |
| |
| function addExpectations(expectationsContainers, container, base, |
| platform, text, png, reftest_html_file, reftest_mismatch_html_file) |
| { |
| var builder = ''; |
| addExpectationItem(expectationsContainers, container, platform, text, base, builder); |
| addExpectationItem(expectationsContainers, container, platform, png, base, builder); |
| addExpectationItem(expectationsContainers, container, platform, reftest_html_file, base, builder); |
| addExpectationItem(expectationsContainers, container, platform, reftest_mismatch_html_file, base, builder); |
| } |
| |
| function expectationsTitle(platform, path, builder) |
| { |
| var header = document.createElement('h3'); |
| header.className = 'expectations-title'; |
| |
| var innerHTML; |
| if (builder) { |
| var resultsType; |
| if (string.endsWith(path, '-crash-log.txt')) |
| resultsType = 'STACKTRACE'; |
| else if (string.endsWith(path, '-actual.txt') || string.endsWith(path, '-actual.png')) |
| resultsType = 'ACTUAL RESULTS'; |
| else if (string.endsWith(path, '-wdiff.html')) |
| resultsType = 'WDIFF'; |
| else |
| resultsType = 'DIFF'; |
| |
| innerHTML = resultsType + ': ' + builder; |
| } else if (platform === "") { |
| var parts = path.split('/'); |
| innerHTML = parts[parts.length - 1]; |
| } else |
| innerHTML = platform || path; |
| |
| header.innerHTML = '<div class=title>' + innerHTML + |
| '</div><div style="float:left"> </div>' + |
| '<div class=platforms style="float:right"></div>'; |
| header.platform = platform; |
| return header; |
| } |
| |
| function loadExpectations(expectationsContainer) |
| { |
| var test = expectationsContainer.getAttribute('test'); |
| if (g_history.isLayoutTestResults()) |
| loadExpectationsLayoutTests(test, expectationsContainer); |
| else { |
| var results = g_testToResultsMap[test]; |
| for (var i = 0; i < results.length; i++) |
| loadNonWebKitResultsForBuilder(results[i].builder, test, expectationsContainer); |
| } |
| } |
| |
| 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_MODIFIERS prefix as well. |
| var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndexes[i]]; |
| var pathToLog = builderMaster(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 buildInfoForRevision(builder, revision) |
| { |
| var revisions = g_resultsByBuilder[builder].webkitRevision; |
| var revisionStart = 0, revisionEnd = 0, buildNumber = 0; |
| for (var i = 0; i < revisions.length; i++) { |
| if (revision > revisions[i]) { |
| revisionStart = revisions[i - 1]; |
| revisionEnd = revisions[i]; |
| buildNumber = g_resultsByBuilder[builder].buildNumbers[i - 1]; |
| break; |
| } |
| } |
| |
| if (revisionEnd) |
| revisionEnd++; |
| else |
| revisionEnd = ''; |
| |
| return {revisionStart: revisionStart, revisionEnd: revisionEnd, buildNumber: buildNumber}; |
| } |
| |
| function loadBaselinesForTest(expectationsContainers, expectationsContainer, test) { |
| var testWithoutSuffix = test.substring(0, test.lastIndexOf('.')); |
| var text = testWithoutSuffix + "-expected.txt"; |
| var png = testWithoutSuffix + "-expected.png"; |
| var reftest_html_file = testWithoutSuffix + "-expected.html"; |
| var reftest_mismatch_html_file = testWithoutSuffix + "-expected-mismatch.html"; |
| |
| addExpectationItem(expectationsContainers, expectationsContainer, null, test, TEST_URL_BASE_PATH); |
| |
| addExpectations(expectationsContainers, expectationsContainer, |
| TEST_URL_BASE_PATH, '', text, png, reftest_html_file, reftest_mismatch_html_file); |
| } |
| |
| function loadExpectationsLayoutTests(test, expectationsContainer) |
| { |
| // Map from file extension to container div for expectations of that type. |
| var expectationsContainers = {}; |
| |
| var revisionContainer = document.createElement('div'); |
| revisionContainer.textContent = "Showing results for: " |
| expectationsContainer.appendChild(revisionContainer); |
| for (var builder in currentBuilders()) { |
| if (builderMaster(builder).name == WEBKIT_BUILDER_MASTER) { |
| var latestRevision = g_history.dashboardSpecificState.revision || g_resultsByBuilder[builder].webkitRevision[0]; |
| var buildInfo = buildInfoForRevision(builder, latestRevision); |
| var revisionInfo = document.createElement('div'); |
| revisionInfo.style.cssText = 'background:lightgray;margin:0 3px;padding:0 2px;display:inline-block;'; |
| revisionInfo.innerHTML = builder + ' r' + buildInfo.revisionEnd + |
| ':r' + buildInfo.revisionStart + ', build ' + buildInfo.buildNumber; |
| revisionContainer.appendChild(revisionInfo); |
| } |
| } |
| |
| loadBaselinesForTest(expectationsContainers, expectationsContainer, test); |
| |
| var testWithoutSuffix = test.substring(0, test.lastIndexOf('.')); |
| var actualResultSuffixes = ['-actual.txt', '-actual.png', '-crash-log.txt', '-diff.txt', '-wdiff.html', '-diff.png']; |
| |
| for (var builder in currentBuilders()) { |
| var actualResultsBase; |
| if (builderMaster(builder).name == WEBKIT_BUILDER_MASTER) { |
| var latestRevision = g_history.dashboardSpecificState.revision || g_resultsByBuilder[builder].webkitRevision[0]; |
| var buildInfo = buildInfoForRevision(builder, latestRevision); |
| actualResultsBase = 'https://build.webkit.org/results/' + builder + |
| '/r' + buildInfo.revisionStart + ' (' + buildInfo.buildNumber + ')/'; |
| } else |
| console.error("Unexpected master name: " + master.name); |
| |
| for (var i = 0; i < actualResultSuffixes.length; i++) { |
| addExpectationItem(expectationsContainers, expectationsContainer, null, |
| testWithoutSuffix + actualResultSuffixes[i], actualResultsBase, 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') : []; |
| // Loading expectations is *very* slow. Use a large timeout to avoid |
| // totally hanging the renderer. |
| performChunkedAction(expectations, function(chunk) { |
| for (var i = 0, len = chunk.length; i < len; i++) |
| loadExpectations(chunk[i]); |
| postHeightChangedMessage(); |
| |
| }, hideLoadingUI, 10000); |
| } |
| |
| 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(tests, function(chunk) { |
| appendHTML(htmlForIndividualTests(chunk)); |
| }, appendExpectations, 500); |
| if (g_history.dashboardSpecificState.showChrome) |
| $('tests-input').value = g_history.dashboardSpecificState.tests; |
| } |
| |
| function performChunkedAction(tests, handleChunk, onComplete, timeout, opt_index) { |
| var index = opt_index || 0; |
| setTimeout(function() { |
| var chunk = Array.prototype.slice.call(tests, index * CHUNK_SIZE, (index + 1) * CHUNK_SIZE); |
| if (chunk.length) { |
| handleChunk(chunk); |
| performChunkedAction(tests, handleChunk, onComplete, timeout, ++index); |
| } else |
| onComplete(); |
| // No need for a timeout on the first chunked action. |
| }, index ? timeout : 0); |
| } |
| |
| function htmlForIndividualTests(tests) |
| { |
| var testsHTML = []; |
| for (var i = 0; i < tests.length; i++) { |
| var test = tests[i]; |
| var testNameHtml = ''; |
| if (g_history.dashboardSpecificState.showChrome || tests.length > 1) { |
| if (g_history.isLayoutTestResults()) { |
| var tracURL = TEST_URL_BASE_PATH_TRAC + test; |
| testNameHtml += '<h2>' + linkHTMLToOpenWindow(tracURL, test) + '</h2>'; |
| } else |
| testNameHtml += '<h2>' + test + '</h2>'; |
| } |
| |
| testsHTML.push(testNameHtml + htmlForIndividualTestOnAllBuildersWithResultsLinks(test)); |
| } |
| return testsHTML.join('<hr>'); |
| } |
| |
| 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><form id=tests-form ' + |
| 'onsubmit="g_history.setQueryParameter(\'tests\', tests.value);' + |
| 'return false;"><span>Show tests on all platforms: </span>' + |
| '<input 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></form>' + |
| '<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">Show Tests Flagged As/With: </h2>' + |
| checkBoxToToggleState('showWontFixSkip', "Won't Fix/Skip") + |
| checkBoxToToggleState('showCorrectExpectations', 'Correct Expectations') + |
| checkBoxToToggleState('showWrongExpectations', 'Incorrect Expectations') + |
| checkBoxToToggleState('showFlaky', 'Flaky') + |
| checkBoxToToggleState('showSlow', 'Slow'); |
| } |
| |
| function generatePageForBuilder(builderName) |
| { |
| processTestRunsForBuilder(builderName); |
| |
| var results = g_perBuilderFailures[builderName]; |
| sortTests(results, g_history.dashboardSpecificState.sortColumn, g_history.dashboardSpecificState.sortOrder); |
| |
| var testsHTML = ''; |
| if (results.length) { |
| var tableRowsHTML = ''; |
| for (var i = 0; i < results.length; i++) |
| tableRowsHTML += htmlForSingleTestRow(results[i]) |
| testsHTML = htmlForTestTable(tableRowsHTML); |
| } else { |
| testsHTML = '<div>No tests found. '; |
| if (g_history.isLayoutTestResults()) |
| testsHTML += 'Try showing tests with correct expectations.</div>'; |
| else |
| testsHTML += 'This means no tests have failed!</div>'; |
| } |
| |
| var html = htmlForNavBar(); |
| |
| if (g_history.isLayoutTestResults()) |
| html += htmlForTestsWithExpectationsButNoFailures(builderName) + 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, |
| legacyExpectationsSemantics: 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>'; |
| for (var expectation in expectationsMap()) |
| html += '<div class=' + expectation + '>' + expectationsMap()[expectation] + '</div>'; |
| |
| html += '<div class=merge>WEBKIT MERGE</div>'; |
| if (g_history.isLayoutTestResults()) |
| html += '</div><br style="clear:both"><div>TIMES:</div>' + |
| htmlForSlowTimes(MIN_SECONDS_FOR_SLOW_TEST) + |
| '<div>DEBUG TIMES:</div>' + |
| htmlForSlowTimes(MIN_SECONDS_FOR_SLOW_TEST_DEBUG); |
| |
| legend.innerHTML = html; |
| } |
| |
| function htmlForSlowTimes(minTime) |
| { |
| return '<ul><li><1 second == !SLOW</li><li>>1 second && <' + |
| minTime + ' seconds == SLOW || !SLOW is fine</li><li>>' + |
| minTime + ' seconds == SLOW</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); |