blob: 6d191ea4098b2721f09f40d5829550e957bd6944 [file] [log] [blame]
epoger@google.comf9d134d2013-09-27 15:02:44 +00001#!/usr/bin/python
2
epoger@google.com9fb6c8a2013-10-09 18:05:58 +00003"""
epoger@google.comf9d134d2013-09-27 15:02:44 +00004Copyright 2013 Google Inc.
5
6Use of this source code is governed by a BSD-style license that can be
7found in the LICENSE file.
epoger@google.comf9d134d2013-09-27 15:02:44 +00008
epoger@google.comf9d134d2013-09-27 15:02:44 +00009HTTP server for our HTML rebaseline viewer.
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000010"""
epoger@google.comf9d134d2013-09-27 15:02:44 +000011
12# System-level imports
13import argparse
14import BaseHTTPServer
15import json
epoger@google.comdcb4e652013-10-11 18:45:33 +000016import logging
epoger@google.comf9d134d2013-09-27 15:02:44 +000017import os
18import posixpath
19import re
20import shutil
epoger@google.comb08c7072013-10-30 14:09:04 +000021import socket
epoger@google.comf9d134d2013-09-27 15:02:44 +000022import sys
epoger@google.com542b65f2013-10-15 20:10:33 +000023import thread
24import time
epoger@google.comdcb4e652013-10-11 18:45:33 +000025import urlparse
epoger@google.comf9d134d2013-09-27 15:02:44 +000026
27# Imports from within Skia
28#
29# We need to add the 'tools' directory, so that we can import svn.py within
30# that directory.
31# 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.
epoger@google.comcb55f112013-10-02 19:27:35 +000033PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
34TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY))
epoger@google.comf9d134d2013-09-27 15:02:44 +000035TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools')
36if TOOLS_DIRECTORY not in sys.path:
37 sys.path.append(TOOLS_DIRECTORY)
38import svn
39
40# Imports from local dir
41import results
42
43ACTUALS_SVN_REPO = 'http://skia-autogen.googlecode.com/svn/gm-actual'
epoger@google.com542b65f2013-10-15 20:10:33 +000044EXPECTATIONS_SVN_REPO = 'http://skia.googlecode.com/svn/trunk/expectations/gm'
epoger@google.comf9d134d2013-09-27 15:02:44 +000045PATHSPLIT_RE = re.compile('/([^/]+)/(.+)')
46TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(os.path.dirname(
47 os.path.realpath(__file__))))
epoger@google.com9dddf6f2013-11-08 16:25:25 +000048GENERATED_IMAGES_ROOT = os.path.join(PARENT_DIRECTORY, 'static',
49 'generated-images')
epoger@google.comf9d134d2013-09-27 15:02:44 +000050
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
53# has no entry in this dictionary.
54MIME_TYPE_MAP = {'': 'application/octet-stream',
55 'html': 'text/html',
56 'css': 'text/css',
57 'png': 'image/png',
58 'js': 'application/javascript',
59 'json': 'application/json'
60 }
61
62DEFAULT_ACTUALS_DIR = '.gm-actuals'
63DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm')
64DEFAULT_PORT = 8888
65
epoger@google.comeb832592013-10-23 15:07:26 +000066_HTTP_HEADER_CONTENT_LENGTH = 'Content-Length'
67_HTTP_HEADER_CONTENT_TYPE = 'Content-Type'
68
epoger@google.comf9d134d2013-09-27 15:02:44 +000069_SERVER = None # This gets filled in by main()
70
epoger@google.com591469b2013-11-20 19:58:06 +000071def _get_routable_ip_address():
epoger@google.comb08c7072013-10-30 14:09:04 +000072 """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
74 interface). See http://stackoverflow.com/a/166589 """
75 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
76 sock.connect(('8.8.8.8', 80))
77 host = sock.getsockname()[0]
78 sock.close()
79 return host
80
epoger@google.com591469b2013-11-20 19:58:06 +000081def _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
epoger@google.comb08c7072013-10-30 14:09:04 +000098
epoger@google.comf9d134d2013-09-27 15:02:44 +000099class Server(object):
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000100 """ HTTP server for our HTML rebaseline viewer. """
epoger@google.comf9d134d2013-09-27 15:02:44 +0000101
epoger@google.comf9d134d2013-09-27 15:02:44 +0000102 def __init__(self,
103 actuals_dir=DEFAULT_ACTUALS_DIR,
104 expectations_dir=DEFAULT_EXPECTATIONS_DIR,
epoger@google.com542b65f2013-10-15 20:10:33 +0000105 port=DEFAULT_PORT, export=False, editable=True,
106 reload_seconds=0):
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000107 """
108 Args:
109 actuals_dir: directory under which we will check out the latest actual
110 GM results
epoger@google.comcc64e7d2013-11-21 18:11:34 +0000111 expectations_dir: DEPRECATED: directory under which to find
112 GM expectations (they must already be in that directory)
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000113 port: which TCP port to listen on for HTTP requests
114 export: whether to allow HTTP clients on other hosts to access this server
epoger@google.com542b65f2013-10-15 20:10:33 +0000115 editable: whether HTTP clients are allowed to submit new baselines
116 reload_seconds: polling interval with which to check for new results;
117 if 0, don't check for new results at all
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000118 """
epoger@google.comf9d134d2013-09-27 15:02:44 +0000119 self._actuals_dir = actuals_dir
120 self._expectations_dir = expectations_dir
121 self._port = port
122 self._export = export
epoger@google.com542b65f2013-10-15 20:10:33 +0000123 self._editable = editable
124 self._reload_seconds = reload_seconds
epoger@google.com591469b2013-11-20 19:58:06 +0000125 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
epoger@google.comf9d134d2013-09-27 15:02:44 +0000139
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000140 def is_exported(self):
141 """ Returns true iff HTTP clients on other hosts are allowed to access
142 this server. """
143 return self._export
144
epoger@google.com542b65f2013-10-15 20:10:33 +0000145 def is_editable(self):
146 """ Returns true iff HTTP clients are allowed to submit new baselines. """
147 return self._editable
epoger@google.comf9d134d2013-09-27 15:02:44 +0000148
epoger@google.com542b65f2013-10-15 20:10:33 +0000149 def reload_seconds(self):
150 """ Returns the result reload period in seconds, or 0 if we don't reload
151 results. """
152 return self._reload_seconds
153
epoger@google.comeb832592013-10-23 15:07:26 +0000154 def update_results(self):
epoger@google.com542b65f2013-10-15 20:10:33 +0000155 """ Create or update self.results, based on the expectations in
156 self._expectations_dir and the latest actuals from skia-autogen.
epoger@google.comf9d134d2013-09-27 15:02:44 +0000157 """
epoger@google.com591469b2013-11-20 19:58:06 +0000158 logging.info('Updating actual GM results in %s from SVN repo %s ...' % (
159 self._actuals_dir, ACTUALS_SVN_REPO))
160 self._actuals_repo.Update('.')
161
162 if self._expectations_repo:
163 logging.info(
164 'Updating expected GM results in %s from SVN repo %s ...' % (
165 self._expectations_dir, EXPECTATIONS_SVN_REPO))
166 self._expectations_repo.Update('.')
epoger@google.comeb832592013-10-23 15:07:26 +0000167
epoger@google.com61952902013-11-08 17:23:54 +0000168 logging.info(
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000169 ('Parsing results from actuals in %s and expectations in %s, '
170 + 'and generating pixel diffs (may take a while) ...') % (
epoger@google.comeb832592013-10-23 15:07:26 +0000171 self._actuals_dir, self._expectations_dir))
epoger@google.com591469b2013-11-20 19:58:06 +0000172 self.results = results.Results(
epoger@google.comeb832592013-10-23 15:07:26 +0000173 actuals_root=self._actuals_dir,
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000174 expected_root=self._expectations_dir,
175 generated_images_root=GENERATED_IMAGES_ROOT)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000176
epoger@google.com542b65f2013-10-15 20:10:33 +0000177 def _result_reloader(self):
178 """ If --reload argument was specified, reload results at the appropriate
179 interval.
180 """
181 while self._reload_seconds:
182 time.sleep(self._reload_seconds)
epoger@google.comeb832592013-10-23 15:07:26 +0000183 self.update_results()
epoger@google.com542b65f2013-10-15 20:10:33 +0000184
epoger@google.comf9d134d2013-09-27 15:02:44 +0000185 def run(self):
epoger@google.comeb832592013-10-23 15:07:26 +0000186 self.update_results()
epoger@google.com542b65f2013-10-15 20:10:33 +0000187 thread.start_new_thread(self._result_reloader, ())
188
epoger@google.comf9d134d2013-09-27 15:02:44 +0000189 if self._export:
190 server_address = ('', self._port)
epoger@google.com591469b2013-11-20 19:58:06 +0000191 host = _get_routable_ip_address()
epoger@google.com542b65f2013-10-15 20:10:33 +0000192 if self._editable:
193 logging.warning('Running with combination of "export" and "editable" '
194 'flags. Users on other machines will '
195 'be able to modify your GM expectations!')
epoger@google.comf9d134d2013-09-27 15:02:44 +0000196 else:
epoger@google.comb08c7072013-10-30 14:09:04 +0000197 host = '127.0.0.1'
198 server_address = (host, self._port)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000199 http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler)
epoger@google.com61952902013-11-08 17:23:54 +0000200 logging.info('Ready for requests on http://%s:%d' % (
201 host, http_server.server_port))
epoger@google.comf9d134d2013-09-27 15:02:44 +0000202 http_server.serve_forever()
203
204
205class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
206 """ HTTP request handlers for various types of queries this server knows
207 how to handle (static HTML and Javascript, expected/actual results, etc.)
208 """
209 def do_GET(self):
210 """ Handles all GET requests, forwarding them to the appropriate
211 do_GET_* dispatcher. """
212 if self.path == '' or self.path == '/' or self.path == '/index.html' :
epoger@google.com045c3d32013-11-01 16:46:41 +0000213 self.redirect_to('/static/index.html')
epoger@google.comf9d134d2013-09-27 15:02:44 +0000214 return
215 if self.path == '/favicon.ico' :
216 self.redirect_to('/static/favicon.ico')
217 return
218
219 # All requests must be of this form:
220 # /dispatcher/remainder
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000221 # where 'dispatcher' indicates which do_GET_* dispatcher to run
222 # and 'remainder' is the remaining path sent to that dispatcher.
epoger@google.comf9d134d2013-09-27 15:02:44 +0000223 normpath = posixpath.normpath(self.path)
224 (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups()
225 dispatchers = {
226 'results': self.do_GET_results,
227 'static': self.do_GET_static,
228 }
229 dispatcher = dispatchers[dispatcher_name]
230 dispatcher(remainder)
231
epoger@google.comdcb4e652013-10-11 18:45:33 +0000232 def do_GET_results(self, type):
epoger@google.comf9d134d2013-09-27 15:02:44 +0000233 """ Handle a GET request for GM results.
epoger@google.comf9d134d2013-09-27 15:02:44 +0000234
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000235 Args:
epoger@google.comdcb4e652013-10-11 18:45:33 +0000236 type: string indicating which set of results to return;
237 must be one of the results.RESULTS_* constants
238 """
239 logging.debug('do_GET_results: sending results of type "%s"' % type)
240 try:
epoger@google.com591469b2013-11-20 19:58:06 +0000241 # 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 #
epoger@google.comdcb4e652013-10-11 18:45:33 +0000245 # TODO(epoger): Rather than using a global variable for the handler
246 # to refer to the Server object, make Server a subclass of
247 # HTTPServer, and then it could be available to the handler via
248 # the handler's .server instance variable.
epoger@google.com591469b2013-11-20 19:58:06 +0000249 results_obj = _SERVER.results
250 response_dict = results_obj.get_results_of_type(type)
251 time_updated = results_obj.get_timestamp()
epoger@google.com542b65f2013-10-15 20:10:33 +0000252
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000253 response_dict['header'] = {
epoger@google.com542b65f2013-10-15 20:10:33 +0000254 # Timestamps:
255 # 1. when this data was last updated
256 # 2. when the caller should check back for new data (if ever)
257 #
258 # We only return these timestamps if the --reload argument was passed;
259 # otherwise, we have no idea when the expectations were last updated
260 # (we allow the user to maintain her own expectations as she sees fit).
261 'timeUpdated': time_updated if _SERVER.reload_seconds() else None,
262 'timeNextUpdateAvailable': (
263 (time_updated+_SERVER.reload_seconds()) if _SERVER.reload_seconds()
264 else None),
265
epoger@google.comeb832592013-10-23 15:07:26 +0000266 # The type we passed to get_results_of_type()
267 'type': type,
268
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000269 # Hash of testData, which the client must return with any edits--
270 # this ensures that the edits were made to a particular dataset.
epoger@google.com542b65f2013-10-15 20:10:33 +0000271 'dataHash': str(hash(repr(response_dict['testData']))),
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000272
273 # Whether the server will accept edits back.
epoger@google.com542b65f2013-10-15 20:10:33 +0000274 'isEditable': _SERVER.is_editable(),
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000275
276 # Whether the service is accessible from other hosts.
277 'isExported': _SERVER.is_exported(),
278 }
epoger@google.comf9d134d2013-09-27 15:02:44 +0000279 self.send_json_dict(response_dict)
epoger@google.comdcb4e652013-10-11 18:45:33 +0000280 except:
epoger@google.comf9d134d2013-09-27 15:02:44 +0000281 self.send_error(404)
epoger@google.com542b65f2013-10-15 20:10:33 +0000282 raise
epoger@google.comf9d134d2013-09-27 15:02:44 +0000283
284 def do_GET_static(self, path):
epoger@google.comcb55f112013-10-02 19:27:35 +0000285 """ Handle a GET request for a file under the 'static' directory.
286 Only allow serving of files within the 'static' directory that is a
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000287 filesystem sibling of this script.
288
289 Args:
290 path: path to file (under static directory) to retrieve
291 """
epoger@google.comdcb4e652013-10-11 18:45:33 +0000292 # Strip arguments ('?resultsToLoad=all') from the path
293 path = urlparse.urlparse(path).path
294
295 logging.debug('do_GET_static: sending file "%s"' % path)
epoger@google.comcb55f112013-10-02 19:27:35 +0000296 static_dir = os.path.realpath(os.path.join(PARENT_DIRECTORY, 'static'))
297 full_path = os.path.realpath(os.path.join(static_dir, path))
298 if full_path.startswith(static_dir):
299 self.send_file(full_path)
300 else:
epoger@google.comdcb4e652013-10-11 18:45:33 +0000301 logging.error(
302 'Attempted do_GET_static() of path [%s] outside of static dir [%s]'
303 % (full_path, static_dir))
epoger@google.comcb55f112013-10-02 19:27:35 +0000304 self.send_error(404)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000305
epoger@google.comeb832592013-10-23 15:07:26 +0000306 def do_POST(self):
307 """ Handles all POST requests, forwarding them to the appropriate
308 do_POST_* dispatcher. """
309 # All requests must be of this form:
310 # /dispatcher
311 # where 'dispatcher' indicates which do_POST_* dispatcher to run.
312 normpath = posixpath.normpath(self.path)
313 dispatchers = {
314 '/edits': self.do_POST_edits,
315 }
316 try:
317 dispatcher = dispatchers[normpath]
318 dispatcher()
319 self.send_response(200)
320 except:
321 self.send_error(404)
322 raise
323
324 def do_POST_edits(self):
325 """ Handle a POST request with modifications to GM expectations, in this
326 format:
327
328 {
329 'oldResultsType': 'all', # type of results that the client loaded
330 # and then made modifications to
331 'oldResultsHash': 39850913, # hash of results when the client loaded them
332 # (ensures that the client and server apply
333 # modifications to the same base)
334 'modifications': [
335 {
336 'builder': 'Test-Android-Nexus10-MaliT604-Arm7-Debug',
337 'test': 'strokerect',
338 'config': 'gpu',
339 'expectedHashType': 'bitmap-64bitMD5',
340 'expectedHashDigest': '1707359671708613629',
341 },
342 ...
343 ],
344 }
345
346 Raises an Exception if there were any problems.
347 """
348 if not _SERVER.is_editable():
349 raise Exception('this server is not running in --editable mode')
350
351 content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE]
352 if content_type != 'application/json;charset=UTF-8':
353 raise Exception('unsupported %s [%s]' % (
354 _HTTP_HEADER_CONTENT_TYPE, content_type))
355
356 content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH])
357 json_data = self.rfile.read(content_length)
358 data = json.loads(json_data)
359 logging.debug('do_POST_edits: received new GM expectations data [%s]' %
360 data)
361
epoger@google.com591469b2013-11-20 19:58:06 +0000362 # Since we must make multiple calls to the Results object, grab a
363 # reference to it in case it is updated to point at a new Results
364 # object within another thread.
365 results_obj = _SERVER.results
366 oldResultsType = data['oldResultsType']
367 oldResults = results_obj.get_results_of_type(oldResultsType)
368 oldResultsHash = str(hash(repr(oldResults['testData'])))
369 if oldResultsHash != data['oldResultsHash']:
370 raise Exception('results of type "%s" changed while the client was '
371 'making modifications. The client should reload the '
372 'results and submit the modifications again.' %
373 oldResultsType)
374 results_obj.edit_expectations(data['modifications'])
epoger@google.comeb832592013-10-23 15:07:26 +0000375
376 # Now that the edits have been committed, update results to reflect them.
377 _SERVER.update_results()
378
epoger@google.comf9d134d2013-09-27 15:02:44 +0000379 def redirect_to(self, url):
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000380 """ Redirect the HTTP client to a different url.
381
382 Args:
383 url: URL to redirect the HTTP client to
384 """
epoger@google.comf9d134d2013-09-27 15:02:44 +0000385 self.send_response(301)
386 self.send_header('Location', url)
387 self.end_headers()
388
389 def send_file(self, path):
390 """ Send the contents of the file at this path, with a mimetype based
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000391 on the filename extension.
392
393 Args:
394 path: path of file whose contents to send to the HTTP client
395 """
epoger@google.comf9d134d2013-09-27 15:02:44 +0000396 # Grab the extension if there is one
397 extension = os.path.splitext(path)[1]
398 if len(extension) >= 1:
399 extension = extension[1:]
400
401 # Determine the MIME type of the file from its extension
402 mime_type = MIME_TYPE_MAP.get(extension, MIME_TYPE_MAP[''])
403
404 # Open the file and send it over HTTP
405 if os.path.isfile(path):
406 with open(path, 'rb') as sending_file:
407 self.send_response(200)
408 self.send_header('Content-type', mime_type)
409 self.end_headers()
410 self.wfile.write(sending_file.read())
411 else:
412 self.send_error(404)
413
414 def send_json_dict(self, json_dict):
415 """ Send the contents of this dictionary in JSON format, with a JSON
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000416 mimetype.
417
418 Args:
419 json_dict: dictionary to send
420 """
epoger@google.comf9d134d2013-09-27 15:02:44 +0000421 self.send_response(200)
422 self.send_header('Content-type', 'application/json')
423 self.end_headers()
424 json.dump(json_dict, self.wfile)
425
426
427def main():
epoger@google.comdcb4e652013-10-11 18:45:33 +0000428 logging.basicConfig(level=logging.INFO)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000429 parser = argparse.ArgumentParser()
430 parser.add_argument('--actuals-dir',
431 help=('Directory into which we will check out the latest '
432 'actual GM results. If this directory does not '
433 'exist, it will be created. Defaults to %(default)s'),
434 default=DEFAULT_ACTUALS_DIR)
epoger@google.com542b65f2013-10-15 20:10:33 +0000435 parser.add_argument('--editable', action='store_true',
epoger@google.comeb832592013-10-23 15:07:26 +0000436 help=('Allow HTTP clients to submit new baselines.'))
epoger@google.comcc64e7d2013-11-21 18:11:34 +0000437 # Deprecated the --expectations-dir option, because once our GM expectations
438 # are maintained within git we will no longer be able to check out and update
439 # them in isolation (in SVN you can update a single directory subtree within
440 # a checkout, but you cannot do that with git).
441 #
442 # In a git world, we will force the user to refer to expectations
443 # within the same checkout as this tool (at the relative path
444 # ../../expectations/gm ). If they specify the --reload option, we will
445 # periodically run "git pull" on the entire Skia checkout, which will update
446 # the GM expectations along with everything else (such as this script).
447 #
448 # We can still allow --actuals-dir to be specified, though, because the
449 # actual results will continue to be maintained in the skia-autogen
450 # SVN repository.
451 parser.add_argument('--deprecated-expectations-dir',
452 help=('DEPRECATED due to our transition from SVN to git '
453 '(formerly known as --expectations-dir). '
454 'If you still need this option, contact '
455 'epoger@google.com as soon as possible. WAS: '
456 'Directory under which to find GM expectations; '
epoger@google.comf9d134d2013-09-27 15:02:44 +0000457 'defaults to %(default)s'),
458 default=DEFAULT_EXPECTATIONS_DIR)
459 parser.add_argument('--export', action='store_true',
460 help=('Instead of only allowing access from HTTP clients '
461 'on localhost, allow HTTP clients on other hosts '
462 'to access this server. WARNING: doing so will '
463 'allow users on other hosts to modify your '
epoger@google.com542b65f2013-10-15 20:10:33 +0000464 'GM expectations, if combined with --editable.'))
epoger@google.comafaad3d2013-09-30 15:06:25 +0000465 parser.add_argument('--port', type=int,
466 help=('Which TCP port to listen on for HTTP requests; '
467 'defaults to %(default)s'),
468 default=DEFAULT_PORT)
epoger@google.com542b65f2013-10-15 20:10:33 +0000469 parser.add_argument('--reload', type=int,
470 help=('How often (a period in seconds) to update the '
epoger@google.comcc64e7d2013-11-21 18:11:34 +0000471 'results. If specified, both '
472 'DEPRECATED_EXPECTATIONS_DIR and '
epoger@google.com542b65f2013-10-15 20:10:33 +0000473 'ACTUAL_DIR will be updated. '
474 'By default, we do not reload at all, and you '
475 'must restart the server to pick up new data.'),
476 default=0)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000477 args = parser.parse_args()
478 global _SERVER
epoger@google.com542b65f2013-10-15 20:10:33 +0000479 _SERVER = Server(actuals_dir=args.actuals_dir,
epoger@google.comcc64e7d2013-11-21 18:11:34 +0000480 expectations_dir=args.deprecated_expectations_dir,
epoger@google.com542b65f2013-10-15 20:10:33 +0000481 port=args.port, export=args.export, editable=args.editable,
482 reload_seconds=args.reload)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000483 _SERVER.run()
484
485if __name__ == '__main__':
486 main()