xref: /aosp_15_r20/tools/external_updater/tests/test_gitrepo.py (revision 3c875a214f382db1236d28570d1304ce57138f32)
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