Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(10)

Side by Side Diff: tools/skpdiff/skpdiff_server.py

Issue 20527002: process gm expectation violations (Closed) Base URL: https://skia.googlecode.com/svn/trunk
Patch Set: Created 7 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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()
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698