summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorojan@google.com <ojan@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2009-09-09 23:51:24 +0000
committerojan@google.com <ojan@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2009-09-09 23:51:24 +0000
commit8c11e836d762ad2149f3956df6f999392b0eb9a4 (patch)
treed8de6f25c4c585b83e5822a2e0448debeaa4e2a6
parent5c9e97acabd4cdab5adb20d2412a5766b3382856 (diff)
downloadchromium_src-8c11e836d762ad2149f3956df6f999392b0eb9a4.zip
chromium_src-8c11e836d762ad2149f3956df6f999392b0eb9a4.tar.gz
chromium_src-8c11e836d762ad2149f3956df6f999392b0eb9a4.tar.bz2
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
-rw-r--r--webkit/tools/layout_tests/flakiness_dashboard.html610
-rw-r--r--webkit/tools/layout_tests/layout_package/json_results_generator.py124
2 files changed, 457 insertions, 277 deletions
diff --git a/webkit/tools/layout_tests/flakiness_dashboard.html b/webkit/tools/layout_tests/flakiness_dashboard.html
index d4a23d5..8af2540 100644
--- a/webkit/tools/layout_tests/flakiness_dashboard.html
+++ b/webkit/tools/layout_tests/flakiness_dashboard.html
@@ -15,17 +15,22 @@
font-size: 16px;
margin-bottom: .25em;
}
- form {
- margin: 3px 0;
+ #max-results-form {
+ display: inline;
+ }
+ #max-results-input {
+ width: 30px;
+ }
+ #tests-form {
display: -webkit-box;
}
- form > * {
+ #tests-form > * {
display: -webkit-box;
}
- form > div {
+ #tests-form > div {
-webkit-box-flex: 0;
}
- form > input {
+ #tests-input {
-webkit-box-flex: 1;
}
.test-link {
@@ -172,60 +177,278 @@
* -add the builder name to the list of builders below.
*/
- // Default to layout_tests.
- var testType = 'layout_test_results';
- var params = window.location.search.substring(1).split('&');
- for (var i = 0; i < params.length; i++) {
- var thisParam = params[i].split('=');
- if (thisParam[0] == 'testtype') {
- testType = thisParam[1];
- break;
+ // 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 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++) {
+ var thisParam = params[i].split('=');
+ if (thisParam.length != 2) {
+ console.log('Invalid query parameter: ' + params[i]);
+ 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;
}
}
- // Map of builderName (the name shown in the waterfall)
- // to builderPath (the path used in the builder's URL)
- // TODO(ojan): Make this switch based off of the testType.
- var builders = {
- 'Webkit': 'webkit-rel',
- 'Webkit (dbg)(1)': 'webkit-dbg-1',
- 'Webkit (dbg)(2)': 'webkit-dbg-2',
- 'Webkit (dbg)(3)': 'webkit-dbg-3',
- 'Webkit Linux': 'webkit-rel-linux',
- 'Webkit Linux (dbg)(1)': 'webkit-dbg-linux-1',
- 'Webkit Linux (dbg)(2)': 'webkit-dbg-linux-2',
- 'Webkit Linux (dbg)(3)': 'webkit-dbg-linux-3',
- 'Webkit Mac10.5': 'webkit-rel-mac5',
- 'Webkit Mac10.5 (dbg)(1)': 'webkit-dbg-mac5-1',
- 'Webkit Mac10.5 (dbg)(2)': 'webkit-dbg-mac5-2',
- 'Webkit Mac10.5 (dbg)(3)': 'webkit-dbg-mac5-3'
+ 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)
+ // to builderPath (the path used in the builder's URL)
+ // TODO(ojan): Make this switch based off of the testType.
+ var builders = {
+ 'Webkit': 'webkit-rel',
+ 'Webkit (dbg)(1)': 'webkit-dbg-1',
+ 'Webkit (dbg)(2)': 'webkit-dbg-2',
+ 'Webkit (dbg)(3)': 'webkit-dbg-3',
+ 'Webkit Linux': 'webkit-rel-linux',
+ 'Webkit Linux (dbg)(1)': 'webkit-dbg-linux-1',
+ 'Webkit Linux (dbg)(2)': 'webkit-dbg-linux-2',
+ 'Webkit Linux (dbg)(3)': 'webkit-dbg-linux-3',
+ 'Webkit Mac10.5': 'webkit-rel-mac5',
+ 'Webkit Mac10.5 (dbg)(1)': 'webkit-dbg-mac5-1',
+ 'Webkit Mac10.5 (dbg)(2)': 'webkit-dbg-mac5-2',
+ '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 = {};
// Maps test path to an array of {builder, testResults} objects.
var testToResultsMap = {};
var expectationsByTest = {};
function ADD_RESULTS(builds) {
for (var builderName in builds) {
- resultsByBuilder[builderName] = builds[builderName];
+ if (builderName != 'version')
+ resultsByBuilder[builderName] = builds[builderName];
}
generatePage();
}
- var BUILDER_BASE = 'http://build.chromium.org/buildbot/';
+
function getPathToBuilderResultsFile(builderName) {
- return BUILDER_BASE + testType + '/' + builders[builderName] + '/';
+ return builderBase + queryState['testType'] + '/' +
+ builders[builderName] + '/';
}
+
for (var builderName in builders) {
- var script = document.createElement('script');
- script.src = getPathToBuilderResultsFile(builderName) + 'results.json';
- document.getElementsByTagName('head')[0].appendChild(script);
+ appendScript(getPathToBuilderResultsFile(builderName) + 'results.json');
}
- var script = document.createElement('script');
// Grab expectations file from any builder.
- script.src = getPathToBuilderResultsFile(builderName) + 'expectations.json';
- document.getElementsByTagName('head')[0].appendChild(script);
+ appendScript(getPathToBuilderResultsFile(builderName) + 'expectations.json');
var expectationsLoaded = false;
function ADD_EXPECTATIONS(expectations) {
@@ -234,38 +457,6 @@
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) {
return {
@@ -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
* build type of the given builder.
@@ -585,17 +756,16 @@
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;
- var results = rawResults.split('');
+ resultsForTest.flips = rawResults.length - 1;
var unexpectedExpectations = [];
var resultsMap = {}
- for (var i = 0; i < results.length - 1; i++) {
- if (results[i] != results[i + 1])
- resultsForTest.flips++;
-
- var expectation = getExpectationsFileStringForResult(results[i]);
+ for (var i = 0; i < rawResults.length; i++) {
+ var expectation = getExpectationsFileStringForResult(rawResults[i][1]);
resultsMap[expectation] = true;
}
@@ -618,20 +788,25 @@
missingExpectations.push(result);
}
- // TODO(ojan): Make this detect the case of a test that has NODATA,
- // then fails for a few runs, then passes for the rest. We should
- // consider that as meetsExpectations since every new test will have
- // that pattern.
+ var times = resultsByBuilder[builderName].tests[test].times;
+ for (var i = 0; i < times.length; i++) {
+ resultsForTest.slowestTime = Math.max(resultsForTest.slowestTime,
+ times[i][1]);
+ }
+
+ if (resultsForTest.slowestTime &&
+ (!resultsForTest.expectations ||
+ !stringContains(resultsForTest.expectations, 'TIMEOUT')) &&
+ (!resultsForTest.modifiers ||
+ !stringContains(resultsForTest.modifiers, 'SLOW'))) {
+ missingExpectations.push('SLOW');
+ }
+
resultsForTest.meetsExpectations =
!missingExpectations.length && !extraExpectations.length;
resultsForTest.missing = missingExpectations.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);
if (!testToResultsMap[test])
@@ -672,29 +847,49 @@
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) {
window.open(BUILDERS_BASE_PATH + builderName + '/builds/' + buildNumber);
}
- function getHtmlForTestResults(builderName, testPath) {
+ function getHtmlForTestResults(test, builder) {
var html = '';
- var test = resultsByBuilder[builderName].tests[testPath];
- var results = test.results.split('');
- var times = test.times;
- var buildNumbers = resultsByBuilder[builderName].buildNumbers;
- for (var i = 0; i < results.length; i++) {
+ var results = test.rawResults.concat();
+ var times = test.rawTimes.concat();
+ var buildNumbers = resultsByBuilder[builder].buildNumbers;
+
+ 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 innerHTML = times[i] > 0 ? times[i] : '&nbsp;';
html += '<td title="Build:' + buildNumber + '" class="results ' +
- results[i] + '" onclick=\'loadBuilderPageForBuildNumber("' +
- builderName + '","' + buildNumber + '")\'>' + innerHTML + '</td>';
+ currentResult + '" onclick=\'loadBuilderPageForBuildNumber("' +
+ builder + '","' + buildNumber + '")\'>' + innerHTML + '</td>';
}
return html;
}
@@ -717,7 +912,7 @@
* avoid common non-flaky cases.
*/
function isTestFlaky(testResult) {
- return testResult.flips > 1 && !isFixedNewTest(testResult);
+ return testResult.flips > 1 && !isFixedTest(testResult);
}
/**
@@ -727,12 +922,21 @@
* Where that middle part can be a series of F's, S's or I's for the
* different types of failures.
*/
- function isFixedNewTest(testResult) {
- if (testResult.isFixedNewTest === undefined) {
- testResult.isFixedNewTest =
- testResult.rawResults.match(/^P+(S+|F+|I+)N+$/);
+ function isFixedTest(testResult) {
+ if (testResult.isFixedTest === undefined) {
+ var results = testResult.rawResults;
+ var isFixedTest = results[0][1] == 'P';
+ if (isFixedTest && results.length > 1) {
+ var secondResult = results[1][1];
+ isFixedTest = secondResult == 'S' || secondResult == 'F' ||
+ secondResult == 'I';
+ }
+ if (isFixedTest && results.length > 2) {
+ isFixedTest = results.length == 3 && results[2][1] == 'N';
+ }
+ testResult.isFixedTest = isFixedTest;
}
- return testResult.isFixedNewTest;
+ return testResult.isFixedTest;
}
/**
@@ -748,7 +952,7 @@
if (testResult.isWontFix && !currentState.showWontFix)
return true;
- if ((testResult.meetsExpectations || isFixedNewTest(testResult)) &&
+ if ((testResult.meetsExpectations || isFixedTest(testResult)) &&
!currentState.showCorrectExpectations) {
// Only hide flaky tests that match their expectations if showFlaky
// is false.
@@ -758,17 +962,17 @@
return !currentState.showFlaky && isTestFlaky(testResult);
}
- function getHTMLForSingleTestRow(test, opt_builder) {
+ function getHTMLForSingleTestRow(test, builder, opt_isCrossBuilderView) {
if (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.
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
// instead of test paths.
- var testCellHTML = opt_builder ? opt_builder :
+ var testCellHTML = opt_isCrossBuilderView ? builder :
'<span class="link" onclick="setState(\'tests\', \'' + test.test +
'\');return false;">' + test.test + '</span>';
@@ -783,9 +987,7 @@
'</td><td>' + test.missing +
'</td><td>' + test.extra +
'</td><td>' + (test.slowestTime ? test.slowestTime + 's' : '') +
- '</td>' +
- test.html +
- '</tr>';
+ '</td>' + getHtmlForTestResults(test, builder) + '</tr>';
}
function getSortColumnFromTableHeader(headerText) {
@@ -890,8 +1092,6 @@
}
function generatePage() {
- parseCurrentLocation();
-
// Only continue if all the JSON files have loaded.
if (!expectationsLoaded)
return;
@@ -930,7 +1130,7 @@
var tableRowsHTML = '';
for (var j = 0; j < testResults.length; j++) {
tableRowsHTML += getHTMLForSingleTestRow(testResults[j].results,
- testResults[j].builder);
+ testResults[j].builder, true);
}
html += getHTMLForTestTable(tableRowsHTML);
} else {
@@ -952,7 +1152,8 @@
builder + '</span>';
}
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 ' +
'placeholder="LayoutTests/foo/bar.html,LayoutTests/foo/baz.html" ' +
'id=tests-input></form><div id="loading-ui">LOADING...</div>' +
@@ -968,8 +1169,8 @@
function getLinkHTMLToToggleState(key, linkText) {
var isTrue = currentState[key];
- return '<span class=link onclick="setState(\'' + key + '\', \'' + !isTrue +
- '\')">' + (isTrue ? 'Hide' : 'Show') + ' ' + linkText + '</span> | ';
+ return '<span class=link onclick="setState(\'' + key + '\', ' + !isTrue +
+ ')">' + (isTrue ? 'Hide' : 'Show') + ' ' + linkText + '</span> | ';
}
function generatePageForBuilder(builderName) {
@@ -979,9 +1180,12 @@
var results = perBuilderFailures[builderName];
sortTests(results, currentState.sortColumn, currentState.sortOrder);
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) +
getHTMLForTestsWithExpectationsButNoFailures(builderName) +
'<h2>Failing tests</h2><div>' +
@@ -989,9 +1193,13 @@
getLinkHTMLToToggleState('showCorrectExpectations',
'tests with correct expectations') +
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. | ' +
'Flakiness reader order is newer --> older runs.</b></div>' +
- getHTMLForTestTable(tableRowsHTML);
+ testsHTML;
setFullPageHTML(html);
@@ -1000,33 +1208,8 @@
ths[i].addEventListener('click', changeSort, false);
ths[i].className = "sortable";
}
- }
- // Permalinkable state of the page.
- 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];
- }
+ $('max-results-input').value = currentState.maxResults;
}
/**
@@ -1046,88 +1229,29 @@
currentState[key] = arguments[i + 1];
}
+ window.location.replace(getPermaLinkURL());
+ handleLocationChange();
+ }
+ function handleLocationChange() {
$('loading-ui').style.display = 'block';
setTimeout(function() {
- window.location.replace(getPermaLinkURL());
+ oldLocation = window.location.href;
generatePage();
- $('loading-ui').style.display = 'none';
}, 0);
}
- function parseCurrentLocation() {
- oldLocation = window.location.href;
- var hash = window.location.hash;
- 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() {
+ return window.location.pathname + '?' + joinParameters(queryState) + '#' +
+ joinParameters(currentState);
}
- function getPermaLinkURL() {
+ function joinParameters(stateObject) {
var state = [];
- for (var key in currentState) {
- state.push(key + '=' + currentState[key]);
+ for (var key in stateObject) {
+ state.push(key + '=' + encodeURIComponent(stateObject[key]));
}
- return window.location.pathname + '#' + state.join('&');
+ return state.join('&');
}
function logTime(msg, startTime) {
@@ -1139,8 +1263,10 @@
// onload firing and the last script tag being executed.
logTime('Time to load JS', pageLoadStartTime);
setInterval(function() {
- if (oldLocation != window.location)
- generatePage();
+ if (oldLocation != window.location.href) {
+ parseAllParameters();
+ handleLocationChange();
+ }
}, 100);
}
</script>
diff --git a/webkit/tools/layout_tests/layout_package/json_results_generator.py b/webkit/tools/layout_tests/layout_package/json_results_generator.py
index 2e24196..38a9825 100644
--- a/webkit/tools/layout_tests/layout_package/json_results_generator.py
+++ b/webkit/tools/layout_tests/layout_package/json_results_generator.py
@@ -15,7 +15,7 @@ import simplejson
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 = 1
JSON_PREFIX = "ADD_RESULTS("
@@ -24,6 +24,12 @@ class JSONResultsGenerator:
LAYOUT_TESTS_PATH = "layout_tests"
PASS_RESULT = "P"
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,
build_number, results_file_path, all_tests):
@@ -119,17 +125,19 @@ class JSONResultsGenerator:
# just grab it from wherever it's archived to.
results_json = {}
+ self._ConvertJSONToCurrentVersion(results_json)
+
if self._builder_name not in results_json:
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.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 = 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)
for test in all_failing_tests:
@@ -142,25 +150,66 @@ class JSONResultsGenerator:
tests[test] = self._CreateResultsAndTimesJSON()
thisTest = tests[test]
- thisTest["results"] = result_and_time.result + thisTest["results"]
- thisTest["times"].insert(0, result_and_time.time)
-
+ self._InsertItemRunLengthEncoded(result_and_time.result,
+ thisTest[self.RESULTS])
+ self._InsertItemRunLengthEncoded(result_and_time.time,
+ thisTest[self.TIMES])
self._NormalizeResultsJSON(thisTest, test, tests, num_build_numbers)
# Specify separators in order to get compact encoding.
results_str = simplejson.dumps(results_json, separators=(',', ':'))
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):
results_and_times = {}
- results_and_times["results"] = ""
- results_and_times["times"] = []
+ results_and_times[self.RESULTS] = []
+ results_and_times[self.TIMES] = []
return results_and_times
def _CreateResultsForBuilderJSON(self):
results_for_builder = {}
- results_for_builder['buildNumbers'] = []
- results_for_builder['tests'] = {}
+ results_for_builder[self.BUILD_NUMBERS] = []
+ results_for_builder[self.TESTS] = {}
return results_for_builder
def _GetResultsCharForFailure(self, test):
@@ -182,6 +231,23 @@ class JSONResultsGenerator:
else:
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):
""" Prune tests where all runs pass or tests that no longer exist and
truncate all results to maxNumberOfBuilds and pad results that don't
@@ -193,32 +259,15 @@ class JSONResultsGenerator:
tests: The JSON object with all the test results for this builder.
num_build_numbers: The number to truncate/pad results to.
"""
- results = test["results"]
- num_results = len(results)
- times = test["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
+ test[self.RESULTS] = self._RemoveItemsOverMaxNumberOfBuilds(
+ test[self.RESULTS])
+ test[self.TIMES] = self._RemoveItemsOverMaxNumberOfBuilds(test[self.TIMES])
# Remove all passes/no-data from the results to reduce noise and filesize.
- if (results == num_build_numbers * self.NO_DATA_RESULT or
- (max(times) <= self.MIN_TIME and num_results and
- results == num_build_numbers * self.PASS_RESULT)):
+ if (self._IsResultsAllOfType(test[self.RESULTS], self.PASS_RESULT) or
+ (self._IsResultsAllOfType(test[self.RESULTS], self.NO_DATA_RESULT) and
+ max(test[self.TIMES],
+ lambda x, y : cmp(x[1], y[1])) <= self.MIN_TIME)):
del tests[test_path]
# Remove tests that don't exist anymore.
@@ -227,6 +276,11 @@ class JSONResultsGenerator:
if not os.path.exists(full_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:
"""A holder for a single result and runtime for a test."""
def __init__(self, test, all_tests):