diff options
Diffstat (limited to 'tests/test_project.py')
| -rw-r--r-- | tests/test_project.py | 297 |
1 files changed, 249 insertions, 48 deletions
diff --git a/tests/test_project.py b/tests/test_project.py index 77126dff..9b2cc4e9 100644 --- a/tests/test_project.py +++ b/tests/test_project.py | |||
| @@ -1,5 +1,3 @@ | |||
| 1 | # -*- coding:utf-8 -*- | ||
| 2 | # | ||
| 3 | # Copyright (C) 2019 The Android Open Source Project | 1 | # Copyright (C) 2019 The Android Open Source Project |
| 4 | # | 2 | # |
| 5 | # Licensed under the Apache License, Version 2.0 (the "License"); | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| @@ -16,8 +14,6 @@ | |||
| 16 | 14 | ||
| 17 | """Unittests for the project.py module.""" | 15 | """Unittests for the project.py module.""" |
| 18 | 16 | ||
| 19 | from __future__ import print_function | ||
| 20 | |||
| 21 | import contextlib | 17 | import contextlib |
| 22 | import os | 18 | import os |
| 23 | import shutil | 19 | import shutil |
| @@ -25,7 +21,10 @@ import subprocess | |||
| 25 | import tempfile | 21 | import tempfile |
| 26 | import unittest | 22 | import unittest |
| 27 | 23 | ||
| 24 | import error | ||
| 25 | import git_command | ||
| 28 | import git_config | 26 | import git_config |
| 27 | import platform_utils | ||
| 29 | import project | 28 | import project |
| 30 | 29 | ||
| 31 | 30 | ||
| @@ -36,49 +35,22 @@ def TempGitTree(): | |||
| 36 | # Python 2 support entirely. | 35 | # Python 2 support entirely. |
| 37 | try: | 36 | try: |
| 38 | tempdir = tempfile.mkdtemp(prefix='repo-tests') | 37 | tempdir = tempfile.mkdtemp(prefix='repo-tests') |
| 39 | subprocess.check_call(['git', 'init'], cwd=tempdir) | 38 | |
| 39 | # Tests need to assume, that main is default branch at init, | ||
| 40 | # which is not supported in config until 2.28. | ||
| 41 | cmd = ['git', 'init'] | ||
| 42 | if git_command.git_require((2, 28, 0)): | ||
| 43 | cmd += ['--initial-branch=main'] | ||
| 44 | else: | ||
| 45 | # Use template dir for init. | ||
| 46 | templatedir = tempfile.mkdtemp(prefix='.test-template') | ||
| 47 | with open(os.path.join(templatedir, 'HEAD'), 'w') as fp: | ||
| 48 | fp.write('ref: refs/heads/main\n') | ||
| 49 | cmd += ['--template', templatedir] | ||
| 50 | subprocess.check_call(cmd, cwd=tempdir) | ||
| 40 | yield tempdir | 51 | yield tempdir |
| 41 | finally: | 52 | finally: |
| 42 | shutil.rmtree(tempdir) | 53 | platform_utils.rmtree(tempdir) |
| 43 | |||
| 44 | |||
| 45 | class RepoHookShebang(unittest.TestCase): | ||
| 46 | """Check shebang parsing in RepoHook.""" | ||
| 47 | |||
| 48 | def test_no_shebang(self): | ||
| 49 | """Lines w/out shebangs should be rejected.""" | ||
| 50 | DATA = ( | ||
| 51 | '', | ||
| 52 | '# -*- coding:utf-8 -*-\n', | ||
| 53 | '#\n# foo\n', | ||
| 54 | '# Bad shebang in script\n#!/foo\n' | ||
| 55 | ) | ||
| 56 | for data in DATA: | ||
| 57 | self.assertIsNone(project.RepoHook._ExtractInterpFromShebang(data)) | ||
| 58 | |||
| 59 | def test_direct_interp(self): | ||
| 60 | """Lines whose shebang points directly to the interpreter.""" | ||
| 61 | DATA = ( | ||
| 62 | ('#!/foo', '/foo'), | ||
| 63 | ('#! /foo', '/foo'), | ||
| 64 | ('#!/bin/foo ', '/bin/foo'), | ||
| 65 | ('#! /usr/foo ', '/usr/foo'), | ||
| 66 | ('#! /usr/foo -args', '/usr/foo'), | ||
| 67 | ) | ||
| 68 | for shebang, interp in DATA: | ||
| 69 | self.assertEqual(project.RepoHook._ExtractInterpFromShebang(shebang), | ||
| 70 | interp) | ||
| 71 | |||
| 72 | def test_env_interp(self): | ||
| 73 | """Lines whose shebang launches through `env`.""" | ||
| 74 | DATA = ( | ||
| 75 | ('#!/usr/bin/env foo', 'foo'), | ||
| 76 | ('#!/bin/env foo', 'foo'), | ||
| 77 | ('#! /bin/env /bin/foo ', '/bin/foo'), | ||
| 78 | ) | ||
| 79 | for shebang, interp in DATA: | ||
| 80 | self.assertEqual(project.RepoHook._ExtractInterpFromShebang(shebang), | ||
| 81 | interp) | ||
| 82 | 54 | ||
| 83 | 55 | ||
| 84 | class FakeProject(object): | 56 | class FakeProject(object): |
| @@ -114,7 +86,7 @@ class ReviewableBranchTests(unittest.TestCase): | |||
| 114 | 86 | ||
| 115 | # Start off with the normal details. | 87 | # Start off with the normal details. |
| 116 | rb = project.ReviewableBranch( | 88 | rb = project.ReviewableBranch( |
| 117 | fakeproj, fakeproj.config.GetBranch('work'), 'master') | 89 | fakeproj, fakeproj.config.GetBranch('work'), 'main') |
| 118 | self.assertEqual('work', rb.name) | 90 | self.assertEqual('work', rb.name) |
| 119 | self.assertEqual(1, len(rb.commits)) | 91 | self.assertEqual(1, len(rb.commits)) |
| 120 | self.assertIn('Del file', rb.commits[0]) | 92 | self.assertIn('Del file', rb.commits[0]) |
| @@ -127,10 +99,239 @@ class ReviewableBranchTests(unittest.TestCase): | |||
| 127 | self.assertTrue(rb.date) | 99 | self.assertTrue(rb.date) |
| 128 | 100 | ||
| 129 | # Now delete the tracking branch! | 101 | # Now delete the tracking branch! |
| 130 | fakeproj.work_git.branch('-D', 'master') | 102 | fakeproj.work_git.branch('-D', 'main') |
| 131 | rb = project.ReviewableBranch( | 103 | rb = project.ReviewableBranch( |
| 132 | fakeproj, fakeproj.config.GetBranch('work'), 'master') | 104 | fakeproj, fakeproj.config.GetBranch('work'), 'main') |
| 133 | self.assertEqual(0, len(rb.commits)) | 105 | self.assertEqual(0, len(rb.commits)) |
| 134 | self.assertFalse(rb.base_exists) | 106 | self.assertFalse(rb.base_exists) |
| 135 | # Hard to assert anything useful about this. | 107 | # Hard to assert anything useful about this. |
| 136 | self.assertTrue(rb.date) | 108 | self.assertTrue(rb.date) |
| 109 | |||
| 110 | |||
| 111 | class CopyLinkTestCase(unittest.TestCase): | ||
| 112 | """TestCase for stub repo client checkouts. | ||
| 113 | |||
| 114 | It'll have a layout like: | ||
| 115 | tempdir/ # self.tempdir | ||
| 116 | checkout/ # self.topdir | ||
| 117 | git-project/ # self.worktree | ||
| 118 | |||
| 119 | Attributes: | ||
| 120 | tempdir: A dedicated temporary directory. | ||
| 121 | worktree: The top of the repo client checkout. | ||
| 122 | topdir: The top of a project checkout. | ||
| 123 | """ | ||
| 124 | |||
| 125 | def setUp(self): | ||
| 126 | self.tempdir = tempfile.mkdtemp(prefix='repo_tests') | ||
| 127 | self.topdir = os.path.join(self.tempdir, 'checkout') | ||
| 128 | self.worktree = os.path.join(self.topdir, 'git-project') | ||
| 129 | os.makedirs(self.topdir) | ||
| 130 | os.makedirs(self.worktree) | ||
| 131 | |||
| 132 | def tearDown(self): | ||
| 133 | shutil.rmtree(self.tempdir, ignore_errors=True) | ||
| 134 | |||
| 135 | @staticmethod | ||
| 136 | def touch(path): | ||
| 137 | with open(path, 'w'): | ||
| 138 | pass | ||
| 139 | |||
| 140 | def assertExists(self, path, msg=None): | ||
| 141 | """Make sure |path| exists.""" | ||
| 142 | if os.path.exists(path): | ||
| 143 | return | ||
| 144 | |||
| 145 | if msg is None: | ||
| 146 | msg = ['path is missing: %s' % path] | ||
| 147 | while path != '/': | ||
| 148 | path = os.path.dirname(path) | ||
| 149 | if not path: | ||
| 150 | # If we're given something like "foo", abort once we get to "". | ||
| 151 | break | ||
| 152 | result = os.path.exists(path) | ||
| 153 | msg.append('\tos.path.exists(%s): %s' % (path, result)) | ||
| 154 | if result: | ||
| 155 | msg.append('\tcontents: %r' % os.listdir(path)) | ||
| 156 | break | ||
| 157 | msg = '\n'.join(msg) | ||
| 158 | |||
| 159 | raise self.failureException(msg) | ||
| 160 | |||
| 161 | |||
| 162 | class CopyFile(CopyLinkTestCase): | ||
| 163 | """Check _CopyFile handling.""" | ||
| 164 | |||
| 165 | def CopyFile(self, src, dest): | ||
| 166 | return project._CopyFile(self.worktree, src, self.topdir, dest) | ||
| 167 | |||
| 168 | def test_basic(self): | ||
| 169 | """Basic test of copying a file from a project to the toplevel.""" | ||
| 170 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 171 | self.touch(src) | ||
| 172 | cf = self.CopyFile('foo.txt', 'foo') | ||
| 173 | cf._Copy() | ||
| 174 | self.assertExists(os.path.join(self.topdir, 'foo')) | ||
| 175 | |||
| 176 | def test_src_subdir(self): | ||
| 177 | """Copy a file from a subdir of a project.""" | ||
| 178 | src = os.path.join(self.worktree, 'bar', 'foo.txt') | ||
| 179 | os.makedirs(os.path.dirname(src)) | ||
| 180 | self.touch(src) | ||
| 181 | cf = self.CopyFile('bar/foo.txt', 'new.txt') | ||
| 182 | cf._Copy() | ||
| 183 | self.assertExists(os.path.join(self.topdir, 'new.txt')) | ||
| 184 | |||
| 185 | def test_dest_subdir(self): | ||
| 186 | """Copy a file to a subdir of a checkout.""" | ||
| 187 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 188 | self.touch(src) | ||
| 189 | cf = self.CopyFile('foo.txt', 'sub/dir/new.txt') | ||
| 190 | self.assertFalse(os.path.exists(os.path.join(self.topdir, 'sub'))) | ||
| 191 | cf._Copy() | ||
| 192 | self.assertExists(os.path.join(self.topdir, 'sub', 'dir', 'new.txt')) | ||
| 193 | |||
| 194 | def test_update(self): | ||
| 195 | """Make sure changed files get copied again.""" | ||
| 196 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 197 | dest = os.path.join(self.topdir, 'bar') | ||
| 198 | with open(src, 'w') as f: | ||
| 199 | f.write('1st') | ||
| 200 | cf = self.CopyFile('foo.txt', 'bar') | ||
| 201 | cf._Copy() | ||
| 202 | self.assertExists(dest) | ||
| 203 | with open(dest) as f: | ||
| 204 | self.assertEqual(f.read(), '1st') | ||
| 205 | |||
| 206 | with open(src, 'w') as f: | ||
| 207 | f.write('2nd!') | ||
| 208 | cf._Copy() | ||
| 209 | with open(dest) as f: | ||
| 210 | self.assertEqual(f.read(), '2nd!') | ||
| 211 | |||
| 212 | def test_src_block_symlink(self): | ||
| 213 | """Do not allow reading from a symlinked path.""" | ||
| 214 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 215 | sym = os.path.join(self.worktree, 'sym') | ||
| 216 | self.touch(src) | ||
| 217 | platform_utils.symlink('foo.txt', sym) | ||
| 218 | self.assertExists(sym) | ||
| 219 | cf = self.CopyFile('sym', 'foo') | ||
| 220 | self.assertRaises(error.ManifestInvalidPathError, cf._Copy) | ||
| 221 | |||
| 222 | def test_src_block_symlink_traversal(self): | ||
| 223 | """Do not allow reading through a symlink dir.""" | ||
| 224 | realfile = os.path.join(self.tempdir, 'file.txt') | ||
| 225 | self.touch(realfile) | ||
| 226 | src = os.path.join(self.worktree, 'bar', 'file.txt') | ||
| 227 | platform_utils.symlink(self.tempdir, os.path.join(self.worktree, 'bar')) | ||
| 228 | self.assertExists(src) | ||
| 229 | cf = self.CopyFile('bar/file.txt', 'foo') | ||
| 230 | self.assertRaises(error.ManifestInvalidPathError, cf._Copy) | ||
| 231 | |||
| 232 | def test_src_block_copy_from_dir(self): | ||
| 233 | """Do not allow copying from a directory.""" | ||
| 234 | src = os.path.join(self.worktree, 'dir') | ||
| 235 | os.makedirs(src) | ||
| 236 | cf = self.CopyFile('dir', 'foo') | ||
| 237 | self.assertRaises(error.ManifestInvalidPathError, cf._Copy) | ||
| 238 | |||
| 239 | def test_dest_block_symlink(self): | ||
| 240 | """Do not allow writing to a symlink.""" | ||
| 241 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 242 | self.touch(src) | ||
| 243 | platform_utils.symlink('dest', os.path.join(self.topdir, 'sym')) | ||
| 244 | cf = self.CopyFile('foo.txt', 'sym') | ||
| 245 | self.assertRaises(error.ManifestInvalidPathError, cf._Copy) | ||
| 246 | |||
| 247 | def test_dest_block_symlink_traversal(self): | ||
| 248 | """Do not allow writing through a symlink dir.""" | ||
| 249 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 250 | self.touch(src) | ||
| 251 | platform_utils.symlink(tempfile.gettempdir(), | ||
| 252 | os.path.join(self.topdir, 'sym')) | ||
| 253 | cf = self.CopyFile('foo.txt', 'sym/foo.txt') | ||
| 254 | self.assertRaises(error.ManifestInvalidPathError, cf._Copy) | ||
| 255 | |||
| 256 | def test_src_block_copy_to_dir(self): | ||
| 257 | """Do not allow copying to a directory.""" | ||
| 258 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 259 | self.touch(src) | ||
| 260 | os.makedirs(os.path.join(self.topdir, 'dir')) | ||
| 261 | cf = self.CopyFile('foo.txt', 'dir') | ||
| 262 | self.assertRaises(error.ManifestInvalidPathError, cf._Copy) | ||
| 263 | |||
| 264 | |||
| 265 | class LinkFile(CopyLinkTestCase): | ||
| 266 | """Check _LinkFile handling.""" | ||
| 267 | |||
| 268 | def LinkFile(self, src, dest): | ||
| 269 | return project._LinkFile(self.worktree, src, self.topdir, dest) | ||
| 270 | |||
| 271 | def test_basic(self): | ||
| 272 | """Basic test of linking a file from a project into the toplevel.""" | ||
| 273 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 274 | self.touch(src) | ||
| 275 | lf = self.LinkFile('foo.txt', 'foo') | ||
| 276 | lf._Link() | ||
| 277 | dest = os.path.join(self.topdir, 'foo') | ||
| 278 | self.assertExists(dest) | ||
| 279 | self.assertTrue(os.path.islink(dest)) | ||
| 280 | self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest)) | ||
| 281 | |||
| 282 | def test_src_subdir(self): | ||
| 283 | """Link to a file in a subdir of a project.""" | ||
| 284 | src = os.path.join(self.worktree, 'bar', 'foo.txt') | ||
| 285 | os.makedirs(os.path.dirname(src)) | ||
| 286 | self.touch(src) | ||
| 287 | lf = self.LinkFile('bar/foo.txt', 'foo') | ||
| 288 | lf._Link() | ||
| 289 | self.assertExists(os.path.join(self.topdir, 'foo')) | ||
| 290 | |||
| 291 | def test_src_self(self): | ||
| 292 | """Link to the project itself.""" | ||
| 293 | dest = os.path.join(self.topdir, 'foo', 'bar') | ||
| 294 | lf = self.LinkFile('.', 'foo/bar') | ||
| 295 | lf._Link() | ||
| 296 | self.assertExists(dest) | ||
| 297 | self.assertEqual(os.path.join('..', 'git-project'), os.readlink(dest)) | ||
| 298 | |||
| 299 | def test_dest_subdir(self): | ||
| 300 | """Link a file to a subdir of a checkout.""" | ||
| 301 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 302 | self.touch(src) | ||
| 303 | lf = self.LinkFile('foo.txt', 'sub/dir/foo/bar') | ||
| 304 | self.assertFalse(os.path.exists(os.path.join(self.topdir, 'sub'))) | ||
| 305 | lf._Link() | ||
| 306 | self.assertExists(os.path.join(self.topdir, 'sub', 'dir', 'foo', 'bar')) | ||
| 307 | |||
| 308 | def test_src_block_relative(self): | ||
| 309 | """Do not allow relative symlinks.""" | ||
| 310 | BAD_SOURCES = ( | ||
| 311 | './', | ||
| 312 | '..', | ||
| 313 | '../', | ||
| 314 | 'foo/.', | ||
| 315 | 'foo/./bar', | ||
| 316 | 'foo/..', | ||
| 317 | 'foo/../foo', | ||
| 318 | ) | ||
| 319 | for src in BAD_SOURCES: | ||
| 320 | lf = self.LinkFile(src, 'foo') | ||
| 321 | self.assertRaises(error.ManifestInvalidPathError, lf._Link) | ||
| 322 | |||
| 323 | def test_update(self): | ||
| 324 | """Make sure changed targets get updated.""" | ||
| 325 | dest = os.path.join(self.topdir, 'sym') | ||
| 326 | |||
| 327 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 328 | self.touch(src) | ||
| 329 | lf = self.LinkFile('foo.txt', 'sym') | ||
| 330 | lf._Link() | ||
| 331 | self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest)) | ||
| 332 | |||
| 333 | # Point the symlink somewhere else. | ||
| 334 | os.unlink(dest) | ||
| 335 | platform_utils.symlink(self.tempdir, dest) | ||
| 336 | lf._Link() | ||
| 337 | self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest)) | ||
