xref: /aosp_15_r20/external/pigweed/pw_cli/py/git_repo_test.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1#!/usr/bin/env python3
2# Copyright 2024 The Pigweed Authors
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may not
5# use this file except in compliance with the License. You may obtain a copy of
6# the License at
7#
8#     https://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, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations under
14# the License.
15"""git repo module tests"""
16
17from pathlib import Path
18from subprocess import CompletedProcess
19from typing import Dict
20import re
21import unittest
22from unittest import mock
23
24from pw_cli.tool_runner import ToolRunner
25from pw_cli.git_repo import GitRepo
26
27
28class FakeGitToolRunner(ToolRunner):
29    def __init__(self, command_results: Dict[str, CompletedProcess]):
30        self._results = command_results
31
32    def _run_tool(self, tool: str, args, **kwargs) -> CompletedProcess:
33        full_command = ' '.join((tool, *tuple(args)))
34        for cmd, result in self._results.items():
35            if cmd in full_command:
36                return result
37
38        return CompletedProcess(
39            args=full_command,
40            returncode=0xFF,
41            stderr=f'I do not know how to `{full_command}`'.encode(),
42            stdout=b'Failed to execute command',
43        )
44
45
46def git_ok(cmd: str, stdout: str) -> CompletedProcess:
47    return CompletedProcess(
48        args=cmd,
49        returncode=0,
50        stderr='',
51        stdout=stdout.encode(),
52    )
53
54
55def git_err(cmd: str, stderr: str, returncode: int = 1) -> CompletedProcess:
56    return CompletedProcess(
57        args=cmd,
58        returncode=returncode,
59        stderr=stderr.encode(),
60        stdout='',
61    )
62
63
64class TestGitRepo(unittest.TestCase):
65    """Tests for git_repo.py"""
66
67    GIT_ROOT = Path("/dev/null/test").resolve()
68    SUBMODULES = [
69        Path("/dev/null/test/third_party/pigweed").resolve(),
70        Path("/dev/null/test/vendor/anycom/p1").resolve(),
71        Path("/dev/null/test/vendor/anycom/p2").resolve(),
72    ]
73    GIT_SUBMODULES_OUT = "\n".join([str(x) for x in SUBMODULES])
74
75    EXPECTED_SUBMODULE_LIST_CMD = ' '.join(
76        (
77            'submodule',
78            'foreach',
79            '--quiet',
80            '--recursive',
81            'echo $toplevel/$sm_path',
82        )
83    )
84
85    def make_fake_git_repo(self, cmds):
86        return GitRepo(self.GIT_ROOT, FakeGitToolRunner(cmds))
87
88    def test_mock_root(self):
89        """Ensure our mock works since so many of our tests depend upon it."""
90        cmds = {}
91        repo = self.make_fake_git_repo(cmds)
92        self.assertEqual(repo.root(), self.GIT_ROOT)
93
94    def test_list_submodules_1(self):
95        """Ensures the root git repo appears in the submodule list."""
96        cmds = {
97            self.EXPECTED_SUBMODULE_LIST_CMD: git_ok(
98                self.EXPECTED_SUBMODULE_LIST_CMD, self.GIT_SUBMODULES_OUT
99            )
100        }
101        repo = self.make_fake_git_repo(cmds)
102        paths = repo.list_submodules()
103        self.assertNotIn(self.GIT_ROOT, paths)
104
105    def test_list_submodules_2(self):
106        cmds = {
107            self.EXPECTED_SUBMODULE_LIST_CMD: git_ok(
108                self.EXPECTED_SUBMODULE_LIST_CMD, self.GIT_SUBMODULES_OUT
109            )
110        }
111        repo = self.make_fake_git_repo(cmds)
112        paths = repo.list_submodules()
113        self.assertIn(self.SUBMODULES[2], paths)
114
115    def test_list_submodules_with_exclude_str(self):
116        cmds = {
117            self.EXPECTED_SUBMODULE_LIST_CMD: git_ok(
118                self.EXPECTED_SUBMODULE_LIST_CMD, self.GIT_SUBMODULES_OUT
119            )
120        }
121        repo = self.make_fake_git_repo(cmds)
122        paths = repo.list_submodules(
123            excluded_paths=(self.GIT_ROOT.as_posix(),),
124        )
125        self.assertNotIn(self.GIT_ROOT, paths)
126
127    def test_list_submodules_with_exclude_regex(self):
128        cmds = {
129            self.EXPECTED_SUBMODULE_LIST_CMD: git_ok(
130                self.EXPECTED_SUBMODULE_LIST_CMD, self.GIT_SUBMODULES_OUT
131            )
132        }
133        repo = self.make_fake_git_repo(cmds)
134        paths = repo.list_submodules(
135            excluded_paths=(re.compile("third_party/.*"),),
136        )
137        self.assertNotIn(self.SUBMODULES[0], paths)
138
139    def test_list_submodules_with_exclude_str_miss(self):
140        cmds = {
141            self.EXPECTED_SUBMODULE_LIST_CMD: git_ok(
142                self.EXPECTED_SUBMODULE_LIST_CMD, self.GIT_SUBMODULES_OUT
143            )
144        }
145        repo = self.make_fake_git_repo(cmds)
146        paths = repo.list_submodules(
147            excluded_paths=(re.compile("pigweed"),),
148        )
149        self.assertIn(self.SUBMODULES[-1], paths)
150
151    def test_list_submodules_with_exclude_regex_miss_1(self):
152        cmds = {
153            self.EXPECTED_SUBMODULE_LIST_CMD: git_ok(
154                self.EXPECTED_SUBMODULE_LIST_CMD, self.GIT_SUBMODULES_OUT
155            )
156        }
157        repo = self.make_fake_git_repo(cmds)
158        paths = repo.list_submodules(
159            excluded_paths=(re.compile("foo/.*"),),
160        )
161        self.assertNotIn(self.GIT_ROOT, paths)
162        for module in self.SUBMODULES:
163            self.assertIn(module, paths)
164
165    def test_list_submodules_with_exclude_regex_miss_2(self):
166        cmds = {
167            self.EXPECTED_SUBMODULE_LIST_CMD: git_ok(
168                self.EXPECTED_SUBMODULE_LIST_CMD, self.GIT_SUBMODULES_OUT
169            )
170        }
171        repo = self.make_fake_git_repo(cmds)
172        paths = repo.list_submodules(
173            excluded_paths=(re.compile("pigweed"),),
174        )
175        self.assertNotIn(self.GIT_ROOT, paths)
176        for module in self.SUBMODULES:
177            self.assertIn(module, paths)
178
179    def test_list_files_unknown_hash(self):
180        bad_cmd = "diff --name-only --diff-filter=d 'something' --"
181        good_cmd = 'ls-files --'
182        fake_path = 'path/to/foo.h'
183        cmds = {
184            bad_cmd: git_err(bad_cmd, "fatal: bad revision 'something'"),
185            good_cmd: git_ok(good_cmd, fake_path + '\n'),
186        }
187
188        expected_file_path = self.GIT_ROOT / Path(fake_path)
189        repo = self.make_fake_git_repo(cmds)
190
191        # This function needs to be mocked because it does a `is_file()` check
192        # on returned paths. Since we're not using real files, nothing will
193        # be yielded.
194        repo._ls_files = mock.MagicMock(  # pylint: disable=protected-access
195            return_value=[expected_file_path]
196        )
197        paths = repo.list_files(commit='something')
198        self.assertIn(expected_file_path, paths)
199
200    def test_fake_uncommitted_changes(self):
201        index_update = 'update-index -q --refresh'
202        diff_index = 'diff-index --quiet HEAD --'
203        cmds = {
204            index_update: git_ok(index_update, ''),
205            diff_index: git_err(diff_index, '', returncode=1),
206        }
207        repo = self.make_fake_git_repo(cmds)
208        self.assertTrue(repo.has_uncommitted_changes())
209
210
211if __name__ == '__main__':
212    unittest.main()
213