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 10 matching lines...) Expand all Loading... | |
| 21 import socket | 21 import socket |
| 22 import subprocess | 22 import subprocess |
| 23 import sys | 23 import sys |
| 24 import thread | 24 import thread |
| 25 import threading | 25 import threading |
| 26 import time | 26 import time |
| 27 import urlparse | 27 import urlparse |
| 28 | 28 |
| 29 # Imports from within Skia | 29 # Imports from within Skia |
| 30 # | 30 # |
| 31 # We need to add the 'tools' directory for svn.py, and the 'gm' directory for | 31 # We need to add the 'gm' directory for gm_json.py . |
| 32 # gm_json.py . | 32 # Make sure it is in the PYTHONPATH, but add it at the *end* |
| 33 # that directory. | |
| 34 # Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end* | |
| 35 # so any dirs that are already in the PYTHONPATH will be preferred. | 33 # so any dirs that are already in the PYTHONPATH will be preferred. |
| 36 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) | 34 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) |
| 37 GM_DIRECTORY = os.path.dirname(PARENT_DIRECTORY) | 35 GM_DIRECTORY = os.path.dirname(PARENT_DIRECTORY) |
| 38 TRUNK_DIRECTORY = os.path.dirname(GM_DIRECTORY) | 36 TRUNK_DIRECTORY = os.path.dirname(GM_DIRECTORY) |
| 39 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') | |
| 40 if TOOLS_DIRECTORY not in sys.path: | |
| 41 sys.path.append(TOOLS_DIRECTORY) | |
| 42 import svn | |
| 43 if GM_DIRECTORY not in sys.path: | 37 if GM_DIRECTORY not in sys.path: |
| 44 sys.path.append(GM_DIRECTORY) | 38 sys.path.append(GM_DIRECTORY) |
| 45 import gm_json | 39 import gm_json |
| 46 | 40 |
| 47 # Imports from local dir | 41 # Imports from local dir |
| 48 # | 42 # |
| 49 # Note: we import results under a different name, to avoid confusion with the | 43 # Note: we import results under a different name, to avoid confusion with the |
| 50 # Server.results() property. See discussion at | 44 # Server.results() property. See discussion at |
| 51 # https://codereview.chromium.org/195943004/diff/1/gm/rebaseline_server/server.p y#newcode44 | 45 # https://codereview.chromium.org/195943004/diff/1/gm/rebaseline_server/server.p y#newcode44 |
| 52 import compare_configs | 46 import compare_configs |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 67 'json': 'application/json' | 61 'json': 'application/json' |
| 68 } | 62 } |
| 69 | 63 |
| 70 # Keys that server.py uses to create the toplevel content header. | 64 # Keys that server.py uses to create the toplevel content header. |
| 71 # NOTE: Keep these in sync with static/constants.js | 65 # NOTE: Keep these in sync with static/constants.js |
| 72 KEY__EDITS__MODIFICATIONS = 'modifications' | 66 KEY__EDITS__MODIFICATIONS = 'modifications' |
| 73 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' | 67 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' |
| 74 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' | 68 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' |
| 75 | 69 |
| 76 DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR | 70 DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR |
| 77 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD' | 71 DEFAULT_ACTUALS_GS_ROOT = 'gs://chromium-skia-gm-summaries' |
|
rmistry
2014/05/30 14:15:28
Should we add this to global_variables.json? I bel
| |
| 78 DEFAULT_ACTUALS_REPO_URL = 'http://skia-autogen.googlecode.com/svn/gm-actual' | |
| 79 DEFAULT_PORT = 8888 | 72 DEFAULT_PORT = 8888 |
| 80 | 73 |
| 81 # Directory, relative to PARENT_DIRECTORY, within which the server will serve | 74 # Directory, relative to PARENT_DIRECTORY, within which the server will serve |
| 82 # out live results (not static files). | 75 # out live results (not static files). |
| 83 RESULTS_SUBDIR = 'results' | 76 RESULTS_SUBDIR = 'results' |
| 84 # Directory, relative to PARENT_DIRECTORY, within which the server will serve | 77 # Directory, relative to PARENT_DIRECTORY, within which the server will serve |
| 85 # out static files. | 78 # out static files. |
| 86 STATIC_CONTENTS_SUBDIR = 'static' | 79 STATIC_CONTENTS_SUBDIR = 'static' |
| 87 # All of the GENERATED_*_SUBDIRS are relative to STATIC_CONTENTS_SUBDIR | 80 # All of the GENERATED_*_SUBDIRS are relative to STATIC_CONTENTS_SUBDIR |
| 88 GENERATED_HTML_SUBDIR = 'generated-html' | 81 GENERATED_HTML_SUBDIR = 'generated-html' |
| (...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 132 """Returns routable IP address of this host (the IP address of its network | 125 """Returns routable IP address of this host (the IP address of its network |
| 133 interface that would be used for most traffic, not its localhost | 126 interface that would be used for most traffic, not its localhost |
| 134 interface). See http://stackoverflow.com/a/166589 """ | 127 interface). See http://stackoverflow.com/a/166589 """ |
| 135 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | 128 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| 136 sock.connect(('8.8.8.8', 80)) | 129 sock.connect(('8.8.8.8', 80)) |
| 137 host = sock.getsockname()[0] | 130 host = sock.getsockname()[0] |
| 138 sock.close() | 131 sock.close() |
| 139 return host | 132 return host |
| 140 | 133 |
| 141 | 134 |
| 142 def _create_svn_checkout(dir_path, repo_url): | |
| 143 """Creates local checkout of an SVN repository at the specified directory | |
| 144 path, returning an svn.Svn object referring to the local checkout. | |
| 145 | |
| 146 Args: | |
| 147 dir_path: path to the local checkout; if this directory does not yet exist, | |
| 148 it will be created and the repo will be checked out into it | |
| 149 repo_url: URL of SVN repo to check out into dir_path (unless the local | |
| 150 checkout already exists) | |
| 151 Returns: an svn.Svn object referring to the local checkout. | |
| 152 """ | |
| 153 local_checkout = svn.Svn(dir_path) | |
| 154 if not os.path.isdir(dir_path): | |
| 155 os.makedirs(dir_path) | |
| 156 local_checkout.Checkout(repo_url, '.') | |
| 157 return local_checkout | |
| 158 | |
| 159 | |
| 160 def _create_index(file_path, config_pairs): | 135 def _create_index(file_path, config_pairs): |
| 161 """Creates an index file linking to all results available from this server. | 136 """Creates an index file linking to all results available from this server. |
| 162 | 137 |
| 163 Prior to https://codereview.chromium.org/215503002 , we had a static | 138 Prior to https://codereview.chromium.org/215503002 , we had a static |
| 164 index.html within our repo. But now that the results may or may not include | 139 index.html within our repo. But now that the results may or may not include |
| 165 config comparisons, index.html needs to be generated differently depending | 140 config comparisons, index.html needs to be generated differently depending |
| 166 on which results are included. | 141 on which results are included. |
| 167 | 142 |
| 168 TODO(epoger): Instead of including raw HTML within the Python code, | 143 TODO(epoger): Instead of including raw HTML within the Python code, |
| 169 consider restoring the index.html file as a template and using django (or | 144 consider restoring the index.html file as a template and using django (or |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 206 file_handle.write('</li>') | 181 file_handle.write('</li>') |
| 207 file_handle.write('</ul>') | 182 file_handle.write('</ul>') |
| 208 file_handle.write('</ul></body></html>') | 183 file_handle.write('</ul></body></html>') |
| 209 | 184 |
| 210 | 185 |
| 211 class Server(object): | 186 class Server(object): |
| 212 """ HTTP server for our HTML rebaseline viewer. """ | 187 """ HTTP server for our HTML rebaseline viewer. """ |
| 213 | 188 |
| 214 def __init__(self, | 189 def __init__(self, |
| 215 actuals_dir=DEFAULT_ACTUALS_DIR, | 190 actuals_dir=DEFAULT_ACTUALS_DIR, |
| 216 actuals_repo_revision=DEFAULT_ACTUALS_REPO_REVISION, | 191 actuals_gs_root=DEFAULT_ACTUALS_GS_ROOT, |
| 217 actuals_repo_url=DEFAULT_ACTUALS_REPO_URL, | |
| 218 port=DEFAULT_PORT, export=False, editable=True, | 192 port=DEFAULT_PORT, export=False, editable=True, |
| 219 reload_seconds=0, config_pairs=None, builder_regex_list=None): | 193 reload_seconds=0, config_pairs=None, builder_regex_list=None): |
| 220 """ | 194 """ |
| 221 Args: | 195 Args: |
| 222 actuals_dir: directory under which we will check out the latest actual | 196 actuals_dir: directory into which we will download the latest actual |
|
rmistry
2014/05/30 14:15:28
[optional] since we are not going to be dealing wi
rmistry
2014/05/30 14:16:27
So many typos:
"since we are going to be dealing w
| |
| 223 GM results | 197 GM results |
| 224 actuals_repo_revision: revision of actual-results.json files to process | 198 actuals_gs_root: Google Storage root to download actual-results.json files |
| 225 actuals_repo_url: SVN repo to download actual-results.json files from; | 199 from; if None or '', don't fetch new actual-results files at all, |
| 226 if None or '', don't fetch new actual-results files at all, | |
| 227 just compare to whatever files are already in actuals_dir | 200 just compare to whatever files are already in actuals_dir |
| 228 port: which TCP port to listen on for HTTP requests | 201 port: which TCP port to listen on for HTTP requests |
| 229 export: whether to allow HTTP clients on other hosts to access this server | 202 export: whether to allow HTTP clients on other hosts to access this server |
| 230 editable: whether HTTP clients are allowed to submit new baselines | 203 editable: whether HTTP clients are allowed to submit new baselines |
| 231 reload_seconds: polling interval with which to check for new results; | 204 reload_seconds: polling interval with which to check for new results; |
| 232 if 0, don't check for new results at all | 205 if 0, don't check for new results at all |
| 233 config_pairs: List of (string, string) tuples; for each tuple, compare | 206 config_pairs: List of (string, string) tuples; for each tuple, compare |
| 234 actual results of these two configs. If None or empty, | 207 actual results of these two configs. If None or empty, |
| 235 don't compare configs at all. | 208 don't compare configs at all. |
| 236 builder_regex_list: List of regular expressions specifying which builders | 209 builder_regex_list: List of regular expressions specifying which builders |
| 237 we will process. If None, process all builders. | 210 we will process. If None, process all builders. |
| 238 """ | 211 """ |
| 239 self._actuals_dir = actuals_dir | 212 self._actuals_dir = actuals_dir |
| 240 self._actuals_repo_revision = actuals_repo_revision | 213 self._actuals_gs_root = actuals_gs_root |
| 241 self._actuals_repo_url = actuals_repo_url | |
| 242 self._port = port | 214 self._port = port |
| 243 self._export = export | 215 self._export = export |
| 244 self._editable = editable | 216 self._editable = editable |
| 245 self._reload_seconds = reload_seconds | 217 self._reload_seconds = reload_seconds |
| 246 self._config_pairs = config_pairs or [] | 218 self._config_pairs = config_pairs or [] |
| 247 self._builder_regex_list = builder_regex_list | 219 self._builder_regex_list = builder_regex_list |
| 248 _create_index( | 220 _create_index( |
| 249 file_path=os.path.join( | 221 file_path=os.path.join( |
| 250 PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR, GENERATED_HTML_SUBDIR, | 222 PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR, GENERATED_HTML_SUBDIR, |
| 251 "index.html"), | 223 "index.html"), |
| 252 config_pairs=config_pairs) | 224 config_pairs=config_pairs) |
| 253 if actuals_repo_url: | |
| 254 self._actuals_repo = _create_svn_checkout( | |
| 255 dir_path=actuals_dir, repo_url=actuals_repo_url) | |
| 256 | 225 |
| 257 # Reentrant lock that must be held whenever updating EITHER of: | 226 # Reentrant lock that must be held whenever updating EITHER of: |
| 258 # 1. self._results | 227 # 1. self._results |
| 259 # 2. the expected or actual results on local disk | 228 # 2. the expected or actual results on local disk |
| 260 self.results_rlock = threading.RLock() | 229 self.results_rlock = threading.RLock() |
| 261 # self._results will be filled in by calls to update_results() | 230 # self._results will be filled in by calls to update_results() |
| 262 self._results = None | 231 self._results = None |
| 263 | 232 |
| 264 @property | 233 @property |
| 265 def results(self): | 234 def results(self): |
| (...skipping 27 matching lines...) Expand all Loading... | |
| 293 the same time. | 262 the same time. |
| 294 | 263 |
| 295 Args: | 264 Args: |
| 296 invalidate: if True, invalidate self._results immediately upon entry; | 265 invalidate: if True, invalidate self._results immediately upon entry; |
| 297 otherwise, we will let readers see those results until we | 266 otherwise, we will let readers see those results until we |
| 298 replace them | 267 replace them |
| 299 """ | 268 """ |
| 300 with self.results_rlock: | 269 with self.results_rlock: |
| 301 if invalidate: | 270 if invalidate: |
| 302 self._results = None | 271 self._results = None |
| 303 if self._actuals_repo_url: | 272 if self._actuals_gs_root: |
| 304 logging.info( | 273 logging.info('Updating actual GM results in %s from %s ...' % ( |
| 305 'Updating actual GM results in %s to revision %s from repo %s ...' | 274 self._actuals_dir, self._actuals_gs_root)) |
| 306 % ( | 275 if os.path.isdir(self._actuals_dir): |
| 307 self._actuals_dir, self._actuals_repo_revision, | 276 shutil.rmtree(self._actuals_dir) |
| 308 self._actuals_repo_url)) | 277 os.makedirs(self._actuals_dir) |
| 309 self._actuals_repo.Update( | 278 # EPOGER: what if the user does not have gsutil installed in her $PATH? |
|
epoger
2014/05/29 21:19:57
Any thoughts on what to do about this?
I had orig
rmistry
2014/05/30 14:15:28
Maybe add a check at the top to make sure that the
| |
| 310 path='.', revision=self._actuals_repo_revision) | 279 _run_command( |
| 280 args=['gsutil', '-m', 'cp', '-R', | |
| 281 posixpath.join(self._actuals_gs_root, '*'), '.'], | |
| 282 directory=self._actuals_dir) | |
| 311 | 283 |
| 312 # We only update the expectations dir if the server was run with a | 284 # We only update the expectations dir if the server was run with a |
| 313 # nonzero --reload argument; otherwise, we expect the user to maintain | 285 # nonzero --reload argument; otherwise, we expect the user to maintain |
| 314 # her own expectations as she sees fit. | 286 # her own expectations as she sees fit. |
| 315 # | 287 # |
| 316 # Because the Skia repo is moving from SVN to git, and git does not | 288 # Because the Skia repo is moving from SVN to git, and git does not |
| 317 # support updating a single directory tree, we have to update the entire | 289 # support updating a single directory tree, we have to update the entire |
| 318 # repo checkout. | 290 # repo checkout. |
| 319 # | 291 # |
| 320 # Because Skia uses depot_tools, we have to update using "gclient sync" | 292 # Because Skia uses depot_tools, we have to update using "gclient sync" |
| (...skipping 289 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 610 self.end_headers() | 582 self.end_headers() |
| 611 json.dump(json_dict, self.wfile) | 583 json.dump(json_dict, self.wfile) |
| 612 | 584 |
| 613 | 585 |
| 614 def main(): | 586 def main(): |
| 615 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', | 587 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', |
| 616 datefmt='%m/%d/%Y %H:%M:%S', | 588 datefmt='%m/%d/%Y %H:%M:%S', |
| 617 level=logging.INFO) | 589 level=logging.INFO) |
| 618 parser = argparse.ArgumentParser() | 590 parser = argparse.ArgumentParser() |
| 619 parser.add_argument('--actuals-dir', | 591 parser.add_argument('--actuals-dir', |
| 620 help=('Directory into which we will check out the latest ' | 592 help=('Directory into which we will download the latest ' |
| 621 'actual GM results. If this directory does not ' | 593 'actual GM results. If this directory does not ' |
| 622 'exist, it will be created. Defaults to %(default)s'), | 594 'exist, it will be created. Defaults to %(default)s'), |
| 623 default=DEFAULT_ACTUALS_DIR) | 595 default=DEFAULT_ACTUALS_DIR) |
| 624 parser.add_argument('--actuals-repo', | 596 parser.add_argument('--actuals-gs-root', |
| 625 help=('URL of SVN repo to download actual-results.json ' | 597 help=('Google Storage root to download actual-results.json ' |
| 626 'files from. Defaults to %(default)s ; if set to ' | 598 'files from. Defaults to %(default)s ; if set to ' |
| 627 'empty string, just compare to actual-results ' | 599 'empty string, just compare to actual-results ' |
| 628 'already found in ACTUALS_DIR.'), | 600 'already found in ACTUALS_DIR.'), |
| 629 default=DEFAULT_ACTUALS_REPO_URL) | 601 default=DEFAULT_ACTUALS_GS_ROOT) |
| 630 parser.add_argument('--actuals-revision', | |
| 631 help=('revision of actual-results.json files to process. ' | |
| 632 'Defaults to %(default)s . Beware of setting this ' | |
| 633 'argument in conjunction with --editable; you ' | |
| 634 'probably only want to edit results at HEAD.'), | |
| 635 default=DEFAULT_ACTUALS_REPO_REVISION) | |
| 636 parser.add_argument('--builders', metavar='BUILDER_REGEX', nargs='+', | 602 parser.add_argument('--builders', metavar='BUILDER_REGEX', nargs='+', |
| 637 help=('Only process builders matching these regular ' | 603 help=('Only process builders matching these regular ' |
| 638 'expressions. If unspecified, process all ' | 604 'expressions. If unspecified, process all ' |
| 639 'builders.')) | 605 'builders.')) |
| 640 parser.add_argument('--compare-configs', action='store_true', | 606 parser.add_argument('--compare-configs', action='store_true', |
| 641 help=('In addition to generating differences between ' | 607 help=('In addition to generating differences between ' |
| 642 'expectations and actuals, also generate ' | 608 'expectations and actuals, also generate ' |
| 643 'differences between these config pairs: ' | 609 'differences between these config pairs: ' |
| 644 + str(CONFIG_PAIRS_TO_COMPARE))) | 610 + str(CONFIG_PAIRS_TO_COMPARE))) |
| 645 parser.add_argument('--editable', action='store_true', | 611 parser.add_argument('--editable', action='store_true', |
| 646 help=('Allow HTTP clients to submit new baselines.')) | 612 help=('Allow HTTP clients to submit new baselines.')) |
| 647 parser.add_argument('--export', action='store_true', | 613 parser.add_argument('--export', action='store_true', |
| 648 help=('Instead of only allowing access from HTTP clients ' | 614 help=('Instead of only allowing access from HTTP clients ' |
| 649 'on localhost, allow HTTP clients on other hosts ' | 615 'on localhost, allow HTTP clients on other hosts ' |
| 650 'to access this server. WARNING: doing so will ' | 616 'to access this server. WARNING: doing so will ' |
| 651 'allow users on other hosts to modify your ' | 617 'allow users on other hosts to modify your ' |
| 652 'GM expectations, if combined with --editable.')) | 618 'GM expectations, if combined with --editable.')) |
| 653 parser.add_argument('--port', type=int, | 619 parser.add_argument('--port', type=int, |
| 654 help=('Which TCP port to listen on for HTTP requests; ' | 620 help=('Which TCP port to listen on for HTTP requests; ' |
| 655 'defaults to %(default)s'), | 621 'defaults to %(default)s'), |
| 656 default=DEFAULT_PORT) | 622 default=DEFAULT_PORT) |
| 657 parser.add_argument('--reload', type=int, | 623 parser.add_argument('--reload', type=int, |
| 658 help=('How often (a period in seconds) to update the ' | 624 help=('How often (a period in seconds) to update the ' |
| 659 'results. If specified, both expected and actual ' | 625 'results. If specified, both expected and actual ' |
| 660 'results will be updated by running "gclient sync" ' | 626 'results will be updated, and "gclient sync" will ' |
| 661 'on your Skia checkout as a whole. ' | 627 'be run on your Skia checkout as a whole. ' |
| 662 'By default, we do not reload at all, and you ' | 628 'By default, we do not reload at all, and you ' |
| 663 'must restart the server to pick up new data.'), | 629 'must restart the server to pick up new data.'), |
| 664 default=0) | 630 default=0) |
| 665 args = parser.parse_args() | 631 args = parser.parse_args() |
| 666 if args.compare_configs: | 632 if args.compare_configs: |
| 667 config_pairs = CONFIG_PAIRS_TO_COMPARE | 633 config_pairs = CONFIG_PAIRS_TO_COMPARE |
| 668 else: | 634 else: |
| 669 config_pairs = None | 635 config_pairs = None |
| 670 | 636 |
| 671 global _SERVER | 637 global _SERVER |
| 672 _SERVER = Server(actuals_dir=args.actuals_dir, | 638 _SERVER = Server(actuals_dir=args.actuals_dir, |
| 673 actuals_repo_revision=args.actuals_revision, | 639 actuals_gs_root=args.actuals_gs_root, |
| 674 actuals_repo_url=args.actuals_repo, | |
| 675 port=args.port, export=args.export, editable=args.editable, | 640 port=args.port, export=args.export, editable=args.editable, |
| 676 reload_seconds=args.reload, config_pairs=config_pairs, | 641 reload_seconds=args.reload, config_pairs=config_pairs, |
| 677 builder_regex_list=args.builders) | 642 builder_regex_list=args.builders) |
| 678 _SERVER.run() | 643 _SERVER.run() |
| 679 | 644 |
| 680 | 645 |
| 681 if __name__ == '__main__': | 646 if __name__ == '__main__': |
| 682 main() | 647 main() |
| OLD | NEW |