test_project.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. # -*- coding:utf-8 -*-
  2. #
  3. # Copyright (C) 2019 The Android Open Source Project
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. """Unittests for the project.py module."""
  17. from __future__ import print_function
  18. import contextlib
  19. import os
  20. import shutil
  21. import subprocess
  22. import tempfile
  23. import unittest
  24. import error
  25. import git_command
  26. import git_config
  27. import platform_utils
  28. import project
  29. @contextlib.contextmanager
  30. def TempGitTree():
  31. """Create a new empty git checkout for testing."""
  32. # TODO(vapier): Convert this to tempfile.TemporaryDirectory once we drop
  33. # Python 2 support entirely.
  34. try:
  35. tempdir = tempfile.mkdtemp(prefix='repo-tests')
  36. # Tests need to assume, that main is default branch at init,
  37. # which is not supported in config until 2.28.
  38. cmd = ['git', 'init']
  39. if git_command.git_require((2, 28, 0)):
  40. cmd += ['--initial-branch=main']
  41. else:
  42. # Use template dir for init.
  43. templatedir = tempfile.mkdtemp(prefix='.test-template')
  44. with open(os.path.join(templatedir, 'HEAD'), 'w') as fp:
  45. fp.write('ref: refs/heads/main\n')
  46. cmd += ['--template=', templatedir]
  47. subprocess.check_call(cmd, cwd=tempdir)
  48. yield tempdir
  49. finally:
  50. platform_utils.rmtree(tempdir)
  51. class FakeProject(object):
  52. """A fake for Project for basic functionality."""
  53. def __init__(self, worktree):
  54. self.worktree = worktree
  55. self.gitdir = os.path.join(worktree, '.git')
  56. self.name = 'fakeproject'
  57. self.work_git = project.Project._GitGetByExec(
  58. self, bare=False, gitdir=self.gitdir)
  59. self.bare_git = project.Project._GitGetByExec(
  60. self, bare=True, gitdir=self.gitdir)
  61. self.config = git_config.GitConfig.ForRepository(gitdir=self.gitdir)
  62. class ReviewableBranchTests(unittest.TestCase):
  63. """Check ReviewableBranch behavior."""
  64. def test_smoke(self):
  65. """A quick run through everything."""
  66. with TempGitTree() as tempdir:
  67. fakeproj = FakeProject(tempdir)
  68. # Generate some commits.
  69. with open(os.path.join(tempdir, 'readme'), 'w') as fp:
  70. fp.write('txt')
  71. fakeproj.work_git.add('readme')
  72. fakeproj.work_git.commit('-mAdd file')
  73. fakeproj.work_git.checkout('-b', 'work')
  74. fakeproj.work_git.rm('-f', 'readme')
  75. fakeproj.work_git.commit('-mDel file')
  76. # Start off with the normal details.
  77. rb = project.ReviewableBranch(
  78. fakeproj, fakeproj.config.GetBranch('work'), 'main')
  79. self.assertEqual('work', rb.name)
  80. self.assertEqual(1, len(rb.commits))
  81. self.assertIn('Del file', rb.commits[0])
  82. d = rb.unabbrev_commits
  83. self.assertEqual(1, len(d))
  84. short, long = next(iter(d.items()))
  85. self.assertTrue(long.startswith(short))
  86. self.assertTrue(rb.base_exists)
  87. # Hard to assert anything useful about this.
  88. self.assertTrue(rb.date)
  89. # Now delete the tracking branch!
  90. fakeproj.work_git.branch('-D', 'main')
  91. rb = project.ReviewableBranch(
  92. fakeproj, fakeproj.config.GetBranch('work'), 'main')
  93. self.assertEqual(0, len(rb.commits))
  94. self.assertFalse(rb.base_exists)
  95. # Hard to assert anything useful about this.
  96. self.assertTrue(rb.date)
  97. class CopyLinkTestCase(unittest.TestCase):
  98. """TestCase for stub repo client checkouts.
  99. It'll have a layout like:
  100. tempdir/ # self.tempdir
  101. checkout/ # self.topdir
  102. git-project/ # self.worktree
  103. Attributes:
  104. tempdir: A dedicated temporary directory.
  105. worktree: The top of the repo client checkout.
  106. topdir: The top of a project checkout.
  107. """
  108. def setUp(self):
  109. self.tempdir = tempfile.mkdtemp(prefix='repo_tests')
  110. self.topdir = os.path.join(self.tempdir, 'checkout')
  111. self.worktree = os.path.join(self.topdir, 'git-project')
  112. os.makedirs(self.topdir)
  113. os.makedirs(self.worktree)
  114. def tearDown(self):
  115. shutil.rmtree(self.tempdir, ignore_errors=True)
  116. @staticmethod
  117. def touch(path):
  118. with open(path, 'w'):
  119. pass
  120. def assertExists(self, path, msg=None):
  121. """Make sure |path| exists."""
  122. if os.path.exists(path):
  123. return
  124. if msg is None:
  125. msg = ['path is missing: %s' % path]
  126. while path != '/':
  127. path = os.path.dirname(path)
  128. if not path:
  129. # If we're given something like "foo", abort once we get to "".
  130. break
  131. result = os.path.exists(path)
  132. msg.append('\tos.path.exists(%s): %s' % (path, result))
  133. if result:
  134. msg.append('\tcontents: %r' % os.listdir(path))
  135. break
  136. msg = '\n'.join(msg)
  137. raise self.failureException(msg)
  138. class CopyFile(CopyLinkTestCase):
  139. """Check _CopyFile handling."""
  140. def CopyFile(self, src, dest):
  141. return project._CopyFile(self.worktree, src, self.topdir, dest)
  142. def test_basic(self):
  143. """Basic test of copying a file from a project to the toplevel."""
  144. src = os.path.join(self.worktree, 'foo.txt')
  145. self.touch(src)
  146. cf = self.CopyFile('foo.txt', 'foo')
  147. cf._Copy()
  148. self.assertExists(os.path.join(self.topdir, 'foo'))
  149. def test_src_subdir(self):
  150. """Copy a file from a subdir of a project."""
  151. src = os.path.join(self.worktree, 'bar', 'foo.txt')
  152. os.makedirs(os.path.dirname(src))
  153. self.touch(src)
  154. cf = self.CopyFile('bar/foo.txt', 'new.txt')
  155. cf._Copy()
  156. self.assertExists(os.path.join(self.topdir, 'new.txt'))
  157. def test_dest_subdir(self):
  158. """Copy a file to a subdir of a checkout."""
  159. src = os.path.join(self.worktree, 'foo.txt')
  160. self.touch(src)
  161. cf = self.CopyFile('foo.txt', 'sub/dir/new.txt')
  162. self.assertFalse(os.path.exists(os.path.join(self.topdir, 'sub')))
  163. cf._Copy()
  164. self.assertExists(os.path.join(self.topdir, 'sub', 'dir', 'new.txt'))
  165. def test_update(self):
  166. """Make sure changed files get copied again."""
  167. src = os.path.join(self.worktree, 'foo.txt')
  168. dest = os.path.join(self.topdir, 'bar')
  169. with open(src, 'w') as f:
  170. f.write('1st')
  171. cf = self.CopyFile('foo.txt', 'bar')
  172. cf._Copy()
  173. self.assertExists(dest)
  174. with open(dest) as f:
  175. self.assertEqual(f.read(), '1st')
  176. with open(src, 'w') as f:
  177. f.write('2nd!')
  178. cf._Copy()
  179. with open(dest) as f:
  180. self.assertEqual(f.read(), '2nd!')
  181. def test_src_block_symlink(self):
  182. """Do not allow reading from a symlinked path."""
  183. src = os.path.join(self.worktree, 'foo.txt')
  184. sym = os.path.join(self.worktree, 'sym')
  185. self.touch(src)
  186. platform_utils.symlink('foo.txt', sym)
  187. self.assertExists(sym)
  188. cf = self.CopyFile('sym', 'foo')
  189. self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
  190. def test_src_block_symlink_traversal(self):
  191. """Do not allow reading through a symlink dir."""
  192. realfile = os.path.join(self.tempdir, 'file.txt')
  193. self.touch(realfile)
  194. src = os.path.join(self.worktree, 'bar', 'file.txt')
  195. platform_utils.symlink(self.tempdir, os.path.join(self.worktree, 'bar'))
  196. self.assertExists(src)
  197. cf = self.CopyFile('bar/file.txt', 'foo')
  198. self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
  199. def test_src_block_copy_from_dir(self):
  200. """Do not allow copying from a directory."""
  201. src = os.path.join(self.worktree, 'dir')
  202. os.makedirs(src)
  203. cf = self.CopyFile('dir', 'foo')
  204. self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
  205. def test_dest_block_symlink(self):
  206. """Do not allow writing to a symlink."""
  207. src = os.path.join(self.worktree, 'foo.txt')
  208. self.touch(src)
  209. platform_utils.symlink('dest', os.path.join(self.topdir, 'sym'))
  210. cf = self.CopyFile('foo.txt', 'sym')
  211. self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
  212. def test_dest_block_symlink_traversal(self):
  213. """Do not allow writing through a symlink dir."""
  214. src = os.path.join(self.worktree, 'foo.txt')
  215. self.touch(src)
  216. platform_utils.symlink(tempfile.gettempdir(),
  217. os.path.join(self.topdir, 'sym'))
  218. cf = self.CopyFile('foo.txt', 'sym/foo.txt')
  219. self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
  220. def test_src_block_copy_to_dir(self):
  221. """Do not allow copying to a directory."""
  222. src = os.path.join(self.worktree, 'foo.txt')
  223. self.touch(src)
  224. os.makedirs(os.path.join(self.topdir, 'dir'))
  225. cf = self.CopyFile('foo.txt', 'dir')
  226. self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
  227. class LinkFile(CopyLinkTestCase):
  228. """Check _LinkFile handling."""
  229. def LinkFile(self, src, dest):
  230. return project._LinkFile(self.worktree, src, self.topdir, dest)
  231. def test_basic(self):
  232. """Basic test of linking a file from a project into the toplevel."""
  233. src = os.path.join(self.worktree, 'foo.txt')
  234. self.touch(src)
  235. lf = self.LinkFile('foo.txt', 'foo')
  236. lf._Link()
  237. dest = os.path.join(self.topdir, 'foo')
  238. self.assertExists(dest)
  239. self.assertTrue(os.path.islink(dest))
  240. self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest))
  241. def test_src_subdir(self):
  242. """Link to a file in a subdir of a project."""
  243. src = os.path.join(self.worktree, 'bar', 'foo.txt')
  244. os.makedirs(os.path.dirname(src))
  245. self.touch(src)
  246. lf = self.LinkFile('bar/foo.txt', 'foo')
  247. lf._Link()
  248. self.assertExists(os.path.join(self.topdir, 'foo'))
  249. def test_src_self(self):
  250. """Link to the project itself."""
  251. dest = os.path.join(self.topdir, 'foo', 'bar')
  252. lf = self.LinkFile('.', 'foo/bar')
  253. lf._Link()
  254. self.assertExists(dest)
  255. self.assertEqual(os.path.join('..', 'git-project'), os.readlink(dest))
  256. def test_dest_subdir(self):
  257. """Link a file to a subdir of a checkout."""
  258. src = os.path.join(self.worktree, 'foo.txt')
  259. self.touch(src)
  260. lf = self.LinkFile('foo.txt', 'sub/dir/foo/bar')
  261. self.assertFalse(os.path.exists(os.path.join(self.topdir, 'sub')))
  262. lf._Link()
  263. self.assertExists(os.path.join(self.topdir, 'sub', 'dir', 'foo', 'bar'))
  264. def test_src_block_relative(self):
  265. """Do not allow relative symlinks."""
  266. BAD_SOURCES = (
  267. './',
  268. '..',
  269. '../',
  270. 'foo/.',
  271. 'foo/./bar',
  272. 'foo/..',
  273. 'foo/../foo',
  274. )
  275. for src in BAD_SOURCES:
  276. lf = self.LinkFile(src, 'foo')
  277. self.assertRaises(error.ManifestInvalidPathError, lf._Link)
  278. def test_update(self):
  279. """Make sure changed targets get updated."""
  280. dest = os.path.join(self.topdir, 'sym')
  281. src = os.path.join(self.worktree, 'foo.txt')
  282. self.touch(src)
  283. lf = self.LinkFile('foo.txt', 'sym')
  284. lf._Link()
  285. self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest))
  286. # Point the symlink somewhere else.
  287. os.unlink(dest)
  288. platform_utils.symlink(self.tempdir, dest)
  289. lf._Link()
  290. self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest))