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 |