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

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

Issue 310093003: rebaseline_server: download actual-results.json files from GCS instead of SVN (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: download actual-results.json files from GCS instead of SVN Created 6 years, 6 months 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
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 """
(...skipping 10 matching lines...) Expand all
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 this dir 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
53 import compare_to_expectations 47 import compare_to_expectations
48 import download_actuals
54 import imagepairset 49 import imagepairset
55 import results as results_mod 50 import results as results_mod
56 51
57 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') 52 PATHSPLIT_RE = re.compile('/([^/]+)/(.+)')
58 53
59 # A simple dictionary of file name extensions to MIME types. The empty string 54 # A simple dictionary of file name extensions to MIME types. The empty string
60 # entry is used as the default when no extension was given or if the extension 55 # entry is used as the default when no extension was given or if the extension
61 # has no entry in this dictionary. 56 # has no entry in this dictionary.
62 MIME_TYPE_MAP = {'': 'application/octet-stream', 57 MIME_TYPE_MAP = {'': 'application/octet-stream',
63 'html': 'text/html', 58 'html': 'text/html',
64 'css': 'text/css', 59 'css': 'text/css',
65 'png': 'image/png', 60 'png': 'image/png',
66 'js': 'application/javascript', 61 'js': 'application/javascript',
67 'json': 'application/json' 62 'json': 'application/json'
68 } 63 }
69 64
70 # Keys that server.py uses to create the toplevel content header. 65 # Keys that server.py uses to create the toplevel content header.
71 # NOTE: Keep these in sync with static/constants.js 66 # NOTE: Keep these in sync with static/constants.js
72 KEY__EDITS__MODIFICATIONS = 'modifications' 67 KEY__EDITS__MODIFICATIONS = 'modifications'
73 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash' 68 KEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash'
74 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType' 69 KEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType'
75 70
76 DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR 71 DEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR
77 DEFAULT_ACTUALS_REPO_REVISION = 'HEAD' 72 DEFAULT_GM_SUMMARIES_BUCKET = download_actuals.GM_SUMMARIES_BUCKET
78 DEFAULT_ACTUALS_REPO_URL = 'http://skia-autogen.googlecode.com/svn/gm-actual' 73 DEFAULT_JSON_FILENAME = download_actuals.DEFAULT_JSON_FILENAME
79 DEFAULT_PORT = 8888 74 DEFAULT_PORT = 8888
80 75
81 # Directory, relative to PARENT_DIRECTORY, within which the server will serve 76 # Directory, relative to PARENT_DIRECTORY, within which the server will serve
82 # out live results (not static files). 77 # out live results (not static files).
83 RESULTS_SUBDIR = 'results' 78 RESULTS_SUBDIR = 'results'
84 # Directory, relative to PARENT_DIRECTORY, within which the server will serve 79 # Directory, relative to PARENT_DIRECTORY, within which the server will serve
85 # out static files. 80 # out static files.
86 STATIC_CONTENTS_SUBDIR = 'static' 81 STATIC_CONTENTS_SUBDIR = 'static'
87 # All of the GENERATED_*_SUBDIRS are relative to STATIC_CONTENTS_SUBDIR 82 # All of the GENERATED_*_SUBDIRS are relative to STATIC_CONTENTS_SUBDIR
88 GENERATED_HTML_SUBDIR = 'generated-html' 83 GENERATED_HTML_SUBDIR = 'generated-html'
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after
132 """Returns routable IP address of this host (the IP address of its network 127 """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 128 interface that would be used for most traffic, not its localhost
134 interface). See http://stackoverflow.com/a/166589 """ 129 interface). See http://stackoverflow.com/a/166589 """
135 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 130 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
136 sock.connect(('8.8.8.8', 80)) 131 sock.connect(('8.8.8.8', 80))
137 host = sock.getsockname()[0] 132 host = sock.getsockname()[0]
138 sock.close() 133 sock.close()
139 return host 134 return host
140 135
141 136
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): 137 def _create_index(file_path, config_pairs):
161 """Creates an index file linking to all results available from this server. 138 """Creates an index file linking to all results available from this server.
162 139
163 Prior to https://codereview.chromium.org/215503002 , we had a static 140 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 141 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 142 config comparisons, index.html needs to be generated differently depending
166 on which results are included. 143 on which results are included.
167 144
168 TODO(epoger): Instead of including raw HTML within the Python code, 145 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 146 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
206 file_handle.write('</li>') 183 file_handle.write('</li>')
207 file_handle.write('</ul>') 184 file_handle.write('</ul>')
208 file_handle.write('</ul></body></html>') 185 file_handle.write('</ul></body></html>')
209 186
210 187
211 class Server(object): 188 class Server(object):
212 """ HTTP server for our HTML rebaseline viewer. """ 189 """ HTTP server for our HTML rebaseline viewer. """
213 190
214 def __init__(self, 191 def __init__(self,
215 actuals_dir=DEFAULT_ACTUALS_DIR, 192 actuals_dir=DEFAULT_ACTUALS_DIR,
216 actuals_repo_revision=DEFAULT_ACTUALS_REPO_REVISION, 193 json_filename=DEFAULT_JSON_FILENAME,
217 actuals_repo_url=DEFAULT_ACTUALS_REPO_URL, 194 gm_summaries_bucket=DEFAULT_GM_SUMMARIES_BUCKET,
218 port=DEFAULT_PORT, export=False, editable=True, 195 port=DEFAULT_PORT, export=False, editable=True,
219 reload_seconds=0, config_pairs=None, builder_regex_list=None): 196 reload_seconds=0, config_pairs=None, builder_regex_list=None):
220 """ 197 """
221 Args: 198 Args:
222 actuals_dir: directory under which we will check out the latest actual 199 actuals_dir: directory under which we will check out the latest actual
223 GM results 200 GM results
224 actuals_repo_revision: revision of actual-results.json files to process 201 json_filename: basename of the JSON summary file to load for each builder
225 actuals_repo_url: SVN repo to download actual-results.json files from; 202 gm_summaries_bucket: Google Storage bucket to download json_filename
226 if None or '', don't fetch new actual-results files at all, 203 files from; if None or '', don't fetch new actual-results files
227 just compare to whatever files are already in actuals_dir 204 at all, just compare to whatever files are already in actuals_dir
228 port: which TCP port to listen on for HTTP requests 205 port: which TCP port to listen on for HTTP requests
229 export: whether to allow HTTP clients on other hosts to access this server 206 export: whether to allow HTTP clients on other hosts to access this server
230 editable: whether HTTP clients are allowed to submit new baselines 207 editable: whether HTTP clients are allowed to submit new baselines
231 reload_seconds: polling interval with which to check for new results; 208 reload_seconds: polling interval with which to check for new results;
232 if 0, don't check for new results at all 209 if 0, don't check for new results at all
233 config_pairs: List of (string, string) tuples; for each tuple, compare 210 config_pairs: List of (string, string) tuples; for each tuple, compare
234 actual results of these two configs. If None or empty, 211 actual results of these two configs. If None or empty,
235 don't compare configs at all. 212 don't compare configs at all.
236 builder_regex_list: List of regular expressions specifying which builders 213 builder_regex_list: List of regular expressions specifying which builders
237 we will process. If None, process all builders. 214 we will process. If None, process all builders.
238 """ 215 """
239 self._actuals_dir = actuals_dir 216 self._actuals_dir = actuals_dir
240 self._actuals_repo_revision = actuals_repo_revision 217 self._json_filename = json_filename
241 self._actuals_repo_url = actuals_repo_url 218 self._gm_summaries_bucket = gm_summaries_bucket
242 self._port = port 219 self._port = port
243 self._export = export 220 self._export = export
244 self._editable = editable 221 self._editable = editable
245 self._reload_seconds = reload_seconds 222 self._reload_seconds = reload_seconds
246 self._config_pairs = config_pairs or [] 223 self._config_pairs = config_pairs or []
247 self._builder_regex_list = builder_regex_list 224 self._builder_regex_list = builder_regex_list
248 _create_index( 225 _create_index(
249 file_path=os.path.join( 226 file_path=os.path.join(
250 PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR, GENERATED_HTML_SUBDIR, 227 PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR, GENERATED_HTML_SUBDIR,
251 "index.html"), 228 "index.html"),
252 config_pairs=config_pairs) 229 config_pairs=config_pairs)
253 # TODO(epoger): Create shareable functions within download_actuals.py that
254 # we can use both there and here to download the actual image results.
255 if actuals_repo_url:
256 self._actuals_repo = _create_svn_checkout(
257 dir_path=actuals_dir, repo_url=actuals_repo_url)
258 230
259 # Reentrant lock that must be held whenever updating EITHER of: 231 # Reentrant lock that must be held whenever updating EITHER of:
260 # 1. self._results 232 # 1. self._results
261 # 2. the expected or actual results on local disk 233 # 2. the expected or actual results on local disk
262 self.results_rlock = threading.RLock() 234 self.results_rlock = threading.RLock()
263 # self._results will be filled in by calls to update_results() 235 # self._results will be filled in by calls to update_results()
264 self._results = None 236 self._results = None
265 237
266 @property 238 @property
267 def results(self): 239 def results(self):
(...skipping 27 matching lines...) Expand all
295 the same time. 267 the same time.
296 268
297 Args: 269 Args:
298 invalidate: if True, invalidate self._results immediately upon entry; 270 invalidate: if True, invalidate self._results immediately upon entry;
299 otherwise, we will let readers see those results until we 271 otherwise, we will let readers see those results until we
300 replace them 272 replace them
301 """ 273 """
302 with self.results_rlock: 274 with self.results_rlock:
303 if invalidate: 275 if invalidate:
304 self._results = None 276 self._results = None
305 if self._actuals_repo_url: 277 if self._gm_summaries_bucket:
306 logging.info( 278 logging.info(
307 'Updating actual GM results in %s to revision %s from repo %s ...' 279 'Updating GM result summaries in %s from gm_summaries_bucket %s ...'
308 % ( 280 % (self._actuals_dir, self._gm_summaries_bucket))
309 self._actuals_dir, self._actuals_repo_revision, 281
310 self._actuals_repo_url)) 282 # Clean out actuals_dir first, in case some builders have gone away
311 self._actuals_repo.Update( 283 # since we last ran.
312 path='.', revision=self._actuals_repo_revision) 284 if os.path.isdir(self._actuals_dir):
285 shutil.rmtree(self._actuals_dir)
286
287 # Get the list of builders we care about.
288 all_builders = download_actuals.get_builders_list(
289 summaries_bucket=self._gm_summaries_bucket)
290 if self._builder_regex_list:
291 matching_builders = []
292 for builder in all_builders:
293 for regex in self._builder_regex_list:
294 if re.match(regex, builder):
295 matching_builders.append(builder)
296 break # go on to the next builder, no need to try more regexes
297 else:
298 matching_builders = all_builders
299
300 # Download the JSON file for each builder we care about.
301 #
302 # TODO(epoger): When this is a large number of builders, we would be
303 # better off downloading them in parallel!
304 for builder in matching_builders:
305 download_actuals.gcs_download_file(
306 source_bucket=self._gm_summaries_bucket,
307 source_path=posixpath.join(builder, self._json_filename),
308 dest_path=os.path.join(self._actuals_dir, builder,
309 self._json_filename),
310 create_subdirs_if_needed=True)
313 311
314 # We only update the expectations dir if the server was run with a 312 # We only update the expectations dir if the server was run with a
315 # nonzero --reload argument; otherwise, we expect the user to maintain 313 # nonzero --reload argument; otherwise, we expect the user to maintain
316 # her own expectations as she sees fit. 314 # her own expectations as she sees fit.
317 # 315 #
318 # Because the Skia repo is moving from SVN to git, and git does not 316 # Because the Skia repo is hosted using git, and git does not
319 # support updating a single directory tree, we have to update the entire 317 # support updating a single directory tree, we have to update the entire
320 # repo checkout. 318 # repo checkout.
321 # 319 #
322 # Because Skia uses depot_tools, we have to update using "gclient sync" 320 # Because Skia uses depot_tools, we have to update using "gclient sync"
323 # instead of raw git (or SVN) update. Happily, this will work whether 321 # instead of raw git commands.
borenet 2014/06/03 20:27:47 This is not really true, and the below will not ha
epoger 2014/06/03 20:39:04 Yikes. [Insert "blah blah what is our officially
324 # the checkout was created using git or SVN.
325 if self._reload_seconds: 322 if self._reload_seconds:
326 logging.info( 323 logging.info(
327 'Updating expected GM results in %s by syncing Skia repo ...' % 324 'Updating expected GM results in %s by syncing Skia repo ...' %
328 compare_to_expectations.DEFAULT_EXPECTATIONS_DIR) 325 compare_to_expectations.DEFAULT_EXPECTATIONS_DIR)
329 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY) 326 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY)
330 327
331 self._results = compare_to_expectations.ExpectationComparisons( 328 self._results = compare_to_expectations.ExpectationComparisons(
332 actuals_root=self._actuals_dir, 329 actuals_root=self._actuals_dir,
333 generated_images_root=os.path.join( 330 generated_images_root=os.path.join(
334 PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR, 331 PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR,
(...skipping 281 matching lines...) Expand 10 before | Expand all | Expand 10 after
616 def main(): 613 def main():
617 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', 614 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
618 datefmt='%m/%d/%Y %H:%M:%S', 615 datefmt='%m/%d/%Y %H:%M:%S',
619 level=logging.INFO) 616 level=logging.INFO)
620 parser = argparse.ArgumentParser() 617 parser = argparse.ArgumentParser()
621 parser.add_argument('--actuals-dir', 618 parser.add_argument('--actuals-dir',
622 help=('Directory into which we will check out the latest ' 619 help=('Directory into which we will check out the latest '
623 'actual GM results. If this directory does not ' 620 'actual GM results. If this directory does not '
624 'exist, it will be created. Defaults to %(default)s'), 621 'exist, it will be created. Defaults to %(default)s'),
625 default=DEFAULT_ACTUALS_DIR) 622 default=DEFAULT_ACTUALS_DIR)
626 parser.add_argument('--actuals-repo', 623 # TODO(epoger): Before https://codereview.chromium.org/310093003 ,
627 help=('URL of SVN repo to download actual-results.json ' 624 # when this tool downloaded the JSON summaries from skia-autogen,
628 'files from. Defaults to %(default)s ; if set to ' 625 # it had an --actuals-revision the caller could specify to download
629 'empty string, just compare to actual-results ' 626 # actual results as of a specific point in time. We should add similar
630 'already found in ACTUALS_DIR.'), 627 # functionality when retrieving the summaries from Google Storage.
631 default=DEFAULT_ACTUALS_REPO_URL)
632 parser.add_argument('--actuals-revision',
633 help=('revision of actual-results.json files to process. '
634 'Defaults to %(default)s . Beware of setting this '
635 'argument in conjunction with --editable; you '
636 'probably only want to edit results at HEAD.'),
637 default=DEFAULT_ACTUALS_REPO_REVISION)
638 parser.add_argument('--builders', metavar='BUILDER_REGEX', nargs='+', 628 parser.add_argument('--builders', metavar='BUILDER_REGEX', nargs='+',
639 help=('Only process builders matching these regular ' 629 help=('Only process builders matching these regular '
640 'expressions. If unspecified, process all ' 630 'expressions. If unspecified, process all '
641 'builders.')) 631 'builders.'))
642 parser.add_argument('--compare-configs', action='store_true', 632 parser.add_argument('--compare-configs', action='store_true',
643 help=('In addition to generating differences between ' 633 help=('In addition to generating differences between '
644 'expectations and actuals, also generate ' 634 'expectations and actuals, also generate '
645 'differences between these config pairs: ' 635 'differences between these config pairs: '
646 + str(CONFIG_PAIRS_TO_COMPARE))) 636 + str(CONFIG_PAIRS_TO_COMPARE)))
647 parser.add_argument('--editable', action='store_true', 637 parser.add_argument('--editable', action='store_true',
648 help=('Allow HTTP clients to submit new baselines.')) 638 help=('Allow HTTP clients to submit new baselines.'))
649 parser.add_argument('--export', action='store_true', 639 parser.add_argument('--export', action='store_true',
650 help=('Instead of only allowing access from HTTP clients ' 640 help=('Instead of only allowing access from HTTP clients '
651 'on localhost, allow HTTP clients on other hosts ' 641 'on localhost, allow HTTP clients on other hosts '
652 'to access this server. WARNING: doing so will ' 642 'to access this server. WARNING: doing so will '
653 'allow users on other hosts to modify your ' 643 'allow users on other hosts to modify your '
654 'GM expectations, if combined with --editable.')) 644 'GM expectations, if combined with --editable.'))
645 parser.add_argument('--gm-summaries-bucket',
646 help=('Google Cloud Storage bucket to download '
647 'JSON_FILENAME files from. '
648 'Defaults to %(default)s ; if set to '
649 'empty string, just compare to actual-results '
650 'already found in ACTUALS_DIR.'),
651 default=DEFAULT_GM_SUMMARIES_BUCKET)
652 parser.add_argument('--json-filename',
653 help=('JSON summary filename to read for each builder; '
654 'defaults to %(default)s.'),
655 default=DEFAULT_JSON_FILENAME)
655 parser.add_argument('--port', type=int, 656 parser.add_argument('--port', type=int,
656 help=('Which TCP port to listen on for HTTP requests; ' 657 help=('Which TCP port to listen on for HTTP requests; '
657 'defaults to %(default)s'), 658 'defaults to %(default)s'),
658 default=DEFAULT_PORT) 659 default=DEFAULT_PORT)
659 parser.add_argument('--reload', type=int, 660 parser.add_argument('--reload', type=int,
660 help=('How often (a period in seconds) to update the ' 661 help=('How often (a period in seconds) to update the '
661 'results. If specified, both expected and actual ' 662 'results. If specified, both expected and actual '
662 'results will be updated by running "gclient sync" ' 663 'results will be updated by running "gclient sync" '
663 'on your Skia checkout as a whole. ' 664 'on your Skia checkout as a whole. '
664 'By default, we do not reload at all, and you ' 665 'By default, we do not reload at all, and you '
665 'must restart the server to pick up new data.'), 666 'must restart the server to pick up new data.'),
666 default=0) 667 default=0)
667 args = parser.parse_args() 668 args = parser.parse_args()
668 if args.compare_configs: 669 if args.compare_configs:
669 config_pairs = CONFIG_PAIRS_TO_COMPARE 670 config_pairs = CONFIG_PAIRS_TO_COMPARE
670 else: 671 else:
671 config_pairs = None 672 config_pairs = None
672 673
673 global _SERVER 674 global _SERVER
674 _SERVER = Server(actuals_dir=args.actuals_dir, 675 _SERVER = Server(actuals_dir=args.actuals_dir,
675 actuals_repo_revision=args.actuals_revision, 676 json_filename=args.json_filename,
676 actuals_repo_url=args.actuals_repo, 677 gm_summaries_bucket=args.gm_summaries_bucket,
677 port=args.port, export=args.export, editable=args.editable, 678 port=args.port, export=args.export, editable=args.editable,
678 reload_seconds=args.reload, config_pairs=config_pairs, 679 reload_seconds=args.reload, config_pairs=config_pairs,
679 builder_regex_list=args.builders) 680 builder_regex_list=args.builders)
680 _SERVER.run() 681 _SERVER.run()
681 682
682 683
683 if __name__ == '__main__': 684 if __name__ == '__main__':
684 main() 685 main()
OLDNEW
« gm/rebaseline_server/download_actuals.py ('K') | « gm/rebaseline_server/download_actuals.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698