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 """ |
| 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 socket | 21 import socket |
| 22 import subprocess | |
| 22 import sys | 23 import sys |
| 23 import thread | 24 import thread |
| 25 import threading | |
| 24 import time | 26 import time |
| 25 import urlparse | 27 import urlparse |
| 26 | 28 |
| 27 # Imports from within Skia | 29 # Imports from within Skia |
| 28 # | 30 # |
| 29 # We need to add the 'tools' directory, so that we can import svn.py within | 31 # We need to add the 'tools' directory, so that we can import svn.py within |
| 30 # that directory. | 32 # that directory. |
| 31 # Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end* | 33 # Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end* |
| 32 # so any dirs that are already in the PYTHONPATH will be preferred. | 34 # so any dirs that are already in the PYTHONPATH will be preferred. |
| 33 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) | 35 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) |
| 34 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY)) | 36 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY)) |
| 35 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') | 37 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') |
| 36 if TOOLS_DIRECTORY not in sys.path: | 38 if TOOLS_DIRECTORY not in sys.path: |
| 37 sys.path.append(TOOLS_DIRECTORY) | 39 sys.path.append(TOOLS_DIRECTORY) |
| 38 import svn | 40 import svn |
| 39 | 41 |
| 40 # Imports from local dir | 42 # Imports from local dir |
| 41 import results | 43 import results |
| 42 | 44 |
| 43 ACTUALS_SVN_REPO = 'http://skia-autogen.googlecode.com/svn/gm-actual' | 45 ACTUALS_SVN_REPO = 'http://skia-autogen.googlecode.com/svn/gm-actual' |
| 44 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') | 46 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') |
| 45 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(os.path.dirname( | |
| 46 os.path.realpath(__file__)))) | |
| 47 EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') | 47 EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') |
| 48 GENERATED_IMAGES_ROOT = os.path.join(PARENT_DIRECTORY, 'static', | 48 GENERATED_IMAGES_ROOT = os.path.join(PARENT_DIRECTORY, 'static', |
| 49 'generated-images') | 49 'generated-images') |
| 50 | 50 |
| 51 # A simple dictionary of file name extensions to MIME types. The empty string | 51 # A simple dictionary of file name extensions to MIME types. The empty string |
| 52 # entry is used as the default when no extension was given or if the extension | 52 # entry is used as the default when no extension was given or if the extension |
| 53 # has no entry in this dictionary. | 53 # has no entry in this dictionary. |
| 54 MIME_TYPE_MAP = {'': 'application/octet-stream', | 54 MIME_TYPE_MAP = {'': 'application/octet-stream', |
| 55 'html': 'text/html', | 55 'html': 'text/html', |
| 56 'css': 'text/css', | 56 'css': 'text/css', |
| 57 'png': 'image/png', | 57 'png': 'image/png', |
| 58 'js': 'application/javascript', | 58 'js': 'application/javascript', |
| 59 'json': 'application/json' | 59 'json': 'application/json' |
| 60 } | 60 } |
| 61 | 61 |
| 62 DEFAULT_ACTUALS_DIR = '.gm-actuals' | 62 DEFAULT_ACTUALS_DIR = '.gm-actuals' |
| 63 DEFAULT_PORT = 8888 | 63 DEFAULT_PORT = 8888 |
| 64 | 64 |
| 65 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' | 65 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' |
| 66 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' | 66 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' |
| 67 | 67 |
| 68 _SERVER = None # This gets filled in by main() | 68 _SERVER = None # This gets filled in by main() |
| 69 | 69 |
| 70 def _run_command(args, directory): | |
| 71 """Runs a command and returns stdout as a single string. | |
| 72 | |
| 73 Args: | |
| 74 args: the command to run, as a list of arguments | |
| 75 directory: directory within which to run the command | |
| 76 | |
| 77 Returns: stdout, as a string | |
| 78 | |
| 79 Raises an Exception if the command failed (exited with nonzero return code). | |
| 80 """ | |
| 81 logging.debug('_run_command: %s in directory %s' % (args, directory)) | |
| 82 proc = subprocess.Popen(args, cwd=directory, | |
| 83 stdout=subprocess.PIPE, | |
| 84 stderr=subprocess.PIPE) | |
| 85 (stdout, stderr) = proc.communicate() | |
| 86 if proc.returncode is not 0: | |
| 87 raise Exception('command "%s" failed in dir "%s": %s' % | |
| 88 (args, directory, stderr)) | |
| 89 return stdout | |
| 90 | |
| 70 def _get_routable_ip_address(): | 91 def _get_routable_ip_address(): |
| 71 """Returns routable IP address of this host (the IP address of its network | 92 """Returns routable IP address of this host (the IP address of its network |
| 72 interface that would be used for most traffic, not its localhost | 93 interface that would be used for most traffic, not its localhost |
| 73 interface). See http://stackoverflow.com/a/166589 """ | 94 interface). See http://stackoverflow.com/a/166589 """ |
| 74 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | 95 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| 75 sock.connect(('8.8.8.8', 80)) | 96 sock.connect(('8.8.8.8', 80)) |
| 76 host = sock.getsockname()[0] | 97 host = sock.getsockname()[0] |
| 77 sock.close() | 98 sock.close() |
| 78 return host | 99 return host |
| 79 | 100 |
| (...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 113 if 0, don't check for new results at all | 134 if 0, don't check for new results at all |
| 114 """ | 135 """ |
| 115 self._actuals_dir = actuals_dir | 136 self._actuals_dir = actuals_dir |
| 116 self._port = port | 137 self._port = port |
| 117 self._export = export | 138 self._export = export |
| 118 self._editable = editable | 139 self._editable = editable |
| 119 self._reload_seconds = reload_seconds | 140 self._reload_seconds = reload_seconds |
| 120 self._actuals_repo = _create_svn_checkout( | 141 self._actuals_repo = _create_svn_checkout( |
| 121 dir_path=actuals_dir, repo_url=ACTUALS_SVN_REPO) | 142 dir_path=actuals_dir, repo_url=ACTUALS_SVN_REPO) |
| 122 | 143 |
| 123 # We only update the expectations dir if the server was run with a | 144 # Reentrant lock that must be held whenever updating EITHER of: |
|
epoger
2013/11/25 19:43:47
We didn't need this lock before, because the self.
| |
| 124 # nonzero --reload argument; otherwise, we expect the user to maintain | 145 # 1. self.results |
| 125 # her own expectations as she sees fit. | 146 # 2. the expected or actual results on local disk |
| 126 # | 147 self.results_rlock = threading.RLock() |
| 127 # TODO(epoger): Use git instead of svn to update the expectations dir, since | |
| 128 # the Skia repo is moving to git. | |
| 129 # When we make that change, we will have to update the entire workspace, | |
| 130 # not just the expectations dir, because git only operates on the repo | |
| 131 # as a whole. | |
| 132 # And since Skia uses depot_tools to manage its dependencies, we will have | |
| 133 # to run "gclient sync" rather than a raw "git pull". | |
| 134 if reload_seconds: | |
| 135 self._expectations_repo = svn.Svn(EXPECTATIONS_DIR) | |
| 136 else: | |
| 137 self._expectations_repo = None | |
| 138 | 148 |
| 139 def is_exported(self): | 149 def is_exported(self): |
| 140 """ Returns true iff HTTP clients on other hosts are allowed to access | 150 """ Returns true iff HTTP clients on other hosts are allowed to access |
| 141 this server. """ | 151 this server. """ |
| 142 return self._export | 152 return self._export |
| 143 | 153 |
| 144 def is_editable(self): | 154 def is_editable(self): |
| 145 """ Returns true iff HTTP clients are allowed to submit new baselines. """ | 155 """ Returns true iff HTTP clients are allowed to submit new baselines. """ |
| 146 return self._editable | 156 return self._editable |
| 147 | 157 |
| 148 def reload_seconds(self): | 158 def reload_seconds(self): |
| 149 """ Returns the result reload period in seconds, or 0 if we don't reload | 159 """ Returns the result reload period in seconds, or 0 if we don't reload |
| 150 results. """ | 160 results. """ |
| 151 return self._reload_seconds | 161 return self._reload_seconds |
| 152 | 162 |
| 153 def update_results(self): | 163 def update_results(self): |
| 154 """ Create or update self.results, based on the expectations in | 164 """ Create or update self.results, based on the expectations in |
| 155 EXPECTATIONS_DIR and the latest actuals from skia-autogen. | 165 EXPECTATIONS_DIR and the latest actuals from skia-autogen. |
| 166 | |
| 167 We hold self.results_rlock while we do this, to guarantee that no other | |
| 168 thread attempts to update either self.results or the underlying files at | |
| 169 the same time. | |
| 156 """ | 170 """ |
| 157 logging.info('Updating actual GM results in %s from SVN repo %s ...' % ( | 171 with self.results_rlock: |
| 158 self._actuals_dir, ACTUALS_SVN_REPO)) | 172 logging.info('Updating actual GM results in %s from SVN repo %s ...' % ( |
| 159 self._actuals_repo.Update('.') | 173 self._actuals_dir, ACTUALS_SVN_REPO)) |
| 174 self._actuals_repo.Update('.') | |
| 160 | 175 |
| 161 if self._expectations_repo: | 176 # We only update the expectations dir if the server was run with a |
| 177 # nonzero --reload argument; otherwise, we expect the user to maintain | |
| 178 # her own expectations as she sees fit. | |
| 179 # | |
| 180 # Because the Skia repo is moving from SVN to git, and git does not | |
| 181 # support updating a single directory tree, we have to update the entire | |
| 182 # repo checkout. | |
| 183 # | |
| 184 # Because Skia uses depot_tools, we have to update using "gclient sync" | |
| 185 # instead of raw git (or SVN) update. Happily, this will work whether | |
| 186 # the checkout was created using git or SVN. | |
| 187 if self._reload_seconds: | |
| 188 logging.info( | |
| 189 'Updating expected GM results in %s by syncing Skia repo ...' % | |
| 190 EXPECTATIONS_DIR) | |
| 191 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY) | |
| 192 | |
| 162 logging.info( | 193 logging.info( |
| 163 'Updating expected GM results in %s ...' % EXPECTATIONS_DIR) | |
| 164 self._expectations_repo.Update('.') | |
| 165 | |
| 166 logging.info( | |
| 167 ('Parsing results from actuals in %s and expectations in %s, ' | 194 ('Parsing results from actuals in %s and expectations in %s, ' |
| 168 + 'and generating pixel diffs (may take a while) ...') % ( | 195 + 'and generating pixel diffs (may take a while) ...') % ( |
| 169 self._actuals_dir, EXPECTATIONS_DIR)) | 196 self._actuals_dir, EXPECTATIONS_DIR)) |
| 170 self.results = results.Results( | 197 self.results = results.Results( |
| 171 actuals_root=self._actuals_dir, | 198 actuals_root=self._actuals_dir, |
| 172 expected_root=EXPECTATIONS_DIR, | 199 expected_root=EXPECTATIONS_DIR, |
| 173 generated_images_root=GENERATED_IMAGES_ROOT) | 200 generated_images_root=GENERATED_IMAGES_ROOT) |
| 174 | 201 |
| 175 def _result_reloader(self): | 202 def _result_reloader(self): |
| 176 """ If --reload argument was specified, reload results at the appropriate | 203 """ If --reload argument was specified, reload results at the appropriate |
| 177 interval. | 204 interval. |
| 178 """ | 205 """ |
| 179 while self._reload_seconds: | 206 while self._reload_seconds: |
| 180 time.sleep(self._reload_seconds) | 207 time.sleep(self._reload_seconds) |
| 181 self.update_results() | 208 self.update_results() |
| 182 | 209 |
| 183 def run(self): | 210 def run(self): |
| (...skipping 166 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 350 if content_type != 'application/json;charset=UTF-8': | 377 if content_type != 'application/json;charset=UTF-8': |
| 351 raise Exception('unsupported %s [%s]' % ( | 378 raise Exception('unsupported %s [%s]' % ( |
| 352 _HTTP_HEADER_CONTENT_TYPE, content_type)) | 379 _HTTP_HEADER_CONTENT_TYPE, content_type)) |
| 353 | 380 |
| 354 content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH]) | 381 content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH]) |
| 355 json_data = self.rfile.read(content_length) | 382 json_data = self.rfile.read(content_length) |
| 356 data = json.loads(json_data) | 383 data = json.loads(json_data) |
| 357 logging.debug('do_POST_edits: received new GM expectations data [%s]' % | 384 logging.debug('do_POST_edits: received new GM expectations data [%s]' % |
| 358 data) | 385 data) |
| 359 | 386 |
| 360 # Since we must make multiple calls to the Results object, grab a | 387 # Update the results on disk with the information we received from the |
| 361 # reference to it in case it is updated to point at a new Results | 388 # client. |
| 362 # object within another thread. | 389 # We must hold _SERVER.results_rlock while we do this, to guarantee that |
| 363 results_obj = _SERVER.results | 390 # no other thread updates expectations (from the Skia repo) while we are |
| 364 oldResultsType = data['oldResultsType'] | 391 # updating them (using the info we received from the client). |
| 365 oldResults = results_obj.get_results_of_type(oldResultsType) | 392 with _SERVER.results_rlock: |
| 366 oldResultsHash = str(hash(repr(oldResults['testData']))) | 393 oldResultsType = data['oldResultsType'] |
| 367 if oldResultsHash != data['oldResultsHash']: | 394 oldResults = _SERVER.results.get_results_of_type(oldResultsType) |
| 368 raise Exception('results of type "%s" changed while the client was ' | 395 oldResultsHash = str(hash(repr(oldResults['testData']))) |
| 369 'making modifications. The client should reload the ' | 396 if oldResultsHash != data['oldResultsHash']: |
| 370 'results and submit the modifications again.' % | 397 raise Exception('results of type "%s" changed while the client was ' |
| 371 oldResultsType) | 398 'making modifications. The client should reload the ' |
| 372 results_obj.edit_expectations(data['modifications']) | 399 'results and submit the modifications again.' % |
| 373 | 400 oldResultsType) |
| 374 # Now that the edits have been committed, update results to reflect them. | 401 _SERVER.results.edit_expectations(data['modifications']) |
| 375 _SERVER.update_results() | 402 # Read the updated results back from disk. |
| 403 _SERVER.update_results() | |
| 376 | 404 |
| 377 def redirect_to(self, url): | 405 def redirect_to(self, url): |
| 378 """ Redirect the HTTP client to a different url. | 406 """ Redirect the HTTP client to a different url. |
| 379 | 407 |
| 380 Args: | 408 Args: |
| 381 url: URL to redirect the HTTP client to | 409 url: URL to redirect the HTTP client to |
| 382 """ | 410 """ |
| 383 self.send_response(301) | 411 self.send_response(301) |
| 384 self.send_header('Location', url) | 412 self.send_header('Location', url) |
| 385 self.end_headers() | 413 self.end_headers() |
| (...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 451 default=0) | 479 default=0) |
| 452 args = parser.parse_args() | 480 args = parser.parse_args() |
| 453 global _SERVER | 481 global _SERVER |
| 454 _SERVER = Server(actuals_dir=args.actuals_dir, | 482 _SERVER = Server(actuals_dir=args.actuals_dir, |
| 455 port=args.port, export=args.export, editable=args.editable, | 483 port=args.port, export=args.export, editable=args.editable, |
| 456 reload_seconds=args.reload) | 484 reload_seconds=args.reload) |
| 457 _SERVER.run() | 485 _SERVER.run() |
| 458 | 486 |
| 459 if __name__ == '__main__': | 487 if __name__ == '__main__': |
| 460 main() | 488 main() |
| OLD | NEW |