Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(42)

Side by Side Diff: gm/rebaseline_server/server.py

Issue 86343002: rebaseline_server: make --reload work in git checkout (Closed) Base URL: http://skia.googlecode.com/svn/trunk/
Patch Set: Created 7 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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()
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698