1# 2# Copyright (C) 2024 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 git_utils.""" 17import os 18import unittest 19from contextlib import ExitStack 20from pathlib import Path 21from subprocess import CalledProcessError 22from tempfile import TemporaryDirectory 23 24import git_utils 25 26from .gitrepo import GitRepo 27 28 29class GitRepoTestCase(unittest.TestCase): 30 """Common base for tests that operate on a git repo.""" 31 32 def setUp(self) -> None: 33 # Local test runs will probably pass without this since the caller 34 # almost certainly has git configured, but the bots that run the tests 35 # may not. **Do not** use `git config --global` for this, since that 36 # will modify the caller's config during local testing. 37 self._original_env = os.environ.copy() 38 os.environ["GIT_AUTHOR_NAME"] = "Testy McTestFace" 39 os.environ["GIT_AUTHOR_EMAIL"] = "test@example.com" 40 os.environ["GIT_COMMITTER_NAME"] = os.environ["GIT_AUTHOR_NAME"] 41 os.environ["GIT_COMMITTER_EMAIL"] = os.environ["GIT_AUTHOR_EMAIL"] 42 43 with ExitStack() as stack: 44 temp_dir = TemporaryDirectory() # pylint: disable=consider-using-with 45 stack.enter_context(temp_dir) 46 self.addCleanup(stack.pop_all().close) 47 self.repo = GitRepo(Path(temp_dir.name) / "repo") 48 49 def tearDown(self) -> None: 50 # This isn't trivially `os.environ = self._original_env` because 51 # os.environ isn't actually a dict, it's an os._Environ, and there isn't 52 # a good way to construct a new one of those. 53 os.environ.clear() 54 os.environ.update(self._original_env) 55 56 57class IsAncestorTest(GitRepoTestCase): 58 """Tests for git_utils.is_ancestor.""" 59 60 def test_if_commit_is_its_own_ancestor(self) -> None: 61 """Tests that False is returned when both commits are the same.""" 62 self.repo.init() 63 self.repo.commit("Initial commit.", allow_empty=True) 64 initial_commit = self.repo.head() 65 assert not git_utils.is_ancestor(self.repo.path, initial_commit, initial_commit) 66 67 def test_is_ancestor(self) -> None: 68 """Tests that True is returned when the ref is an ancestor.""" 69 self.repo.init() 70 self.repo.commit("Initial commit.", allow_empty=True) 71 initial_commit = self.repo.head() 72 self.repo.commit("Second commit.", allow_empty=True) 73 second_commit = self.repo.head() 74 git_utils.is_ancestor(self.repo.path, initial_commit, second_commit) 75 76 def test_is_not_ancestor(self) -> None: 77 """Tests that False is returned when the ref is not an ancestor.""" 78 self.repo.init() 79 self.repo.commit("Initial commit.", allow_empty=True) 80 initial_commit = self.repo.head() 81 self.repo.commit("Second commit.", allow_empty=True) 82 second_commit = self.repo.head() 83 assert not git_utils.is_ancestor(self.repo.path, second_commit, initial_commit) 84 85 def test_error(self) -> None: 86 """Tests that an error is raised when git encounters an error.""" 87 self.repo.init() 88 with self.assertRaises(CalledProcessError): 89 git_utils.is_ancestor(self.repo.path, "not-a-ref", "not-a-ref") 90 91 92class GetMostRecentTagTest(GitRepoTestCase): 93 """Tests for git_utils.get_most_recent_tag.""" 94 95 def test_find_tag_on_correct_branch(self) -> None: 96 """Tests that only tags on the given branch are found.""" 97 self.repo.init("main") 98 self.repo.commit("Initial commit.", allow_empty=True) 99 self.repo.tag("v1.0.0") 100 self.repo.switch_to_new_branch("release-2.0") 101 self.repo.commit("Second commit.", allow_empty=True) 102 self.repo.tag("v2.0.0") 103 self.assertEqual( 104 git_utils.get_most_recent_tag(self.repo.path, "main"), "v1.0.0" 105 ) 106 107 def test_no_tags(self) -> None: 108 """Tests that None is returned when the repo has no tags.""" 109 self.repo.init("main") 110 self.repo.commit("Initial commit.", allow_empty=True) 111 self.assertIsNone(git_utils.get_most_recent_tag(self.repo.path, "main")) 112 113 def test_no_describing_tags(self) -> None: 114 """Tests that None is returned when no tags describe the ref.""" 115 self.repo.init("main") 116 self.repo.commit("Initial commit.", allow_empty=True) 117 self.repo.switch_to_new_branch("release-2.0") 118 self.repo.commit("Second commit.", allow_empty=True) 119 self.repo.tag("v2.0.0") 120 self.assertIsNone(git_utils.get_most_recent_tag(self.repo.path, "main")) 121 122 123class DiffTest(GitRepoTestCase): 124 def test_diff_stat_A_filter(self) -> None: 125 """Tests for git_utils.diff_stat.""" 126 self.repo.init("main") 127 self.repo.commit( 128 "Add README.md", update_files={"README.md": "Hello, world!"} 129 ) 130 first_commit = self.repo.head() 131 self.repo.commit( 132 "Add OWNERS and METADATA", 133 update_files={"OWNERS": "nobody"} 134 ) 135 diff = git_utils.diff_stat(self.repo.path, 'A', first_commit) 136 assert 'OWNERS | 1 +' in diff 137 138 def test_diff_name_only_A_filter(self) -> None: 139 """Tests for git_utils.diff_name_only.""" 140 self.repo.init("main") 141 self.repo.commit( 142 "Add README.md", update_files={"README.md": "Hello, world!"} 143 ) 144 first_commit = self.repo.head() 145 self.repo.commit( 146 "Add OWNERS and METADATA", 147 update_files={"OWNERS": "nobody", "METADATA": "name: 'foo'"} 148 ) 149 diff = git_utils.diff_name_only(self.repo.path, 'A', first_commit) 150 assert diff == 'METADATA\nOWNERS\n' 151 152 153if __name__ == "__main__": 154 unittest.main(verbosity=2) 155