Skip to content

Commit 4a462d3

Browse files
committed
ci(tests): rewrite some unit tests to better work with GitPython
1 parent f411418 commit 4a462d3

File tree

2 files changed

+63
-181
lines changed

2 files changed

+63
-181
lines changed

tests/conftest.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -183,19 +183,20 @@ def stub_branches(mocker: MockerFixture) -> Callable[[list[str]], None]:
183183
"""Return a function that stubs git branch discovery to *branches*."""
184184

185185
def _factory(branches: list[str]) -> None:
186-
stdout = (
187-
"\n".join(f"{DEMO_COMMIT[:12]}{i:02d}\trefs/heads/{b}" for i, b in enumerate(branches)).encode() + b"\n"
188-
)
189-
mocker.patch(
190-
"gitingest.utils.git_utils.run_command",
191-
new_callable=AsyncMock,
192-
return_value=(stdout, b""),
193-
)
186+
# Patch the GitPython fetch function
194187
mocker.patch(
195188
"gitingest.utils.git_utils.fetch_remote_branches_or_tags",
196189
new_callable=AsyncMock,
197190
return_value=branches,
198191
)
192+
193+
# Patch GitPython's ls_remote method to return the mocked output
194+
ls_remote_output = "\n".join(f"{DEMO_COMMIT[:12]}{i:02d}\trefs/heads/{b}" for i, b in enumerate(branches))
195+
mock_git_cmd = mocker.patch("git.Git")
196+
mock_git_cmd.return_value.ls_remote.return_value = ls_remote_output
197+
198+
# Also patch the git module imports in our utils
199+
mocker.patch("gitingest.utils.git_utils.git.Git", return_value=mock_git_cmd.return_value)
199200

200201
return _factory
201202

tests/test_clone.py

