Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
|
epoger
2013/07/26 16:15:57
Just to make sure I understand: the core change in
Zach Reizner
2013/07/26 18:00:40
Yep
| |
| 2 # -*- coding: utf-8 -*- | 2 # -*- coding: utf-8 -*- |
|
epoger
2013/07/26 16:15:57
Be sure to mark this file as executable, if you di
Zach Reizner
2013/07/26 18:00:40
Done.
| |
| 3 | 3 |
|
epoger
2013/07/26 16:15:57
Here's an alternate set of steps to test this code
| |
| 4 from __future__ import print_function | 4 from __future__ import print_function |
|
epoger
2013/07/26 16:15:57
I guess this is more of a viewer.html issue, but w
Zach Reizner
2013/07/26 18:00:40
Weird. Clicking is not supposed to do anything. Ho
| |
| 5 import argparse | |
| 5 import BaseHTTPServer | 6 import BaseHTTPServer |
| 7 import json | |
| 6 import os | 8 import os |
| 7 import os.path | 9 import os.path |
| 10 import re | |
| 11 import sys | |
| 12 import tempfile | |
| 13 import urllib2 | |
| 14 | |
| 15 # Grab the script path because that is where all the static assets are | |
| 16 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| 17 | |
| 18 # Find the tools directory for python imports | |
| 19 TOOLS_DIR = os.path.dirname(SCRIPT_DIR) | |
| 20 | |
| 21 # Find the root of the skia trunk for finding skpdiff binary | |
| 22 SKIA_ROOT_DIR = os.path.dirname(TOOLS_DIR) | |
| 23 | |
| 24 # Find the default location of gm expectations | |
| 25 DEFAULT_GM_EXPECTATIONS_DIR = os.path.join(SKIA_ROOT_DIR, 'expectations', 'gm') | |
| 26 | |
| 27 # Imports from within Skia | |
| 28 if TOOLS_DIR not in sys.path: | |
| 29 sys.path.append(TOOLS_DIR) | |
| 30 GM_DIR = os.path.join(SKIA_ROOT_DIR, 'gm') | |
| 31 if GM_DIR not in sys.path: | |
| 32 sys.path.append(GM_DIR) | |
| 33 import gm_json | |
| 34 import jsondiff | |
| 8 | 35 |
| 9 # A simple dictionary of file name extensions to MIME types. The empty string | 36 # A simple dictionary of file name extensions to MIME types. The empty string |
| 10 # entry is used as the default when no extension was given or if the extension | 37 # entry is used as the default when no extension was given or if the extension |
| 11 # has no entry in this dictionary. | 38 # has no entry in this dictionary. |
| 12 MIME_TYPE_MAP = {'': 'application/octet-stream', | 39 MIME_TYPE_MAP = {'': 'application/octet-stream', |
| 13 'html': 'text/html', | 40 'html': 'text/html', |
| 14 'css': 'text/css', | 41 'css': 'text/css', |
| 15 'png': 'image/png', | 42 'png': 'image/png', |
| 16 'js': 'application/javascript' | 43 'js': 'application/javascript', |
| 44 'json': 'application/json' | |
| 17 } | 45 } |
| 18 | 46 |
| 19 | 47 |
| 48 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) | |
| 49 | |
| 50 SKPDIFF_INVOKE_FORMAT = '{} --jsonp=false -o {} -f {} {}' | |
| 51 | |
| 52 | |
| 53 def get_skpdiff_path(user_path=None): | |
| 54 """Find the skpdiff binary. | |
| 55 | |
| 56 @param user_path If none, searches in Release and Debug out directories of | |
| 57 the skia root. If set, checks that the path is a real file and | |
| 58 returns it. | |
| 59 """ | |
| 60 skpdiff_path = None | |
| 61 possible_paths = [] | |
| 62 | |
| 63 # Use the user given path, or try out some good default paths. | |
| 64 if user_path: | |
| 65 possible_paths.append(user_path) | |
| 66 else: | |
| 67 possible_paths.append(os.path.join(SKIA_ROOT_DIR, 'out', | |
| 68 'Release', 'skpdiff')) | |
| 69 possible_paths.append(os.path.join(SKIA_ROOT_DIR, 'out', | |
| 70 'Debug', 'skpdiff')) | |
| 71 # Use the first path that actually points to the binary | |
| 72 for possible_path in possible_paths: | |
| 73 if os.path.isfile(possible_path): | |
| 74 skpdiff_path = possible_path | |
| 75 break | |
| 76 | |
| 77 # If skpdiff was not found, print out diagnostic info for the user. | |
| 78 if skpdiff_path is None: | |
| 79 print('Could not find skpdiff binary. Either build it into the ' + | |
| 80 'default directory, or specify the path on the command line.') | |
| 81 print('skpdiff paths tried:') | |
| 82 for possible_path in possible_paths: | |
| 83 print(' ', possible_path) | |
| 84 return skpdiff_path | |
| 85 | |
| 86 | |
| 87 def download_file(url, output_path): | |
| 88 """Download the file at url and place it in output_path""" | |
| 89 reader = urllib2.urlopen(url) | |
| 90 with open(output_path, 'wb') as writer: | |
| 91 writer.write(reader.read()) | |
| 92 | |
| 93 | |
| 94 def download_gm_images(image_name, expected_image_path, actual_image_path, | |
| 95 old_hash, new_hash): | |
|
epoger
2013/07/26 16:15:57
I think it would be clearer if you made this funct
Zach Reizner
2013/07/26 18:00:40
Done.
| |
| 96 """Download the expected and actual results into the given paths. | |
| 97 | |
| 98 @param image_name The GM file name, for example imageblur_gpu.png. | |
| 99 @param expected_image_path Path to place the expected image. | |
| 100 @param actual_image_path Path to place the actual image. | |
| 101 @param old_hash The hash value of the expected image. | |
| 102 @param new_hash The hash value of the actual image. | |
| 103 """ | |
| 104 | |
| 105 # Seperate the image name into a GM name and syffix | |
|
epoger
2013/07/26 16:15:57
More like...
# Extract the test name (e.g. "image
Zach Reizner
2013/07/26 18:00:40
Done.
| |
| 106 image_match = IMAGE_FILENAME_RE.match(image_name) | |
| 107 gm_name = image_match.group(1) | |
| 108 | |
| 109 # Calculate the URLs of the requested images | |
| 110 expected_image_url = gm_json.CreateGmActualUrl( | |
| 111 gm_name, gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5, old_hash) | |
| 112 | |
| 113 actual_image_url = gm_json.CreateGmActualUrl( | |
| 114 gm_name, gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5, new_hash) | |
| 115 | |
| 116 # Download the images as requested | |
| 117 download_file(expected_image_url, expected_image_path) | |
| 118 download_file(actual_image_url, actual_image_path) | |
| 119 | |
| 120 | |
| 121 def download_actuals(expectations_dir, expected_name, updated_name, | |
|
epoger
2013/07/26 16:15:57
It's downloading both expected and actual images,
Zach Reizner
2013/07/26 18:00:40
Done.
epoger
2013/07/26 18:36:57
Ah, I get the "updated" part of the name (it's a b
| |
| 122 expected_image_dir, actual_image_dir): | |
| 123 | |
| 124 """Download the expected and actual GMs that changed into the given paths. | |
| 125 Determining what changed will be done by comparing each expected_name JSON | |
| 126 results file to its corresponding updated_name JSON results file if it | |
| 127 exists. | |
| 128 | |
| 129 @param expectations_dir The directory to traverse for results files. This | |
| 130 should resmble expectations/gm in the Skia trunk. | |
| 131 @param expected_name The name of the expected result files. These are | |
| 132 in the format of expected-results.json. | |
| 133 @param updated_name The name of the updated expected result files. | |
| 134 Normally this matches --expectations-filename-output for the | |
| 135 rebaseline.py tool. | |
| 136 @param expected_image_dir The directory to place downloaded expected images | |
| 137 into. | |
| 138 @param actual_image_dir The directory to place downloaded actual images | |
| 139 into. | |
| 140 """ | |
| 141 | |
| 142 differ = jsondiff.GMDiffer() | |
| 143 | |
| 144 # Look through expectations for hashes that changed | |
| 145 for root, dirs, files in os.walk(expectations_dir): | |
| 146 for expectation_file in files: | |
| 147 # There are many files in the expectations directory. We only care | |
| 148 # about expected results. | |
| 149 if expectation_file != expected_name: | |
| 150 continue | |
| 151 | |
| 152 # Get the name of the results file, and be sure there is an updated | |
| 153 # result to compare against. If there is not, there is no point in | |
| 154 # diffing this device. | |
| 155 expected_file_path = os.path.join(root, expected_name) | |
| 156 updated_file_path = os.path.join(root, updated_name) | |
| 157 if not os.path.isfile(updated_file_path): | |
| 158 continue | |
| 159 | |
| 160 # Find all expectations that did not match. | |
| 161 expected_diff = differ.GenerateDiffDict(expected_file_path, | |
| 162 updated_file_path) | |
| 163 | |
| 164 # The name of the device corresponds to the name of the folder we | |
| 165 # are in. | |
| 166 device_name = os.path.basename(root) | |
| 167 | |
| 168 # Create name prefixes to store the devices old and new GM results | |
| 169 expected_image_prefix = os.path.join(expected_image_dir, | |
| 170 device_name) + '-' | |
| 171 | |
| 172 actual_image_prefix = os.path.join(actual_image_dir, | |
| 173 device_name) + '-' | |
| 174 | |
| 175 # Download each image that had a differing result | |
| 176 for image_name, hashes in expected_diff.iteritems(): | |
| 177 print('Downloading', image_name, 'for device', device_name) | |
| 178 download_gm_images(image_name, | |
| 179 expected_image_prefix + image_name, | |
| 180 actual_image_prefix + image_name, | |
| 181 hashes['old'], hashes['new']) | |
| 182 | |
| 183 | |
| 184 def get_image_set_from_skpdiff(skpdiff_records): | |
| 185 """Get the set of all images references in the given records. | |
| 186 | |
| 187 @param skpdiff_records An array of records, which are dictionary objects. | |
| 188 """ | |
| 189 expected_set = frozenset([r['baselinePath'] for r in skpdiff_records]) | |
| 190 actual_set = frozenset([r['testPath'] for r in skpdiff_records]) | |
| 191 return expected_set | actual_set | |
| 192 | |
| 193 | |
| 20 class SkPDiffHandler(BaseHTTPServer.BaseHTTPRequestHandler): | 194 class SkPDiffHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
| 21 def send_file(self, file_path): | 195 def send_file(self, file_path): |
| 22 # Grab the extension if there is one | 196 # Grab the extension if there is one |
| 23 extension = os.path.splitext(file_path)[1] | 197 extension = os.path.splitext(file_path)[1] |
| 24 if len(extension) >= 1: | 198 if len(extension) >= 1: |
| 25 extension = extension[1:] | 199 extension = extension[1:] |
| 26 | 200 |
| 27 # Determine the MIME type of the file from its extension | 201 # Determine the MIME type of the file from its extension |
| 28 mime_type = MIME_TYPE_MAP.get(extension, MIME_TYPE_MAP['']) | 202 mime_type = MIME_TYPE_MAP.get(extension, MIME_TYPE_MAP['']) |
| 29 | 203 |
| 30 # Open the file and send it over HTTP | 204 # Open the file and send it over HTTP |
| 31 sending_file = open(file_path, 'rb') | 205 if os.path.isfile(file_path): |
| 32 self.send_response(200) | 206 with open(file_path, 'rb') as sending_file: |
| 33 self.send_header('Content-type', mime_type) | 207 self.send_response(200) |
| 34 self.end_headers() | 208 self.send_header('Content-type', mime_type) |
| 35 self.wfile.write(sending_file.read()) | 209 self.end_headers() |
| 36 sending_file.close() | 210 self.wfile.write(sending_file.read()) |
| 211 else: | |
| 212 self.send_error(404) | |
| 37 | 213 |
| 38 def serve_if_in_dir(self, dir_path, file_path): | 214 def serve_if_in_dir(self, dir_path, file_path): |
| 39 # Determine if the file exists relative to the given dir_path AND exists | 215 # Determine if the file exists relative to the given dir_path AND exists |
| 40 # under the dir_path. This is to prevent accidentally serving files | 216 # under the dir_path. This is to prevent accidentally serving files |
| 41 # outside the directory intended using symlinks, or '../'. | 217 # outside the directory intended using symlinks, or '../'. |
| 42 real_path = os.path.normpath(os.path.join(dir_path, file_path)) | 218 real_path = os.path.normpath(os.path.join(dir_path, file_path)) |
| 43 print(repr(real_path)) | 219 print(repr(real_path)) |
| 44 if os.path.commonprefix([real_path, dir_path]) == dir_path: | 220 if os.path.commonprefix([real_path, dir_path]) == dir_path: |
| 45 if os.path.isfile(real_path): | 221 if os.path.isfile(real_path): |
| 46 self.send_file(real_path) | 222 self.send_file(real_path) |
| 47 return True | 223 return True |
| 48 return False | 224 return False |
| 49 | 225 |
| 50 def do_GET(self): | 226 def do_GET(self): |
| 51 # Grab the script path because that is where all the static assets are | |
| 52 script_dir = os.path.dirname(os.path.abspath(__file__)) | |
| 53 | |
| 54 # Simple rewrite rule of the root path to 'viewer.html' | 227 # Simple rewrite rule of the root path to 'viewer.html' |
| 55 if self.path == '' or self.path == '/': | 228 if self.path == '' or self.path == '/': |
| 56 self.path = '/viewer.html' | 229 self.path = '/viewer.html' |
| 57 | 230 |
| 58 # The [1:] chops off the leading '/' | 231 # The [1:] chops off the leading '/' |
| 59 file_path = self.path[1:] | 232 file_path = self.path[1:] |
| 60 | 233 |
| 61 # Attempt to send static asset files first | 234 # Handle skpdiff_output.json manually because it is loaded manually into |
|
epoger
2013/07/26 16:15:57
Is it really necessary to handle this specially?
| |
| 62 if self.serve_if_in_dir(script_dir, file_path): | 235 # memory. |
| 236 if file_path == 'skpdiff_output.json': | |
| 237 self.send_response(200) | |
| 238 self.send_header('Content-type', MIME_TYPE_MAP['json']) | |
| 239 self.end_headers() | |
| 240 self.wfile.write(self.server.skpdiff_output_json) | |
| 63 return | 241 return |
| 64 | 242 |
| 65 # WARNING: Using the root is a big ol' hack. Incredibly insecure. Only | 243 # Attempt to send static asset files first. |
| 66 # allow serving to localhost unless you want the network to be able to | 244 if self.serve_if_in_dir(SCRIPT_DIR, file_path): |
| 67 # see ALL of the files you can. | 245 return |
| 68 # Attempt to send gm image files | 246 |
| 69 if self.serve_if_in_dir('/', file_path): | 247 # WARNING: Serving any file the user wants is incredibly insecure. Its |
| 248 # redeeming quality is that we only serve gm files on a white list. | |
| 249 if self.path in self.server.image_set: | |
| 250 self.send_file(self.path) | |
| 70 return | 251 return |
| 71 | 252 |
| 72 # If no file to send was found, just give the standard 404 | 253 # If no file to send was found, just give the standard 404 |
| 73 self.send_error(404) | 254 self.send_error(404) |
| 74 | 255 |
| 75 | 256 |
| 76 def main(): | 257 def run_server(skpdiff_output_path, port=8080): |
| 258 # Preload the skpdiff results file. | |
| 259 skpdiff_output_json = '' | |
| 260 with open(skpdiff_output_path, 'rb') as records_file: | |
| 261 skpdiff_output_json = records_file.read() | |
| 262 | |
| 263 # It's important to parse the results file so that we can make a set of | |
| 264 # images that the web page might request. | |
| 265 skpdiff_records = json.loads(skpdiff_output_json)['records'] | |
| 266 image_set = get_image_set_from_skpdiff(skpdiff_records) | |
| 267 | |
| 268 # Add JSONP padding to the JSON because the web page expects it. | |
| 269 skpdiff_output_json = 'var SkPDiffRecords = ' + skpdiff_output_json | |
| 270 | |
| 77 # Do not bind to interfaces other than localhost because the server will | 271 # Do not bind to interfaces other than localhost because the server will |
| 78 # attempt to serve files relative to the root directory as a last resort | 272 # attempt to serve files relative to the root directory as a last resort |
| 79 # before 404ing. This means all of your files can be accessed from this | 273 # before 404ing. This means all of your files can be accessed from this |
| 80 # server, so DO NOT let this server listen to anything but localhost. | 274 # server, so DO NOT let this server listen to anything but localhost. |
| 81 server_address = ('127.0.0.1', 8080) | 275 server_address = ('127.0.0.1', port) |
| 82 http_server = BaseHTTPServer.HTTPServer(server_address, SkPDiffHandler) | 276 http_server = BaseHTTPServer.HTTPServer(server_address, SkPDiffHandler) |
| 277 http_server.image_set = image_set | |
| 278 http_server.skpdiff_output_json = skpdiff_output_json | |
| 83 print('Navigate thine browser to: {}:{}'.format(*server_address)) | 279 print('Navigate thine browser to: {}:{}'.format(*server_address)) |
|
epoger
2013/07/26 16:15:57
maybe change this line as follows, so the user can
| |
| 84 http_server.serve_forever() | 280 http_server.serve_forever() |
| 85 | 281 |
| 282 | |
| 283 def main(): | |
| 284 parser = argparse.ArgumentParser() | |
| 285 parser.add_argument('--port', '-p', metavar='PORT', | |
| 286 type=int, | |
| 287 default=8080, | |
| 288 help='port to bind the server to; ' + | |
| 289 'defaults to %(default)s', | |
| 290 ) | |
| 291 | |
| 292 parser.add_argument('--expectations-dir', metavar='EXPECTATIONS_DIR', | |
| 293 default=DEFAULT_GM_EXPECTATIONS_DIR, | |
| 294 help='path to the gm expectations; ' + | |
| 295 'defaults to %(default)s' | |
| 296 ) | |
| 297 | |
| 298 parser.add_argument('--expected', | |
| 299 metavar='EXPECTATIONS_FILE_NAME', | |
| 300 default='expected-results.json', | |
| 301 help='the file name of the expectations JSON; ' + | |
| 302 'defaults to %(default)s' | |
| 303 ) | |
| 304 | |
| 305 parser.add_argument('--updated', | |
| 306 metavar='UPDATED_FILE_NAME', | |
| 307 default='updated-results.json', | |
| 308 help='the file name of the updated expectations JSON;' + | |
| 309 ' defaults to %(default)s' | |
| 310 ) | |
| 311 | |
| 312 parser.add_argument('--skpdiff-path', metavar='SKPDIFF_PATH', | |
| 313 default=None, | |
| 314 help='the path to the skpdiff binary to use; ' + | |
| 315 'defaults to out/Release/skpdiff or out/Default/skpdiff' | |
| 316 ) | |
| 317 | |
| 318 args = vars(parser.parse_args()) # Convert args into a python dict | |
| 319 | |
| 320 # Make sure we have access to an skpdiff binary | |
| 321 skpdiff_path = get_skpdiff_path(args['skpdiff_path']) | |
| 322 if skpdiff_path is None: | |
| 323 sys.exit(1) | |
| 324 | |
| 325 # Create a temporary file tree that makes sense for skpdiff.to operate on | |
| 326 image_output_dir = tempfile.mkdtemp('skpdiff') | |
| 327 expected_image_dir = os.path.join(image_output_dir, 'expected') | |
| 328 actual_image_dir = os.path.join(image_output_dir, 'actual') | |
| 329 os.mkdir(expected_image_dir) | |
| 330 os.mkdir(actual_image_dir) | |
| 331 | |
| 332 # Print out the paths of things for easier debugging | |
| 333 print('script dir :', SCRIPT_DIR) | |
| 334 print('tools dir :', TOOLS_DIR) | |
| 335 print('root dir :', SKIA_ROOT_DIR) | |
| 336 print('expectations dir :', args['expectations_dir']) | |
| 337 print('skpdiff path :', skpdiff_path) | |
| 338 print('tmp dir :', image_output_dir) | |
| 339 print('expected image dir :', expected_image_dir) | |
| 340 print('actual image dir :', actual_image_dir) | |
| 341 | |
| 342 # Download expected and actual images that differed into the temporary file | |
| 343 # tree. | |
| 344 download_actuals(args['expectations_dir'], | |
| 345 args['expected'], args['updated'], | |
| 346 expected_image_dir, actual_image_dir) | |
| 347 | |
| 348 # Invoke skpdiff with our downloaded images and place its results in the | |
| 349 # temporary directory. | |
| 350 skpdiff_output_path = os.path.join(image_output_dir, 'skpdiff_output.json') | |
| 351 skpdiff_cmd = SKPDIFF_INVOKE_FORMAT.format(skpdiff_path, | |
| 352 skpdiff_output_path, | |
| 353 expected_image_dir, | |
| 354 actual_image_dir) | |
| 355 os.system(skpdiff_cmd) | |
| 356 | |
| 357 run_server(skpdiff_output_path, port=args['port']) | |
| 358 | |
| 86 if __name__ == '__main__': | 359 if __name__ == '__main__': |
| 87 main() | 360 main() |
| OLD | NEW |