| 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 """ |
| 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 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 |