test_project.py 11 KB

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