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