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

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

Issue 28903008: rebaseline_server: add tabs, and ability to submit new baselines to the server (Closed) Base URL: http://skia.googlecode.com/svn/trunk/
Patch Set: more 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 """
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after
53 'css': 'text/css', 53 'css': 'text/css',
54 'png': 'image/png', 54 'png': 'image/png',
55 'js': 'application/javascript', 55 'js': 'application/javascript',
56 'json': 'application/json' 56 'json': 'application/json'
57 } 57 }
58 58
59 DEFAULT_ACTUALS_DIR = '.gm-actuals' 59 DEFAULT_ACTUALS_DIR = '.gm-actuals'
60 DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') 60 DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm')
61 DEFAULT_PORT = 8888 61 DEFAULT_PORT = 8888
62 62
63 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length'
64 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type'
65
63 _SERVER = None # This gets filled in by main() 66 _SERVER = None # This gets filled in by main()
64 67
65 class Server(object): 68 class Server(object):
66 """ HTTP server for our HTML rebaseline viewer. """ 69 """ HTTP server for our HTML rebaseline viewer. """
67 70
68 def __init__(self, 71 def __init__(self,
69 actuals_dir=DEFAULT_ACTUALS_DIR, 72 actuals_dir=DEFAULT_ACTUALS_DIR,
70 expectations_dir=DEFAULT_EXPECTATIONS_DIR, 73 expectations_dir=DEFAULT_EXPECTATIONS_DIR,
71 port=DEFAULT_PORT, export=False, editable=True, 74 port=DEFAULT_PORT, export=False, editable=True,
72 reload_seconds=0): 75 reload_seconds=0):
(...skipping 23 matching lines...) Expand all
96 99
97 def is_editable(self): 100 def is_editable(self):
98 """ Returns true iff HTTP clients are allowed to submit new baselines. """ 101 """ Returns true iff HTTP clients are allowed to submit new baselines. """
99 return self._editable 102 return self._editable
100 103
101 def reload_seconds(self): 104 def reload_seconds(self):
102 """ Returns the result reload period in seconds, or 0 if we don't reload 105 """ Returns the result reload period in seconds, or 0 if we don't reload
103 results. """ 106 results. """
104 return self._reload_seconds 107 return self._reload_seconds
105 108
106 def _update_results(self): 109 def update_results(self):
107 """ Create or update self.results, based on the expectations in 110 """ Create or update self.results, based on the expectations in
108 self._expectations_dir and the latest actuals from skia-autogen. 111 self._expectations_dir and the latest actuals from skia-autogen.
109 """ 112 """
110 logging.info('Updating actual GM results in %s from SVN repo %s ...' % ( 113 with self.results_lock:
111 self._actuals_dir, ACTUALS_SVN_REPO)) 114 # self.results_lock prevents us from updating the actual GM results
112 actuals_repo = svn.Svn(self._actuals_dir) 115 # in multiple threads simultaneously
113 if not os.path.isdir(self._actuals_dir): 116 logging.info('Updating actual GM results in %s from SVN repo %s ...' % (
114 os.makedirs(self._actuals_dir) 117 self._actuals_dir, ACTUALS_SVN_REPO))
115 actuals_repo.Checkout(ACTUALS_SVN_REPO, '.') 118 actuals_repo = svn.Svn(self._actuals_dir)
116 else: 119 if not os.path.isdir(self._actuals_dir):
117 actuals_repo.Update('.') 120 os.makedirs(self._actuals_dir)
121 actuals_repo.Checkout(ACTUALS_SVN_REPO, '.')
122 else:
123 actuals_repo.Update('.')
118 124
119 # We only update the expectations dir if the server was run with a nonzero 125 # We only update the expectations dir if the server was run with a
120 # --reload argument; otherwise, we expect the user to maintain her own 126 # nonzero --reload argument; otherwise, we expect the user to maintain
121 # expectations as she sees fit. 127 # her own expectations as she sees fit.
122 # 128 #
123 # TODO(epoger): Use git instead of svn to check out expectations, since 129 # self.results_lock prevents us from updating the expected GM results
124 # the Skia repo is moving to git. 130 # in multiple threads simultaneously
125 if self._reload_seconds: 131 #
126 logging.info('Updating expected GM results in %s from SVN repo %s ...' % ( 132 # TODO(epoger): Use git instead of svn to check out expectations, since
127 self._expectations_dir, EXPECTATIONS_SVN_REPO)) 133 # the Skia repo is moving to git.
128 expectations_repo = svn.Svn(self._expectations_dir) 134 if self._reload_seconds:
129 if not os.path.isdir(self._expectations_dir): 135 logging.info(
130 os.makedirs(self._expectations_dir) 136 'Updating expected GM results in %s from SVN repo %s ...' % (
131 expectations_repo.Checkout(EXPECTATIONS_SVN_REPO, '.') 137 self._expectations_dir, EXPECTATIONS_SVN_REPO))
132 else: 138 expectations_repo = svn.Svn(self._expectations_dir)
133 expectations_repo.Update('.') 139 if not os.path.isdir(self._expectations_dir):
140 os.makedirs(self._expectations_dir)
141 expectations_repo.Checkout(EXPECTATIONS_SVN_REPO, '.')
142 else:
143 expectations_repo.Update('.')
134 144
135 logging.info( 145 logging.info(
136 'Parsing results from actuals in %s and expectations in %s ...' % ( 146 'Parsing results from actuals in %s and expectations in %s ...' % (
137 self._actuals_dir, self._expectations_dir)) 147 self._actuals_dir, self._expectations_dir))
138 self.results = results.Results( 148 self.results = results.Results(
139 actuals_root=self._actuals_dir, 149 actuals_root=self._actuals_dir,
140 expected_root=self._expectations_dir) 150 expected_root=self._expectations_dir)
141 151
142 def _result_reloader(self): 152 def _result_reloader(self):
143 """ If --reload argument was specified, reload results at the appropriate 153 """ If --reload argument was specified, reload results at the appropriate
144 interval. 154 interval.
145 """ 155 """
146 while self._reload_seconds: 156 while self._reload_seconds:
147 time.sleep(self._reload_seconds) 157 time.sleep(self._reload_seconds)
148 with self.results_lock: 158 self.update_results()
149 self._update_results()
150 159
151 def run(self): 160 def run(self):
152 self._update_results()
153 self.results_lock = thread.allocate_lock() 161 self.results_lock = thread.allocate_lock()
162 self.update_results()
154 thread.start_new_thread(self._result_reloader, ()) 163 thread.start_new_thread(self._result_reloader, ())
155 164
156 if self._export: 165 if self._export:
157 server_address = ('', self._port) 166 server_address = ('', self._port)
158 if self._editable: 167 if self._editable:
159 logging.warning('Running with combination of "export" and "editable" ' 168 logging.warning('Running with combination of "export" and "editable" '
160 'flags. Users on other machines will ' 169 'flags. Users on other machines will '
161 'be able to modify your GM expectations!') 170 'be able to modify your GM expectations!')
162 else: 171 else:
163 server_address = ('127.0.0.1', self._port) 172 server_address = ('127.0.0.1', self._port)
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after
217 # 2. when the caller should check back for new data (if ever) 226 # 2. when the caller should check back for new data (if ever)
218 # 227 #
219 # We only return these timestamps if the --reload argument was passed; 228 # We only return these timestamps if the --reload argument was passed;
220 # otherwise, we have no idea when the expectations were last updated 229 # 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). 230 # (we allow the user to maintain her own expectations as she sees fit).
222 'timeUpdated': time_updated if _SERVER.reload_seconds() else None, 231 'timeUpdated': time_updated if _SERVER.reload_seconds() else None,
223 'timeNextUpdateAvailable': ( 232 'timeNextUpdateAvailable': (
224 (time_updated+_SERVER.reload_seconds()) if _SERVER.reload_seconds() 233 (time_updated+_SERVER.reload_seconds()) if _SERVER.reload_seconds()
225 else None), 234 else None),
226 235
236 # The type we passed to get_results_of_type()
237 'type': type,
238
227 # Hash of testData, which the client must return with any edits-- 239 # Hash of testData, which the client must return with any edits--
228 # this ensures that the edits were made to a particular dataset. 240 # this ensures that the edits were made to a particular dataset.
229 'dataHash': str(hash(repr(response_dict['testData']))), 241 'dataHash': str(hash(repr(response_dict['testData']))),
230 242
231 # Whether the server will accept edits back. 243 # Whether the server will accept edits back.
232 'isEditable': _SERVER.is_editable(), 244 'isEditable': _SERVER.is_editable(),
233 245
234 # Whether the service is accessible from other hosts. 246 # Whether the service is accessible from other hosts.
235 'isExported': _SERVER.is_exported(), 247 'isExported': _SERVER.is_exported(),
236 } 248 }
(...skipping 17 matching lines...) Expand all
254 static_dir = os.path.realpath(os.path.join(PARENT_DIRECTORY, 'static')) 266 static_dir = os.path.realpath(os.path.join(PARENT_DIRECTORY, 'static'))
255 full_path = os.path.realpath(os.path.join(static_dir, path)) 267 full_path = os.path.realpath(os.path.join(static_dir, path))
256 if full_path.startswith(static_dir): 268 if full_path.startswith(static_dir):
257 self.send_file(full_path) 269 self.send_file(full_path)
258 else: 270 else:
259 logging.error( 271 logging.error(
260 'Attempted do_GET_static() of path [%s] outside of static dir [%s]' 272 'Attempted do_GET_static() of path [%s] outside of static dir [%s]'
261 % (full_path, static_dir)) 273 % (full_path, static_dir))
262 self.send_error(404) 274 self.send_error(404)
263 275
276 def do_POST(self):
277 """ Handles all POST requests, forwarding them to the appropriate
278 do_POST_* dispatcher. """
279 # All requests must be of this form:
280 # /dispatcher
281 # where 'dispatcher' indicates which do_POST_* dispatcher to run.
282 normpath = posixpath.normpath(self.path)
283 dispatchers = {
284 '/edits': self.do_POST_edits,
285 }
286 try:
287 dispatcher = dispatchers[normpath]
288 dispatcher()
289 self.send_response(200)
290 except:
291 self.send_error(404)
292 raise
293
294 def do_POST_edits(self):
295 """ Handle a POST request with modifications to GM expectations, in this
296 format:
297
298 {
299 'oldResultsType': 'all', # type of results that the client loaded
300 # and then made modifications to
301 'oldResultsHash': 39850913, # hash of results when the client loaded them
302 # (ensures that the client and server apply
303 # modifications to the same base)
304 'modifications': [
305 {
306 'builder': 'Test-Android-Nexus10-MaliT604-Arm7-Debug',
307 'test': 'strokerect',
308 'config': 'gpu',
309 'expectedHashType': 'bitmap-64bitMD5',
310 'expectedHashDigest': '1707359671708613629',
311 },
312 ...
313 ],
314 }
315
316 Raises an Exception if there were any problems.
317 """
318 if not _SERVER.is_editable():
319 raise Exception('this server is not running in --editable mode')
320
321 content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE]
322 if content_type != 'application/json;charset=UTF-8':
323 raise Exception('unsupported %s [%s]' % (
324 _HTTP_HEADER_CONTENT_TYPE, content_type))
325
326 content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH])
327 json_data = self.rfile.read(content_length)
328 data = json.loads(json_data)
329 logging.debug('do_POST_edits: received new GM expectations data [%s]' %
330 data)
331
332 with _SERVER.results_lock:
333 oldResultsType = data['oldResultsType']
334 oldResults = _SERVER.results.get_results_of_type(oldResultsType)
335 oldResultsHash = str(hash(repr(oldResults['testData'])))
336 if oldResultsHash != data['oldResultsHash']:
337 raise Exception('results of type "%s" changed while the client was '
338 'making modifications. The client should reload the '
339 'results and submit the modifications again.' %
340 oldResultsType)
341 _SERVER.results.edit_expectations(data['modifications'])
342
343 # Now that the edits have been committed, update results to reflect them.
344 _SERVER.update_results()
345
264 def redirect_to(self, url): 346 def redirect_to(self, url):
265 """ Redirect the HTTP client to a different url. 347 """ Redirect the HTTP client to a different url.
266 348
267 Args: 349 Args:
268 url: URL to redirect the HTTP client to 350 url: URL to redirect the HTTP client to
269 """ 351 """
270 self.send_response(301) 352 self.send_response(301)
271 self.send_header('Location', url) 353 self.send_header('Location', url)
272 self.end_headers() 354 self.end_headers()
273 355
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
311 393
312 def main(): 394 def main():
313 logging.basicConfig(level=logging.INFO) 395 logging.basicConfig(level=logging.INFO)
314 parser = argparse.ArgumentParser() 396 parser = argparse.ArgumentParser()
315 parser.add_argument('--actuals-dir', 397 parser.add_argument('--actuals-dir',
316 help=('Directory into which we will check out the latest ' 398 help=('Directory into which we will check out the latest '
317 'actual GM results. If this directory does not ' 399 'actual GM results. If this directory does not '
318 'exist, it will be created. Defaults to %(default)s'), 400 'exist, it will be created. Defaults to %(default)s'),
319 default=DEFAULT_ACTUALS_DIR) 401 default=DEFAULT_ACTUALS_DIR)
320 parser.add_argument('--editable', action='store_true', 402 parser.add_argument('--editable', action='store_true',
321 help=('TODO(epoger): NOT YET IMPLEMENTED. ' 403 help=('Allow HTTP clients to submit new baselines.'))
322 'Allow HTTP clients to submit new baselines.'))
323 parser.add_argument('--expectations-dir', 404 parser.add_argument('--expectations-dir',
324 help=('Directory under which to find GM expectations; ' 405 help=('Directory under which to find GM expectations; '
325 'defaults to %(default)s'), 406 'defaults to %(default)s'),
326 default=DEFAULT_EXPECTATIONS_DIR) 407 default=DEFAULT_EXPECTATIONS_DIR)
327 parser.add_argument('--export', action='store_true', 408 parser.add_argument('--export', action='store_true',
328 help=('Instead of only allowing access from HTTP clients ' 409 help=('Instead of only allowing access from HTTP clients '
329 'on localhost, allow HTTP clients on other hosts ' 410 'on localhost, allow HTTP clients on other hosts '
330 'to access this server. WARNING: doing so will ' 411 'to access this server. WARNING: doing so will '
331 'allow users on other hosts to modify your ' 412 'allow users on other hosts to modify your '
332 'GM expectations, if combined with --editable.')) 413 'GM expectations, if combined with --editable.'))
(...skipping 11 matching lines...) Expand all
344 args = parser.parse_args() 425 args = parser.parse_args()
345 global _SERVER 426 global _SERVER
346 _SERVER = Server(actuals_dir=args.actuals_dir, 427 _SERVER = Server(actuals_dir=args.actuals_dir,
347 expectations_dir=args.expectations_dir, 428 expectations_dir=args.expectations_dir,
348 port=args.port, export=args.export, editable=args.editable, 429 port=args.port, export=args.export, editable=args.editable,
349 reload_seconds=args.reload) 430 reload_seconds=args.reload)
350 _SERVER.run() 431 _SERVER.run()
351 432
352 if __name__ == '__main__': 433 if __name__ == '__main__':
353 main() 434 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