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

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

Issue 20654006: download and rebaseline images using server (Closed) Base URL: https://skia.googlecode.com/svn/trunk
Patch Set: Created 7 years, 4 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
OLDNEW
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 argparse
6 import BaseHTTPServer 6 import BaseHTTPServer
7 import json 7 import json
8 import os 8 import os
9 import os.path 9 import os.path
10 import re 10 import re
11 import subprocess
11 import sys 12 import sys
12 import tempfile 13 import tempfile
13 import urllib2 14 import urllib2
14 15
15 # Grab the script path because that is where all the static assets are 16 # Grab the script path because that is where all the static assets are
16 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 17 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
17 18
18 # Find the tools directory for python imports 19 # Find the tools directory for python imports
19 TOOLS_DIR = os.path.dirname(SCRIPT_DIR) 20 TOOLS_DIR = os.path.dirname(SCRIPT_DIR)
20 21
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after
92 93
93 94
94 def download_gm_image(image_name, image_path, hash_val): 95 def download_gm_image(image_name, image_path, hash_val):
95 """Download the gm result into the given path. 96 """Download the gm result into the given path.
96 97
97 @param image_name The GM file name, for example imageblur_gpu.png. 98 @param image_name The GM file name, for example imageblur_gpu.png.
98 @param image_path Path to place the image. 99 @param image_path Path to place the image.
99 @param hash_val The hash value of the image. 100 @param hash_val The hash value of the image.
100 """ 101 """
101 102
102 # Seperate the test name from a image name 103 # Separate the test name from a image name
103 image_match = IMAGE_FILENAME_RE.match(image_name) 104 image_match = IMAGE_FILENAME_RE.match(image_name)
104 test_name = image_match.group(1) 105 test_name = image_match.group(1)
105 106
106 # Calculate the URL of the requested image 107 # Calculate the URL of the requested image
107 image_url = gm_json.CreateGmActualUrl( 108 image_url = gm_json.CreateGmActualUrl(
108 test_name, gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5, hash_val) 109 test_name, gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5, hash_val)
109 110
110 # Download the image as requested 111 # Download the image as requested
111 download_file(image_url, image_path) 112 download_file(image_url, image_path)
112 113
113 114
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): 115 def get_image_set_from_skpdiff(skpdiff_records):
180 """Get the set of all images references in the given records. 116 """Get the set of all images references in the given records.
181 117
182 @param skpdiff_records An array of records, which are dictionary objects. 118 @param skpdiff_records An array of records, which are dictionary objects.
183 """ 119 """
184 expected_set = frozenset([r['baselinePath'] for r in skpdiff_records]) 120 expected_set = frozenset([r['baselinePath'] for r in skpdiff_records])
185 actual_set = frozenset([r['testPath'] for r in skpdiff_records]) 121 actual_set = frozenset([r['testPath'] for r in skpdiff_records])
186 return expected_set | actual_set 122 return expected_set | actual_set
187 123
188 124
125 def set_expected_hash_in_json(expected_results_json, image_name, hash_value):
126 """Set the expected hash for the object extracted from
127 expected-results.json.
128
129 @param expected_results_json The Python dictionary with the results to
130 modify.
131 @param image_name The name of the image to set the hash of.
132 @param hash_value The hash to set for the image.
133 """
134 expected_results = expected_results_json[gm_json.JSONKEY_EXPECTEDRESULTS]
135
136 if image_name in expected_results:
137 expected_results[image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGE STS][0][1] = hash_value
Zach Reizner 2013/08/01 22:24:40 Curse you 80 character line length.
epoger 2013/08/02 14:33:06 Indeed. If you want to, you can split it like so
Zach Reizner 2013/08/02 21:30:58 I do not want to.
epoger 2013/08/06 15:26:43 Fine, be that way.
138 else:
139 expected_results[image_name] = {
140 gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS:
141 [
142 [
143 'bitmap-64bitMD5',
epoger 2013/08/02 14:33:06 please use the constant reference to this string v
Zach Reizner 2013/08/02 21:30:58 Done.
144 hash_value
145 ]
146 ]
147 }
148
149
150 def get_git_version(path):
epoger 2013/08/02 14:33:06 IMHO, get_head_version or get_base_version would b
Zach Reizner 2013/08/02 21:30:58 Done.
151 """Get the version of the file at the given path stored inside the HEAD of
152 the git repository. It is returned as a string.
153
154 @param path The path of the file whose HEAD is returned. It is assumed the
155 path is inside a git repo rooted at SKIA_ROOT_DIR.
156 """
157
158 # git-show will only work with paths relative to the root of the git repo.
epoger 2013/08/02 14:33:06 This is great, but FYI, git-show works with a path
Zach Reizner 2013/08/02 21:30:58 Thanks
159 # This ensures we give it that relative path.
160 git_path = os.path.relpath(path, SKIA_ROOT_DIR)
161 git_show_proc = subprocess.Popen(['git', 'show', 'HEAD:' + git_path],
162 stdout=subprocess.PIPE)
163
164 # When invoked outside a shell, git will output the last committed version
165 # of the file directly to stdout.
166 git_version_content, _ = git_show_proc.communicate()
167 return git_version_content
168
169
170 class ExpectationsManager:
171 def __init__(self, expectations_dir, expected_name, updated_name,
172 skpdiff_path):
173 """
174 @param expectations_dir The directory to traverse for results files.
175 This should resmble expectations/gm in the Skia trunk.
176 @param expected_name The name of the expected result files. These
177 are in the format of expected-results.json.
178 @param updated_name The name of the updated expected result files.
179 Normally this matches --expectations-filename-output for the
epoger 2013/08/02 14:33:06 I don't understand why we need the updated expecte
Zach Reizner 2013/08/02 21:30:58 You make an excellent point. Derek and I have bee
180 rebaseline.py tool.
181 @param skpdiff_path The path used to execute the skpdiff command.
182 """
183 self.expectations_dir = expectations_dir
epoger 2013/08/02 14:33:06 Typically, we would use underscored (self._expecta
Zach Reizner 2013/08/02 21:30:58 Done.
184 self.expected_name = expected_name
185 self.updated_name = updated_name
186 self.skpdiff_path = skpdiff_path
187 self.generate_gm_comparison()
188
189 def generate_gm_comparison(self):
190 """Generate all the data needed to compare GMs including downloading the
191 changed images and comparing them skpdiff.
epoger 2013/08/02 14:33:06 I think it would be clearer in bullet list form:
Zach Reizner 2013/08/02 21:30:58 Done.
192 """
193 # Create a temporary file tree that makes sense for skpdiff to operate
194 # on.
195 image_output_dir = tempfile.mkdtemp('skpdiff')
196 expected_image_dir = os.path.join(image_output_dir, 'expected')
197 actual_image_dir = os.path.join(image_output_dir, 'actual')
198 os.mkdir(expected_image_dir)
199 os.mkdir(actual_image_dir)
200
201 # Download expected and actual images that differed into the temporary
202 # file tree.
203 self.download_changed_images(expected_image_dir, actual_image_dir)
204
205 # Invoke skpdiff with our downloaded images and place its results in the
206 # temporary directory.
207 self.skpdiff_output_path = os.path.join(image_output_dir,
208 'skpdiff_output.json')
209 skpdiff_cmd = SKPDIFF_INVOKE_FORMAT.format(self.skpdiff_path,
210 self.skpdiff_output_path,
211 expected_image_dir,
212 actual_image_dir)
213 os.system(skpdiff_cmd)
214
215 def download_changed_images(self, expected_image_dir, actual_image_dir):
epoger 2013/08/02 14:33:06 Ultimately, it would be good for us to have self-t
216 """Download the expected and actual GMs that changed into the given
217 paths. Determining what changed will be done by comparing each
218 expected_name JSON results file to its corresponding updated_name JSON
219 results file if it exists.
220
221 @param expected_image_dir The directory to place downloaded expected
epoger 2013/08/02 14:33:06 maybe clearer to call this "The directory to downl
Zach Reizner 2013/08/02 21:30:58 Done.
222 into.
223 @param actual_image_dir The directory to place downloaded actual
224 images into.
225 """
226 image_map = {}
227 differ = jsondiff.GMDiffer()
228
229 # Look through expectations for hashes that changed
230 for root, dirs, files in os.walk(self.expectations_dir):
231 for expectation_file in files:
232 # There are many files in the expectations directory. We only
233 # care about expected results.
234 if expectation_file != self.expected_name:
235 continue
236
237 # Get the name of the results file, and be sure there is an
238 # updated result to compare against. If there is not, there is
239 # no point in diffing this device.
240 expected_file_path = os.path.join(root, self.expected_name)
241 updated_file_path = os.path.join(root, self.updated_name)
242 if not os.path.isfile(updated_file_path):
243 continue
244
245 # Always get the expected results from git because we may have
246 # changed them in a previous instance of the server.
247 expected_contents = get_git_version(expected_file_path)
248 updated_contents = None
249 with open(updated_file_path, 'rb') as updated_file:
250 updated_contents = updated_file.read()
251
252 # Find all expectations that did not match.
253 expected_diff = differ.GenerateDiffDictFromStrings(
254 expected_contents,
255 updated_contents)
256
257 # The name of the device corresponds to the name of the folder
258 # we are in.
259 device_name = os.path.basename(root)
260
261 # Creat name prefixes to store the device's old and new GM
262 # results.
263 expected_image_prefix = os.path.join(expected_image_dir,
264 device_name) + '-'
265
266 actual_image_prefix = os.path.join(actual_image_dir,
267 device_name) + '-'
268
269 # Download each image that had a differing result. We take care
270 # to store metadata about each image so that we know about an
271 # image from its path on disk alone. That is all we will know
272 # after skpdiff outputs its results.
273 for image_name, hashes in expected_diff.iteritems():
274 print('Downloading', image_name, 'for device', device_name)
275 download_gm_image(image_name,
276 expected_image_prefix + image_name,
277 hashes['old'])
278 image_map[expected_image_prefix + image_name] = (
279 device_name, image_name, hashes['old'])
280 download_gm_image(image_name,
281 actual_image_prefix + image_name,
282 hashes['new'])
283 image_map[actual_image_prefix + image_name] = (
284 device_name, image_name, hashes['new'])
285
286 self.image_map = image_map
287
288 def set_expected_hash(self, device_name, image_name, hash_value):
289 """Set the expected hash for the image of the given device. This always
290 writes directly to the expected results file of the given device
291
292 @param device_name The name of the device to write the hash to.
293 @param image_name The name of the image whose hash to set.
294 @param hash_value The value of the hash to set.
295 """
296
297 # Retrieve the expected results file as it is in the working tree
298 json_path = os.path.join(self.expectations_dir, device_name,
299 self.expected_name)
300 expectations = gm_json.LoadFromFile(json_path)
301
302 # Set the specified hash.
303 set_expected_hash_in_json(expectations, image_name, hash_value)
304
305 # Write it out to disk using gm_json to keep the formatting consistent.
306 gm_json.WriteToFile(expectations, json_path)
307
308 def use_hash_of(self, image_path):
309 """Determine the hash of the image at the path using the records, and
310 set it as the expected hash for its device and image config.
311
312 @param image_path The path of the image as it was stored in the output
313 of skpdiff_path
314 """
315
316 # Get the metadata about the image at the path.
317 device_name, image_name, hash_value = self.image_map[image_path]
318
319 # Write out that image's hash directly to the expected results file.
320 self.set_expected_hash(device_name, image_name, hash_value)
321
322
189 class SkPDiffHandler(BaseHTTPServer.BaseHTTPRequestHandler): 323 class SkPDiffHandler(BaseHTTPServer.BaseHTTPRequestHandler):
190 def send_file(self, file_path): 324 def send_file(self, file_path):
191 # Grab the extension if there is one 325 # Grab the extension if there is one
192 extension = os.path.splitext(file_path)[1] 326 extension = os.path.splitext(file_path)[1]
193 if len(extension) >= 1: 327 if len(extension) >= 1:
194 extension = extension[1:] 328 extension = extension[1:]
195 329
196 # Determine the MIME type of the file from its extension 330 # Determine the MIME type of the file from its extension
197 mime_type = MIME_TYPE_MAP.get(extension, MIME_TYPE_MAP['']) 331 mime_type = MIME_TYPE_MAP.get(extension, MIME_TYPE_MAP[''])
198 332
199 # Open the file and send it over HTTP 333 # Open the file and send it over HTTP
200 if os.path.isfile(file_path): 334 if os.path.isfile(file_path):
201 with open(file_path, 'rb') as sending_file: 335 with open(file_path, 'rb') as sending_file:
202 self.send_response(200) 336 self.send_response(200)
203 self.send_header('Content-type', mime_type) 337 self.send_header('Content-type', mime_type)
204 self.end_headers() 338 self.end_headers()
205 self.wfile.write(sending_file.read()) 339 self.wfile.write(sending_file.read())
206 else: 340 else:
207 self.send_error(404) 341 self.send_error(404)
208 342
209 def serve_if_in_dir(self, dir_path, file_path): 343 def serve_if_in_dir(self, dir_path, file_path):
210 # Determine if the file exists relative to the given dir_path AND exists 344 # 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 345 # under the dir_path. This is to prevent accidentally serving files
212 # outside the directory intended using symlinks, or '../'. 346 # outside the directory intended using symlinks, or '../'.
213 real_path = os.path.normpath(os.path.join(dir_path, file_path)) 347 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: 348 if os.path.commonprefix([real_path, dir_path]) == dir_path:
216 if os.path.isfile(real_path): 349 if os.path.isfile(real_path):
217 self.send_file(real_path) 350 self.send_file(real_path)
218 return True 351 return True
219 return False 352 return False
220 353
221 def do_GET(self): 354 def do_GET(self):
222 # Simple rewrite rule of the root path to 'viewer.html' 355 # Simple rewrite rule of the root path to 'viewer.html'
223 if self.path == '' or self.path == '/': 356 if self.path == '' or self.path == '/':
224 self.path = '/viewer.html' 357 self.path = '/viewer.html'
(...skipping 16 matching lines...) Expand all
241 374
242 # WARNING: Serving any file the user wants is incredibly insecure. Its 375 # 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. 376 # redeeming quality is that we only serve gm files on a white list.
244 if self.path in self.server.image_set: 377 if self.path in self.server.image_set:
245 self.send_file(self.path) 378 self.send_file(self.path)
246 return 379 return
247 380
248 # If no file to send was found, just give the standard 404 381 # If no file to send was found, just give the standard 404
249 self.send_error(404) 382 self.send_error(404)
250 383
384 def do_POST(self):
385 if self.path == '/set_hash':
386 content_length = int(self.headers['Content-length'])
387 request_data = json.loads(self.rfile.read(content_length))
388 self.server.expectations_manager.use_hash_of(request_data['path'])
389 self.send_response(200)
390 self.send_header('Content-type', 'application/json')
391 self.end_headers()
392 self.wfile.write('{"success":true}')
393 return
251 394
252 def run_server(skpdiff_output_path, port=8080): 395 # If the we have no handler for this path, give em' the 404
396 self.send_error(404)
397
398
399 def run_server(expectations_manager, port=8080):
253 # Preload the skpdiff results file. This is so we can perform some 400 # Preload the skpdiff results file. This is so we can perform some
254 # processing on it. 401 # processing on it.
255 skpdiff_output_json = '' 402 skpdiff_output_json = ''
256 with open(skpdiff_output_path, 'rb') as records_file: 403 with open(expectations_manager.skpdiff_output_path, 'rb') as records_file:
257 skpdiff_output_json = records_file.read() 404 skpdiff_output_json = records_file.read()
258 405
259 # It's important to parse the results file so that we can make a set of 406 # It's important to parse the results file so that we can make a set of
260 # images that the web page might request. 407 # images that the web page might request.
261 skpdiff_records = json.loads(skpdiff_output_json)['records'] 408 skpdiff_records = json.loads(skpdiff_output_json)['records']
262 image_set = get_image_set_from_skpdiff(skpdiff_records) 409 image_set = get_image_set_from_skpdiff(skpdiff_records)
263 410
411 for record in skpdiff_records:
412 record['device'] = os.path.basename(record['baselinePath'])
413
264 # Add JSONP padding to the JSON because the web page expects it. It expects 414 # 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 415 # 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. 416 # web server, the only way to load JSON is with JSONP.
267 skpdiff_output_json = 'var SkPDiffRecords = ' + skpdiff_output_json 417 skpdiff_output_json = ('var SkPDiffRecords = ' +
418 json.dumps({'records': skpdiff_records}) + ';')
268 419
269 # Do not bind to interfaces other than localhost because the server will 420 # 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 421 # 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 422 # 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. 423 # server, so DO NOT let this server listen to anything but localhost.
273 server_address = ('127.0.0.1', port) 424 server_address = ('127.0.0.1', port)
274 http_server = BaseHTTPServer.HTTPServer(server_address, SkPDiffHandler) 425 http_server = BaseHTTPServer.HTTPServer(server_address, SkPDiffHandler)
275 http_server.image_set = image_set 426 http_server.image_set = image_set
276 http_server.skpdiff_output_json = skpdiff_output_json 427 http_server.skpdiff_output_json = skpdiff_output_json
277 print('Navigate thine browser to: http://{}:{}'.format(*server_address)) 428 http_server.expectations_manager = expectations_manager
429 print('Navigate thine browser to: http://{}:{}/'.format(*server_address))
278 http_server.serve_forever() 430 http_server.serve_forever()
279 431
280 432
281 def main(): 433 def main():
282 parser = argparse.ArgumentParser() 434 parser = argparse.ArgumentParser()
283 parser.add_argument('--port', '-p', metavar='PORT', 435 parser.add_argument('--port', '-p', metavar='PORT',
284 type=int, 436 type=int,
285 default=8080, 437 default=8080,
286 help='port to bind the server to; ' + 438 help='port to bind the server to; ' +
287 'defaults to %(default)s', 439 'defaults to %(default)s',
(...skipping 25 matching lines...) Expand all
313 'defaults to out/Release/skpdiff or out/Default/skpdiff' 465 'defaults to out/Release/skpdiff or out/Default/skpdiff'
314 ) 466 )
315 467
316 args = vars(parser.parse_args()) # Convert args into a python dict 468 args = vars(parser.parse_args()) # Convert args into a python dict
317 469
318 # Make sure we have access to an skpdiff binary 470 # Make sure we have access to an skpdiff binary
319 skpdiff_path = get_skpdiff_path(args['skpdiff_path']) 471 skpdiff_path = get_skpdiff_path(args['skpdiff_path'])
320 if skpdiff_path is None: 472 if skpdiff_path is None:
321 sys.exit(1) 473 sys.exit(1)
322 474
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 475 # Print out the paths of things for easier debugging
331 print('script dir :', SCRIPT_DIR) 476 print('script dir :', SCRIPT_DIR)
332 print('tools dir :', TOOLS_DIR) 477 print('tools dir :', TOOLS_DIR)
333 print('root dir :', SKIA_ROOT_DIR) 478 print('root dir :', SKIA_ROOT_DIR)
334 print('expectations dir :', args['expectations_dir']) 479 print('expectations dir :', args['expectations_dir'])
335 print('skpdiff path :', skpdiff_path) 480 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 481
340 # Download expected and actual images that differed into the temporary file 482 expectations_manager = ExpectationsManager(args['expectations_dir'],
341 # tree. 483 args['expected'],
342 download_changed_images(args['expectations_dir'], 484 args['updated'],
343 args['expected'], args['updated'], 485 skpdiff_path)
344 expected_image_dir, actual_image_dir)
345 486
346 # Invoke skpdiff with our downloaded images and place its results in the 487 run_server(expectations_manager, port=args['port'])
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 488
357 if __name__ == '__main__': 489 if __name__ == '__main__':
358 main() 490 main()
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698