blob: 79ffc839e25064e97d3c117f1879b999b9ffcae5 [file] [log] [blame]
zachr@google.com48b88912013-07-25 19:49:17 +00001#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4from __future__ import print_function
zachr@google.com785ef5d2013-07-26 19:29:39 +00005import argparse
zachr@google.com48b88912013-07-25 19:49:17 +00006import BaseHTTPServer
zachr@google.com785ef5d2013-07-26 19:29:39 +00007import json
zachr@google.com48b88912013-07-25 19:49:17 +00008import os
9import os.path
zachr@google.com785ef5d2013-07-26 19:29:39 +000010import re
11import sys
12import tempfile
13import urllib2
14
15# Grab the script path because that is where all the static assets are
16SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
17
18# Find the tools directory for python imports
19TOOLS_DIR = os.path.dirname(SCRIPT_DIR)
20
21# Find the root of the skia trunk for finding skpdiff binary
22SKIA_ROOT_DIR = os.path.dirname(TOOLS_DIR)
23
24# Find the default location of gm expectations
25DEFAULT_GM_EXPECTATIONS_DIR = os.path.join(SKIA_ROOT_DIR, 'expectations', 'gm')
26
27# Imports from within Skia
28if TOOLS_DIR not in sys.path:
29 sys.path.append(TOOLS_DIR)
30GM_DIR = os.path.join(SKIA_ROOT_DIR, 'gm')
31if GM_DIR not in sys.path:
32 sys.path.append(GM_DIR)
33import gm_json
34import jsondiff
zachr@google.com48b88912013-07-25 19:49:17 +000035
36# A simple dictionary of file name extensions to MIME types. The empty string
37# entry is used as the default when no extension was given or if the extension
38# has no entry in this dictionary.
39MIME_TYPE_MAP = {'': 'application/octet-stream',
40 'html': 'text/html',
41 'css': 'text/css',
42 'png': 'image/png',
zachr@google.com785ef5d2013-07-26 19:29:39 +000043 'js': 'application/javascript',
44 'json': 'application/json'
zachr@google.com48b88912013-07-25 19:49:17 +000045 }
46
47
zachr@google.com785ef5d2013-07-26 19:29:39 +000048IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN)
49
50SKPDIFF_INVOKE_FORMAT = '{} --jsonp=false -o {} -f {} {}'
51
52
53def 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
87def 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
94def 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
114def 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
179def 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
zachr@google.com48b88912013-07-25 19:49:17 +0000189class SkPDiffHandler(BaseHTTPServer.BaseHTTPRequestHandler):
190 def send_file(self, file_path):
191 # Grab the extension if there is one
192 extension = os.path.splitext(file_path)[1]
193 if len(extension) >= 1:
194 extension = extension[1:]
195
196 # Determine the MIME type of the file from its extension
197 mime_type = MIME_TYPE_MAP.get(extension, MIME_TYPE_MAP[''])
198
199 # Open the file and send it over HTTP
zachr@google.com785ef5d2013-07-26 19:29:39 +0000200 if os.path.isfile(file_path):
201 with open(file_path, 'rb') as sending_file:
202 self.send_response(200)
203 self.send_header('Content-type', mime_type)
204 self.end_headers()
205 self.wfile.write(sending_file.read())
206 else:
207 self.send_error(404)
zachr@google.com48b88912013-07-25 19:49:17 +0000208
209 def serve_if_in_dir(self, dir_path, file_path):
210 # Determine if the file exists relative to the given dir_path AND exists
211 # under the dir_path. This is to prevent accidentally serving files
212 # outside the directory intended using symlinks, or '../'.
213 real_path = os.path.normpath(os.path.join(dir_path, file_path))
214 print(repr(real_path))
215 if os.path.commonprefix([real_path, dir_path]) == dir_path:
216 if os.path.isfile(real_path):
217 self.send_file(real_path)
218 return True
219 return False
220
221 def do_GET(self):
zachr@google.com48b88912013-07-25 19:49:17 +0000222 # Simple rewrite rule of the root path to 'viewer.html'
223 if self.path == '' or self.path == '/':
224 self.path = '/viewer.html'
225
226 # The [1:] chops off the leading '/'
227 file_path = self.path[1:]
228
zachr@google.com785ef5d2013-07-26 19:29:39 +0000229 # Handle skpdiff_output.json manually because it is was processed by the
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)
zachr@google.com48b88912013-07-25 19:49:17 +0000236 return
237
zachr@google.com785ef5d2013-07-26 19:29:39 +0000238 # Attempt to send static asset files first.
239 if self.serve_if_in_dir(SCRIPT_DIR, file_path):
240 return
241
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)
zachr@google.com48b88912013-07-25 19:49:17 +0000246 return
247
248 # If no file to send was found, just give the standard 404
249 self.send_error(404)
250
251
zachr@google.com785ef5d2013-07-26 19:29:39 +0000252def 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
zachr@google.com48b88912013-07-25 19:49:17 +0000269 # Do not bind to interfaces other than localhost because the server will
270 # attempt to serve files relative to the root directory as a last resort
271 # before 404ing. This means all of your files can be accessed from this
272 # server, so DO NOT let this server listen to anything but localhost.
zachr@google.com785ef5d2013-07-26 19:29:39 +0000273 server_address = ('127.0.0.1', port)
zachr@google.com48b88912013-07-25 19:49:17 +0000274 http_server = BaseHTTPServer.HTTPServer(server_address, SkPDiffHandler)
zachr@google.com785ef5d2013-07-26 19:29:39 +0000275 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))
zachr@google.com48b88912013-07-25 19:49:17 +0000278 http_server.serve_forever()
279
zachr@google.com785ef5d2013-07-26 19:29:39 +0000280
281def 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
zachr@google.com48b88912013-07-25 19:49:17 +0000357if __name__ == '__main__':
358 main()