fake_repos.py 22.4 KB
Newer Older
1 2
#!/usr/bin/env python
# Copyright (c) 2011 The Chromium Authors. All rights reserved.
3 4 5 6 7
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Generate fake repositories for testing."""

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 testing_support import trial_dir
22
import gclient_utils
23
import scm
24
import subprocess2
25 26 27 28 29 30 31 32


def write(path, content):
  f = open(path, 'wb')
  f.write(content)
  f.close()


33 34 35
join = os.path.join


36 37 38 39 40 41 42
def read_tree(tree_root):
  """Returns a dict of all the files in a tree. Defaults to self.root_dir."""
  tree = {}
  for root, dirs, files in os.walk(tree_root):
    for d in filter(lambda x: x.startswith('.'), dirs):
      dirs.remove(d)
    for f in [join(root, f) for f in files if not f.startswith('.')]:
43 44 45
      filepath = f[len(tree_root) + 1:].replace(os.sep, '/')
      assert len(filepath), f
      tree[filepath] = open(join(root, f), 'rU').read()
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
  return tree


def dict_diff(dict1, dict2):
  diff = {}
  for k, v in dict1.iteritems():
    if k not in dict2:
      diff[k] = v
    elif v != dict2[k]:
      diff[k] = (v, dict2[k])
  for k, v in dict2.iteritems():
    if k not in dict1:
      diff[k] = v
  return diff


62 63
def commit_git(repo):
  """Commits the changes and returns the new hash."""
64 65 66 67
  subprocess2.check_call(['git', 'add', '-A', '-f'], cwd=repo)
  subprocess2.check_call(['git', 'commit', '-q', '--message', 'foo'], cwd=repo)
  rev = subprocess2.check_output(
      ['git', 'show-ref', '--head', 'HEAD'], cwd=repo).split(' ', 1)[0]
68 69 70 71
  logging.debug('At revision %s' % rev)
  return rev


72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
def test_port(host, port):
  s = socket.socket()
  try:
    return s.connect_ex((host, port)) == 0
  finally:
    s.close()


def find_free_port(host, base_port):
  """Finds a listening port free to listen to."""
  while base_port < (2<<16):
    if not test_port(host, base_port):
      return base_port
    base_port += 1
  assert False, 'Having issues finding an available port'


def wait_for_port_to_bind(host, port, process):
  sock = socket.socket()

  if sys.platform == 'darwin':
    # On Mac SnowLeopard, if we attempt to connect to the socket
    # immediately, it fails with EINVAL and never gets a chance to
    # connect (putting us into a hard spin and then failing).
    # Linux doesn't need this.
97
    time.sleep(0.2)
98 99 100 101 102 103 104 105 106

  try:
    start = datetime.datetime.utcnow()
    maxdelay = datetime.timedelta(seconds=30)
    while (datetime.datetime.utcnow() - start) < maxdelay:
      try:
        sock.connect((host, port))
        logging.debug('%d is now bound' % port)
        return
107
      except (socket.error, EnvironmentError):
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
        pass
      logging.debug('%d is still not bound' % port)
  finally:
    sock.close()
  # The process failed to bind. Kill it and dump its ouput.
  process.kill()
  logging.error('%s' % process.communicate()[0])
  assert False, '%d is still not bound' % port


def wait_for_port_to_free(host, port):
  start = datetime.datetime.utcnow()
  maxdelay = datetime.timedelta(seconds=30)
  while (datetime.datetime.utcnow() - start) < maxdelay:
    try:
      sock = socket.socket()
      sock.connect((host, port))
      logging.debug('%d was bound, waiting to free' % port)
126
    except (socket.error, EnvironmentError):
127 128 129 130 131 132 133
      logging.debug('%d now free' % port)
      return
    finally:
      sock.close()
  assert False, '%d is still bound' % port


134
class FakeReposBase(object):
135
  """Generate git repositories to test gclient functionality.
136

137
  Many DEPS functionalities need to be tested: Var, deps_os, hooks,
138 139
  use_relative_paths.

140
  And types of dependencies: Relative urls, Full urls, git.
141

142
  populateGit() needs to be implemented by the subclass.