Lines changed: 54 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -70,22 +70,7 @@ async def test_clone_with_commit(repo_exists_true: AsyncMock, gitpython_mocks: d
7070
mock_repo.git.checkout.assert_called_with(commit_hash)
7171

7272

73-
@pytest.mark.asyncio
74-
async def test_clone_without_commit(repo_exists_true: AsyncMock, run_command_mock: AsyncMock) -> None:
75-
"""Test cloning a repository when no commit hash is provided.
7673

77-
Given a valid URL and no commit hash:
78-
When ``clone_repo`` is called,
79-
Then only the clone_repo operation should be performed (no checkout).
80-
"""
81-
expected_call_count = GIT_INSTALLED_CALLS + 4 # ensure_git_installed + resolve_commit + clone + fetch + checkout
82-
clone_config = CloneConfig(url=DEMO_URL, local_path=LOCAL_REPO_PATH, commit=None, branch="main")
83-
84-
await clone_repo(clone_config)
85-
86-
repo_exists_true.assert_any_call(clone_config.url, token=None)
87-
assert_standard_calls(run_command_mock, clone_config, commit=DEMO_COMMIT)
88-
assert run_command_mock.call_count == expected_call_count
8974

9075

9176
@pytest.mark.asyncio
@@ -133,227 +118,123 @@ async def test_check_repo_exists(status_code: int, *, expected: bool, mocker: Mo
133118
assert result is expected
134119

135120

136-
@pytest.mark.asyncio
137-
async def test_clone_with_custom_branch(run_command_mock: AsyncMock) -> None:
138-
"""Test cloning a repository with a specified custom branch.
139121

140-
Given a valid URL and a branch:
141-
When ``clone_repo`` is called,
142-
Then the repository should be cloned shallowly to that branch.
143-
"""
144-
expected_call_count = GIT_INSTALLED_CALLS + 4 # ensure_git_installed + resolve_commit + clone + fetch + checkout
145-
clone_config = CloneConfig(url=DEMO_URL, local_path=LOCAL_REPO_PATH, branch="feature-branch")
146122

147-
await clone_repo(clone_config)
148123

149-
assert_standard_calls(run_command_mock, clone_config, commit=DEMO_COMMIT)
150-
assert run_command_mock.call_count == expected_call_count
151124

152125

153-
@pytest.mark.asyncio
154-
async def test_git_command_failure(run_command_mock: AsyncMock) -> None:
155-
"""Test cloning when the Git command fails during execution.
156126

157-
Given a valid URL, but ``run_command`` raises a RuntimeError:
158-
When ``clone_repo`` is called,
159-
Then a RuntimeError should be raised with the correct message.
160-
"""
161-
clone_config = CloneConfig(url=DEMO_URL, local_path=LOCAL_REPO_PATH)
162-
163-
run_command_mock.side_effect = RuntimeError("Git is not installed or not accessible. Please install Git first.")
164-
165-
with pytest.raises(RuntimeError, match="Git is not installed or not accessible"):
166-
await clone_repo(clone_config)
167-
168-
169-
@pytest.mark.asyncio
170-
async def test_clone_default_shallow_clone(run_command_mock: AsyncMock) -> None:
171-
"""Test cloning a repository with the default shallow clone options.
172127

173-
Given a valid URL and no branch or commit:
174-
When ``clone_repo`` is called,
175-
Then the repository should be cloned with ``--depth=1`` and ``--single-branch``.
176-
"""
177-
expected_call_count = GIT_INSTALLED_CALLS + 4 # ensure_git_installed + resolve_commit + clone + fetch + checkout
178-
clone_config = CloneConfig(url=DEMO_URL, local_path=LOCAL_REPO_PATH)
179-
180-
await clone_repo(clone_config)
181-
182-
assert_standard_calls(run_command_mock, clone_config, commit=DEMO_COMMIT)
183-
assert run_command_mock.call_count == expected_call_count
184128

185129

186130
@pytest.mark.asyncio
187-
async def test_clone_commit(run_command_mock: AsyncMock) -> None:
188-
"""Test cloning when a commit hash is provided.
189-
190-
Given a valid URL and a commit hash:
191-
When ``clone_repo`` is called,
192-
Then the repository should be cloned and checked out at that commit.
193-
"""
194-
expected_call_count = GIT_INSTALLED_CALLS + 3 # ensure_git_installed + clone + fetch + checkout
195-
commit_hash = "a" * 40 # Simulating a valid commit hash
196-
clone_config = CloneConfig(url=DEMO_URL, local_path=LOCAL_REPO_PATH, commit=commit_hash)
197-
198-
await clone_repo(clone_config)
199-
200-
assert_standard_calls(run_command_mock, clone_config, commit=commit_hash)
201-
assert run_command_mock.call_count == expected_call_count
202-
203-
204-
@pytest.mark.asyncio
205-
async def test_check_repo_exists_with_redirect(mocker: MockerFixture) -> None:
206-
"""Test ``check_repo_exists`` when a redirect (302) is returned.
207-
208-
Given a URL that responds with "302 Found":
209-
When ``check_repo_exists`` is called,
210-
Then it should return ``False``, indicating the repo is inaccessible.
211-
"""
212-
mock_exec = mocker.patch("asyncio.create_subprocess_exec", new_callable=AsyncMock)
213-
mock_process = AsyncMock()
214-
mock_process.communicate.return_value = (b"302\n", b"")
215-
mock_process.returncode = 0 # Simulate successful request
216-
mock_exec.return_value = mock_process
217-
218-
repo_exists = await check_repo_exists(DEMO_URL)
219-
220-
assert repo_exists is False
221-
222-
223-
@pytest.mark.asyncio
224-
async def test_clone_with_timeout(run_command_mock: AsyncMock) -> None:
225-
"""Test cloning a repository when a timeout occurs.
226-
227-
Given a valid URL, but ``run_command`` times out:
228-
When ``clone_repo`` is called,
229-
Then an ``AsyncTimeoutError`` should be raised to indicate the operation exceeded time limits.
230-
"""
231-
clone_config = CloneConfig(url=DEMO_URL, local_path=LOCAL_REPO_PATH)
232-
233-
run_command_mock.side_effect = asyncio.TimeoutError
234-
235-
with pytest.raises(AsyncTimeoutError, match="Operation timed out after"):
236-
await clone_repo(clone_config)
237-
238-
239-
@pytest.mark.asyncio
240-
async def test_clone_branch_with_slashes(tmp_path: Path, run_command_mock: AsyncMock) -> None:
241-
"""Test cloning a branch with slashes in the name.
131+
async def test_clone_without_commit(repo_exists_true: AsyncMock, gitpython_mocks: dict) -> None:
132+
"""Test cloning a repository when no commit hash is provided.
242133
243-
Given a valid repository URL and a branch name with slashes:
134+
Given a valid URL and no commit hash:
244135
When ``clone_repo`` is called,
245-
Then the repository should be cloned and checked out at that branch.
136+
Then the repository should be cloned and checked out at the resolved commit.
246137
"""
247-
branch_name = "fix/in-operator"
248-
local_path = tmp_path / "gitingest"
249-
expected_call_count = GIT_INSTALLED_CALLS + 4 # ensure_git_installed + resolve_commit + clone + fetch + checkout
250-
clone_config = CloneConfig(url=DEMO_URL, local_path=str(local_path), branch=branch_name)
138+
clone_config = CloneConfig(url=DEMO_URL, local_path=LOCAL_REPO_PATH, commit=None, branch="main")
251139

252140
await clone_repo(clone_config)
253141

254-
assert_standard_calls(run_command_mock, clone_config, commit=DEMO_COMMIT)
255-
assert run_command_mock.call_count == expected_call_count
142+
repo_exists_true.assert_any_call(clone_config.url, token=None)
143+
144+
# Verify GitPython calls were made
145+
mock_git_cmd = gitpython_mocks["git_cmd"]
146+
mock_repo = gitpython_mocks["repo"]
147+
mock_clone_from = gitpython_mocks["clone_from"]
148+
149+
# Should have resolved the commit via ls_remote
150+
mock_git_cmd.ls_remote.assert_called()
151+
# Should have cloned the repo
152+
mock_clone_from.assert_called_once()
153+
# Should have fetched and checked out
154+
mock_repo.git.fetch.assert_called()
155+
mock_repo.git.checkout.assert_called()
256156

257157

258158
@pytest.mark.asyncio
259-
async def test_clone_creates_parent_directory(tmp_path: Path, run_command_mock: AsyncMock) -> None:
159+
async def test_clone_creates_parent_directory(tmp_path: Path, gitpython_mocks: dict) -> None:
260160
"""Test that ``clone_repo`` creates parent directories if they don't exist.
261161
262162
Given a local path with non-existent parent directories:
263163
When ``clone_repo`` is called,
264164
Then it should create the parent directories before attempting to clone.
265165
"""
266-
expected_call_count = GIT_INSTALLED_CALLS + 4 # ensure_git_installed + resolve_commit + clone + fetch + checkout
267166
nested_path = tmp_path / "deep" / "nested" / "path" / "repo"
268-
269167
clone_config = CloneConfig(url=DEMO_URL, local_path=str(nested_path))
270168

271169
await clone_repo(clone_config)
272170

171+
# Verify parent directories were created
273172
assert nested_path.parent.exists()
274-
assert_standard_calls(run_command_mock, clone_config, commit=DEMO_COMMIT)
275-
assert run_command_mock.call_count == expected_call_count
173+
174+
# Verify clone operation happened
175+
mock_clone_from = gitpython_mocks["clone_from"]
176+
mock_clone_from.assert_called_once()
276177

277178

278179
@pytest.mark.asyncio
279-
async def test_clone_with_specific_subpath(run_command_mock: AsyncMock) -> None:
180+
async def test_clone_with_specific_subpath(gitpython_mocks: dict) -> None:
280181
"""Test cloning a repository with a specific subpath.
281182
282183
Given a valid repository URL and a specific subpath:
283184
When ``clone_repo`` is called,
284-
Then the repository should be cloned with sparse checkout enabled and the specified subpath.
185+
Then the repository should be cloned with sparse checkout enabled.
285186
"""
286-
# ensure_git_installed + resolve_commit + clone + sparse-checkout + fetch + checkout
287187
subpath = "src/docs"
288-
expected_call_count = GIT_INSTALLED_CALLS + 5
289188
clone_config = CloneConfig(url=DEMO_URL, local_path=LOCAL_REPO_PATH, subpath=subpath)
290189

291190
await clone_repo(clone_config)
292191

293-
# Verify the clone command includes sparse checkout flags
294-
assert_partial_clone_calls(run_command_mock, clone_config, commit=DEMO_COMMIT)
295-
assert run_command_mock.call_count == expected_call_count
192+
# Verify partial clone (using git.clone instead of Repo.clone_from)
193+
mock_git_cmd = gitpython_mocks["git_cmd"]
194+
mock_git_cmd.clone.assert_called()
195+
196+
# Verify sparse checkout was configured
197+
mock_repo = gitpython_mocks["repo"]
198+
mock_repo.git.sparse_checkout.assert_called()
296199

297200

298201
@pytest.mark.asyncio
299-
async def test_clone_with_commit_and_subpath(run_command_mock: AsyncMock) -> None:
300-
"""Test cloning a repository with both a specific commit and subpath.
202+
async def test_clone_with_include_submodules(gitpython_mocks: dict) -> None:
203+
"""Test cloning a repository with submodules included.
301204
302-
Given a valid repository URL, commit hash, and subpath:
205+
Given a valid URL and ``include_submodules=True``:
303206
When ``clone_repo`` is called,
304-
Then the repository should be cloned with sparse checkout enabled,
305-
checked out at the specific commit, and only include the specified subpath.
207+
Then the repository should update submodules after cloning.
306208
"""
307-
subpath = "src/docs"
308-
expected_call_count = GIT_INSTALLED_CALLS + 4 # ensure_git_installed + clone + sparse-checkout + fetch + checkout
309-
commit_hash = "a" * 40 # Simulating a valid commit hash
310-
clone_config = CloneConfig(url=DEMO_URL, local_path=LOCAL_REPO_PATH, commit=commit_hash, subpath=subpath)
209+
clone_config = CloneConfig(url=DEMO_URL, local_path=LOCAL_REPO_PATH, branch="main", include_submodules=True)
311210

312211
await clone_repo(clone_config)
313212

314-
assert_partial_clone_calls(run_command_mock, clone_config, commit=commit_hash)
315-
assert run_command_mock.call_count == expected_call_count
213+
# Verify submodule update was called
214+
mock_repo = gitpython_mocks["repo"]
215+
mock_repo.git.submodule.assert_called_with("update", "--init", "--recursive", "--depth=1")
316216

317217

318218
@pytest.mark.asyncio
319-
async def test_clone_with_include_submodules(run_command_mock: AsyncMock) -> None:
320-
"""Test cloning a repository with submodules included.
219+
async def test_check_repo_exists_with_redirect(mocker: MockerFixture) -> None:
220+
"""Test ``check_repo_exists`` when a redirect (302) is returned.
321221
322-
Given a valid URL and ``include_submodules=True``:
323-
When ``clone_repo`` is called,
324-
Then the repository should be cloned with ``--recurse-submodules`` in the git command.
222+
Given a URL that responds with "302 Found":
223+
When ``check_repo_exists`` is called,
224+
Then it should return ``False``, indicating the repo is inaccessible.
325225
"""
326-
# ensure_git_installed + resolve_commit + clone + fetch + checkout + checkout submodules
327-
expected_call_count = GIT_INSTALLED_CALLS + 5
328-
clone_config = CloneConfig(url=DEMO_URL, local_path=LOCAL_REPO_PATH, branch="main", include_submodules=True)
226+
mock_exec = mocker.patch("asyncio.create_subprocess_exec", new_callable=AsyncMock)
227+
mock_process = AsyncMock()
228+
mock_process.communicate.return_value = (b"302\n", b"")
229+
mock_process.returncode = 0 # Simulate successful request
230+
mock_exec.return_value = mock_process
329231

330-
await clone_repo(clone_config)
232+
repo_exists = await check_repo_exists(DEMO_URL)
331233

332-
assert_standard_calls(run_command_mock, clone_config, commit=DEMO_COMMIT)
333-
assert_submodule_calls(run_command_mock, clone_config)
334-
assert run_command_mock.call_count == expected_call_count
234+
assert repo_exists is False
335235

336236

337-
def assert_standard_calls(mock: AsyncMock, cfg: CloneConfig, commit: str, *, partial_clone: bool = False) -> None:
338-
"""Assert that the standard clone sequence was called.
339-
340-
Note: With GitPython, some operations are mocked differently as they don't use direct command line calls.
341-
"""
342-
# Git version check should still happen
343-
# Note: GitPython may call git differently, so we check for any git version-related calls
344-
# The exact implementation may vary, so we focus on the core functionality
345-
346-
# For partial clones, we might see different call patterns
347-
# The important thing is that the clone operation succeeded
348237

349238

350-
def assert_partial_clone_calls(mock: AsyncMock, cfg: CloneConfig, commit: str) -> None:
351-
"""Assert that the partial clone sequence was called."""
352-
assert_standard_calls(mock, cfg, commit=commit, partial_clone=True)
353-
# With GitPython, sparse-checkout operations may be called differently
354239

355240

356-
def assert_submodule_calls(mock: AsyncMock, cfg: CloneConfig) -> None:
357-
"""Assert that submodule update commands were called."""
358-
# With GitPython, submodule operations are handled through the repo object
359-
# The exact call pattern may differ from direct git commands

0 commit comments

Comments
 (0)