1# 2# Copyright (C) 2023 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15# 16"""Tests for gitrepo.""" 17import os 18import subprocess 19import unittest 20from contextlib import ExitStack 21from pathlib import Path 22from tempfile import TemporaryDirectory 23 24from .gitrepo import GitRepo 25 26 27class GitRepoTest(unittest.TestCase): 28 """Tests for gitrepo.GitRepo.""" 29 30 def setUp(self) -> None: 31 # Local test runs will probably pass without this since the caller 32 # almost certainly has git configured, but the bots that run the tests 33 # may not. **Do not** use `git config --global` for this, since that 34 # will modify the caller's config during local testing. 35 self._original_env = os.environ.copy() 36 os.environ["GIT_AUTHOR_NAME"] = "Testy McTestFace" 37 os.environ["GIT_AUTHOR_EMAIL"] = "[email protected]" 38 os.environ["GIT_COMMITTER_NAME"] = os.environ["GIT_AUTHOR_NAME"] 39 os.environ["GIT_COMMITTER_EMAIL"] = os.environ["GIT_AUTHOR_EMAIL"] 40 41 with ExitStack() as stack: 42 temp_dir = TemporaryDirectory() # pylint: disable=consider-using-with 43 stack.enter_context(temp_dir) 44 self.addCleanup(stack.pop_all().close) 45 self.tmp_path = Path(temp_dir.name) 46 47 def tearDown(self) -> None: 48 # This isn't trivially `os.environ = self._original_env` because 49 # os.environ isn't actually a dict, it's an os._Environ, and there isn't 50 # a good way to construct a new one of those. 51 os.environ.clear() 52 os.environ.update(self._original_env) 53 54 def test_commit_adds_files(self) -> None: 55 """Tests that new files in commit are added to the repo.""" 56 repo = GitRepo(self.tmp_path / "repo") 57 repo.init() 58 repo.commit("Add README.md.", update_files={"README.md": "Hello, world!"}) 59 self.assertEqual(repo.commit_message_at_revision("HEAD"), "Add README.md.\n") 60 self.assertEqual( 61 repo.file_contents_at_revision("HEAD", "README.md"), "Hello, world!" 62 ) 63 64 def test_commit_updates_files(self) -> None: 65 """Tests that updated files in commit are modified.""" 66 repo = GitRepo(self.tmp_path / "repo") 67 repo.init() 68 repo.commit("Add README.md.", update_files={"README.md": "Hello, world!"}) 69 repo.commit("Update README.md.", update_files={"README.md": "Goodbye, world!"}) 70 self.assertEqual(repo.commit_message_at_revision("HEAD^"), "Add README.md.\n") 71 self.assertEqual( 72 repo.file_contents_at_revision("HEAD^", "README.md"), "Hello, world!" 73 ) 74 self.assertEqual(repo.commit_message_at_revision("HEAD"), "Update README.md.\n") 75 self.assertEqual( 76 repo.file_contents_at_revision("HEAD", "README.md"), "Goodbye, world!" 77 ) 78 79 def test_commit_deletes_files(self) -> None: 80 """Tests that files deleted by commit are removed from the repo.""" 81 repo = GitRepo(self.tmp_path / "repo") 82 repo.init() 83 repo.commit("Add README.md.", update_files={"README.md": "Hello, world!"}) 84 repo.commit("Remove README.md.", delete_files={"README.md"}) 85 self.assertEqual(repo.commit_message_at_revision("HEAD^"), "Add README.md.\n") 86 self.assertEqual( 87 repo.file_contents_at_revision("HEAD^", "README.md"), "Hello, world!" 88 ) 89 self.assertEqual(repo.commit_message_at_revision("HEAD"), "Remove README.md.\n") 90 self.assertNotEqual( 91 subprocess.run( 92 [ 93 "git", 94 "-C", 95 str(repo.path), 96 "ls-files", 97 "--error-unmatch", 98 "README.md", 99 ], 100 # The atest runner cannot parse test lines that have output. Hide the 101 # descriptive error from git (README.md does not exist, exactly what 102 # we're testing) so the test result can be parsed. 103 stderr=subprocess.DEVNULL, 104 check=False, 105 ).returncode, 106 0, 107 ) 108 109 def test_current_branch(self) -> None: 110 """Tests that current branch returns the current branch name.""" 111 repo = GitRepo(self.tmp_path / "repo") 112 repo.init("main") 113 self.assertEqual(repo.current_branch(), "main") 114 115 def test_current_branch_fails_if_not_init(self) -> None: 116 """Tests that current branch fails when there is no git repo.""" 117 with self.assertRaises(subprocess.CalledProcessError): 118 GitRepo(self.tmp_path / "repo").current_branch() 119 120 def test_switch_to_new_branch(self) -> None: 121 """Tests that switch_to_new_branch creates a new branch and switches to it.""" 122 repo = GitRepo(self.tmp_path / "repo") 123 repo.init("main") 124 repo.switch_to_new_branch("feature") 125 self.assertEqual(repo.current_branch(), "feature") 126 127 def test_switch_to_new_branch_does_not_clobber_existing_branches(self) -> None: 128 """Tests that switch_to_new_branch raises an error for extant branches.""" 129 repo = GitRepo(self.tmp_path / "repo") 130 repo.init("main") 131 repo.commit("Initial commit.", allow_empty=True) 132 with self.assertRaises(subprocess.CalledProcessError): 133 repo.switch_to_new_branch("main") 134 135 def test_switch_to_new_branch_with_start_point(self) -> None: 136 """Tests that switch_to_new_branch uses the provided start point.""" 137 repo = GitRepo(self.tmp_path / "repo") 138 repo.init("main") 139 repo.commit("Initial commit.", allow_empty=True) 140 initial_commit = repo.head() 141 repo.commit("Second commit.", allow_empty=True) 142 repo.switch_to_new_branch("feature", start_point=initial_commit) 143 self.assertEqual(repo.current_branch(), "feature") 144 self.assertEqual(repo.head(), initial_commit) 145 146 def test_sha_of_ref(self) -> None: 147 """Tests that sha_of_ref returns the SHA of the given ref.""" 148 repo = GitRepo(self.tmp_path / "repo") 149 repo.init("main") 150 repo.commit("Initial commit.", allow_empty=True) 151 self.assertEqual(repo.sha_of_ref("heads/main"), repo.head()) 152 153 def test_tag_head(self) -> None: 154 """Tests that tag creates a tag at HEAD.""" 155 repo = GitRepo(self.tmp_path / "repo") 156 repo.init() 157 repo.commit("Initial commit.", allow_empty=True) 158 repo.commit("Second commit.", allow_empty=True) 159 repo.tag("v1.0.0") 160 self.assertEqual(repo.sha_of_ref("tags/v1.0.0"), repo.head()) 161 162 def test_tag_ref(self) -> None: 163 """Tests that tag creates a tag at the given ref.""" 164 repo = GitRepo(self.tmp_path / "repo") 165 repo.init() 166 repo.commit("Initial commit.", allow_empty=True) 167 first_commit = repo.head() 168 repo.commit("Second commit.", allow_empty=True) 169 repo.tag("v1.0.0", first_commit) 170 self.assertEqual(repo.sha_of_ref("tags/v1.0.0"), first_commit) 171 172 173if __name__ == "__main__": 174 unittest.main(verbosity=2) 175