diff options
author | zachr@google.com <zachr@google.com@2bbb7eff-a529-9590-31e7-b0007b416f81> | 2013-08-07 18:06:39 +0000 |
---|---|---|
committer | zachr@google.com <zachr@google.com@2bbb7eff-a529-9590-31e7-b0007b416f81> | 2013-08-07 18:06:39 +0000 |
commit | 74c5ab19fd69d3c030f53e5b64493d548b74f916 (patch) | |
tree | bd57d431e3817665da5872a066f03ec5a2cf283c | |
parent | e57c62d039cbd67a4e52776b3e95c5d002b818d2 (diff) |
add ui for mutli-rebaselining
R=epoger@google.com
Review URL: https://codereview.chromium.org/22580004
git-svn-id: http://skia.googlecode.com/svn/trunk@10618 2bbb7eff-a529-9590-31e7-b0007b416f81
-rw-r--r-- | tools/skpdiff/diff_viewer.js | 64 | ||||
-rwxr-xr-x | tools/skpdiff/skpdiff_server.py | 107 | ||||
-rw-r--r-- | tools/skpdiff/viewer.html | 99 | ||||
-rw-r--r-- | tools/skpdiff/viewer_style.css | 62 |
4 files changed, 242 insertions, 90 deletions
diff --git a/tools/skpdiff/diff_viewer.js b/tools/skpdiff/diff_viewer.js index e7156b359f..9c33f84fa1 100644 --- a/tools/skpdiff/diff_viewer.js +++ b/tools/skpdiff/diff_viewer.js @@ -76,7 +76,11 @@ directive('swapImg', function() { }; }); -function DiffListController($scope, $http, $timeout) { +function DiffListController($scope, $http, $location, $timeout, $parse) { + // Detect if we are running the web server version of the viewer. If so, we set a flag and + // enable some extra functionality of the website for rebaselining. + $scope.isDynamic = ($location.protocol() == "http" || $location.protocol() == "https"); + // Label each kind of differ for the sort buttons. $scope.differs = [ { @@ -90,12 +94,20 @@ function DiffListController($scope, $http, $timeout) { // Puts the records within AngularJS scope $scope.records = SkPDiffRecords.records; + // Keep track of the index of the last record to change so that shift clicking knows what range + // of records to apply the action to. + $scope.lastSelectedIndex = undefined; + // Indicates which diff metric is used for sorting $scope.sortIndex = 1; // Called by the sort buttons to adjust the metric used for sorting $scope.setSortIndex = function(idx) { $scope.sortIndex = idx; + + // Because the index of things has most likely changed, the ranges of shift clicking no + // longer make sense from the user's point of view. We reset it to avoid confusion. + $scope.lastSelectedIndex = undefined; }; // A predicate for pulling out the number used for sorting @@ -103,30 +115,58 @@ function DiffListController($scope, $http, $timeout) { return record.diffs[$scope.sortIndex].result; }; - // Flash status indicators on the rows, and then remove them so the style can potentially be + // Flash status indicator on the page, and then remove it so the style can potentially be // reapplied later. - $scope.flashRowStatus = function(success, record) { + $scope.flashStatus = function(success) { var flashStyle = success ? "success-flash" : "failure-flash"; var flashDurationMillis = success ? 500 : 800; // Store the style in the record. The row will pick up the style this way instead of through // index because index can change with sort order. - record.cssClasses = flashStyle; + $scope.statusClass = flashStyle; // The animation cannot be repeated unless the class is removed the element. $timeout(function() { - record.cssClasses = ""; + $scope.statusClass = ""; }, flashDurationMillis); - } + }; + + $scope.selectedRebaseline = function(index, event) { + // Retrieve the records in the same order they are displayed. + var recordsInOrder = $parse("records | orderBy:sortingDiffer")($scope); + + // If the user is shift clicking, apply the last tick/untick to all elements in between this + // record, and the last one they ticked/unticked. + if (event.shiftKey && $scope.lastSelectedIndex !== undefined) { + var currentAction = recordsInOrder[index].isRebaselined; + var smallerIndex = Math.min($scope.lastSelectedIndex, index); + var largerIndex = Math.max($scope.lastSelectedIndex, index); + for (var recordIndex = smallerIndex; recordIndex <= largerIndex; recordIndex++) { + recordsInOrder[recordIndex].isRebaselined = currentAction; + } + $scope.lastSelectedIndex = index; + } + else + { + $scope.lastSelectedIndex = index; + } + + }; - $scope.setHashOf = function(imagePath, record) { - $http.post("/set_hash", { - "path": imagePath + $scope.commitRebaselines = function() { + // Gather up all records that have the rebaseline set. + var rebaselines = []; + for (var recordIndex = 0; recordIndex < $scope.records.length; recordIndex++) { + if ($scope.records[recordIndex].isRebaselined) { + rebaselines.push($scope.records[recordIndex].testPath); + } + } + $http.post("/commit_rebaselines", { + "rebaselines": rebaselines }).success(function(data) { - $scope.flashRowStatus(data.success, record); + $scope.flashStatus(data.success); }).error(function() { - $scope.flashRowStatus(false, record); + $scope.flashStatus(false); }); - }; } diff --git a/tools/skpdiff/skpdiff_server.py b/tools/skpdiff/skpdiff_server.py index 14035403a2..053291af9a 100755 --- a/tools/skpdiff/skpdiff_server.py +++ b/tools/skpdiff/skpdiff_server.py @@ -99,6 +99,8 @@ def download_gm_image(image_name, image_path, hash_val): @param image_path Path to place the image. @param hash_val The hash value of the image. """ + if hash_val is None: + return # Separate the test name from a image name image_match = IMAGE_FILENAME_RE.match(image_name) @@ -174,14 +176,18 @@ class GMInstance: - image_name = the GM test name and config - expected_hash = the current expected hash value - actual_hash = the actual hash value + - is_rebaselined = True if actual_hash is what is currently in the expected + results file, False otherwise. """ def __init__(self, device_name, image_name, - expected_hash, actual_hash): + expected_hash, actual_hash, + is_rebaselined): self.device_name = device_name self.image_name = image_name self.expected_hash = expected_hash self.actual_hash = actual_hash + self.is_rebaselined = is_rebaselined class ExpectationsManager: @@ -228,13 +234,14 @@ class ExpectationsManager: # Invoke skpdiff with our downloaded images and place its results in the # temporary directory. - self.skpdiff_output_path = os.path.join(image_output_dir, + self._skpdiff_output_path = os.path.join(image_output_dir, 'skpdiff_output.json') skpdiff_cmd = SKPDIFF_INVOKE_FORMAT.format(self._skpdiff_path, - self.skpdiff_output_path, + self._skpdiff_output_path, expected_image_dir, actual_image_dir) os.system(skpdiff_cmd) + self._load_skpdiff_output() def _get_expectations(self): @@ -267,11 +274,25 @@ class ExpectationsManager: with open(updated_file_path, 'rb') as updated_file: updated_contents = updated_file.read() + # Read the expected results on disk to determine what we've + # already rebaselined. + commited_contents = None + with open(expected_file_path, 'rb') as expected_file: + commited_contents = expected_file.read() + # Find all expectations that did not match. expected_diff = differ.GenerateDiffDictFromStrings( expected_contents, updated_contents) + # Generate a set of images that have already been rebaselined + # onto disk. + rebaselined_diff = differ.GenerateDiffDictFromStrings( + expected_contents, + commited_contents) + + rebaselined_set = set(rebaselined_diff.keys()) + # The name of the device corresponds to the name of the folder # we are in. device_name = os.path.basename(root) @@ -280,7 +301,18 @@ class ExpectationsManager: for image_name, hashes in expected_diff.iteritems(): self._expectations.append( GMInstance(device_name, image_name, - hashes['old'], hashes['new'])) + hashes['old'], hashes['new'], + image_name in rebaselined_set)) + + def _load_skpdiff_output(self): + """Loads the results of skpdiff and annotates them with whether they + have already been rebaselined or not. The resulting data is store in + self.skpdiff_records.""" + self.skpdiff_records = None + with open(self._skpdiff_output_path, 'rb') as skpdiff_output_file: + self.skpdiff_records = json.load(skpdiff_output_file)['records'] + for record in self.skpdiff_records: + record['isRebaselined'] = self.image_map[record['baselinePath']][1].is_rebaselined def _download_expectation_images(self, expected_image_dir, actual_image_dir): @@ -346,23 +378,36 @@ class ExpectationsManager: # Write it out to disk using gm_json to keep the formatting consistent. gm_json.WriteToFile(expectations, json_path) - def use_hash_of(self, image_path): - """Determine the hash of the image at the path using the records, and - set it as the expected hash for its device and image config. + def commit_rebaselines(self, rebaselines): + """Sets the expected results file to use the hashes of the images in + the rebaselines list. If a expected result image is not in rebaselines + at all, the old hash will be used. - @param image_path The path of the image as it was stored in the output - of skpdiff_path + @param rebaselines A list of image paths to use the hash of. """ + # Reset all expectations to their old hashes because some of them may + # have been set to the new hash by a previous call to this function. + for expectation in self._expectations: + expectation.is_rebaselined = False + self._set_expected_hash(expectation.device_name, + expectation.image_name, + expectation.expected_hash) + + # Take all the images to rebaseline + for image_path in rebaselines: + # Get the metadata about the image at the path. + is_actual, expectation = self.image_map[image_path] - # Get the metadata about the image at the path. - is_actual, expectation = self.image_map[image_path] + expectation.is_rebaselined = is_actual + expectation_hash = expectation.actual_hash if is_actual else\ + expectation.expected_hash - expectation_hash = expectation.actual_hash if is_actual else\ - expectation.expected_hash + # Write out that image's hash directly to the expected results file. + self._set_expected_hash(expectation.device_name, + expectation.image_name, + expectation_hash) - # Write out that image's hash directly to the expected results file. - self._set_expected_hash(expectation.device_name, expectation.image_name, - expectation_hash) + self._load_skpdiff_output() class SkPDiffHandler(BaseHTTPServer.BaseHTTPRequestHandler): @@ -410,7 +455,15 @@ class SkPDiffHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.send_response(200) self.send_header('Content-type', MIME_TYPE_MAP['json']) self.end_headers() - self.wfile.write(self.server.skpdiff_output_json) + + # Add JSONP padding to the JSON because the web page expects it. It + # expects it because it was designed to run with or without a web + # server. Without a web server, the only way to load JSON is with + # JSONP. + skpdiff_records = self.server.expectations_manager.skpdiff_records + self.wfile.write('var SkPDiffRecords = ') + json.dump({'records': skpdiff_records}, self.wfile) + self.wfile.write(';') return # Attempt to send static asset files first. @@ -427,10 +480,11 @@ class SkPDiffHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.send_error(404) def do_POST(self): - if self.path == '/set_hash': + if self.path == '/commit_rebaselines': content_length = int(self.headers['Content-length']) request_data = json.loads(self.rfile.read(content_length)) - self.server.expectations_manager.use_hash_of(request_data['path']) + rebaselines = request_data['rebaselines'] + self.server.expectations_manager.commit_rebaselines(rebaselines) self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() @@ -442,23 +496,11 @@ class SkPDiffHandler(BaseHTTPServer.BaseHTTPRequestHandler): def run_server(expectations_manager, port=8080): - # Preload the skpdiff results file. This is so we can perform some - # processing on it. - skpdiff_output_json = '' - with open(expectations_manager.skpdiff_output_path, 'rb') as records_file: - skpdiff_output_json = records_file.read() - # It's important to parse the results file so that we can make a set of # images that the web page might request. - skpdiff_records = json.loads(skpdiff_output_json)['records'] + skpdiff_records = expectations_manager.skpdiff_records image_set = get_image_set_from_skpdiff(skpdiff_records) - # Add JSONP padding to the JSON because the web page expects it. It expects - # it because it was designed to run with or without a web server. Without a - # web server, the only way to load JSON is with JSONP. - skpdiff_output_json = ('var SkPDiffRecords = ' + - json.dumps({'records': skpdiff_records}) + ';') - # Do not bind to interfaces other than localhost because the server will # attempt to serve files relative to the root directory as a last resort # before 404ing. This means all of your files can be accessed from this @@ -466,7 +508,6 @@ def run_server(expectations_manager, port=8080): server_address = ('127.0.0.1', port) http_server = BaseHTTPServer.HTTPServer(server_address, SkPDiffHandler) http_server.image_set = image_set - http_server.skpdiff_output_json = skpdiff_output_json http_server.expectations_manager = expectations_manager print('Navigate thine browser to: http://{}:{}/'.format(*server_address)) http_server.serve_forever() diff --git a/tools/skpdiff/viewer.html b/tools/skpdiff/viewer.html index 46c5871ae1..f4b8fd7fc9 100644 --- a/tools/skpdiff/viewer.html +++ b/tools/skpdiff/viewer.html @@ -15,50 +15,63 @@ us to use a webserver. --> <script type="text/ng-template" id="/diff_list.html"> - <!-- Give a choice of how to order the differs --> - <div style="margin:8px"> - Show me the worst by metric: - <button ng-repeat="differ in differs" ng-click="setSortIndex($index)"> - <span class="result-{{ $index }}" style="padding-left:12px;"> </span> - {{ differ.title }} - </button> + <div ng-class="statusClass"> + <!-- The commit button --> + <div ng-show="isDynamic" class="commit"> + <button ng-click="commitRebaselines()">Commit</button> + </div> + <!-- Give a choice of how to order the differs --> + <div style="margin:8px"> + Show me the worst by metric: + <button ng-repeat="differ in differs" ng-click="setSortIndex($index)"> + <span class="result-{{ $index }}" style="padding-left:12px;"> </span> + {{ differ.title }} + </button> + </div> + <!-- Begin list of differences --> + <table> + <thead> + <tr> + <td ng-show="isDynamic">Rebaseline?</td> + <td>Expected Image</td> + <td>Actual Image</td> + <td>Results</td> + </tr> + </thead> + <tbody> + <!-- + Loops through every record and crates a row for it. This sorts based on the whichever + metric the user chose, and places a limit on the max number of records to show. + --> + <tr ng-repeat="record in records | orderBy:sortingDiffer | limitTo:500" + ng-class="{selected: record.isRebaselined}"> + <td ng-show="isDynamic"> + <input type="checkbox" + ng-model="record.isRebaselined" + ng-click="selectedRebaseline($index, $event)" + ng-class="{lastselected: lastSelectedIndex == $index}" /> + </td> + <td> + <swap-img left-src="{{ record.baselinePath }}" + right-src="{{ record.testPath }}" + side="left" + class="gm-image left-image" /></td> + <td> + <swap-img left-src="{{ record.baselinePath }}" + right-src="{{ record.testPath }}" + side="right" + class="gm-image right-image" /></td> + <td> + <div ng-repeat="diff in record.diffs" + ng-mouseover="highlight(diff.differName)" + class="result result-{{$index}}"> + <span class="result-button">{{ diff.result }}</span> + </div> + </td> + </tr> + </tbody> + </table> </div> - <!-- Begin list of differences --> - <table> - <thead> - <tr> - <td>Expected Image</td> - <td>Actual Image</td> - <td>Results</td> - </tr> - </thead> - <tbody> - <!-- - Loops through every record and crates a row for it. This sorts based on the whichever - metric the user chose, and places a limit on the max number of records to show. - --> - <tr ng-repeat="record in records | orderBy:sortingDiffer | limitTo:500" - class="{{ record.cssClasses }}"> - <td ng-click="setHashOf(record.baselinePath, record)"> - <swap-img left-src="{{ record.baselinePath }}" - right-src="{{ record.testPath }}" - side="left" - class="gm-image left-image" /></td> - <td ng-click="setHashOf(record.testPath, record)"> - <swap-img left-src="{{ record.baselinePath }}" - right-src="{{ record.testPath }}" - side="right" - class="gm-image right-image" /></td> - <td> - <div ng-repeat="diff in record.diffs" - ng-mouseover="highlight(diff.differName)" - class="result result-{{$index}}"> - <span class="result-button">{{ diff.result }}</span> - </div> - </td> - </tr> - </tbody> - </table> </script> <!-- Whatever template is used is rendered in the following div --> <div ng-view></div> diff --git a/tools/skpdiff/viewer_style.css b/tools/skpdiff/viewer_style.css index 81b4f906c9..e172667061 100644 --- a/tools/skpdiff/viewer_style.css +++ b/tools/skpdiff/viewer_style.css @@ -19,6 +19,60 @@ thead > tr > td { border: none; } +button { + font-family: Verdana; + font-weight: 900; +} + +input[type="checkbox"] { + -webkit-appearance: none; + -moz-appearance: none; + width: 20px; + height:20px; + padding: 2px; + background: #EEE; + border-radius: 2px; + box-shadow: inset 0 0 8px -2px black; +} + +input[type="checkbox"]:checked { + background: #a9db80; + background: -webkit-linear-gradient(top, #00b7ea 0%,#009ec3 100%); +} + +input[type="checkbox"]:active { + background: #a9db80; + background: -webkit-linear-gradient(top, #009ec3 0%,#00b7ea 100%); +} + +input[type="checkbox"].lastselected { + padding: 0; + border: 2px solid #009ec3; + box-shadow: inset 0 0 8px -2px black, 0 0 6px black; +} + +.commit { + position: absolute; + top: 8px; + right: 8px; +} + +.commit > button { + border: 1px solid #00687F; + border-radius: 4px; + padding: 8px; + color: white; + text-shadow: 0 0 4px black; + box-shadow: 0 0 8px black; + background: #a9db80; + background: -webkit-linear-gradient(top, #a9db80 0%,#96c56f 100%); +} + +.commit > button:active { + background: #96c56f; + background: -webkit-linear-gradient(top, #96c56f 0%,#a9db80 100%); +} + .gm-image { border: 1px dotted black; } @@ -27,6 +81,10 @@ thead > tr > td { border: 1px dashed black; } +.selected { + background: #ffff88; +} + .left-image { float: right; } @@ -68,13 +126,13 @@ thead > tr > td { .result { padding: 8px; cursor: default; - opacity: 0.7; + -webkit-filter: grayscale(30%) } .result:hover { border: 2px dotted #DDD; padding: 6px; - opacity: 1.0; + -webkit-filter: grayscale(0) } .result-0 { |