Index: gm/rebaseline_server/server.py |
=================================================================== |
--- gm/rebaseline_server/server.py (revision 11776) |
+++ gm/rebaseline_server/server.py (working copy) |
@@ -19,6 +19,8 @@ |
import re |
import shutil |
import sys |
+import thread |
+import time |
import urlparse |
# Imports from within Skia |
@@ -38,6 +40,7 @@ |
import results |
ACTUALS_SVN_REPO = 'http://skia-autogen.googlecode.com/svn/gm-actual' |
+EXPECTATIONS_SVN_REPO = 'http://skia.googlecode.com/svn/trunk/expectations/gm' |
PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') |
TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(os.path.dirname( |
os.path.realpath(__file__)))) |
@@ -65,7 +68,8 @@ |
def __init__(self, |
actuals_dir=DEFAULT_ACTUALS_DIR, |
expectations_dir=DEFAULT_EXPECTATIONS_DIR, |
- port=DEFAULT_PORT, export=False): |
+ port=DEFAULT_PORT, export=False, editable=True, |
+ reload_seconds=0): |
""" |
Args: |
actuals_dir: directory under which we will check out the latest actual |
@@ -74,33 +78,60 @@ |
must already be in that directory) |
port: which TCP port to listen on for HTTP requests |
export: whether to allow HTTP clients on other hosts to access this server |
+ editable: whether HTTP clients are allowed to submit new baselines |
+ reload_seconds: polling interval with which to check for new results; |
+ if 0, don't check for new results at all |
""" |
self._actuals_dir = actuals_dir |
self._expectations_dir = expectations_dir |
self._port = port |
self._export = export |
+ self._editable = editable |
+ self._reload_seconds = reload_seconds |
def is_exported(self): |
""" Returns true iff HTTP clients on other hosts are allowed to access |
this server. """ |
return self._export |
- def fetch_results(self): |
- """ Create self.results, based on the expectations in |
+ def is_editable(self): |
+ """ Returns true iff HTTP clients are allowed to submit new baselines. """ |
+ return self._editable |
+ |
+ def reload_seconds(self): |
+ """ Returns the result reload period in seconds, or 0 if we don't reload |
+ results. """ |
+ return self._reload_seconds |
+ |
+ def _update_results(self): |
+ """ Create or update self.results, based on the expectations in |
self._expectations_dir and the latest actuals from skia-autogen. |
- |
- TODO(epoger): Add a new --browseonly mode setting. In that mode, |
- the gm-actuals and expectations will automatically be updated every few |
- minutes. See discussion in https://codereview.chromium.org/24274003/ . |
""" |
- logging.info('Checking out latest actual GM results from %s into %s ...' % ( |
- ACTUALS_SVN_REPO, self._actuals_dir)) |
+ logging.info('Updating actual GM results in %s from SVN repo %s ...' % ( |
+ self._actuals_dir, ACTUALS_SVN_REPO)) |
actuals_repo = svn.Svn(self._actuals_dir) |
if not os.path.isdir(self._actuals_dir): |
os.makedirs(self._actuals_dir) |
actuals_repo.Checkout(ACTUALS_SVN_REPO, '.') |
else: |
actuals_repo.Update('.') |
+ |
+ # We only update the expectations dir if the server was run with a nonzero |
+ # --reload argument; otherwise, we expect the user to maintain her own |
+ # expectations as she sees fit. |
+ # |
+ # TODO(epoger): Use git instead of svn to check out expectations, since |
+ # the Skia repo is moving to git. |
+ if self._reload_seconds: |
+ logging.info('Updating expected GM results in %s from SVN repo %s ...' % ( |
+ self._expectations_dir, EXPECTATIONS_SVN_REPO)) |
+ expectations_repo = svn.Svn(self._expectations_dir) |
+ if not os.path.isdir(self._expectations_dir): |
+ os.makedirs(self._expectations_dir) |
+ expectations_repo.Checkout(EXPECTATIONS_SVN_REPO, '.') |
+ else: |
+ expectations_repo.Update('.') |
+ |
logging.info( |
'Parsing results from actuals in %s and expectations in %s ...' % ( |
self._actuals_dir, self._expectations_dir)) |
@@ -108,12 +139,26 @@ |
actuals_root=self._actuals_dir, |
expected_root=self._expectations_dir) |
+ def _result_reloader(self): |
+ """ If --reload argument was specified, reload results at the appropriate |
+ interval. |
+ """ |
+ while self._reload_seconds: |
+ time.sleep(self._reload_seconds) |
+ with self.results_lock: |
+ self._update_results() |
+ |
def run(self): |
- self.fetch_results() |
+ self._update_results() |
+ self.results_lock = thread.allocate_lock() |
+ thread.start_new_thread(self._result_reloader, ()) |
+ |
if self._export: |
server_address = ('', self._port) |
- logging.warning('Running in "export" mode. Users on other machines will ' |
- 'be able to modify your GM expectations!') |
+ if self._editable: |
+ logging.warning('Running with combination of "export" and "editable" ' |
+ 'flags. Users on other machines will ' |
+ 'be able to modify your GM expectations!') |
else: |
server_address = ('127.0.0.1', self._port) |
http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler) |
@@ -162,17 +207,29 @@ |
# to refer to the Server object, make Server a subclass of |
# HTTPServer, and then it could be available to the handler via |
# the handler's .server instance variable. |
- response_dict = _SERVER.results.get_results_of_type(type) |
+ |
+ with _SERVER.results_lock: |
+ response_dict = _SERVER.results.get_results_of_type(type) |
+ time_updated = _SERVER.results.get_timestamp() |
response_dict['header'] = { |
+ # Timestamps: |
+ # 1. when this data was last updated |
+ # 2. when the caller should check back for new data (if ever) |
+ # |
+ # We only return these timestamps if the --reload argument was passed; |
+ # otherwise, we have no idea when the expectations were last updated |
+ # (we allow the user to maintain her own expectations as she sees fit). |
+ 'timeUpdated': time_updated if _SERVER.reload_seconds() else None, |
+ 'timeNextUpdateAvailable': ( |
+ (time_updated+_SERVER.reload_seconds()) if _SERVER.reload_seconds() |
+ else None), |
+ |
# Hash of testData, which the client must return with any edits-- |
# this ensures that the edits were made to a particular dataset. |
- 'data-hash': str(hash(repr(response_dict['testData']))), |
+ 'dataHash': str(hash(repr(response_dict['testData']))), |
# Whether the server will accept edits back. |
- # TODO(epoger): Not yet implemented, so hardcoding to False; |
- # once we implement the 'browseonly' mode discussed in |
- # https://codereview.chromium.org/24274003/#msg6 , this value will vary. |
- 'isEditable': False, |
+ 'isEditable': _SERVER.is_editable(), |
# Whether the service is accessible from other hosts. |
'isExported': _SERVER.is_exported(), |
@@ -180,6 +237,7 @@ |
self.send_json_dict(response_dict) |
except: |
self.send_error(404) |
+ raise |
def do_GET_static(self, path): |
""" Handle a GET request for a file under the 'static' directory. |
@@ -259,6 +317,9 @@ |
'actual GM results. If this directory does not ' |
'exist, it will be created. Defaults to %(default)s'), |
default=DEFAULT_ACTUALS_DIR) |
+ parser.add_argument('--editable', action='store_true', |
+ help=('TODO(epoger): NOT YET IMPLEMENTED. ' |
+ 'Allow HTTP clients to submit new baselines.')) |
parser.add_argument('--expectations-dir', |
help=('Directory under which to find GM expectations; ' |
'defaults to %(default)s'), |
@@ -268,15 +329,24 @@ |
'on localhost, allow HTTP clients on other hosts ' |
'to access this server. WARNING: doing so will ' |
'allow users on other hosts to modify your ' |
- 'GM expectations!')) |
+ 'GM expectations, if combined with --editable.')) |
parser.add_argument('--port', type=int, |
help=('Which TCP port to listen on for HTTP requests; ' |
'defaults to %(default)s'), |
default=DEFAULT_PORT) |
+ parser.add_argument('--reload', type=int, |
+ help=('How often (a period in seconds) to update the ' |
+ 'results. If specified, both EXPECTATIONS_DIR and ' |
+ 'ACTUAL_DIR will be updated. ' |
+ 'By default, we do not reload at all, and you ' |
+ 'must restart the server to pick up new data.'), |
+ default=0) |
args = parser.parse_args() |
global _SERVER |
- _SERVER = Server(expectations_dir=args.expectations_dir, |
- port=args.port, export=args.export) |
+ _SERVER = Server(actuals_dir=args.actuals_dir, |
+ expectations_dir=args.expectations_dir, |
+ port=args.port, export=args.export, editable=args.editable, |
+ reload_seconds=args.reload) |
_SERVER.run() |
if __name__ == '__main__': |