| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 """Generate fake repositories for testing.""" | |
| 7 | |
| 8 import atexit | |
| 9 import datetime | |
| 10 import errno | |
| 11 import logging | |
| 12 import os | |
| 13 import pprint | |
| 14 import re | |
| 15 import socket | |
| 16 import sys | |
| 17 import tempfile | |
| 18 import time | |
| 19 | |
| 20 # trial_dir must be first for non-system libraries. | |
| 21 from tests import trial_dir | |
| 22 import gclient_utils | |
| 23 import scm | |
| 24 import subprocess2 | |
| 25 | |
| 26 | |
| 27 def write(path, content): | |
| 28 f = open(path, 'wb') | |
| 29 f.write(content) | |
| 30 f.close() | |
| 31 | |
| 32 | |
| 33 join = os.path.join | |
| 34 | |
| 35 | |
| 36 def read_tree(tree_root): | |
| 37 """Returns a dict of all the files in a tree. Defaults to self.root_dir.""" | |
| 38 tree = {} | |
| 39 for root, dirs, files in os.walk(tree_root): | |
| 40 for d in filter(lambda x: x.startswith('.'), dirs): | |
| 41 dirs.remove(d) | |
| 42 for f in [join(root, f) for f in files if not f.startswith('.')]: | |
| 43 filepath = f[len(tree_root) + 1:].replace(os.sep, '/') | |
| 44 assert len(filepath), f | |
| 45 tree[filepath] = open(join(root, f), 'rU').read() | |
| 46 return tree | |
| 47 | |
| 48 | |
| 49 def dict_diff(dict1, dict2): | |
| 50 diff = {} | |
| 51 for k, v in dict1.iteritems(): | |
| 52 if k not in dict2: | |
| 53 diff[k] = v | |
| 54 elif v != dict2[k]: | |
| 55 diff[k] = (v, dict2[k]) | |
| 56 for k, v in dict2.iteritems(): | |
| 57 if k not in dict1: | |
| 58 diff[k] = v | |
| 59 return diff | |
| 60 | |
| 61 | |
| 62 def commit_svn(repo, usr, pwd): | |
| 63 """Commits the changes and returns the new revision number.""" | |
| 64 to_add = [] | |
| 65 to_remove = [] | |
| 66 for status, filepath in scm.SVN.CaptureStatus(repo): | |
| 67 if status[0] == '?': | |
| 68 to_add.append(filepath) | |
| 69 elif status[0] == '!': | |
| 70 to_remove.append(filepath) | |
| 71 if to_add: | |
| 72 subprocess2.check_output( | |
| 73 ['svn', 'add', '--no-auto-props', '-q'] + to_add, cwd=repo) | |
| 74 if to_remove: | |
| 75 subprocess2.check_output(['svn', 'remove', '-q'] + to_remove, cwd=repo) | |
| 76 | |
| 77 out = subprocess2.check_output( | |
| 78 ['svn', 'commit', repo, '-m', 'foo', '--non-interactive', | |
| 79 '--no-auth-cache', | |
| 80 '--username', usr, '--password', pwd], | |
| 81 cwd=repo) | |
| 82 match = re.search(r'(\d+)', out) | |
| 83 if not match: | |
| 84 raise Exception('Commit failed', out) | |
| 85 rev = match.group(1) | |
| 86 status = subprocess2.check_output(['svn', 'status'], cwd=repo) | |
| 87 assert len(status) == 0, status | |
| 88 logging.debug('At revision %s' % rev) | |
| 89 return rev | |
| 90 | |
| 91 | |
| 92 def commit_git(repo): | |
| 93 """Commits the changes and returns the new hash.""" | |
| 94 subprocess2.check_call(['git', 'add', '-A', '-f'], cwd=repo) | |
| 95 subprocess2.check_call(['git', 'commit', '-q', '--message', 'foo'], cwd=repo) | |
| 96 rev = subprocess2.check_output( | |
| 97 ['git', 'show-ref', '--head', 'HEAD'], cwd=repo).split(' ', 1)[0] | |
| 98 logging.debug('At revision %s' % rev) | |
| 99 return rev | |
| 100 | |
| 101 | |
| 102 def test_port(host, port): | |
| 103 s = socket.socket() | |
| 104 try: | |
| 105 return s.connect_ex((host, port)) == 0 | |
| 106 finally: | |
| 107 s.close() | |
| 108 | |
| 109 | |
| 110 def find_free_port(host, base_port): | |
| 111 """Finds a listening port free to listen to.""" | |
| 112 while base_port < (2<<16): | |
| 113 if not test_port(host, base_port): | |
| 114 return base_port | |
| 115 base_port += 1 | |
| 116 assert False, 'Having issues finding an available port' | |
| 117 | |
| 118 | |
| 119 def wait_for_port_to_bind(host, port, process): | |
| 120 sock = socket.socket() | |
| 121 | |
| 122 if sys.platform == 'darwin': | |
| 123 # On Mac SnowLeopard, if we attempt to connect to the socket | |
| 124 # immediately, it fails with EINVAL and never gets a chance to | |
| 125 # connect (putting us into a hard spin and then failing). | |
| 126 # Linux doesn't need this. | |
| 127 time.sleep(0.1) | |
| 128 | |
| 129 try: | |
| 130 start = datetime.datetime.utcnow() | |
| 131 maxdelay = datetime.timedelta(seconds=30) | |
| 132 while (datetime.datetime.utcnow() - start) < maxdelay: | |
| 133 try: | |
| 134 sock.connect((host, port)) | |
| 135 logging.debug('%d is now bound' % port) | |
| 136 return | |
| 137 except (socket.error, EnvironmentError): | |
| 138 pass | |
| 139 logging.debug('%d is still not bound' % port) | |
| 140 finally: | |
| 141 sock.close() | |
| 142 # The process failed to bind. Kill it and dump its ouput. | |
| 143 process.kill() | |
| 144 logging.error('%s' % process.communicate()[0]) | |
| 145 assert False, '%d is still not bound' % port | |
| 146 | |
| 147 | |
| 148 def wait_for_port_to_free(host, port): | |
| 149 start = datetime.datetime.utcnow() | |
| 150 maxdelay = datetime.timedelta(seconds=30) | |
| 151 while (datetime.datetime.utcnow() - start) < maxdelay: | |
| 152 try: | |
| 153 sock = socket.socket() | |
| 154 sock.connect((host, port)) | |
| 155 logging.debug('%d was bound, waiting to free' % port) | |
| 156 except (socket.error, EnvironmentError): | |
| 157 logging.debug('%d now free' % port) | |
| 158 return | |
| 159 finally: | |
| 160 sock.close() | |
| 161 assert False, '%d is still bound' % port | |
| 162 | |
| 163 | |
| 164 _FAKE_LOADED = False | |
| 165 | |
| 166 class FakeReposBase(object): | |
| 167 """Generate both svn and git repositories to test gclient functionality. | |
| 168 | |
| 169 Many DEPS functionalities need to be tested: Var, File, From, deps_os, hooks, | |
| 170 use_relative_paths. | |
| 171 | |
| 172 And types of dependencies: Relative urls, Full urls, both svn and git. | |
| 173 | |
| 174 populateSvn() and populateGit() need to be implemented by the subclass. | |
| 175 """ | |
| 176 # Hostname | |
| 177 NB_GIT_REPOS = 1 | |
| 178 USERS = [ | |
| 179 ('user1@example.com', 'foo'), | |
| 180 ('user2@example.com', 'bar'), | |
| 181 ] | |
| 182 | |
| 183 def __init__(self, host=None): | |
| 184 global _FAKE_LOADED | |
| 185 if _FAKE_LOADED: | |
| 186 raise Exception('You can only start one FakeRepos at a time.') | |
| 187 _FAKE_LOADED = True | |
| 188 | |
| 189 self.trial = trial_dir.TrialDir('repos') | |
| 190 self.host = host or '127.0.0.1' | |
| 191 # Format is [ None, tree, tree, ...] | |
| 192 # i.e. revisions are 1-based. | |
| 193 self.svn_revs = [None] | |
| 194 # Format is { repo: [ None, (hash, tree), (hash, tree), ... ], ... } | |
| 195 # so reference looks like self.git_hashes[repo][rev][0] for hash and | |
| 196 # self.git_hashes[repo][rev][1] for it's tree snapshot. | |
| 197 # For consistency with self.svn_revs, it is 1-based too. | |
| 198 self.git_hashes = {} | |
| 199 self.svnserve = None | |
| 200 self.gitdaemon = None | |
| 201 self.git_pid_file = None | |
| 202 self.git_root = None | |
| 203 self.svn_checkout = None | |
| 204 self.svn_repo = None | |
| 205 self.git_dirty = False | |
| 206 self.svn_dirty = False | |
| 207 self.svn_port = None | |
| 208 self.git_port = None | |
| 209 self.svn_base = None | |
| 210 self.git_base = None | |
| 211 | |
| 212 @property | |
| 213 def root_dir(self): | |
| 214 return self.trial.root_dir | |
| 215 | |
| 216 def set_up(self): | |
| 217 """All late initialization comes here.""" | |
| 218 self.cleanup_dirt() | |
| 219 if not self.root_dir: | |
| 220 try: | |
| 221 # self.root_dir is not set before this call. | |
| 222 self.trial.set_up() | |
| 223 self.git_root = join(self.root_dir, 'git') | |
| 224 self.svn_checkout = join(self.root_dir, 'svn_checkout') | |
| 225 self.svn_repo = join(self.root_dir, 'svn') | |
| 226 finally: | |
| 227 # Registers cleanup. | |
| 228 atexit.register(self.tear_down) | |
| 229 | |
| 230 def cleanup_dirt(self): | |
| 231 """For each dirty repository, destroy it.""" | |
| 232 if self.svn_dirty: | |
| 233 if not self.tear_down_svn(): | |
| 234 logging.error('Using both leaking checkout and svn dirty checkout') | |
| 235 if self.git_dirty: | |
| 236 if not self.tear_down_git(): | |
| 237 logging.error('Using both leaking checkout and git dirty checkout') | |
| 238 | |
| 239 def tear_down(self): | |
| 240 """Kills the servers and delete the directories.""" | |
| 241 self.tear_down_svn() | |
| 242 self.tear_down_git() | |
| 243 # This deletes the directories. | |
| 244 self.trial.tear_down() | |
| 245 self.trial = None | |
| 246 | |
| 247 def tear_down_svn(self): | |
| 248 if self.svnserve: | |
| 249 logging.debug('Killing svnserve pid %s' % self.svnserve.pid) | |
| 250 try: | |
| 251 self.svnserve.kill() | |
| 252 except OSError, e: | |
| 253 if e.errno != errno.ESRCH: # no such process | |
| 254 raise | |
| 255 wait_for_port_to_free(self.host, self.svn_port) | |
| 256 self.svnserve = None | |
| 257 self.svn_port = None | |
| 258 self.svn_base = None | |
| 259 if not self.trial.SHOULD_LEAK: | |
| 260 logging.debug('Removing %s' % self.svn_repo) | |
| 261 gclient_utils.rmtree(self.svn_repo) | |
| 262 logging.debug('Removing %s' % self.svn_checkout) | |
| 263 gclient_utils.rmtree(self.svn_checkout) | |
| 264 else: | |
| 265 return False | |
| 266 return True | |
| 267 | |
| 268 def tear_down_git(self): | |
| 269 if self.gitdaemon: | |
| 270 logging.debug('Killing git-daemon pid %s' % self.gitdaemon.pid) | |
| 271 self.gitdaemon.kill() | |
| 272 self.gitdaemon = None | |
| 273 if self.git_pid_file: | |
| 274 pid = int(self.git_pid_file.read()) | |
| 275 self.git_pid_file.close() | |
| 276 logging.debug('Killing git daemon pid %s' % pid) | |
| 277 subprocess2.kill_pid(pid) | |
| 278 self.git_pid_file = None | |
| 279 wait_for_port_to_free(self.host, self.git_port) | |
| 280 self.git_port = None | |
| 281 self.git_base = None | |
| 282 if not self.trial.SHOULD_LEAK: | |
| 283 logging.debug('Removing %s' % self.git_root) | |
| 284 gclient_utils.rmtree(self.git_root) | |
| 285 else: | |
| 286 return False | |
| 287 return True | |
| 288 | |
| 289 @staticmethod | |
| 290 def _genTree(root, tree_dict): | |
| 291 """For a dictionary of file contents, generate a filesystem.""" | |
| 292 if not os.path.isdir(root): | |
| 293 os.makedirs(root) | |
| 294 for (k, v) in tree_dict.iteritems(): | |
| 295 k_os = k.replace('/', os.sep) | |
| 296 k_arr = k_os.split(os.sep) | |
| 297 if len(k_arr) > 1: | |
| 298 p = os.sep.join([root] + k_arr[:-1]) | |
| 299 if not os.path.isdir(p): | |
| 300 os.makedirs(p) | |
| 301 if v is None: | |
| 302 os.remove(join(root, k)) | |
| 303 else: | |
| 304 write(join(root, k), v) | |
| 305 | |
| 306 def set_up_svn(self): | |
| 307 """Creates subversion repositories and start the servers.""" | |
| 308 self.set_up() | |
| 309 if self.svnserve: | |
| 310 return True | |
| 311 try: | |
| 312 subprocess2.check_call(['svnadmin', 'create', self.svn_repo]) | |
| 313 except (OSError, subprocess2.CalledProcessError): | |
| 314 return False | |
| 315 write(join(self.svn_repo, 'conf', 'svnserve.conf'), | |
| 316 '[general]\n' | |
| 317 'anon-access = read\n' | |
| 318 'auth-access = write\n' | |
| 319 'password-db = passwd\n') | |
| 320 text = '[users]\n' | |
| 321 text += ''.join('%s = %s\n' % (usr, pwd) for usr, pwd in self.USERS) | |
| 322 write(join(self.svn_repo, 'conf', 'passwd'), text) | |
| 323 | |
| 324 # Mac 10.6 ships with a buggy subversion build and we need this line | |
| 325 # to work around the bug. | |
| 326 write(join(self.svn_repo, 'db', 'fsfs.conf'), | |
| 327 '[rep-sharing]\n' | |
| 328 'enable-rep-sharing = false\n') | |
| 329 | |
| 330 # Start the daemon. | |
| 331 self.svn_port = find_free_port(self.host, 10000) | |
| 332 logging.debug('Using port %d' % self.svn_port) | |
| 333 cmd = ['svnserve', '-d', '--foreground', '-r', self.root_dir, | |
| 334 '--listen-port=%d' % self.svn_port] | |
| 335 if self.host == '127.0.0.1': | |
| 336 cmd.append('--listen-host=' + self.host) | |
| 337 self.check_port_is_free(self.svn_port) | |
| 338 self.svnserve = subprocess2.Popen( | |
| 339 cmd, | |
| 340 cwd=self.svn_repo, | |
| 341 stdout=subprocess2.PIPE, | |
| 342 stderr=subprocess2.PIPE) | |
| 343 wait_for_port_to_bind(self.host, self.svn_port, self.svnserve) | |
| 344 self.svn_base = 'svn://%s:%d/svn/' % (self.host, self.svn_port) | |
| 345 self.populateSvn() | |
| 346 self.svn_dirty = False | |
| 347 return True | |
| 348 | |
| 349 def set_up_git(self): | |
| 350 """Creates git repositories and start the servers.""" | |
| 351 self.set_up() | |
| 352 if self.gitdaemon: | |
| 353 return True | |
| 354 assert self.git_pid_file == None | |
| 355 try: | |
| 356 subprocess2.check_output(['git', '--version']) | |
| 357 except (OSError, subprocess2.CalledProcessError): | |
| 358 return False | |
| 359 for repo in ['repo_%d' % r for r in range(1, self.NB_GIT_REPOS + 1)]: | |
| 360 subprocess2.check_call(['git', 'init', '-q', join(self.git_root, repo)]) | |
| 361 self.git_hashes[repo] = [None] | |
| 362 self.git_port = find_free_port(self.host, 20000) | |
| 363 self.git_base = 'git://%s:%d/git/' % (self.host, self.git_port) | |
| 364 # Start the daemon. | |
| 365 self.git_pid_file = tempfile.NamedTemporaryFile() | |
| 366 cmd = ['git', 'daemon', | |
| 367 '--export-all', | |
| 368 '--reuseaddr', | |
| 369 '--base-path=' + self.root_dir, | |
| 370 '--pid-file=' + self.git_pid_file.name, | |
| 371 '--port=%d' % self.git_port] | |
| 372 if self.host == '127.0.0.1': | |
| 373 cmd.append('--listen=' + self.host) | |
| 374 self.check_port_is_free(self.git_port) | |
| 375 self.gitdaemon = subprocess2.Popen( | |
| 376 cmd, | |
| 377 cwd=self.root_dir, | |
| 378 stdout=subprocess2.PIPE, | |
| 379 stderr=subprocess2.PIPE) | |
| 380 wait_for_port_to_bind(self.host, self.git_port, self.gitdaemon) | |
| 381 self.populateGit() | |
| 382 self.git_dirty = False | |
| 383 return True | |
| 384 | |
| 385 def _commit_svn(self, tree): | |
| 386 self._genTree(self.svn_checkout, tree) | |
| 387 commit_svn(self.svn_checkout, self.USERS[0][0], self.USERS[0][1]) | |
| 388 if self.svn_revs and self.svn_revs[-1]: | |
| 389 new_tree = self.svn_revs[-1].copy() | |
| 390 new_tree.update(tree) | |
| 391 else: | |
| 392 new_tree = tree.copy() | |
| 393 self.svn_revs.append(new_tree) | |
| 394 | |
| 395 def _commit_git(self, repo, tree): | |
| 396 repo_root = join(self.git_root, repo) | |
| 397 self._genTree(repo_root, tree) | |
| 398 commit_hash = commit_git(repo_root) | |
| 399 if self.git_hashes[repo][-1]: | |
| 400 new_tree = self.git_hashes[repo][-1][1].copy() | |
| 401 new_tree.update(tree) | |
| 402 else: | |
| 403 new_tree = tree.copy() | |
| 404 self.git_hashes[repo].append((commit_hash, new_tree)) | |
| 405 | |
| 406 def check_port_is_free(self, port): | |
| 407 sock = socket.socket() | |
| 408 try: | |
| 409 sock.connect((self.host, port)) | |
| 410 # It worked, throw. | |
| 411 assert False, '%d shouldn\'t be bound' % port | |
| 412 except (socket.error, EnvironmentError): | |
| 413 pass | |
| 414 finally: | |
| 415 sock.close() | |
| 416 | |
| 417 def populateSvn(self): | |
| 418 raise NotImplementedError() | |
| 419 | |
| 420 def populateGit(self): | |
| 421 raise NotImplementedError() | |
| 422 | |
| 423 | |
| 424 class FakeRepos(FakeReposBase): | |
| 425 """Implements populateSvn() and populateGit().""" | |
| 426 NB_GIT_REPOS = 4 | |
| 427 | |
| 428 def populateSvn(self): | |
| 429 """Creates a few revisions of changes including DEPS files.""" | |
| 430 # Repos | |
| 431 subprocess2.check_call( | |
| 432 ['svn', 'checkout', self.svn_base, self.svn_checkout, | |
| 433 '-q', '--non-interactive', '--no-auth-cache', | |
| 434 '--username', self.USERS[0][0], '--password', self.USERS[0][1]]) | |
| 435 assert os.path.isdir(join(self.svn_checkout, '.svn')) | |
| 436 def file_system(rev, DEPS, DEPS_ALT=None): | |
| 437 fs = { | |
| 438 'origin': 'svn@%(rev)d\n', | |
| 439 'trunk/origin': 'svn/trunk@%(rev)d\n', | |
| 440 'trunk/src/origin': 'svn/trunk/src@%(rev)d\n', | |
| 441 'trunk/src/third_party/origin': 'svn/trunk/src/third_party@%(rev)d\n', | |
| 442 'trunk/other/origin': 'src/trunk/other@%(rev)d\n', | |
| 443 'trunk/third_party/origin': 'svn/trunk/third_party@%(rev)d\n', | |
| 444 'trunk/third_party/foo/origin': 'svn/trunk/third_party/foo@%(rev)d\n', | |
| 445 'trunk/third_party/prout/origin': 'svn/trunk/third_party/foo@%(rev)d\n', | |
| 446 } | |
| 447 for k in fs.iterkeys(): | |
| 448 fs[k] = fs[k] % { 'rev': rev } | |
| 449 fs['trunk/src/DEPS'] = DEPS | |
| 450 if DEPS_ALT: | |
| 451 fs['trunk/src/DEPS.alt'] = DEPS_ALT | |
| 452 return fs | |
| 453 | |
| 454 # Testing: | |
| 455 # - dependency disapear | |
| 456 # - dependency renamed | |
| 457 # - versioned and unversioned reference | |
| 458 # - relative and full reference | |
| 459 # - deps_os | |
| 460 # - var | |
| 461 # - hooks | |
| 462 # - From | |
| 463 # - File | |
| 464 # TODO(maruel): | |
| 465 # - $matching_files | |
| 466 # - use_relative_paths | |
| 467 DEPS = """ | |
| 468 vars = { | |
| 469 'DummyVariable': 'third_party', | |
| 470 } | |
| 471 deps = { | |
| 472 'src/other': '%(svn_base)strunk/other@1', | |
| 473 'src/third_party/fpp': '/trunk/' + Var('DummyVariable') + '/foo', | |
| 474 } | |
| 475 deps_os = { | |
| 476 'mac': { | |
| 477 'src/third_party/prout': '/trunk/third_party/prout', | |
| 478 }, | |
| 479 }""" % { 'svn_base': self.svn_base } | |
| 480 | |
| 481 DEPS_ALT = """ | |
| 482 deps = { | |
| 483 'src/other2': '%(svn_base)strunk/other@2' | |
| 484 } | |
| 485 """ % { 'svn_base': self.svn_base } | |
| 486 | |
| 487 fs = file_system(1, DEPS, DEPS_ALT) | |
| 488 self._commit_svn(fs) | |
| 489 | |
| 490 fs = file_system(2, """ | |
| 491 deps = { | |
| 492 'src/other': '%(svn_base)strunk/other', | |
| 493 # Load another DEPS and load a dependency from it. That's an example of | |
| 494 # WebKit's chromium checkout flow. Verify it works out of order. | |
| 495 'src/third_party/foo': From('src/file/other', 'foo/bar'), | |
| 496 'src/file/other': File('%(svn_base)strunk/other/DEPS'), | |
| 497 } | |
| 498 # I think this is wrong to have the hooks run from the base of the gclient | |
| 499 # checkout. It's maybe a bit too late to change that behavior. | |
| 500 hooks = [ | |
| 501 { | |
| 502 'pattern': '.', | |
| 503 'action': ['python', '-c', | |
| 504 'open(\\'src/svn_hooked1\\', \\'w\\').write(\\'svn_hooked1\\')'], | |
| 505 }, | |
| 506 { | |
| 507 # Should not be run. | |
| 508 'pattern': 'nonexistent', | |
| 509 'action': ['python', '-c', | |
| 510 'open(\\'src/svn_hooked2\\', \\'w\\').write(\\'svn_hooked2\\')'], | |
| 511 }, | |
| 512 ] | |
| 513 """ % { 'svn_base': self.svn_base }) | |
| 514 fs['trunk/other/DEPS'] = """ | |
| 515 deps = { | |
| 516 'foo/bar': '/trunk/third_party/foo@1', | |
| 517 # Only the requested deps should be processed. | |
| 518 'invalid': '/does_not_exist', | |
| 519 } | |
| 520 """ | |
| 521 # WebKit abuses this. | |
| 522 fs['trunk/webkit/.gclient'] = """ | |
| 523 solutions = [ | |
| 524 { | |
| 525 'name': './', | |
| 526 'url': None, | |
| 527 }, | |
| 528 ] | |
| 529 """ | |
| 530 fs['trunk/webkit/DEPS'] = """ | |
| 531 deps = { | |
| 532 'foo/bar': '%(svn_base)strunk/third_party/foo@1' | |
| 533 } | |
| 534 | |
| 535 hooks = [ | |
| 536 { | |
| 537 'pattern': '.*', | |
| 538 'action': ['echo', 'foo'], | |
| 539 }, | |
| 540 ] | |
| 541 """ % { 'svn_base': self.svn_base } | |
| 542 self._commit_svn(fs) | |
| 543 | |
| 544 def populateGit(self): | |
| 545 # Testing: | |
| 546 # - dependency disapear | |
| 547 # - dependency renamed | |
| 548 # - versioned and unversioned reference | |
| 549 # - relative and full reference | |
| 550 # - deps_os | |
| 551 # - var | |
| 552 # - hooks | |
| 553 # - From | |
| 554 # TODO(maruel): | |
| 555 # - File: File is hard to test here because it's SVN-only. It's | |
| 556 # implementation should probably be replaced to use urllib instead. | |
| 557 # - $matching_files | |
| 558 # - use_relative_paths | |
| 559 self._commit_git('repo_3', { | |
| 560 'origin': 'git/repo_3@1\n', | |
| 561 }) | |
| 562 | |
| 563 self._commit_git('repo_3', { | |
| 564 'origin': 'git/repo_3@2\n', | |
| 565 }) | |
| 566 | |
| 567 self._commit_git('repo_1', { | |
| 568 'DEPS': """ | |
| 569 vars = { | |
| 570 'DummyVariable': 'repo', | |
| 571 } | |
| 572 deps = { | |
| 573 'src/repo2': '%(git_base)srepo_2', | |
| 574 'src/repo2/repo3': '/' + Var('DummyVariable') + '_3@%(hash3)s', | |
| 575 } | |
| 576 deps_os = { | |
| 577 'mac': { | |
| 578 'src/repo4': '/repo_4', | |
| 579 }, | |
| 580 }""" % { | |
| 581 'git_base': self.git_base, | |
| 582 # See self.__init__() for the format. Grab's the hash of the first | |
| 583 # commit in repo_2. Only keep the first 7 character because of: | |
| 584 # TODO(maruel): http://crosbug.com/3591 We need to strip the hash.. | |
| 585 # duh. | |
| 586 'hash3': self.git_hashes['repo_3'][1][0][:7] | |
| 587 }, | |
| 588 'origin': 'git/repo_1@1\n', | |
| 589 }) | |
| 590 | |
| 591 self._commit_git('repo_2', { | |
| 592 'origin': 'git/repo_2@1\n', | |
| 593 'DEPS': """ | |
| 594 deps = { | |
| 595 'foo/bar': '/repo_3', | |
| 596 } | |
| 597 """, | |
| 598 }) | |
| 599 | |
| 600 self._commit_git('repo_2', { | |
| 601 'origin': 'git/repo_2@2\n', | |
| 602 }) | |
| 603 | |
| 604 self._commit_git('repo_4', { | |
| 605 'origin': 'git/repo_4@1\n', | |
| 606 }) | |
| 607 | |
| 608 self._commit_git('repo_4', { | |
| 609 'origin': 'git/repo_4@2\n', | |
| 610 }) | |
| 611 | |
| 612 self._commit_git('repo_1', { | |
| 613 'DEPS': """ | |
| 614 deps = { | |
| 615 'src/repo2': '%(git_base)srepo_2@%(hash)s', | |
| 616 #'src/repo2/repo_renamed': '/repo_3', | |
| 617 'src/repo2/repo_renamed': From('src/repo2', 'foo/bar'), | |
| 618 } | |
| 619 # I think this is wrong to have the hooks run from the base of the gclient | |
| 620 # checkout. It's maybe a bit too late to change that behavior. | |
| 621 hooks = [ | |
| 622 { | |
| 623 'pattern': '.', | |
| 624 'action': ['python', '-c', | |
| 625 'open(\\'src/git_hooked1\\', \\'w\\').write(\\'git_hooked1\\')'], | |
| 626 }, | |
| 627 { | |
| 628 # Should not be run. | |
| 629 'pattern': 'nonexistent', | |
| 630 'action': ['python', '-c', | |
| 631 'open(\\'src/git_hooked2\\', \\'w\\').write(\\'git_hooked2\\')'], | |
| 632 }, | |
| 633 ] | |
| 634 """ % { | |
| 635 'git_base': self.git_base, | |
| 636 # See self.__init__() for the format. Grab's the hash of the first | |
| 637 # commit in repo_2. Only keep the first 7 character because of: | |
| 638 # TODO(maruel): http://crosbug.com/3591 We need to strip the hash.. duh. | |
| 639 'hash': self.git_hashes['repo_2'][1][0][:7] | |
| 640 }, | |
| 641 'origin': 'git/repo_1@2\n', | |
| 642 }) | |
| 643 | |
| 644 | |
| 645 class FakeReposTestBase(trial_dir.TestCase): | |
| 646 """This is vaguely inspired by twisted.""" | |
| 647 # static FakeRepos instance. Lazy loaded. | |
| 648 FAKE_REPOS = None | |
| 649 # Override if necessary. | |
| 650 FAKE_REPOS_CLASS = FakeRepos | |
| 651 | |
| 652 def setUp(self): | |
| 653 super(FakeReposTestBase, self).setUp() | |
| 654 if not FakeReposTestBase.FAKE_REPOS: | |
| 655 # Lazy create the global instance. | |
| 656 FakeReposTestBase.FAKE_REPOS = self.FAKE_REPOS_CLASS() | |
| 657 # No need to call self.FAKE_REPOS.setUp(), it will be called by the child | |
| 658 # class. | |
| 659 # Do not define tearDown(), since super's version does the right thing and | |
| 660 # FAKE_REPOS is kept across tests. | |
| 661 | |
| 662 @property | |
| 663 def svn_base(self): | |
| 664 """Shortcut.""" | |
| 665 return self.FAKE_REPOS.svn_base | |
| 666 | |
| 667 @property | |
| 668 def git_base(self): | |
| 669 """Shortcut.""" | |
| 670 return self.FAKE_REPOS.git_base | |
| 671 | |
| 672 def checkString(self, expected, result, msg=None): | |
| 673 """Prints the diffs to ease debugging.""" | |
| 674 if expected != result: | |
| 675 # Strip the begining | |
| 676 while expected and result and expected[0] == result[0]: | |
| 677 expected = expected[1:] | |
| 678 result = result[1:] | |
| 679 # The exception trace makes it hard to read so dump it too. | |
| 680 if '\n' in result: | |
| 681 print result | |
| 682 self.assertEquals(expected, result, msg) | |
| 683 | |
| 684 def check(self, expected, results): | |
| 685 """Checks stdout, stderr, returncode.""" | |
| 686 self.checkString(expected[0], results[0]) | |
| 687 self.checkString(expected[1], results[1]) | |
| 688 self.assertEquals(expected[2], results[2]) | |
| 689 | |
| 690 def assertTree(self, tree, tree_root=None): | |
| 691 """Diff the checkout tree with a dict.""" | |
| 692 if not tree_root: | |
| 693 tree_root = self.root_dir | |
| 694 actual = read_tree(tree_root) | |
| 695 diff = dict_diff(tree, actual) | |
| 696 if diff: | |
| 697 logging.debug('Actual %s\n%s' % (tree_root, pprint.pformat(actual))) | |
| 698 logging.debug('Expected\n%s' % pprint.pformat(tree)) | |
| 699 logging.debug('Diff\n%s' % pprint.pformat(diff)) | |
| 700 self.assertEquals(diff, []) | |
| 701 | |
| 702 def mangle_svn_tree(self, *args): | |
| 703 """Creates a 'virtual directory snapshot' to compare with the actual result | |
| 704 on disk.""" | |
| 705 result = {} | |
| 706 for item, new_root in args: | |
| 707 old_root, rev = item.split('@', 1) | |
| 708 tree = self.FAKE_REPOS.svn_revs[int(rev)] | |
| 709 for k, v in tree.iteritems(): | |
| 710 if not k.startswith(old_root): | |
| 711 continue | |
| 712 item = k[len(old_root) + 1:] | |
| 713 if item.startswith('.'): | |
| 714 continue | |
| 715 result[join(new_root, item).replace(os.sep, '/')] = v | |
| 716 return result | |
| 717 | |
| 718 def mangle_git_tree(self, *args): | |
| 719 """Creates a 'virtual directory snapshot' to compare with the actual result | |
| 720 on disk.""" | |
| 721 result = {} | |
| 722 for item, new_root in args: | |
| 723 repo, rev = item.split('@', 1) | |
| 724 tree = self.gittree(repo, rev) | |
| 725 for k, v in tree.iteritems(): | |
| 726 result[join(new_root, k)] = v | |
| 727 return result | |
| 728 | |
| 729 def githash(self, repo, rev): | |
| 730 """Sort-hand: Returns the hash for a git 'revision'.""" | |
| 731 return self.FAKE_REPOS.git_hashes[repo][int(rev)][0] | |
| 732 | |
| 733 def gittree(self, repo, rev): | |
| 734 """Sort-hand: returns the directory tree for a git 'revision'.""" | |
| 735 return self.FAKE_REPOS.git_hashes[repo][int(rev)][1] | |
| 736 | |
| 737 | |
| 738 def main(argv): | |
| 739 fake = FakeRepos() | |
| 740 print 'Using %s' % fake.root_dir | |
| 741 try: | |
| 742 fake.set_up_svn() | |
| 743 fake.set_up_git() | |
| 744 print('Fake setup, press enter to quit or Ctrl-C to keep the checkouts.') | |
| 745 sys.stdin.readline() | |
| 746 except KeyboardInterrupt: | |
| 747 trial_dir.TrialDir.SHOULD_LEAK.leak = True | |
| 748 return 0 | |
| 749 | |
| 750 | |
| 751 if __name__ == '__main__': | |
| 752 sys.exit(main(sys.argv)) | |
| OLD | NEW |