143
  """
144
  # Hostname
145
  NB_GIT_REPOS = 1
146 147 148 149
  USERS = [
      ('user1@example.com', 'foo'),
      ('user2@example.com', 'bar'),
  ]
150

151 152 153
  def __init__(self, host=None):
    self.trial = trial_dir.TrialDir('repos')
    self.host = host or '127.0.0.1'
154 155 156
    # Format is { repo: [ None, (hash, tree), (hash, tree), ... ], ... }
    # so reference looks like self.git_hashes[repo][rev][0] for hash and
    # self.git_hashes[repo][rev][1] for it's tree snapshot.
157
    # It is 1-based too.
158
    self.git_hashes = {}
159
    self.gitdaemon = None
160
    self.git_pid_file = None
161 162
    self.git_root = None
    self.git_dirty = False
163 164
    self.git_port = None
    self.git_base = None
165

166 167 168
  @property
  def root_dir(self):
    return self.trial.root_dir
169

170
  def set_up(self):
171
    """All late initialization comes here."""
172
    self.cleanup_dirt()
173 174 175 176 177 178 179 180
    if not self.root_dir:
      try:
        # self.root_dir is not set before this call.
        self.trial.set_up()
        self.git_root = join(self.root_dir, 'git')
      finally:
        # Registers cleanup.
        atexit.register(self.tear_down)
181

182
  def cleanup_dirt(self):
183
    """For each dirty repository, destroy it."""
184 185
    if self.git_dirty:
      if not self.tear_down_git():
186
        logging.error('Using both leaking checkout and git dirty checkout')
187 188

  def tear_down(self):
189
    """Kills the servers and delete the directories."""
190
    self.tear_down_git()
191 192 193
    # This deletes the directories.
    self.trial.tear_down()
    self.trial = None
194 195 196

  def tear_down_git(self):
    if self.gitdaemon:
197 198 199
      logging.debug('Killing git-daemon pid %s' % self.gitdaemon.pid)
      self.gitdaemon.kill()
      self.gitdaemon = None
200 201 202
      if self.git_pid_file:
        pid = int(self.git_pid_file.read())
        self.git_pid_file.close()
203
        logging.debug('Killing git daemon pid %s' % pid)
204 205 206 207 208
        try:
          subprocess2.kill_pid(pid)
        except OSError as e:
          if e.errno != errno.ESRCH:  # no such process
            raise
209
        self.git_pid_file = None
210 211 212
      wait_for_port_to_free(self.host, self.git_port)
      self.git_port = None
      self.git_base = None
213
      if not self.trial.SHOULD_LEAK:
214
        logging.debug('Removing %s' % self.git_root)
215
        gclient_utils.rmtree(self.git_root)
216
      else:
217 218
        return False
    return True
219

220 221
  @staticmethod
  def _genTree(root, tree_dict):
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
    """For a dictionary of file contents, generate a filesystem."""
    if not os.path.isdir(root):
      os.makedirs(root)
    for (k, v) in tree_dict.iteritems():
      k_os = k.replace('/', os.sep)
      k_arr = k_os.split(os.sep)
      if len(k_arr) > 1:
        p = os.sep.join([root] + k_arr[:-1])
        if not os.path.isdir(p):
          os.makedirs(p)
      if v is None:
        os.remove(join(root, k))
      else:
        write(join(root, k), v)

237
  def set_up_git(self):
238
    """Creates git repositories and start the servers."""
239
    self.set_up()
240 241
    if self.gitdaemon:
      return True
242
    assert self.git_pid_file == None
243 244 245 246
    try:
      subprocess2.check_output(['git', '--version'])
    except (OSError, subprocess2.CalledProcessError):
      return False
247
    for repo in ['repo_%d' % r for r in range(1, self.NB_GIT_REPOS + 1)]:
248
      subprocess2.check_call(['git', 'init', '-q', join(self.git_root, repo)])
249
      self.git_hashes[repo] = [None]
250 251
    self.git_port = find_free_port(self.host, 20000)
    self.git_base = 'git://%s:%d/git/' % (self.host, self.git_port)
252
    # Start the daemon.
253 254 255 256
    self.git_pid_file = tempfile.NamedTemporaryFile()
    cmd = ['git', 'daemon',
        '--export-all',
        '--reuseaddr',
257
        '--base-path=' + self.root_dir,
258 259
        '--pid-file=' + self.git_pid_file.name,
        '--port=%d' % self.git_port]
260 261
    if self.host == '127.0.0.1':
      cmd.append('--listen=' + self.host)
262
    self.check_port_is_free(self.git_port)
263 264 265 266 267
    self.gitdaemon = subprocess2.Popen(
        cmd,
        cwd=self.root_dir,
        stdout=subprocess2.PIPE,
        stderr=subprocess2.PIPE)
268 269
    wait_for_port_to_bind(self.host, self.git_port, self.gitdaemon)
    self.populateGit()
270
    self.git_dirty = False
271 272 273 274 275 276 277 278 279 280 281 282 283
    return True

  def _commit_git(self, repo, tree):
    repo_root = join(self.git_root, repo)
    self._genTree(repo_root, tree)
    commit_hash = commit_git(repo_root)
    if self.git_hashes[repo][-1]:
      new_tree = self.git_hashes[repo][-1][1].copy()
      new_tree.update(tree)
    else:
      new_tree = tree.copy()
    self.git_hashes[repo].append((commit_hash, new_tree))

284 285 286 287 288 289
  def _fast_import_git(self, repo, data):
    repo_root = join(self.git_root, repo)
    logging.debug('%s: fast-import %s', repo, data)
    subprocess2.check_call(
        ['git', 'fast-import', '--quiet'], cwd=repo_root, stdin=data)

290 291 292
  def check_port_is_free(self, port):
    sock = socket.socket()
    try:
293
      sock.connect((self.host, port))
294 295
      # It worked, throw.
      assert False, '%d shouldn\'t be bound' % port
296
    except (socket.error, EnvironmentError):
297 298 299 300
      pass
    finally:
      sock.close()

301 302 303
  def populateGit(self):
    raise NotImplementedError()

304 305

class FakeRepos(FakeReposBase):
306
  """Implements populateGit()."""
307
  NB_GIT_REPOS = 13
308 309

  def populateGit(self):
310
    # Testing:
311
    # - dependency disappear
312 313 314 315 316 317
    # - dependency renamed
    # - versioned and unversioned reference
    # - relative and full reference
    # - deps_os
    # - var
    # - hooks
318
    # TODO(maruel):
319
    # - use_relative_paths
320 321 322 323 324 325 326 327
    self._commit_git('repo_3', {
      'origin': 'git/repo_3@1\n',
    })

    self._commit_git('repo_3', {
      'origin': 'git/repo_3@2\n',
    })

328 329
    self._commit_git('repo_1', {
      'DEPS': """
330 331
vars = {
  'DummyVariable': 'repo',
332 333 334 335 336 337
  'false_var': False,
  'false_str_var': 'False',
  'true_var': True,
  'true_str_var': 'True',
  'str_var': 'abc',
  'cond_var': 'false_str_var and true_var',
338
}
339 340 341
# Nest the args file in a sub-repo, to make sure we don't try to
# write it before we've cloned everything.
gclient_gn_args_file = 'src/repo2/gclient.args'
342 343
gclient_gn_args = [
  'false_var',
344
  'false_str_var',
345
  'true_var',
346
  'true_str_var',
347
  'str_var',
348
  'cond_var',
349
]
350
deps = {
351 352 353 354
  'src/repo2': {
    'url': '%(git_base)srepo_2',
    'condition': 'True',
  },
355
  'src/repo2/repo3': '/' + Var('DummyVariable') + '_3@%(hash3)s',
356 357 358 359 360
  # Test that deps where condition evaluates to False are skipped.
  'src/repo5': {
    'url': '/repo_5',
    'condition': 'False',
  },
361 362
}
deps_os = {
363 364 365
  'mac': {
    'src/repo4': '/repo_4',
  },
366
}""" % {
367
            'git_base': self.git_base,
368 369 370 371 372 373 374
            # See self.__init__() for the format. Grab's the hash of the first
            # commit in repo_2. Only keep the first 7 character because of:
            # TODO(maruel): http://crosbug.com/3591 We need to strip the hash..
            # duh.
            'hash3': self.git_hashes['repo_3'][1][0][:7]
        },
        'origin': 'git/repo_1@1\n',
375 376 377
    })

    self._commit_git('repo_2', {
378 379 380 381 382 383
      'origin': 'git/repo_2@1\n',
      'DEPS': """
deps = {
  'foo/bar': '/repo_3',
}
""",
384 385 386
    })

    self._commit_git('repo_2', {
387
      'origin': 'git/repo_2@2\n',
388 389 390
    })

    self._commit_git('repo_4', {
391
      'origin': 'git/repo_4@1\n',
392 393 394
    })

    self._commit_git('repo_4', {
395
      'origin': 'git/repo_4@2\n',
396 397 398 399 400
    })

    self._commit_git('repo_1', {
      'DEPS': """
deps = {
401
  'src/repo2': '%(git_base)srepo_2@%(hash)s',
402
  'src/repo2/repo_renamed': '/repo_3',
403
}
404 405 406 407 408 409
# I think this is wrong to have the hooks run from the base of the gclient
# checkout. It's maybe a bit too late to change that behavior.
hooks = [
  {
    'pattern': '.',
    'action': ['python', '-c',
410
               'open(\\'src/git_hooked1\\', \\'w\\').write(\\'git_hooked1\\')'],
411 412 413 414 415
  },
  {
    # Should not be run.
    'pattern': 'nonexistent',
    'action': ['python', '-c',
416
               'open(\\'src/git_hooked2\\', \\'w\\').write(\\'git_hooked2\\')'],
417 418 419
  },
]
""" % {
420
        'git_base': self.git_base,
421 422 423 424
        # See self.__init__() for the format. Grab's the hash of the first
        # commit in repo_2. Only keep the first 7 character because of:
        # TODO(maruel): http://crosbug.com/3591 We need to strip the hash.. duh.
        'hash': self.git_hashes['repo_2'][1][0][:7]
425
      },
426
      'origin': 'git/repo_1@2\n',
427
    })
428

429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
    self._commit_git('repo_5', {'origin': 'git/repo_5@1\n'})
    self._commit_git('repo_5', {
      'DEPS': """
deps = {
  'src/repo1': '%(git_base)srepo_1@%(hash1)s',
  'src/repo2': '%(git_base)srepo_2@%(hash2)s',
}

# Hooks to run after a project is processed but before its dependencies are
# processed.
pre_deps_hooks = [
  {
    'action': ['python', '-c',
               'print "pre-deps hook"; open(\\'src/git_pre_deps_hooked\\', \\'w\\').write(\\'git_pre_deps_hooked\\')'],
  }
]
""" % {
         'git_base': self.git_base,
         'hash1': self.git_hashes['repo_1'][2][0][:7],
         'hash2': self.git_hashes['repo_2'][1][0][:7],
      },
    'origin': 'git/repo_5@2\n',
    })
    self._commit_git('repo_5', {
      'DEPS': """
deps = {
  'src/repo1': '%(git_base)srepo_1@%(hash1)s',
  'src/repo2': '%(git_base)srepo_2@%(hash2)s',
}

# Hooks to run after a project is processed but before its dependencies are
# processed.
pre_deps_hooks = [
  {
    'action': ['python', '-c',
               'print "pre-deps hook"; open(\\'src/git_pre_deps_hooked\\', \\'w\\').write(\\'git_pre_deps_hooked\\')'],
  },
  {
    'action': ['python', '-c', 'import sys; sys.exit(1)'],
  }
]
""" % {
         'git_base': self.git_base,
         'hash1': self.git_hashes['repo_1'][2][0][:7],
         'hash2': self.git_hashes['repo_2'][1][0][:7],
      },
    'origin': 'git/repo_5@3\n',
    })

478 479
    self._commit_git('repo_6', {
      'DEPS': """
480 481
vars = {
  'DummyVariable': 'repo',
482 483
  'git_base': '%(git_base)s',
  'hook1_contents': 'git_hooked1',
484
  'repo5_var': '/repo_5',
485

486 487 488 489 490 491
  'false_var': False,
  'false_str_var': 'False',
  'true_var': True,
  'true_str_var': 'True',
  'str_var': 'abc',
  'cond_var': 'false_str_var and true_var',
492
}
493

494
gclient_gn_args_file = 'src/repo2/gclient.args'
495 496
gclient_gn_args = [
  'false_var',
497
  'false_str_var',
498
  'true_var',
499
  'true_str_var',
500
  'str_var',
501
  'cond_var',
502 503
]

504 505 506
allowed_hosts = [
  '%(git_base)s',
]
507
deps = {
508
  'src/repo2': {
509
    'url': Var('git_base') + 'repo_2@%(hash)s',
510 511 512 513 514
    'condition': 'True',
  },
  'src/repo4': {
    'url': '/repo_4',
    'condition': 'False',
515
  },
516
  'src/repo8': '/repo_8',
517
}
518 519
deps_os ={
  'mac': {
520
    # This entry should not appear in flattened DEPS' |deps|.
521
    'src/mac_repo': '{repo5_var}',
522 523
  },
  'unix': {
524
    # This entry should not appear in flattened DEPS' |deps|.
525
    'src/unix_repo': '{repo5_var}',
526 527
  },
  'win': {
528
    # This entry should not appear in flattened DEPS' |deps|.
529
    'src/win_repo': '{repo5_var}',
530 531
  },
}
532 533 534
hooks = [
  {
    'pattern': '.',
535
    'condition': 'True',
536
    'action': ['python', '-c',
537
               'open(\\'src/git_hooked1\\', \\'w\\').write(\\'{hook1_contents}\\')'],
538 539 540 541 542 543 544 545
  },
  {
    # Should not be run.
    'pattern': 'nonexistent',
    'action': ['python', '-c',
               'open(\\'src/git_hooked2\\', \\'w\\').write(\\'git_hooked2\\')'],
  },
]
546 547 548 549 550 551 552 553 554 555
hooks_os = {
  'mac': [
    {
      'pattern': '.',
      'action': ['python', '-c',
                 'open(\\'src/git_hooked_mac\\', \\'w\\').write('
                     '\\'git_hooked_mac\\')'],
    },
  ],
}
556 557
recursedeps = [
  'src/repo2',
558
  'src/repo8',
559 560 561 562 563 564 565
]""" % {
        'git_base': self.git_base,
        'hash': self.git_hashes['repo_2'][1][0][:7]
      },
      'origin': 'git/repo_6@1\n',
    })

566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
    self._commit_git('repo_7', {
      'DEPS': """
vars = {
  'true_var': 'True',
  'false_var': 'true_var and False',
}
hooks = [
  {
    'action': ['python', '-c',
               'open(\\'src/should_run\\', \\'w\\').write(\\'should_run\\')'],
    'condition': 'true_var or True',
  },
  {
    'action': ['python', '-c',
               'open(\\'src/should_not_run\\', \\'w\\').write(\\'should_not_run\\')'],
    'condition': 'false_var',
  },
]""",
      'origin': 'git/repo_7@1\n',
    })

587 588 589 590 591 592 593 594 595 596 597 598 599
    self._commit_git('repo_8', {
      'DEPS': """
deps_os ={
  'mac': {
    'src/recursed_os_repo': '/repo_5',
  },
  'unix': {
    'src/recursed_os_repo': '/repo_5',
  },
}""",
      'origin': 'git/repo_8@1\n',
    })

600 601 602 603
    self._commit_git('repo_9', {
      'DEPS': """
deps = {
  'src/repo8': '/repo_8',
604 605 606 607 608

  # This entry should appear in flattened file,
  # but not recursed into, since it's not
  # in recursedeps.
  'src/repo7': '/repo_7',
609
}
610 611 612 613 614 615 616
deps_os = {
  'android': {
    # This entry should only appear in flattened |deps_os|,
    # not |deps|, even when used with |recursedeps|.
    'src/repo4': '/repo_4',
  }
}
617
recursedeps = [
618
  'src/repo4',
619 620 621 622 623 624 625 626 627
  'src/repo8',
]""",
      'origin': 'git/repo_9@1\n',
    })

    self._commit_git('repo_10', {
      'DEPS': """
