OLD | NEW |
---|---|
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 Loading... | |
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 Loading... | |
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 """ |
113 with self.results_lock: | |
114 self._update_results_inside_lock() | |
115 | |
116 def _update_results_inside_lock(self): | |
117 """ Implementation of update_results(), which assumes that the appropriate | |
118 lock is being held. | |
119 """ | |
borenet
2013/10/23 14:22:03
Any reason not to move this implementation into up
epoger
2013/10/23 14:42:14
No reason at all! I was just trying to reduce the
| |
110 logging.info('Updating actual GM results in %s from SVN repo %s ...' % ( | 120 logging.info('Updating actual GM results in %s from SVN repo %s ...' % ( |
111 self._actuals_dir, ACTUALS_SVN_REPO)) | 121 self._actuals_dir, ACTUALS_SVN_REPO)) |
112 actuals_repo = svn.Svn(self._actuals_dir) | 122 actuals_repo = svn.Svn(self._actuals_dir) |
113 if not os.path.isdir(self._actuals_dir): | 123 if not os.path.isdir(self._actuals_dir): |
114 os.makedirs(self._actuals_dir) | 124 os.makedirs(self._actuals_dir) |
115 actuals_repo.Checkout(ACTUALS_SVN_REPO, '.') | 125 actuals_repo.Checkout(ACTUALS_SVN_REPO, '.') |
116 else: | 126 else: |
117 actuals_repo.Update('.') | 127 actuals_repo.Update('.') |
118 | 128 |
119 # We only update the expectations dir if the server was run with a nonzero | 129 # We only update the expectations dir if the server was run with a nonzero |
(...skipping 18 matching lines...) Expand all Loading... | |
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 Loading... | |
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() |
OLD | NEW |