fake_repos.py 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274
  1. #!/usr/bin/env python3
  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. import atexit
  8. import datetime
  9. import errno
  10. import io
  11. import logging
  12. import os
  13. import pprint
  14. import random
  15. import re
  16. import socket
  17. import sys
  18. import tarfile
  19. import tempfile
  20. import textwrap
  21. import time
  22. # trial_dir must be first for non-system libraries.
  23. from testing_support import trial_dir
  24. import gclient_utils
  25. import scm
  26. import subprocess2
  27. DEFAULT_BRANCH = 'main'
  28. def write(path, content):
  29. f = open(path, 'wb')
  30. f.write(content.encode())
  31. f.close()
  32. join = os.path.join
  33. def read_tree(tree_root):
  34. """Returns a dict of all the files in a tree. Defaults to self.root_dir."""
  35. tree = {}
  36. for root, dirs, files in os.walk(tree_root):
  37. for d in filter(lambda x: x.startswith('.'), dirs):
  38. dirs.remove(d)
  39. for f in [join(root, f) for f in files if not f.startswith('.')]:
  40. filepath = f[len(tree_root) + 1:].replace(os.sep, '/')
  41. assert len(filepath) > 0, f
  42. if tarfile.is_tarfile(join(root, f)):
  43. tree[filepath] = 'tarfile'
  44. continue
  45. with io.open(join(root, f), encoding='utf-8') as f:
  46. tree[filepath] = f.read()
  47. return tree
  48. def dict_diff(dict1, dict2):
  49. diff = {}
  50. for k, v in dict1.items():
  51. if k not in dict2:
  52. diff[k] = v
  53. elif v != dict2[k]:
  54. diff[k] = (v, dict2[k])
  55. for k, v in dict2.items():
  56. if k not in dict1:
  57. diff[k] = v
  58. return diff
  59. def commit_git(repo):
  60. """Commits the changes and returns the new hash."""
  61. subprocess2.check_call(['git', 'add', '-A', '-f'], cwd=repo)
  62. subprocess2.check_call(['git', 'commit', '-q', '--message', 'foo'],
  63. cwd=repo)
  64. rev = subprocess2.check_output(['git', 'show-ref', '--head', 'HEAD'],
  65. cwd=repo).split(b' ', 1)[0]
  66. rev = rev.decode('utf-8')
  67. logging.debug('At revision %s' % rev)
  68. return rev
  69. class FakeReposBase(object):
  70. """Generate git repositories to test gclient functionality.
  71. Many DEPS functionalities need to be tested: Var, deps_os, hooks,
  72. use_relative_paths.
  73. And types of dependencies: Relative urls, Full urls, git.
  74. populateGit() needs to be implemented by the subclass.
  75. """
  76. # Hostname
  77. NB_GIT_REPOS = 1
  78. USERS = [
  79. ('user1@example.com', 'foo Fuß'),
  80. ('user2@example.com', 'bar'),
  81. ]
  82. def __init__(self, host=None):
  83. self.trial = trial_dir.TrialDir('repos')
  84. self.host = host or '127.0.0.1'
  85. # Format is { repo: [ None, (hash, tree), (hash, tree), ... ], ... }
  86. # so reference looks like self.git_hashes[repo][rev][0] for hash and
  87. # self.git_hashes[repo][rev][1] for it's tree snapshot.
  88. # It is 1-based too.
  89. self.git_hashes = {}
  90. self.git_pid_file_name = None
  91. self.git_base = None
  92. self.initialized = False
  93. @property
  94. def root_dir(self):
  95. return self.trial.root_dir
  96. def set_up(self):
  97. """All late initialization comes here."""
  98. if not self.root_dir:
  99. try:
  100. # self.root_dir is not set before this call.
  101. self.trial.set_up()
  102. self.git_base = join(self.root_dir, 'git') + os.sep
  103. finally:
  104. # Registers cleanup.
  105. atexit.register(self.tear_down)
  106. def tear_down(self):
  107. """Kills the servers and delete the directories."""
  108. self.tear_down_git()
  109. # This deletes the directories.
  110. self.trial.tear_down()
  111. self.trial = None
  112. def tear_down_git(self):
  113. if self.trial.SHOULD_LEAK:
  114. return False
  115. logging.debug('Removing %s' % self.git_base)
  116. gclient_utils.rmtree(self.git_base)
  117. return True
  118. @staticmethod
  119. def _genTree(root, tree_dict):
  120. """For a dictionary of file contents, generate a filesystem."""
  121. if not os.path.isdir(root):
  122. os.makedirs(root)
  123. for (k, v) in tree_dict.items():
  124. k_os = k.replace('/', os.sep)
  125. k_arr = k_os.split(os.sep)
  126. if len(k_arr) > 1:
  127. p = os.sep.join([root] + k_arr[:-1])
  128. if not os.path.isdir(p):
  129. os.makedirs(p)
  130. if v is None:
  131. os.remove(join(root, k))
  132. else:
  133. write(join(root, k), v)
  134. def set_up_git(self):
  135. """Creates git repositories and start the servers."""
  136. self.set_up()
  137. if self.initialized:
  138. return True
  139. try:
  140. subprocess2.check_output(['git', '--version'])
  141. except (OSError, subprocess2.CalledProcessError):
  142. return False
  143. for repo in ['repo_%d' % r for r in range(1, self.NB_GIT_REPOS + 1)]:
  144. subprocess2.check_call([
  145. 'git', 'init', '-b', DEFAULT_BRANCH, '-q',
  146. join(self.git_base, repo)
  147. ])
  148. subprocess2.check_call([
  149. 'git',
  150. '-C',
  151. join(self.git_base, repo),
  152. 'config',
  153. 'user.name',
  154. 'Hina Hoshino',
  155. ])
  156. subprocess2.check_call([
  157. 'git',
  158. '-C',
  159. join(self.git_base, repo),
  160. 'config',
  161. 'user.email',
  162. 'testing@example.com',
  163. ])
  164. self.git_hashes[repo] = [(None, None)]
  165. self.populateGit()
  166. self.initialized = True
  167. return True
  168. def _git_rev_parse(self, path):
  169. return subprocess2.check_output(['git', 'rev-parse', 'HEAD'],
  170. cwd=path).strip()
  171. def _commit_git(self, repo, tree, base=None):
  172. repo_root = join(self.git_base, repo)
  173. if base:
  174. base_commit = self.git_hashes[repo][base][0]
  175. subprocess2.check_call(['git', 'checkout', base_commit],
  176. cwd=repo_root)
  177. self._genTree(repo_root, tree)
  178. commit_hash = commit_git(repo_root)
  179. base = base or -1
  180. if self.git_hashes[repo][base][1]:
  181. new_tree = self.git_hashes[repo][base][1].copy()
  182. new_tree.update(tree)
  183. else:
  184. new_tree = tree.copy()
  185. self.git_hashes[repo].append((commit_hash, new_tree))
  186. def _create_ref(self, repo, ref, revision):
  187. repo_root = join(self.git_base, repo)
  188. subprocess2.check_call(
  189. ['git', 'update-ref', ref, self.git_hashes[repo][revision][0]],
  190. cwd=repo_root)
  191. def _fast_import_git(self, repo, data):
  192. repo_root = join(self.git_base, repo)
  193. logging.debug('%s: fast-import %s', repo, data)
  194. subprocess2.check_call(['git', 'fast-import', '--quiet'],
  195. cwd=repo_root,
  196. stdin=data.encode())
  197. def populateGit(self):
  198. raise NotImplementedError()
  199. class FakeRepos(FakeReposBase):
  200. """Implements populateGit()."""
  201. NB_GIT_REPOS = 24
  202. def populateGit(self):
  203. # Testing:
  204. # - dependency disappear
  205. # - dependency renamed
  206. # - versioned and unversioned reference
  207. # - relative and full reference
  208. # - deps_os
  209. # - var
  210. # - hooks
  211. # TODO(maruel):
  212. # - use_relative_paths
  213. self._commit_git('repo_3', {
  214. 'origin': 'git/repo_3@1\n',
  215. })
  216. self._commit_git('repo_3', {
  217. 'origin': 'git/repo_3@2\n',
  218. })
  219. self._commit_git(
  220. 'repo_1',
  221. {
  222. 'DEPS': """
  223. vars = {
  224. 'DummyVariable': 'repo',
  225. 'false_var': False,
  226. 'false_str_var': 'False',
  227. 'true_var': True,
  228. 'true_str_var': 'True',
  229. 'str_var': 'abc',
  230. 'cond_var': 'false_str_var and true_var',
  231. }
  232. # Nest the args file in a sub-repo, to make sure we don't try to
  233. # write it before we've cloned everything.
  234. gclient_gn_args_file = 'src/repo2/gclient.args'
  235. gclient_gn_args = [
  236. 'false_var',
  237. 'false_str_var',
  238. 'true_var',
  239. 'true_str_var',
  240. 'str_var',
  241. 'cond_var',
  242. ]
  243. deps = {
  244. 'src/repo2': {
  245. 'url': %(git_base)r + 'repo_2',
  246. 'condition': 'True',
  247. },
  248. 'src/repo2/repo3': '/' + Var('DummyVariable') + '_3@%(hash3)s',
  249. # Test that deps where condition evaluates to False are skipped.
  250. 'src/repo5': {
  251. 'url': '/repo_5',
  252. 'condition': 'False',
  253. },
  254. }
  255. deps_os = {
  256. 'mac': {
  257. 'src/repo4': '/repo_4',
  258. },
  259. }""" % {
  260. 'git_base': self.git_base,
  261. # See self.__init__() for the format. Grab's the hash of the
  262. # first commit in repo_2. Only keep the first 7 character
  263. # because of: TODO(maruel): http://crosbug.com/3591 We need
  264. # to strip the hash.. duh.
  265. 'hash3': self.git_hashes['repo_3'][1][0][:7]
  266. },
  267. 'origin': 'git/repo_1@1\n',
  268. 'foo bar': 'some file with a space',
  269. })
  270. self._commit_git(
  271. 'repo_2', {
  272. 'origin':
  273. 'git/repo_2@1\n',
  274. 'DEPS':
  275. """
  276. vars = {
  277. 'repo2_false_var': 'False',
  278. }
  279. deps = {
  280. 'foo/bar': {
  281. 'url': '/repo_3',
  282. 'condition': 'repo2_false_var',
  283. }
  284. }
  285. """,
  286. })
  287. self._commit_git('repo_2', {
  288. 'origin': 'git/repo_2@2\n',
  289. })
  290. self._commit_git('repo_4', {
  291. 'origin': 'git/repo_4@1\n',
  292. })
  293. self._commit_git('repo_4', {
  294. 'origin': 'git/repo_4@2\n',
  295. })
  296. self._commit_git(
  297. 'repo_1',
  298. {
  299. 'DEPS': """
  300. deps = {
  301. 'src/repo2': %(git_base)r + 'repo_2@%(hash)s',
  302. 'src/repo2/repo_renamed': '/repo_3',
  303. 'src/should_not_process': {
  304. 'url': '/repo_4',
  305. 'condition': 'False',
  306. }
  307. }
  308. # I think this is wrong to have the hooks run from the base of the gclient
  309. # checkout. It's maybe a bit too late to change that behavior.
  310. hooks = [
  311. {
  312. 'pattern': '.',
  313. 'action': ['python3', '-c',
  314. 'open(\\'src/git_hooked1\\', \\'w\\').write(\\'git_hooked1\\')'],
  315. },
  316. {
  317. # Should not be run.
  318. 'pattern': 'nonexistent',
  319. 'action': ['python3', '-c',
  320. 'open(\\'src/git_hooked2\\', \\'w\\').write(\\'git_hooked2\\')'],
  321. },
  322. ]
  323. """ % {
  324. 'git_base': self.git_base,
  325. # See self.__init__() for the format. Grab's the hash of the
  326. # first commit in repo_2. Only keep the first 7 character
  327. # because of: TODO(maruel): http://crosbug.com/3591 We need
  328. # to strip the hash.. duh.
  329. 'hash': self.git_hashes['repo_2'][1][0][:7]
  330. },
  331. 'origin': 'git/repo_1@2\n',
  332. })
  333. self._commit_git('repo_5', {'origin': 'git/repo_5@1\n'})
  334. self._commit_git(
  335. 'repo_5', {
  336. 'DEPS': """
  337. deps = {
  338. 'src/repo1': %(git_base)r + 'repo_1@%(hash1)s',
  339. 'src/repo2': %(git_base)r + 'repo_2@%(hash2)s',
  340. }
  341. # Hooks to run after a project is processed but before its dependencies are
  342. # processed.
  343. pre_deps_hooks = [
  344. {
  345. 'action': ['python3', '-c',
  346. 'print("pre-deps hook"); open(\\'src/git_pre_deps_hooked\\', \\'w\\').write(\\'git_pre_deps_hooked\\')'],
  347. }
  348. ]
  349. """ % {
  350. 'git_base': self.git_base,
  351. 'hash1': self.git_hashes['repo_1'][2][0][:7],
  352. 'hash2': self.git_hashes['repo_2'][1][0][:7],
  353. },
  354. 'origin': 'git/repo_5@2\n',
  355. })
  356. self._commit_git(
  357. 'repo_5', {
  358. 'DEPS': """
  359. deps = {
  360. 'src/repo1': %(git_base)r + 'repo_1@%(hash1)s',
  361. 'src/repo2': %(git_base)r + 'repo_2@%(hash2)s',
  362. }
  363. # Hooks to run after a project is processed but before its dependencies are
  364. # processed.
  365. pre_deps_hooks = [
  366. {
  367. 'action': ['python3', '-c',
  368. 'print("pre-deps hook"); open(\\'src/git_pre_deps_hooked\\', \\'w\\').write(\\'git_pre_deps_hooked\\')'],
  369. },
  370. {
  371. 'action': ['python3', '-c', 'import sys; sys.exit(1)'],
  372. }
  373. ]
  374. """ % {
  375. 'git_base': self.git_base,
  376. 'hash1': self.git_hashes['repo_1'][2][0][:7],
  377. 'hash2': self.git_hashes['repo_2'][1][0][:7],
  378. },
  379. 'origin': 'git/repo_5@3\n',
  380. })
  381. self._commit_git(
  382. 'repo_6', {
  383. 'DEPS': """
  384. vars = {
  385. 'DummyVariable': 'repo',
  386. 'git_base': %(git_base)r,
  387. 'hook1_contents': 'git_hooked1',
  388. 'repo5_var': '/repo_5',
  389. 'false_var': False,
  390. 'false_str_var': 'False',
  391. 'true_var': True,
  392. 'true_str_var': 'True',
  393. 'str_var': 'abc',
  394. 'cond_var': 'false_str_var and true_var',
  395. }
  396. gclient_gn_args_file = 'src/repo2/gclient.args'
  397. gclient_gn_args = [
  398. 'false_var',
  399. 'false_str_var',
  400. 'true_var',
  401. 'true_str_var',
  402. 'str_var',
  403. 'cond_var',
  404. ]
  405. allowed_hosts = [
  406. %(git_base)r,
  407. ]
  408. deps = {
  409. 'src/repo2': {
  410. 'url': Var('git_base') + 'repo_2@%(hash)s',
  411. 'condition': 'true_str_var',
  412. },
  413. 'src/repo4': {
  414. 'url': '/repo_4',
  415. 'condition': 'False',
  416. },
  417. # Entries can have a None repository, which has the effect of either:
  418. # - disabling a dep checkout (e.g. in a .gclient solution to prevent checking
  419. # out optional large repos, or in deps_os where some repos aren't used on some
  420. # platforms)
  421. # - allowing a completely local directory to be processed by gclient (handy
  422. # for dealing with "custom" DEPS, like buildspecs).
  423. '/repoLocal': {
  424. 'url': None,
  425. },
  426. 'src/repo8': '/repo_8',
  427. 'src/repo15': '/repo_15',
  428. 'src/repo16': '/repo_16',
  429. }
  430. deps_os ={
  431. 'mac': {
  432. # This entry should not appear in flattened DEPS' |deps|.
  433. 'src/mac_repo': '{repo5_var}',
  434. },
  435. 'unix': {
  436. # This entry should not appear in flattened DEPS' |deps|.
  437. 'src/unix_repo': '{repo5_var}',
  438. },
  439. 'win': {
  440. # This entry should not appear in flattened DEPS' |deps|.
  441. 'src/win_repo': '{repo5_var}',
  442. },
  443. }
  444. hooks = [
  445. {
  446. 'pattern': '.',
  447. 'condition': 'True',
  448. 'action': ['python3', '-c',
  449. 'open(\\'src/git_hooked1\\', \\'w\\').write(\\'{hook1_contents}\\')'],
  450. },
  451. {
  452. # Should not be run.
  453. 'pattern': 'nonexistent',
  454. 'action': ['python3', '-c',
  455. 'open(\\'src/git_hooked2\\', \\'w\\').write(\\'git_hooked2\\')'],
  456. },
  457. ]
  458. hooks_os = {
  459. 'mac': [
  460. {
  461. 'pattern': '.',
  462. 'action': ['python3', '-c',
  463. 'open(\\'src/git_hooked_mac\\', \\'w\\').write('
  464. '\\'git_hooked_mac\\')'],
  465. },
  466. ],
  467. }
  468. recursedeps = [
  469. 'src/repo2',
  470. 'src/repo8',
  471. 'src/repo15',
  472. 'src/repo16',
  473. ]""" % {
  474. 'git_base': self.git_base,
  475. 'hash': self.git_hashes['repo_2'][1][0][:7]
  476. },
  477. 'origin': 'git/repo_6@1\n',
  478. })
  479. self._commit_git(
  480. 'repo_7', {
  481. 'DEPS': """
  482. vars = {
  483. 'true_var': 'True',
  484. 'false_var': 'true_var and False',
  485. }
  486. hooks = [
  487. {
  488. 'action': ['python3', '-c',
  489. 'open(\\'src/should_run\\', \\'w\\').write(\\'should_run\\')'],
  490. 'condition': 'true_var or True',
  491. },
  492. {
  493. 'action': ['python3', '-c',
  494. 'open(\\'src/should_not_run\\', \\'w\\').write(\\'should_not_run\\')'],
  495. 'condition': 'false_var',
  496. },
  497. ]""",
  498. 'origin': 'git/repo_7@1\n',
  499. })
  500. self._commit_git(
  501. 'repo_8', {
  502. 'DEPS': """
  503. deps_os ={
  504. 'mac': {
  505. 'src/recursed_os_repo': '/repo_5',
  506. },
  507. 'unix': {
  508. 'src/recursed_os_repo': '/repo_5',
  509. },
  510. }""",
  511. 'origin': 'git/repo_8@1\n',
  512. })
  513. self._commit_git(
  514. 'repo_9', {
  515. 'DEPS': """
  516. vars = {
  517. 'str_var': 'xyz',
  518. }
  519. gclient_gn_args_file = 'src/repo8/gclient.args'
  520. gclient_gn_args = [
  521. 'str_var',
  522. ]
  523. deps = {
  524. 'src/repo8': '/repo_8',
  525. # This entry should appear in flattened file,
  526. # but not recursed into, since it's not
  527. # in recursedeps.
  528. 'src/repo7': '/repo_7',
  529. }
  530. deps_os = {
  531. 'android': {
  532. # This entry should only appear in flattened |deps_os|,
  533. # not |deps|, even when used with |recursedeps|.
  534. 'src/repo4': '/repo_4',
  535. }
  536. }
  537. recursedeps = [
  538. 'src/repo4',
  539. 'src/repo8',
  540. ]""",
  541. 'origin': 'git/repo_9@1\n',
  542. })
  543. self._commit_git(
  544. 'repo_10', {
  545. 'DEPS': """
  546. gclient_gn_args_from = 'src/repo9'
  547. deps = {
  548. 'src/repo9': '/repo_9',
  549. # This entry should appear in flattened file,
  550. # but not recursed into, since it's not
  551. # in recursedeps.
  552. 'src/repo6': '/repo_6',
  553. }
  554. deps_os = {
  555. 'mac': {
  556. 'src/repo11': '/repo_11',
  557. },
  558. 'ios': {
  559. 'src/repo11': '/repo_11',
  560. }
  561. }
  562. recursedeps = [
  563. 'src/repo9',
  564. 'src/repo11',
  565. ]""",
  566. 'origin': 'git/repo_10@1\n',
  567. })
  568. self._commit_git(
  569. 'repo_11', {
  570. 'DEPS': """
  571. deps = {
  572. 'src/repo12': '/repo_12',
  573. }""",
  574. 'origin': 'git/repo_11@1\n',
  575. })
  576. self._commit_git('repo_12', {
  577. 'origin': 'git/repo_12@1\n',
  578. })
  579. self._fast_import_git(
  580. 'repo_12', """blob
  581. mark :1
  582. data 6
  583. Hello
  584. blob
  585. mark :2
  586. data 4
  587. Bye
  588. reset refs/changes/1212
  589. commit refs/changes/1212
  590. mark :3
  591. author Bob <bob@example.com> 1253744361 -0700
  592. committer Bob <bob@example.com> 1253744361 -0700
  593. data 8
  594. A and B
  595. M 100644 :1 a
  596. M 100644 :2 b
  597. """)
  598. self._commit_git(
  599. 'repo_13', {
  600. 'DEPS': """
  601. deps = {
  602. 'src/repo12': '/repo_12',
  603. }""",
  604. 'origin': 'git/repo_13@1\n',
  605. })
  606. self._commit_git(
  607. 'repo_13', {
  608. 'DEPS': """
  609. deps = {
  610. 'src/repo12': '/repo_12@refs/changes/1212',
  611. }""",
  612. 'origin': 'git/repo_13@2\n',
  613. })
  614. # src/repo12 is now a CIPD dependency.
  615. self._commit_git(
  616. 'repo_13', {
  617. 'DEPS': """
  618. deps = {
  619. 'src/repo12': {
  620. 'packages': [
  621. {
  622. 'package': 'foo',
  623. 'version': '1.3',
  624. },
  625. ],
  626. 'dep_type': 'cipd',
  627. },
  628. }
  629. hooks = [{
  630. # make sure src/repo12 exists and is a CIPD dir.
  631. 'action': ['python3', '-c', 'with open("src/repo12/_cipd"): pass'],
  632. }]
  633. """,
  634. 'origin': 'git/repo_13@3\n'
  635. })
  636. self._commit_git(
  637. 'repo_14', {
  638. 'DEPS':
  639. textwrap.dedent("""\
  640. vars = {}
  641. deps = {
  642. 'src/cipd_dep': {
  643. 'packages': [
  644. {
  645. 'package': 'package0',
  646. 'version': '0.1',
  647. },
  648. ],
  649. 'dep_type': 'cipd',
  650. },
  651. 'src/another_cipd_dep': {
  652. 'packages': [
  653. {
  654. 'package': 'package1',
  655. 'version': '1.1-cr0',
  656. },
  657. {
  658. 'package': 'package2',
  659. 'version': '1.13',
  660. },
  661. ],
  662. 'dep_type': 'cipd',
  663. },
  664. 'src/cipd_dep_with_cipd_variable': {
  665. 'packages': [
  666. {
  667. 'package': 'package3/${{platform}}',
  668. 'version': '1.2',
  669. },
  670. ],
  671. 'dep_type': 'cipd',
  672. },
  673. }"""),
  674. 'origin':
  675. 'git/repo_14@2\n'
  676. })
  677. # A repo with a hook to be recursed in, without use_relative_paths
  678. self._commit_git(
  679. 'repo_15', {
  680. 'DEPS':
  681. textwrap.dedent("""\
  682. hooks = [{
  683. "name": "absolute_cwd",
  684. "pattern": ".",
  685. "action": ["python3", "-c", "pass"]
  686. }]"""),
  687. 'origin':
  688. 'git/repo_15@2\n'
  689. })
  690. # A repo with a hook to be recursed in, with use_relative_paths
  691. self._commit_git(
  692. 'repo_16', {
  693. 'DEPS':
  694. textwrap.dedent("""\
  695. use_relative_paths=True
  696. hooks = [{
  697. "name": "relative_cwd",
  698. "pattern": ".",
  699. "action": ["python3", "relative.py"]
  700. }]"""),
  701. 'relative.py':
  702. 'pass',
  703. 'origin':
  704. 'git/repo_16@2\n'
  705. })
  706. # A repo with a gclient_gn_args_file and use_relative_paths
  707. self._commit_git(
  708. 'repo_17', {
  709. 'DEPS':
  710. textwrap.dedent("""\
  711. use_relative_paths=True
  712. vars = {
  713. 'toto': 'tata',
  714. }
  715. gclient_gn_args_file = 'repo17_gclient.args'
  716. gclient_gn_args = [
  717. 'toto',
  718. ]"""),
  719. 'origin':
  720. 'git/repo_17@2\n'
  721. })
  722. self._commit_git(
  723. 'repo_18', {
  724. 'DEPS':
  725. textwrap.dedent("""\
  726. deps = {
  727. 'src/cipd_dep': {
  728. 'packages': [
  729. {
  730. 'package': 'package0',
  731. 'version': 'package0-fake-tag:1.0',
  732. },
  733. {
  734. 'package': 'package0/${{platform}}',
  735. 'version': 'package0/${{platform}}-fake-tag:1.0',
  736. },
  737. {
  738. 'package': 'package1/another',
  739. 'version': 'package1/another-fake-tag:1.0',
  740. },
  741. ],
  742. 'dep_type': 'cipd',
  743. },
  744. }"""),
  745. 'origin':
  746. 'git/repo_18@2\n'
  747. })
  748. # a relative path repo
  749. self._commit_git(
  750. 'repo_19', {
  751. 'DEPS': """
  752. git_dependencies = "SUBMODULES"
  753. use_relative_paths = True
  754. vars = {
  755. 'foo_checkout': True,
  756. }
  757. deps = {
  758. "some_repo": {
  759. "url": '/repo_2@%(hash_2)s',
  760. "condition": "not foo_checkout",
  761. },
  762. "chicken/dickens": {
  763. "url": '/repo_3@%(hash_3)s',
  764. },
  765. "weird/deps": {
  766. "url": '/repo_1'
  767. },
  768. "bar": {
  769. "packages": [{
  770. "package": "lemur",
  771. "version": "version:1234",
  772. }],
  773. "dep_type": "cipd",
  774. },
  775. }
  776. recursedeps = [
  777. 'chicken/dickens',
  778. ]""" % {
  779. 'hash_2': self.git_hashes['repo_2'][1][0],
  780. 'hash_3': self.git_hashes['repo_3'][1][0],
  781. },
  782. })
  783. # a non-relative_path repo
  784. self._commit_git(
  785. 'repo_20', {
  786. 'DEPS': """
  787. git_dependencies = "SUBMODULES"
  788. vars = {
  789. 'foo_checkout': True,
  790. }
  791. deps = {
  792. "foo/some_repo": {
  793. "url": '/repo_2@%(hash_2)s',
  794. "condition": "not foo_checkout",
  795. },
  796. "foo/chicken/dickens": {
  797. "url": '/repo_3@%(hash_3)s',
  798. },
  799. "foo/weird/deps": {
  800. "url": '/repo_1'
  801. },
  802. "foo/bar": {
  803. "packages": [{
  804. "package": "lemur",
  805. "version": "version:1234",
  806. }],
  807. "dep_type": "cipd",
  808. },
  809. }
  810. recursedeps = [
  811. 'foo/chicken/dickens',
  812. ]
  813. """ % {
  814. 'hash_2': self.git_hashes['repo_2'][1][0],
  815. 'hash_3': self.git_hashes['repo_3'][1][0],
  816. },
  817. })
  818. # gitmodules already present, test migration, .git suffix
  819. self._commit_git(
  820. 'repo_21',
  821. {
  822. 'DEPS':
  823. """
  824. use_relative_paths = True
  825. git_dependencies = "SYNC"
  826. deps = {
  827. "bar": {
  828. "url": 'https://example.googlesource.com/repo.git@%(hash)s',
  829. },
  830. }""" % {
  831. 'hash': self.git_hashes['repo_2'][1][0],
  832. },
  833. '.gitmodules':
  834. """
  835. [submodule "bar"]
  836. path = bar
  837. url = invalid/repo_url.git"""
  838. },
  839. )
  840. self._commit_git(
  841. 'repo_22', {
  842. 'DEPS':
  843. textwrap.dedent("""\
  844. vars = {}
  845. deps = {
  846. 'src/gcs_dep': {
  847. 'bucket': '123bucket',
  848. 'dep_type': 'gcs',
  849. 'objects': [{
  850. 'object_name': 'deadbeef',
  851. 'sha256sum': 'abcd123',
  852. 'size_bytes': 10000,
  853. 'generation': 1542380408102454,
  854. }]
  855. },
  856. 'src/another_gcs_dep': {
  857. 'bucket': '456bucket',
  858. 'dep_type': 'gcs',
  859. 'objects': [{
  860. 'object_name': 'Linux/llvmfile.tar.gz',
  861. 'sha256sum': 'abcd123',
  862. 'size_bytes': 10000,
  863. 'generation': 1542380408102455,
  864. }]
  865. },
  866. 'src/gcs_dep_with_output_file': {
  867. 'bucket': '789bucket',
  868. 'dep_type': 'gcs',
  869. 'objects': [{
  870. 'object_name': 'clang-format-version123',
  871. 'sha256sum': 'abcd123',
  872. 'output_file': 'clang-format-no-extract',
  873. 'size_bytes': 10000,
  874. 'generation': 1542380408102456,
  875. }]
  876. },
  877. 'src/gcs_dep_ignored': {
  878. 'bucket': '789bucket',
  879. 'dep_type': 'gcs',
  880. 'objects': [{
  881. 'object_name': 'foobar',
  882. 'sha256sum': 'abcd124',
  883. 'size_bytes': 10000,
  884. 'generation': 1542380408102457,
  885. 'condition': 'False',
  886. }]
  887. },
  888. }"""),
  889. 'origin':
  890. 'git/repo_22@1\n'
  891. })
  892. self._commit_git(
  893. 'repo_23', {
  894. 'DEPS': """
  895. deps = {
  896. 'src/repo12': '/repo_12',
  897. }""",
  898. 'origin': 'git/repo_23@1\n',
  899. })
  900. self._commit_git(
  901. 'repo_23', {
  902. 'DEPS': """
  903. deps = {
  904. 'src/repo12': '/repo_12@refs/changes/1212',
  905. }""",
  906. 'origin': 'git/repo_23@2\n',
  907. })
  908. # src/repo12 is now a GCS dependency.
  909. self._commit_git(
  910. 'repo_23', {
  911. 'DEPS': """
  912. deps = {
  913. 'src/repo12': {
  914. 'bucket': 'bucket123',
  915. 'dep_type': 'gcs',
  916. 'objects': [{
  917. 'object_name': 'path_to_file.tar.gz',
  918. 'sha256sum': 'abcd123',
  919. 'size_bytes': 10000,
  920. 'generation': 1542380408102454,
  921. }]
  922. },
  923. }
  924. """,
  925. 'origin': 'git/repo_23@3\n'
  926. })
  927. # gitmodules already present, test migration, gclient-recursedeps
  928. self._commit_git(
  929. 'repo_24',
  930. {
  931. 'DEPS':
  932. """
  933. use_relative_paths = True
  934. git_dependencies = "SYNC"
  935. deps = {
  936. "bar": {
  937. "url": 'https://example.googlesource.com/repo@%(hash)s',
  938. },
  939. }
  940. recursedeps = [
  941. 'bar',
  942. ]""" % {
  943. 'hash': self.git_hashes['repo_2'][1][0],
  944. },
  945. '.gitmodules':
  946. """
  947. [submodule "bar"]
  948. path = bar
  949. url = https://example.googlesource.com/repo"""
  950. },
  951. ) # Update `NB_GIT_REPOS` if you add more repos.
  952. class FakeRepoSkiaDEPS(FakeReposBase):
  953. """Simulates the Skia DEPS transition in Chrome."""
  954. NB_GIT_REPOS = 5
  955. DEPS_git_pre = """deps = {
  956. 'src/third_party/skia/gyp': %(git_base)r + 'repo_3',
  957. 'src/third_party/skia/include': %(git_base)r + 'repo_4',
  958. 'src/third_party/skia/src': %(git_base)r + 'repo_5',
  959. }"""
  960. DEPS_post = """deps = {
  961. 'src/third_party/skia': %(git_base)r + 'repo_1',
  962. }"""
  963. def populateGit(self):
  964. # Skia repo.
  965. self._commit_git(
  966. 'repo_1', {
  967. 'skia_base_file': 'root-level file.',
  968. 'gyp/gyp_file': 'file in the gyp directory',
  969. 'include/include_file': 'file in the include directory',
  970. 'src/src_file': 'file in the src directory',
  971. })
  972. self._commit_git(
  973. 'repo_3',
  974. { # skia/gyp
  975. 'gyp_file': 'file in the gyp directory',
  976. })
  977. self._commit_git('repo_4', { # skia/include
  978. 'include_file': 'file in the include directory',
  979. })
  980. self._commit_git(
  981. 'repo_5',
  982. { # skia/src
  983. 'src_file': 'file in the src directory',
  984. })
  985. # Chrome repo.
  986. self._commit_git(
  987. 'repo_2', {
  988. 'DEPS': self.DEPS_git_pre % {
  989. 'git_base': self.git_base
  990. },
  991. 'myfile': 'src/trunk/src@1'
  992. })
  993. self._commit_git(
  994. 'repo_2', {
  995. 'DEPS': self.DEPS_post % {
  996. 'git_base': self.git_base
  997. },
  998. 'myfile': 'src/trunk/src@2'
  999. })
  1000. class FakeRepoBlinkDEPS(FakeReposBase):
  1001. """Simulates the Blink DEPS transition in Chrome."""
  1002. NB_GIT_REPOS = 2
  1003. DEPS_pre = 'deps = {"src/third_party/WebKit": "%(git_base)srepo_2",}'
  1004. DEPS_post = 'deps = {}'
  1005. def populateGit(self):
  1006. # Blink repo.
  1007. self._commit_git(
  1008. 'repo_2', {
  1009. 'OWNERS': 'OWNERS-pre',
  1010. 'Source/exists_always': '_ignored_',
  1011. 'Source/exists_before_but_not_after': '_ignored_',
  1012. })
  1013. # Chrome repo.
  1014. self._commit_git(
  1015. 'repo_1', {
  1016. 'DEPS': self.DEPS_pre % {
  1017. 'git_base': self.git_base
  1018. },
  1019. 'myfile': 'myfile@1',
  1020. '.gitignore': '/third_party/WebKit',
  1021. })
  1022. self._commit_git(
  1023. 'repo_1', {
  1024. 'DEPS': self.DEPS_post % {
  1025. 'git_base': self.git_base
  1026. },
  1027. 'myfile': 'myfile@2',
  1028. '.gitignore': '',
  1029. 'third_party/WebKit/OWNERS': 'OWNERS-post',
  1030. 'third_party/WebKit/Source/exists_always': '_ignored_',
  1031. 'third_party/WebKit/Source/exists_after_but_not_before':
  1032. '_ignored',
  1033. })
  1034. def populateSvn(self):
  1035. raise NotImplementedError()
  1036. class FakeRepoNoSyncDEPS(FakeReposBase):
  1037. """Simulates a repo with some DEPS changes."""
  1038. NB_GIT_REPOS = 2
  1039. def populateGit(self):
  1040. self._commit_git('repo_2', {'myfile': 'then egg'})
  1041. self._commit_git('repo_2', {'myfile': 'before egg!'})
  1042. self._commit_git(
  1043. 'repo_1', {
  1044. 'DEPS':
  1045. textwrap.dedent(
  1046. """\
  1047. deps = {
  1048. 'src/repo2': {
  1049. 'url': %(git_base)r + 'repo_2@%(repo2hash)s',
  1050. },
  1051. }""" % {
  1052. 'git_base': self.git_base,
  1053. 'repo2hash': self.git_hashes['repo_2'][1][0][:7]
  1054. })
  1055. })
  1056. self._commit_git(
  1057. 'repo_1', {
  1058. 'DEPS':
  1059. textwrap.dedent(
  1060. """\
  1061. deps = {
  1062. 'src/repo2': {
  1063. 'url': %(git_base)r + 'repo_2@%(repo2hash)s',
  1064. },
  1065. }""" % {
  1066. 'git_base': self.git_base,
  1067. 'repo2hash': self.git_hashes['repo_2'][2][0][:7]
  1068. })
  1069. })
  1070. self._commit_git(
  1071. 'repo_1', {
  1072. 'foo_file':
  1073. 'chicken content',
  1074. 'DEPS':
  1075. textwrap.dedent(
  1076. """\
  1077. deps = {
  1078. 'src/repo2': {
  1079. 'url': %(git_base)r + 'repo_2@%(repo2hash)s',
  1080. },
  1081. }""" % {
  1082. 'git_base': self.git_base,
  1083. 'repo2hash': self.git_hashes['repo_2'][1][0][:7]
  1084. })
  1085. })
  1086. self._commit_git('repo_1', {'foo_file': 'chicken content@4'})
  1087. class FakeReposTestBase(trial_dir.TestCase):
  1088. """This is vaguely inspired by twisted."""
  1089. # Static FakeRepos instances. Lazy loaded.
  1090. CACHED_FAKE_REPOS = {}
  1091. # Override if necessary.
  1092. FAKE_REPOS_CLASS = FakeRepos
  1093. def setUp(self):
  1094. super(FakeReposTestBase, self).setUp()
  1095. if not self.FAKE_REPOS_CLASS in self.CACHED_FAKE_REPOS:
  1096. self.CACHED_FAKE_REPOS[
  1097. self.FAKE_REPOS_CLASS] = self.FAKE_REPOS_CLASS()
  1098. self.FAKE_REPOS = self.CACHED_FAKE_REPOS[self.FAKE_REPOS_CLASS]
  1099. # No need to call self.FAKE_REPOS.setUp(), it will be called by the
  1100. # child class. Do not define tearDown(), since super's version does the
  1101. # right thing and self.FAKE_REPOS is kept across tests.
  1102. @property
  1103. def git_base(self):
  1104. """Shortcut."""
  1105. return self.FAKE_REPOS.git_base
  1106. def checkString(self, expected, result, msg=None):
  1107. """Prints the diffs to ease debugging."""
  1108. self.assertEqual(expected.splitlines(), result.splitlines(), msg)
  1109. if expected != result:
  1110. # Strip the beginning
  1111. while expected and result and expected[0] == result[0]:
  1112. expected = expected[1:]
  1113. result = result[1:]
  1114. # The exception trace makes it hard to read so dump it too.
  1115. if '\n' in result:
  1116. print(result)
  1117. self.assertEqual(expected, result, msg)
  1118. def check(self, expected, results):
  1119. """Checks stdout, stderr, returncode."""
  1120. self.checkString(expected[0], results[0])
  1121. self.checkString(expected[1], results[1])
  1122. self.assertEqual(expected[2], results[2])
  1123. def assertTree(self, tree, tree_root=None):
  1124. """Diff the checkout tree with a dict."""
  1125. if not tree_root:
  1126. tree_root = self.root_dir
  1127. actual = read_tree(tree_root)
  1128. self.assertEqual(sorted(tree.keys()), sorted(actual.keys()))
  1129. self.assertEqual(tree, actual)
  1130. def mangle_git_tree(self, *args):
  1131. """Creates a 'virtual directory snapshot' to compare with the actual
  1132. result on disk."""
  1133. result = {}
  1134. for item, new_root in args:
  1135. repo, rev = item.split('@', 1)
  1136. tree = self.gittree(repo, rev)
  1137. for k, v in tree.items():
  1138. path = join(new_root, k).replace(os.sep, '/')
  1139. result[path] = v
  1140. return result
  1141. def githash(self, repo, rev):
  1142. """Sort-hand: Returns the hash for a git 'revision'."""
  1143. return self.FAKE_REPOS.git_hashes[repo][int(rev)][0]
  1144. def gittree(self, repo, rev):
  1145. """Sort-hand: returns the directory tree for a git 'revision'."""
  1146. return self.FAKE_REPOS.git_hashes[repo][int(rev)][1]
  1147. def gitrevparse(self, repo):
  1148. """Returns the actual revision for a given repo."""
  1149. return self.FAKE_REPOS._git_rev_parse(repo).decode('utf-8')
  1150. def main(argv):
  1151. fake = FakeRepos()
  1152. print('Using %s' % fake.root_dir)
  1153. try:
  1154. fake.set_up_git()
  1155. print(
  1156. 'Fake setup, press enter to quit or Ctrl-C to keep the checkouts.')
  1157. sys.stdin.readline()
  1158. except KeyboardInterrupt:
  1159. trial_dir.TrialDir.SHOULD_LEAK.leak = True
  1160. return 0
  1161. if __name__ == '__main__':
  1162. sys.exit(main(sys.argv))