OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 # -*- coding: utf-8 -*- | 2 # -*- coding: utf-8 -*- |
3 | 3 |
4 from __future__ import print_function | 4 from __future__ import print_function |
| 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_image(image_name, image_path, hash_val): |
| 95 """Download the gm result into the given path. |
| 96 |
| 97 @param image_name The GM file name, for example imageblur_gpu.png. |
| 98 @param image_path Path to place the image. |
| 99 @param hash_val The hash value of the image. |
| 100 """ |
| 101 |
| 102 # Seperate the test name from a image name |
| 103 image_match = IMAGE_FILENAME_RE.match(image_name) |
| 104 test_name = image_match.group(1) |
| 105 |
| 106 # Calculate the URL of the requested image |
| 107 image_url = gm_json.CreateGmActualUrl( |
| 108 test_name, gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5, hash_val) |
| 109 |
| 110 # Download the image as requested |
| 111 download_file(image_url, image_path) |
| 112 |
| 113 |
| 114 def download_changed_images(expectations_dir, expected_name, updated_name, |
| 115 expected_image_dir, actual_image_dir): |
| 116 |
| 117 """Download the expected and actual GMs that changed into the given paths. |
| 118 Determining what changed will be done by comparing each expected_name JSON |
| 119 results file to its corresponding updated_name JSON results file if it |
| 120 exists. |
| 121 |
| 122 @param expectations_dir The directory to traverse for results files. This |
| 123 should resmble expectations/gm in the Skia trunk. |
| 124 @param expected_name The name of the expected result files. These are |
| 125 in the format of expected-results.json. |
| 126 @param updated_name The name of the updated expected result files. |
| 127 Normally this matches --expectations-filename-output for the |
| 128 rebaseline.py tool. |
| 129 @param expected_image_dir The directory to place downloaded expected images |
| 130 into. |
| 131 @param actual_image_dir The directory to place downloaded actual images |
| 132 into. |
| 133 """ |
| 134 |
| 135 differ = jsondiff.GMDiffer() |
| 136 |
| 137 # Look through expectations for hashes that changed |
| 138 for root, dirs, files in os.walk(expectations_dir): |
| 139 for expectation_file in files: |
| 140 # There are many files in the expectations directory. We only care |
| 141 # about expected results. |
| 142 if expectation_file != expected_name: |
| 143 continue |
| 144 |
| 145 # Get the name of the results file, and be sure there is an updated |
| 146 # result to compare against. If there is not, there is no point in |
| 147 # diffing this device. |
| 148 expected_file_path = os.path.join(root, expected_name) |
| 149 updated_file_path = os.path.join(root, updated_name) |
| 150 if not os.path.isfile(updated_file_path): |
| 151 continue |
| 152 |
| 153 # Find all expectations that did not match. |
| 154 expected_diff = differ.GenerateDiffDict(expected_file_path, |
| 155 updated_file_path) |
| 156 |
| 157 # The name of the device corresponds to the name of the folder we |
| 158 # are in. |
| 159 device_name = os.path.basename(root) |
| 160 |
| 161 # Create name prefixes to store the devices old and new GM results |
| 162 expected_image_prefix = os.path.join(expected_image_dir, |
| 163 device_name) + '-' |
| 164 |
| 165 actual_image_prefix = os.path.join(actual_image_dir, |
| 166 device_name) + '-' |
| 167 |
| 168 # Download each image that had a differing result |
| 169 for image_name, hashes in expected_diff.iteritems(): |
| 170 print('Downloading', image_name, 'for device', device_name) |
| 171 download_gm_image(image_name, |
| 172 expected_image_prefix + image_name, |
| 173 hashes['old']) |
| 174 download_gm_image(image_name, |
| 175 actual_image_prefix + image_name, |
| 176 hashes['new']) |
| 177 |
| 178 |
| 179 def get_image_set_from_skpdiff(skpdiff_records): |
| 180 """Get the set of all images references in the given records. |
| 181 |
| 182 @param skpdiff_records An array of records, which are dictionary objects. |
| 183 """ |
| 184 expected_set = frozenset([r['baselinePath'] for r in skpdiff_records]) |
| 185 actual_set = frozenset([r['testPath'] for r in skpdiff_records]) |
| 186 return expected_set | actual_set |
| 187 |
| 188 |
20 class SkPDiffHandler(BaseHTTPServer.BaseHTTPRequestHandler): | 189 class SkPDiffHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
21 def send_file(self, file_path): | 190 def send_file(self, file_path): |
22 # Grab the extension if there is one | 191 # Grab the extension if there is one |
23 extension = os.path.splitext(file_path)[1] | 192 extension = os.path.splitext(file_path)[1] |
24 if len(extension) >= 1: | 193 if len(extension) >= 1: |
25 extension = extension[1:] | 194 extension = extension[1:] |
26 | 195 |
27 # Determine the MIME type of the file from its extension | 196 # Determine the MIME type of the file from its extension |
28 mime_type = MIME_TYPE_MAP.get(extension, MIME_TYPE_MAP['']) | 197 mime_type = MIME_TYPE_MAP.get(extension, MIME_TYPE_MAP['']) |
29 | 198 |
30 # Open the file and send it over HTTP | 199 # Open the file and send it over HTTP |
31 sending_file = open(file_path, 'rb') | 200 if os.path.isfile(file_path): |
32 self.send_response(200) | 201 with open(file_path, 'rb') as sending_file: |
33 self.send_header('Content-type', mime_type) | 202 self.send_response(200) |
34 self.end_headers() | 203 self.send_header('Content-type', mime_type) |
35 self.wfile.write(sending_file.read()) | 204 self.end_headers() |
36 sending_file.close() | 205 self.wfile.write(sending_file.read()) |
| 206 else: |
| 207 self.send_error(404) |
37 | 208 |
38 def serve_if_in_dir(self, dir_path, file_path): | 209 def serve_if_in_dir(self, dir_path, file_path): |
39 # Determine if the file exists relative to the given dir_path AND exists | 210 # 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 | 211 # under the dir_path. This is to prevent accidentally serving files |
41 # outside the directory intended using symlinks, or '../'. | 212 # outside the directory intended using symlinks, or '../'. |
42 real_path = os.path.normpath(os.path.join(dir_path, file_path)) | 213 real_path = os.path.normpath(os.path.join(dir_path, file_path)) |
43 print(repr(real_path)) | 214 print(repr(real_path)) |
44 if os.path.commonprefix([real_path, dir_path]) == dir_path: | 215 if os.path.commonprefix([real_path, dir_path]) == dir_path: |
45 if os.path.isfile(real_path): | 216 if os.path.isfile(real_path): |
46 self.send_file(real_path) | 217 self.send_file(real_path) |
47 return True | 218 return True |
48 return False | 219 return False |
49 | 220 |
50 def do_GET(self): | 221 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' | 222 # Simple rewrite rule of the root path to 'viewer.html' |
55 if self.path == '' or self.path == '/': | 223 if self.path == '' or self.path == '/': |
56 self.path = '/viewer.html' | 224 self.path = '/viewer.html' |
57 | 225 |
58 # The [1:] chops off the leading '/' | 226 # The [1:] chops off the leading '/' |
59 file_path = self.path[1:] | 227 file_path = self.path[1:] |
60 | 228 |
61 # Attempt to send static asset files first | 229 # Handle skpdiff_output.json manually because it is was processed by the |
62 if self.serve_if_in_dir(script_dir, file_path): | 230 # server when it was started and does not exist as a file. |
| 231 if file_path == 'skpdiff_output.json': |
| 232 self.send_response(200) |
| 233 self.send_header('Content-type', MIME_TYPE_MAP['json']) |
| 234 self.end_headers() |
| 235 self.wfile.write(self.server.skpdiff_output_json) |
63 return | 236 return |
64 | 237 |
65 # WARNING: Using the root is a big ol' hack. Incredibly insecure. Only | 238 # Attempt to send static asset files first. |
66 # allow serving to localhost unless you want the network to be able to | 239 if self.serve_if_in_dir(SCRIPT_DIR, file_path): |
67 # see ALL of the files you can. | 240 return |
68 # Attempt to send gm image files | 241 |
69 if self.serve_if_in_dir('/', file_path): | 242 # WARNING: Serving any file the user wants is incredibly insecure. Its |
| 243 # redeeming quality is that we only serve gm files on a white list. |
| 244 if self.path in self.server.image_set: |
| 245 self.send_file(self.path) |
70 return | 246 return |
71 | 247 |
72 # If no file to send was found, just give the standard 404 | 248 # If no file to send was found, just give the standard 404 |
73 self.send_error(404) | 249 self.send_error(404) |
74 | 250 |
75 | 251 |
76 def main(): | 252 def run_server(skpdiff_output_path, port=8080): |
| 253 # Preload the skpdiff results file. This is so we can perform some |
| 254 # processing on it. |
| 255 skpdiff_output_json = '' |
| 256 with open(skpdiff_output_path, 'rb') as records_file: |
| 257 skpdiff_output_json = records_file.read() |
| 258 |
| 259 # It's important to parse the results file so that we can make a set of |
| 260 # images that the web page might request. |
| 261 skpdiff_records = json.loads(skpdiff_output_json)['records'] |
| 262 image_set = get_image_set_from_skpdiff(skpdiff_records) |
| 263 |
| 264 # Add JSONP padding to the JSON because the web page expects it. It expects |
| 265 # it because it was designed to run with or without a web server. Without a |
| 266 # web server, the only way to load JSON is with JSONP. |
| 267 skpdiff_output_json = 'var SkPDiffRecords = ' + skpdiff_output_json |
| 268 |
77 # Do not bind to interfaces other than localhost because the server will | 269 # 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 | 270 # 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 | 271 # 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. | 272 # server, so DO NOT let this server listen to anything but localhost. |
81 server_address = ('127.0.0.1', 8080) | 273 server_address = ('127.0.0.1', port) |
82 http_server = BaseHTTPServer.HTTPServer(server_address, SkPDiffHandler) | 274 http_server = BaseHTTPServer.HTTPServer(server_address, SkPDiffHandler) |
83 print('Navigate thine browser to: {}:{}'.format(*server_address)) | 275 http_server.image_set = image_set |
| 276 http_server.skpdiff_output_json = skpdiff_output_json |
| 277 print('Navigate thine browser to: http://{}:{}'.format(*server_address)) |
84 http_server.serve_forever() | 278 http_server.serve_forever() |
85 | 279 |
| 280 |
| 281 def main(): |
| 282 parser = argparse.ArgumentParser() |
| 283 parser.add_argument('--port', '-p', metavar='PORT', |
| 284 type=int, |
| 285 default=8080, |
| 286 help='port to bind the server to; ' + |
| 287 'defaults to %(default)s', |
| 288 ) |
| 289 |
| 290 parser.add_argument('--expectations-dir', metavar='EXPECTATIONS_DIR', |
| 291 default=DEFAULT_GM_EXPECTATIONS_DIR, |
| 292 help='path to the gm expectations; ' + |
| 293 'defaults to %(default)s' |
| 294 ) |
| 295 |
| 296 parser.add_argument('--expected', |
| 297 metavar='EXPECTATIONS_FILE_NAME', |
| 298 default='expected-results.json', |
| 299 help='the file name of the expectations JSON; ' + |
| 300 'defaults to %(default)s' |
| 301 ) |
| 302 |
| 303 parser.add_argument('--updated', |
| 304 metavar='UPDATED_FILE_NAME', |
| 305 default='updated-results.json', |
| 306 help='the file name of the updated expectations JSON;' + |
| 307 ' defaults to %(default)s' |
| 308 ) |
| 309 |
| 310 parser.add_argument('--skpdiff-path', metavar='SKPDIFF_PATH', |
| 311 default=None, |
| 312 help='the path to the skpdiff binary to use; ' + |
| 313 'defaults to out/Release/skpdiff or out/Default/skpdiff' |
| 314 ) |
| 315 |
| 316 args = vars(parser.parse_args()) # Convert args into a python dict |
| 317 |
| 318 # Make sure we have access to an skpdiff binary |
| 319 skpdiff_path = get_skpdiff_path(args['skpdiff_path']) |
| 320 if skpdiff_path is None: |
| 321 sys.exit(1) |
| 322 |
| 323 # Create a temporary file tree that makes sense for skpdiff.to operate on |
| 324 image_output_dir = tempfile.mkdtemp('skpdiff') |
| 325 expected_image_dir = os.path.join(image_output_dir, 'expected') |
| 326 actual_image_dir = os.path.join(image_output_dir, 'actual') |
| 327 os.mkdir(expected_image_dir) |
| 328 os.mkdir(actual_image_dir) |
| 329 |
| 330 # Print out the paths of things for easier debugging |
| 331 print('script dir :', SCRIPT_DIR) |
| 332 print('tools dir :', TOOLS_DIR) |
| 333 print('root dir :', SKIA_ROOT_DIR) |
| 334 print('expectations dir :', args['expectations_dir']) |
| 335 print('skpdiff path :', skpdiff_path) |
| 336 print('tmp dir :', image_output_dir) |
| 337 print('expected image dir :', expected_image_dir) |
| 338 print('actual image dir :', actual_image_dir) |
| 339 |
| 340 # Download expected and actual images that differed into the temporary file |
| 341 # tree. |
| 342 download_changed_images(args['expectations_dir'], |
| 343 args['expected'], args['updated'], |
| 344 expected_image_dir, actual_image_dir) |
| 345 |
| 346 # Invoke skpdiff with our downloaded images and place its results in the |
| 347 # temporary directory. |
| 348 skpdiff_output_path = os.path.join(image_output_dir, 'skpdiff_output.json') |
| 349 skpdiff_cmd = SKPDIFF_INVOKE_FORMAT.format(skpdiff_path, |
| 350 skpdiff_output_path, |
| 351 expected_image_dir, |
| 352 actual_image_dir) |
| 353 os.system(skpdiff_cmd) |
| 354 |
| 355 run_server(skpdiff_output_path, port=args['port']) |
| 356 |
86 if __name__ == '__main__': | 357 if __name__ == '__main__': |
87 main() | 358 main() |
OLD | NEW |