Commit 8c11e836 authored by ojan@google.com's avatar ojan@google.com

Run-length encode the JSON results. This makes them considerably

smaller, which should make the dashboard load a ton faster and
allows us to store a lot more runs. Changing the default to 500 runs
for now.

JS Changes:
-Add ability to control the number of results shown per test.
-Add a debug mode for easier local testing
-Consolidate query and hash parameter handling
-Identify tests that need to be marked "SLOW"
-Hide tests that fail for many runs then start passing for many runs.

Tony, can you review the python?
Arv, can you review the JS?

BUG=none
TEST=manual
Review URL: http://codereview.chromium.org/201073

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@25820 0039d316-1c4b-4281-b951-d872f2087c98
parent 5c9e97ac
...@@ -15,17 +15,22 @@ ...@@ -15,17 +15,22 @@
font-size: 16px; font-size: 16px;
margin-bottom: .25em; margin-bottom: .25em;
} }
form { #max-results-form {
margin: 3px 0; display: inline;
}
#max-results-input {
width: 30px;
}
#tests-form {
display: -webkit-box; display: -webkit-box;
} }
form > * { #tests-form > * {
display: -webkit-box; display: -webkit-box;
} }
form > div { #tests-form > div {
-webkit-box-flex: 0; -webkit-box-flex: 0;
} }
form > input { #tests-input {
-webkit-box-flex: 1; -webkit-box-flex: 1;
} }
.test-link { .test-link {
...@@ -172,17 +177,131 @@ ...@@ -172,17 +177,131 @@
* -add the builder name to the list of builders below. * -add the builder name to the list of builders below.
*/ */
// Default to layout_tests. // CONSTANTS
var testType = 'layout_test_results'; var FORWARD = 'forward';
var params = window.location.search.substring(1).split('&'); var BACKWARD = 'backward';
var TEST_URL_BASE_PATH =
'http://trac.webkit.org/projects/webkit/browser/trunk/';
var BUILDERS_BASE_PATH =
'http://build.chromium.org/buildbot/waterfall/builders/';
var EXPECTATIONS_MAP = {
'T': 'TIMEOUT',
'C': 'CRASH',
'P': 'PASS',
'F': 'TEXT FAIL',
'S': 'SIMPLIFIED',
'I': 'IMAGE',
'O': 'OTHER',
'N': 'NO DATA'
};
var PLATFORMS = {'MAC': 'MAC', 'LINUX': 'LINUX', 'WIN': 'WIN'};
var BUILD_TYPES = {'DEBUG': 'DBG', 'RELEASE': 'RELEASE'};
// GLOBALS
// The DUMMYVALUE gets shifted off the array in the first call to
// generatePage.
var tableHeaders = ['DUMMYVALUE', 'bugs', 'modifiers', 'expectations',
'missing', 'extra', 'slowest run',
'flakiness (numbers are runtimes in seconds)'];
var perBuilderPlatformAndBuildType = {};
var perBuilderFailures = {};
// Map of builder to arrays of tests that are listed in the expectations file
// but have for that builder.
var perBuilderWithExpectationsButNoFailures = {};
// Generic utility functions.
function $(id) {
return document.getElementById(id);
}
function stringContains(a, b) {
return a.indexOf(b) != -1;
}
function isValidName(str) {
return str.match(/[A-Za-z0-9\-\_,]/);
}
function trimString(str) {
return str.replace(/^\s+|\s+$/g, '');
}
function anyKeyInString(object, string) {
for (var key in object) {
if (stringContains(string, key))
return true;
}
return false;
}
function validateParameter(state, key, value, validateFn) {
if (validateFn()) {
state[key] = value;
} else {
console.log(key + ' value is not valid: ' + value);
}
}
/**
* Parses a string (e.g. window.location.hash) and calls
* validValueHandler(key, value) for each key-value pair in the string.
*/
function parseParameters(parameterStr, validValueHandler) {
var params = parameterStr.split('&');
for (var i = 0; i < params.length; i++) { for (var i = 0; i < params.length; i++) {
var thisParam = params[i].split('='); var thisParam = params[i].split('=');
if (thisParam[0] == 'testtype') { if (thisParam.length != 2) {
testType = thisParam[1]; console.log('Invalid query parameter: ' + params[i]);
break; continue;
}
var key = thisParam[0];
var value = decodeURIComponent(thisParam[1]);
if (!validValueHandler(key, value))
console.log('Invalid key: ' + key + ' value: ' + value);
}
}
function appendScript(path) {
var script = document.createElement('script');
script.src = path;
document.getElementsByTagName('head')[0].appendChild(script);
}
// Parse query parameters.
var queryState = {'debug': false, 'testType': 'layout_test_results'};
function handleValidQueryParameter(key, value) {
switch (key) {
case 'testType':
validateParameter(queryState, key, value,
function() {
return isValidName(value);
});
return true;
case 'debug':
queryState[key] = value == 'true';
return true;
default:
return false;
} }
} }
parseParameters(window.location.search.substring(1),
handleValidQueryParameter);
if (queryState['debug']) {
// In debug mode point to the results.json and expectations.json in the
// local tree. Useful for debugging changes to the python JSON generator.
var builders = {'DUMMY_BUILDER_NAME': ''};
var builderBase = '../../Debug/';
queryState['testType'] = 'layout-test-results';
} else {
// Map of builderName (the name shown in the waterfall) // Map of builderName (the name shown in the waterfall)
// to builderPath (the path used in the builder's URL) // to builderPath (the path used in the builder's URL)
// TODO(ojan): Make this switch based off of the testType. // TODO(ojan): Make this switch based off of the testType.
...@@ -200,32 +319,136 @@ ...@@ -200,32 +319,136 @@
'Webkit Mac10.5 (dbg)(2)': 'webkit-dbg-mac5-2', 'Webkit Mac10.5 (dbg)(2)': 'webkit-dbg-mac5-2',
'Webkit Mac10.5 (dbg)(3)': 'webkit-dbg-mac5-3' 'Webkit Mac10.5 (dbg)(3)': 'webkit-dbg-mac5-3'
}; };
var builderBase = 'http://build.chromium.org/buildbot/';
}
// Parse hash parameters.
// Permalinkable state of the page.
var currentState = {};
var defaultStateValues = {
sortOrder: BACKWARD,
sortColumn: 'flakiness',
showWontFix: false,
showCorrectExpectations: false,
showFlaky: true,
maxResults: 200
};
for (var builder in builders) {
defaultStateValues.builder = builder;
break;
}
function fillDefaultStateValues() {
// tests has no states with default values.
if (currentState.tests)
return;
for (var state in defaultStateValues) {
if (!(state in currentState))
currentState[state] = defaultStateValues[state];
}
}
function handleValidHashParameter(key, value) {
switch(key) {
case 'tests':
validateParameter(currentState, key, value,
function() {
return isValidName(value);
});
return true;
case 'builder':
validateParameter(currentState, key, value,
function() {
return value in builders;
});
return true;
case 'sortColumn':
validateParameter(currentState, key, value,
function() {
for (var i = 0; i < tableHeaders.length; i++) {
if (value == getSortColumnFromTableHeader(tableHeaders[i]))
return true;
}
return value == 'test';
});
return true;
case 'sortOrder':
validateParameter(currentState, key, value,
function() {
return value == FORWARD || value == BACKWARD;
});
return true;
case 'maxResults':
validateParameter(currentState, key, value,
function() {
return value.match(/^\d+$/)
});
return true;
case 'showWontFix':
case 'showCorrectExpectations':
case 'showFlaky':
currentState[key] = value == 'true';
return true;
default:
return false;
}
}
// Keep the location around for detecting changes to hash arguments
// manually typed into the URL bar.
var oldLocation;
function parseAllParameters() {
oldLocation = window.location.href;
parseParameters(window.location.search.substring(1),
handleValidQueryParameter);
parseParameters(window.location.hash.substring(1),
handleValidHashParameter);
fillDefaultStateValues();
}
parseAllParameters();
// Append JSON script elements.
var resultsByBuilder = {}; var resultsByBuilder = {};
// Maps test path to an array of {builder, testResults} objects. // Maps test path to an array of {builder, testResults} objects.
var testToResultsMap = {}; var testToResultsMap = {};
var expectationsByTest = {}; var expectationsByTest = {};
function ADD_RESULTS(builds) { function ADD_RESULTS(builds) {
for (var builderName in builds) { for (var builderName in builds) {
if (builderName != 'version')
resultsByBuilder[builderName] = builds[builderName]; resultsByBuilder[builderName] = builds[builderName];
} }
generatePage(); generatePage();
} }
var BUILDER_BASE = 'http://build.chromium.org/buildbot/';
function getPathToBuilderResultsFile(builderName) { function getPathToBuilderResultsFile(builderName) {
return BUILDER_BASE + testType + '/' + builders[builderName] + '/'; return builderBase + queryState['testType'] + '/' +
builders[builderName] + '/';
} }
for (var builderName in builders) { for (var builderName in builders) {
var script = document.createElement('script'); appendScript(getPathToBuilderResultsFile(builderName) + 'results.json');
script.src = getPathToBuilderResultsFile(builderName) + 'results.json';
document.getElementsByTagName('head')[0].appendChild(script);
} }
var script = document.createElement('script');
// Grab expectations file from any builder. // Grab expectations file from any builder.
script.src = getPathToBuilderResultsFile(builderName) + 'expectations.json'; appendScript(getPathToBuilderResultsFile(builderName) + 'expectations.json');
document.getElementsByTagName('head')[0].appendChild(script);
var expectationsLoaded = false; var expectationsLoaded = false;
function ADD_EXPECTATIONS(expectations) { function ADD_EXPECTATIONS(expectations) {
...@@ -234,38 +457,6 @@ ...@@ -234,38 +457,6 @@
generatePage(); generatePage();
} }
// CONSTANTS
var FORWARD = 'forward';
var BACKWARD = 'backward';
var TEST_URL_BASE_PATH =
'http://trac.webkit.org/projects/webkit/browser/trunk/';
var BUILDERS_BASE_PATH =
'http://build.chromium.org/buildbot/waterfall/builders/';
var EXPECTATIONS_MAP = {
'T': 'TIMEOUT',
'C': 'CRASH',
'P': 'PASS',
'F': 'TEXT FAIL',
'S': 'SIMPLIFIED',
'I': 'IMAGE',
'O': 'OTHER',
'N': 'NO DATA'
};
var PLATFORMS = {'MAC': 'MAC', 'LINUX': 'LINUX', 'WIN': 'WIN'};
var BUILD_TYPES = {'DEBUG': 'DBG', 'RELEASE': 'RELEASE'};
// GLOBALS
// The DUMMYVALUE gets shifted off the array in the first call to
// generatePage.
var tableHeaders = ['DUMMYVALUE', 'bugs', 'modifiers', 'expectations',
'missing', 'extra', 'slowest run',
'flakiness (numbers are runtimes in seconds)'];
var perBuilderPlatformAndBuildType = {};
var oldLocation;
var perBuilderFailures = {};
// Map of builder to arrays of tests that are listed in the expectations file
// but have for that builder.
var perBuilderWithExpectationsButNoFailures = {};
function createResultsObjectForTest(test) { function createResultsObjectForTest(test) {
return { return {
...@@ -297,26 +488,6 @@ ...@@ -297,26 +488,6 @@
} }
} }
function $(id) {
return document.getElementById(id);
}
function stringContains(a, b) {
return a.indexOf(b) != -1;
}
function trimString(str) {
return str.replace(/^\s+|\s+$/g, '');
}
function anyKeyInString(object, string) {
for (var key in object) {
if (stringContains(string, key))
return true;
}
return false;
}
/** /**
* Returns whether the given string of modifiers applies to the platform and * Returns whether the given string of modifiers applies to the platform and
* build type of the given builder. * build type of the given builder.
...@@ -585,17 +756,16 @@ ...@@ -585,17 +756,16 @@
htmlArrays.modifiers.join('<div class=separator></div>'); htmlArrays.modifiers.join('<div class=separator></div>');
} }
var rawResults = resultsByBuilder[builderName].tests[test].results; var rawTest = resultsByBuilder[builderName].tests[test];
resultsForTest.rawTimes = rawTest.times;
var rawResults = rawTest.results;
resultsForTest.rawResults = rawResults; resultsForTest.rawResults = rawResults;
var results = rawResults.split(''); resultsForTest.flips = rawResults.length - 1;
var unexpectedExpectations = []; var unexpectedExpectations = [];
var resultsMap = {} var resultsMap = {}
for (var i = 0; i < results.length - 1; i++) { for (var i = 0; i < rawResults.length; i++) {
if (results[i] != results[i + 1]) var expectation = getExpectationsFileStringForResult(rawResults[i][1]);
resultsForTest.flips++;
var expectation = getExpectationsFileStringForResult(results[i]);
resultsMap[expectation] = true; resultsMap[expectation] = true;
} }
...@@ -618,20 +788,25 @@ ...@@ -618,20 +788,25 @@
missingExpectations.push(result); missingExpectations.push(result);
} }
// TODO(ojan): Make this detect the case of a test that has NODATA, var times = resultsByBuilder[builderName].tests[test].times;
// then fails for a few runs, then passes for the rest. We should for (var i = 0; i < times.length; i++) {
// consider that as meetsExpectations since every new test will have resultsForTest.slowestTime = Math.max(resultsForTest.slowestTime,
// that pattern. times[i][1]);
}
if (resultsForTest.slowestTime &&
(!resultsForTest.expectations ||
!stringContains(resultsForTest.expectations, 'TIMEOUT')) &&
(!resultsForTest.modifiers ||
!stringContains(resultsForTest.modifiers, 'SLOW'))) {
missingExpectations.push('SLOW');
}
resultsForTest.meetsExpectations = resultsForTest.meetsExpectations =
!missingExpectations.length && !extraExpectations.length; !missingExpectations.length && !extraExpectations.length;
resultsForTest.missing = missingExpectations.sort().join(' '); resultsForTest.missing = missingExpectations.sort().join(' ');
resultsForTest.extra = extraExpectations.sort().join(' '); resultsForTest.extra = extraExpectations.sort().join(' ');
var times = resultsByBuilder[builderName].tests[test].times;
resultsForTest.slowestTime = Math.max.apply(null, times)
resultsForTest.html = getHtmlForTestResults(builderName, test);
failures.push(resultsForTest); failures.push(resultsForTest);
if (!testToResultsMap[test]) if (!testToResultsMap[test])
...@@ -672,29 +847,49 @@ ...@@ -672,29 +847,49 @@
return bugs; return bugs;
} }
function didTestPassAllRuns(builderName, testPath) {
var numBuilds = resultsByBuilder[builderName].buildNumbers.length;
var passingResults = Array(numBuilds + 1).join('P');
var results = resultsByBuilder[builderName].tests[testPath].results;
return results == passingResults;
}
function loadBuilderPageForBuildNumber(builderName, buildNumber) { function loadBuilderPageForBuildNumber(builderName, buildNumber) {
window.open(BUILDERS_BASE_PATH + builderName + '/builds/' + buildNumber); window.open(BUILDERS_BASE_PATH + builderName + '/builds/' + buildNumber);
} }
function getHtmlForTestResults(builderName, testPath) { function getHtmlForTestResults(test, builder) {
var html = ''; var html = '';
var test = resultsByBuilder[builderName].tests[testPath]; var results = test.rawResults.concat();
var results = test.results.split(''); var times = test.rawTimes.concat();
var times = test.times; var buildNumbers = resultsByBuilder[builder].buildNumbers;
var buildNumbers = resultsByBuilder[builderName].buildNumbers;
for (var i = 0; i < results.length; i++) { var indexToReplaceCurrentResult = -1;
var indexToReplaceCurrentTime = -1;
var currentResultArray, currentTimeArray, currentResult, innerHTML;
for (var i = 0;
i < buildNumbers.length && i < currentState.maxResults;
i++) {
if (i > indexToReplaceCurrentResult) {
currentResultArray = results.shift();
if (currentResultArray) {
currentResult = currentResultArray[1];
indexToReplaceCurrentResult += currentResultArray[0];
} else {
currentResult = 'N';
indexToReplaceCurrentResult += buildNumbers.length;
}
}
if (i > indexToReplaceCurrentTime) {
currentTimeArray = times.shift();
var currentTime = 0;
if (currentResultArray) {
currentTime = currentTimeArray[1];
indexToReplaceCurrentTime += currentTimeArray[0];
} else {
indexToReplaceCurrentTime += buildNumbers.length;
}
innerHTML = currentTime || '&nbsp;';
}
var buildNumber = buildNumbers[i]; var buildNumber = buildNumbers[i];
var innerHTML = times[i] > 0 ? times[i] : '&nbsp;';
html += '<td title="Build:' + buildNumber + '" class="results ' + html += '<td title="Build:' + buildNumber + '" class="results ' +
results[i] + '" onclick=\'loadBuilderPageForBuildNumber("' + currentResult + '" onclick=\'loadBuilderPageForBuildNumber("' +
builderName + '","' + buildNumber + '")\'>' + innerHTML + '</td>'; builder + '","' + buildNumber + '")\'>' + innerHTML + '</td>';
} }
return html; return html;
} }
...@@ -717,7 +912,7 @@ ...@@ -717,7 +912,7 @@
* avoid common non-flaky cases. * avoid common non-flaky cases.
*/ */
function isTestFlaky(testResult) { function isTestFlaky(testResult) {
return testResult.flips > 1 && !isFixedNewTest(testResult); return testResult.flips > 1 && !isFixedTest(testResult);
} }
/** /**
...@@ -727,12 +922,21 @@ ...@@ -727,12 +922,21 @@
* Where that middle part can be a series of F's, S's or I's for the * Where that middle part can be a series of F's, S's or I's for the
* different types of failures. * different types of failures.
*/ */
function isFixedNewTest(testResult) { function isFixedTest(testResult) {
if (testResult.isFixedNewTest === undefined) { if (testResult.isFixedTest === undefined) {
testResult.isFixedNewTest = var results = testResult.rawResults;
testResult.rawResults.match(/^P+(S+|F+|I+)N+$/); var isFixedTest = results[0][1] == 'P';
if (isFixedTest && results.length > 1) {
var secondResult = results[1][1];
isFixedTest = secondResult == 'S' || secondResult == 'F' ||
secondResult == 'I';
} }
return testResult.isFixedNewTest; if (isFixedTest && results.length > 2) {
isFixedTest = results.length == 3 && results[2][1] == 'N';
}
testResult.isFixedTest = isFixedTest;
}
return testResult.isFixedTest;
} }
/** /**
...@@ -748,7 +952,7 @@ ...@@ -748,7 +952,7 @@
if (testResult.isWontFix && !currentState.showWontFix) if (testResult.isWontFix && !currentState.showWontFix)
return true; return true;
if ((testResult.meetsExpectations || isFixedNewTest(testResult)) && if ((testResult.meetsExpectations || isFixedTest(testResult)) &&
!currentState.showCorrectExpectations) { !currentState.showCorrectExpectations) {
// Only hide flaky tests that match their expectations if showFlaky // Only hide flaky tests that match their expectations if showFlaky
// is false. // is false.
...@@ -758,17 +962,17 @@ ...@@ -758,17 +962,17 @@
return !currentState.showFlaky && isTestFlaky(testResult); return !currentState.showFlaky && isTestFlaky(testResult);
} }
function getHTMLForSingleTestRow(test, opt_builder) { function getHTMLForSingleTestRow(test, builder, opt_isCrossBuilderView) {
if (shouldHideTest(test)) { if (shouldHideTest(test)) {
// The innerHTML call is considerably faster if we exclude the rows for // 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. // items we're not showing than if we hide them using display:none.
return ''; return '';
} }
// If opt_builder is provided, we're just viewing a single test // If opt_isCrossBuilderView is true, we're just viewing a single test
// with results for many builders, so the first column is builder names // with results for many builders, so the first column is builder names
// instead of test paths. // instead of test paths.
var testCellHTML = opt_builder ? opt_builder : var testCellHTML = opt_isCrossBuilderView ? builder :
'<span class="link" onclick="setState(\'tests\', \'' + test.test + '<span class="link" onclick="setState(\'tests\', \'' + test.test +
'\');return false;">' + test.test + '</span>'; '\');return false;">' + test.test + '</span>';
...@@ -783,9 +987,7 @@ ...@@ -783,9 +987,7 @@
'</td><td>' + test.missing + '</td><td>' + test.missing +
'</td><td>' + test.extra + '</td><td>' + test.extra +
'</td><td>' + (test.slowestTime ? test.slowestTime + 's' : '') + '</td><td>' + (test.slowestTime ? test.slowestTime + 's' : '') +
'</td>' + '</td>' + getHtmlForTestResults(test, builder) + '</tr>';
test.html +
'</tr>';
} }
function getSortColumnFromTableHeader(headerText) { function getSortColumnFromTableHeader(headerText) {
...@@ -890,8 +1092,6 @@ ...@@ -890,8 +1092,6 @@
} }
function generatePage() { function generatePage() {
parseCurrentLocation();
// Only continue if all the JSON files have loaded. // Only continue if all the JSON files have loaded.
if (!expectationsLoaded) if (!expectationsLoaded)
return; return;
...@@ -930,7 +1130,7 @@ ...@@ -930,7 +1130,7 @@
var tableRowsHTML = ''; var tableRowsHTML = '';
for (var j = 0; j < testResults.length; j++) { for (var j = 0; j < testResults.length; j++) {
tableRowsHTML += getHTMLForSingleTestRow(testResults[j].results, tableRowsHTML += getHTMLForSingleTestRow(testResults[j].results,
testResults[j].builder); testResults[j].builder, true);
} }
html += getHTMLForTestTable(tableRowsHTML); html += getHTMLForTestTable(tableRowsHTML);
} else { } else {
...@@ -952,7 +1152,8 @@ ...@@ -952,7 +1152,8 @@
builder + '</span>'; builder + '</span>';
} }
html += '</div>' + html += '</div>' +
'<form onsubmit="setState(\'tests\', tests.value);return false;">' + '<form id=tests-form ' +
'onsubmit="setState(\'tests\', tests.value);return false;">' +
'<div>Show tests on all platforms (slow): </div><input name=tests ' + '<div>Show tests on all platforms (slow): </div><input name=tests ' +
'placeholder="LayoutTests/foo/bar.html,LayoutTests/foo/baz.html" ' + 'placeholder="LayoutTests/foo/bar.html,LayoutTests/foo/baz.html" ' +
'id=tests-input></form><div id="loading-ui">LOADING...</div>' + 'id=tests-input></form><div id="loading-ui">LOADING...</div>' +
...@@ -968,8 +1169,8 @@ ...@@ -968,8 +1169,8 @@
function getLinkHTMLToToggleState(key, linkText) { function getLinkHTMLToToggleState(key, linkText) {
var isTrue = currentState[key]; var isTrue = currentState[key];
return '<span class=link onclick="setState(\'' + key + '\', \'' + !isTrue + return '<span class=link onclick="setState(\'' + key + '\', ' + !isTrue +
'\')">' + (isTrue ? 'Hide' : 'Show') + ' ' + linkText + '</span> | '; ')">' + (isTrue ? 'Hide' : 'Show') + ' ' + linkText + '</span> | ';
} }
function generatePageForBuilder(builderName) { function generatePageForBuilder(builderName) {
...@@ -979,9 +1180,12 @@ ...@@ -979,9 +1180,12 @@
var results = perBuilderFailures[builderName]; var results = perBuilderFailures[builderName];
sortTests(results, currentState.sortColumn, currentState.sortOrder); sortTests(results, currentState.sortColumn, currentState.sortOrder);
for (var i = 0; i < results.length; i++) { for (var i = 0; i < results.length; i++) {
tableRowsHTML += getHTMLForSingleTestRow(results[i]); tableRowsHTML += getHTMLForSingleTestRow(results[i], builderName);
} }
var testsHTML = tableRowsHTML ? getHTMLForTestTable(tableRowsHTML) :
'<div>No tests. Try showing tests with correct expectations.</div>';
var html = getHTMLForNavBar(builderName) + var html = getHTMLForNavBar(builderName) +
getHTMLForTestsWithExpectationsButNoFailures(builderName) + getHTMLForTestsWithExpectationsButNoFailures(builderName) +
'<h2>Failing tests</h2><div>' + '<h2>Failing tests</h2><div>' +
...@@ -989,9 +1193,13 @@ ...@@ -989,9 +1193,13 @@
getLinkHTMLToToggleState('showCorrectExpectations', getLinkHTMLToToggleState('showCorrectExpectations',
'tests with correct expectations') + 'tests with correct expectations') +
getLinkHTMLToToggleState('showFlaky', 'flaky tests') + getLinkHTMLToToggleState('showFlaky', 'flaky tests') +
'<form id=max-results-form ' +
'onsubmit="setState(\'maxResults\', maxResults.value);return false;"' +
'><span>Max results to show: </span>' +
'<input name=maxResults id=max-results-input></form> | ' +
'<b>All columns are sortable. | Skipped tests are not listed. | ' + '<b>All columns are sortable. | Skipped tests are not listed. | ' +
'Flakiness reader order is newer --> older runs.</b></div>' + 'Flakiness reader order is newer --> older runs.</b></div>' +
getHTMLForTestTable(tableRowsHTML); testsHTML;
setFullPageHTML(html); setFullPageHTML(html);
...@@ -1000,33 +1208,8 @@ ...@@ -1000,33 +1208,8 @@
ths[i].addEventListener('click', changeSort, false); ths[i].addEventListener('click', changeSort, false);
ths[i].className = "sortable"; ths[i].className = "sortable";
} }
}
// Permalinkable state of the page. $('max-results-input').value = currentState.maxResults;
var currentState = {};
var defaultStateValues = {
sortOrder: BACKWARD,
sortColumn: 'flakiness',
showWontFix: false,
showCorrectExpectations: false,
showFlaky: true
};
for (var builder in builders) {
defaultStateValues.builder = builder;
break;
}
function fillDefaultStateValues() {
// tests has no states with default values.
if (currentState.tests)
return;
for (var state in defaultStateValues) {
if (!(state in currentState))
currentState[state] = defaultStateValues[state];
}
} }
/** /**
...@@ -1046,88 +1229,29 @@ ...@@ -1046,88 +1229,29 @@
currentState[key] = arguments[i + 1]; currentState[key] = arguments[i + 1];
} }
window.location.replace(getPermaLinkURL());
handleLocationChange();
}
function handleLocationChange() {
$('loading-ui').style.display = 'block'; $('loading-ui').style.display = 'block';
setTimeout(function() { setTimeout(function() {
window.location.replace(getPermaLinkURL()); oldLocation = window.location.href;
generatePage(); generatePage();
$('loading-ui').style.display = 'none';
}, 0); }, 0);
} }
function parseCurrentLocation() { function getPermaLinkURL() {
oldLocation = window.location.href; return window.location.pathname + '?' + joinParameters(queryState) + '#' +
var hash = window.location.hash; joinParameters(currentState);
if (!hash) {
fillDefaultStateValues();
return;
}
var hashParts = hash.slice(1).split('&');
var urlHasTests = false;
for (var i = 0; i < hashParts.length; i++) {
var itemParts = hashParts[i].split('=');
if (itemParts.length != 2) {
console.log("Invalid parameter: " + hashParts[i]);
continue;
}
var key = itemParts[0];
var value = decodeURIComponent(itemParts[1]);
switch(key) {
case 'tests':
validateParameter(key, value,
function() { return value.match(/[A-Za-z0-9\-\_,]/); });
break;
case 'builder':
validateParameter(key, value,
function() { return value in builders; });
break;
case 'sortColumn':
validateParameter(key, value,
function() {
for (var i = 0; i < tableHeaders.length; i++) {
if (value == getSortColumnFromTableHeader(tableHeaders[i]))
return true;
}
return value == 'test';
});
break;
case 'sortOrder':
validateParameter(key, value,
function() { return value == FORWARD || value == BACKWARD; });
break;
case 'showWontFix':
case 'showCorrectExpectations':
case 'showFlaky':
currentState[key] = value == 'true';
break;
default:
console.log('Invalid key: ' + key + ' value: ' + value);
}
}
fillDefaultStateValues();
}
function validateParameter(key, value, validateFn) {
if (validateFn()) {
currentState[key] = value;
} else {
console.log(key + ' value is not valid: ' + value);
}
} }
function getPermaLinkURL() { function joinParameters(stateObject) {
var state = []; var state = [];
for (var key in currentState) { for (var key in stateObject) {
state.push(key + '=' + currentState[key]); state.push(key + '=' + encodeURIComponent(stateObject[key]));
} }
return window.location.pathname + '#' + state.join('&'); return state.join('&');
} }
function logTime(msg, startTime) { function logTime(msg, startTime) {
...@@ -1139,8 +1263,10 @@ ...@@ -1139,8 +1263,10 @@
// onload firing and the last script tag being executed. // onload firing and the last script tag being executed.
logTime('Time to load JS', pageLoadStartTime); logTime('Time to load JS', pageLoadStartTime);
setInterval(function() { setInterval(function() {
if (oldLocation != window.location) if (oldLocation != window.location.href) {
generatePage(); parseAllParameters();
handleLocationChange();
}
}, 100); }, 100);
} }
</script> </script>
......
...@@ -15,7 +15,7 @@ import simplejson ...@@ -15,7 +15,7 @@ import simplejson
class JSONResultsGenerator: class JSONResultsGenerator:
MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG = 200 MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG = 500
# Min time (seconds) that will be added to the JSON. # Min time (seconds) that will be added to the JSON.
MIN_TIME = 1 MIN_TIME = 1
JSON_PREFIX = "ADD_RESULTS(" JSON_PREFIX = "ADD_RESULTS("
...@@ -24,6 +24,12 @@ class JSONResultsGenerator: ...@@ -24,6 +24,12 @@ class JSONResultsGenerator:
LAYOUT_TESTS_PATH = "layout_tests" LAYOUT_TESTS_PATH = "layout_tests"
PASS_RESULT = "P" PASS_RESULT = "P"
NO_DATA_RESULT = "N" NO_DATA_RESULT = "N"
VERSION = 1
VERSION_KEY = "version"
RESULTS = "results"
TIMES = "times"
BUILD_NUMBERS = "buildNumbers"
TESTS = "tests"
def __init__(self, failures, individual_test_timings, builder_name, def __init__(self, failures, individual_test_timings, builder_name,
build_number, results_file_path, all_tests): build_number, results_file_path, all_tests):
...@@ -119,17 +125,19 @@ class JSONResultsGenerator: ...@@ -119,17 +125,19 @@ class JSONResultsGenerator:
# just grab it from wherever it's archived to. # just grab it from wherever it's archived to.
results_json = {} results_json = {}
self._ConvertJSONToCurrentVersion(results_json)
if self._builder_name not in results_json: if self._builder_name not in results_json:
results_json[self._builder_name] = self._CreateResultsForBuilderJSON() results_json[self._builder_name] = self._CreateResultsForBuilderJSON()
tests = results_json[self._builder_name]["tests"] tests = results_json[self._builder_name][self.TESTS]
all_failing_tests = set(self._failures.iterkeys()) all_failing_tests = set(self._failures.iterkeys())
all_failing_tests.update(tests.iterkeys()) all_failing_tests.update(tests.iterkeys())
build_numbers = results_json[self._builder_name]["buildNumbers"] build_numbers = results_json[self._builder_name][self.BUILD_NUMBERS]
build_numbers.insert(0, self._build_number) build_numbers.insert(0, self._build_number)
build_numbers = build_numbers[:self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG] build_numbers = build_numbers[:self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG]
results_json[self._builder_name]["buildNumbers"] = build_numbers results_json[self._builder_name][self.BUILD_NUMBERS] = build_numbers
num_build_numbers = len(build_numbers) num_build_numbers = len(build_numbers)
for test in all_failing_tests: for test in all_failing_tests:
...@@ -142,25 +150,66 @@ class JSONResultsGenerator: ...@@ -142,25 +150,66 @@ class JSONResultsGenerator:
tests[test] = self._CreateResultsAndTimesJSON() tests[test] = self._CreateResultsAndTimesJSON()
thisTest = tests[test] thisTest = tests[test]
thisTest["results"] = result_and_time.result + thisTest["results"] self._InsertItemRunLengthEncoded(result_and_time.result,
thisTest["times"].insert(0, result_and_time.time) thisTest[self.RESULTS])
self._InsertItemRunLengthEncoded(result_and_time.time,
thisTest[self.TIMES])
self._NormalizeResultsJSON(thisTest, test, tests, num_build_numbers) self._NormalizeResultsJSON(thisTest, test, tests, num_build_numbers)
# Specify separators in order to get compact encoding. # Specify separators in order to get compact encoding.
results_str = simplejson.dumps(results_json, separators=(',', ':')) results_str = simplejson.dumps(results_json, separators=(',', ':'))
return self.JSON_PREFIX + results_str + self.JSON_SUFFIX return self.JSON_PREFIX + results_str + self.JSON_SUFFIX
def _InsertItemRunLengthEncoded(self, item, encoded_results):
"""Inserts the item into the run-length encoded results.
Args:
item: String or number to insert.
encoded_results: run-length encoded results. An array of arrays, e.g.
[[3,'A'],[1,'Q']] encodes AAAQ.
"""
if len(encoded_results) and item == encoded_results[0][1]:
encoded_results[0][0] += 1
else:
# Use a list instead of a class for the run-length encoding since we
# want the serialized form to be concise.
encoded_results.insert(0, [1, item])
def _ConvertJSONToCurrentVersion(self, results_json):
"""If the JSON does not match the current version, converts it to the
current version and adds in the new version number.
"""
if (self.VERSION_KEY in results_json and
results_json[self.VERSION_KEY] == self.VERSION):
return
for builder in results_json:
tests = results_json[builder][self.TESTS]
for path in tests:
test = tests[path]
test[self.RESULTS] = self._RunLengthEncode(test[self.RESULTS])
test[self.TIMES] = self._RunLengthEncode(test[self.TIMES])
results_json[self.VERSION_KEY] = self.VERSION
def _RunLengthEncode(self, result_list):
"""Run-length encodes a list or string of results."""
encoded_results = [];
current_result = None;
for item in reversed(result_list):
self._InsertItemRunLengthEncoded(item, encoded_results)
return encoded_results
def _CreateResultsAndTimesJSON(self): def _CreateResultsAndTimesJSON(self):
results_and_times = {} results_and_times = {}
results_and_times["results"] = "" results_and_times[self.RESULTS] = []
results_and_times["times"] = [] results_and_times[self.TIMES] = []
return results_and_times return results_and_times
def _CreateResultsForBuilderJSON(self): def _CreateResultsForBuilderJSON(self):
results_for_builder = {} results_for_builder = {}
results_for_builder['buildNumbers'] = [] results_for_builder[self.BUILD_NUMBERS] = []
results_for_builder['tests'] = {} results_for_builder[self.TESTS] = {}
return results_for_builder return results_for_builder
def _GetResultsCharForFailure(self, test): def _GetResultsCharForFailure(self, test):
...@@ -182,6 +231,23 @@ class JSONResultsGenerator: ...@@ -182,6 +231,23 @@ class JSONResultsGenerator:
else: else:
return "O" return "O"
def _RemoveItemsOverMaxNumberOfBuilds(self, encoded_list):
"""Removes items from the run-length encoded list after the final itme that
exceeds the max number of builds to track.
Args:
encoded_results: run-length encoded results. An array of arrays, e.g.
[[3,'A'],[1,'Q']] encodes AAAQ.
"""
num_builds = 0
index = 0
for result in encoded_list:
num_builds = num_builds + result[0]
index = index + 1
if num_builds > self.MAX_NUMBER_OF_BUILD_RESULTS_TO_LOG:
return encoded_list[:index]
return encoded_list
def _NormalizeResultsJSON(self, test, test_path, tests, num_build_numbers): def _NormalizeResultsJSON(self, test, test_path, tests, num_build_numbers):
""" Prune tests where all runs pass or tests that no longer exist and """ Prune tests where all runs pass or tests that no longer exist and
truncate all results to maxNumberOfBuilds and pad results that don't truncate all results to maxNumberOfBuilds and pad results that don't
...@@ -193,32 +259,15 @@ class JSONResultsGenerator: ...@@ -193,32 +259,15 @@ class JSONResultsGenerator:
tests: The JSON object with all the test results for this builder. tests: The JSON object with all the test results for this builder.
num_build_numbers: The number to truncate/pad results to. num_build_numbers: The number to truncate/pad results to.
""" """
results = test["results"] test[self.RESULTS] = self._RemoveItemsOverMaxNumberOfBuilds(
num_results = len(results) test[self.RESULTS])
times = test["times"] test[self.TIMES] = self._RemoveItemsOverMaxNumberOfBuilds(test[self.TIMES])
if num_results != len(times):
logging.error("Test has different number of build times versus results")
times = []
results = ""
num_results = 0
# Truncate or right-pad so there are exactly maxNumberOfBuilds results.
if num_results > num_build_numbers:
results = results[:num_build_numbers]
times = times[:num_build_numbers]
elif num_results < num_build_numbers:
num_to_pad = num_build_numbers - num_results
results = results + num_to_pad * self.NO_DATA_RESULT
times.extend(num_to_pad * [0])
test["results"] = results
test["times"] = times
# Remove all passes/no-data from the results to reduce noise and filesize. # Remove all passes/no-data from the results to reduce noise and filesize.
if (results == num_build_numbers * self.NO_DATA_RESULT or if (self._IsResultsAllOfType(test[self.RESULTS], self.PASS_RESULT) or
(max(times) <= self.MIN_TIME and num_results and (self._IsResultsAllOfType(test[self.RESULTS], self.NO_DATA_RESULT) and
results == num_build_numbers * self.PASS_RESULT)): max(test[self.TIMES],
lambda x, y : cmp(x[1], y[1])) <= self.MIN_TIME)):
del tests[test_path] del tests[test_path]
# Remove tests that don't exist anymore. # Remove tests that don't exist anymore.
...@@ -227,6 +276,11 @@ class JSONResultsGenerator: ...@@ -227,6 +276,11 @@ class JSONResultsGenerator:
if not os.path.exists(full_path): if not os.path.exists(full_path):
del tests[test_path] del tests[test_path]
def _IsResultsAllOfType(self, results, type):
"""Returns whether all teh results are of the given type (e.g. all passes).
"""
return len(results) == 1 and results[0][1] == type
class ResultAndTime: class ResultAndTime:
"""A holder for a single result and runtime for a test.""" """A holder for a single result and runtime for a test."""
def __init__(self, test, all_tests): def __init__(self, test, all_tests):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment