blob: 02285e2faa4b806711d9eb7cc3b7921a0870b7a9 [file] [log] [blame]
Mike Frysingerf7c51602019-06-18 17:23:39 -04001# -*- 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
Mike Frysinger87deaef2019-07-26 21:14:55 -040017"""Unittests for the project.py module."""
18
19from __future__ import print_function
20
Mike Frysinger6da17752019-09-11 18:43:17 -040021import contextlib
22import os
23import shutil
24import subprocess
25import tempfile
Mike Frysingerf7c51602019-06-18 17:23:39 -040026import unittest
27
Mike Frysingere6a202f2019-08-02 15:57:57 -040028import error
Fredrik de Groot6342d562020-12-01 15:58:53 +010029import git_command
Mike Frysinger6da17752019-09-11 18:43:17 -040030import git_config
Mike Frysingerd9254592020-02-19 22:36:26 -050031import platform_utils
Mike Frysingerf7c51602019-06-18 17:23:39 -040032import project
33
34
Mike Frysinger6da17752019-09-11 18:43:17 -040035@contextlib.contextmanager
36def TempGitTree():
37 """Create a new empty git checkout for testing."""
38 # TODO(vapier): Convert this to tempfile.TemporaryDirectory once we drop
39 # Python 2 support entirely.
40 try:
41 tempdir = tempfile.mkdtemp(prefix='repo-tests')
Fredrik de Groot6342d562020-12-01 15:58:53 +010042
43 # Tests need to assume, that main is default branch at init,
44 # which is not supported in config until 2.28.
45 cmd = ['git', 'init']
46 if git_command.git_require((2, 28, 0)):
47 cmd += ['--initial-branch=main']
48 else:
49 # Use template dir for init.
50 templatedir = tempfile.mkdtemp(prefix='.test-template')
51 with open(os.path.join(templatedir, 'HEAD'), 'w') as fp:
52 fp.write('ref: refs/heads/main\n')
53 cmd += ['--template=', templatedir]
54 subprocess.check_call(cmd, cwd=tempdir)
Mike Frysinger6da17752019-09-11 18:43:17 -040055 yield tempdir
56 finally:
Mike Frysingerd9254592020-02-19 22:36:26 -050057 platform_utils.rmtree(tempdir)
Mike Frysinger6da17752019-09-11 18:43:17 -040058
59
Mike Frysinger6da17752019-09-11 18:43:17 -040060class FakeProject(object):
61 """A fake for Project for basic functionality."""
62
63 def __init__(self, worktree):
64 self.worktree = worktree
65 self.gitdir = os.path.join(worktree, '.git')
66 self.name = 'fakeproject'
67 self.work_git = project.Project._GitGetByExec(
68 self, bare=False, gitdir=self.gitdir)
69 self.bare_git = project.Project._GitGetByExec(
70 self, bare=True, gitdir=self.gitdir)
71 self.config = git_config.GitConfig.ForRepository(gitdir=self.gitdir)
72
73
74class ReviewableBranchTests(unittest.TestCase):
75 """Check ReviewableBranch behavior."""
76
77 def test_smoke(self):
78 """A quick run through everything."""
79 with TempGitTree() as tempdir:
80 fakeproj = FakeProject(tempdir)
81
82 # Generate some commits.
83 with open(os.path.join(tempdir, 'readme'), 'w') as fp:
84 fp.write('txt')
85 fakeproj.work_git.add('readme')
86 fakeproj.work_git.commit('-mAdd file')
87 fakeproj.work_git.checkout('-b', 'work')
88 fakeproj.work_git.rm('-f', 'readme')
89 fakeproj.work_git.commit('-mDel file')
90
91 # Start off with the normal details.
92 rb = project.ReviewableBranch(
Mike Frysingere283b952020-11-16 22:56:35 -050093 fakeproj, fakeproj.config.GetBranch('work'), 'main')
Mike Frysinger6da17752019-09-11 18:43:17 -040094 self.assertEqual('work', rb.name)
95 self.assertEqual(1, len(rb.commits))
96 self.assertIn('Del file', rb.commits[0])
97 d = rb.unabbrev_commits
98 self.assertEqual(1, len(d))
99 short, long = next(iter(d.items()))
100 self.assertTrue(long.startswith(short))
101 self.assertTrue(rb.base_exists)
102 # Hard to assert anything useful about this.
103 self.assertTrue(rb.date)
104
105 # Now delete the tracking branch!
Mike Frysingere283b952020-11-16 22:56:35 -0500106 fakeproj.work_git.branch('-D', 'main')
Mike Frysinger6da17752019-09-11 18:43:17 -0400107 rb = project.ReviewableBranch(
Mike Frysingere283b952020-11-16 22:56:35 -0500108 fakeproj, fakeproj.config.GetBranch('work'), 'main')
Mike Frysinger6da17752019-09-11 18:43:17 -0400109 self.assertEqual(0, len(rb.commits))
110 self.assertFalse(rb.base_exists)
111 # Hard to assert anything useful about this.
112 self.assertTrue(rb.date)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400113
114
115class CopyLinkTestCase(unittest.TestCase):
116 """TestCase for stub repo client checkouts.
117
118 It'll have a layout like:
119 tempdir/ # self.tempdir
120 checkout/ # self.topdir
121 git-project/ # self.worktree
122
123 Attributes:
124 tempdir: A dedicated temporary directory.
125 worktree: The top of the repo client checkout.
126 topdir: The top of a project checkout.
127 """
128
129 def setUp(self):
130 self.tempdir = tempfile.mkdtemp(prefix='repo_tests')
131 self.topdir = os.path.join(self.tempdir, 'checkout')
132 self.worktree = os.path.join(self.topdir, 'git-project')
133 os.makedirs(self.topdir)
134 os.makedirs(self.worktree)
135
136 def tearDown(self):
137 shutil.rmtree(self.tempdir, ignore_errors=True)
138
139 @staticmethod
140 def touch(path):
David Pursehouse348e2182020-02-12 11:36:14 +0900141 with open(path, 'w'):
Mike Frysingere6a202f2019-08-02 15:57:57 -0400142 pass
143
144 def assertExists(self, path, msg=None):
145 """Make sure |path| exists."""
146 if os.path.exists(path):
147 return
148
149 if msg is None:
150 msg = ['path is missing: %s' % path]
151 while path != '/':
152 path = os.path.dirname(path)
153 if not path:
154 # If we're given something like "foo", abort once we get to "".
155 break
156 result = os.path.exists(path)
157 msg.append('\tos.path.exists(%s): %s' % (path, result))
158 if result:
159 msg.append('\tcontents: %r' % os.listdir(path))
160 break
161 msg = '\n'.join(msg)
162
163 raise self.failureException(msg)
164
165
166class CopyFile(CopyLinkTestCase):
167 """Check _CopyFile handling."""
168
169 def CopyFile(self, src, dest):
170 return project._CopyFile(self.worktree, src, self.topdir, dest)
171
172 def test_basic(self):
173 """Basic test of copying a file from a project to the toplevel."""
174 src = os.path.join(self.worktree, 'foo.txt')
175 self.touch(src)
176 cf = self.CopyFile('foo.txt', 'foo')
177 cf._Copy()
178 self.assertExists(os.path.join(self.topdir, 'foo'))
179
180 def test_src_subdir(self):
181 """Copy a file from a subdir of a project."""
182 src = os.path.join(self.worktree, 'bar', 'foo.txt')
183 os.makedirs(os.path.dirname(src))
184 self.touch(src)
185 cf = self.CopyFile('bar/foo.txt', 'new.txt')
186 cf._Copy()
187 self.assertExists(os.path.join(self.topdir, 'new.txt'))
188
189 def test_dest_subdir(self):
190 """Copy a file to a subdir of a checkout."""
191 src = os.path.join(self.worktree, 'foo.txt')
192 self.touch(src)
193 cf = self.CopyFile('foo.txt', 'sub/dir/new.txt')
194 self.assertFalse(os.path.exists(os.path.join(self.topdir, 'sub')))
195 cf._Copy()
196 self.assertExists(os.path.join(self.topdir, 'sub', 'dir', 'new.txt'))
197
198 def test_update(self):
199 """Make sure changed files get copied again."""
200 src = os.path.join(self.worktree, 'foo.txt')
201 dest = os.path.join(self.topdir, 'bar')
202 with open(src, 'w') as f:
203 f.write('1st')
204 cf = self.CopyFile('foo.txt', 'bar')
205 cf._Copy()
206 self.assertExists(dest)
207 with open(dest) as f:
208 self.assertEqual(f.read(), '1st')
209
210 with open(src, 'w') as f:
211 f.write('2nd!')
212 cf._Copy()
213 with open(dest) as f:
214 self.assertEqual(f.read(), '2nd!')
215
216 def test_src_block_symlink(self):
217 """Do not allow reading from a symlinked path."""
218 src = os.path.join(self.worktree, 'foo.txt')
219 sym = os.path.join(self.worktree, 'sym')
220 self.touch(src)
Mike Frysingerd9254592020-02-19 22:36:26 -0500221 platform_utils.symlink('foo.txt', sym)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400222 self.assertExists(sym)
223 cf = self.CopyFile('sym', 'foo')
224 self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
225
226 def test_src_block_symlink_traversal(self):
227 """Do not allow reading through a symlink dir."""
Mike Frysingerd9254592020-02-19 22:36:26 -0500228 realfile = os.path.join(self.tempdir, 'file.txt')
229 self.touch(realfile)
230 src = os.path.join(self.worktree, 'bar', 'file.txt')
231 platform_utils.symlink(self.tempdir, os.path.join(self.worktree, 'bar'))
Mike Frysingere6a202f2019-08-02 15:57:57 -0400232 self.assertExists(src)
Mike Frysingerd9254592020-02-19 22:36:26 -0500233 cf = self.CopyFile('bar/file.txt', 'foo')
Mike Frysingere6a202f2019-08-02 15:57:57 -0400234 self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
235
David Pursehouse4bbba7d2020-02-12 11:14:55 +0900236 def test_src_block_copy_from_dir(self):
Mike Frysingere6a202f2019-08-02 15:57:57 -0400237 """Do not allow copying from a directory."""
238 src = os.path.join(self.worktree, 'dir')
239 os.makedirs(src)
240 cf = self.CopyFile('dir', 'foo')
241 self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
242
243 def test_dest_block_symlink(self):
244 """Do not allow writing to a symlink."""
245 src = os.path.join(self.worktree, 'foo.txt')
246 self.touch(src)
Mike Frysingerd9254592020-02-19 22:36:26 -0500247 platform_utils.symlink('dest', os.path.join(self.topdir, 'sym'))
Mike Frysingere6a202f2019-08-02 15:57:57 -0400248 cf = self.CopyFile('foo.txt', 'sym')
249 self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
250
251 def test_dest_block_symlink_traversal(self):
252 """Do not allow writing through a symlink dir."""
253 src = os.path.join(self.worktree, 'foo.txt')
254 self.touch(src)
Mike Frysingerd9254592020-02-19 22:36:26 -0500255 platform_utils.symlink(tempfile.gettempdir(),
256 os.path.join(self.topdir, 'sym'))
Mike Frysingere6a202f2019-08-02 15:57:57 -0400257 cf = self.CopyFile('foo.txt', 'sym/foo.txt')
258 self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
259
David Pursehouse4bbba7d2020-02-12 11:14:55 +0900260 def test_src_block_copy_to_dir(self):
Mike Frysingere6a202f2019-08-02 15:57:57 -0400261 """Do not allow copying to a directory."""
262 src = os.path.join(self.worktree, 'foo.txt')
263 self.touch(src)
264 os.makedirs(os.path.join(self.topdir, 'dir'))
265 cf = self.CopyFile('foo.txt', 'dir')
266 self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
267
268
269class LinkFile(CopyLinkTestCase):
270 """Check _LinkFile handling."""
271
272 def LinkFile(self, src, dest):
273 return project._LinkFile(self.worktree, src, self.topdir, dest)
274
275 def test_basic(self):
276 """Basic test of linking a file from a project into the toplevel."""
277 src = os.path.join(self.worktree, 'foo.txt')
278 self.touch(src)
279 lf = self.LinkFile('foo.txt', 'foo')
280 lf._Link()
281 dest = os.path.join(self.topdir, 'foo')
282 self.assertExists(dest)
283 self.assertTrue(os.path.islink(dest))
Mike Frysingerd9254592020-02-19 22:36:26 -0500284 self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest))
Mike Frysingere6a202f2019-08-02 15:57:57 -0400285
286 def test_src_subdir(self):
287 """Link to a file in a subdir of a project."""
288 src = os.path.join(self.worktree, 'bar', 'foo.txt')
289 os.makedirs(os.path.dirname(src))
290 self.touch(src)
291 lf = self.LinkFile('bar/foo.txt', 'foo')
292 lf._Link()
293 self.assertExists(os.path.join(self.topdir, 'foo'))
294
Mike Frysinger07392ed2020-02-10 21:35:48 -0500295 def test_src_self(self):
296 """Link to the project itself."""
297 dest = os.path.join(self.topdir, 'foo', 'bar')
298 lf = self.LinkFile('.', 'foo/bar')
299 lf._Link()
300 self.assertExists(dest)
Mike Frysingerd9254592020-02-19 22:36:26 -0500301 self.assertEqual(os.path.join('..', 'git-project'), os.readlink(dest))
Mike Frysinger07392ed2020-02-10 21:35:48 -0500302
Mike Frysingere6a202f2019-08-02 15:57:57 -0400303 def test_dest_subdir(self):
304 """Link a file to a subdir of a checkout."""
305 src = os.path.join(self.worktree, 'foo.txt')
306 self.touch(src)
307 lf = self.LinkFile('foo.txt', 'sub/dir/foo/bar')
308 self.assertFalse(os.path.exists(os.path.join(self.topdir, 'sub')))
309 lf._Link()
310 self.assertExists(os.path.join(self.topdir, 'sub', 'dir', 'foo', 'bar'))
311
Mike Frysinger07392ed2020-02-10 21:35:48 -0500312 def test_src_block_relative(self):
313 """Do not allow relative symlinks."""
314 BAD_SOURCES = (
315 './',
316 '..',
317 '../',
318 'foo/.',
319 'foo/./bar',
320 'foo/..',
321 'foo/../foo',
322 )
323 for src in BAD_SOURCES:
324 lf = self.LinkFile(src, 'foo')
325 self.assertRaises(error.ManifestInvalidPathError, lf._Link)
326
Mike Frysingere6a202f2019-08-02 15:57:57 -0400327 def test_update(self):
328 """Make sure changed targets get updated."""
329 dest = os.path.join(self.topdir, 'sym')
330
331 src = os.path.join(self.worktree, 'foo.txt')
332 self.touch(src)
333 lf = self.LinkFile('foo.txt', 'sym')
334 lf._Link()
Mike Frysingerd9254592020-02-19 22:36:26 -0500335 self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest))
Mike Frysingere6a202f2019-08-02 15:57:57 -0400336
337 # Point the symlink somewhere else.
338 os.unlink(dest)
Mike Frysingerd9254592020-02-19 22:36:26 -0500339 platform_utils.symlink(self.tempdir, dest)
Mike Frysingere6a202f2019-08-02 15:57:57 -0400340 lf._Link()
Mike Frysingerd9254592020-02-19 22:36:26 -0500341 self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest))