Chromium Code Reviews| 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 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 |
| (...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 92 writer.write(reader.read()) | 92 writer.write(reader.read()) |
| 93 | 93 |
| 94 | 94 |
| 95 def download_gm_image(image_name, image_path, hash_val): | 95 def download_gm_image(image_name, image_path, hash_val): |
| 96 """Download the gm result into the given path. | 96 """Download the gm result into the given path. |
| 97 | 97 |
| 98 @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. |
| 99 @param image_path Path to place the image. | 99 @param image_path Path to place the image. |
| 100 @param hash_val The hash value of the image. | 100 @param hash_val The hash value of the image. |
| 101 """ | 101 """ |
| 102 if hash_val is None: | |
| 103 return | |
| 102 | 104 |
| 103 # Separate the test name from a image name | 105 # Separate the test name from a image name |
| 104 image_match = IMAGE_FILENAME_RE.match(image_name) | 106 image_match = IMAGE_FILENAME_RE.match(image_name) |
| 105 test_name = image_match.group(1) | 107 test_name = image_match.group(1) |
| 106 | 108 |
| 107 # Calculate the URL of the requested image | 109 # Calculate the URL of the requested image |
| 108 image_url = gm_json.CreateGmActualUrl( | 110 image_url = gm_json.CreateGmActualUrl( |
| 109 test_name, gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5, hash_val) | 111 test_name, gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5, hash_val) |
| 110 | 112 |
| 111 # Download the image as requested | 113 # Download the image as requested |
| (...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 167 git_version_content, _ = git_show_proc.communicate() | 169 git_version_content, _ = git_show_proc.communicate() |
| 168 return git_version_content | 170 return git_version_content |
| 169 | 171 |
| 170 | 172 |
| 171 class GMInstance: | 173 class GMInstance: |
| 172 """Information about a GM test result on a specific device: | 174 """Information about a GM test result on a specific device: |
| 173 - device_name = the name of the device that rendered it | 175 - device_name = the name of the device that rendered it |
| 174 - image_name = the GM test name and config | 176 - image_name = the GM test name and config |
| 175 - expected_hash = the current expected hash value | 177 - expected_hash = the current expected hash value |
| 176 - actual_hash = the actual hash value | 178 - actual_hash = the actual hash value |
| 179 - is_rebaselined = True if actual_hash is what is currently in the expected | |
| 180 results file, False otherwise. | |
| 177 """ | 181 """ |
| 178 def __init__(self, | 182 def __init__(self, |
| 179 device_name, image_name, | 183 device_name, image_name, |
| 180 expected_hash, actual_hash): | 184 expected_hash, actual_hash, |
| 185 is_rebaselined): | |
| 181 self.device_name = device_name | 186 self.device_name = device_name |
| 182 self.image_name = image_name | 187 self.image_name = image_name |
| 183 self.expected_hash = expected_hash | 188 self.expected_hash = expected_hash |
| 184 self.actual_hash = actual_hash | 189 self.actual_hash = actual_hash |
| 190 self.is_rebaselined = is_rebaselined | |
| 185 | 191 |
| 186 | 192 |
| 187 class ExpectationsManager: | 193 class ExpectationsManager: |
| 188 def __init__(self, expectations_dir, expected_name, updated_name, | 194 def __init__(self, expectations_dir, expected_name, updated_name, |
| 189 skpdiff_path): | 195 skpdiff_path): |
| 190 """ | 196 """ |
| 191 @param expectations_dir The directory to traverse for results files. | 197 @param expectations_dir The directory to traverse for results files. |
| 192 This should resemble expectations/gm in the Skia trunk. | 198 This should resemble expectations/gm in the Skia trunk. |
| 193 @param expected_name The name of the expected result files. These | 199 @param expected_name The name of the expected result files. These |
| 194 are in the format of expected-results.json. | 200 are in the format of expected-results.json. |
| (...skipping 26 matching lines...) Expand all Loading... | |
| 221 actual_image_dir = os.path.join(image_output_dir, 'actual') | 227 actual_image_dir = os.path.join(image_output_dir, 'actual') |
| 222 os.mkdir(expected_image_dir) | 228 os.mkdir(expected_image_dir) |
| 223 os.mkdir(actual_image_dir) | 229 os.mkdir(actual_image_dir) |
| 224 | 230 |
| 225 # Download expected and actual images that differed into the temporary | 231 # Download expected and actual images that differed into the temporary |
| 226 # file tree. | 232 # file tree. |
| 227 self._download_expectation_images(expected_image_dir, actual_image_dir) | 233 self._download_expectation_images(expected_image_dir, actual_image_dir) |
| 228 | 234 |
| 229 # Invoke skpdiff with our downloaded images and place its results in the | 235 # Invoke skpdiff with our downloaded images and place its results in the |
| 230 # temporary directory. | 236 # temporary directory. |
| 231 self.skpdiff_output_path = os.path.join(image_output_dir, | 237 self._skpdiff_output_path = os.path.join(image_output_dir, |
| 232 'skpdiff_output.json') | 238 'skpdiff_output.json') |
| 233 skpdiff_cmd = SKPDIFF_INVOKE_FORMAT.format(self._skpdiff_path, | 239 skpdiff_cmd = SKPDIFF_INVOKE_FORMAT.format(self._skpdiff_path, |
| 234 self.skpdiff_output_path, | 240 self._skpdiff_output_path, |
| 235 expected_image_dir, | 241 expected_image_dir, |
| 236 actual_image_dir) | 242 actual_image_dir) |
| 237 os.system(skpdiff_cmd) | 243 os.system(skpdiff_cmd) |
| 244 self._load_skpdiff_output() | |
| 238 | 245 |
| 239 | 246 |
| 240 def _get_expectations(self): | 247 def _get_expectations(self): |
| 241 """Fills self._expectations with GMInstance objects for each test whose | 248 """Fills self._expectations with GMInstance objects for each test whose |
| 242 expectation is different between the following two files: | 249 expectation is different between the following two files: |
| 243 - the local filesystem's updated results file | 250 - the local filesystem's updated results file |
| 244 - git's head version of the expected results file | 251 - git's head version of the expected results file |
| 245 """ | 252 """ |
| 246 differ = jsondiff.GMDiffer() | 253 differ = jsondiff.GMDiffer() |
| 247 self._expectations = [] | 254 self._expectations = [] |
| (...skipping 12 matching lines...) Expand all Loading... | |
| 260 if not os.path.isfile(updated_file_path): | 267 if not os.path.isfile(updated_file_path): |
| 261 continue | 268 continue |
| 262 | 269 |
| 263 # Always get the expected results from git because we may have | 270 # Always get the expected results from git because we may have |
| 264 # changed them in a previous instance of the server. | 271 # changed them in a previous instance of the server. |
| 265 expected_contents = get_head_version(expected_file_path) | 272 expected_contents = get_head_version(expected_file_path) |
| 266 updated_contents = None | 273 updated_contents = None |
| 267 with open(updated_file_path, 'rb') as updated_file: | 274 with open(updated_file_path, 'rb') as updated_file: |
| 268 updated_contents = updated_file.read() | 275 updated_contents = updated_file.read() |
| 269 | 276 |
| 277 # Read the expected results on disk to determine what we've | |
| 278 # already rebaselined. | |
| 279 commited_contents = None | |
| 280 with open(expected_file_path, 'rb') as expected_file: | |
| 281 commited_contents = expected_file.read() | |
| 282 | |
| 270 # Find all expectations that did not match. | 283 # Find all expectations that did not match. |
| 271 expected_diff = differ.GenerateDiffDictFromStrings( | 284 expected_diff = differ.GenerateDiffDictFromStrings( |
| 272 expected_contents, | 285 expected_contents, |
| 273 updated_contents) | 286 updated_contents) |
| 274 | 287 |
| 288 # Generate a set of images that have already been rebaselined | |
| 289 # onto disk. | |
| 290 rebaselined_diff = differ.GenerateDiffDictFromStrings( | |
| 291 expected_contents, | |
| 292 commited_contents) | |
| 293 | |
| 294 rebaselined_set = set(rebaselined_diff.keys()) | |
| 295 | |
| 275 # The name of the device corresponds to the name of the folder | 296 # The name of the device corresponds to the name of the folder |
| 276 # we are in. | 297 # we are in. |
| 277 device_name = os.path.basename(root) | 298 device_name = os.path.basename(root) |
| 278 | 299 |
| 279 # Store old and new versions of the expectation for each GM | 300 # Store old and new versions of the expectation for each GM |
| 280 for image_name, hashes in expected_diff.iteritems(): | 301 for image_name, hashes in expected_diff.iteritems(): |
| 281 self._expectations.append( | 302 self._expectations.append( |
| 282 GMInstance(device_name, image_name, | 303 GMInstance(device_name, image_name, |
| 283 hashes['old'], hashes['new'])) | 304 hashes['old'], hashes['new'], |
| 305 image_name in rebaselined_set)) | |
| 306 | |
| 307 def _load_skpdiff_output(self): | |
| 308 """Loads the results of skpdiff and annotates them with whether they | |
| 309 have already been rebaselined or not. The resulting data is store in | |
| 310 self.skpdiff_records.""" | |
| 311 self.skpdiff_records = None | |
| 312 with open(self._skpdiff_output_path, 'rb') as skpdiff_output_file: | |
| 313 self.skpdiff_records = json.load(skpdiff_output_file)['records'] | |
| 314 for record in self.skpdiff_records: | |
| 315 record['isRebaselined'] = self.image_map[record['baselinePath']] [1].is_rebaselined | |
| 284 | 316 |
| 285 | 317 |
| 286 def _download_expectation_images(self, expected_image_dir, actual_image_dir) : | 318 def _download_expectation_images(self, expected_image_dir, actual_image_dir) : |
| 287 """Download the expected and actual images for the _expectations array. | 319 """Download the expected and actual images for the _expectations array. |
| 288 | 320 |
| 289 @param expected_image_dir The directory to download expected images | 321 @param expected_image_dir The directory to download expected images |
| 290 into. | 322 into. |
| 291 @param actual_image_dir The directory to download actual images into. | 323 @param actual_image_dir The directory to download actual images into. |
| 292 """ | 324 """ |
| 293 image_map = {} | 325 image_map = {} |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 339 json_path = os.path.join(self._expectations_dir, device_name, | 371 json_path = os.path.join(self._expectations_dir, device_name, |
| 340 self._expected_name) | 372 self._expected_name) |
| 341 expectations = gm_json.LoadFromFile(json_path) | 373 expectations = gm_json.LoadFromFile(json_path) |
| 342 | 374 |
| 343 # Set the specified hash. | 375 # Set the specified hash. |
| 344 set_expected_hash_in_json(expectations, image_name, hash_value) | 376 set_expected_hash_in_json(expectations, image_name, hash_value) |
| 345 | 377 |
| 346 # Write it out to disk using gm_json to keep the formatting consistent. | 378 # Write it out to disk using gm_json to keep the formatting consistent. |
| 347 gm_json.WriteToFile(expectations, json_path) | 379 gm_json.WriteToFile(expectations, json_path) |
| 348 | 380 |
| 349 def use_hash_of(self, image_path): | 381 def commit_rebaselines(self, rebaselines): |
| 350 """Determine the hash of the image at the path using the records, and | 382 """Resets all expectations to their defaults and uses the hashes for the |
| 351 set it as the expected hash for its device and image config. | 383 images in the given list. |
| 352 | 384 |
| 353 @param image_path The path of the image as it was stored in the output | 385 @param rebaselines A list of image paths to use the hash of. |
| 354 of skpdiff_path | |
| 355 """ | 386 """ |
| 387 # Reset all expectations to their old hashes | |
|
epoger
2013/08/07 16:51:55
I don't understand why you have to reset all the e
| |
| 388 for expectation in self._expectations: | |
| 389 expectation.is_rebaselined = False | |
| 390 self._set_expected_hash(expectation.device_name, | |
| 391 expectation.image_name, | |
| 392 expectation.expected_hash) | |
| 356 | 393 |
| 357 # Get the metadata about the image at the path. | 394 # Take all the images to rebaseline |
| 358 is_actual, expectation = self.image_map[image_path] | 395 for image_path in rebaselines: |
| 396 # Get the metadata about the image at the path. | |
| 397 is_actual, expectation = self.image_map[image_path] | |
| 359 | 398 |
| 360 expectation_hash = expectation.actual_hash if is_actual else\ | 399 expectation.is_rebaselined = is_actual |
| 361 expectation.expected_hash | 400 expectation_hash = expectation.actual_hash if is_actual else\ |
| 401 expectation.expected_hash | |
| 362 | 402 |
| 363 # Write out that image's hash directly to the expected results file. | 403 # Write out that image's hash directly to the expected results file. |
| 364 self._set_expected_hash(expectation.device_name, expectation.image_name, | 404 self._set_expected_hash(expectation.device_name, |
| 365 expectation_hash) | 405 expectation.image_name, |
| 406 expectation_hash) | |
| 407 | |
| 408 self._load_skpdiff_output() | |
| 366 | 409 |
| 367 | 410 |
| 368 class SkPDiffHandler(BaseHTTPServer.BaseHTTPRequestHandler): | 411 class SkPDiffHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
| 369 def send_file(self, file_path): | 412 def send_file(self, file_path): |
| 370 # Grab the extension if there is one | 413 # Grab the extension if there is one |
| 371 extension = os.path.splitext(file_path)[1] | 414 extension = os.path.splitext(file_path)[1] |
| 372 if len(extension) >= 1: | 415 if len(extension) >= 1: |
| 373 extension = extension[1:] | 416 extension = extension[1:] |
| 374 | 417 |
| 375 # Determine the MIME type of the file from its extension | 418 # Determine the MIME type of the file from its extension |
| (...skipping 27 matching lines...) Expand all Loading... | |
| 403 | 446 |
| 404 # The [1:] chops off the leading '/' | 447 # The [1:] chops off the leading '/' |
| 405 file_path = self.path[1:] | 448 file_path = self.path[1:] |
| 406 | 449 |
| 407 # Handle skpdiff_output.json manually because it is was processed by the | 450 # Handle skpdiff_output.json manually because it is was processed by the |
| 408 # server when it was started and does not exist as a file. | 451 # server when it was started and does not exist as a file. |
| 409 if file_path == 'skpdiff_output.json': | 452 if file_path == 'skpdiff_output.json': |
| 410 self.send_response(200) | 453 self.send_response(200) |
| 411 self.send_header('Content-type', MIME_TYPE_MAP['json']) | 454 self.send_header('Content-type', MIME_TYPE_MAP['json']) |
| 412 self.end_headers() | 455 self.end_headers() |
| 413 self.wfile.write(self.server.skpdiff_output_json) | 456 |
| 457 # Add JSONP padding to the JSON because the web page expects it. It | |
| 458 # expects it because it was designed to run with or without a web | |
| 459 # server. Without a web server, the only way to load JSON is with | |
| 460 # JSONP. | |
| 461 skpdiff_records = self.server.expectations_manager.skpdiff_records | |
| 462 self.wfile.write('var SkPDiffRecords = ') | |
| 463 json.dump({'records': skpdiff_records}, self.wfile) | |
| 464 self.wfile.write(';') | |
| 414 return | 465 return |
| 415 | 466 |
| 416 # Attempt to send static asset files first. | 467 # Attempt to send static asset files first. |
| 417 if self.serve_if_in_dir(SCRIPT_DIR, file_path): | 468 if self.serve_if_in_dir(SCRIPT_DIR, file_path): |
| 418 return | 469 return |
| 419 | 470 |
| 420 # WARNING: Serving any file the user wants is incredibly insecure. Its | 471 # WARNING: Serving any file the user wants is incredibly insecure. Its |
| 421 # redeeming quality is that we only serve gm files on a white list. | 472 # redeeming quality is that we only serve gm files on a white list. |
| 422 if self.path in self.server.image_set: | 473 if self.path in self.server.image_set: |
| 423 self.send_file(self.path) | 474 self.send_file(self.path) |
| 424 return | 475 return |
| 425 | 476 |
| 426 # If no file to send was found, just give the standard 404 | 477 # If no file to send was found, just give the standard 404 |
| 427 self.send_error(404) | 478 self.send_error(404) |
| 428 | 479 |
| 429 def do_POST(self): | 480 def do_POST(self): |
| 430 if self.path == '/set_hash': | 481 if self.path == '/commit_rebaselines': |
| 431 content_length = int(self.headers['Content-length']) | 482 content_length = int(self.headers['Content-length']) |
| 432 request_data = json.loads(self.rfile.read(content_length)) | 483 request_data = json.loads(self.rfile.read(content_length)) |
| 433 self.server.expectations_manager.use_hash_of(request_data['path']) | 484 rebaselines = request_data['rebaselines'] |
| 485 self.server.expectations_manager.commit_rebaselines(rebaselines) | |
| 434 self.send_response(200) | 486 self.send_response(200) |
| 435 self.send_header('Content-type', 'application/json') | 487 self.send_header('Content-type', 'application/json') |
| 436 self.end_headers() | 488 self.end_headers() |
| 437 self.wfile.write('{"success":true}') | 489 self.wfile.write('{"success":true}') |
| 438 return | 490 return |
| 439 | 491 |
| 440 # If the we have no handler for this path, give em' the 404 | 492 # If the we have no handler for this path, give em' the 404 |
| 441 self.send_error(404) | 493 self.send_error(404) |
| 442 | 494 |
| 443 | 495 |
| 444 def run_server(expectations_manager, port=8080): | 496 def run_server(expectations_manager, port=8080): |
| 445 # Preload the skpdiff results file. This is so we can perform some | |
| 446 # processing on it. | |
| 447 skpdiff_output_json = '' | |
| 448 with open(expectations_manager.skpdiff_output_path, 'rb') as records_file: | |
| 449 skpdiff_output_json = records_file.read() | |
| 450 | |
| 451 # It's important to parse the results file so that we can make a set of | 497 # It's important to parse the results file so that we can make a set of |
| 452 # images that the web page might request. | 498 # images that the web page might request. |
| 453 skpdiff_records = json.loads(skpdiff_output_json)['records'] | 499 skpdiff_records = expectations_manager.skpdiff_records |
| 454 image_set = get_image_set_from_skpdiff(skpdiff_records) | 500 image_set = get_image_set_from_skpdiff(skpdiff_records) |
| 455 | 501 |
| 456 # Add JSONP padding to the JSON because the web page expects it. It expects | |
| 457 # it because it was designed to run with or without a web server. Without a | |
| 458 # web server, the only way to load JSON is with JSONP. | |
| 459 skpdiff_output_json = ('var SkPDiffRecords = ' + | |
| 460 json.dumps({'records': skpdiff_records}) + ';') | |
| 461 | |
| 462 # Do not bind to interfaces other than localhost because the server will | 502 # Do not bind to interfaces other than localhost because the server will |
| 463 # attempt to serve files relative to the root directory as a last resort | 503 # attempt to serve files relative to the root directory as a last resort |
| 464 # before 404ing. This means all of your files can be accessed from this | 504 # before 404ing. This means all of your files can be accessed from this |
| 465 # server, so DO NOT let this server listen to anything but localhost. | 505 # server, so DO NOT let this server listen to anything but localhost. |
| 466 server_address = ('127.0.0.1', port) | 506 server_address = ('127.0.0.1', port) |
| 467 http_server = BaseHTTPServer.HTTPServer(server_address, SkPDiffHandler) | 507 http_server = BaseHTTPServer.HTTPServer(server_address, SkPDiffHandler) |
| 468 http_server.image_set = image_set | 508 http_server.image_set = image_set |
| 469 http_server.skpdiff_output_json = skpdiff_output_json | |
| 470 http_server.expectations_manager = expectations_manager | 509 http_server.expectations_manager = expectations_manager |
| 471 print('Navigate thine browser to: http://{}:{}/'.format(*server_address)) | 510 print('Navigate thine browser to: http://{}:{}/'.format(*server_address)) |
| 472 http_server.serve_forever() | 511 http_server.serve_forever() |
| 473 | 512 |
| 474 | 513 |
| 475 def main(): | 514 def main(): |
| 476 parser = argparse.ArgumentParser() | 515 parser = argparse.ArgumentParser() |
| 477 parser.add_argument('--port', '-p', metavar='PORT', | 516 parser.add_argument('--port', '-p', metavar='PORT', |
| 478 type=int, | 517 type=int, |
| 479 default=8080, | 518 default=8080, |
| (...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 523 | 562 |
| 524 expectations_manager = ExpectationsManager(args['expectations_dir'], | 563 expectations_manager = ExpectationsManager(args['expectations_dir'], |
| 525 args['expected'], | 564 args['expected'], |
| 526 args['updated'], | 565 args['updated'], |
| 527 skpdiff_path) | 566 skpdiff_path) |
| 528 | 567 |
| 529 run_server(expectations_manager, port=args['port']) | 568 run_server(expectations_manager, port=args['port']) |
| 530 | 569 |
| 531 if __name__ == '__main__': | 570 if __name__ == '__main__': |
| 532 main() | 571 main() |
| OLD | NEW |