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 |