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 |