rebaseline_server: make --reload work in git checkout

(SkipBuildbotRuns)

R=rmistry@google.com

Review URL: https://codereview.chromium.org/86343002

git-svn-id: http://skia.googlecode.com/svn/trunk@12397 2bbb7eff-a529-9590-31e7-b0007b416f81
This commit is contained in:
epoger@google.com 2013-11-26 17:59:28 +00:00
parent 7affd1fd6a
commit d7255b6ae4

View File

@ -19,8 +19,10 @@ import posixpath
import re
import shutil
import socket
import subprocess
import sys
import thread
import threading
import time
import urlparse
@ -42,8 +44,6 @@ import results
ACTUALS_SVN_REPO = 'http://skia-autogen.googlecode.com/svn/gm-actual'
PATHSPLIT_RE = re.compile('/([^/]+)/(.+)')
TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(os.path.dirname(
os.path.realpath(__file__))))
EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm')
GENERATED_IMAGES_ROOT = os.path.join(PARENT_DIRECTORY, 'static',
'generated-images')
@ -67,6 +67,29 @@ _HTTP_HEADER_CONTENT_TYPE = 'Content-Type'
_SERVER = None # This gets filled in by main()
def _run_command(args, directory):
"""Runs a command and returns stdout as a single string.
Args:
args: the command to run, as a list of arguments
directory: directory within which to run the command
Returns: stdout, as a string
Raises an Exception if the command failed (exited with nonzero return code).
"""
logging.debug('_run_command: %s in directory %s' % (args, directory))
proc = subprocess.Popen(args, cwd=directory,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(stdout, stderr) = proc.communicate()
if proc.returncode is not 0:
raise Exception('command "%s" failed in dir "%s": %s' %
(args, directory, stderr))
return stdout
def _get_routable_ip_address():
"""Returns routable IP address of this host (the IP address of its network
interface that would be used for most traffic, not its localhost
@ -77,6 +100,7 @@ def _get_routable_ip_address():
sock.close()
return host
def _create_svn_checkout(dir_path, repo_url):
"""Creates local checkout of an SVN repository at the specified directory
path, returning an svn.Svn object referring to the local checkout.
@ -120,69 +144,87 @@ class Server(object):
self._actuals_repo = _create_svn_checkout(
dir_path=actuals_dir, repo_url=ACTUALS_SVN_REPO)
# 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 update the expectations dir, since
# the Skia repo is moving to git.
# When we make that change, we will have to update the entire workspace,
# not just the expectations dir, because git only operates on the repo
# as a whole.
# And since Skia uses depot_tools to manage its dependencies, we will have
# to run "gclient sync" rather than a raw "git pull".
if reload_seconds:
self._expectations_repo = svn.Svn(EXPECTATIONS_DIR)
else:
self._expectations_repo = None
# Reentrant lock that must be held whenever updating EITHER of:
# 1. self._results
# 2. the expected or actual results on local disk
self.results_rlock = threading.RLock()
# self._results will be filled in by calls to update_results()
self._results = None
@property
def results(self):
""" Returns the most recently generated results, or None if update_results()
has not been called yet. """
return self._results
@property
def is_exported(self):
""" Returns true iff HTTP clients on other hosts are allowed to access
this server. """
return self._export
@property
def is_editable(self):
""" Returns true iff HTTP clients are allowed to submit new baselines. """
return self._editable
@property
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
""" Create or update self._results, based on the expectations in
EXPECTATIONS_DIR and the latest actuals from skia-autogen.
We hold self.results_rlock while we do this, to guarantee that no other
thread attempts to update either self._results or the underlying files at
the same time.
"""
logging.info('Updating actual GM results in %s from SVN repo %s ...' % (
self._actuals_dir, ACTUALS_SVN_REPO))
self._actuals_repo.Update('.')
with self.results_rlock:
logging.info('Updating actual GM results in %s from SVN repo %s ...' % (
self._actuals_dir, ACTUALS_SVN_REPO))
self._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.
#
# Because the Skia repo is moving from SVN to git, and git does not
# support updating a single directory tree, we have to update the entire
# repo checkout.
#
# Because Skia uses depot_tools, we have to update using "gclient sync"
# instead of raw git (or SVN) update. Happily, this will work whether
# the checkout was created using git or SVN.
if self._reload_seconds:
logging.info(
'Updating expected GM results in %s by syncing Skia repo ...' %
EXPECTATIONS_DIR)
_run_command(['gclient', 'sync'], TRUNK_DIRECTORY)
if self._expectations_repo:
logging.info(
'Updating expected GM results in %s ...' % EXPECTATIONS_DIR)
self._expectations_repo.Update('.')
logging.info(
('Parsing results from actuals in %s and expectations in %s, '
+ 'and generating pixel diffs (may take a while) ...') % (
self._actuals_dir, EXPECTATIONS_DIR))
self.results = results.Results(
actuals_root=self._actuals_dir,
expected_root=EXPECTATIONS_DIR,
generated_images_root=GENERATED_IMAGES_ROOT)
+ 'and generating pixel diffs (may take a while) ...') % (
self._actuals_dir, EXPECTATIONS_DIR))
self._results = results.Results(
actuals_root=self._actuals_dir,
expected_root=EXPECTATIONS_DIR,
generated_images_root=GENERATED_IMAGES_ROOT)
def _result_reloader(self):
""" If --reload argument was specified, reload results at the appropriate
interval.
""" Reload results at the appropriate interval. This never exits, so it
should be run in its own thread.
"""
while self._reload_seconds:
while True:
time.sleep(self._reload_seconds)
self.update_results()
def run(self):
self.update_results()
thread.start_new_thread(self._result_reloader, ())
if self._reload_seconds:
thread.start_new_thread(self._result_reloader, ())
if self._export:
server_address = ('', self._port)
@ -256,9 +298,9 @@ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
# 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,
'timeUpdated': time_updated if _SERVER.reload_seconds else None,
'timeNextUpdateAvailable': (
(time_updated+_SERVER.reload_seconds()) if _SERVER.reload_seconds()
(time_updated+_SERVER.reload_seconds) if _SERVER.reload_seconds
else None),
# The type we passed to get_results_of_type()
@ -269,10 +311,10 @@ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
'dataHash': str(hash(repr(response_dict['testData']))),
# Whether the server will accept edits back.
'isEditable': _SERVER.is_editable(),
'isEditable': _SERVER.is_editable,
# Whether the service is accessible from other hosts.
'isExported': _SERVER.is_exported(),
'isExported': _SERVER.is_exported,
}
self.send_json_dict(response_dict)
except:
@ -343,7 +385,7 @@ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
Raises an Exception if there were any problems.
"""
if not _SERVER.is_editable():
if not _SERVER.is_editable:
raise Exception('this server is not running in --editable mode')
content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE]
@ -357,22 +399,23 @@ class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
logging.debug('do_POST_edits: received new GM expectations data [%s]' %
data)
# Since we must make multiple calls to the Results object, grab a
# reference to it in case it is updated to point at a new Results
# object within another thread.
results_obj = _SERVER.results
oldResultsType = data['oldResultsType']
oldResults = results_obj.get_results_of_type(oldResultsType)
oldResultsHash = str(hash(repr(oldResults['testData'])))
if oldResultsHash != data['oldResultsHash']:
raise Exception('results of type "%s" changed while the client was '
'making modifications. The client should reload the '
'results and submit the modifications again.' %
oldResultsType)
results_obj.edit_expectations(data['modifications'])
# Now that the edits have been committed, update results to reflect them.
_SERVER.update_results()
# Update the results on disk with the information we received from the
# client.
# We must hold _SERVER.results_rlock while we do this, to guarantee that
# no other thread updates expectations (from the Skia repo) while we are
# updating them (using the info we received from the client).
with _SERVER.results_rlock:
oldResultsType = data['oldResultsType']
oldResults = _SERVER.results.get_results_of_type(oldResultsType)
oldResultsHash = str(hash(repr(oldResults['testData'])))
if oldResultsHash != data['oldResultsHash']:
raise Exception('results of type "%s" changed while the client was '
'making modifications. The client should reload the '
'results and submit the modifications again.' %
oldResultsType)
_SERVER.results.edit_expectations(data['modifications'])
# Read the updated results back from disk.
_SERVER.update_results()
def redirect_to(self, url):
""" Redirect the HTTP client to a different url.
@ -445,7 +488,8 @@ def main():
parser.add_argument('--reload', type=int,
help=('How often (a period in seconds) to update the '
'results. If specified, both expected and actual '
'results will be updated. '
'results will be updated by running "gclient sync" '
'on your Skia checkout as a whole. '
'By default, we do not reload at all, and you '
'must restart the server to pick up new data.'),
default=0)
@ -456,5 +500,6 @@ def main():
reload_seconds=args.reload)
_SERVER.run()
if __name__ == '__main__':
main()