fake_repos.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # Copyright (c) 2011 The Chromium Authors. All rights reserved.
  4. # Use of this source code is governed by a BSD-style license that can be
  5. # found in the LICENSE file.
  6. """Generate fake repositories for testing."""
  7. from __future__ import print_function
  8. import atexit
  9. import datetime
  10. import errno
  11. import logging
  12. import os
  13. import pprint
  14. import random
  15. import re
  16. import socket
  17. import sys
  18. import tempfile
  19. import textwrap
  20. import time
  21. # trial_dir must be first for non-system libraries.
  22. from testing_support import trial_dir
  23. import gclient_utils
  24. import scm
  25. import subprocess2
  26. # Attempt |MAX_TRY| times to find a free port. Each time select one port at
  27. # random from the range [|PORT_START|, |PORT_END|].
  28. MAX_TRY = 10
  29. PORT_START = 20000
  30. PORT_END = 65535
  31. def write(path, content):
  32. f = open(path, 'wb')
  33. f.write(content.encode())
  34. f.close()
  35. join = os.path.join
  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] = gclient_utils.FileRead(join(root, f))
  46. return tree
  47. def dict_diff(dict1, dict2):
  48. diff = {}
  49. for k, v in dict1.items():
  50. if k not in dict2:
  51. diff[k] = v
  52. elif v != dict2[k]:
  53. diff[k] = (v, dict2[k])
  54. for k, v in dict2.items():
  55. if k not in dict1:
  56. diff[k] = v
  57. return diff
  58. def commit_git(repo):
  59. """Commits the changes and returns the new hash."""
  60. subprocess2.check_call(['git', 'add', '-A', '-f'], cwd=repo)
  61. subprocess2.check_call(['git', 'commit', '-q', '--message', 'foo'], cwd=repo)
  62. rev = subprocess2.check_output(
  63. ['git', 'show-ref', '--head', 'HEAD'], cwd=repo).split(b' ', 1)[0]
  64. rev = rev.decode('utf-8')
  65. logging.debug('At revision %s' % rev)
  66. return rev
  67. def port_is_free(host, port):
  68. s = socket.socket()
  69. try:
  70. return s.connect_ex((host, port)) != 0
  71. finally:
  72. s.close()
  73. def find_free_port(host):
  74. """Finds a listening port free to listen to."""
  75. for _ in range(MAX_TRY):
  76. base_port = random.randint(PORT_START, PORT_END)
  77. if port_is_free(host, base_port):
  78. return base_port
  79. assert False, 'Having issues finding an available port'
  80. def wait_for_port_to_bind(host, port, process):
  81. try:
  82. start = datetime.datetime.utcnow()
  83. maxdelay = datetime.timedelta(seconds=30)
  84. while (datetime.datetime.utcnow() - start) < maxdelay:
  85. sock = socket.socket()
  86. try:
  87. sock.connect((host, port))
  88. logging.debug('%d is now bound' % port)
  89. return
  90. except (socket.error, EnvironmentError):
  91. # Sleep a little bit to avoid spinning too much.
  92. time.sleep(0.2)
  93. logging.debug('%d is still not bound' % port)
  94. finally:
  95. sock.close()
  96. # The process failed to bind. Kill it and dump its ouput.
  97. process.kill()
  98. stdout, stderr = process.communicate()
  99. logging.debug('%s' % stdout)
  100. logging.error('%s' % stderr)
  101. assert False, '%d is still not bound' % port
  102. def wait_for_port_to_free(host, port):
  103. start = datetime.datetime.utcnow()
  104. maxdelay = datetime.timedelta(seconds=30)
  105. while (datetime.datetime.utcnow() - start) < maxdelay:
  106. try:
  107. sock = socket.socket()
  108. sock.connect((host, port))
  109. logging.debug('%d was bound, waiting to free' % port)
  110. except (socket.error, EnvironmentError):
  111. logging.debug('%d now free' % port)
  112. return
  113. finally:
  114. sock.close()
  115. assert False, '%d is still bound' % port
  116. class FakeReposBase(object):
  117. """Generate git repositories to test gclient functionality.
  118. Many DEPS functionalities need to be tested: Var, deps_os, hooks,
  119. use_relative_paths.
  120. And types of dependencies: Relative urls, Full urls, git.
  121. populateGit() needs to be implemented by the subclass.
  122. """
  123. # Hostname
  124. NB_GIT_REPOS = 1
  125. USERS = [
  126. ('user1@example.com', 'foo Fuß'),
  127. ('user2@example.com', 'bar'),
  128. ]
  129. def __init__(self, host=None):
  130. self.trial = trial_dir.TrialDir('repos')
  131. self.host = host or '127.0.0.1'
  132. # Format is { repo: [ None, (hash, tree), (hash, tree), ... ], ... }
  133. # so reference looks like self.git_hashes[repo][rev][0] for hash and
  134. # self.git_hashes[repo][rev][1] for it's tree snapshot.
  135. # It is 1-based too.
  136. self.git_hashes = {}
  137. self.gitdaemon = None
  138. self.git_pid_file_name = None
  139. self.git_root = None
  140. self.git_dirty = False
  141. self.git_port = None
  142. self.git_base = None
  143. @property
  144. def root_dir(self):
  145. return self.trial.root_dir
  146. def set_up(self):
  147. """All late initialization comes here."""
  148. self.cleanup_dirt()
  149. if not self.root_dir:
  150. try:
  151. # self.root_dir is not set before this call.
  152. self.trial.set_up()
  153. self.git_root = join(self.root_dir, 'git')
  154. finally:
  155. # Registers cleanup.
  156. atexit.register(self.tear_down)
  157. def cleanup_dirt(self):
  158. """For each dirty repository, destroy it."""
  159. if self.git_dirty:
  160. if not self.tear_down_git():
  161. logging.error('Using both leaking checkout and git dirty checkout')
  162. def tear_down(self):
  163. """Kills the servers and delete the directories."""
  164. self.tear_down_git()
  165. # This deletes the directories.
  166. self.trial.tear_down()
  167. self.trial = None
  168. def tear_down_git(self):
  169. if self.gitdaemon:
  170. logging.debug('Killing git-daemon pid %s' % self.gitdaemon.pid)
  171. self.gitdaemon.kill()
  172. self.gitdaemon = None
  173. if self.git_pid_file_name:
  174. pid = int(open(self.git_pid_file_name).read())
  175. logging.debug('Killing git daemon pid %s' % pid)
  176. try:
  177. subprocess2.kill_pid(pid)
  178. except OSError as e:
  179. if e.errno != errno.ESRCH: # no such process
  180. raise
  181. os.remove(self.git_pid_file_name)
  182. self.git_pid_file_name = None
  183. wait_for_port_to_free(self.host, self.git_port)
  184. self.git_port = None
  185. self.git_base = None
  186. if not self.trial.SHOULD_LEAK:
  187. logging.debug('Removing %s' % self.git_root)
  188. gclient_utils.rmtree(self.git_root)
  189. else:
  190. return False
  191. return True
  192. @staticmethod
  193. def _genTree(root, tree_dict):
  194. """For a dictionary of file contents, generate a filesystem."""
  195. if not os.path.isdir(root):
  196. os.makedirs(root)
  197. for (k, v) in tree_dict.items():
  198. k_os = k.replace('/', os.sep)
  199. k_arr = k_os.split(os.sep)
  200. if len(k_arr) > 1:
  201. p = os.sep.join([root] + k_arr[:-1])
  202. if not os.path.isdir(p):
  203. os.makedirs(p)
  204. if v is None:
  205. os.remove(join(root, k))
  206. else:
  207. write(join(root, k), v)
  208. def set_up_git(self):
  209. """Creates git repositories and start the servers."""
  210. self.set_up()
  211. if self.gitdaemon:
  212. return True
  213. assert self.git_pid_file_name == None, self.git_pid_file_name
  214. try:
  215. subprocess2.check_output(['git', '--version'])
  216. except (OSError, subprocess2.CalledProcessError):
  217. return False
  218. for repo in ['repo_%d' % r for r in range(1, self.NB_GIT_REPOS + 1)]:
  219. subprocess2.check_call(['git', 'init', '-q', join(self.git_root, repo)])
  220. self.git_hashes[repo] = [(None, None)]
  221. git_pid_file = tempfile.NamedTemporaryFile(delete=False)
  222. self.git_pid_file_name = git_pid_file.name
  223. git_pid_file.close()
  224. self.git_port = find_free_port(self.host)
  225. self.git_base = 'git://%s:%d/git/' % (self.host, self.git_port)
  226. cmd = ['git', 'daemon',
  227. '--export-all',
  228. '--reuseaddr',
  229. '--base-path=' + self.root_dir,
  230. '--pid-file=' + self.git_pid_file_name,
  231. '--port=%d' % self.git_port]
  232. if self.host == '127.0.0.1':
  233. cmd.append('--listen=' + self.host)
  234. # Verify that the port is free.
  235. if not port_is_free(self.host, self.git_port):
  236. return False
  237. # Start the daemon.
  238. self.gitdaemon = subprocess2.Popen(
  239. cmd,
  240. cwd=self.root_dir,
  241. stdout=subprocess2.PIPE,
  242. stderr=subprocess2.PIPE)
  243. wait_for_port_to_bind(self.host, self.git_port, self.gitdaemon)
  244. self.populateGit()
  245. self.git_dirty = False
  246. return True
  247. def _git_rev_parse(self, path):
  248. return subprocess2.check_output(
  249. ['git', 'rev-parse', 'HEAD'], cwd=path).strip()
  250. def _commit_git(self, repo, tree, base=None):
  251. repo_root = join(self.git_root, repo)
  252. if base:
  253. base_commit = self.git_hashes[repo][base][0]
  254. subprocess2.check_call(
  255. ['git', 'checkout', base_commit], cwd=repo_root)
  256. self._genTree(repo_root, tree)
  257. commit_hash = commit_git(repo_root)
  258. base = base or -1
  259. if self.git_hashes[repo][base][1]:
  260. new_tree = self.git_hashes[repo][base][1].copy()
  261. new_tree.update(tree)
  262. else:
  263. new_tree = tree.copy()
  264. self.git_hashes[repo].append((commit_hash, new_tree))
  265. def _create_ref(self, repo, ref, revision):
  266. repo_root = join(self.git_root, repo)
  267. subprocess2.check_call(
  268. ['git', 'update-ref', ref, self.git_hashes[repo][revision][0]],
  269. cwd=repo_root)
  270. def _fast_import_git(self, repo, data):
  271. repo_root = join(self.git_root, repo)
  272. logging.debug('%s: fast-import %s', repo, data)
  273. subprocess2.check_call(
  274. ['git', 'fast-import', '--quiet'], cwd=repo_root, stdin=data.encode())
  275. def populateGit(self):
  276. raise NotImplementedError()
  277. class FakeRepos(FakeReposBase):
  278. """Implements populateGit()."""
  279. NB_GIT_REPOS = 16
  280. def populateGit(self):
  281. # Testing:
  282. # - dependency disappear
  283. # - dependency renamed
  284. # - versioned and unversioned reference
  285. # - relative and full reference
  286. # - deps_os
  287. # - var
  288. # - hooks
  289. # TODO(maruel):
  290. # - use_relative_paths
  291. self._commit_git('repo_3', {
  292. 'origin': 'git/repo_3@1\n',
  293. })
  294. self._commit_git('repo_3', {
  295. 'origin': 'git/repo_3@2\n',
  296. })
  297. self._commit_git('repo_1', {
  298. 'DEPS': """
  299. vars = {
  300. 'DummyVariable': 'repo',
  301. 'false_var': False,
  302. 'false_str_var': 'False',
  303. 'true_var': True,
  304. 'true_str_var': 'True',
  305. 'str_var': 'abc',
  306. 'cond_var': 'false_str_var and true_var',
  307. }
  308. # Nest the args file in a sub-repo, to make sure we don't try to
  309. # write it before we've cloned everything.
  310. gclient_gn_args_file = 'src/repo2/gclient.args'
  311. gclient_gn_args = [
  312. 'false_var',
  313. 'false_str_var',
  314. 'true_var',
  315. 'true_str_var',
  316. 'str_var',
  317. 'cond_var',
  318. ]
  319. deps = {
  320. 'src/repo2': {
  321. 'url': '%(git_base)srepo_2',
  322. 'condition': 'True',
  323. },
  324. 'src/repo2/repo3': '/' + Var('DummyVariable') + '_3@%(hash3)s',
  325. # Test that deps where condition evaluates to False are skipped.
  326. 'src/repo5': {
  327. 'url': '/repo_5',
  328. 'condition': 'False',
  329. },
  330. }
  331. deps_os = {
  332. 'mac': {
  333. 'src/repo4': '/repo_4',
  334. },
  335. }""" % {
  336. 'git_base': self.git_base,
  337. # See self.__init__() for the format. Grab's the hash of the first
  338. # commit in repo_2. Only keep the first 7 character because of:
  339. # TODO(maruel): http://crosbug.com/3591 We need to strip the hash..
  340. # duh.
  341. 'hash3': self.git_hashes['repo_3'][1][0][:7]
  342. },
  343. 'origin': 'git/repo_1@1\n',
  344. })
  345. self._commit_git('repo_2', {
  346. 'origin': 'git/repo_2@1\n',
  347. 'DEPS': """
  348. vars = {
  349. 'repo2_false_var': 'False',
  350. }
  351. deps = {
  352. 'foo/bar': {
  353. 'url': '/repo_3',
  354. 'condition': 'repo2_false_var',
  355. }
  356. }
  357. """,
  358. })
  359. self._commit_git('repo_2', {
  360. 'origin': 'git/repo_2@2\n',
  361. })
  362. self._commit_git('repo_4', {
  363. 'origin': 'git/repo_4@1\n',
  364. })
  365. self._commit_git('repo_4', {
  366. 'origin': 'git/repo_4@2\n',
  367. })
  368. self._commit_git('repo_1', {
  369. 'DEPS': """
  370. deps = {
  371. 'src/repo2': '%(git_base)srepo_2@%(hash)s',
  372. 'src/repo2/repo_renamed': '/repo_3',
  373. 'src/should_not_process': {
  374. 'url': '/repo_4',
  375. 'condition': 'False',
  376. }
  377. }
  378. # I think this is wrong to have the hooks run from the base of the gclient
  379. # checkout. It's maybe a bit too late to change that behavior.
  380. hooks = [
  381. {
  382. 'pattern': '.',
  383. 'action': ['python', '-c',
  384. 'open(\\'src/git_hooked1\\', \\'w\\').write(\\'git_hooked1\\')'],
  385. },
  386. {
  387. # Should not be run.
  388. 'pattern': 'nonexistent',
  389. 'action': ['python', '-c',
  390. 'open(\\'src/git_hooked2\\', \\'w\\').write(\\'git_hooked2\\')'],
  391. },
  392. ]
  393. """ % {
  394. 'git_base': self.git_base,
  395. # See self.__init__() for the format. Grab's the hash of the first
  396. # commit in repo_2. Only keep the first 7 character because of:
  397. # TODO(maruel): http://crosbug.com/3591 We need to strip the hash.. duh.
  398. 'hash': self.git_hashes['repo_2'][1][0][:7]
  399. },
  400. 'origin': 'git/repo_1@2\n',
  401. })
  402. self._commit_git('repo_5', {'origin': 'git/repo_5@1\n'})
  403. self._commit_git('repo_5', {
  404. 'DEPS': """
  405. deps = {
  406. 'src/repo1': '%(git_base)srepo_1@%(hash1)s',
  407. 'src/repo2': '%(git_base)srepo_2@%(hash2)s',
  408. }
  409. # Hooks to run after a project is processed but before its dependencies are
  410. # processed.
  411. pre_deps_hooks = [
  412. {
  413. 'action': ['python', '-c',
  414. 'print("pre-deps hook"); open(\\'src/git_pre_deps_hooked\\', \\'w\\').write(\\'git_pre_deps_hooked\\')'],
  415. }
  416. ]
  417. """ % {
  418. 'git_base': self.git_base,
  419. 'hash1': self.git_hashes['repo_1'][2][0][:7],
  420. 'hash2': self.git_hashes['repo_2'][1][0][:7],
  421. },
  422. 'origin': 'git/repo_5@2\n',
  423. })
  424. self._commit_git('repo_5', {
  425. 'DEPS': """
  426. deps = {
  427. 'src/repo1': '%(git_base)srepo_1@%(hash1)s',
  428. 'src/repo2': '%(git_base)srepo_2@%(hash2)s',
  429. }
  430. # Hooks to run after a project is processed but before its dependencies are
  431. # processed.
  432. pre_deps_hooks = [
  433. {
  434. 'action': ['python', '-c',
  435. 'print("pre-deps hook"); open(\\'src/git_pre_deps_hooked\\', \\'w\\').write(\\'git_pre_deps_hooked\\')'],
  436. },
  437. {
  438. 'action': ['python', '-c', 'import sys; sys.exit(1)'],
  439. }
  440. ]
  441. """ % {
  442. 'git_base': self.git_base,
  443. 'hash1': self.git_hashes['repo_1'][2][0][:7],
  444. 'hash2': self.git_hashes['repo_2'][1][0][:7],
  445. },
  446. 'origin': 'git/repo_5@3\n',
  447. })
  448. self._commit_git('repo_6', {
  449. 'DEPS': """
  450. vars = {
  451. 'DummyVariable': 'repo',
  452. 'git_base': '%(git_base)s',
  453. 'hook1_contents': 'git_hooked1',
  454. 'repo5_var': '/repo_5',
  455. 'false_var': False,
  456. 'false_str_var': 'False',
  457. 'true_var': True,
  458. 'true_str_var': 'True',
  459. 'str_var': 'abc',
  460. 'cond_var': 'false_str_var and true_var',
  461. }
  462. gclient_gn_args_file = 'src/repo2/gclient.args'
  463. gclient_gn_args = [
  464. 'false_var',
  465. 'false_str_var',
  466. 'true_var',
  467. 'true_str_var',
  468. 'str_var',
  469. 'cond_var',
  470. ]
  471. allowed_hosts = [
  472. '%(git_base)s',
  473. ]
  474. deps = {
  475. 'src/repo2': {
  476. 'url': Var('git_base') + 'repo_2@%(hash)s',
  477. 'condition': 'true_str_var',
  478. },
  479. 'src/repo4': {
  480. 'url': '/repo_4',
  481. 'condition': 'False',
  482. },
  483. # Entries can have a None repository, which has the effect of either:
  484. # - disabling a dep checkout (e.g. in a .gclient solution to prevent checking
  485. # out optional large repos, or in deps_os where some repos aren't used on some
  486. # platforms)
  487. # - allowing a completely local directory to be processed by gclient (handy
  488. # for dealing with "custom" DEPS, like buildspecs).
  489. '/repoLocal': {
  490. 'url': None,
  491. },
  492. 'src/repo8': '/repo_8',
  493. 'src/repo15': '/repo_15',
  494. 'src/repo16': '/repo_16',
  495. }
  496. deps_os ={
  497. 'mac': {
  498. # This entry should not appear in flattened DEPS' |deps|.
  499. 'src/mac_repo': '{repo5_var}',
  500. },
  501. 'unix': {
  502. # This entry should not appear in flattened DEPS' |deps|.
  503. 'src/unix_repo': '{repo5_var}',
  504. },
  505. 'win': {
  506. # This entry should not appear in flattened DEPS' |deps|.
  507. 'src/win_repo': '{repo5_var}',
  508. },
  509. }
  510. hooks = [
  511. {
  512. 'pattern': '.',
  513. 'condition': 'True',
  514. 'action': ['python', '-c',
  515. 'open(\\'src/git_hooked1\\', \\'w\\').write(\\'{hook1_contents}\\')'],
  516. },
  517. {
  518. # Should not be run.
  519. 'pattern': 'nonexistent',
  520. 'action': ['python', '-c',
  521. 'open(\\'src/git_hooked2\\', \\'w\\').write(\\'git_hooked2\\')'],
  522. },
  523. ]
  524. hooks_os = {
  525. 'mac': [
  526. {
  527. 'pattern': '.',
  528. 'action': ['python', '-c',
  529. 'open(\\'src/git_hooked_mac\\', \\'w\\').write('
  530. '\\'git_hooked_mac\\')'],
  531. },
  532. ],
  533. }
  534. recursedeps = [
  535. 'src/repo2',
  536. 'src/repo8',
  537. 'src/repo15',
  538. 'src/repo16',
  539. ]""" % {
  540. 'git_base': self.git_base,
  541. 'hash': self.git_hashes['repo_2'][1][0][:7]
  542. },
  543. 'origin': 'git/repo_6@1\n',
  544. })
  545. self._commit_git('repo_7', {
  546. 'DEPS': """
  547. vars = {
  548. 'true_var': 'True',
  549. 'false_var': 'true_var and False',
  550. }
  551. hooks = [
  552. {
  553. 'action': ['python', '-c',
  554. 'open(\\'src/should_run\\', \\'w\\').write(\\'should_run\\')'],
  555. 'condition': 'true_var or True',
  556. },
  557. {
  558. 'action': ['python', '-c',
  559. 'open(\\'src/should_not_run\\', \\'w\\').write(\\'should_not_run\\')'],
  560. 'condition': 'false_var',
  561. },
  562. ]""",
  563. 'origin': 'git/repo_7@1\n',
  564. })
  565. self._commit_git('repo_8', {
  566. 'DEPS': """
  567. deps_os ={
  568. 'mac': {
  569. 'src/recursed_os_repo': '/repo_5',
  570. },
  571. 'unix': {
  572. 'src/recursed_os_repo': '/repo_5',
  573. },
  574. }""",
  575. 'origin': 'git/repo_8@1\n',
  576. })
  577. self._commit_git('repo_9', {
  578. 'DEPS': """
  579. vars = {
  580. 'str_var': 'xyz',
  581. }
  582. gclient_gn_args_file = 'src/repo2/gclient.args'
  583. gclient_gn_args = [
  584. 'str_var',
  585. ]
  586. deps = {
  587. 'src/repo8': '/repo_8',
  588. # This entry should appear in flattened file,
  589. # but not recursed into, since it's not
  590. # in recursedeps.
  591. 'src/repo7': '/repo_7',
  592. }
  593. deps_os = {
  594. 'android': {
  595. # This entry should only appear in flattened |deps_os|,
  596. # not |deps|, even when used with |recursedeps|.
  597. 'src/repo4': '/repo_4',
  598. }
  599. }
  600. recursedeps = [
  601. 'src/repo4',
  602. 'src/repo8',
  603. ]""",
  604. 'origin': 'git/repo_9@1\n',
  605. })
  606. self._commit_git('repo_10', {
  607. 'DEPS': """
  608. gclient_gn_args_from = 'src/repo9'
  609. deps = {
  610. 'src/repo9': '/repo_9',
  611. # This entry should appear in flattened file,
  612. # but not recursed into, since it's not
  613. # in recursedeps.
  614. 'src/repo6': '/repo_6',
  615. }
  616. deps_os = {
  617. 'mac': {
  618. 'src/repo11': '/repo_11',
  619. },
  620. 'ios': {
  621. 'src/repo11': '/repo_11',
  622. }
  623. }
  624. recursedeps = [
  625. 'src/repo9',
  626. 'src/repo11',
  627. ]""",
  628. 'origin': 'git/repo_10@1\n',
  629. })
  630. self._commit_git('repo_11', {
  631. 'DEPS': """
  632. deps = {
  633. 'src/repo12': '/repo_12',
  634. }""",
  635. 'origin': 'git/repo_11@1\n',
  636. })
  637. self._commit_git('repo_12', {
  638. 'origin': 'git/repo_12@1\n',
  639. })
  640. self._fast_import_git('repo_12', """blob
  641. mark :1
  642. data 6
  643. Hello
  644. blob
  645. mark :2
  646. data 4
  647. Bye
  648. reset refs/changes/1212
  649. commit refs/changes/1212
  650. mark :3
  651. author Bob <bob@example.com> 1253744361 -0700
  652. committer Bob <bob@example.com> 1253744361 -0700
  653. data 8
  654. A and B
  655. M 100644 :1 a
  656. M 100644 :2 b
  657. """)
  658. self._commit_git('repo_13', {
  659. 'DEPS': """
  660. deps = {
  661. 'src/repo12': '/repo_12',
  662. }""",
  663. 'origin': 'git/repo_13@1\n',
  664. })
  665. self._commit_git('repo_13', {
  666. 'DEPS': """
  667. deps = {
  668. 'src/repo12': '/repo_12@refs/changes/1212',
  669. }""",
  670. 'origin': 'git/repo_13@2\n',
  671. })
  672. # src/repo12 is now a CIPD dependency.
  673. self._commit_git('repo_13', {
  674. 'DEPS': """
  675. deps = {
  676. 'src/repo12': {
  677. 'packages': [
  678. {
  679. 'package': 'foo',
  680. 'version': '1.3',
  681. },
  682. ],
  683. 'dep_type': 'cipd',
  684. },
  685. }
  686. hooks = [{
  687. # make sure src/repo12 exists and is a CIPD dir.
  688. 'action': ['python', '-c', 'with open("src/repo12/_cipd"): pass'],
  689. }]
  690. """,
  691. 'origin': 'git/repo_13@3\n'
  692. })
  693. self._commit_git('repo_14', {
  694. 'DEPS': textwrap.dedent("""\
  695. vars = {}
  696. deps = {
  697. 'src/cipd_dep': {
  698. 'packages': [
  699. {
  700. 'package': 'package0',
  701. 'version': '0.1',
  702. },
  703. ],
  704. 'dep_type': 'cipd',
  705. },
  706. 'src/another_cipd_dep': {
  707. 'packages': [
  708. {
  709. 'package': 'package1',
  710. 'version': '1.1-cr0',
  711. },
  712. {
  713. 'package': 'package2',
  714. 'version': '1.13',
  715. },
  716. ],
  717. 'dep_type': 'cipd',
  718. },
  719. 'src/cipd_dep_with_cipd_variable': {
  720. 'packages': [
  721. {
  722. 'package': 'package3/${{platform}}',
  723. 'version': '1.2',
  724. },
  725. ],
  726. 'dep_type': 'cipd',
  727. },
  728. }"""),
  729. 'origin': 'git/repo_14@2\n'
  730. })
  731. # A repo with a hook to be recursed in, without use_relative_hooks
  732. self._commit_git('repo_15', {
  733. 'DEPS': textwrap.dedent("""\
  734. hooks = [{
  735. "name": "absolute_cwd",
  736. "pattern": ".",
  737. "action": ["python", "-c", "pass"]
  738. }]"""),
  739. 'origin': 'git/repo_15@2\n'
  740. })
  741. # A repo with a hook to be recursed in, with use_relative_hooks
  742. self._commit_git('repo_16', {
  743. 'DEPS': textwrap.dedent("""\
  744. use_relative_paths=True
  745. use_relative_hooks=True
  746. hooks = [{
  747. "name": "relative_cwd",
  748. "pattern": ".",
  749. "action": ["python", "relative.py"]
  750. }]"""),
  751. 'relative.py': 'pass',
  752. 'origin': 'git/repo_16@2\n'
  753. })
  754. class FakeRepoSkiaDEPS(FakeReposBase):
  755. """Simulates the Skia DEPS transition in Chrome."""
  756. NB_GIT_REPOS = 5
  757. DEPS_git_pre = """deps = {
  758. 'src/third_party/skia/gyp': '%(git_base)srepo_3',
  759. 'src/third_party/skia/include': '%(git_base)srepo_4',
  760. 'src/third_party/skia/src': '%(git_base)srepo_5',
  761. }"""
  762. DEPS_post = """deps = {
  763. 'src/third_party/skia': '%(git_base)srepo_1',
  764. }"""
  765. def populateGit(self):
  766. # Skia repo.
  767. self._commit_git('repo_1', {
  768. 'skia_base_file': 'root-level file.',
  769. 'gyp/gyp_file': 'file in the gyp directory',
  770. 'include/include_file': 'file in the include directory',
  771. 'src/src_file': 'file in the src directory',
  772. })
  773. self._commit_git('repo_3', { # skia/gyp
  774. 'gyp_file': 'file in the gyp directory',
  775. })
  776. self._commit_git('repo_4', { # skia/include
  777. 'include_file': 'file in the include directory',
  778. })
  779. self._commit_git('repo_5', { # skia/src
  780. 'src_file': 'file in the src directory',
  781. })
  782. # Chrome repo.
  783. self._commit_git('repo_2', {
  784. 'DEPS': self.DEPS_git_pre % {'git_base': self.git_base},
  785. 'myfile': 'src/trunk/src@1'
  786. })
  787. self._commit_git('repo_2', {
  788. 'DEPS': self.DEPS_post % {'git_base': self.git_base},
  789. 'myfile': 'src/trunk/src@2'
  790. })
  791. class FakeRepoBlinkDEPS(FakeReposBase):
  792. """Simulates the Blink DEPS transition in Chrome."""
  793. NB_GIT_REPOS = 2
  794. DEPS_pre = 'deps = {"src/third_party/WebKit": "%(git_base)srepo_2",}'
  795. DEPS_post = 'deps = {}'
  796. def populateGit(self):
  797. # Blink repo.
  798. self._commit_git('repo_2', {
  799. 'OWNERS': 'OWNERS-pre',
  800. 'Source/exists_always': '_ignored_',
  801. 'Source/exists_before_but_not_after': '_ignored_',
  802. })
  803. # Chrome repo.
  804. self._commit_git('repo_1', {
  805. 'DEPS': self.DEPS_pre % {'git_base': self.git_base},
  806. 'myfile': 'myfile@1',
  807. '.gitignore': '/third_party/WebKit',
  808. })
  809. self._commit_git('repo_1', {
  810. 'DEPS': self.DEPS_post % {'git_base': self.git_base},
  811. 'myfile': 'myfile@2',
  812. '.gitignore': '',
  813. 'third_party/WebKit/OWNERS': 'OWNERS-post',
  814. 'third_party/WebKit/Source/exists_always': '_ignored_',
  815. 'third_party/WebKit/Source/exists_after_but_not_before': '_ignored',
  816. })
  817. def populateSvn(self):
  818. raise NotImplementedError()
  819. class FakeReposTestBase(trial_dir.TestCase):
  820. """This is vaguely inspired by twisted."""
  821. # Static FakeRepos instances. Lazy loaded.
  822. CACHED_FAKE_REPOS = {}
  823. # Override if necessary.
  824. FAKE_REPOS_CLASS = FakeRepos
  825. def setUp(self):
  826. super(FakeReposTestBase, self).setUp()
  827. if not self.FAKE_REPOS_CLASS in self.CACHED_FAKE_REPOS:
  828. self.CACHED_FAKE_REPOS[self.FAKE_REPOS_CLASS] = self.FAKE_REPOS_CLASS()
  829. self.FAKE_REPOS = self.CACHED_FAKE_REPOS[self.FAKE_REPOS_CLASS]
  830. # No need to call self.FAKE_REPOS.setUp(), it will be called by the child
  831. # class.
  832. # Do not define tearDown(), since super's version does the right thing and
  833. # self.FAKE_REPOS is kept across tests.
  834. @property
  835. def git_base(self):
  836. """Shortcut."""
  837. return self.FAKE_REPOS.git_base
  838. def checkString(self, expected, result, msg=None):
  839. """Prints the diffs to ease debugging."""
  840. self.assertEqual(expected.splitlines(), result.splitlines(), msg)
  841. if expected != result:
  842. # Strip the begining
  843. while expected and result and expected[0] == result[0]:
  844. expected = expected[1:]
  845. result = result[1:]
  846. # The exception trace makes it hard to read so dump it too.
  847. if '\n' in result:
  848. print(result)
  849. self.assertEqual(expected, result, msg)
  850. def check(self, expected, results):
  851. """Checks stdout, stderr, returncode."""
  852. self.checkString(expected[0], results[0])
  853. self.checkString(expected[1], results[1])
  854. self.assertEqual(expected[2], results[2])
  855. def assertTree(self, tree, tree_root=None):
  856. """Diff the checkout tree with a dict."""
  857. if not tree_root:
  858. tree_root = self.root_dir
  859. actual = read_tree(tree_root)
  860. diff = dict_diff(tree, actual)
  861. if diff:
  862. logging.error('Actual %s\n%s' % (tree_root, pprint.pformat(actual)))
  863. logging.error('Expected\n%s' % pprint.pformat(tree))
  864. logging.error('Diff\n%s' % pprint.pformat(diff))
  865. self.assertEqual(diff, {})
  866. def mangle_git_tree(self, *args):
  867. """Creates a 'virtual directory snapshot' to compare with the actual result
  868. on disk."""
  869. result = {}
  870. for item, new_root in args:
  871. repo, rev = item.split('@', 1)
  872. tree = self.gittree(repo, rev)
  873. for k, v in tree.items():
  874. result[join(new_root, k)] = v
  875. return result
  876. def githash(self, repo, rev):
  877. """Sort-hand: Returns the hash for a git 'revision'."""
  878. return self.FAKE_REPOS.git_hashes[repo][int(rev)][0]
  879. def gittree(self, repo, rev):
  880. """Sort-hand: returns the directory tree for a git 'revision'."""
  881. return self.FAKE_REPOS.git_hashes[repo][int(rev)][1]
  882. def gitrevparse(self, repo):
  883. """Returns the actual revision for a given repo."""
  884. return self.FAKE_REPOS._git_rev_parse(repo).decode('utf-8')
  885. def main(argv):
  886. fake = FakeRepos()
  887. print('Using %s' % fake.root_dir)
  888. try:
  889. fake.set_up_git()
  890. print('Fake setup, press enter to quit or Ctrl-C to keep the checkouts.')
  891. sys.stdin.readline()
  892. except KeyboardInterrupt:
  893. trial_dir.TrialDir.SHOULD_LEAK.leak = True
  894. return 0
  895. if __name__ == '__main__':
  896. sys.exit(main(sys.argv))