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

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

Issue 178253010: rebaseline_server: use new intermediate JSON format (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: incorporate Ravi's suggestions 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_test.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 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
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
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
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()
OLDNEW
« no previous file with comments | « gm/rebaseline_server/results_test.py ('k') | gm/rebaseline_server/static/constants.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698