| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 | 2 |
| 3 """ | 3 """ |
| 4 Copyright 2013 Google Inc. | 4 Copyright 2013 Google Inc. |
| 5 | 5 |
| 6 Use of this source code is governed by a BSD-style license that can be | 6 Use of this source code is governed by a BSD-style license that can be |
| 7 found in the LICENSE file. | 7 found in the LICENSE file. |
| 8 | 8 |
| 9 HTTP server for our HTML rebaseline viewer. | 9 HTTP server for our HTML rebaseline viewer. |
| 10 """ | 10 """ |
| (...skipping 22 matching lines...) Expand all Loading... |
| 33 # Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end* | 33 # Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end* |
| 34 # so any dirs that are already in the PYTHONPATH will be preferred. | 34 # so any dirs that are already in the PYTHONPATH will be preferred. |
| 35 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) | 35 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) |
| 36 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY)) | 36 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY)) |
| 37 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') | 37 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') |
| 38 if TOOLS_DIRECTORY not in sys.path: | 38 if TOOLS_DIRECTORY not in sys.path: |
| 39 sys.path.append(TOOLS_DIRECTORY) | 39 sys.path.append(TOOLS_DIRECTORY) |
| 40 import svn | 40 import svn |
| 41 | 41 |
| 42 # Imports from local dir | 42 # Imports from local dir |
| 43 import imagepairset |
| 43 import results | 44 import results |
| 44 | 45 |
| 45 ACTUALS_SVN_REPO = 'http://skia-autogen.googlecode.com/svn/gm-actual' | 46 ACTUALS_SVN_REPO = 'http://skia-autogen.googlecode.com/svn/gm-actual' |
| 46 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') | 47 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') |
| 47 EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') | 48 EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') |
| 48 GENERATED_IMAGES_ROOT = os.path.join(PARENT_DIRECTORY, 'static', | 49 GENERATED_IMAGES_ROOT = os.path.join(PARENT_DIRECTORY, 'static', |
| 49 'generated-images') | 50 'generated-images') |
| 50 | 51 |
| 51 # A simple dictionary of file name extensions to MIME types. The empty string | 52 # A simple dictionary of file name extensions to MIME types. The empty string |
| 52 # entry is used as the default when no extension was given or if the extension | 53 # entry is used as the default when no extension was given or if the extension |
| 53 # has no entry in this dictionary. | 54 # has no entry in this dictionary. |
| 54 MIME_TYPE_MAP = {'': 'application/octet-stream', | 55 MIME_TYPE_MAP = {'': 'application/octet-stream', |
| 55 'html': 'text/html', | 56 'html': 'text/html', |
| 56 'css': 'text/css', | 57 'css': 'text/css', |
| 57 'png': 'image/png', | 58 'png': 'image/png', |
| 58 'js': 'application/javascript', | 59 'js': 'application/javascript', |
| 59 'json': 'application/json' | 60 'json': 'application/json' |
| 60 } | 61 } |
| 61 | 62 |
| 63 # Keys that server.py uses to create the toplevel content header. |
| 64 # NOTE: Keep these in sync with static/constants.js |
| 65 KEY__EDITS__MODIFICATIONS = 'modifications' |
| 66 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' |
| 67 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' |
| 68 KEY__HEADER = 'header' |
| 69 KEY__HEADER__DATAHASH = 'dataHash' |
| 70 KEY__HEADER__IS_EDITABLE = 'isEditable' |
| 71 KEY__HEADER__IS_EXPORTED = 'isExported' |
| 72 KEY__HEADER__IS_STILL_LOADING = 'resultsStillLoading' |
| 73 KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE = 'timeNextUpdateAvailable' |
| 74 KEY__HEADER__TIME_UPDATED = 'timeUpdated' |
| 75 KEY__HEADER__TYPE = 'type' |
| 76 |
| 62 DEFAULT_ACTUALS_DIR = '.gm-actuals' | 77 DEFAULT_ACTUALS_DIR = '.gm-actuals' |
| 63 DEFAULT_PORT = 8888 | 78 DEFAULT_PORT = 8888 |
| 64 | 79 |
| 65 # How often (in seconds) clients should reload while waiting for initial | 80 # How often (in seconds) clients should reload while waiting for initial |
| 66 # results to load. | 81 # results to load. |
| 67 RELOAD_INTERVAL_UNTIL_READY = 10 | 82 RELOAD_INTERVAL_UNTIL_READY = 10 |
| 68 | 83 |
| 69 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' | 84 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' |
| 70 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' | 85 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' |
| 71 | 86 |
| (...skipping 234 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 306 # TODO(epoger): Rather than using a global variable for the handler | 321 # TODO(epoger): Rather than using a global variable for the handler |
| 307 # to refer to the Server object, make Server a subclass of | 322 # to refer to the Server object, make Server a subclass of |
| 308 # HTTPServer, and then it could be available to the handler via | 323 # HTTPServer, and then it could be available to the handler via |
| 309 # the handler's .server instance variable. | 324 # the handler's .server instance variable. |
| 310 results_obj = _SERVER.results | 325 results_obj = _SERVER.results |
| 311 if results_obj: | 326 if results_obj: |
| 312 response_dict = self.package_results(results_obj, type) | 327 response_dict = self.package_results(results_obj, type) |
| 313 else: | 328 else: |
| 314 now = int(time.time()) | 329 now = int(time.time()) |
| 315 response_dict = { | 330 response_dict = { |
| 316 'header': { | 331 KEY__HEADER: { |
| 317 'resultsStillLoading': True, | 332 KEY__HEADER__IS_STILL_LOADING: True, |
| 318 'timeUpdated': now, | 333 KEY__HEADER__TIME_UPDATED: now, |
| 319 'timeNextUpdateAvailable': now + RELOAD_INTERVAL_UNTIL_READY, | 334 KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: |
| 335 now + RELOAD_INTERVAL_UNTIL_READY, |
| 320 }, | 336 }, |
| 321 } | 337 } |
| 322 self.send_json_dict(response_dict) | 338 self.send_json_dict(response_dict) |
| 323 | 339 |
| 324 def package_results(self, results_obj, type): | 340 def package_results(self, results_obj, type): |
| 325 """ Given a nonempty "results" object, package it as a response_dict | 341 """ Given a nonempty "results" object, package it as a response_dict |
| 326 as needed within do_GET_results. | 342 as needed within do_GET_results. |
| 327 | 343 |
| 328 Args: | 344 Args: |
| 329 results_obj: nonempty "results" object | 345 results_obj: nonempty "results" object |
| 330 type: string indicating which set of results to return; | 346 type: string indicating which set of results to return; |
| 331 must be one of the results.RESULTS_* constants | 347 must be one of the results.RESULTS_* constants |
| 332 """ | 348 """ |
| 333 response_dict = results_obj.get_results_of_type(type) | 349 response_dict = results_obj.get_results_of_type(type) |
| 334 time_updated = results_obj.get_timestamp() | 350 time_updated = results_obj.get_timestamp() |
| 335 response_dict['header'] = { | 351 response_dict[KEY__HEADER] = { |
| 336 # Timestamps: | 352 # Timestamps: |
| 337 # 1. when this data was last updated | 353 # 1. when this data was last updated |
| 338 # 2. when the caller should check back for new data (if ever) | 354 # 2. when the caller should check back for new data (if ever) |
| 339 # | 355 # |
| 340 # We only return these timestamps if the --reload argument was passed; | 356 # We only return these timestamps if the --reload argument was passed; |
| 341 # otherwise, we have no idea when the expectations were last updated | 357 # otherwise, we have no idea when the expectations were last updated |
| 342 # (we allow the user to maintain her own expectations as she sees fit). | 358 # (we allow the user to maintain her own expectations as she sees fit). |
| 343 'timeUpdated': time_updated if _SERVER.reload_seconds else None, | 359 KEY__HEADER__TIME_UPDATED: |
| 344 'timeNextUpdateAvailable': ( | 360 time_updated if _SERVER.reload_seconds else None, |
| 361 KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: |
| 345 (time_updated+_SERVER.reload_seconds) if _SERVER.reload_seconds | 362 (time_updated+_SERVER.reload_seconds) if _SERVER.reload_seconds |
| 346 else None), | 363 else None, |
| 347 | 364 |
| 348 # The type we passed to get_results_of_type() | 365 # The type we passed to get_results_of_type() |
| 349 'type': type, | 366 KEY__HEADER__TYPE: type, |
| 350 | 367 |
| 351 # Hash of testData, which the client must return with any edits-- | 368 # Hash of dataset, which the client must return with any edits-- |
| 352 # this ensures that the edits were made to a particular dataset. | 369 # this ensures that the edits were made to a particular dataset. |
| 353 'dataHash': str(hash(repr(response_dict['testData']))), | 370 KEY__HEADER__DATAHASH: str(hash(repr( |
| 371 response_dict[imagepairset.KEY__IMAGEPAIRS]))), |
| 354 | 372 |
| 355 # Whether the server will accept edits back. | 373 # Whether the server will accept edits back. |
| 356 'isEditable': _SERVER.is_editable, | 374 KEY__HEADER__IS_EDITABLE: _SERVER.is_editable, |
| 357 | 375 |
| 358 # Whether the service is accessible from other hosts. | 376 # Whether the service is accessible from other hosts. |
| 359 'isExported': _SERVER.is_exported, | 377 KEY__HEADER__IS_EXPORTED: _SERVER.is_exported, |
| 360 } | 378 } |
| 361 return response_dict | 379 return response_dict |
| 362 | 380 |
| 363 def do_GET_static(self, path): | 381 def do_GET_static(self, path): |
| 364 """ Handle a GET request for a file under the 'static' directory. | 382 """ Handle a GET request for a file under the 'static' directory. |
| 365 Only allow serving of files within the 'static' directory that is a | 383 Only allow serving of files within the 'static' directory that is a |
| 366 filesystem sibling of this script. | 384 filesystem sibling of this script. |
| 367 | 385 |
| 368 Args: | 386 Args: |
| 369 path: path to file (under static directory) to retrieve | 387 path: path to file (under static directory) to retrieve |
| (...skipping 29 matching lines...) Expand all Loading... |
| 399 self.send_response(200) | 417 self.send_response(200) |
| 400 except: | 418 except: |
| 401 self.send_error(404) | 419 self.send_error(404) |
| 402 raise | 420 raise |
| 403 | 421 |
| 404 def do_POST_edits(self): | 422 def do_POST_edits(self): |
| 405 """ Handle a POST request with modifications to GM expectations, in this | 423 """ Handle a POST request with modifications to GM expectations, in this |
| 406 format: | 424 format: |
| 407 | 425 |
| 408 { | 426 { |
| 409 'oldResultsType': 'all', # type of results that the client loaded | 427 KEY__EDITS__OLD_RESULTS_TYPE: 'all', # type of results that the client |
| 410 # and then made modifications to | 428 # loaded and then made |
| 411 'oldResultsHash': 39850913, # hash of results when the client loaded them | 429 # modifications to |
| 412 # (ensures that the client and server apply | 430 KEY__EDITS__OLD_RESULTS_HASH: 39850913, # hash of results when the client |
| 413 # modifications to the same base) | 431 # loaded them (ensures that the |
| 414 'modifications': [ | 432 # client and server apply |
| 415 { | 433 # modifications to the same base) |
| 416 'builder': 'Test-Android-Nexus10-MaliT604-Arm7-Debug', | 434 KEY__EDITS__MODIFICATIONS: [ |
| 417 'test': 'strokerect', | 435 # as needed by results.edit_expectations() |
| 418 'config': 'gpu', | |
| 419 'expectedHashType': 'bitmap-64bitMD5', | |
| 420 'expectedHashDigest': '1707359671708613629', | |
| 421 }, | |
| 422 ... | 436 ... |
| 423 ], | 437 ], |
| 424 } | 438 } |
| 425 | 439 |
| 426 Raises an Exception if there were any problems. | 440 Raises an Exception if there were any problems. |
| 427 """ | 441 """ |
| 428 if not _SERVER.is_editable: | 442 if not _SERVER.is_editable: |
| 429 raise Exception('this server is not running in --editable mode') | 443 raise Exception('this server is not running in --editable mode') |
| 430 | 444 |
| 431 content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE] | 445 content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE] |
| 432 if content_type != 'application/json;charset=UTF-8': | 446 if content_type != 'application/json;charset=UTF-8': |
| 433 raise Exception('unsupported %s [%s]' % ( | 447 raise Exception('unsupported %s [%s]' % ( |
| 434 _HTTP_HEADER_CONTENT_TYPE, content_type)) | 448 _HTTP_HEADER_CONTENT_TYPE, content_type)) |
| 435 | 449 |
| 436 content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH]) | 450 content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH]) |
| 437 json_data = self.rfile.read(content_length) | 451 json_data = self.rfile.read(content_length) |
| 438 data = json.loads(json_data) | 452 data = json.loads(json_data) |
| 439 logging.debug('do_POST_edits: received new GM expectations data [%s]' % | 453 logging.debug('do_POST_edits: received new GM expectations data [%s]' % |
| 440 data) | 454 data) |
| 441 | 455 |
| 442 # Update the results on disk with the information we received from the | 456 # Update the results on disk with the information we received from the |
| 443 # client. | 457 # client. |
| 444 # We must hold _SERVER.results_rlock while we do this, to guarantee that | 458 # We must hold _SERVER.results_rlock while we do this, to guarantee that |
| 445 # no other thread updates expectations (from the Skia repo) while we are | 459 # no other thread updates expectations (from the Skia repo) while we are |
| 446 # updating them (using the info we received from the client). | 460 # updating them (using the info we received from the client). |
| 447 with _SERVER.results_rlock: | 461 with _SERVER.results_rlock: |
| 448 oldResultsType = data['oldResultsType'] | 462 oldResultsType = data[KEY__EDITS__OLD_RESULTS_TYPE] |
| 449 oldResults = _SERVER.results.get_results_of_type(oldResultsType) | 463 oldResults = _SERVER.results.get_results_of_type(oldResultsType) |
| 450 oldResultsHash = str(hash(repr(oldResults['testData']))) | 464 oldResultsHash = str(hash(repr(oldResults[imagepairset.KEY__IMAGEPAIRS]))) |
| 451 if oldResultsHash != data['oldResultsHash']: | 465 if oldResultsHash != data[KEY__EDITS__OLD_RESULTS_HASH]: |
| 452 raise Exception('results of type "%s" changed while the client was ' | 466 raise Exception('results of type "%s" changed while the client was ' |
| 453 'making modifications. The client should reload the ' | 467 'making modifications. The client should reload the ' |
| 454 'results and submit the modifications again.' % | 468 'results and submit the modifications again.' % |
| 455 oldResultsType) | 469 oldResultsType) |
| 456 _SERVER.results.edit_expectations(data['modifications']) | 470 _SERVER.results.edit_expectations(data[KEY__EDITS__MODIFICATIONS]) |
| 457 | 471 |
| 458 # Read the updated results back from disk. | 472 # Read the updated results back from disk. |
| 459 # We can do this in a separate thread; we should return our success message | 473 # We can do this in a separate thread; we should return our success message |
| 460 # to the UI as soon as possible. | 474 # to the UI as soon as possible. |
| 461 thread.start_new_thread(_SERVER.update_results, (True,)) | 475 thread.start_new_thread(_SERVER.update_results, (True,)) |
| 462 | 476 |
| 463 def redirect_to(self, url): | 477 def redirect_to(self, url): |
| 464 """ Redirect the HTTP client to a different url. | 478 """ Redirect the HTTP client to a different url. |
| 465 | 479 |
| 466 Args: | 480 Args: |
| (...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 541 args = parser.parse_args() | 555 args = parser.parse_args() |
| 542 global _SERVER | 556 global _SERVER |
| 543 _SERVER = Server(actuals_dir=args.actuals_dir, | 557 _SERVER = Server(actuals_dir=args.actuals_dir, |
| 544 port=args.port, export=args.export, editable=args.editable, | 558 port=args.port, export=args.export, editable=args.editable, |
| 545 reload_seconds=args.reload) | 559 reload_seconds=args.reload) |
| 546 _SERVER.run() | 560 _SERVER.run() |
| 547 | 561 |
| 548 | 562 |
| 549 if __name__ == '__main__': | 563 if __name__ == '__main__': |
| 550 main() | 564 main() |
| OLD | NEW |