blob: 1cc98e7f2907215a8cee987094225120fa7eedf5 [file] [log] [blame]
// 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 GTEST_MODIFIERS = ['FLAKY', 'FAILS', 'MAYBE', 'DISABLED'];
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 = 'http://build.chromium.org/f/chromium/layout_test_results/';
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;
var CHUNK_SIZE = 25;
// FIXME: Figure out how to make this not be hard-coded.
var VIRTUAL_SUITES = {
'virtual/gpu/fast/canvas': 'fast/canvas',
'virtual/gpu/canvas/philip': 'canvas/philip'
};
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 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 '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) {
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;
}
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;
// 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];
resultsForTest.slowestTime = Math.max(resultsForTest.slowestTime, times[i][RLE.VALUE]);
}
determineFlakiness(g_resultsByBuilder[builderName][FAILURE_MAP_KEY], resultsForTest);
failures.push(resultsForTest);
if (!g_testToResultsMap[test])
g_testToResultsMap[test] = [];
g_testToResultsMap[test].push(resultsForTest);
}
g_perBuilderFailures[builderName] = failures;
}
function determineFlakiness(failureMap, 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;
// 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 = failureMap[result];
resultsMap[expectation] = true;
numRealResults++;
}
resultsForTest.actualResults = Object.keys(resultsMap);
resultsForTest.flips = i - 1;
resultsForTest.isFlaky = numRealResults > 1;
}
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][FAILURE_MAP_KEY];
for (var i = 0; i < rawResults.length; i++) {
currentIndex += rawResults[i][RLE.LENGTH];
if (currentIndex > index)
return isFailingResult(failureMap, 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 failureMap = g_resultsByBuilder[builder][FAILURE_MAP_KEY];
var index = 0;
var failures = [];
for (var i = 0; i < rawResults.length; i++) {
var numResults = rawResults[i][RLE.LENGTH];
if (isFailingResult(failureMap, 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');
if (g_resultsByBuilder[builder][BLINK_REVISIONS_KEY])
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] +
'/' + chromeRevision + '/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 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;
for (var i = 0; i < buildNumbers.length; i++) {
var currentResultArray, currentTimeArray, innerHTML, resultString;
if (i > indexToReplaceCurrentResult) {
currentResultArray = results.shift();
if (currentResultArray) {
resultString = g_resultsByBuilder[builder][FAILURE_MAP_KEY][currentResultArray[RLE.VALUE]];
indexToReplaceCurrentResult += currentResultArray[RLE.LENGTH];
} else {
resultString = NO_DATA;
indexToReplaceCurrentResult += buildNumbers.length;
}
}
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 || '&nbsp;';
}
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 == '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)
{
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, '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 ? '&uarr;' : '&darr;' ) + '</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 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() || 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 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.
// @param {string} opt_suite "virtual suite" that the test belongs to, if any.
function addExpectationItem(expectationsContainers, parentContainer, platform, path, base, opt_builder, opt_suite)
{
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 suitePart = opt_suite ? ensureTrailingSlash(opt_suite) : '';
var childContainer = document.createElement('span');
childContainer.className = 'unloaded';
var appendExpectationsItem = function(item) {
childContainer.appendChild(expectationsTitle(platformPart + suitePart, path, opt_builder));
childContainer.className = 'expectations-item';
item.className = 'expectation ' + fileExtension;
if (g_history.dashboardSpecificState.showLargeExpectations)
item.className += ' large';
childContainer.appendChild(item);
handleFinishedLoadingExpectations(container);
};
var url = base + platformPart + path;
if (isImage) {
var dummyNode = document.createElement(isImage ? 'img' : 'script');
dummyNode.src = url;
dummyNode.onload = function() {
var item;
if (isImage) {
item = dummyNode;
maybeAddPngChecksum(item, url);
} else {
item = document.createElement('iframe');
item.src = url;
}
appendExpectationsItem(item);
}
dummyNode.onerror = function() {
childContainer.parentNode.removeChild(childContainer);
handleFinishedLoadingExpectations(container);
}
// 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);
}
// Identifies which expectations are used on which platform once all the
// expectations of a given type have loaded (e.g. the container for png
// expectations for this test had no child elements with the class
// "unloaded").
//
// @param {string} container Element containing the expectations for a given
// test and a given type (e.g. png).
function handleFinishedLoadingExpectations(container)
{
if (container.getElementsByClassName('unloaded').length)
return;
var titles = container.getElementsByClassName('expectations-title');
for (var platform in g_fallbacksMap) {
var fallbacks = g_fallbacksMap[platform];
var winner = null;
var winningIndex = -1;
for (var i = 0; i < titles.length; i++) {
var title = titles[i];
if (!winner && title.platform == "") {
winner = title;
continue;
}
var rawPlatform = title.platform && title.platform.replace('platform/', '');
for (var j = 0; j < fallbacks.length; j++) {
if ((winningIndex == -1 || winningIndex > j) && rawPlatform == fallbacks[j]) {
winningIndex = j;
winner = title;
break;
}
}
}
if (winner)
winner.getElementsByClassName('platforms')[0].innerHTML += '<div class=used-platform>' + platform + '</div>';
else {
console.log('No expectations identified for this test. This means ' +
'there is a logic bug in the dashboard for which expectations a ' +
'platform uses or src.chromium.org is giving 5XXs.');
}
}
consolidateUsedPlatforms(container);
}
// Consolidate platforms when all sub-platforms for a given platform are represented.
// e.g., if all of the WIN- platforms are there, replace them with just WIN.
function consolidateUsedPlatforms(container)
{
var allPlatforms = Object.keys(g_fallbacksMap);
var platformElements = container.getElementsByClassName('platforms');
for (var i = 0, platformsLength = platformElements.length; i < platformsLength; i++) {
var usedPlatforms = platformElements[i].getElementsByClassName('used-platform');
if (!usedPlatforms.length)
continue;
var platforms = {};
platforms['MAC'] = {};
platforms['WIN'] = {};
platforms['LINUX'] = {};
allPlatforms.forEach(function(platform) {
if (string.startsWith(platform, 'MAC'))
platforms['MAC'][platform] = 1;
else if (string.startsWith(platform, 'WIN'))
platforms['WIN'][platform] = 1;
else if (string.startsWith(platform, 'LINUX'))
platforms['LINUX'][platform] = 1;
});
for (var j = 0, usedPlatformsLength = usedPlatforms.length; j < usedPlatformsLength; j++) {
for (var platform in platforms)
delete platforms[platform][usedPlatforms[j].textContent];
}
for (var platform in platforms) {
if (!Object.keys(platforms[platform]).length) {
var nodesToRemove = [];
for (var j = 0, usedPlatformsLength = usedPlatforms.length; j < usedPlatformsLength; j++) {
var usedPlatform = usedPlatforms[j];
if (string.startsWith(usedPlatform.textContent, platform))
nodesToRemove.push(usedPlatform);
}
nodesToRemove.forEach(function(element) { element.parentNode.removeChild(element); });
platformElements[i].insertAdjacentHTML('afterBegin', '<div class=used-platform>' + platform + '</div>');
}
}
}
}
function addExpectations(expectationsContainers, container, base,
platform, text, png, reftest_html_file, reftest_mismatch_html_file, suite)
{
var builder = '';
addExpectationItem(expectationsContainers, container, platform, text, base, builder, suite);
addExpectationItem(expectationsContainers, container, platform, png, base, builder, suite);
addExpectationItem(expectationsContainers, container, platform, reftest_html_file, base, builder, suite);
addExpectationItem(expectationsContainers, container, platform, reftest_mismatch_html_file, base, builder, suite);
}
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">&nbsp;</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++)
if (g_history.isGPUTestResults())
loadGPUResultsForBuilder(results[i].builder, test, expectationsContainer);
else
loadNonWebKitResultsForBuilder(results[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 = builderMaster(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_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 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 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";
var suite = lookupVirtualTestSuite(test);
if (!suite)
addExpectationItem(expectationsContainers, expectationsContainer, null, test, TEST_URL_BASE_PATH_FOR_XHR);
addExpectations(expectationsContainers, expectationsContainer,
TEST_URL_BASE_PATH_FOR_XHR, '', text, png, reftest_html_file, reftest_mismatch_html_file, suite);
var fallbacks = allFallbacks();
for (var i = 0; i < fallbacks.length; i++) {
var fallback = 'platform/' + fallbacks[i];
addExpectations(expectationsContainers, expectationsContainer, TEST_URL_BASE_PATH_FOR_XHR, fallback, text, png,
reftest_html_file, reftest_mismatch_html_file, suite);
}
if (suite)
loadBaselinesForTest(expectationsContainers, expectationsContainer, baseTest(test, suite));
}
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);
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 = TEST_RESULTS_BASE_PATH + currentBuilders()[builder] + '/results/layout-test-results/';
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);
}
var g_allFallbacks;
// Returns the reverse sorted, deduped list of all platform fallback
// directories.
function allFallbacks()
{
if (!g_allFallbacks) {
var holder = {};
for (var platform in g_fallbacksMap) {
var fallbacks = g_fallbacksMap[platform];
for (var i = 0; i < fallbacks.length; i++)
holder[fallbacks[i]] = 1;
}
g_allFallbacks = [];
for (var fallback in holder)
g_allFallbacks.push(fallback);
g_allFallbacks.sort(function(a, b) {
if (a == b)
return 0;
return a < b;
});
}
return g_allFallbacks;
}
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 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>';
}
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">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 results = g_perBuilderFailures[builderName].filter(shouldShowTest);
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 {
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);
}
var g_fallbacksMap = {};
g_fallbacksMap['WIN-XP'] = ['chromium-win-xp', 'chromium-win', 'chromium'];
g_fallbacksMap['WIN-7'] = ['chromium-win', 'chromium'];
g_fallbacksMap['MAC-SNOWLEOPARD'] = ['chromium-mac-snowleopard', 'chromium-mac', 'chromium'];
g_fallbacksMap['MAC-LION'] = ['chromium-mac', 'chromium'];
g_fallbacksMap['LINUX-32'] = ['chromium-linux-x86', 'chromium-linux', 'chromium-win', 'chromium'];
g_fallbacksMap['LINUX-64'] = ['chromium-linux', 'chromium-win', 'chromium'];
function htmlForFallbackHelp(fallbacks)
{
return '<ol class=fallback-list><li>' + fallbacks.join('</li><li>') + '</li></ol>';
}
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]][FAILURE_MAP_KEY];
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><h3>Test expectations fallback order.</h3>';
for (var platform in g_fallbacksMap)
html += '<div class=fallback-header>' + platform + '</div>' + htmlForFallbackHelp(g_fallbacksMap[platform]);
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);