summaryrefslogtreecommitdiffstats
path: root/chromium/third_party/skia/gm/rebaseline_server/static/loader.js
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/skia/gm/rebaseline_server/static/loader.js')
-rw-r--r--chromium/third_party/skia/gm/rebaseline_server/static/loader.js932
1 files changed, 932 insertions, 0 deletions
diff --git a/chromium/third_party/skia/gm/rebaseline_server/static/loader.js b/chromium/third_party/skia/gm/rebaseline_server/static/loader.js
new file mode 100644
index 00000000000..296689bde23
--- /dev/null
+++ b/chromium/third_party/skia/gm/rebaseline_server/static/loader.js
@@ -0,0 +1,932 @@
+/*
+ * Loader:
+ * Reads GM result reports written out by results.py, and imports
+ * them into $scope.extraColumnHeaders and $scope.imagePairs .
+ */
+var Loader = angular.module(
+ 'Loader',
+ ['ConstantsModule']
+);
+
+Loader.directive(
+ 'resultsUpdatedCallbackDirective',
+ ['$timeout',
+ function($timeout) {
+ return function(scope, element, attrs) {
+ if (scope.$last) {
+ $timeout(function() {
+ scope.resultsUpdatedCallback();
+ });
+ }
+ };
+ }
+ ]
+);
+
+// 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(
+ 'removeHiddenImagePairs',
+ function(constants) {
+ return function(unfilteredImagePairs, hiddenResultTypes, hiddenConfigs,
+ builderSubstring, testSubstring, viewingTab) {
+ var filteredImagePairs = [];
+ for (var i = 0; i < unfilteredImagePairs.length; i++) {
+ var imagePair = unfilteredImagePairs[i];
+ var extraColumnValues = imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS];
+ // 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[extraColumnValues[
+ constants.KEY__EXTRACOLUMNS__RESULT_TYPE]]) &&
+ !(true == hiddenConfigs[extraColumnValues[
+ constants.KEY__EXTRACOLUMNS__CONFIG]]) &&
+ !(-1 == extraColumnValues[constants.KEY__EXTRACOLUMNS__BUILDER]
+ .indexOf(builderSubstring)) &&
+ !(-1 == extraColumnValues[constants.KEY__EXTRACOLUMNS__TEST]
+ .indexOf(testSubstring)) &&
+ (viewingTab == imagePair.tab)) {
+ filteredImagePairs.push(imagePair);
+ }
+ }
+ return filteredImagePairs;
+ };
+ }
+);
+
+/**
+ * Limit the input imagePairs to some max number, and merge identical rows
+ * (adjacent rows which have the same (imageA, imageB) pair).
+ *
+ * @param unfilteredImagePairs imagePairs to filter
+ * @param maxPairs maximum number of pairs to output, or <0 for no limit
+ * @param mergeIdenticalRows if true, merge identical rows by setting
+ * ROWSPAN>1 on the first merged row, and ROWSPAN=0 for the rest
+ */
+Loader.filter(
+ 'mergeAndLimit',
+ function(constants) {
+ return function(unfilteredImagePairs, maxPairs, mergeIdenticalRows) {
+ var numPairs = unfilteredImagePairs.length;
+ if ((maxPairs > 0) && (maxPairs < numPairs)) {
+ numPairs = maxPairs;
+ }
+ var filteredImagePairs = [];
+ if (!mergeIdenticalRows || (numPairs == 1)) {
+ // Take a shortcut if we're not merging identical rows.
+ // We still need to set ROWSPAN to 1 for each row, for the HTML viewer.
+ for (var i = numPairs-1; i >= 0; i--) {
+ var imagePair = unfilteredImagePairs[i];
+ imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] = 1;
+ filteredImagePairs[i] = imagePair;
+ }
+ } else if (numPairs > 1) {
+ // General case--there are at least 2 rows, so we may need to merge some.
+ // Work from the bottom up, so we can keep a running total of how many
+ // rows should be merged, and set ROWSPAN of the top row accordingly.
+ var imagePair = unfilteredImagePairs[numPairs-1];
+ var nextRowImageAUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_A_URL];
+ var nextRowImageBUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL];
+ imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] = 1;
+ filteredImagePairs[numPairs-1] = imagePair;
+ for (var i = numPairs-2; i >= 0; i--) {
+ imagePair = unfilteredImagePairs[i];
+ var thisRowImageAUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_A_URL];
+ var thisRowImageBUrl = imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL];
+ if ((thisRowImageAUrl == nextRowImageAUrl) &&
+ (thisRowImageBUrl == nextRowImageBUrl)) {
+ imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] =
+ filteredImagePairs[i+1][constants.KEY__IMAGEPAIRS__ROWSPAN] + 1;
+ filteredImagePairs[i+1][constants.KEY__IMAGEPAIRS__ROWSPAN] = 0;
+ } else {
+ imagePair[constants.KEY__IMAGEPAIRS__ROWSPAN] = 1;
+ nextRowImageAUrl = thisRowImageAUrl;
+ nextRowImageBUrl = thisRowImageBUrl;
+ }
+ filteredImagePairs[i] = imagePair;
+ }
+ } else {
+ // No results.
+ }
+ return filteredImagePairs;
+ };
+ }
+);
+
+
+Loader.controller(
+ 'Loader.Controller',
+ function($scope, $http, $filter, $location, $log, $timeout, constants) {
+ $scope.constants = constants;
+ $scope.windowTitle = "Loading GM Results...";
+ $scope.resultsToLoad = $location.search().resultsToLoad;
+ $scope.loadingMessage = "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($scope.resultsToLoad).success(
+ function(data, status, header, config) {
+ var dataHeader = data[constants.KEY__ROOT__HEADER];
+ if (dataHeader[constants.KEY__HEADER__SCHEMA_VERSION] !=
+ constants.VALUE__HEADER__SCHEMA_VERSION) {
+ $scope.loadingMessage = "ERROR: Got JSON file with schema version "
+ + dataHeader[constants.KEY__HEADER__SCHEMA_VERSION]
+ + " but expected schema version "
+ + constants.VALUE__HEADER__SCHEMA_VERSION;
+ } else if (dataHeader[constants.KEY__HEADER__IS_STILL_LOADING]) {
+ // Apply the server's requested reload delay to local time,
+ // so we will wait the right number of seconds regardless of clock
+ // skew between client and server.
+ var reloadDelayInSeconds =
+ dataHeader[constants.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE] -
+ dataHeader[constants.KEY__HEADER__TIME_UPDATED];
+ var timeNow = new Date().getTime();
+ var timeToReload = timeNow + reloadDelayInSeconds * 1000;
+ $scope.loadingMessage =
+ "server is still loading results; will retry at " +
+ $scope.localTimeString(timeToReload / 1000);
+ $timeout(
+ function(){location.reload();},
+ timeToReload - timeNow);
+ } else {
+ $scope.loadingMessage = "processing data, please wait...";
+
+ $scope.header = dataHeader;
+ $scope.extraColumnHeaders = data[constants.KEY__ROOT__EXTRACOLUMNHEADERS];
+ $scope.imagePairs = data[constants.KEY__ROOT__IMAGEPAIRS];
+ $scope.imageSets = data[constants.KEY__ROOT__IMAGESETS];
+ $scope.sortColumnSubdict = constants.KEY__IMAGEPAIRS__DIFFERENCES;
+ $scope.sortColumnKey = constants.KEY__DIFFERENCES__PERCEPTUAL_DIFF;
+
+ $scope.showSubmitAdvancedSettings = false;
+ $scope.submitAdvancedSettings = {};
+ $scope.submitAdvancedSettings[
+ constants.KEY__EXPECTATIONS__REVIEWED] = true;
+ $scope.submitAdvancedSettings[
+ constants.KEY__EXPECTATIONS__IGNOREFAILURE] = 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 (dataHeader[constants.KEY__HEADER__IS_EDITABLE]) {
+ $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.imagePairs.length;
+
+ // Add index and tab fields to all records.
+ for (var i = 0; i < $scope.imagePairs.length; i++) {
+ $scope.imagePairs[i].index = i;
+ $scope.imagePairs[i].tab = $scope.defaultTab;
+ }
+
+ // Arrays within which the user can toggle individual elements.
+ $scope.selectedImagePairs = [];
+
+ // Sets within which the user can toggle individual elements.
+ $scope.hiddenResultTypes = {};
+ $scope.hiddenResultTypes[
+ constants.KEY__RESULT_TYPE__FAILUREIGNORED] = true;
+ $scope.hiddenResultTypes[
+ constants.KEY__RESULT_TYPE__NOCOMPARISON] = true;
+ $scope.hiddenResultTypes[
+ constants.KEY__RESULT_TYPE__SUCCEEDED] = true;
+ $scope.allResultTypes = $scope.columnSliceOf2DArray(
+ $scope.extraColumnHeaders[constants.KEY__EXTRACOLUMNS__RESULT_TYPE]
+ [constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS],
+ 0);
+ $scope.hiddenConfigs = {};
+ $scope.allConfigs = $scope.columnSliceOf2DArray(
+ $scope.extraColumnHeaders[constants.KEY__EXTRACOLUMNS__CONFIG]
+ [constants.KEY__EXTRACOLUMNHEADERS__VALUES_AND_COUNTS],
+ 0);
+
+ // 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();
+
+ // Any image URLs which are relative should be relative to the JSON
+ // file's source directory; absolute URLs should be left alone.
+ var baseUrlKey = constants.KEY__IMAGESETS__FIELD__BASE_URL;
+ angular.forEach(
+ $scope.imageSets,
+ function(imageSet) {
+ var baseUrl = imageSet[baseUrlKey];
+ if ((baseUrl.substring(0, 1) != '/') &&
+ (baseUrl.indexOf('://') == -1)) {
+ imageSet[baseUrlKey] = $scope.resultsToLoad + '/../' + baseUrl;
+ }
+ }
+ );
+
+ $scope.updateResults();
+ $scope.loadingMessage = "";
+ $scope.windowTitle = "Current GM Results";
+ }
+ }
+ ).error(
+ function(data, status, header, config) {
+ $scope.loadingMessage = "FAILED to load.";
+ $scope.windowTitle = "Failed to Load GM Results";
+ }
+ );
+
+
+ //
+ // Select/Clear/Toggle all tests.
+ //
+
+ /**
+ * Select all currently showing tests.
+ */
+ $scope.selectAllImagePairs = function() {
+ var numImagePairsShowing = $scope.limitedImagePairs.length;
+ for (var i = 0; i < numImagePairsShowing; i++) {
+ var index = $scope.limitedImagePairs[i].index;
+ if (!$scope.isValueInArray(index, $scope.selectedImagePairs)) {
+ $scope.toggleValueInArray(index, $scope.selectedImagePairs);
+ }
+ }
+ }
+
+ /**
+ * Deselect all currently showing tests.
+ */
+ $scope.clearAllImagePairs = function() {
+ var numImagePairsShowing = $scope.limitedImagePairs.length;
+ for (var i = 0; i < numImagePairsShowing; i++) {
+ var index = $scope.limitedImagePairs[i].index;
+ if ($scope.isValueInArray(index, $scope.selectedImagePairs)) {
+ $scope.toggleValueInArray(index, $scope.selectedImagePairs);
+ }
+ }
+ }
+
+ /**
+ * Toggle selection of all currently showing tests.
+ */
+ $scope.toggleAllImagePairs = function() {
+ var numImagePairsShowing = $scope.limitedImagePairs.length;
+ for (var i = 0; i < numImagePairsShowing; i++) {
+ var index = $scope.limitedImagePairs[i].index;
+ $scope.toggleValueInArray(index, $scope.selectedImagePairs);
+ }
+ }
+
+ /**
+ * Toggle selection state of a subset of the currently showing tests.
+ *
+ * @param startIndex index within $scope.limitedImagePairs of the first
+ * test to toggle selection state of
+ * @param num number of tests (in a contiguous block) to toggle
+ */
+ $scope.toggleSomeImagePairs = function(startIndex, num) {
+ var numImagePairsShowing = $scope.limitedImagePairs.length;
+ for (var i = startIndex; i < startIndex + num; i++) {
+ var index = $scope.limitedImagePairs[i].index;
+ $scope.toggleValueInArray(index, $scope.selectedImagePairs);
+ }
+ }
+
+
+ //
+ // 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 imagePairs in $scope.selectedImagePairs to a different tab,
+ * and then clear $scope.selectedImagePairs.
+ *
+ * @param newTab (string): name of the tab to move the tests to
+ */
+ $scope.moveSelectedImagePairsToTab = function(newTab) {
+ $scope.moveImagePairsToTab($scope.selectedImagePairs, newTab);
+ $scope.selectedImagePairs = [];
+ $scope.updateResults();
+ }
+
+ /**
+ * Move a subset of $scope.imagePairs to a different tab.
+ *
+ * @param imagePairIndices (array of ints): indices into $scope.imagePairs
+ * indicating which test results to move
+ * @param newTab (string): name of the tab to move the tests to
+ */
+ $scope.moveImagePairsToTab = function(imagePairIndices, newTab) {
+ var imagePairIndex;
+ var numImagePairs = imagePairIndices.length;
+ for (var i = 0; i < numImagePairs; i++) {
+ imagePairIndex = imagePairIndices[i];
+ $scope.numResultsPerTab[$scope.imagePairs[imagePairIndex].tab]--;
+ $scope.imagePairs[imagePairIndex].tab = newTab;
+ }
+ $scope.numResultsPerTab[newTab] += numImagePairs;
+ }
+
+
+ //
+ // $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,
+ 'showThumbnailsPending': $scope.queryParameters.copiers.simple,
+ 'mergeIdenticalRowsPending': $scope.queryParameters.copiers.simple,
+ 'imageSizePending': $scope.queryParameters.copiers.simple,
+ 'sortColumnSubdict': $scope.queryParameters.copiers.simple,
+ 'sortColumnKey': $scope.queryParameters.copiers.simple,
+
+ 'hiddenResultTypes': $scope.queryParameters.copiers.set,
+ 'hiddenConfigs': $scope.queryParameters.copiers.set,
+ };
+ $scope.queryParameters.map[constants.KEY__EXTRACOLUMNS__BUILDER] =
+ $scope.queryParameters.copiers.categoryValueMatch;
+ $scope.queryParameters.map[constants.KEY__EXTRACOLUMNS__TEST] =
+ $scope.queryParameters.copiers.categoryValueMatch;
+
+ // 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.renderStartTime = window.performance.now();
+ $log.debug("renderStartTime: " + $scope.renderStartTime);
+ $scope.displayLimit = $scope.displayLimitPending;
+ $scope.mergeIdenticalRows = $scope.mergeIdenticalRowsPending;
+ // TODO(epoger): Every time we apply a filter, AngularJS creates
+ // another copy of the array. Is there a way we can filter out
+ // the imagePairs 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.sortColumnKey ==
+ constants.KEY__DIFFERENCES__PERCENT_DIFF_PIXELS) ||
+ ($scope.sortColumnKey ==
+ constants.KEY__DIFFERENCES__PERCEPTUAL_DIFF));
+
+ $scope.filteredImagePairs =
+ $filter("orderBy")(
+ $filter("removeHiddenImagePairs")(
+ $scope.imagePairs,
+ $scope.hiddenResultTypes,
+ $scope.hiddenConfigs,
+ $scope.categoryValueMatch.builder,
+ $scope.categoryValueMatch.test,
+ $scope.viewingTab
+ ),
+ [$scope.getSortColumnValue, $scope.getSecondOrderSortValue],
+ doReverse);
+ $scope.limitedImagePairs = $filter("mergeAndLimit")(
+ $scope.filteredImagePairs, $scope.displayLimit, $scope.mergeIdenticalRows);
+ } else {
+ $scope.filteredImagePairs =
+ $filter("orderBy")(
+ $filter("filter")(
+ $scope.imagePairs,
+ {tab: $scope.viewingTab},
+ true
+ ),
+ [$scope.getSortColumnValue, $scope.getSecondOrderSortValue]);
+ $scope.limitedImagePairs = $filter("mergeAndLimit")(
+ $scope.filteredImagePairs, -1, $scope.mergeIdenticalRows);
+ }
+ $scope.showThumbnails = $scope.showThumbnailsPending;
+ $scope.imageSize = $scope.imageSizePending;
+ $scope.setUpdatesPending(false);
+ $scope.queryParameters.save();
+ }
+
+ /**
+ * This function is called when the results have been completely rendered
+ * after updateResults().
+ */
+ $scope.resultsUpdatedCallback = function() {
+ $scope.renderEndTime = window.performance.now();
+ $log.debug("renderEndTime: " + $scope.renderEndTime);
+ }
+
+ /**
+ * Re-sort the displayed results.
+ *
+ * @param subdict (string): which KEY__IMAGEPAIRS__* subdictionary
+ * the sort column key is within, or 'none' if the sort column
+ * key is one of KEY__IMAGEPAIRS__*
+ * @param key (string): sort by value associated with this key in subdict
+ */
+ $scope.sortResultsBy = function(subdict, key) {
+ $scope.sortColumnSubdict = subdict;
+ $scope.sortColumnKey = key;
+ $scope.updateResults();
+ }
+
+ /**
+ * For a particular ImagePair, return the value of the column we are
+ * sorting on (according to $scope.sortColumnSubdict and
+ * $scope.sortColumnKey).
+ *
+ * @param imagePair: imagePair to get a column value out of.
+ */
+ $scope.getSortColumnValue = function(imagePair) {
+ if ($scope.sortColumnSubdict in imagePair) {
+ return imagePair[$scope.sortColumnSubdict][$scope.sortColumnKey];
+ } else if ($scope.sortColumnKey in imagePair) {
+ return imagePair[$scope.sortColumnKey];
+ } else {
+ return undefined;
+ }
+ }
+
+ /**
+ * For a particular ImagePair, return the value we use for the
+ * second-order sort (tiebreaker when multiple rows have
+ * the same getSortColumnValue()).
+ *
+ * We join the imageA and imageB urls for this value, so that we merge
+ * adjacent rows as much as possible.
+ *
+ * @param imagePair: imagePair to get a column value out of.
+ */
+ $scope.getSecondOrderSortValue = function(imagePair) {
+ return imagePair[constants.KEY__IMAGEPAIRS__IMAGE_A_URL] + "-vs-" +
+ imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL];
+ }
+
+ /**
+ * 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.hiddenResultTypes so that ALL resultTypes are showing,
+ * and update the visible results.
+ */
+ $scope.showAllResultTypes = function() {
+ $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();
+ }
+
+ /**
+ * Update $scope.hiddenConfigs so that ALL configs are showing,
+ * and update the visible results.
+ */
+ $scope.showAllConfigs = function() {
+ $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.
+ *
+ * TODO(epoger): This assumes that the original expectations are in
+ * imageSetA, and the actuals are in imageSetB.
+ *
+ * @param imagePairsSubset an array of test results, most likely a subset of
+ * $scope.imagePairs (perhaps with some modifications)
+ */
+ $scope.submitApprovals = function(imagePairsSubset) {
+ $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 updatedExpectations = [];
+ for (var i = 0; i < imagePairsSubset.length; i++) {
+ var imagePair = imagePairsSubset[i];
+ var updatedExpectation = {};
+ updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS] =
+ imagePair[constants.KEY__IMAGEPAIRS__EXPECTATIONS];
+ updatedExpectation[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS] =
+ imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS];
+ // IMAGE_B_URL contains the actual image (which is now the expectation)
+ updatedExpectation[constants.KEY__IMAGEPAIRS__IMAGE_B_URL] =
+ imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL];
+ if (0 == updatedExpectation[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS]
+ [constants.KEY__EXTRACOLUMNS__CONFIG]
+ .indexOf('comparison-')) {
+ encounteredComparisonConfig = true;
+ }
+
+ // Advanced settings...
+ if (null == updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]) {
+ updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS] = {};
+ }
+ updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]
+ [constants.KEY__EXPECTATIONS__REVIEWED] =
+ $scope.submitAdvancedSettings[
+ constants.KEY__EXPECTATIONS__REVIEWED];
+ if (true == $scope.submitAdvancedSettings[
+ constants.KEY__EXPECTATIONS__IGNOREFAILURE]) {
+ // if it's false, don't send it at all (just keep the default)
+ updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]
+ [constants.KEY__EXPECTATIONS__IGNOREFAILURE] = true;
+ }
+ updatedExpectation[constants.KEY__IMAGEPAIRS__EXPECTATIONS]
+ [constants.KEY__EXPECTATIONS__BUGS] = bugs;
+
+ updatedExpectations.push(updatedExpectation);
+ }
+ if (encounteredComparisonConfig) {
+ alert("Approval failed -- you cannot approve results with config " +
+ "type comparison-*");
+ $scope.submitPending = false;
+ return;
+ }
+ var modificationData = {};
+ modificationData[constants.KEY__EDITS__MODIFICATIONS] =
+ updatedExpectations;
+ modificationData[constants.KEY__EDITS__OLD_RESULTS_HASH] =
+ $scope.header[constants.KEY__HEADER__DATAHASH];
+ modificationData[constants.KEY__EDITS__OLD_RESULTS_TYPE] =
+ $scope.header[constants.KEY__HEADER__TYPE];
+ $http({
+ method: "POST",
+ url: "/edits",
+ data: modificationData
+ }).success(function(data, status, headers, config) {
+ var imagePairIndicesToMove = [];
+ for (var i = 0; i < imagePairsSubset.length; i++) {
+ imagePairIndicesToMove.push(imagePairsSubset[i].index);
+ }
+ $scope.moveImagePairsToTab(imagePairIndicesToMove,
+ "HackToMakeSureThisImagePairDisappears");
+ $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 the number of values present within set "set".
+ *
+ * @param set an Object which we use to mimic set semantics
+ */
+ $scope.setSize = function(set) {
+ return Object.keys(set).length;
+ }
+
+ /**
+ * 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 single "column slice" of a 2D array.
+ *
+ * For example, if array is:
+ * [[A0, A1],
+ * [B0, B1],
+ * [C0, C1]]
+ * and index is 0, this this will return:
+ * [A0, B0, C0]
+ *
+ * @param array a Javascript Array
+ * @param column (numeric): index within each row array
+ */
+ $scope.columnSliceOf2DArray = function(array, column) {
+ var slice = [];
+ var numRows = array.length;
+ for (var row = 0; row < numRows; row++) {
+ slice.push(array[row][column]);
+ }
+ return slice;
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * Returns a hex color string (such as "#aabbcc") for the given RGB values.
+ *
+ * @param r (numeric): red channel value, 0-255
+ * @param g (numeric): green channel value, 0-255
+ * @param b (numeric): blue channel value, 0-255
+ */
+ $scope.hexColorString = function(r, g, b) {
+ var rString = r.toString(16);
+ if (r < 16) {
+ rString = "0" + rString;
+ }
+ var gString = g.toString(16);
+ if (g < 16) {
+ gString = "0" + gString;
+ }
+ var bString = b.toString(16);
+ if (b < 16) {
+ bString = "0" + bString;
+ }
+ return '#' + rString + gString + bString;
+ }
+
+ /**
+ * Returns a hex color string (such as "#aabbcc") for the given brightness.
+ *
+ * @param brightnessString (string): 0-255, 0 is completely black
+ *
+ * TODO(epoger): It might be nice to tint the color when it's not completely
+ * black or completely white.
+ */
+ $scope.brightnessStringToHexColor = function(brightnessString) {
+ var v = parseInt(brightnessString);
+ return $scope.hexColorString(v, v, v);
+ }
+
+ /**
+ * Returns the last path component of image diff URL for a given ImagePair.
+ *
+ * Depending on which diff this is (whitediffs, pixeldiffs, etc.) this
+ * will be relative to different base URLs.
+ *
+ * We must keep this function in sync with _get_difference_locator() in
+ * ../imagediffdb.py
+ *
+ * @param imagePair: ImagePair to generate image diff URL for
+ */
+ $scope.getImageDiffRelativeUrl = function(imagePair) {
+ var before =
+ imagePair[constants.KEY__IMAGEPAIRS__IMAGE_A_URL] + "-vs-" +
+ imagePair[constants.KEY__IMAGEPAIRS__IMAGE_B_URL];
+ return before.replace(/[^\w\-]/g, "_") + ".png";
+ }
+
+ }
+);