076f078a5e
NOTRY=true BUG=skia:1917 R=epoger@google.com Author: rmistry@google.com Review URL: https://codereview.chromium.org/149473005 git-svn-id: http://skia.googlecode.com/svn/trunk@13315 2bbb7eff-a529-9590-31e7-b0007b416f81
624 lines
21 KiB
JavaScript
624 lines
21 KiB
JavaScript
/*
|
|
* Loader:
|
|
* Reads GM result reports written out by results.py, and imports
|
|
* them into $scope.categories and $scope.testData .
|
|
*/
|
|
var Loader = angular.module(
|
|
'Loader',
|
|
['diff_viewer']
|
|
);
|
|
|
|
|
|
// TODO(epoger): Combine ALL of our filtering operations (including
|
|
// truncation) into this one filter, so that runs most efficiently?
|
|
// (We would have to make sure truncation still took place after
|
|
// sorting, though.)
|
|
Loader.filter(
|
|
'removeHiddenItems',
|
|
function() {
|
|
return function(unfilteredItems, hiddenResultTypes, hiddenConfigs,
|
|
builderSubstring, testSubstring, viewingTab) {
|
|
var filteredItems = [];
|
|
for (var i = 0; i < unfilteredItems.length; i++) {
|
|
var item = unfilteredItems[i];
|
|
// For performance, we examine the "set" objects directly rather
|
|
// than calling $scope.isValueInSet().
|
|
// Besides, I don't think we have access to $scope in here...
|
|
if (!(true == hiddenResultTypes[item.resultType]) &&
|
|
!(true == hiddenConfigs[item.config]) &&
|
|
!(-1 == item.builder.indexOf(builderSubstring)) &&
|
|
!(-1 == item.test.indexOf(testSubstring)) &&
|
|
(viewingTab == item.tab)) {
|
|
filteredItems.push(item);
|
|
}
|
|
}
|
|
return filteredItems;
|
|
};
|
|
}
|
|
);
|
|
|
|
|
|
Loader.controller(
|
|
'Loader.Controller',
|
|
function($scope, $http, $filter, $location, $timeout) {
|
|
$scope.windowTitle = "Loading GM Results...";
|
|
$scope.resultsToLoad = $location.search().resultsToLoad;
|
|
$scope.loadingMessage = "Loading results of type '" + $scope.resultsToLoad +
|
|
"', please wait...";
|
|
|
|
/**
|
|
* On initial page load, load a full dictionary of results.
|
|
* Once the dictionary is loaded, unhide the page elements so they can
|
|
* render the data.
|
|
*/
|
|
$http.get("/results/" + $scope.resultsToLoad).success(
|
|
function(data, status, header, config) {
|
|
if (data.header.resultsStillLoading) {
|
|
$scope.loadingMessage =
|
|
"Server is still loading results; will retry at " +
|
|
$scope.localTimeString(data.header.timeNextUpdateAvailable);
|
|
$timeout(
|
|
function(){location.reload();},
|
|
(data.header.timeNextUpdateAvailable * 1000) - new Date().getTime());
|
|
} else {
|
|
$scope.loadingMessage = "Processing data, please wait...";
|
|
|
|
$scope.header = data.header;
|
|
$scope.categories = data.categories;
|
|
$scope.testData = data.testData;
|
|
$scope.sortColumn = 'weightedDiffMeasure';
|
|
$scope.showTodos = false;
|
|
|
|
$scope.showSubmitAdvancedSettings = false;
|
|
$scope.submitAdvancedSettings = {};
|
|
$scope.submitAdvancedSettings['reviewed-by-human'] = true;
|
|
$scope.submitAdvancedSettings['ignore-failure'] = false;
|
|
$scope.submitAdvancedSettings['bug'] = '';
|
|
|
|
// Create the list of tabs (lists into which the user can file each
|
|
// test). This may vary, depending on isEditable.
|
|
$scope.tabs = [
|
|
'Unfiled', 'Hidden'
|
|
];
|
|
if (data.header.isEditable) {
|
|
$scope.tabs = $scope.tabs.concat(
|
|
['Pending Approval']);
|
|
}
|
|
$scope.defaultTab = $scope.tabs[0];
|
|
$scope.viewingTab = $scope.defaultTab;
|
|
|
|
// Track the number of results on each tab.
|
|
$scope.numResultsPerTab = {};
|
|
for (var i = 0; i < $scope.tabs.length; i++) {
|
|
$scope.numResultsPerTab[$scope.tabs[i]] = 0;
|
|
}
|
|
$scope.numResultsPerTab[$scope.defaultTab] = $scope.testData.length;
|
|
|
|
// Add index and tab fields to all records.
|
|
for (var i = 0; i < $scope.testData.length; i++) {
|
|
$scope.testData[i].index = i;
|
|
$scope.testData[i].tab = $scope.defaultTab;
|
|
}
|
|
|
|
// Arrays within which the user can toggle individual elements.
|
|
$scope.selectedItems = [];
|
|
|
|
// Sets within which the user can toggle individual elements.
|
|
$scope.hiddenResultTypes = {
|
|
'failure-ignored': true,
|
|
'no-comparison': true,
|
|
'succeeded': true,
|
|
};
|
|
$scope.allResultTypes = Object.keys(data.categories['resultType']);
|
|
$scope.hiddenConfigs = {};
|
|
$scope.allConfigs = Object.keys(data.categories['config']);
|
|
|
|
// Associative array of partial string matches per category.
|
|
$scope.categoryValueMatch = {};
|
|
$scope.categoryValueMatch.builder = "";
|
|
$scope.categoryValueMatch.test = "";
|
|
|
|
// If any defaults were overridden in the URL, get them now.
|
|
$scope.queryParameters.load();
|
|
|
|
$scope.updateResults();
|
|
$scope.loadingMessage = "";
|
|
$scope.windowTitle = "Current GM Results";
|
|
}
|
|
}
|
|
).error(
|
|
function(data, status, header, config) {
|
|
$scope.loadingMessage = "Failed to load results of type '"
|
|
+ $scope.resultsToLoad + "'";
|
|
$scope.windowTitle = "Failed to Load GM Results";
|
|
}
|
|
);
|
|
|
|
|
|
//
|
|
// Select/Clear/Toggle all tests.
|
|
//
|
|
|
|
/**
|
|
* Select all currently showing tests.
|
|
*/
|
|
$scope.selectAllItems = function() {
|
|
var numItemsShowing = $scope.limitedTestData.length;
|
|
for (var i = 0; i < numItemsShowing; i++) {
|
|
var index = $scope.limitedTestData[i].index;
|
|
if (!$scope.isValueInArray(index, $scope.selectedItems)) {
|
|
$scope.toggleValueInArray(index, $scope.selectedItems);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deselect all currently showing tests.
|
|
*/
|
|
$scope.clearAllItems = function() {
|
|
var numItemsShowing = $scope.limitedTestData.length;
|
|
for (var i = 0; i < numItemsShowing; i++) {
|
|
var index = $scope.limitedTestData[i].index;
|
|
if ($scope.isValueInArray(index, $scope.selectedItems)) {
|
|
$scope.toggleValueInArray(index, $scope.selectedItems);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle selection of all currently showing tests.
|
|
*/
|
|
$scope.toggleAllItems = function() {
|
|
var numItemsShowing = $scope.limitedTestData.length;
|
|
for (var i = 0; i < numItemsShowing; i++) {
|
|
var index = $scope.limitedTestData[i].index;
|
|
$scope.toggleValueInArray(index, $scope.selectedItems);
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// Tab operations.
|
|
//
|
|
|
|
/**
|
|
* Change the selected tab.
|
|
*
|
|
* @param tab (string): name of the tab to select
|
|
*/
|
|
$scope.setViewingTab = function(tab) {
|
|
$scope.viewingTab = tab;
|
|
$scope.updateResults();
|
|
}
|
|
|
|
/**
|
|
* Move the items in $scope.selectedItems to a different tab,
|
|
* and then clear $scope.selectedItems.
|
|
*
|
|
* @param newTab (string): name of the tab to move the tests to
|
|
*/
|
|
$scope.moveSelectedItemsToTab = function(newTab) {
|
|
$scope.moveItemsToTab($scope.selectedItems, newTab);
|
|
$scope.selectedItems = [];
|
|
$scope.updateResults();
|
|
}
|
|
|
|
/**
|
|
* Move a subset of $scope.testData to a different tab.
|
|
*
|
|
* @param itemIndices (array of ints): indices into $scope.testData
|
|
* indicating which test results to move
|
|
* @param newTab (string): name of the tab to move the tests to
|
|
*/
|
|
$scope.moveItemsToTab = function(itemIndices, newTab) {
|
|
var itemIndex;
|
|
var numItems = itemIndices.length;
|
|
for (var i = 0; i < numItems; i++) {
|
|
itemIndex = itemIndices[i];
|
|
$scope.numResultsPerTab[$scope.testData[itemIndex].tab]--;
|
|
$scope.testData[itemIndex].tab = newTab;
|
|
}
|
|
$scope.numResultsPerTab[newTab] += numItems;
|
|
}
|
|
|
|
|
|
//
|
|
// $scope.queryParameters:
|
|
// Transfer parameter values between $scope and the URL query string.
|
|
//
|
|
$scope.queryParameters = {};
|
|
|
|
// load and save functions for parameters of each type
|
|
// (load a parameter value into $scope from nameValuePairs,
|
|
// save a parameter value from $scope into nameValuePairs)
|
|
$scope.queryParameters.copiers = {
|
|
'simple': {
|
|
'load': function(nameValuePairs, name) {
|
|
var value = nameValuePairs[name];
|
|
if (value) {
|
|
$scope[name] = value;
|
|
}
|
|
},
|
|
'save': function(nameValuePairs, name) {
|
|
nameValuePairs[name] = $scope[name];
|
|
}
|
|
},
|
|
|
|
'categoryValueMatch': {
|
|
'load': function(nameValuePairs, name) {
|
|
var value = nameValuePairs[name];
|
|
if (value) {
|
|
$scope.categoryValueMatch[name] = value;
|
|
}
|
|
},
|
|
'save': function(nameValuePairs, name) {
|
|
nameValuePairs[name] = $scope.categoryValueMatch[name];
|
|
}
|
|
},
|
|
|
|
'set': {
|
|
'load': function(nameValuePairs, name) {
|
|
var value = nameValuePairs[name];
|
|
if (value) {
|
|
var valueArray = value.split(',');
|
|
$scope[name] = {};
|
|
$scope.toggleValuesInSet(valueArray, $scope[name]);
|
|
}
|
|
},
|
|
'save': function(nameValuePairs, name) {
|
|
nameValuePairs[name] = Object.keys($scope[name]).join(',');
|
|
}
|
|
},
|
|
|
|
};
|
|
|
|
// parameter name -> copier objects to load/save parameter value
|
|
$scope.queryParameters.map = {
|
|
'resultsToLoad': $scope.queryParameters.copiers.simple,
|
|
'displayLimitPending': $scope.queryParameters.copiers.simple,
|
|
'imageSizePending': $scope.queryParameters.copiers.simple,
|
|
'sortColumn': $scope.queryParameters.copiers.simple,
|
|
|
|
'builder': $scope.queryParameters.copiers.categoryValueMatch,
|
|
'test': $scope.queryParameters.copiers.categoryValueMatch,
|
|
|
|
'hiddenResultTypes': $scope.queryParameters.copiers.set,
|
|
'hiddenConfigs': $scope.queryParameters.copiers.set,
|
|
};
|
|
|
|
// Loads all parameters into $scope from the URL query string;
|
|
// any which are not found within the URL will keep their current value.
|
|
$scope.queryParameters.load = function() {
|
|
var nameValuePairs = $location.search();
|
|
angular.forEach($scope.queryParameters.map,
|
|
function(copier, paramName) {
|
|
copier.load(nameValuePairs, paramName);
|
|
}
|
|
);
|
|
};
|
|
|
|
// Saves all parameters from $scope into the URL query string.
|
|
$scope.queryParameters.save = function() {
|
|
var nameValuePairs = {};
|
|
angular.forEach($scope.queryParameters.map,
|
|
function(copier, paramName) {
|
|
copier.save(nameValuePairs, paramName);
|
|
}
|
|
);
|
|
$location.search(nameValuePairs);
|
|
};
|
|
|
|
|
|
//
|
|
// updateResults() and friends.
|
|
//
|
|
|
|
/**
|
|
* Set $scope.areUpdatesPending (to enable/disable the Update Results
|
|
* button).
|
|
*
|
|
* TODO(epoger): We could reduce the amount of code by just setting the
|
|
* variable directly (from, e.g., a button's ng-click handler). But when
|
|
* I tried that, the HTML elements depending on the variable did not get
|
|
* updated.
|
|
* It turns out that this is due to variable scoping within an ng-repeat
|
|
* element; see http://stackoverflow.com/questions/15388344/behavior-of-assignment-expression-invoked-by-ng-click-within-ng-repeat
|
|
*
|
|
* @param val boolean value to set $scope.areUpdatesPending to
|
|
*/
|
|
$scope.setUpdatesPending = function(val) {
|
|
$scope.areUpdatesPending = val;
|
|
}
|
|
|
|
/**
|
|
* Update the displayed results, based on filters/settings,
|
|
* and call $scope.queryParameters.save() so that the new filter results
|
|
* can be bookmarked.
|
|
*/
|
|
$scope.updateResults = function() {
|
|
$scope.displayLimit = $scope.displayLimitPending;
|
|
// TODO(epoger): Every time we apply a filter, AngularJS creates
|
|
// another copy of the array. Is there a way we can filter out
|
|
// the items as they are displayed, rather than storing multiple
|
|
// array copies? (For better performance.)
|
|
|
|
if ($scope.viewingTab == $scope.defaultTab) {
|
|
|
|
// TODO(epoger): Until we allow the user to reverse sort order,
|
|
// there are certain columns we want to sort in a different order.
|
|
var doReverse = (
|
|
($scope.sortColumn == 'percentDifferingPixels') ||
|
|
($scope.sortColumn == 'weightedDiffMeasure'));
|
|
|
|
$scope.filteredTestData =
|
|
$filter("orderBy")(
|
|
$filter("removeHiddenItems")(
|
|
$scope.testData,
|
|
$scope.hiddenResultTypes,
|
|
$scope.hiddenConfigs,
|
|
$scope.categoryValueMatch.builder,
|
|
$scope.categoryValueMatch.test,
|
|
$scope.viewingTab
|
|
),
|
|
$scope.sortColumn, doReverse);
|
|
$scope.limitedTestData = $filter("limitTo")(
|
|
$scope.filteredTestData, $scope.displayLimit);
|
|
} else {
|
|
$scope.filteredTestData =
|
|
$filter("orderBy")(
|
|
$filter("filter")(
|
|
$scope.testData,
|
|
{tab: $scope.viewingTab},
|
|
true
|
|
),
|
|
$scope.sortColumn);
|
|
$scope.limitedTestData = $scope.filteredTestData;
|
|
}
|
|
$scope.imageSize = $scope.imageSizePending;
|
|
$scope.setUpdatesPending(false);
|
|
$scope.queryParameters.save();
|
|
}
|
|
|
|
/**
|
|
* Re-sort the displayed results.
|
|
*
|
|
* @param sortColumn (string): name of the column to sort on
|
|
*/
|
|
$scope.sortResultsBy = function(sortColumn) {
|
|
$scope.sortColumn = sortColumn;
|
|
$scope.updateResults();
|
|
}
|
|
|
|
/**
|
|
* Set $scope.categoryValueMatch[name] = value, and update results.
|
|
*
|
|
* @param name
|
|
* @param value
|
|
*/
|
|
$scope.setCategoryValueMatch = function(name, value) {
|
|
$scope.categoryValueMatch[name] = value;
|
|
$scope.updateResults();
|
|
}
|
|
|
|
/**
|
|
* Update $scope.hiddenResultTypes so that ONLY this resultType is showing,
|
|
* and update the visible results.
|
|
*
|
|
* @param resultType
|
|
*/
|
|
$scope.showOnlyResultType = function(resultType) {
|
|
$scope.hiddenResultTypes = {};
|
|
// TODO(epoger): Maybe change $scope.allResultTypes to be a Set like
|
|
// $scope.hiddenResultTypes (rather than an array), so this operation is
|
|
// simpler (just assign or add allResultTypes to hiddenResultTypes).
|
|
$scope.toggleValuesInSet($scope.allResultTypes, $scope.hiddenResultTypes);
|
|
$scope.toggleValueInSet(resultType, $scope.hiddenResultTypes);
|
|
$scope.updateResults();
|
|
}
|
|
|
|
/**
|
|
* Update $scope.hiddenConfigs so that ONLY this config is showing,
|
|
* and update the visible results.
|
|
*
|
|
* @param config
|
|
*/
|
|
$scope.showOnlyConfig = function(config) {
|
|
$scope.hiddenConfigs = {};
|
|
$scope.toggleValuesInSet($scope.allConfigs, $scope.hiddenConfigs);
|
|
$scope.toggleValueInSet(config, $scope.hiddenConfigs);
|
|
$scope.updateResults();
|
|
}
|
|
|
|
|
|
//
|
|
// Operations for sending info back to the server.
|
|
//
|
|
|
|
/**
|
|
* Tell the server that the actual results of these particular tests
|
|
* are acceptable.
|
|
*
|
|
* @param testDataSubset an array of test results, most likely a subset of
|
|
* $scope.testData (perhaps with some modifications)
|
|
*/
|
|
$scope.submitApprovals = function(testDataSubset) {
|
|
$scope.submitPending = true;
|
|
|
|
// Convert bug text field to null or 1-item array.
|
|
var bugs = null;
|
|
var bugNumber = parseInt($scope.submitAdvancedSettings['bug']);
|
|
if (!isNaN(bugNumber)) {
|
|
bugs = [bugNumber];
|
|
}
|
|
|
|
// TODO(epoger): This is a suboptimal way to prevent users from
|
|
// rebaselining failures in alternative renderModes, but it does work.
|
|
// For a better solution, see
|
|
// https://code.google.com/p/skia/issues/detail?id=1748 ('gm: add new
|
|
// result type, RenderModeMismatch')
|
|
var encounteredComparisonConfig = false;
|
|
|
|
var newResults = [];
|
|
for (var i = 0; i < testDataSubset.length; i++) {
|
|
var actualResult = testDataSubset[i];
|
|
var expectedResult = {
|
|
builder: actualResult['builder'],
|
|
test: actualResult['test'],
|
|
config: actualResult['config'],
|
|
expectedHashType: actualResult['actualHashType'],
|
|
expectedHashDigest: actualResult['actualHashDigest'],
|
|
};
|
|
if (0 == expectedResult.config.indexOf('comparison-')) {
|
|
encounteredComparisonConfig = true;
|
|
}
|
|
|
|
// Advanced settings...
|
|
expectedResult['reviewed-by-human'] =
|
|
$scope.submitAdvancedSettings['reviewed-by-human'];
|
|
if (true == $scope.submitAdvancedSettings['ignore-failure']) {
|
|
// if it's false, don't send it at all (just keep the default)
|
|
expectedResult['ignore-failure'] = true;
|
|
}
|
|
expectedResult['bugs'] = bugs;
|
|
|
|
newResults.push(expectedResult);
|
|
}
|
|
if (encounteredComparisonConfig) {
|
|
alert("Approval failed -- you cannot approve results with config " +
|
|
"type comparison-*");
|
|
$scope.submitPending = false;
|
|
return;
|
|
}
|
|
$http({
|
|
method: "POST",
|
|
url: "/edits",
|
|
data: {
|
|
oldResultsType: $scope.header.type,
|
|
oldResultsHash: $scope.header.dataHash,
|
|
modifications: newResults
|
|
}
|
|
}).success(function(data, status, headers, config) {
|
|
var itemIndicesToMove = [];
|
|
for (var i = 0; i < testDataSubset.length; i++) {
|
|
itemIndicesToMove.push(testDataSubset[i].index);
|
|
}
|
|
$scope.moveItemsToTab(itemIndicesToMove,
|
|
"HackToMakeSureThisItemDisappears");
|
|
$scope.updateResults();
|
|
alert("New baselines submitted successfully!\n\n" +
|
|
"You still need to commit the updated expectations files on " +
|
|
"the server side to the Skia repo.\n\n" +
|
|
"When you click OK, your web UI will reload; after that " +
|
|
"completes, you will see the updated data (once the server has " +
|
|
"finished loading the update results into memory!) and you can " +
|
|
"submit more baselines if you want.");
|
|
// I don't know why, but if I just call reload() here it doesn't work.
|
|
// Making a timer call it fixes the problem.
|
|
$timeout(function(){location.reload();}, 1);
|
|
}).error(function(data, status, headers, config) {
|
|
alert("There was an error submitting your baselines.\n\n" +
|
|
"Please see server-side log for details.");
|
|
$scope.submitPending = false;
|
|
});
|
|
}
|
|
|
|
|
|
//
|
|
// Operations we use to mimic Set semantics, in such a way that
|
|
// checking for presence within the Set is as fast as possible.
|
|
// But getting a list of all values within the Set is not necessarily
|
|
// possible.
|
|
// TODO(epoger): move into a separate .js file?
|
|
//
|
|
|
|
/**
|
|
* Returns true if value "value" is present within set "set".
|
|
*
|
|
* @param value a value of any type
|
|
* @param set an Object which we use to mimic set semantics
|
|
* (this should make isValueInSet faster than if we used an Array)
|
|
*/
|
|
$scope.isValueInSet = function(value, set) {
|
|
return (true == set[value]);
|
|
}
|
|
|
|
/**
|
|
* If value "value" is already in set "set", remove it; otherwise, add it.
|
|
*
|
|
* @param value a value of any type
|
|
* @param set an Object which we use to mimic set semantics
|
|
*/
|
|
$scope.toggleValueInSet = function(value, set) {
|
|
if (true == set[value]) {
|
|
delete set[value];
|
|
} else {
|
|
set[value] = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For each value in valueArray, call toggleValueInSet(value, set).
|
|
*
|
|
* @param valueArray
|
|
* @param set
|
|
*/
|
|
$scope.toggleValuesInSet = function(valueArray, set) {
|
|
var arrayLength = valueArray.length;
|
|
for (var i = 0; i < arrayLength; i++) {
|
|
$scope.toggleValueInSet(valueArray[i], set);
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// Array operations; similar to our Set operations, but operate on a
|
|
// Javascript Array so we *can* easily get a list of all values in the Set.
|
|
// TODO(epoger): move into a separate .js file?
|
|
//
|
|
|
|
/**
|
|
* Returns true if value "value" is present within array "array".
|
|
*
|
|
* @param value a value of any type
|
|
* @param array a Javascript Array
|
|
*/
|
|
$scope.isValueInArray = function(value, array) {
|
|
return (-1 != array.indexOf(value));
|
|
}
|
|
|
|
/**
|
|
* If value "value" is already in array "array", remove it; otherwise,
|
|
* add it.
|
|
*
|
|
* @param value a value of any type
|
|
* @param array a Javascript Array
|
|
*/
|
|
$scope.toggleValueInArray = function(value, array) {
|
|
var i = array.indexOf(value);
|
|
if (-1 == i) {
|
|
array.push(value);
|
|
} else {
|
|
array.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// Miscellaneous utility functions.
|
|
// TODO(epoger): move into a separate .js file?
|
|
//
|
|
|
|
/**
|
|
* Returns a human-readable (in local time zone) time string for a
|
|
* particular moment in time.
|
|
*
|
|
* @param secondsPastEpoch (numeric): seconds past epoch in UTC
|
|
*/
|
|
$scope.localTimeString = function(secondsPastEpoch) {
|
|
var d = new Date(secondsPastEpoch * 1000);
|
|
return d.toString();
|
|
}
|
|
|
|
}
|
|
);
|