test_project.py 12 KB

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