deps = {
  'src/repo9': '/repo_9',
628 629 630 631 632

  # This entry should appear in flattened file,
  # but not recursed into, since it's not
  # in recursedeps.
  'src/repo6': '/repo_6',
633
}
634
deps_os = {
635 636 637
  'mac': {
    'src/repo11': '/repo_11',
  },
638 639 640 641
  'ios': {
    'src/repo11': '/repo_11',
  }
}
642 643
recursedeps = [
  'src/repo9',
644
  'src/repo11',
645 646 647 648
]""",
      'origin': 'git/repo_10@1\n',
    })

649 650 651
    self._commit_git('repo_11', {
      'DEPS': """
deps = {
652
  'src/repo12': '/repo_12',
653 654 655 656 657 658 659 660
}""",
      'origin': 'git/repo_11@1\n',
    })

    self._commit_git('repo_12', {
      'origin': 'git/repo_12@1\n',
    })

661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697
    self._fast_import_git('repo_12', """blob
mark :1
data 6
Hello

blob
mark :2
data 4
Bye

reset refs/changes/1212
commit refs/changes/1212
mark :3
author Bob <bob@example.com> 1253744361 -0700
committer Bob <bob@example.com> 1253744361 -0700
data 8
A and B
M 100644 :1 a
M 100644 :2 b
""")

    self._commit_git('repo_13', {
      'DEPS': """
deps = {
  'src/repo12': '/repo_12',
}""",
      'origin': 'git/repo_13@1\n',
    })

    self._commit_git('repo_13', {
      'DEPS': """
deps = {
  'src/repo12': '/repo_12@refs/changes/1212',
}""",
      'origin': 'git/repo_13@2\n',
    })

698

699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734
class FakeRepoSkiaDEPS(FakeReposBase):
  """Simulates the Skia DEPS transition in Chrome."""

  NB_GIT_REPOS = 5

  DEPS_git_pre = """deps = {
  'src/third_party/skia/gyp': '%(git_base)srepo_3',
  'src/third_party/skia/include': '%(git_base)srepo_4',
  'src/third_party/skia/src': '%(git_base)srepo_5',
}"""

  DEPS_post = """deps = {
  'src/third_party/skia': '%(git_base)srepo_1',
}"""

  def populateGit(self):
    # Skia repo.
    self._commit_git('repo_1', {
        'skia_base_file': 'root-level file.',
        'gyp/gyp_file': 'file in the gyp directory',
        'include/include_file': 'file in the include directory',
        'src/src_file': 'file in the src directory',
    })
    self._commit_git('repo_3', { # skia/gyp
        'gyp_file': 'file in the gyp directory',
    })
    self._commit_git('repo_4', { # skia/include
        'include_file': 'file in the include directory',
    })
    self._commit_git('repo_5', { # skia/src
        'src_file': 'file in the src directory',
    })

    # Chrome repo.
    self._commit_git('repo_2', {
        'DEPS': self.DEPS_git_pre % {'git_base': self.git_base},
735
        'myfile': 'src/trunk/src@1'
736 737 738
    })
    self._commit_git('repo_2', {
        'DEPS': self.DEPS_post % {'git_base': self.git_base},
739
        'myfile': 'src/trunk/src@2'
740 741 742
    })


743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776
class FakeRepoBlinkDEPS(FakeReposBase):
  """Simulates the Blink DEPS transition in Chrome."""

  NB_GIT_REPOS = 2
  DEPS_pre = 'deps = {"src/third_party/WebKit": "%(git_base)srepo_2",}'
  DEPS_post = 'deps = {}'

  def populateGit(self):
    # Blink repo.
    self._commit_git('repo_2', {
        'OWNERS': 'OWNERS-pre',
        'Source/exists_always': '_ignored_',
        'Source/exists_before_but_not_after': '_ignored_',
    })

    # Chrome repo.
    self._commit_git('repo_1', {
        'DEPS': self.DEPS_pre % {'git_base': self.git_base},
        'myfile': 'myfile@1',
        '.gitignore': '/third_party/WebKit',
    })
    self._commit_git('repo_1', {
        'DEPS': self.DEPS_post % {'git_base': self.git_base},
        'myfile': 'myfile@2',
        '.gitignore': '',
        'third_party/WebKit/OWNERS': 'OWNERS-post',
        'third_party/WebKit/Source/exists_always': '_ignored_',
        'third_party/WebKit/Source/exists_after_but_not_before': '_ignored',
    })

  def populateSvn(self):
    raise NotImplementedError()


777
class FakeReposTestBase(trial_dir.TestCase):
778
  """This is vaguely inspired by twisted."""
779 780
  # Static FakeRepos instances. Lazy loaded.
  CACHED_FAKE_REPOS = {}
781 782
  # Override if necessary.
  FAKE_REPOS_CLASS = FakeRepos
783

784 785
  def setUp(self):
    super(FakeReposTestBase, self).setUp()
786 787 788
    if not self.FAKE_REPOS_CLASS in self.CACHED_FAKE_REPOS:
      self.CACHED_FAKE_REPOS[self.FAKE_REPOS_CLASS] = self.FAKE_REPOS_CLASS()
    self.FAKE_REPOS = self.CACHED_FAKE_REPOS[self.FAKE_REPOS_CLASS]
789 790 791
    # No need to call self.FAKE_REPOS.setUp(), it will be called by the child
    # class.
    # Do not define tearDown(), since super's version does the right thing and
792
    # self.FAKE_REPOS is kept across tests.
793

794 795 796 797 798
  @property
  def git_base(self):
    """Shortcut."""
    return self.FAKE_REPOS.git_base

799
  def checkString(self, expected, result, msg=None):
800 801 802 803 804 805 806 807 808
    """Prints the diffs to ease debugging."""
    if expected != result:
      # Strip the begining
      while expected and result and expected[0] == result[0]:
        expected = expected[1:]
        result = result[1:]
      # The exception trace makes it hard to read so dump it too.
      if '\n' in result:
        print result
809
    self.assertEquals(expected, result, msg)
810 811

  def check(self, expected, results):
812
    """Checks stdout, stderr, returncode."""
813 814 815 816 817 818 819 820 821 822 823 824 825 826
    self.checkString(expected[0], results[0])
    self.checkString(expected[1], results[1])
    self.assertEquals(expected[2], results[2])

  def assertTree(self, tree, tree_root=None):
    """Diff the checkout tree with a dict."""
    if not tree_root:
      tree_root = self.root_dir
    actual = read_tree(tree_root)
    diff = dict_diff(tree, actual)
    if diff:
      logging.debug('Actual %s\n%s' % (tree_root, pprint.pformat(actual)))
      logging.debug('Expected\n%s' % pprint.pformat(tree))
      logging.debug('Diff\n%s' % pprint.pformat(diff))
827
    self.assertEquals(diff, {})
828

829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847
  def mangle_git_tree(self, *args):
    """Creates a 'virtual directory snapshot' to compare with the actual result
    on disk."""
    result = {}
    for item, new_root in args:
      repo, rev = item.split('@', 1)
      tree = self.gittree(repo, rev)
      for k, v in tree.iteritems():
        result[join(new_root, k)] = v
    return result

  def githash(self, repo, rev):
    """Sort-hand: Returns the hash for a git 'revision'."""
    return self.FAKE_REPOS.git_hashes[repo][int(rev)][0]

  def gittree(self, repo, rev):
    """Sort-hand: returns the directory tree for a git 'revision'."""
    return self.FAKE_REPOS.git_hashes[repo][int(rev)][1]

848

849
def main(argv):
850
  fake = FakeRepos()
851
  print 'Using %s' % fake.root_dir
852
  try:
853
    fake.set_up_git()
854
    print('Fake setup, press enter to quit or Ctrl-C to keep the checkouts.')
855
    sys.stdin.readline()
856
  except KeyboardInterrupt:
857
    trial_dir.TrialDir.SHOULD_LEAK.leak = True
858 859 860 861 862
  return 0


if __name__ == '__main__':
  sys.exit(main(sys.argv))