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 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
53 'css': 'text/css', | 53 'css': 'text/css', |
54 'png': 'image/png', | 54 'png': 'image/png', |
55 'js': 'application/javascript', | 55 'js': 'application/javascript', |
56 'json': 'application/json' | 56 'json': 'application/json' |
57 } | 57 } |
58 | 58 |
59 DEFAULT_ACTUALS_DIR = '.gm-actuals' | 59 DEFAULT_ACTUALS_DIR = '.gm-actuals' |
60 DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') | 60 DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') |
61 DEFAULT_PORT = 8888 | 61 DEFAULT_PORT = 8888 |
62 | 62 |
| 63 _HTTP_HEADER_CONTENT_LENGTH = 'Content-Length' |
| 64 _HTTP_HEADER_CONTENT_TYPE = 'Content-Type' |
| 65 |
63 _SERVER = None # This gets filled in by main() | 66 _SERVER = None # This gets filled in by main() |
64 | 67 |
65 class Server(object): | 68 class Server(object): |
66 """ HTTP server for our HTML rebaseline viewer. """ | 69 """ HTTP server for our HTML rebaseline viewer. """ |
67 | 70 |
68 def __init__(self, | 71 def __init__(self, |
69 actuals_dir=DEFAULT_ACTUALS_DIR, | 72 actuals_dir=DEFAULT_ACTUALS_DIR, |
70 expectations_dir=DEFAULT_EXPECTATIONS_DIR, | 73 expectations_dir=DEFAULT_EXPECTATIONS_DIR, |
71 port=DEFAULT_PORT, export=False, editable=True, | 74 port=DEFAULT_PORT, export=False, editable=True, |
72 reload_seconds=0): | 75 reload_seconds=0): |
(...skipping 23 matching lines...) Expand all Loading... |
96 | 99 |
97 def is_editable(self): | 100 def is_editable(self): |
98 """ Returns true iff HTTP clients are allowed to submit new baselines. """ | 101 """ Returns true iff HTTP clients are allowed to submit new baselines. """ |
99 return self._editable | 102 return self._editable |
100 | 103 |
101 def reload_seconds(self): | 104 def reload_seconds(self): |
102 """ Returns the result reload period in seconds, or 0 if we don't reload | 105 """ Returns the result reload period in seconds, or 0 if we don't reload |
103 results. """ | 106 results. """ |
104 return self._reload_seconds | 107 return self._reload_seconds |
105 | 108 |
106 def _update_results(self): | 109 def update_results(self): |
107 """ Create or update self.results, based on the expectations in | 110 """ Create or update self.results, based on the expectations in |
108 self._expectations_dir and the latest actuals from skia-autogen. | 111 self._expectations_dir and the latest actuals from skia-autogen. |
109 """ | 112 """ |
110 logging.info('Updating actual GM results in %s from SVN repo %s ...' % ( | 113 with self.results_lock: |
111 self._actuals_dir, ACTUALS_SVN_REPO)) | 114 # self.results_lock prevents us from updating the actual GM results |
112 actuals_repo = svn.Svn(self._actuals_dir) | 115 # in multiple threads simultaneously |
113 if not os.path.isdir(self._actuals_dir): | 116 logging.info('Updating actual GM results in %s from SVN repo %s ...' % ( |
114 os.makedirs(self._actuals_dir) | 117 self._actuals_dir, ACTUALS_SVN_REPO)) |
115 actuals_repo.Checkout(ACTUALS_SVN_REPO, '.') | 118 actuals_repo = svn.Svn(self._actuals_dir) |
116 else: | 119 if not os.path.isdir(self._actuals_dir): |
117 actuals_repo.Update('.') | 120 os.makedirs(self._actuals_dir) |
| 121 actuals_repo.Checkout(ACTUALS_SVN_REPO, '.') |
| 122 else: |
| 123 actuals_repo.Update('.') |
118 | 124 |
119 # We only update the expectations dir if the server was run with a nonzero | 125 # We only update the expectations dir if the server was run with a |
120 # --reload argument; otherwise, we expect the user to maintain her own | 126 # nonzero --reload argument; otherwise, we expect the user to maintain |
121 # expectations as she sees fit. | 127 # her own expectations as she sees fit. |
122 # | 128 # |
123 # TODO(epoger): Use git instead of svn to check out expectations, since | 129 # self.results_lock prevents us from updating the expected GM results |
124 # the Skia repo is moving to git. | 130 # in multiple threads simultaneously |
125 if self._reload_seconds: | 131 # |
126 logging.info('Updating expected GM results in %s from SVN repo %s ...' % ( | 132 # TODO(epoger): Use git instead of svn to check out expectations, since |
127 self._expectations_dir, EXPECTATIONS_SVN_REPO)) | 133 # the Skia repo is moving to git. |
128 expectations_repo = svn.Svn(self._expectations_dir) | 134 if self._reload_seconds: |
129 if not os.path.isdir(self._expectations_dir): | 135 logging.info( |
130 os.makedirs(self._expectations_dir) | 136 'Updating expected GM results in %s from SVN repo %s ...' % ( |
131 expectations_repo.Checkout(EXPECTATIONS_SVN_REPO, '.') | 137 self._expectations_dir, EXPECTATIONS_SVN_REPO)) |
132 else: | 138 expectations_repo = svn.Svn(self._expectations_dir) |
133 expectations_repo.Update('.') | 139 if not os.path.isdir(self._expectations_dir): |
| 140 os.makedirs(self._expectations_dir) |
| 141 expectations_repo.Checkout(EXPECTATIONS_SVN_REPO, '.') |
| 142 else: |
| 143 expectations_repo.Update('.') |
134 | 144 |
135 logging.info( | 145 logging.info( |
136 'Parsing results from actuals in %s and expectations in %s ...' % ( | 146 'Parsing results from actuals in %s and expectations in %s ...' % ( |
137 self._actuals_dir, self._expectations_dir)) | 147 self._actuals_dir, self._expectations_dir)) |
138 self.results = results.Results( | 148 self.results = results.Results( |
139 actuals_root=self._actuals_dir, | 149 actuals_root=self._actuals_dir, |
140 expected_root=self._expectations_dir) | 150 expected_root=self._expectations_dir) |
141 | 151 |
142 def _result_reloader(self): | 152 def _result_reloader(self): |
143 """ If --reload argument was specified, reload results at the appropriate | 153 """ If --reload argument was specified, reload results at the appropriate |
144 interval. | 154 interval. |
145 """ | 155 """ |
146 while self._reload_seconds: | 156 while self._reload_seconds: |
147 time.sleep(self._reload_seconds) | 157 time.sleep(self._reload_seconds) |
148 with self.results_lock: | 158 self.update_results() |
149 self._update_results() | |
150 | 159 |
151 def run(self): | 160 def run(self): |
152 self._update_results() | |
153 self.results_lock = thread.allocate_lock() | 161 self.results_lock = thread.allocate_lock() |
| 162 self.update_results() |
154 thread.start_new_thread(self._result_reloader, ()) | 163 thread.start_new_thread(self._result_reloader, ()) |
155 | 164 |
156 if self._export: | 165 if self._export: |
157 server_address = ('', self._port) | 166 server_address = ('', self._port) |
158 if self._editable: | 167 if self._editable: |
159 logging.warning('Running with combination of "export" and "editable" ' | 168 logging.warning('Running with combination of "export" and "editable" ' |
160 'flags. Users on other machines will ' | 169 'flags. Users on other machines will ' |
161 'be able to modify your GM expectations!') | 170 'be able to modify your GM expectations!') |
162 else: | 171 else: |
163 server_address = ('127.0.0.1', self._port) | 172 server_address = ('127.0.0.1', self._port) |
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
217 # 2. when the caller should check back for new data (if ever) | 226 # 2. when the caller should check back for new data (if ever) |
218 # | 227 # |
219 # We only return these timestamps if the --reload argument was passed; | 228 # We only return these timestamps if the --reload argument was passed; |
220 # otherwise, we have no idea when the expectations were last updated | 229 # otherwise, we have no idea when the expectations were last updated |
221 # (we allow the user to maintain her own expectations as she sees fit). | 230 # (we allow the user to maintain her own expectations as she sees fit). |
222 'timeUpdated': time_updated if _SERVER.reload_seconds() else None, | 231 'timeUpdated': time_updated if _SERVER.reload_seconds() else None, |
223 'timeNextUpdateAvailable': ( | 232 'timeNextUpdateAvailable': ( |
224 (time_updated+_SERVER.reload_seconds()) if _SERVER.reload_seconds() | 233 (time_updated+_SERVER.reload_seconds()) if _SERVER.reload_seconds() |
225 else None), | 234 else None), |
226 | 235 |
| 236 # The type we passed to get_results_of_type() |
| 237 'type': type, |
| 238 |
227 # Hash of testData, which the client must return with any edits-- | 239 # Hash of testData, which the client must return with any edits-- |
228 # this ensures that the edits were made to a particular dataset. | 240 # this ensures that the edits were made to a particular dataset. |
229 'dataHash': str(hash(repr(response_dict['testData']))), | 241 'dataHash': str(hash(repr(response_dict['testData']))), |
230 | 242 |
231 # Whether the server will accept edits back. | 243 # Whether the server will accept edits back. |
232 'isEditable': _SERVER.is_editable(), | 244 'isEditable': _SERVER.is_editable(), |
233 | 245 |
234 # Whether the service is accessible from other hosts. | 246 # Whether the service is accessible from other hosts. |
235 'isExported': _SERVER.is_exported(), | 247 'isExported': _SERVER.is_exported(), |
236 } | 248 } |
(...skipping 17 matching lines...) Expand all Loading... |
254 static_dir = os.path.realpath(os.path.join(PARENT_DIRECTORY, 'static')) | 266 static_dir = os.path.realpath(os.path.join(PARENT_DIRECTORY, 'static')) |
255 full_path = os.path.realpath(os.path.join(static_dir, path)) | 267 full_path = os.path.realpath(os.path.join(static_dir, path)) |
256 if full_path.startswith(static_dir): | 268 if full_path.startswith(static_dir): |
257 self.send_file(full_path) | 269 self.send_file(full_path) |
258 else: | 270 else: |
259 logging.error( | 271 logging.error( |
260 'Attempted do_GET_static() of path [%s] outside of static dir [%s]' | 272 'Attempted do_GET_static() of path [%s] outside of static dir [%s]' |
261 % (full_path, static_dir)) | 273 % (full_path, static_dir)) |
262 self.send_error(404) | 274 self.send_error(404) |
263 | 275 |
| 276 def do_POST(self): |
| 277 """ Handles all POST requests, forwarding them to the appropriate |
| 278 do_POST_* dispatcher. """ |
| 279 # All requests must be of this form: |
| 280 # /dispatcher |
| 281 # where 'dispatcher' indicates which do_POST_* dispatcher to run. |
| 282 normpath = posixpath.normpath(self.path) |
| 283 dispatchers = { |
| 284 '/edits': self.do_POST_edits, |
| 285 } |
| 286 try: |
| 287 dispatcher = dispatchers[normpath] |
| 288 dispatcher() |
| 289 self.send_response(200) |
| 290 except: |
| 291 self.send_error(404) |
| 292 raise |
| 293 |
| 294 def do_POST_edits(self): |
| 295 """ Handle a POST request with modifications to GM expectations, in this |
| 296 format: |
| 297 |
| 298 { |
| 299 'oldResultsType': 'all', # type of results that the client loaded |
| 300 # and then made modifications to |
| 301 'oldResultsHash': 39850913, # hash of results when the client loaded them |
| 302 # (ensures that the client and server apply |
| 303 # modifications to the same base) |
| 304 'modifications': [ |
| 305 { |
| 306 'builder': 'Test-Android-Nexus10-MaliT604-Arm7-Debug', |
| 307 'test': 'strokerect', |
| 308 'config': 'gpu', |
| 309 'expectedHashType': 'bitmap-64bitMD5', |
| 310 'expectedHashDigest': '1707359671708613629', |
| 311 }, |
| 312 ... |
| 313 ], |
| 314 } |
| 315 |
| 316 Raises an Exception if there were any problems. |
| 317 """ |
| 318 if not _SERVER.is_editable(): |
| 319 raise Exception('this server is not running in --editable mode') |
| 320 |
| 321 content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE] |
| 322 if content_type != 'application/json;charset=UTF-8': |
| 323 raise Exception('unsupported %s [%s]' % ( |
| 324 _HTTP_HEADER_CONTENT_TYPE, content_type)) |
| 325 |
| 326 content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH]) |
| 327 json_data = self.rfile.read(content_length) |
| 328 data = json.loads(json_data) |
| 329 logging.debug('do_POST_edits: received new GM expectations data [%s]' % |
| 330 data) |
| 331 |
| 332 with _SERVER.results_lock: |
| 333 oldResultsType = data['oldResultsType'] |
| 334 oldResults = _SERVER.results.get_results_of_type(oldResultsType) |
| 335 oldResultsHash = str(hash(repr(oldResults['testData']))) |
| 336 if oldResultsHash != data['oldResultsHash']: |
| 337 raise Exception('results of type "%s" changed while the client was ' |
| 338 'making modifications. The client should reload the ' |
| 339 'results and submit the modifications again.' % |
| 340 oldResultsType) |
| 341 _SERVER.results.edit_expectations(data['modifications']) |
| 342 |
| 343 # Now that the edits have been committed, update results to reflect them. |
| 344 _SERVER.update_results() |
| 345 |
264 def redirect_to(self, url): | 346 def redirect_to(self, url): |
265 """ Redirect the HTTP client to a different url. | 347 """ Redirect the HTTP client to a different url. |
266 | 348 |
267 Args: | 349 Args: |
268 url: URL to redirect the HTTP client to | 350 url: URL to redirect the HTTP client to |
269 """ | 351 """ |
270 self.send_response(301) | 352 self.send_response(301) |
271 self.send_header('Location', url) | 353 self.send_header('Location', url) |
272 self.end_headers() | 354 self.end_headers() |
273 | 355 |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
311 | 393 |
312 def main(): | 394 def main(): |
313 logging.basicConfig(level=logging.INFO) | 395 logging.basicConfig(level=logging.INFO) |
314 parser = argparse.ArgumentParser() | 396 parser = argparse.ArgumentParser() |
315 parser.add_argument('--actuals-dir', | 397 parser.add_argument('--actuals-dir', |
316 help=('Directory into which we will check out the latest ' | 398 help=('Directory into which we will check out the latest ' |
317 'actual GM results. If this directory does not ' | 399 'actual GM results. If this directory does not ' |
318 'exist, it will be created. Defaults to %(default)s'), | 400 'exist, it will be created. Defaults to %(default)s'), |
319 default=DEFAULT_ACTUALS_DIR) | 401 default=DEFAULT_ACTUALS_DIR) |
320 parser.add_argument('--editable', action='store_true', | 402 parser.add_argument('--editable', action='store_true', |
321 help=('TODO(epoger): NOT YET IMPLEMENTED. ' | 403 help=('Allow HTTP clients to submit new baselines.')) |
322 'Allow HTTP clients to submit new baselines.')) | |
323 parser.add_argument('--expectations-dir', | 404 parser.add_argument('--expectations-dir', |
324 help=('Directory under which to find GM expectations; ' | 405 help=('Directory under which to find GM expectations; ' |
325 'defaults to %(default)s'), | 406 'defaults to %(default)s'), |
326 default=DEFAULT_EXPECTATIONS_DIR) | 407 default=DEFAULT_EXPECTATIONS_DIR) |
327 parser.add_argument('--export', action='store_true', | 408 parser.add_argument('--export', action='store_true', |
328 help=('Instead of only allowing access from HTTP clients ' | 409 help=('Instead of only allowing access from HTTP clients ' |
329 'on localhost, allow HTTP clients on other hosts ' | 410 'on localhost, allow HTTP clients on other hosts ' |
330 'to access this server. WARNING: doing so will ' | 411 'to access this server. WARNING: doing so will ' |
331 'allow users on other hosts to modify your ' | 412 'allow users on other hosts to modify your ' |
332 'GM expectations, if combined with --editable.')) | 413 'GM expectations, if combined with --editable.')) |
(...skipping 11 matching lines...) Expand all Loading... |
344 args = parser.parse_args() | 425 args = parser.parse_args() |
345 global _SERVER | 426 global _SERVER |
346 _SERVER = Server(actuals_dir=args.actuals_dir, | 427 _SERVER = Server(actuals_dir=args.actuals_dir, |
347 expectations_dir=args.expectations_dir, | 428 expectations_dir=args.expectations_dir, |
348 port=args.port, export=args.export, editable=args.editable, | 429 port=args.port, export=args.export, editable=args.editable, |
349 reload_seconds=args.reload) | 430 reload_seconds=args.reload) |
350 _SERVER.run() | 431 _SERVER.run() |
351 | 432 |
352 if __name__ == '__main__': | 433 if __name__ == '__main__': |
353 main() | 434 main() |
OLD | NEW |