aboutsummaryrefslogtreecommitdiffhomepage
path: root/gm/rebaseline_server
diff options
context:
space:
mode:
authorGravatar epoger <epoger@google.com>2014-08-20 08:00:28 -0700
committerGravatar Commit bot <commit-bot@chromium.org>2014-08-20 08:00:28 -0700
commit2c4352bb9db02db70b829f96f397faa80fade220 (patch)
treeb18ff06d99190d6dccd81d6cf241f233f1da7ddc /gm/rebaseline_server
parent802aa8c15b7e88f5c702c2d10b33b3787dd38f47 (diff)
rebaseline_server: allow users to generate SKP diff patches on a shared instance
This will allow developers to maintain SKP baselines without ever running their own rebaseline_server instance! For now, the developer must manually apply the resulting patchset to his local Skia checkout to actually modify expectations; in the near future, we hope to make the UI upload the patchset to Rietveld on the user's behalf. BUG=skia:1918 NOTRY=true R=stephana@google.com, rmistry@google.com Author: epoger@google.com Review URL: https://codereview.chromium.org/487853004
Diffstat (limited to 'gm/rebaseline_server')
-rwxr-xr-xgm/rebaseline_server/compare_rendered_pictures.py20
-rw-r--r--gm/rebaseline_server/imagepair.py9
-rwxr-xr-xgm/rebaseline_server/results.py8
-rwxr-xr-xgm/rebaseline_server/server.py48
-rw-r--r--gm/rebaseline_server/static/constants.js4
-rw-r--r--gm/rebaseline_server/static/live-loader.js58
-rw-r--r--gm/rebaseline_server/static/live-view.html13
-rw-r--r--gm/rebaseline_server/testdata/outputs/expected/compare_rendered_pictures_test.CompareRenderedPicturesTest.test_endToEnd/compare_rendered_pictures.json14
-rw-r--r--gm/rebaseline_server/testdata/outputs/expected/compare_rendered_pictures_test.CompareRenderedPicturesTest.test_repo_url/compare_rendered_pictures.json14
-rw-r--r--gm/rebaseline_server/writable_expectations.py175
10 files changed, 295 insertions, 68 deletions
diff --git a/gm/rebaseline_server/compare_rendered_pictures.py b/gm/rebaseline_server/compare_rendered_pictures.py
index f1af2e02c7..a6e0aa0bc5 100755
--- a/gm/rebaseline_server/compare_rendered_pictures.py
+++ b/gm/rebaseline_server/compare_rendered_pictures.py
@@ -51,6 +51,9 @@ COLUMN__BUILDER_A = 'builderA'
COLUMN__RENDER_MODE_A = 'renderModeA'
COLUMN__BUILDER_B = 'builderB'
COLUMN__RENDER_MODE_B = 'renderModeB'
+# Known values for some of those columns.
+COLUMN__TILED_OR_WHOLE__TILED = 'tiled'
+COLUMN__TILED_OR_WHOLE__WHOLE = 'whole'
FREEFORM_COLUMN_IDS = [
COLUMN__SOURCE_SKP,
@@ -213,8 +216,8 @@ class RenderedPicturesComparisons(results.BaseComparisons):
"""
logging.info('Reading JSON image summaries from dirs %s and %s...' % (
setA_root, setB_root))
- setA_dicts = self._read_dicts_from_root(setA_root)
- setB_dicts = self._read_dicts_from_root(setB_root)
+ setA_dicts = self.read_dicts_from_root(setA_root)
+ setB_dicts = self.read_dicts_from_root(setB_root)
logging.info('Comparing summary dicts...')
all_image_pairs = imagepairset.ImagePairSet(
@@ -296,6 +299,7 @@ class RenderedPicturesComparisons(results.BaseComparisons):
image_dict_A=whole_image_A, image_dict_B=whole_image_B,
builder_A=builder_A, render_mode_A=render_mode_A,
builder_B=builder_B, render_mode_B=render_mode_B,
+ source_json_file=dict_path,
source_skp_name=skp_name, tilenum=None))
tiled_images_A = self.get_default(
@@ -316,6 +320,7 @@ class RenderedPicturesComparisons(results.BaseComparisons):
if tile_num < num_tiles_B else None),
builder_A=builder_A, render_mode_A=render_mode_A,
builder_B=builder_B, render_mode_B=render_mode_B,
+ source_json_file=dict_path,
source_skp_name=skp_name, tilenum=tile_num))
for one_imagepair in imagepairs_for_this_skp:
@@ -364,8 +369,8 @@ class RenderedPicturesComparisons(results.BaseComparisons):
def _create_image_pair(self, image_dict_A, image_dict_B,
builder_A, render_mode_A,
builder_B, render_mode_B,
- source_skp_name,
- tilenum):
+ source_json_file,
+ source_skp_name, tilenum):
"""Creates an ImagePair object for this pair of images.
Args:
@@ -377,6 +382,8 @@ class RenderedPicturesComparisons(results.BaseComparisons):
builder_B: builder that created image set A or None if unknow
render_mode_B: render mode used to generate image set A or None if
unknown.
+ source_json_file: string; relative path of the JSON file where this
+ result came from, within setA and setB.
source_skp_name: string; name of the source SKP file
tilenum: which tile, or None if a wholeimage
@@ -418,10 +425,10 @@ class RenderedPicturesComparisons(results.BaseComparisons):
COLUMN__RENDER_MODE_B: render_mode_B,
}
if tilenum == None:
- extra_columns_dict[COLUMN__TILED_OR_WHOLE] = 'whole'
+ extra_columns_dict[COLUMN__TILED_OR_WHOLE] = COLUMN__TILED_OR_WHOLE__WHOLE
extra_columns_dict[COLUMN__TILENUM] = 'N/A'
else:
- extra_columns_dict[COLUMN__TILED_OR_WHOLE] = 'tiled'
+ extra_columns_dict[COLUMN__TILED_OR_WHOLE] = COLUMN__TILED_OR_WHOLE__TILED
extra_columns_dict[COLUMN__TILENUM] = str(tilenum)
try:
@@ -431,6 +438,7 @@ class RenderedPicturesComparisons(results.BaseComparisons):
imageA_relative_url=imageA_relative_url,
imageB_relative_url=imageB_relative_url,
extra_columns=extra_columns_dict,
+ source_json_file=source_json_file,
download_all_images=self._download_all_images)
except (KeyError, TypeError):
logging.exception(
diff --git a/gm/rebaseline_server/imagepair.py b/gm/rebaseline_server/imagepair.py
index d9c4cb82b9..0ac0c42138 100644
--- a/gm/rebaseline_server/imagepair.py
+++ b/gm/rebaseline_server/imagepair.py
@@ -20,6 +20,7 @@ KEY__IMAGEPAIRS__EXTRACOLUMNS = 'extraColumns'
KEY__IMAGEPAIRS__IMAGE_A_URL = 'imageAUrl'
KEY__IMAGEPAIRS__IMAGE_B_URL = 'imageBUrl'
KEY__IMAGEPAIRS__IS_DIFFERENT = 'isDifferent'
+KEY__IMAGEPAIRS__SOURCE_JSON_FILE = 'sourceJsonFile'
# If self._diff_record is set to this, we haven't asked ImageDiffDB for the
# image diff details yet.
@@ -32,7 +33,7 @@ class ImagePair(object):
def __init__(self, image_diff_db,
base_url, imageA_relative_url, imageB_relative_url,
- expectations=None, extra_columns=None,
+ expectations=None, extra_columns=None, source_json_file=None,
download_all_images=False):
"""
Args:
@@ -46,6 +47,9 @@ class ImagePair(object):
metadata (ignore-failure, bug numbers, etc.)
extra_columns: optional dictionary containing more metadata (test name,
builder name, etc.)
+ source_json_file: relative path of the JSON file where each image came
+ from; this will be the same for both imageA and imageB, within their
+ respective directories
download_all_images: if True, download any images associated with this
image pair, even if we don't need them to generate diffs
(imageA == imageB, or one of them is missing)
@@ -56,6 +60,7 @@ class ImagePair(object):
self.imageB_relative_url = imageB_relative_url
self.expectations_dict = expectations
self.extra_columns_dict = extra_columns
+ self.source_json_file = source_json_file
if not imageA_relative_url or not imageB_relative_url:
self._is_different = True
self._diff_record = None
@@ -89,6 +94,8 @@ class ImagePair(object):
asdict[KEY__IMAGEPAIRS__EXPECTATIONS] = self.expectations_dict
if self.extra_columns_dict:
asdict[KEY__IMAGEPAIRS__EXTRACOLUMNS] = self.extra_columns_dict
+ if self.source_json_file:
+ asdict[KEY__IMAGEPAIRS__SOURCE_JSON_FILE] = self.source_json_file
if self._diff_record is _DIFF_RECORD_STILL_LOADING:
# We have waited as long as we can to ask ImageDiffDB for details of
# this image diff. Now we must block until ImageDiffDB can provide
diff --git a/gm/rebaseline_server/results.py b/gm/rebaseline_server/results.py
index 11a7d6ecfd..b0027d22af 100755
--- a/gm/rebaseline_server/results.py
+++ b/gm/rebaseline_server/results.py
@@ -213,7 +213,7 @@ class BaseComparisons(object):
Raises:
IOError if root does not refer to an existing directory
"""
- # I considered making this call _read_dicts_from_root(), but I decided
+ # I considered making this call read_dicts_from_root(), but I decided
# it was better to prune out the ignored builders within the os.walk().
if not os.path.isdir(root):
raise IOError('no directory found at path %s' % root)
@@ -227,9 +227,13 @@ class BaseComparisons(object):
meta_dict[builder] = gm_json.LoadFromFile(full_path)
return meta_dict
- def _read_dicts_from_root(self, root, pattern='*.json'):
+ @staticmethod
+ def read_dicts_from_root(root, pattern='*.json'):
"""Read all JSON dictionaries within a directory tree.
+ TODO(stephana): Factor this out into a utility module, as a standalone
+ function (not part of a class).
+
Args:
root: path to root of directory tree
pattern: which files to read within root (fnmatch-style pattern)
diff --git a/gm/rebaseline_server/server.py b/gm/rebaseline_server/server.py
index 1bd596340b..6062aed641 100755
--- a/gm/rebaseline_server/server.py
+++ b/gm/rebaseline_server/server.py
@@ -48,6 +48,8 @@ import download_actuals
import imagediffdb
import imagepairset
import results as results_mod
+import writable_expectations as writable_expectations_mod
+
PATHSPLIT_RE = re.compile('/([^/]+)/(.+)')
@@ -67,6 +69,9 @@ MIME_TYPE_MAP = {'': 'application/octet-stream',
KEY__EDITS__MODIFICATIONS = 'modifications'
KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash'
KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType'
+KEY__LIVE_EDITS__MODIFICATIONS = 'modifications'
+KEY__LIVE_EDITS__SET_A_DESCRIPTIONS = 'setA'
+KEY__LIVE_EDITS__SET_B_DESCRIPTIONS = 'setB'
DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR
DEFAULT_GM_SUMMARIES_BUCKET = download_actuals.GM_SUMMARIES_BUCKET
@@ -685,11 +690,11 @@ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
normpath = posixpath.normpath(self.path)
dispatchers = {
'/edits': self.do_POST_edits,
+ '/live-edits': self.do_POST_live_edits,
}
try:
dispatcher = dispatchers[normpath]
dispatcher()
- self.send_response(200)
except:
self.send_error(404)
raise
@@ -749,6 +754,47 @@ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
# We can do this in a separate thread; we should return our success message
# to the UI as soon as possible.
thread.start_new_thread(_SERVER.update_results, (True,))
+ self.send_response(200)
+
+ def do_POST_live_edits(self):
+ """ Handle a POST request with modifications to SKP expectations, in this
+ format:
+
+ {
+ KEY__LIVE_EDITS__SET_A_DESCRIPTIONS: {
+ # setA descriptions from the original data
+ },
+ KEY__LIVE_EDITS__SET_B_DESCRIPTIONS: {
+ # setB descriptions from the original data
+ },
+ KEY__LIVE_EDITS__MODIFICATIONS: [
+ # as needed by writable_expectations.modify()
+ ],
+ }
+
+ Raises an Exception if there were any problems.
+ """
+ content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE]
+ if content_type != 'application/json;charset=UTF-8':
+ raise Exception('unsupported %s [%s]' % (
+ _HTTP_HEADER_CONTENT_TYPE, content_type))
+
+ content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH])
+ json_data = self.rfile.read(content_length)
+ data = json.loads(json_data)
+ logging.debug('do_POST_live_edits: received new GM expectations data [%s]' %
+ data)
+ with writable_expectations_mod.WritableExpectations(
+ data[KEY__LIVE_EDITS__SET_A_DESCRIPTIONS]) as writable_expectations:
+ writable_expectations.modify(data[KEY__LIVE_EDITS__MODIFICATIONS])
+ diffs = writable_expectations.get_diffs()
+ # TODO(stephana): Move to a simpler web framework so we don't have to
+ # call these functions. See http://skbug.com/2856 ('rebaseline_server:
+ # Refactor server to use a simple web framework')
+ self.send_response(200)
+ self.send_header('Content-type', 'text/plain')
+ self.end_headers()
+ self.wfile.write(diffs)
def redirect_to(self, url):
""" Redirect the HTTP client to a different url.
diff --git a/gm/rebaseline_server/static/constants.js b/gm/rebaseline_server/static/constants.js
index aa4b63aa9e..a9601ece71 100644
--- a/gm/rebaseline_server/static/constants.js
+++ b/gm/rebaseline_server/static/constants.js
@@ -31,6 +31,7 @@ module.constant('constants', (function() {
KEY__IMAGEPAIRS__IMAGE_A_URL: 'imageAUrl',
KEY__IMAGEPAIRS__IMAGE_B_URL: 'imageBUrl',
KEY__IMAGEPAIRS__IS_DIFFERENT: 'isDifferent',
+ KEY__IMAGEPAIRS__SOURCE_JSON_FILE: 'sourceJsonFile',
// NOTE: Keep these in sync with ../imagepairset.py
KEY__ROOT__EXTRACOLUMNHEADERS: 'extraColumnHeaders',
@@ -83,6 +84,9 @@ module.constant('constants', (function() {
KEY__EDITS__MODIFICATIONS: 'modifications',
KEY__EDITS__OLD_RESULTS_HASH: 'oldResultsHash',
KEY__EDITS__OLD_RESULTS_TYPE: 'oldResultsType',
+ KEY__LIVE_EDITS__MODIFICATIONS: 'modifications',
+ KEY__LIVE_EDITS__SET_A_DESCRIPTIONS: 'setA',
+ KEY__LIVE_EDITS__SET_B_DESCRIPTIONS: 'setB',
// These are just used on the client side, no need to sync with server code.
KEY__IMAGEPAIRS__ROWSPAN: 'rowspan',
diff --git a/gm/rebaseline_server/static/live-loader.js b/gm/rebaseline_server/static/live-loader.js
index 611c2cd84d..30d05061be 100644
--- a/gm/rebaseline_server/static/live-loader.js
+++ b/gm/rebaseline_server/static/live-loader.js
@@ -776,14 +776,17 @@ Loader.controller(
* 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.
+ * This assumes that the original expectations are in imageSetA, and the
+ * new expectations are in imageSetB. That's fine, because the server
+ * mandates that anyway (it will swap the sets if the user requests them
+ * in the opposite order).
*
* @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;
+ $scope.diffResults = "";
// Convert bug text field to null or 1-item array.
var bugs = null;
@@ -792,13 +795,6 @@ Loader.controller(
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];
@@ -807,14 +803,11 @@ Loader.controller(
imagePair[constants.KEY__IMAGEPAIRS__EXPECTATIONS];
updatedExpectation[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS] =
imagePair[constants.KEY__IMAGEPAIRS__EXTRACOLUMNS];
+ updatedExpectation[constants.KEY__IMAGEPAIRS__SOURCE_JSON_FILE] =
+ imagePair[constants.KEY__IMAGEPAIRS__SOURCE_JSON_FILE];
// 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]) {
@@ -835,41 +828,20 @@ Loader.controller(
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] =
+ modificationData[constants.KEY__LIVE_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];
+ modificationData[constants.KEY__LIVE_EDITS__SET_A_DESCRIPTIONS] =
+ $scope.header[constants.KEY__HEADER__SET_A_DESCRIPTIONS];
+ modificationData[constants.KEY__LIVE_EDITS__SET_B_DESCRIPTIONS] =
+ $scope.header[constants.KEY__HEADER__SET_B_DESCRIPTIONS];
$http({
method: "POST",
- url: "/edits",
+ url: "/live-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);
+ $scope.diffResults = data;
+ $scope.submitPending = false;
}).error(function(data, status, headers, config) {
alert("There was an error submitting your baselines.\n\n" +
"Please see server-side log for details.");
diff --git a/gm/rebaseline_server/static/live-view.html b/gm/rebaseline_server/static/live-view.html
index 44cbd0d337..6c158a6e7e 100644
--- a/gm/rebaseline_server/static/live-view.html
+++ b/gm/rebaseline_server/static/live-view.html
@@ -170,14 +170,11 @@
<!-- Submission UI that we only show in the Pending Approval tab. -->
<div ng-show="'Pending Approval' == viewingTab">
- <div style="font-size:20px">
- TODO(epoger): We don't yet support submitting new SKP expectations.
- </div>
<div style="display:inline-block">
<button style="font-size:20px"
ng-click="submitApprovals(filteredImagePairs)"
- ng-disabled="true || submitPending || (filteredImagePairs.length == 0)">
- Update these {{filteredImagePairs.length}} expectations on the server
+ ng-disabled="submitPending || (filteredImagePairs.length == 0)">
+ Get a patchfile to update these {{filteredImagePairs.length}} expectations
</button>
</div>
<div style="display:inline-block">
@@ -201,6 +198,12 @@
</li>
</ul>
</div>
+ <div ng-show="diffResults">
+ <p>
+ Here is the patch to apply to your local checkout:
+ <br>
+ <textarea rows="8" cols="50">{{diffResults}}</textarea>
+ </div>
</div>
<p>
diff --git a/gm/rebaseline_server/testdata/outputs/expected/compare_rendered_pictures_test.CompareRenderedPicturesTest.test_endToEnd/compare_rendered_pictures.json b/gm/rebaseline_server/testdata/outputs/expected/compare_rendered_pictures_test.CompareRenderedPicturesTest.test_endToEnd/compare_rendered_pictures.json
index 81366205f1..afa8978de8 100644
--- a/gm/rebaseline_server/testdata/outputs/expected/compare_rendered_pictures_test.CompareRenderedPicturesTest.test_endToEnd/compare_rendered_pictures.json
+++ b/gm/rebaseline_server/testdata/outputs/expected/compare_rendered_pictures_test.CompareRenderedPicturesTest.test_endToEnd/compare_rendered_pictures.json
@@ -128,7 +128,7 @@
"renderModeB"
],
"header": {
- "dataHash": "-1510211866509185075",
+ "dataHash": "-5707186260478709107",
"isEditable": false,
"isExported": true,
"schemaVersion": 5,
@@ -164,7 +164,8 @@
},
"imageAUrl": "changed_skp/bitmap-64bitMD5_3101044995537104462.png",
"imageBUrl": "changed_skp/bitmap-64bitMD5_13623922271964399662.png",
- "isDifferent": true
+ "isDifferent": true,
+ "sourceJsonFile": "./summary.json"
},
{
"extraColumns": {
@@ -179,7 +180,8 @@
},
"imageAUrl": null,
"imageBUrl": "only-in-after_skp/bitmap-64bitMD5_2320185040577047131.png",
- "isDifferent": true
+ "isDifferent": true,
+ "sourceJsonFile": "./summary.json"
},
{
"extraColumns": {
@@ -194,7 +196,8 @@
},
"imageAUrl": "only-in-before_skp/bitmap-64bitMD5_2320185040577047131.png",
"imageBUrl": null,
- "isDifferent": true
+ "isDifferent": true,
+ "sourceJsonFile": "./summary.json"
},
{
"extraColumns": {
@@ -209,7 +212,8 @@
},
"imageAUrl": "unchanged_skp/bitmap-64bitMD5_3322248763049618493.png",
"imageBUrl": "unchanged_skp/bitmap-64bitMD5_3322248763049618493.png",
- "isDifferent": false
+ "isDifferent": false,
+ "sourceJsonFile": "./summary.json"
}
],
"imageSets": {
diff --git a/gm/rebaseline_server/testdata/outputs/expected/compare_rendered_pictures_test.CompareRenderedPicturesTest.test_repo_url/compare_rendered_pictures.json b/gm/rebaseline_server/testdata/outputs/expected/compare_rendered_pictures_test.CompareRenderedPicturesTest.test_repo_url/compare_rendered_pictures.json
index ada58a3bd1..5ee60925ab 100644
--- a/gm/rebaseline_server/testdata/outputs/expected/compare_rendered_pictures_test.CompareRenderedPicturesTest.test_repo_url/compare_rendered_pictures.json
+++ b/gm/rebaseline_server/testdata/outputs/expected/compare_rendered_pictures_test.CompareRenderedPicturesTest.test_repo_url/compare_rendered_pictures.json
@@ -128,7 +128,7 @@
"renderModeB"
],
"header": {
- "dataHash": "-1510211866509185075",
+ "dataHash": "-5707186260478709107",
"isEditable": false,
"isExported": true,
"schemaVersion": 5,
@@ -160,7 +160,8 @@
},
"imageAUrl": "changed_skp/bitmap-64bitMD5_3101044995537104462.png",
"imageBUrl": "changed_skp/bitmap-64bitMD5_13623922271964399662.png",
- "isDifferent": true
+ "isDifferent": true,
+ "sourceJsonFile": "./summary.json"
},
{
"extraColumns": {
@@ -175,7 +176,8 @@
},
"imageAUrl": null,
"imageBUrl": "only-in-after_skp/bitmap-64bitMD5_2320185040577047131.png",
- "isDifferent": true
+ "isDifferent": true,
+ "sourceJsonFile": "./summary.json"
},
{
"extraColumns": {
@@ -190,7 +192,8 @@
},
"imageAUrl": "only-in-before_skp/bitmap-64bitMD5_2320185040577047131.png",
"imageBUrl": null,
- "isDifferent": true
+ "isDifferent": true,
+ "sourceJsonFile": "./summary.json"
},
{
"extraColumns": {
@@ -205,7 +208,8 @@
},
"imageAUrl": "unchanged_skp/bitmap-64bitMD5_3322248763049618493.png",
"imageBUrl": "unchanged_skp/bitmap-64bitMD5_3322248763049618493.png",
- "isDifferent": false
+ "isDifferent": false,
+ "sourceJsonFile": "./summary.json"
}
],
"imageSets": {
diff --git a/gm/rebaseline_server/writable_expectations.py b/gm/rebaseline_server/writable_expectations.py
new file mode 100644
index 0000000000..dada0358c7
--- /dev/null
+++ b/gm/rebaseline_server/writable_expectations.py
@@ -0,0 +1,175 @@
+#!/usr/bin/python
+
+"""
+Copyright 2014 Google Inc.
+
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+
+Expectations on local disk that we can modify.
+"""
+
+# System-level imports
+import logging
+import os
+import re
+
+# Must fix up PYTHONPATH before importing from within Skia
+import rs_fixpypath # pylint: disable=W0611
+
+# Imports from within Skia
+from py.utils import git_utils
+import compare_rendered_pictures
+import gm_json
+import imagepair
+import results
+
+FILEPATH_RE = re.compile('.+/' + gm_json.IMAGE_FILENAME_PATTERN)
+
+SKIA_REPO = os.path.abspath(os.path.join(
+ os.path.dirname(__file__), os.pardir, os.pardir, '.git'))
+
+
+class WritableExpectations(git_utils.NewGitCheckout):
+ """Expectations on local disk that we can modify."""
+
+ def __init__(self, set_descriptions):
+ """Creates a sandbox on local disk containing writable expectations.
+
+ You must use the 'with' statement to create this object in such a way that
+ it cleans up after itself:
+
+ with WritableExpectations(*args) as writable_expectations:
+ # make modifications
+ # use the modified results
+ # the sandbox on local disk is automatically cleaned up here
+
+ Args:
+ set_descriptions: SET_DESCRIPTIONS dict describing the set we want to
+ update expectations within; this tells us the subdirectory within the
+ Skia repo where we keep these expectations, and the commithash at
+ which the user evaluated new baselines.
+ """
+ file_section = set_descriptions[results.KEY__SET_DESCRIPTIONS__SECTION]
+ assert file_section == gm_json.JSONKEY_EXPECTEDRESULTS
+
+ source_dir = _unicode_to_ascii(
+ set_descriptions[results.KEY__SET_DESCRIPTIONS__DIR])
+ assert source_dir.startswith(compare_rendered_pictures.REPO_URL_PREFIX)
+ repo_subdir = source_dir[len(compare_rendered_pictures.REPO_URL_PREFIX):]
+ repo_revision = _unicode_to_ascii(
+ set_descriptions[results.KEY__SET_DESCRIPTIONS__REPO_REVISION])
+
+ logging.info('Creating a writable Skia checkout at revision "%s"...' %
+ repo_revision)
+ super(WritableExpectations, self).__init__(
+ repository=SKIA_REPO, commit=repo_revision, subdir=repo_subdir)
+
+ def modify(self, modifications):
+ """Modify the contents of the checkout, using modifications from the UI.
+
+ Args:
+ modifications: data[KEY__LIVE_EDITS__MODIFICATIONS] coming back from the
+ rebaseline_server UI frontend
+ """
+ logging.info('Reading in dicts from writable Skia checkout in %s ...' %
+ self.root)
+ dicts = results.BaseComparisons.read_dicts_from_root(self.root)
+
+ # Make sure we have expected-results sections in all our output dicts.
+ for pathname, adict in dicts.iteritems():
+ if not adict:
+ adict = {}
+ if not adict.get(gm_json.JSONKEY_EXPECTEDRESULTS, None):
+ adict[gm_json.JSONKEY_EXPECTEDRESULTS] = {}
+ dicts[pathname] = adict
+
+ for modification in modifications:
+ expectations = modification[imagepair.KEY__IMAGEPAIRS__EXPECTATIONS]
+ _add_image_info_to_expectations(
+ expectations=expectations,
+ filepath=modification[imagepair.KEY__IMAGEPAIRS__IMAGE_B_URL])
+ extra_columns = modification[imagepair.KEY__IMAGEPAIRS__EXTRACOLUMNS]
+ dictname = modification[imagepair.KEY__IMAGEPAIRS__SOURCE_JSON_FILE]
+ dict_to_modify = dicts[dictname][gm_json.JSONKEY_EXPECTEDRESULTS]
+ test_name = extra_columns[compare_rendered_pictures.COLUMN__SOURCE_SKP]
+ test_record = dict_to_modify.get(test_name, {})
+ if (extra_columns[compare_rendered_pictures.COLUMN__TILED_OR_WHOLE] ==
+ compare_rendered_pictures.COLUMN__TILED_OR_WHOLE__TILED):
+ test_tiles_list = test_record.get(
+ gm_json.JSONKEY_SOURCE_TILEDIMAGES, [])
+ tilenum = int(extra_columns[compare_rendered_pictures.COLUMN__TILENUM])
+ _replace_list_item(test_tiles_list, tilenum, expectations)
+ test_record[gm_json.JSONKEY_SOURCE_TILEDIMAGES] = test_tiles_list
+ else:
+ test_record[gm_json.JSONKEY_SOURCE_WHOLEIMAGE] = expectations
+ dict_to_modify[test_name] = test_record
+
+ # Write the modified files back to disk.
+ self._write_dicts_to_root(meta_dict=dicts, root=self.root)
+
+ def get_diffs(self):
+ """Return patchfile describing any modifications to this checkout."""
+ return self._run_in_git_root(args=[git_utils.GIT, 'diff'])
+
+ @staticmethod
+ def _write_dicts_to_root(meta_dict, root):
+ """Write out multiple dictionaries in JSON format.
+
+ Args:
+ meta_dict: a builder-keyed meta-dictionary containing all the JSON
+ dictionaries we want to write out
+ root: path to root of directory tree within which to write files
+ """
+ if not os.path.isdir(root):
+ raise IOError('no directory found at path %s' % root)
+
+ for rel_path in meta_dict.keys():
+ full_path = os.path.join(root, rel_path)
+ gm_json.WriteToFile(meta_dict[rel_path], full_path)
+
+
+def _unicode_to_ascii(unicode_string):
+ """Returns the plain ASCII form of a unicode string.
+
+ TODO(stephana): We created this because we get unicode strings out of the
+ JSON file, while the git filenames and revision tags are plain ASCII.
+ There may be a better way to handle this... maybe set the JSON util to just
+ return ASCII strings?
+ """
+ return unicode_string.encode('ascii', 'ignore')
+
+
+def _replace_list_item(a_list, index, value):
+ """Replaces value at index "index" within a_list.
+
+ Args:
+ a_list: a list
+ index: index indicating which item in a_list to replace
+ value: value to set a_list[index] to
+
+ If a_list does not contain this index, it will be extended with None entries
+ to that length.
+ """
+ length = len(a_list)
+ while index >= length:
+ a_list.append(None)
+ length += 1
+ a_list[index] = value
+
+
+def _add_image_info_to_expectations(expectations, filepath):
+ """Add JSONKEY_IMAGE_* info to an existing expectations dictionary.
+
+ TODO(stephana): This assumes that the checksumAlgorithm and checksumValue
+ can be derived from the filepath, which is currently true but may not always
+ be true.
+
+ Args:
+ expectations: the expectations dict to augment
+ filepath: relative path to the image file
+ """
+ (checksum_algorithm, checksum_value) = FILEPATH_RE.match(filepath).groups()
+ expectations[gm_json.JSONKEY_IMAGE_CHECKSUMALGORITHM] = checksum_algorithm
+ expectations[gm_json.JSONKEY_IMAGE_CHECKSUMVALUE] = checksum_value
+ expectations[gm_json.JSONKEY_IMAGE_FILEPATH] = filepath