Chromium Code Reviews| 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 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 61 | 61 |
| 62 DEFAULT_ACTUALS_DIR = '.gm-actuals' | 62 DEFAULT_ACTUALS_DIR = '.gm-actuals' |
| 63 DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') | 63 DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') |
| 64 DEFAULT_PORT = 8888 | 64 DEFAULT_PORT = 8888 |
| 65 | 65 |
| 66 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' | 66 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' |
| 67 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' | 67 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' |
| 68 | 68 |
| 69 _SERVER = None # This gets filled in by main() | 69 _SERVER = None # This gets filled in by main() |
| 70 | 70 |
| 71 def get_routable_ip_address(): | 71 def _get_routable_ip_address(): |
|
epoger
2013/11/20 19:51:34
made this function private (it's only used interna
| |
| 72 """Returns routable IP address of this host (the IP address of its network | 72 """Returns routable IP address of this host (the IP address of its network |
| 73 interface that would be used for most traffic, not its localhost | 73 interface that would be used for most traffic, not its localhost |
| 74 interface). See http://stackoverflow.com/a/166589 """ | 74 interface). See http://stackoverflow.com/a/166589 """ |
| 75 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | 75 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| 76 sock.connect(('8.8.8.8', 80)) | 76 sock.connect(('8.8.8.8', 80)) |
| 77 host = sock.getsockname()[0] | 77 host = sock.getsockname()[0] |
| 78 sock.close() | 78 sock.close() |
| 79 return host | 79 return host |
| 80 | 80 |
| 81 def _create_svn_checkout(dir_path, repo_url): | |
| 82 """Creates local checkout of an SVN repository at the specified directory | |
| 83 path, returning an svn.Svn object referring to the local checkout. | |
| 84 | |
| 85 Args: | |
| 86 dir_path: path to the local checkout; if this directory does not yet exist, | |
| 87 it will be created and the repo will be checked out into it | |
| 88 repo_url: URL of SVN repo to check out into dir_path (unless the local | |
| 89 checkout already exists) | |
| 90 Returns: an svn.Svn object referring to the local checkout. | |
| 91 """ | |
| 92 local_checkout = svn.Svn(dir_path) | |
| 93 if not os.path.isdir(dir_path): | |
| 94 os.makedirs(dir_path) | |
| 95 local_checkout.Checkout(repo_url, '.') | |
| 96 return local_checkout | |
| 97 | |
| 81 | 98 |
| 82 class Server(object): | 99 class Server(object): |
| 83 """ HTTP server for our HTML rebaseline viewer. """ | 100 """ HTTP server for our HTML rebaseline viewer. """ |
| 84 | 101 |
| 85 def __init__(self, | 102 def __init__(self, |
| 86 actuals_dir=DEFAULT_ACTUALS_DIR, | 103 actuals_dir=DEFAULT_ACTUALS_DIR, |
| 87 expectations_dir=DEFAULT_EXPECTATIONS_DIR, | 104 expectations_dir=DEFAULT_EXPECTATIONS_DIR, |
| 88 port=DEFAULT_PORT, export=False, editable=True, | 105 port=DEFAULT_PORT, export=False, editable=True, |
| 89 reload_seconds=0): | 106 reload_seconds=0): |
| 90 """ | 107 """ |
| 91 Args: | 108 Args: |
| 92 actuals_dir: directory under which we will check out the latest actual | 109 actuals_dir: directory under which we will check out the latest actual |
| 93 GM results | 110 GM results |
| 94 expectations_dir: directory under which to find GM expectations (they | 111 expectations_dir: directory under which to find GM expectations (they |
| 95 must already be in that directory) | 112 must already be in that directory) |
| 96 port: which TCP port to listen on for HTTP requests | 113 port: which TCP port to listen on for HTTP requests |
| 97 export: whether to allow HTTP clients on other hosts to access this server | 114 export: whether to allow HTTP clients on other hosts to access this server |
| 98 editable: whether HTTP clients are allowed to submit new baselines | 115 editable: whether HTTP clients are allowed to submit new baselines |
| 99 reload_seconds: polling interval with which to check for new results; | 116 reload_seconds: polling interval with which to check for new results; |
| 100 if 0, don't check for new results at all | 117 if 0, don't check for new results at all |
| 101 """ | 118 """ |
| 102 self._actuals_dir = actuals_dir | 119 self._actuals_dir = actuals_dir |
| 103 self._expectations_dir = expectations_dir | 120 self._expectations_dir = expectations_dir |
| 104 self._port = port | 121 self._port = port |
| 105 self._export = export | 122 self._export = export |
| 106 self._editable = editable | 123 self._editable = editable |
| 107 self._reload_seconds = reload_seconds | 124 self._reload_seconds = reload_seconds |
| 125 self._actuals_repo = _create_svn_checkout( | |
| 126 dir_path=actuals_dir, repo_url=ACTUALS_SVN_REPO) | |
| 127 | |
| 128 # We only update the expectations dir if the server was run with a | |
| 129 # nonzero --reload argument; otherwise, we expect the user to maintain | |
| 130 # her own expectations as she sees fit. | |
| 131 # | |
| 132 # TODO(epoger): Use git instead of svn to check out expectations, since | |
| 133 # the Skia repo is moving to git. | |
| 134 if reload_seconds: | |
| 135 self._expectations_repo = _create_svn_checkout( | |
| 136 dir_path=expectations_dir, repo_url=EXPECTATIONS_SVN_REPO) | |
| 137 else: | |
| 138 self._expectations_repo = None | |
| 108 | 139 |
| 109 def is_exported(self): | 140 def is_exported(self): |
| 110 """ Returns true iff HTTP clients on other hosts are allowed to access | 141 """ Returns true iff HTTP clients on other hosts are allowed to access |
| 111 this server. """ | 142 this server. """ |
| 112 return self._export | 143 return self._export |
| 113 | 144 |
| 114 def is_editable(self): | 145 def is_editable(self): |
| 115 """ Returns true iff HTTP clients are allowed to submit new baselines. """ | 146 """ Returns true iff HTTP clients are allowed to submit new baselines. """ |
| 116 return self._editable | 147 return self._editable |
| 117 | 148 |
| 118 def reload_seconds(self): | 149 def reload_seconds(self): |
| 119 """ Returns the result reload period in seconds, or 0 if we don't reload | 150 """ Returns the result reload period in seconds, or 0 if we don't reload |
| 120 results. """ | 151 results. """ |
| 121 return self._reload_seconds | 152 return self._reload_seconds |
| 122 | 153 |
| 123 def update_results(self): | 154 def update_results(self): |
| 124 """ Create or update self.results, based on the expectations in | 155 """ Create or update self.results, based on the expectations in |
| 125 self._expectations_dir and the latest actuals from skia-autogen. | 156 self._expectations_dir and the latest actuals from skia-autogen. |
| 126 """ | 157 """ |
| 127 with self._svn_update_lock: | 158 logging.info('Updating actual GM results in %s from SVN repo %s ...' % ( |
| 128 # self._svn_update_lock prevents us from updating the actual GM results | 159 self._actuals_dir, ACTUALS_SVN_REPO)) |
| 129 # in multiple threads simultaneously | 160 self._actuals_repo.Update('.') |
| 130 logging.info('Updating actual GM results in %s from SVN repo %s ...' % ( | 161 |
| 131 self._actuals_dir, ACTUALS_SVN_REPO)) | 162 if self._expectations_repo: |
| 132 actuals_repo = svn.Svn(self._actuals_dir) | 163 logging.info( |
| 133 if not os.path.isdir(self._actuals_dir): | 164 'Updating expected GM results in %s from SVN repo %s ...' % ( |
| 134 os.makedirs(self._actuals_dir) | 165 self._expectations_dir, EXPECTATIONS_SVN_REPO)) |
| 135 actuals_repo.Checkout(ACTUALS_SVN_REPO, '.') | 166 self._expectations_repo.Update('.') |
| 136 else: | |
| 137 actuals_repo.Update('.') | |
| 138 # We only update the expectations dir if the server was run with a | |
| 139 # nonzero --reload argument; otherwise, we expect the user to maintain | |
| 140 # her own expectations as she sees fit. | |
| 141 # | |
| 142 # self._svn_update_lock prevents us from updating the expected GM results | |
| 143 # in multiple threads simultaneously | |
| 144 # | |
| 145 # TODO(epoger): Use git instead of svn to check out expectations, since | |
| 146 # the Skia repo is moving to git. | |
| 147 if self._reload_seconds: | |
| 148 logging.info( | |
| 149 'Updating expected GM results in %s from SVN repo %s ...' % ( | |
| 150 self._expectations_dir, EXPECTATIONS_SVN_REPO)) | |
| 151 expectations_repo = svn.Svn(self._expectations_dir) | |
| 152 if not os.path.isdir(self._expectations_dir): | |
| 153 os.makedirs(self._expectations_dir) | |
| 154 expectations_repo.Checkout(EXPECTATIONS_SVN_REPO, '.') | |
| 155 else: | |
| 156 expectations_repo.Update('.') | |
| 157 # end of "with self._svn_update_lock:" | |
| 158 | 167 |
| 159 logging.info( | 168 logging.info( |
| 160 ('Parsing results from actuals in %s and expectations in %s, ' | 169 ('Parsing results from actuals in %s and expectations in %s, ' |
| 161 + 'and generating pixel diffs (may take a while) ...') % ( | 170 + 'and generating pixel diffs (may take a while) ...') % ( |
| 162 self._actuals_dir, self._expectations_dir)) | 171 self._actuals_dir, self._expectations_dir)) |
| 163 new_results = results.Results( | 172 self.results = results.Results( |
| 164 actuals_root=self._actuals_dir, | 173 actuals_root=self._actuals_dir, |
| 165 expected_root=self._expectations_dir, | 174 expected_root=self._expectations_dir, |
| 166 generated_images_root=GENERATED_IMAGES_ROOT) | 175 generated_images_root=GENERATED_IMAGES_ROOT) |
| 167 | 176 |
| 168 # Make sure we don't update self.results while a client is in the middle | |
| 169 # of reading from it. | |
| 170 with self.results_lock: | |
| 171 self.results = new_results | |
| 172 | |
| 173 def _result_reloader(self): | 177 def _result_reloader(self): |
| 174 """ If --reload argument was specified, reload results at the appropriate | 178 """ If --reload argument was specified, reload results at the appropriate |
| 175 interval. | 179 interval. |
| 176 """ | 180 """ |
| 177 while self._reload_seconds: | 181 while self._reload_seconds: |
| 178 time.sleep(self._reload_seconds) | 182 time.sleep(self._reload_seconds) |
| 179 self.update_results() | 183 self.update_results() |
| 180 | 184 |
| 181 def run(self): | 185 def run(self): |
| 182 self.results_lock = thread.allocate_lock() | |
| 183 self._svn_update_lock = thread.allocate_lock() | |
| 184 self.update_results() | 186 self.update_results() |
| 185 thread.start_new_thread(self._result_reloader, ()) | 187 thread.start_new_thread(self._result_reloader, ()) |
| 186 | 188 |
| 187 if self._export: | 189 if self._export: |
| 188 server_address = ('', self._port) | 190 server_address = ('', self._port) |
| 189 host = get_routable_ip_address() | 191 host = _get_routable_ip_address() |
| 190 if self._editable: | 192 if self._editable: |
| 191 logging.warning('Running with combination of "export" and "editable" ' | 193 logging.warning('Running with combination of "export" and "editable" ' |
| 192 'flags. Users on other machines will ' | 194 'flags. Users on other machines will ' |
| 193 'be able to modify your GM expectations!') | 195 'be able to modify your GM expectations!') |
| 194 else: | 196 else: |
| 195 host = '127.0.0.1' | 197 host = '127.0.0.1' |
| 196 server_address = (host, self._port) | 198 server_address = (host, self._port) |
| 197 http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler) | 199 http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler) |
| 198 logging.info('Ready for requests on http://%s:%d' % ( | 200 logging.info('Ready for requests on http://%s:%d' % ( |
| 199 host, http_server.server_port)) | 201 host, http_server.server_port)) |
| (...skipping 29 matching lines...) Expand all Loading... | |
| 229 | 231 |
| 230 def do_GET_results(self, type): | 232 def do_GET_results(self, type): |
| 231 """ Handle a GET request for GM results. | 233 """ Handle a GET request for GM results. |
| 232 | 234 |
| 233 Args: | 235 Args: |
| 234 type: string indicating which set of results to return; | 236 type: string indicating which set of results to return; |
| 235 must be one of the results.RESULTS_* constants | 237 must be one of the results.RESULTS_* constants |
| 236 """ | 238 """ |
| 237 logging.debug('do_GET_results: sending results of type "%s"' % type) | 239 logging.debug('do_GET_results: sending results of type "%s"' % type) |
| 238 try: | 240 try: |
| 241 # Since we must make multiple calls to the Results object, grab a | |
| 242 # reference to it in case it is updated to point at a new Results | |
| 243 # object within another thread. | |
| 244 # | |
| 239 # TODO(epoger): Rather than using a global variable for the handler | 245 # TODO(epoger): Rather than using a global variable for the handler |
| 240 # to refer to the Server object, make Server a subclass of | 246 # to refer to the Server object, make Server a subclass of |
| 241 # HTTPServer, and then it could be available to the handler via | 247 # HTTPServer, and then it could be available to the handler via |
| 242 # the handler's .server instance variable. | 248 # the handler's .server instance variable. |
| 249 results_obj = _SERVER.results | |
| 250 response_dict = results_obj.get_results_of_type(type) | |
| 251 time_updated = results_obj.get_timestamp() | |
| 243 | 252 |
| 244 with _SERVER.results_lock: | |
| 245 response_dict = _SERVER.results.get_results_of_type(type) | |
| 246 time_updated = _SERVER.results.get_timestamp() | |
| 247 response_dict['header'] = { | 253 response_dict['header'] = { |
| 248 # Timestamps: | 254 # Timestamps: |
| 249 # 1. when this data was last updated | 255 # 1. when this data was last updated |
| 250 # 2. when the caller should check back for new data (if ever) | 256 # 2. when the caller should check back for new data (if ever) |
| 251 # | 257 # |
| 252 # We only return these timestamps if the --reload argument was passed; | 258 # We only return these timestamps if the --reload argument was passed; |
| 253 # otherwise, we have no idea when the expectations were last updated | 259 # otherwise, we have no idea when the expectations were last updated |
| 254 # (we allow the user to maintain her own expectations as she sees fit). | 260 # (we allow the user to maintain her own expectations as she sees fit). |
| 255 'timeUpdated': time_updated if _SERVER.reload_seconds() else None, | 261 'timeUpdated': time_updated if _SERVER.reload_seconds() else None, |
| 256 'timeNextUpdateAvailable': ( | 262 'timeNextUpdateAvailable': ( |
| (...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 346 if content_type != 'application/json;charset=UTF-8': | 352 if content_type != 'application/json;charset=UTF-8': |
| 347 raise Exception('unsupported %s [%s]' % ( | 353 raise Exception('unsupported %s [%s]' % ( |
| 348 _HTTP_HEADER_CONTENT_TYPE, content_type)) | 354 _HTTP_HEADER_CONTENT_TYPE, content_type)) |
| 349 | 355 |
| 350 content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH]) | 356 content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH]) |
| 351 json_data = self.rfile.read(content_length) | 357 json_data = self.rfile.read(content_length) |
| 352 data = json.loads(json_data) | 358 data = json.loads(json_data) |
| 353 logging.debug('do_POST_edits: received new GM expectations data [%s]' % | 359 logging.debug('do_POST_edits: received new GM expectations data [%s]' % |
| 354 data) | 360 data) |
| 355 | 361 |
| 356 with _SERVER.results_lock: | 362 # Since we must make multiple calls to the Results object, grab a |
| 357 oldResultsType = data['oldResultsType'] | 363 # reference to it in case it is updated to point at a new Results |
| 358 oldResults = _SERVER.results.get_results_of_type(oldResultsType) | 364 # object within another thread. |
| 359 oldResultsHash = str(hash(repr(oldResults['testData']))) | 365 results_obj = _SERVER.results |
| 360 if oldResultsHash != data['oldResultsHash']: | 366 oldResultsType = data['oldResultsType'] |
| 361 raise Exception('results of type "%s" changed while the client was ' | 367 oldResults = results_obj.get_results_of_type(oldResultsType) |
| 362 'making modifications. The client should reload the ' | 368 oldResultsHash = str(hash(repr(oldResults['testData']))) |
| 363 'results and submit the modifications again.' % | 369 if oldResultsHash != data['oldResultsHash']: |
| 364 oldResultsType) | 370 raise Exception('results of type "%s" changed while the client was ' |
| 365 _SERVER.results.edit_expectations(data['modifications']) | 371 'making modifications. The client should reload the ' |
| 372 'results and submit the modifications again.' % | |
| 373 oldResultsType) | |
| 374 results_obj.edit_expectations(data['modifications']) | |
| 366 | 375 |
| 367 # Now that the edits have been committed, update results to reflect them. | 376 # Now that the edits have been committed, update results to reflect them. |
| 368 _SERVER.update_results() | 377 _SERVER.update_results() |
| 369 | 378 |
| 370 def redirect_to(self, url): | 379 def redirect_to(self, url): |
| 371 """ Redirect the HTTP client to a different url. | 380 """ Redirect the HTTP client to a different url. |
| 372 | 381 |
| 373 Args: | 382 Args: |
| 374 url: URL to redirect the HTTP client to | 383 url: URL to redirect the HTTP client to |
| 375 """ | 384 """ |
| (...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 449 args = parser.parse_args() | 458 args = parser.parse_args() |
| 450 global _SERVER | 459 global _SERVER |
| 451 _SERVER = Server(actuals_dir=args.actuals_dir, | 460 _SERVER = Server(actuals_dir=args.actuals_dir, |
| 452 expectations_dir=args.expectations_dir, | 461 expectations_dir=args.expectations_dir, |
| 453 port=args.port, export=args.export, editable=args.editable, | 462 port=args.port, export=args.export, editable=args.editable, |
| 454 reload_seconds=args.reload) | 463 reload_seconds=args.reload) |
| 455 _SERVER.run() | 464 _SERVER.run() |
| 456 | 465 |
| 457 if __name__ == '__main__': | 466 if __name__ == '__main__': |
| 458 main() | 467 main() |
| OLD | NEW |