Chromium Code Reviews| 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 23 matching lines...) Expand all Loading... | |
| 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 imagepairset |
| 44 import results | 44 import results as results_mod |
|
rmistry
2014/03/13 13:11:46
This seems inconsistent with other imported module
epoger
2014/03/13 14:37:04
Indeed. I changed this because I noticed that I h
| |
| 45 | 45 |
| 46 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') | 46 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') |
| 47 EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') | |
| 48 GENERATED_IMAGES_ROOT = os.path.join(PARENT_DIRECTORY, 'static', | |
| 49 'generated-images') | |
| 50 | 47 |
| 51 # A simple dictionary of file name extensions to MIME types. The empty string | 48 # 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 | 49 # entry is used as the default when no extension was given or if the extension |
| 53 # has no entry in this dictionary. | 50 # has no entry in this dictionary. |
| 54 MIME_TYPE_MAP = {'': 'application/octet-stream', | 51 MIME_TYPE_MAP = {'': 'application/octet-stream', |
| 55 'html': 'text/html', | 52 'html': 'text/html', |
| 56 'css': 'text/css', | 53 'css': 'text/css', |
| 57 'png': 'image/png', | 54 'png': 'image/png', |
| 58 'js': 'application/javascript', | 55 'js': 'application/javascript', |
| 59 'json': 'application/json' | 56 'json': 'application/json' |
| 60 } | 57 } |
| 61 | 58 |
| 62 # Keys that server.py uses to create the toplevel content header. | 59 # Keys that server.py uses to create the toplevel content header. |
| 63 # NOTE: Keep these in sync with static/constants.js | 60 # NOTE: Keep these in sync with static/constants.js |
| 64 KEY__EDITS__MODIFICATIONS = 'modifications' | 61 KEY__EDITS__MODIFICATIONS = 'modifications' |
| 65 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' | 62 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' |
| 66 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' | 63 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' |
| 67 KEY__HEADER = 'header' | |
| 68 KEY__HEADER__DATAHASH = 'dataHash' | |
| 69 KEY__HEADER__IS_EDITABLE = 'isEditable' | |
| 70 KEY__HEADER__IS_EXPORTED = 'isExported' | |
| 71 KEY__HEADER__IS_STILL_LOADING = 'resultsStillLoading' | |
| 72 KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE = 'timeNextUpdateAvailable' | |
| 73 KEY__HEADER__TIME_UPDATED = 'timeUpdated' | |
| 74 KEY__HEADER__TYPE = 'type' | |
| 75 | 64 |
| 76 DEFAULT_ACTUALS_DIR = '.gm-actuals' | 65 DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR |
| 77 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD' | 66 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD' |
| 78 DEFAULT_ACTUALS_REPO_URL = 'http://skia-autogen.googlecode.com/svn/gm-actual' | 67 DEFAULT_ACTUALS_REPO_URL = 'http://skia-autogen.googlecode.com/svn/gm-actual' |
| 79 DEFAULT_PORT = 8888 | 68 DEFAULT_PORT = 8888 |
| 80 | 69 |
| 81 # How often (in seconds) clients should reload while waiting for initial | 70 # How often (in seconds) clients should reload while waiting for initial |
| 82 # results to load. | 71 # results to load. |
| 83 RELOAD_INTERVAL_UNTIL_READY = 10 | 72 RELOAD_INTERVAL_UNTIL_READY = 10 |
| 84 | 73 |
| 85 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' | 74 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' |
| 86 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' | 75 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' |
| (...skipping 107 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 194 """ Returns true iff HTTP clients are allowed to submit new baselines. """ | 183 """ Returns true iff HTTP clients are allowed to submit new baselines. """ |
| 195 return self._editable | 184 return self._editable |
| 196 | 185 |
| 197 @property | 186 @property |
| 198 def reload_seconds(self): | 187 def reload_seconds(self): |
| 199 """ Returns the result reload period in seconds, or 0 if we don't reload | 188 """ Returns the result reload period in seconds, or 0 if we don't reload |
| 200 results. """ | 189 results. """ |
| 201 return self._reload_seconds | 190 return self._reload_seconds |
| 202 | 191 |
| 203 def update_results(self, invalidate=False): | 192 def update_results(self, invalidate=False): |
| 204 """ Create or update self._results, based on the expectations in | 193 """ Create or update self._results, based on the latest expectations and |
| 205 EXPECTATIONS_DIR and the latest actuals from skia-autogen. | 194 actuals. |
| 206 | 195 |
| 207 We hold self.results_rlock while we do this, to guarantee that no other | 196 We hold self.results_rlock while we do this, to guarantee that no other |
| 208 thread attempts to update either self._results or the underlying files at | 197 thread attempts to update either self._results or the underlying files at |
| 209 the same time. | 198 the same time. |
| 210 | 199 |
| 211 Args: | 200 Args: |
| 212 invalidate: if True, invalidate self._results immediately upon entry; | 201 invalidate: if True, invalidate self._results immediately upon entry; |
| 213 otherwise, we will let readers see those results until we | 202 otherwise, we will let readers see those results until we |
| 214 replace them | 203 replace them |
| 215 """ | 204 """ |
| (...skipping 13 matching lines...) Expand all Loading... | |
| 229 # Because the Skia repo is moving from SVN to git, and git does not | 218 # Because the Skia repo is moving from SVN to git, and git does not |
| 230 # support updating a single directory tree, we have to update the entire | 219 # support updating a single directory tree, we have to update the entire |
| 231 # repo checkout. | 220 # repo checkout. |
| 232 # | 221 # |
| 233 # Because Skia uses depot_tools, we have to update using "gclient sync" | 222 # Because Skia uses depot_tools, we have to update using "gclient sync" |
| 234 # instead of raw git (or SVN) update. Happily, this will work whether | 223 # instead of raw git (or SVN) update. Happily, this will work whether |
| 235 # the checkout was created using git or SVN. | 224 # the checkout was created using git or SVN. |
| 236 if self._reload_seconds: | 225 if self._reload_seconds: |
| 237 logging.info( | 226 logging.info( |
| 238 'Updating expected GM results in %s by syncing Skia repo ...' % | 227 'Updating expected GM results in %s by syncing Skia repo ...' % |
| 239 EXPECTATIONS_DIR) | 228 results_mod.DEFAULT_EXPECTATIONS_DIR) |
| 240 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY) | 229 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY) |
| 241 | 230 |
| 242 self._results = results.Results( | 231 self._results = results_mod.Results(actuals_root=self._actuals_dir) |
| 243 actuals_root=self._actuals_dir, | |
| 244 expected_root=EXPECTATIONS_DIR, | |
| 245 generated_images_root=GENERATED_IMAGES_ROOT) | |
| 246 | 232 |
| 247 def _result_loader(self, reload_seconds=0): | 233 def _result_loader(self, reload_seconds=0): |
| 248 """ Call self.update_results(), either once or periodically. | 234 """ Call self.update_results(), either once or periodically. |
| 249 | 235 |
| 250 Params: | 236 Params: |
| 251 reload_seconds: integer; if nonzero, reload results at this interval | 237 reload_seconds: integer; if nonzero, reload results at this interval |
| 252 (in which case, this method will never return!) | 238 (in which case, this method will never return!) |
| 253 """ | 239 """ |
| 254 self.update_results() | 240 self.update_results() |
| 255 logging.info('Initial results loaded. Ready for requests on %s' % self._url) | 241 logging.info('Initial results loaded. Ready for requests on %s' % self._url) |
| (...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 308 dispatchers = { | 294 dispatchers = { |
| 309 'results': self.do_GET_results, | 295 'results': self.do_GET_results, |
| 310 'static': self.do_GET_static, | 296 'static': self.do_GET_static, |
| 311 } | 297 } |
| 312 dispatcher = dispatchers[dispatcher_name] | 298 dispatcher = dispatchers[dispatcher_name] |
| 313 dispatcher(remainder) | 299 dispatcher(remainder) |
| 314 except: | 300 except: |
| 315 self.send_error(404) | 301 self.send_error(404) |
| 316 raise | 302 raise |
| 317 | 303 |
| 318 def do_GET_results(self, type): | 304 def do_GET_results(self, results_type): |
| 319 """ Handle a GET request for GM results. | 305 """ Handle a GET request for GM results. |
| 320 | 306 |
| 321 Args: | 307 Args: |
| 322 type: string indicating which set of results to return; | 308 results_type: string indicating which set of results to return; |
| 323 must be one of the results.RESULTS_* constants | 309 must be one of the results_mod.RESULTS_* constants |
| 324 """ | 310 """ |
| 325 logging.debug('do_GET_results: sending results of type "%s"' % type) | 311 logging.debug('do_GET_results: sending results of type "%s"' % results_type) |
| 326 # Since we must make multiple calls to the Results object, grab a | 312 # Since we must make multiple calls to the Results object, grab a |
| 327 # reference to it in case it is updated to point at a new Results | 313 # reference to it in case it is updated to point at a new Results |
| 328 # object within another thread. | 314 # object within another thread. |
| 329 # | 315 # |
| 330 # TODO(epoger): Rather than using a global variable for the handler | 316 # TODO(epoger): Rather than using a global variable for the handler |
| 331 # to refer to the Server object, make Server a subclass of | 317 # to refer to the Server object, make Server a subclass of |
| 332 # HTTPServer, and then it could be available to the handler via | 318 # HTTPServer, and then it could be available to the handler via |
| 333 # the handler's .server instance variable. | 319 # the handler's .server instance variable. |
| 334 results_obj = _SERVER.results | 320 results_obj = _SERVER.results |
| 335 if results_obj: | 321 if results_obj: |
| 336 response_dict = self.package_results(results_obj, type) | 322 response_dict = results_obj.get_packaged_results_of_type( |
| 323 results_type=results_type, reload_seconds=_SERVER.reload_seconds, | |
| 324 is_editable=_SERVER.is_editable, is_exported=_SERVER.is_exported) | |
| 337 else: | 325 else: |
| 338 now = int(time.time()) | 326 now = int(time.time()) |
| 339 response_dict = { | 327 response_dict = { |
| 340 KEY__HEADER: { | 328 results_mod.KEY__HEADER: { |
| 341 KEY__HEADER__IS_STILL_LOADING: True, | 329 results_mod.KEY__HEADER__IS_STILL_LOADING: True, |
| 342 KEY__HEADER__TIME_UPDATED: now, | 330 results_mod.KEY__HEADER__TIME_UPDATED: now, |
| 343 KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: ( | 331 results_mod.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: ( |
| 344 now + RELOAD_INTERVAL_UNTIL_READY), | 332 now + RELOAD_INTERVAL_UNTIL_READY), |
| 345 }, | 333 }, |
| 346 } | 334 } |
| 347 self.send_json_dict(response_dict) | 335 self.send_json_dict(response_dict) |
| 348 | 336 |
| 349 def package_results(self, results_obj, type): | |
| 350 """ Given a nonempty "results" object, package it as a response_dict | |
| 351 as needed within do_GET_results. | |
| 352 | |
| 353 Args: | |
| 354 results_obj: nonempty "results" object | |
| 355 type: string indicating which set of results to return; | |
| 356 must be one of the results.RESULTS_* constants | |
| 357 """ | |
| 358 response_dict = results_obj.get_results_of_type(type) | |
| 359 time_updated = results_obj.get_timestamp() | |
| 360 response_dict[KEY__HEADER] = { | |
| 361 # Timestamps: | |
| 362 # 1. when this data was last updated | |
| 363 # 2. when the caller should check back for new data (if ever) | |
| 364 # | |
| 365 # We only return these timestamps if the --reload argument was passed; | |
| 366 # otherwise, we have no idea when the expectations were last updated | |
| 367 # (we allow the user to maintain her own expectations as she sees fit). | |
| 368 KEY__HEADER__TIME_UPDATED: | |
| 369 time_updated if _SERVER.reload_seconds else None, | |
| 370 KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: | |
| 371 (time_updated+_SERVER.reload_seconds) if _SERVER.reload_seconds | |
| 372 else None, | |
| 373 | |
| 374 # The type we passed to get_results_of_type() | |
| 375 KEY__HEADER__TYPE: type, | |
| 376 | |
| 377 # Hash of dataset, which the client must return with any edits-- | |
| 378 # this ensures that the edits were made to a particular dataset. | |
| 379 KEY__HEADER__DATAHASH: str(hash(repr( | |
| 380 response_dict[imagepairset.KEY__IMAGEPAIRS]))), | |
| 381 | |
| 382 # Whether the server will accept edits back. | |
| 383 KEY__HEADER__IS_EDITABLE: _SERVER.is_editable, | |
| 384 | |
| 385 # Whether the service is accessible from other hosts. | |
| 386 KEY__HEADER__IS_EXPORTED: _SERVER.is_exported, | |
| 387 } | |
| 388 return response_dict | |
| 389 | |
| 390 def do_GET_static(self, path): | 337 def do_GET_static(self, path): |
| 391 """ Handle a GET request for a file under the 'static' directory. | 338 """ Handle a GET request for a file under the 'static' directory. |
| 392 Only allow serving of files within the 'static' directory that is a | 339 Only allow serving of files within the 'static' directory that is a |
| 393 filesystem sibling of this script. | 340 filesystem sibling of this script. |
| 394 | 341 |
| 395 Args: | 342 Args: |
| 396 path: path to file (under static directory) to retrieve | 343 path: path to file (under static directory) to retrieve |
| 397 """ | 344 """ |
| 398 # Strip arguments ('?resultsToLoad=all') from the path | 345 # Strip arguments ('?resultsToLoad=all') from the path |
| 399 path = urlparse.urlparse(path).path | 346 path = urlparse.urlparse(path).path |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 434 | 381 |
| 435 { | 382 { |
| 436 KEY__EDITS__OLD_RESULTS_TYPE: 'all', # type of results that the client | 383 KEY__EDITS__OLD_RESULTS_TYPE: 'all', # type of results that the client |
| 437 # loaded and then made | 384 # loaded and then made |
| 438 # modifications to | 385 # modifications to |
| 439 KEY__EDITS__OLD_RESULTS_HASH: 39850913, # hash of results when the client | 386 KEY__EDITS__OLD_RESULTS_HASH: 39850913, # hash of results when the client |
| 440 # loaded them (ensures that the | 387 # loaded them (ensures that the |
| 441 # client and server apply | 388 # client and server apply |
| 442 # modifications to the same base) | 389 # modifications to the same base) |
| 443 KEY__EDITS__MODIFICATIONS: [ | 390 KEY__EDITS__MODIFICATIONS: [ |
| 444 # as needed by results.edit_expectations() | 391 # as needed by results_mod.edit_expectations() |
| 445 ... | 392 ... |
| 446 ], | 393 ], |
| 447 } | 394 } |
| 448 | 395 |
| 449 Raises an Exception if there were any problems. | 396 Raises an Exception if there were any problems. |
| 450 """ | 397 """ |
| 451 if not _SERVER.is_editable: | 398 if not _SERVER.is_editable: |
| 452 raise Exception('this server is not running in --editable mode') | 399 raise Exception('this server is not running in --editable mode') |
| 453 | 400 |
| 454 content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE] | 401 content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE] |
| (...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 576 _SERVER = Server(actuals_dir=args.actuals_dir, | 523 _SERVER = Server(actuals_dir=args.actuals_dir, |
| 577 actuals_repo_revision=args.actuals_revision, | 524 actuals_repo_revision=args.actuals_revision, |
| 578 actuals_repo_url=args.actuals_repo, | 525 actuals_repo_url=args.actuals_repo, |
| 579 port=args.port, export=args.export, editable=args.editable, | 526 port=args.port, export=args.export, editable=args.editable, |
| 580 reload_seconds=args.reload) | 527 reload_seconds=args.reload) |
| 581 _SERVER.run() | 528 _SERVER.run() |
| 582 | 529 |
| 583 | 530 |
| 584 if __name__ == '__main__': | 531 if __name__ == '__main__': |
| 585 main() | 532 main() |
| OLD | NEW |