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

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

Issue 27218005: rebaseline_server: add --editable and --reload flags (Closed) Base URL: http://skia.googlecode.com/svn/trunk/
Patch Set: windowTitle Created 7 years, 2 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 | Annotate | Revision Log
« no previous file with comments | « gm/rebaseline_server/results.py ('k') | gm/rebaseline_server/static/loader.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 """
11 11
12 # System-level imports 12 # System-level imports
13 import argparse 13 import argparse
14 import BaseHTTPServer 14 import BaseHTTPServer
15 import json 15 import json
16 import logging 16 import logging
17 import os 17 import os
18 import posixpath 18 import posixpath
19 import re 19 import re
20 import shutil 20 import shutil
21 import sys 21 import sys
22 import thread
23 import time
22 import urlparse 24 import urlparse
23 25
24 # Imports from within Skia 26 # Imports from within Skia
25 # 27 #
26 # We need to add the 'tools' directory, so that we can import svn.py within 28 # We need to add the 'tools' directory, so that we can import svn.py within
27 # that directory. 29 # that directory.
28 # Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end* 30 # Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end*
29 # so any dirs that are already in the PYTHONPATH will be preferred. 31 # so any dirs that are already in the PYTHONPATH will be preferred.
30 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) 32 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
31 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY)) 33 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY))
32 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') 34 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools')
33 if TOOLS_DIRECTORY not in sys.path: 35 if TOOLS_DIRECTORY not in sys.path:
34 sys.path.append(TOOLS_DIRECTORY) 36 sys.path.append(TOOLS_DIRECTORY)
35 import svn 37 import svn
36 38
37 # Imports from local dir 39 # Imports from local dir
38 import results 40 import results
39 41
40 ACTUALS_SVN_REPO = 'http://skia-autogen.googlecode.com/svn/gm-actual' 42 ACTUALS_SVN_REPO = 'http://skia-autogen.googlecode.com/svn/gm-actual'
43 EXPECTATIONS_SVN_REPO = 'http://skia.googlecode.com/svn/trunk/expectations/gm'
41 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') 44 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)')
42 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(os.path.dirname( 45 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(os.path.dirname(
43 os.path.realpath(__file__)))) 46 os.path.realpath(__file__))))
44 47
45 # 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
46 # 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
47 # has no entry in this dictionary. 50 # has no entry in this dictionary.
48 MIME_TYPE_MAP = {'': 'application/octet-stream', 51 MIME_TYPE_MAP = {'': 'application/octet-stream',
49 'html': 'text/html', 52 'html': 'text/html',
50 'css': 'text/css', 53 'css': 'text/css',
51 'png': 'image/png', 54 'png': 'image/png',
52 'js': 'application/javascript', 55 'js': 'application/javascript',
53 'json': 'application/json' 56 'json': 'application/json'
54 } 57 }
55 58
56 DEFAULT_ACTUALS_DIR = '.gm-actuals' 59 DEFAULT_ACTUALS_DIR = '.gm-actuals'
57 DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') 60 DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm')
58 DEFAULT_PORT = 8888 61 DEFAULT_PORT = 8888
59 62
60 _SERVER = None # This gets filled in by main() 63 _SERVER = None # This gets filled in by main()
61 64
62 class Server(object): 65 class Server(object):
63 """ HTTP server for our HTML rebaseline viewer. """ 66 """ HTTP server for our HTML rebaseline viewer. """
64 67
65 def __init__(self, 68 def __init__(self,
66 actuals_dir=DEFAULT_ACTUALS_DIR, 69 actuals_dir=DEFAULT_ACTUALS_DIR,
67 expectations_dir=DEFAULT_EXPECTATIONS_DIR, 70 expectations_dir=DEFAULT_EXPECTATIONS_DIR,
68 port=DEFAULT_PORT, export=False): 71 port=DEFAULT_PORT, export=False, editable=True,
72 reload_seconds=0):
69 """ 73 """
70 Args: 74 Args:
71 actuals_dir: directory under which we will check out the latest actual 75 actuals_dir: directory under which we will check out the latest actual
72 GM results 76 GM results
73 expectations_dir: directory under which to find GM expectations (they 77 expectations_dir: directory under which to find GM expectations (they
74 must already be in that directory) 78 must already be in that directory)
75 port: which TCP port to listen on for HTTP requests 79 port: which TCP port to listen on for HTTP requests
76 export: whether to allow HTTP clients on other hosts to access this server 80 export: whether to allow HTTP clients on other hosts to access this server
81 editable: whether HTTP clients are allowed to submit new baselines
82 reload_seconds: polling interval with which to check for new results;
83 if 0, don't check for new results at all
77 """ 84 """
78 self._actuals_dir = actuals_dir 85 self._actuals_dir = actuals_dir
79 self._expectations_dir = expectations_dir 86 self._expectations_dir = expectations_dir
80 self._port = port 87 self._port = port
81 self._export = export 88 self._export = export
89 self._editable = editable
90 self._reload_seconds = reload_seconds
82 91
83 def is_exported(self): 92 def is_exported(self):
84 """ Returns true iff HTTP clients on other hosts are allowed to access 93 """ Returns true iff HTTP clients on other hosts are allowed to access
85 this server. """ 94 this server. """
86 return self._export 95 return self._export
87 96
88 def fetch_results(self): 97 def is_editable(self):
89 """ Create self.results, based on the expectations in 98 """ Returns true iff HTTP clients are allowed to submit new baselines. """
99 return self._editable
100
101 def reload_seconds(self):
102 """ Returns the result reload period in seconds, or 0 if we don't reload
103 results. """
104 return self._reload_seconds
105
106 def _update_results(self):
107 """ Create or update self.results, based on the expectations in
90 self._expectations_dir and the latest actuals from skia-autogen. 108 self._expectations_dir and the latest actuals from skia-autogen.
91
92 TODO(epoger): Add a new --browseonly mode setting. In that mode,
93 the gm-actuals and expectations will automatically be updated every few
94 minutes. See discussion in https://codereview.chromium.org/24274003/ .
95 """ 109 """
96 logging.info('Checking out latest actual GM results from %s into %s ...' % ( 110 logging.info('Updating actual GM results in %s from SVN repo %s ...' % (
97 ACTUALS_SVN_REPO, self._actuals_dir)) 111 self._actuals_dir, ACTUALS_SVN_REPO))
98 actuals_repo = svn.Svn(self._actuals_dir) 112 actuals_repo = svn.Svn(self._actuals_dir)
99 if not os.path.isdir(self._actuals_dir): 113 if not os.path.isdir(self._actuals_dir):
100 os.makedirs(self._actuals_dir) 114 os.makedirs(self._actuals_dir)
101 actuals_repo.Checkout(ACTUALS_SVN_REPO, '.') 115 actuals_repo.Checkout(ACTUALS_SVN_REPO, '.')
102 else: 116 else:
103 actuals_repo.Update('.') 117 actuals_repo.Update('.')
118
119 # We only update the expectations dir if the server was run with a nonzero
120 # --reload argument; otherwise, we expect the user to maintain her own
121 # expectations as she sees fit.
122 #
123 # TODO(epoger): Use git instead of svn to check out expectations, since
124 # the Skia repo is moving to git.
125 if self._reload_seconds:
126 logging.info('Updating expected GM results in %s from SVN repo %s ...' % (
127 self._expectations_dir, EXPECTATIONS_SVN_REPO))
128 expectations_repo = svn.Svn(self._expectations_dir)
129 if not os.path.isdir(self._expectations_dir):
130 os.makedirs(self._expectations_dir)
131 expectations_repo.Checkout(EXPECTATIONS_SVN_REPO, '.')
132 else:
133 expectations_repo.Update('.')
134
104 logging.info( 135 logging.info(
105 'Parsing results from actuals in %s and expectations in %s ...' % ( 136 'Parsing results from actuals in %s and expectations in %s ...' % (
106 self._actuals_dir, self._expectations_dir)) 137 self._actuals_dir, self._expectations_dir))
107 self.results = results.Results( 138 self.results = results.Results(
108 actuals_root=self._actuals_dir, 139 actuals_root=self._actuals_dir,
109 expected_root=self._expectations_dir) 140 expected_root=self._expectations_dir)
110 141
142 def _result_reloader(self):
143 """ If --reload argument was specified, reload results at the appropriate
144 interval.
145 """
146 while self._reload_seconds:
147 time.sleep(self._reload_seconds)
148 with self.results_lock:
149 self._update_results()
150
111 def run(self): 151 def run(self):
112 self.fetch_results() 152 self._update_results()
153 self.results_lock = thread.allocate_lock()
154 thread.start_new_thread(self._result_reloader, ())
155
113 if self._export: 156 if self._export:
114 server_address = ('', self._port) 157 server_address = ('', self._port)
115 logging.warning('Running in "export" mode. Users on other machines will ' 158 if self._editable:
116 'be able to modify your GM expectations!') 159 logging.warning('Running with combination of "export" and "editable" '
160 'flags. Users on other machines will '
161 'be able to modify your GM expectations!')
117 else: 162 else:
118 server_address = ('127.0.0.1', self._port) 163 server_address = ('127.0.0.1', self._port)
119 http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler) 164 http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler)
120 logging.info('Ready for requests on http://%s:%d' % ( 165 logging.info('Ready for requests on http://%s:%d' % (
121 http_server.server_name, http_server.server_port)) 166 http_server.server_name, http_server.server_port))
122 http_server.serve_forever() 167 http_server.serve_forever()
123 168
124 169
125 class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 170 class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
126 """ HTTP request handlers for various types of queries this server knows 171 """ HTTP request handlers for various types of queries this server knows
(...skipping 28 matching lines...) Expand all
155 Args: 200 Args:
156 type: string indicating which set of results to return; 201 type: string indicating which set of results to return;
157 must be one of the results.RESULTS_* constants 202 must be one of the results.RESULTS_* constants
158 """ 203 """
159 logging.debug('do_GET_results: sending results of type "%s"' % type) 204 logging.debug('do_GET_results: sending results of type "%s"' % type)
160 try: 205 try:
161 # TODO(epoger): Rather than using a global variable for the handler 206 # TODO(epoger): Rather than using a global variable for the handler
162 # to refer to the Server object, make Server a subclass of 207 # to refer to the Server object, make Server a subclass of
163 # HTTPServer, and then it could be available to the handler via 208 # HTTPServer, and then it could be available to the handler via
164 # the handler's .server instance variable. 209 # the handler's .server instance variable.
165 response_dict = _SERVER.results.get_results_of_type(type) 210
211 with _SERVER.results_lock:
212 response_dict = _SERVER.results.get_results_of_type(type)
213 time_updated = _SERVER.results.get_timestamp()
166 response_dict['header'] = { 214 response_dict['header'] = {
215 # Timestamps:
216 # 1. when this data was last updated
217 # 2. when the caller should check back for new data (if ever)
218 #
219 # We only return these timestamps if the --reload argument was passed;
220 # otherwise, we have no idea when the expectations were last updated
221 # (we allow the user to maintain her own expectations as she sees fit).
222 'timeUpdated': time_updated if _SERVER.reload_seconds() else None,
223 'timeNextUpdateAvailable': (
224 (time_updated+_SERVER.reload_seconds()) if _SERVER.reload_seconds()
225 else None),
226
167 # Hash of testData, which the client must return with any edits-- 227 # Hash of testData, which the client must return with any edits--
168 # this ensures that the edits were made to a particular dataset. 228 # this ensures that the edits were made to a particular dataset.
169 'data-hash': str(hash(repr(response_dict['testData']))), 229 'dataHash': str(hash(repr(response_dict['testData']))),
170 230
171 # Whether the server will accept edits back. 231 # Whether the server will accept edits back.
172 # TODO(epoger): Not yet implemented, so hardcoding to False; 232 'isEditable': _SERVER.is_editable(),
173 # once we implement the 'browseonly' mode discussed in
174 # https://codereview.chromium.org/24274003/#msg6 , this value will vary.
175 'isEditable': False,
176 233
177 # Whether the service is accessible from other hosts. 234 # Whether the service is accessible from other hosts.
178 'isExported': _SERVER.is_exported(), 235 'isExported': _SERVER.is_exported(),
179 } 236 }
180 self.send_json_dict(response_dict) 237 self.send_json_dict(response_dict)
181 except: 238 except:
182 self.send_error(404) 239 self.send_error(404)
240 raise
183 241
184 def do_GET_static(self, path): 242 def do_GET_static(self, path):
185 """ Handle a GET request for a file under the 'static' directory. 243 """ Handle a GET request for a file under the 'static' directory.
186 Only allow serving of files within the 'static' directory that is a 244 Only allow serving of files within the 'static' directory that is a
187 filesystem sibling of this script. 245 filesystem sibling of this script.
188 246
189 Args: 247 Args:
190 path: path to file (under static directory) to retrieve 248 path: path to file (under static directory) to retrieve
191 """ 249 """
192 # Strip arguments ('?resultsToLoad=all') from the path 250 # Strip arguments ('?resultsToLoad=all') from the path
(...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after
252 310
253 311
254 def main(): 312 def main():
255 logging.basicConfig(level=logging.INFO) 313 logging.basicConfig(level=logging.INFO)
256 parser = argparse.ArgumentParser() 314 parser = argparse.ArgumentParser()
257 parser.add_argument('--actuals-dir', 315 parser.add_argument('--actuals-dir',
258 help=('Directory into which we will check out the latest ' 316 help=('Directory into which we will check out the latest '
259 'actual GM results. If this directory does not ' 317 'actual GM results. If this directory does not '
260 'exist, it will be created. Defaults to %(default)s'), 318 'exist, it will be created. Defaults to %(default)s'),
261 default=DEFAULT_ACTUALS_DIR) 319 default=DEFAULT_ACTUALS_DIR)
320 parser.add_argument('--editable', action='store_true',
321 help=('TODO(epoger): NOT YET IMPLEMENTED. '
322 'Allow HTTP clients to submit new baselines.'))
262 parser.add_argument('--expectations-dir', 323 parser.add_argument('--expectations-dir',
263 help=('Directory under which to find GM expectations; ' 324 help=('Directory under which to find GM expectations; '
264 'defaults to %(default)s'), 325 'defaults to %(default)s'),
265 default=DEFAULT_EXPECTATIONS_DIR) 326 default=DEFAULT_EXPECTATIONS_DIR)
266 parser.add_argument('--export', action='store_true', 327 parser.add_argument('--export', action='store_true',
267 help=('Instead of only allowing access from HTTP clients ' 328 help=('Instead of only allowing access from HTTP clients '
268 'on localhost, allow HTTP clients on other hosts ' 329 'on localhost, allow HTTP clients on other hosts '
269 'to access this server. WARNING: doing so will ' 330 'to access this server. WARNING: doing so will '
270 'allow users on other hosts to modify your ' 331 'allow users on other hosts to modify your '
271 'GM expectations!')) 332 'GM expectations, if combined with --editable.'))
272 parser.add_argument('--port', type=int, 333 parser.add_argument('--port', type=int,
273 help=('Which TCP port to listen on for HTTP requests; ' 334 help=('Which TCP port to listen on for HTTP requests; '
274 'defaults to %(default)s'), 335 'defaults to %(default)s'),
275 default=DEFAULT_PORT) 336 default=DEFAULT_PORT)
337 parser.add_argument('--reload', type=int,
338 help=('How often (a period in seconds) to update the '
339 'results. If specified, both EXPECTATIONS_DIR and '
340 'ACTUAL_DIR will be updated. '
341 'By default, we do not reload at all, and you '
342 'must restart the server to pick up new data.'),
343 default=0)
276 args = parser.parse_args() 344 args = parser.parse_args()
277 global _SERVER 345 global _SERVER
278 _SERVER = Server(expectations_dir=args.expectations_dir, 346 _SERVER = Server(actuals_dir=args.actuals_dir,
279 port=args.port, export=args.export) 347 expectations_dir=args.expectations_dir,
348 port=args.port, export=args.export, editable=args.editable,
349 reload_seconds=args.reload)
280 _SERVER.run() 350 _SERVER.run()
281 351
282 if __name__ == '__main__': 352 if __name__ == '__main__':
283 main() 353 main()
OLDNEW
« no previous file with comments | « gm/rebaseline_server/results.py ('k') | gm/rebaseline_server/static/loader.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698