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, so that we can import svn.py within | 31 # We need to add the 'tools' directory for svn.py, and the 'gm' directory for |
| 32 # gm_json.py . |
32 # that directory. | 33 # that directory. |
33 # Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end* | 34 # Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end* |
34 # so any dirs that are already in the PYTHONPATH will be preferred. | 35 # so any dirs that are already in the PYTHONPATH will be preferred. |
35 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) | 36 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) |
36 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY)) | 37 GM_DIRECTORY = os.path.dirname(PARENT_DIRECTORY) |
| 38 TRUNK_DIRECTORY = os.path.dirname(GM_DIRECTORY) |
37 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') | 39 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') |
38 if TOOLS_DIRECTORY not in sys.path: | 40 if TOOLS_DIRECTORY not in sys.path: |
39 sys.path.append(TOOLS_DIRECTORY) | 41 sys.path.append(TOOLS_DIRECTORY) |
40 import svn | 42 import svn |
| 43 if GM_DIRECTORY not in sys.path: |
| 44 sys.path.append(GM_DIRECTORY) |
| 45 import gm_json |
41 | 46 |
42 # Imports from local dir | 47 # Imports from local dir |
43 # | 48 # |
44 # Note: we import results under a different name, to avoid confusion with the | 49 # Note: we import results under a different name, to avoid confusion with the |
45 # Server.results() property. See discussion at | 50 # Server.results() property. See discussion at |
46 # https://codereview.chromium.org/195943004/diff/1/gm/rebaseline_server/server.p
y#newcode44 | 51 # https://codereview.chromium.org/195943004/diff/1/gm/rebaseline_server/server.p
y#newcode44 |
| 52 import compare_configs |
47 import compare_to_expectations | 53 import compare_to_expectations |
48 import imagepairset | 54 import imagepairset |
49 import results as results_mod | 55 import results as results_mod |
50 | 56 |
51 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') | 57 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') |
52 | 58 |
53 # A simple dictionary of file name extensions to MIME types. The empty string | 59 # A simple dictionary of file name extensions to MIME types. The empty string |
54 # entry is used as the default when no extension was given or if the extension | 60 # entry is used as the default when no extension was given or if the extension |
55 # has no entry in this dictionary. | 61 # has no entry in this dictionary. |
56 MIME_TYPE_MAP = {'': 'application/octet-stream', | 62 MIME_TYPE_MAP = {'': 'application/octet-stream', |
57 'html': 'text/html', | 63 'html': 'text/html', |
58 'css': 'text/css', | 64 'css': 'text/css', |
59 'png': 'image/png', | 65 'png': 'image/png', |
60 'js': 'application/javascript', | 66 'js': 'application/javascript', |
61 'json': 'application/json' | 67 'json': 'application/json' |
62 } | 68 } |
63 | 69 |
64 # Keys that server.py uses to create the toplevel content header. | 70 # Keys that server.py uses to create the toplevel content header. |
65 # NOTE: Keep these in sync with static/constants.js | 71 # NOTE: Keep these in sync with static/constants.js |
66 KEY__EDITS__MODIFICATIONS = 'modifications' | 72 KEY__EDITS__MODIFICATIONS = 'modifications' |
67 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' | 73 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' |
68 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' | 74 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' |
69 | 75 |
70 DEFAULT_ACTUALS_DIR = compare_to_expectations.DEFAULT_ACTUALS_DIR | 76 DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR |
71 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD' | 77 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD' |
72 DEFAULT_ACTUALS_REPO_URL = 'http://skia-autogen.googlecode.com/svn/gm-actual' | 78 DEFAULT_ACTUALS_REPO_URL = 'http://skia-autogen.googlecode.com/svn/gm-actual' |
73 DEFAULT_PORT = 8888 | 79 DEFAULT_PORT = 8888 |
74 | 80 |
75 # Directory within which the server will serve out static files. | 81 # Directory, relative to PARENT_DIRECTORY, within which the server will serve |
76 STATIC_CONTENTS_SUBDIR = 'static' # within PARENT_DIR | 82 # out live results (not static files). |
77 GENERATED_IMAGES_SUBDIR = 'generated-images' # within STATIC_CONTENTS_SUBDIR | 83 RESULTS_SUBDIR = 'results' |
| 84 # Directory, relative to PARENT_DIRECTORY, within which the server will serve |
| 85 # out static files. |
| 86 STATIC_CONTENTS_SUBDIR = 'static' |
| 87 # All of the GENERATED_*_SUBDIRS are relative to STATIC_CONTENTS_SUBDIR |
| 88 GENERATED_HTML_SUBDIR = 'generated-html' |
| 89 GENERATED_IMAGES_SUBDIR = 'generated-images' |
| 90 GENERATED_JSON_SUBDIR = 'generated-json' |
78 | 91 |
79 # How often (in seconds) clients should reload while waiting for initial | 92 # How often (in seconds) clients should reload while waiting for initial |
80 # results to load. | 93 # results to load. |
81 RELOAD_INTERVAL_UNTIL_READY = 10 | 94 RELOAD_INTERVAL_UNTIL_READY = 10 |
82 | 95 |
| 96 SUMMARY_TYPES = [ |
| 97 results_mod.KEY__HEADER__RESULTS_FAILURES, |
| 98 results_mod.KEY__HEADER__RESULTS_ALL, |
| 99 ] |
| 100 # If --compare-configs is specified, compare these configs. |
| 101 CONFIG_PAIRS_TO_COMPARE = [('8888', 'gpu')] |
| 102 |
83 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' | 103 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' |
84 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' | 104 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' |
85 | 105 |
86 _SERVER = None # This gets filled in by main() | 106 _SERVER = None # This gets filled in by main() |
87 | 107 |
88 | 108 |
89 def _run_command(args, directory): | 109 def _run_command(args, directory): |
90 """Runs a command and returns stdout as a single string. | 110 """Runs a command and returns stdout as a single string. |
91 | 111 |
92 Args: | 112 Args: |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
130 checkout already exists) | 150 checkout already exists) |
131 Returns: an svn.Svn object referring to the local checkout. | 151 Returns: an svn.Svn object referring to the local checkout. |
132 """ | 152 """ |
133 local_checkout = svn.Svn(dir_path) | 153 local_checkout = svn.Svn(dir_path) |
134 if not os.path.isdir(dir_path): | 154 if not os.path.isdir(dir_path): |
135 os.makedirs(dir_path) | 155 os.makedirs(dir_path) |
136 local_checkout.Checkout(repo_url, '.') | 156 local_checkout.Checkout(repo_url, '.') |
137 return local_checkout | 157 return local_checkout |
138 | 158 |
139 | 159 |
| 160 def _create_index(file_path, config_pairs): |
| 161 """Creates an index file linking to all results available from this server. |
| 162 |
| 163 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 |
| 165 config comparisons, index.html needs to be generated differently depending |
| 166 on which results are included. |
| 167 |
| 168 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 |
| 170 similar) to fill in dynamic content. |
| 171 |
| 172 Args: |
| 173 file_path: path on local disk to write index to; any directory components |
| 174 of this path that do not already exist will be created |
| 175 config_pairs: what pairs of configs (if any) we compare actual results of |
| 176 """ |
| 177 dir_path = os.path.dirname(file_path) |
| 178 if not os.path.isdir(dir_path): |
| 179 os.makedirs(dir_path) |
| 180 with open(file_path, 'w') as file_handle: |
| 181 file_handle.write( |
| 182 '<!DOCTYPE html><html>' |
| 183 '<head><title>rebaseline_server</title></head>' |
| 184 '<body><ul>') |
| 185 if SUMMARY_TYPES: |
| 186 file_handle.write('<li>Expectations vs Actuals</li><ul>') |
| 187 for summary_type in SUMMARY_TYPES: |
| 188 file_handle.write( |
| 189 '<li>' |
| 190 '<a href="/%s/view.html#/view.html?resultsToLoad=/%s/%s">' |
| 191 '%s</a></li>' % ( |
| 192 STATIC_CONTENTS_SUBDIR, RESULTS_SUBDIR, |
| 193 summary_type, summary_type)) |
| 194 file_handle.write('</ul>') |
| 195 if config_pairs: |
| 196 file_handle.write('<li>Comparing configs within actual results</li><ul>') |
| 197 for config_pair in config_pairs: |
| 198 file_handle.write('<li>%s vs %s:' % config_pair) |
| 199 for summary_type in SUMMARY_TYPES: |
| 200 file_handle.write( |
| 201 ' <a href="/%s/view.html#/view.html?' |
| 202 'resultsToLoad=/%s/%s/%s-vs-%s_%s.json">%s</a>' % ( |
| 203 STATIC_CONTENTS_SUBDIR, STATIC_CONTENTS_SUBDIR, |
| 204 GENERATED_JSON_SUBDIR, config_pair[0], config_pair[1], |
| 205 summary_type, summary_type)) |
| 206 file_handle.write('</li>') |
| 207 file_handle.write('</ul>') |
| 208 file_handle.write('</ul></body></html>') |
| 209 |
| 210 |
140 class Server(object): | 211 class Server(object): |
141 """ HTTP server for our HTML rebaseline viewer. """ | 212 """ HTTP server for our HTML rebaseline viewer. """ |
142 | 213 |
143 def __init__(self, | 214 def __init__(self, |
144 actuals_dir=DEFAULT_ACTUALS_DIR, | 215 actuals_dir=DEFAULT_ACTUALS_DIR, |
145 actuals_repo_revision=DEFAULT_ACTUALS_REPO_REVISION, | 216 actuals_repo_revision=DEFAULT_ACTUALS_REPO_REVISION, |
146 actuals_repo_url=DEFAULT_ACTUALS_REPO_URL, | 217 actuals_repo_url=DEFAULT_ACTUALS_REPO_URL, |
147 port=DEFAULT_PORT, export=False, editable=True, | 218 port=DEFAULT_PORT, export=False, editable=True, |
148 reload_seconds=0): | 219 reload_seconds=0, config_pairs=None): |
149 """ | 220 """ |
150 Args: | 221 Args: |
151 actuals_dir: directory under which we will check out the latest actual | 222 actuals_dir: directory under which we will check out the latest actual |
152 GM results | 223 GM results |
153 actuals_repo_revision: revision of actual-results.json files to process | 224 actuals_repo_revision: revision of actual-results.json files to process |
154 actuals_repo_url: SVN repo to download actual-results.json files from; | 225 actuals_repo_url: SVN repo to download actual-results.json files from; |
155 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, |
156 just compare to whatever files are already in actuals_dir | 227 just compare to whatever files are already in actuals_dir |
157 port: which TCP port to listen on for HTTP requests | 228 port: which TCP port to listen on for HTTP requests |
158 export: whether to allow HTTP clients on other hosts to access this server | 229 export: whether to allow HTTP clients on other hosts to access this server |
159 editable: whether HTTP clients are allowed to submit new baselines | 230 editable: whether HTTP clients are allowed to submit new baselines |
160 reload_seconds: polling interval with which to check for new results; | 231 reload_seconds: polling interval with which to check for new results; |
161 if 0, don't check for new results at all | 232 if 0, don't check for new results at all |
| 233 config_pairs: List of (string, string) tuples; for each tuple, compare |
| 234 actual results of these two configs. If None or empty, |
| 235 don't compare configs at all. |
162 """ | 236 """ |
163 self._actuals_dir = actuals_dir | 237 self._actuals_dir = actuals_dir |
164 self._actuals_repo_revision = actuals_repo_revision | 238 self._actuals_repo_revision = actuals_repo_revision |
165 self._actuals_repo_url = actuals_repo_url | 239 self._actuals_repo_url = actuals_repo_url |
166 self._port = port | 240 self._port = port |
167 self._export = export | 241 self._export = export |
168 self._editable = editable | 242 self._editable = editable |
169 self._reload_seconds = reload_seconds | 243 self._reload_seconds = reload_seconds |
| 244 self._config_pairs = config_pairs or [] |
| 245 _create_index( |
| 246 file_path=os.path.join( |
| 247 PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR, GENERATED_HTML_SUBDIR, |
| 248 "index.html"), |
| 249 config_pairs=config_pairs) |
170 if actuals_repo_url: | 250 if actuals_repo_url: |
171 self._actuals_repo = _create_svn_checkout( | 251 self._actuals_repo = _create_svn_checkout( |
172 dir_path=actuals_dir, repo_url=actuals_repo_url) | 252 dir_path=actuals_dir, repo_url=actuals_repo_url) |
173 | 253 |
174 # Reentrant lock that must be held whenever updating EITHER of: | 254 # Reentrant lock that must be held whenever updating EITHER of: |
175 # 1. self._results | 255 # 1. self._results |
176 # 2. the expected or actual results on local disk | 256 # 2. the expected or actual results on local disk |
177 self.results_rlock = threading.RLock() | 257 self.results_rlock = threading.RLock() |
178 # self._results will be filled in by calls to update_results() | 258 # self._results will be filled in by calls to update_results() |
179 self._results = None | 259 self._results = None |
(...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
236 # | 316 # |
237 # Because Skia uses depot_tools, we have to update using "gclient sync" | 317 # Because Skia uses depot_tools, we have to update using "gclient sync" |
238 # instead of raw git (or SVN) update. Happily, this will work whether | 318 # instead of raw git (or SVN) update. Happily, this will work whether |
239 # the checkout was created using git or SVN. | 319 # the checkout was created using git or SVN. |
240 if self._reload_seconds: | 320 if self._reload_seconds: |
241 logging.info( | 321 logging.info( |
242 'Updating expected GM results in %s by syncing Skia repo ...' % | 322 'Updating expected GM results in %s by syncing Skia repo ...' % |
243 compare_to_expectations.DEFAULT_EXPECTATIONS_DIR) | 323 compare_to_expectations.DEFAULT_EXPECTATIONS_DIR) |
244 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY) | 324 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY) |
245 | 325 |
246 self._results = compare_to_expectations.Results( | 326 self._results = compare_to_expectations.ExpectationComparisons( |
247 actuals_root=self._actuals_dir, | 327 actuals_root=self._actuals_dir, |
248 generated_images_root=os.path.join( | 328 generated_images_root=os.path.join( |
249 PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR, | 329 PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR, |
250 GENERATED_IMAGES_SUBDIR), | 330 GENERATED_IMAGES_SUBDIR), |
251 diff_base_url=posixpath.join( | 331 diff_base_url=posixpath.join( |
252 os.pardir, STATIC_CONTENTS_SUBDIR, GENERATED_IMAGES_SUBDIR)) | 332 os.pardir, STATIC_CONTENTS_SUBDIR, GENERATED_IMAGES_SUBDIR)) |
253 | 333 |
| 334 json_dir = os.path.join( |
| 335 PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR, GENERATED_JSON_SUBDIR) |
| 336 if not os.path.isdir(json_dir): |
| 337 os.makedirs(json_dir) |
| 338 |
| 339 for config_pair in self._config_pairs: |
| 340 config_comparisons = compare_configs.ConfigComparisons( |
| 341 configs=config_pair, |
| 342 actuals_root=self._actuals_dir, |
| 343 generated_images_root=os.path.join( |
| 344 PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR, |
| 345 GENERATED_IMAGES_SUBDIR), |
| 346 diff_base_url=posixpath.join( |
| 347 os.pardir, GENERATED_IMAGES_SUBDIR)) |
| 348 for summary_type in SUMMARY_TYPES: |
| 349 gm_json.WriteToFile( |
| 350 config_comparisons.get_packaged_results_of_type( |
| 351 results_type=summary_type), |
| 352 os.path.join( |
| 353 json_dir, '%s-vs-%s_%s.json' % ( |
| 354 config_pair[0], config_pair[1], summary_type))) |
| 355 |
254 def _result_loader(self, reload_seconds=0): | 356 def _result_loader(self, reload_seconds=0): |
255 """ Call self.update_results(), either once or periodically. | 357 """ Call self.update_results(), either once or periodically. |
256 | 358 |
257 Params: | 359 Params: |
258 reload_seconds: integer; if nonzero, reload results at this interval | 360 reload_seconds: integer; if nonzero, reload results at this interval |
259 (in which case, this method will never return!) | 361 (in which case, this method will never return!) |
260 """ | 362 """ |
261 self.update_results() | 363 self.update_results() |
262 logging.info('Initial results loaded. Ready for requests on %s' % self._url) | 364 logging.info('Initial results loaded. Ready for requests on %s' % self._url) |
263 if reload_seconds: | 365 if reload_seconds: |
(...skipping 29 matching lines...) Expand all Loading... |
293 def do_GET(self): | 395 def do_GET(self): |
294 """ | 396 """ |
295 Handles all GET requests, forwarding them to the appropriate | 397 Handles all GET requests, forwarding them to the appropriate |
296 do_GET_* dispatcher. | 398 do_GET_* dispatcher. |
297 | 399 |
298 If we see any Exceptions, return a 404. This fixes http://skbug.com/2147 | 400 If we see any Exceptions, return a 404. This fixes http://skbug.com/2147 |
299 """ | 401 """ |
300 try: | 402 try: |
301 logging.debug('do_GET: path="%s"' % self.path) | 403 logging.debug('do_GET: path="%s"' % self.path) |
302 if self.path == '' or self.path == '/' or self.path == '/index.html' : | 404 if self.path == '' or self.path == '/' or self.path == '/index.html' : |
303 self.redirect_to('/%s/index.html' % STATIC_CONTENTS_SUBDIR) | 405 self.redirect_to('/%s/%s/index.html' % ( |
| 406 STATIC_CONTENTS_SUBDIR, GENERATED_HTML_SUBDIR)) |
304 return | 407 return |
305 if self.path == '/favicon.ico' : | 408 if self.path == '/favicon.ico' : |
306 self.redirect_to('/%s/favicon.ico' % STATIC_CONTENTS_SUBDIR) | 409 self.redirect_to('/%s/favicon.ico' % STATIC_CONTENTS_SUBDIR) |
307 return | 410 return |
308 | 411 |
309 # All requests must be of this form: | 412 # All requests must be of this form: |
310 # /dispatcher/remainder | 413 # /dispatcher/remainder |
311 # where 'dispatcher' indicates which do_GET_* dispatcher to run | 414 # where 'dispatcher' indicates which do_GET_* dispatcher to run |
312 # and 'remainder' is the remaining path sent to that dispatcher. | 415 # and 'remainder' is the remaining path sent to that dispatcher. |
313 normpath = posixpath.normpath(self.path) | 416 normpath = posixpath.normpath(self.path) |
314 (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups() | 417 (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups() |
315 dispatchers = { | 418 dispatchers = { |
316 'results': self.do_GET_results, | 419 RESULTS_SUBDIR: self.do_GET_results, |
317 STATIC_CONTENTS_SUBDIR: self.do_GET_static, | 420 STATIC_CONTENTS_SUBDIR: self.do_GET_static, |
318 } | 421 } |
319 dispatcher = dispatchers[dispatcher_name] | 422 dispatcher = dispatchers[dispatcher_name] |
320 dispatcher(remainder) | 423 dispatcher(remainder) |
321 except: | 424 except: |
322 self.send_error(404) | 425 self.send_error(404) |
323 raise | 426 raise |
324 | 427 |
325 def do_GET_results(self, results_type): | 428 def do_GET_results(self, results_type): |
326 """ Handle a GET request for GM results. | 429 """ Handle a GET request for GM results. |
327 | 430 |
328 Args: | 431 Args: |
329 results_type: string indicating which set of results to return; | 432 results_type: string indicating which set of results to return; |
330 must be one of the results_mod.RESULTS_* constants | 433 must be one of the results_mod.RESULTS_* constants |
331 """ | 434 """ |
332 logging.debug('do_GET_results: sending results of type "%s"' % results_type) | 435 logging.debug('do_GET_results: sending results of type "%s"' % results_type) |
333 # Since we must make multiple calls to the Results object, grab a | 436 # Since we must make multiple calls to the ExpectationComparisons object, |
334 # reference to it in case it is updated to point at a new Results | 437 # grab a reference to it in case it is updated to point at a new |
335 # object within another thread. | 438 # ExpectationComparisons object within another thread. |
336 # | 439 # |
337 # TODO(epoger): Rather than using a global variable for the handler | 440 # TODO(epoger): Rather than using a global variable for the handler |
338 # to refer to the Server object, make Server a subclass of | 441 # to refer to the Server object, make Server a subclass of |
339 # HTTPServer, and then it could be available to the handler via | 442 # HTTPServer, and then it could be available to the handler via |
340 # the handler's .server instance variable. | 443 # the handler's .server instance variable. |
341 results_obj = _SERVER.results | 444 results_obj = _SERVER.results |
342 if results_obj: | 445 if results_obj: |
343 response_dict = results_obj.get_packaged_results_of_type( | 446 response_dict = results_obj.get_packaged_results_of_type( |
344 results_type=results_type, reload_seconds=_SERVER.reload_seconds, | 447 results_type=results_type, reload_seconds=_SERVER.reload_seconds, |
345 is_editable=_SERVER.is_editable, is_exported=_SERVER.is_exported) | 448 is_editable=_SERVER.is_editable, is_exported=_SERVER.is_exported) |
(...skipping 171 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
517 'files from. Defaults to %(default)s ; if set to ' | 620 'files from. Defaults to %(default)s ; if set to ' |
518 'empty string, just compare to actual-results ' | 621 'empty string, just compare to actual-results ' |
519 'already found in ACTUALS_DIR.'), | 622 'already found in ACTUALS_DIR.'), |
520 default=DEFAULT_ACTUALS_REPO_URL) | 623 default=DEFAULT_ACTUALS_REPO_URL) |
521 parser.add_argument('--actuals-revision', | 624 parser.add_argument('--actuals-revision', |
522 help=('revision of actual-results.json files to process. ' | 625 help=('revision of actual-results.json files to process. ' |
523 'Defaults to %(default)s . Beware of setting this ' | 626 'Defaults to %(default)s . Beware of setting this ' |
524 'argument in conjunction with --editable; you ' | 627 'argument in conjunction with --editable; you ' |
525 'probably only want to edit results at HEAD.'), | 628 'probably only want to edit results at HEAD.'), |
526 default=DEFAULT_ACTUALS_REPO_REVISION) | 629 default=DEFAULT_ACTUALS_REPO_REVISION) |
| 630 parser.add_argument('--compare-configs', action='store_true', |
| 631 help=('In addition to generating differences between ' |
| 632 'expectations and actuals, also generate ' |
| 633 'differences between these config pairs: ' |
| 634 + str(CONFIG_PAIRS_TO_COMPARE))) |
527 parser.add_argument('--editable', action='store_true', | 635 parser.add_argument('--editable', action='store_true', |
528 help=('Allow HTTP clients to submit new baselines.')) | 636 help=('Allow HTTP clients to submit new baselines.')) |
529 parser.add_argument('--export', action='store_true', | 637 parser.add_argument('--export', action='store_true', |
530 help=('Instead of only allowing access from HTTP clients ' | 638 help=('Instead of only allowing access from HTTP clients ' |
531 'on localhost, allow HTTP clients on other hosts ' | 639 'on localhost, allow HTTP clients on other hosts ' |
532 'to access this server. WARNING: doing so will ' | 640 'to access this server. WARNING: doing so will ' |
533 'allow users on other hosts to modify your ' | 641 'allow users on other hosts to modify your ' |
534 'GM expectations, if combined with --editable.')) | 642 'GM expectations, if combined with --editable.')) |
535 parser.add_argument('--port', type=int, | 643 parser.add_argument('--port', type=int, |
536 help=('Which TCP port to listen on for HTTP requests; ' | 644 help=('Which TCP port to listen on for HTTP requests; ' |
537 'defaults to %(default)s'), | 645 'defaults to %(default)s'), |
538 default=DEFAULT_PORT) | 646 default=DEFAULT_PORT) |
539 parser.add_argument('--reload', type=int, | 647 parser.add_argument('--reload', type=int, |
540 help=('How often (a period in seconds) to update the ' | 648 help=('How often (a period in seconds) to update the ' |
541 'results. If specified, both expected and actual ' | 649 'results. If specified, both expected and actual ' |
542 'results will be updated by running "gclient sync" ' | 650 'results will be updated by running "gclient sync" ' |
543 'on your Skia checkout as a whole. ' | 651 'on your Skia checkout as a whole. ' |
544 'By default, we do not reload at all, and you ' | 652 'By default, we do not reload at all, and you ' |
545 'must restart the server to pick up new data.'), | 653 'must restart the server to pick up new data.'), |
546 default=0) | 654 default=0) |
547 args = parser.parse_args() | 655 args = parser.parse_args() |
| 656 if args.compare_configs: |
| 657 config_pairs = CONFIG_PAIRS_TO_COMPARE |
| 658 else: |
| 659 config_pairs = None |
| 660 |
548 global _SERVER | 661 global _SERVER |
549 _SERVER = Server(actuals_dir=args.actuals_dir, | 662 _SERVER = Server(actuals_dir=args.actuals_dir, |
550 actuals_repo_revision=args.actuals_revision, | 663 actuals_repo_revision=args.actuals_revision, |
551 actuals_repo_url=args.actuals_repo, | 664 actuals_repo_url=args.actuals_repo, |
552 port=args.port, export=args.export, editable=args.editable, | 665 port=args.port, export=args.export, editable=args.editable, |
553 reload_seconds=args.reload) | 666 reload_seconds=args.reload, config_pairs=config_pairs) |
554 _SERVER.run() | 667 _SERVER.run() |
555 | 668 |
556 | 669 |
557 if __name__ == '__main__': | 670 if __name__ == '__main__': |
558 main() | 671 main() |
OLD | NEW |