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

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: 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
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 23 matching lines...) Expand all
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
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
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
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
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
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()
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698