Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(694)

Side by Side Diff: gm/rebaseline_server/server.py

Issue 195943004: rebaseline_server: generate JSON that can be viewed without a live server (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: comment Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « gm/rebaseline_server/results.py ('k') | gm/rebaseline_server/static/constants.js » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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 #
44 # Note: we import results under a different name, to avoid confusion with the
45 # Server.results() property. See discussion at
46 # https://codereview.chromium.org/195943004/diff/1/gm/rebaseline_server/server.p y#newcode44
43 import imagepairset 47 import imagepairset
44 import results 48 import results as results_mod
45 49
46 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') 50 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 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
62 # Keys that server.py uses to create the toplevel content header. 63 # Keys that server.py uses to create the toplevel content header.
63 # NOTE: Keep these in sync with static/constants.js 64 # NOTE: Keep these in sync with static/constants.js
64 KEY__EDITS__MODIFICATIONS = 'modifications' 65 KEY__EDITS__MODIFICATIONS = 'modifications'
65 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' 66 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash'
66 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' 67 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 68
76 DEFAULT_ACTUALS_DIR = '.gm-actuals' 69 DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR
77 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD' 70 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD'
78 DEFAULT_ACTUALS_REPO_URL = 'http://skia-autogen.googlecode.com/svn/gm-actual' 71 DEFAULT_ACTUALS_REPO_URL = 'http://skia-autogen.googlecode.com/svn/gm-actual'
79 DEFAULT_PORT = 8888 72 DEFAULT_PORT = 8888
80 73
81 # How often (in seconds) clients should reload while waiting for initial 74 # How often (in seconds) clients should reload while waiting for initial
82 # results to load. 75 # results to load.
83 RELOAD_INTERVAL_UNTIL_READY = 10 76 RELOAD_INTERVAL_UNTIL_READY = 10
84 77
85 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' 78 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length'
86 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' 79 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type'
(...skipping 107 matching lines...) Expand 10 before | Expand all | Expand 10 after
194 """ Returns true iff HTTP clients are allowed to submit new baselines. """ 187 """ Returns true iff HTTP clients are allowed to submit new baselines. """
195 return self._editable 188 return self._editable
196 189
197 @property 190 @property
198 def reload_seconds(self): 191 def reload_seconds(self):
199 """ Returns the result reload period in seconds, or 0 if we don't reload 192 """ Returns the result reload period in seconds, or 0 if we don't reload
200 results. """ 193 results. """
201 return self._reload_seconds 194 return self._reload_seconds
202 195
203 def update_results(self, invalidate=False): 196 def update_results(self, invalidate=False):
204 """ Create or update self._results, based on the expectations in 197 """ Create or update self._results, based on the latest expectations and
205 EXPECTATIONS_DIR and the latest actuals from skia-autogen. 198 actuals.
206 199
207 We hold self.results_rlock while we do this, to guarantee that no other 200 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 201 thread attempts to update either self._results or the underlying files at
209 the same time. 202 the same time.
210 203
211 Args: 204 Args:
212 invalidate: if True, invalidate self._results immediately upon entry; 205 invalidate: if True, invalidate self._results immediately upon entry;
213 otherwise, we will let readers see those results until we 206 otherwise, we will let readers see those results until we
214 replace them 207 replace them
215 """ 208 """
(...skipping 13 matching lines...) Expand all
229 # Because the Skia repo is moving from SVN to git, and git does not 222 # 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 223 # support updating a single directory tree, we have to update the entire
231 # repo checkout. 224 # repo checkout.
232 # 225 #
233 # Because Skia uses depot_tools, we have to update using "gclient sync" 226 # 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 227 # instead of raw git (or SVN) update. Happily, this will work whether
235 # the checkout was created using git or SVN. 228 # the checkout was created using git or SVN.
236 if self._reload_seconds: 229 if self._reload_seconds:
237 logging.info( 230 logging.info(
238 'Updating expected GM results in %s by syncing Skia repo ...' % 231 'Updating expected GM results in %s by syncing Skia repo ...' %
239 EXPECTATIONS_DIR) 232 results_mod.DEFAULT_EXPECTATIONS_DIR)
240 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY) 233 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY)
241 234
242 self._results = results.Results( 235 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 236
247 def _result_loader(self, reload_seconds=0): 237 def _result_loader(self, reload_seconds=0):
248 """ Call self.update_results(), either once or periodically. 238 """ Call self.update_results(), either once or periodically.
249 239
250 Params: 240 Params:
251 reload_seconds: integer; if nonzero, reload results at this interval 241 reload_seconds: integer; if nonzero, reload results at this interval
252 (in which case, this method will never return!) 242 (in which case, this method will never return!)
253 """ 243 """
254 self.update_results() 244 self.update_results()
255 logging.info('Initial results loaded. Ready for requests on %s' % self._url) 245 logging.info('Initial results loaded. Ready for requests on %s' % self._url)
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after
308 dispatchers = { 298 dispatchers = {
309 'results': self.do_GET_results, 299 'results': self.do_GET_results,
310 'static': self.do_GET_static, 300 'static': self.do_GET_static,
311 } 301 }
312 dispatcher = dispatchers[dispatcher_name] 302 dispatcher = dispatchers[dispatcher_name]
313 dispatcher(remainder) 303 dispatcher(remainder)
314 except: 304 except:
315 self.send_error(404) 305 self.send_error(404)
316 raise 306 raise
317 307
318 def do_GET_results(self, type): 308 def do_GET_results(self, results_type):
319 """ Handle a GET request for GM results. 309 """ Handle a GET request for GM results.
320 310
321 Args: 311 Args:
322 type: string indicating which set of results to return; 312 results_type: string indicating which set of results to return;
323 must be one of the results.RESULTS_* constants 313 must be one of the results_mod.RESULTS_* constants
324 """ 314 """
325 logging.debug('do_GET_results: sending results of type "%s"' % type) 315 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 316 # 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 317 # reference to it in case it is updated to point at a new Results
328 # object within another thread. 318 # object within another thread.
329 # 319 #
330 # TODO(epoger): Rather than using a global variable for the handler 320 # TODO(epoger): Rather than using a global variable for the handler
331 # to refer to the Server object, make Server a subclass of 321 # to refer to the Server object, make Server a subclass of
332 # HTTPServer, and then it could be available to the handler via 322 # HTTPServer, and then it could be available to the handler via
333 # the handler's .server instance variable. 323 # the handler's .server instance variable.
334 results_obj = _SERVER.results 324 results_obj = _SERVER.results
335 if results_obj: 325 if results_obj:
336 response_dict = self.package_results(results_obj, type) 326 response_dict = results_obj.get_packaged_results_of_type(
327 results_type=results_type, reload_seconds=_SERVER.reload_seconds,
328 is_editable=_SERVER.is_editable, is_exported=_SERVER.is_exported)
337 else: 329 else:
338 now = int(time.time()) 330 now = int(time.time())
339 response_dict = { 331 response_dict = {
340 KEY__HEADER: { 332 results_mod.KEY__HEADER: {
341 KEY__HEADER__IS_STILL_LOADING: True, 333 results_mod.KEY__HEADER__IS_STILL_LOADING: True,
342 KEY__HEADER__TIME_UPDATED: now, 334 results_mod.KEY__HEADER__TIME_UPDATED: now,
343 KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: ( 335 results_mod.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: (
344 now + RELOAD_INTERVAL_UNTIL_READY), 336 now + RELOAD_INTERVAL_UNTIL_READY),
345 }, 337 },
346 } 338 }
347 self.send_json_dict(response_dict) 339 self.send_json_dict(response_dict)
348 340
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): 341 def do_GET_static(self, path):
391 """ Handle a GET request for a file under the 'static' directory. 342 """ Handle a GET request for a file under the 'static' directory.
392 Only allow serving of files within the 'static' directory that is a 343 Only allow serving of files within the 'static' directory that is a
393 filesystem sibling of this script. 344 filesystem sibling of this script.
394 345
395 Args: 346 Args:
396 path: path to file (under static directory) to retrieve 347 path: path to file (under static directory) to retrieve
397 """ 348 """
398 # Strip arguments ('?resultsToLoad=all') from the path 349 # Strip arguments ('?resultsToLoad=all') from the path
399 path = urlparse.urlparse(path).path 350 path = urlparse.urlparse(path).path
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after
434 385
435 { 386 {
436 KEY__EDITS__OLD_RESULTS_TYPE: 'all', # type of results that the client 387 KEY__EDITS__OLD_RESULTS_TYPE: 'all', # type of results that the client
437 # loaded and then made 388 # loaded and then made
438 # modifications to 389 # modifications to
439 KEY__EDITS__OLD_RESULTS_HASH: 39850913, # hash of results when the client 390 KEY__EDITS__OLD_RESULTS_HASH: 39850913, # hash of results when the client
440 # loaded them (ensures that the 391 # loaded them (ensures that the
441 # client and server apply 392 # client and server apply
442 # modifications to the same base) 393 # modifications to the same base)
443 KEY__EDITS__MODIFICATIONS: [ 394 KEY__EDITS__MODIFICATIONS: [
444 # as needed by results.edit_expectations() 395 # as needed by results_mod.edit_expectations()
445 ... 396 ...
446 ], 397 ],
447 } 398 }
448 399
449 Raises an Exception if there were any problems. 400 Raises an Exception if there were any problems.
450 """ 401 """
451 if not _SERVER.is_editable: 402 if not _SERVER.is_editable:
452 raise Exception('this server is not running in --editable mode') 403 raise Exception('this server is not running in --editable mode')
453 404
454 content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE] 405 content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE]
(...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after
576 _SERVER = Server(actuals_dir=args.actuals_dir, 527 _SERVER = Server(actuals_dir=args.actuals_dir,
577 actuals_repo_revision=args.actuals_revision, 528 actuals_repo_revision=args.actuals_revision,
578 actuals_repo_url=args.actuals_repo, 529 actuals_repo_url=args.actuals_repo,
579 port=args.port, export=args.export, editable=args.editable, 530 port=args.port, export=args.export, editable=args.editable,
580 reload_seconds=args.reload) 531 reload_seconds=args.reload)
581 _SERVER.run() 532 _SERVER.run()
582 533
583 534
584 if __name__ == '__main__': 535 if __name__ == '__main__':
585 main() 536 main()
OLDNEW
« no previous file with comments | « gm/rebaseline_server/results.py ('k') | gm/rebaseline_server/static/constants.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698