forked from cardosofelipe/fast-next-template
- Updated line breaks and indentation across test modules to enhance clarity and maintain consistent style. - Applied changes to workspace, provider, server, and GitWrapper-related test cases. No functional changes introduced.
944 lines
33 KiB
Python
944 lines
33 KiB
Python
"""
|
|
Tests for the git_wrapper module.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from git import GitCommandError
|
|
|
|
from exceptions import (
|
|
BranchExistsError,
|
|
BranchNotFoundError,
|
|
CheckoutError,
|
|
CloneError,
|
|
CommitError,
|
|
GitError,
|
|
PullError,
|
|
PushError,
|
|
)
|
|
from git_wrapper import GitWrapper, run_in_executor
|
|
from models import FileChangeType
|
|
|
|
|
|
class TestGitWrapperInit:
|
|
"""Tests for GitWrapper initialization."""
|
|
|
|
def test_init_with_valid_path(self, temp_workspace, test_settings):
|
|
"""Test initialization with a valid path."""
|
|
wrapper = GitWrapper(temp_workspace, test_settings)
|
|
assert wrapper.workspace_path == temp_workspace
|
|
assert wrapper.settings == test_settings
|
|
|
|
def test_repo_property_raises_on_non_git(self, temp_workspace, test_settings):
|
|
"""Test that accessing repo on non-git dir raises error."""
|
|
wrapper = GitWrapper(temp_workspace, test_settings)
|
|
with pytest.raises(GitError, match="Not a git repository"):
|
|
_ = wrapper.repo
|
|
|
|
def test_repo_property_works_on_git_dir(self, git_repo, test_settings):
|
|
"""Test that repo property works for git directory."""
|
|
wrapper = GitWrapper(Path(git_repo.working_dir), test_settings)
|
|
assert wrapper.repo is not None
|
|
assert wrapper.repo.head is not None
|
|
|
|
|
|
class TestGitWrapperStatus:
|
|
"""Tests for git status operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_status_clean_repo(self, git_wrapper_with_repo):
|
|
"""Test status on a clean repository."""
|
|
result = await git_wrapper_with_repo.status()
|
|
|
|
assert result.branch == "main"
|
|
assert result.is_clean is True
|
|
assert len(result.staged) == 0
|
|
assert len(result.unstaged) == 0
|
|
assert len(result.untracked) == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_status_with_untracked(self, git_wrapper_with_repo, git_repo):
|
|
"""Test status with untracked files."""
|
|
# Create untracked file
|
|
untracked_file = Path(git_repo.working_dir) / "untracked.txt"
|
|
untracked_file.write_text("untracked content")
|
|
|
|
result = await git_wrapper_with_repo.status()
|
|
|
|
assert result.is_clean is False
|
|
assert "untracked.txt" in result.untracked
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_status_with_modified(self, git_wrapper_with_repo, git_repo):
|
|
"""Test status with modified files."""
|
|
# Modify existing file
|
|
readme = Path(git_repo.working_dir) / "README.md"
|
|
readme.write_text("# Modified content\n")
|
|
|
|
result = await git_wrapper_with_repo.status()
|
|
|
|
assert result.is_clean is False
|
|
assert len(result.unstaged) > 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_status_with_staged(self, git_wrapper_with_repo, git_repo):
|
|
"""Test status with staged changes."""
|
|
# Create and stage a file
|
|
new_file = Path(git_repo.working_dir) / "staged.txt"
|
|
new_file.write_text("staged content")
|
|
git_repo.index.add(["staged.txt"])
|
|
|
|
result = await git_wrapper_with_repo.status()
|
|
|
|
assert result.is_clean is False
|
|
assert len(result.staged) > 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_status_exclude_untracked(self, git_wrapper_with_repo, git_repo):
|
|
"""Test status without untracked files."""
|
|
untracked_file = Path(git_repo.working_dir) / "untracked.txt"
|
|
untracked_file.write_text("untracked")
|
|
|
|
result = await git_wrapper_with_repo.status(include_untracked=False)
|
|
|
|
assert len(result.untracked) == 0
|
|
|
|
|
|
class TestGitWrapperBranch:
|
|
"""Tests for branch operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_branch(self, git_wrapper_with_repo):
|
|
"""Test creating a new branch."""
|
|
result = await git_wrapper_with_repo.create_branch("feature-test")
|
|
|
|
assert result.success is True
|
|
assert result.branch == "feature-test"
|
|
assert result.is_current is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_branch_without_checkout(self, git_wrapper_with_repo):
|
|
"""Test creating branch without checkout."""
|
|
result = await git_wrapper_with_repo.create_branch(
|
|
"feature-no-checkout", checkout=False
|
|
)
|
|
|
|
assert result.success is True
|
|
assert result.branch == "feature-no-checkout"
|
|
assert result.is_current is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_branch_exists_error(self, git_wrapper_with_repo):
|
|
"""Test error when branch already exists."""
|
|
await git_wrapper_with_repo.create_branch("existing-branch", checkout=False)
|
|
|
|
with pytest.raises(BranchExistsError):
|
|
await git_wrapper_with_repo.create_branch("existing-branch")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_branch(self, git_wrapper_with_repo):
|
|
"""Test deleting a branch."""
|
|
# Create branch first
|
|
await git_wrapper_with_repo.create_branch("to-delete", checkout=False)
|
|
|
|
# Delete it
|
|
result = await git_wrapper_with_repo.delete_branch("to-delete")
|
|
|
|
assert result.success is True
|
|
assert result.branch == "to-delete"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_branch_not_found(self, git_wrapper_with_repo):
|
|
"""Test error when deleting non-existent branch."""
|
|
with pytest.raises(BranchNotFoundError):
|
|
await git_wrapper_with_repo.delete_branch("nonexistent")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_current_branch_error(self, git_wrapper_with_repo):
|
|
"""Test error when deleting current branch."""
|
|
with pytest.raises(GitError, match="Cannot delete current branch"):
|
|
await git_wrapper_with_repo.delete_branch("main")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_branches(self, git_wrapper_with_repo):
|
|
"""Test listing branches."""
|
|
# Create some branches
|
|
await git_wrapper_with_repo.create_branch("branch-a", checkout=False)
|
|
await git_wrapper_with_repo.create_branch("branch-b", checkout=False)
|
|
|
|
result = await git_wrapper_with_repo.list_branches()
|
|
|
|
assert result.current_branch == "main"
|
|
branch_names = [b["name"] for b in result.local_branches]
|
|
assert "main" in branch_names
|
|
assert "branch-a" in branch_names
|
|
assert "branch-b" in branch_names
|
|
|
|
|
|
class TestGitWrapperCheckout:
|
|
"""Tests for checkout operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_checkout_existing_branch(self, git_wrapper_with_repo):
|
|
"""Test checkout of existing branch."""
|
|
# Create branch first
|
|
await git_wrapper_with_repo.create_branch("test-branch", checkout=False)
|
|
|
|
result = await git_wrapper_with_repo.checkout("test-branch")
|
|
|
|
assert result.success is True
|
|
assert result.ref == "test-branch"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_checkout_create_new(self, git_wrapper_with_repo):
|
|
"""Test checkout with branch creation."""
|
|
result = await git_wrapper_with_repo.checkout("new-branch", create_branch=True)
|
|
|
|
assert result.success is True
|
|
assert result.ref == "new-branch"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_checkout_nonexistent_error(self, git_wrapper_with_repo):
|
|
"""Test error when checking out non-existent ref."""
|
|
with pytest.raises(CheckoutError):
|
|
await git_wrapper_with_repo.checkout("nonexistent-branch")
|
|
|
|
|
|
class TestGitWrapperCommit:
|
|
"""Tests for commit operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_commit_staged_changes(self, git_wrapper_with_repo, git_repo):
|
|
"""Test committing staged changes."""
|
|
# Create and stage a file
|
|
new_file = Path(git_repo.working_dir) / "newfile.txt"
|
|
new_file.write_text("new content")
|
|
git_repo.index.add(["newfile.txt"])
|
|
|
|
result = await git_wrapper_with_repo.commit("Add new file")
|
|
|
|
assert result.success is True
|
|
assert result.message == "Add new file"
|
|
assert result.files_changed == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_commit_all_changes(self, git_wrapper_with_repo, git_repo):
|
|
"""Test committing all changes (auto-stage)."""
|
|
# Create a file without staging
|
|
new_file = Path(git_repo.working_dir) / "unstaged.txt"
|
|
new_file.write_text("content")
|
|
|
|
result = await git_wrapper_with_repo.commit("Commit unstaged")
|
|
|
|
assert result.success is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_commit_nothing_to_commit(self, git_wrapper_with_repo):
|
|
"""Test error when nothing to commit."""
|
|
with pytest.raises(CommitError, match="Nothing to commit"):
|
|
await git_wrapper_with_repo.commit("Empty commit")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_commit_with_author(self, git_wrapper_with_repo, git_repo):
|
|
"""Test commit with custom author."""
|
|
new_file = Path(git_repo.working_dir) / "authored.txt"
|
|
new_file.write_text("authored content")
|
|
|
|
result = await git_wrapper_with_repo.commit(
|
|
"Custom author commit",
|
|
author_name="Custom Author",
|
|
author_email="custom@test.com",
|
|
)
|
|
|
|
assert result.success is True
|
|
|
|
|
|
class TestGitWrapperDiff:
|
|
"""Tests for diff operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_no_changes(self, git_wrapper_with_repo):
|
|
"""Test diff with no changes."""
|
|
result = await git_wrapper_with_repo.diff()
|
|
|
|
assert result.files_changed == 0
|
|
assert result.total_additions == 0
|
|
assert result.total_deletions == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_with_changes(self, git_wrapper_with_repo, git_repo):
|
|
"""Test diff with modified files."""
|
|
# Modify a file
|
|
readme = Path(git_repo.working_dir) / "README.md"
|
|
readme.write_text("# Modified\nNew line\n")
|
|
|
|
result = await git_wrapper_with_repo.diff()
|
|
|
|
assert result.files_changed > 0
|
|
|
|
|
|
class TestGitWrapperLog:
|
|
"""Tests for log operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_basic(self, git_wrapper_with_repo):
|
|
"""Test basic log."""
|
|
result = await git_wrapper_with_repo.log()
|
|
|
|
assert result.total_commits > 0
|
|
assert len(result.commits) > 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_with_limit(self, git_wrapper_with_repo, git_repo):
|
|
"""Test log with limit."""
|
|
# Create more commits
|
|
for i in range(5):
|
|
file_path = Path(git_repo.working_dir) / f"file{i}.txt"
|
|
file_path.write_text(f"content {i}")
|
|
git_repo.index.add([f"file{i}.txt"])
|
|
git_repo.index.commit(f"Commit {i}")
|
|
|
|
result = await git_wrapper_with_repo.log(limit=3)
|
|
|
|
assert len(result.commits) == 3
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_commit_info(self, git_wrapper_with_repo):
|
|
"""Test that log returns proper commit info."""
|
|
result = await git_wrapper_with_repo.log(limit=1)
|
|
|
|
commit = result.commits[0]
|
|
assert "sha" in commit
|
|
assert "message" in commit
|
|
assert "author_name" in commit
|
|
assert "author_email" in commit
|
|
|
|
|
|
class TestGitWrapperUtilities:
|
|
"""Tests for utility methods."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_is_valid_ref_true(self, git_wrapper_with_repo):
|
|
"""Test valid ref detection."""
|
|
is_valid = await git_wrapper_with_repo.is_valid_ref("main")
|
|
assert is_valid is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_is_valid_ref_false(self, git_wrapper_with_repo):
|
|
"""Test invalid ref detection."""
|
|
is_valid = await git_wrapper_with_repo.is_valid_ref("nonexistent")
|
|
assert is_valid is False
|
|
|
|
def test_diff_to_change_type(self, git_wrapper_with_repo):
|
|
"""Test change type conversion."""
|
|
wrapper = git_wrapper_with_repo
|
|
|
|
assert wrapper._diff_to_change_type("A") == FileChangeType.ADDED
|
|
assert wrapper._diff_to_change_type("M") == FileChangeType.MODIFIED
|
|
assert wrapper._diff_to_change_type("D") == FileChangeType.DELETED
|
|
assert wrapper._diff_to_change_type("R") == FileChangeType.RENAMED
|
|
|
|
|
|
class TestGitWrapperStage:
|
|
"""Tests for staging operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stage_specific_files(self, git_wrapper_with_repo, git_repo):
|
|
"""Test staging specific files."""
|
|
# Create files
|
|
file1 = Path(git_repo.working_dir) / "file1.txt"
|
|
file2 = Path(git_repo.working_dir) / "file2.txt"
|
|
file1.write_text("content 1")
|
|
file2.write_text("content 2")
|
|
|
|
count = await git_wrapper_with_repo.stage(["file1.txt"])
|
|
|
|
assert count == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stage_all(self, git_wrapper_with_repo, git_repo):
|
|
"""Test staging all files."""
|
|
file1 = Path(git_repo.working_dir) / "all1.txt"
|
|
file2 = Path(git_repo.working_dir) / "all2.txt"
|
|
file1.write_text("content 1")
|
|
file2.write_text("content 2")
|
|
|
|
count = await git_wrapper_with_repo.stage()
|
|
|
|
assert count >= 2
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unstage_files(self, git_wrapper_with_repo, git_repo):
|
|
"""Test unstaging files."""
|
|
# Create and stage file
|
|
file1 = Path(git_repo.working_dir) / "unstage.txt"
|
|
file1.write_text("to unstage")
|
|
git_repo.index.add(["unstage.txt"])
|
|
|
|
count = await git_wrapper_with_repo.unstage()
|
|
|
|
assert count >= 1
|
|
|
|
|
|
class TestGitWrapperReset:
|
|
"""Tests for reset operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reset_soft(self, git_wrapper_with_repo, git_repo):
|
|
"""Test soft reset."""
|
|
# Create a commit to reset
|
|
file1 = Path(git_repo.working_dir) / "reset_soft.txt"
|
|
file1.write_text("content")
|
|
git_repo.index.add(["reset_soft.txt"])
|
|
git_repo.index.commit("Commit to reset")
|
|
|
|
result = await git_wrapper_with_repo.reset("HEAD~1", mode="soft")
|
|
|
|
assert result is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reset_mixed(self, git_wrapper_with_repo, git_repo):
|
|
"""Test mixed reset (default)."""
|
|
file1 = Path(git_repo.working_dir) / "reset_mixed.txt"
|
|
file1.write_text("content")
|
|
git_repo.index.add(["reset_mixed.txt"])
|
|
git_repo.index.commit("Commit to reset")
|
|
|
|
result = await git_wrapper_with_repo.reset("HEAD~1", mode="mixed")
|
|
|
|
assert result is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reset_invalid_mode(self, git_wrapper_with_repo):
|
|
"""Test error on invalid reset mode."""
|
|
with pytest.raises(GitError, match="Invalid reset mode"):
|
|
await git_wrapper_with_repo.reset("HEAD", mode="invalid")
|
|
|
|
|
|
class TestGitWrapperStash:
|
|
"""Tests for stash operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stash_changes(self, git_wrapper_with_repo, git_repo):
|
|
"""Test stashing changes."""
|
|
# Make changes
|
|
readme = Path(git_repo.working_dir) / "README.md"
|
|
readme.write_text("Modified for stash")
|
|
|
|
result = await git_wrapper_with_repo.stash("Test stash")
|
|
|
|
# Result should be stash ref or None if nothing to stash
|
|
# (depends on whether changes were already staged)
|
|
assert result is None or result.startswith("stash@")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stash_nothing(self, git_wrapper_with_repo):
|
|
"""Test stash with no changes."""
|
|
result = await git_wrapper_with_repo.stash()
|
|
|
|
assert result is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stash_pop(self, git_wrapper_with_repo, git_repo):
|
|
"""Test popping a stash."""
|
|
# Make changes and stash them
|
|
readme = Path(git_repo.working_dir) / "README.md"
|
|
original_content = readme.read_text()
|
|
readme.write_text("Modified for stash pop test")
|
|
git_repo.index.add(["README.md"])
|
|
|
|
stash_ref = await git_wrapper_with_repo.stash("Test stash for pop")
|
|
|
|
if stash_ref:
|
|
# Pop the stash
|
|
result = await git_wrapper_with_repo.stash_pop()
|
|
assert result is True
|
|
|
|
|
|
class TestGitWrapperRepoProperty:
|
|
"""Tests for repo property edge cases."""
|
|
|
|
def test_repo_property_path_not_exists(self, test_settings):
|
|
"""Test that accessing repo on non-existent path raises error."""
|
|
wrapper = GitWrapper(
|
|
Path("/nonexistent/path/that/does/not/exist"), test_settings
|
|
)
|
|
with pytest.raises(GitError, match="Path does not exist"):
|
|
_ = wrapper.repo
|
|
|
|
def test_refresh_repo(self, git_wrapper_with_repo):
|
|
"""Test _refresh_repo clears cached repo."""
|
|
# Access repo to cache it
|
|
_ = git_wrapper_with_repo.repo
|
|
assert git_wrapper_with_repo._repo is not None
|
|
|
|
# Refresh should clear it
|
|
git_wrapper_with_repo._refresh_repo()
|
|
assert git_wrapper_with_repo._repo is None
|
|
|
|
|
|
class TestGitWrapperBranchAdvanced:
|
|
"""Advanced tests for branch operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_branch_from_ref(self, git_wrapper_with_repo, git_repo):
|
|
"""Test creating branch from specific ref."""
|
|
# Get current HEAD SHA
|
|
head_sha = git_repo.head.commit.hexsha
|
|
|
|
result = await git_wrapper_with_repo.create_branch(
|
|
"feature-from-ref",
|
|
from_ref=head_sha,
|
|
checkout=False,
|
|
)
|
|
|
|
assert result.success is True
|
|
assert result.branch == "feature-from-ref"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_branch_force(self, git_wrapper_with_repo, git_repo):
|
|
"""Test force deleting a branch."""
|
|
# Create branch and add unmerged commit
|
|
await git_wrapper_with_repo.create_branch("unmerged-branch", checkout=True)
|
|
new_file = Path(git_repo.working_dir) / "unmerged.txt"
|
|
new_file.write_text("unmerged content")
|
|
git_repo.index.add(["unmerged.txt"])
|
|
git_repo.index.commit("Unmerged commit")
|
|
|
|
# Switch back to main
|
|
await git_wrapper_with_repo.checkout("main")
|
|
|
|
# Force delete
|
|
result = await git_wrapper_with_repo.delete_branch(
|
|
"unmerged-branch", force=True
|
|
)
|
|
|
|
assert result.success is True
|
|
|
|
|
|
class TestGitWrapperListBranchesRemote:
|
|
"""Tests for listing remote branches."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_branches_with_remote(self, git_wrapper_with_repo):
|
|
"""Test listing branches including remote."""
|
|
# Even without remotes, this should work
|
|
result = await git_wrapper_with_repo.list_branches(include_remote=True)
|
|
|
|
assert result.current_branch == "main"
|
|
# Remote branches list should be empty for local repo
|
|
assert len(result.remote_branches) == 0
|
|
|
|
|
|
class TestGitWrapperCheckoutAdvanced:
|
|
"""Advanced tests for checkout operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_checkout_create_existing_error(self, git_wrapper_with_repo):
|
|
"""Test error when creating branch that already exists."""
|
|
with pytest.raises(BranchExistsError):
|
|
await git_wrapper_with_repo.checkout("main", create_branch=True)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_checkout_force(self, git_wrapper_with_repo, git_repo):
|
|
"""Test force checkout discards local changes."""
|
|
# Create branch
|
|
await git_wrapper_with_repo.create_branch("force-test", checkout=False)
|
|
|
|
# Make local changes
|
|
readme = Path(git_repo.working_dir) / "README.md"
|
|
readme.write_text("local changes")
|
|
|
|
# Force checkout should work
|
|
result = await git_wrapper_with_repo.checkout("force-test", force=True)
|
|
|
|
assert result.success is True
|
|
|
|
|
|
class TestGitWrapperCommitAdvanced:
|
|
"""Advanced tests for commit operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_commit_specific_files(self, git_wrapper_with_repo, git_repo):
|
|
"""Test committing specific files only."""
|
|
# Create multiple files
|
|
file1 = Path(git_repo.working_dir) / "commit_specific1.txt"
|
|
file2 = Path(git_repo.working_dir) / "commit_specific2.txt"
|
|
file1.write_text("content 1")
|
|
file2.write_text("content 2")
|
|
|
|
result = await git_wrapper_with_repo.commit(
|
|
"Commit specific file",
|
|
files=["commit_specific1.txt"],
|
|
)
|
|
|
|
assert result.success is True
|
|
assert result.files_changed == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_commit_with_partial_author(self, git_wrapper_with_repo, git_repo):
|
|
"""Test commit with only author name."""
|
|
new_file = Path(git_repo.working_dir) / "partial_author.txt"
|
|
new_file.write_text("content")
|
|
|
|
result = await git_wrapper_with_repo.commit(
|
|
"Partial author commit",
|
|
author_name="Test Author",
|
|
)
|
|
|
|
assert result.success is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_commit_allow_empty(self, git_wrapper_with_repo):
|
|
"""Test allowing empty commits."""
|
|
result = await git_wrapper_with_repo.commit(
|
|
"Empty commit allowed",
|
|
allow_empty=True,
|
|
)
|
|
|
|
assert result.success is True
|
|
|
|
|
|
class TestGitWrapperUnstageAdvanced:
|
|
"""Advanced tests for unstaging operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unstage_specific_files(self, git_wrapper_with_repo, git_repo):
|
|
"""Test unstaging specific files."""
|
|
# Create and stage files
|
|
file1 = Path(git_repo.working_dir) / "unstage1.txt"
|
|
file2 = Path(git_repo.working_dir) / "unstage2.txt"
|
|
file1.write_text("content 1")
|
|
file2.write_text("content 2")
|
|
git_repo.index.add(["unstage1.txt", "unstage2.txt"])
|
|
|
|
count = await git_wrapper_with_repo.unstage(["unstage1.txt"])
|
|
|
|
assert count == 1
|
|
|
|
|
|
class TestGitWrapperResetAdvanced:
|
|
"""Advanced tests for reset operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reset_hard(self, git_wrapper_with_repo, git_repo):
|
|
"""Test hard reset."""
|
|
# Create a commit
|
|
file1 = Path(git_repo.working_dir) / "reset_hard.txt"
|
|
file1.write_text("content")
|
|
git_repo.index.add(["reset_hard.txt"])
|
|
git_repo.index.commit("Commit for hard reset")
|
|
|
|
result = await git_wrapper_with_repo.reset("HEAD~1", mode="hard")
|
|
|
|
assert result is True
|
|
# File should be gone after hard reset
|
|
assert not file1.exists()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reset_specific_files(self, git_wrapper_with_repo, git_repo):
|
|
"""Test resetting specific files."""
|
|
# Create and stage a file
|
|
file1 = Path(git_repo.working_dir) / "reset_file.txt"
|
|
file1.write_text("content")
|
|
git_repo.index.add(["reset_file.txt"])
|
|
|
|
result = await git_wrapper_with_repo.reset("HEAD", files=["reset_file.txt"])
|
|
|
|
assert result is True
|
|
|
|
|
|
class TestGitWrapperDiffAdvanced:
|
|
"""Advanced tests for diff operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_between_refs(self, git_wrapper_with_repo, git_repo):
|
|
"""Test diff between two refs."""
|
|
# Create initial commit
|
|
file1 = Path(git_repo.working_dir) / "diff_ref.txt"
|
|
file1.write_text("initial")
|
|
git_repo.index.add(["diff_ref.txt"])
|
|
commit1 = git_repo.index.commit("First commit for diff")
|
|
|
|
# Create second commit
|
|
file1.write_text("modified")
|
|
git_repo.index.add(["diff_ref.txt"])
|
|
commit2 = git_repo.index.commit("Second commit for diff")
|
|
|
|
result = await git_wrapper_with_repo.diff(
|
|
base=commit1.hexsha,
|
|
head=commit2.hexsha,
|
|
)
|
|
|
|
assert result.files_changed > 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_specific_files(self, git_wrapper_with_repo, git_repo):
|
|
"""Test diff for specific files only."""
|
|
# Create files
|
|
file1 = Path(git_repo.working_dir) / "diff_specific1.txt"
|
|
file2 = Path(git_repo.working_dir) / "diff_specific2.txt"
|
|
file1.write_text("content 1")
|
|
file2.write_text("content 2")
|
|
|
|
result = await git_wrapper_with_repo.diff(files=["diff_specific1.txt"])
|
|
|
|
# Should only show changes for specified file
|
|
for f in result.files:
|
|
assert "diff_specific2.txt" not in f.get("path", "")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_base_only(self, git_wrapper_with_repo, git_repo):
|
|
"""Test diff with base ref only (vs HEAD)."""
|
|
# Create commit
|
|
file1 = Path(git_repo.working_dir) / "diff_base.txt"
|
|
file1.write_text("content")
|
|
git_repo.index.add(["diff_base.txt"])
|
|
commit = git_repo.index.commit("Commit for diff base test")
|
|
|
|
# Get parent commit
|
|
parent = commit.parents[0] if commit.parents else commit
|
|
|
|
result = await git_wrapper_with_repo.diff(base=parent.hexsha)
|
|
|
|
assert isinstance(result.files_changed, int)
|
|
|
|
|
|
class TestGitWrapperLogAdvanced:
|
|
"""Advanced tests for log operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_with_ref(self, git_wrapper_with_repo, git_repo):
|
|
"""Test log starting from specific ref."""
|
|
# Create branch with commits
|
|
await git_wrapper_with_repo.create_branch("log-test", checkout=True)
|
|
file1 = Path(git_repo.working_dir) / "log_ref.txt"
|
|
file1.write_text("content")
|
|
git_repo.index.add(["log_ref.txt"])
|
|
git_repo.index.commit("Commit on log-test branch")
|
|
|
|
result = await git_wrapper_with_repo.log(ref="log-test", limit=5)
|
|
|
|
assert result.total_commits > 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_with_path(self, git_wrapper_with_repo, git_repo):
|
|
"""Test log filtered by path."""
|
|
# Create file and commit
|
|
file1 = Path(git_repo.working_dir) / "log_path.txt"
|
|
file1.write_text("content")
|
|
git_repo.index.add(["log_path.txt"])
|
|
git_repo.index.commit("Commit for path log")
|
|
|
|
result = await git_wrapper_with_repo.log(path="log_path.txt")
|
|
|
|
assert result.total_commits >= 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_with_skip(self, git_wrapper_with_repo, git_repo):
|
|
"""Test log with skip parameter."""
|
|
# Create multiple commits
|
|
for i in range(3):
|
|
file_path = Path(git_repo.working_dir) / f"skip_test{i}.txt"
|
|
file_path.write_text(f"content {i}")
|
|
git_repo.index.add([f"skip_test{i}.txt"])
|
|
git_repo.index.commit(f"Skip test commit {i}")
|
|
|
|
result = await git_wrapper_with_repo.log(skip=1, limit=2)
|
|
|
|
# Should have skipped first commit
|
|
assert len(result.commits) <= 2
|
|
|
|
|
|
class TestGitWrapperRemoteUrl:
|
|
"""Tests for remote URL operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_remote_url_nonexistent(self, git_wrapper_with_repo):
|
|
"""Test getting URL for non-existent remote."""
|
|
url = await git_wrapper_with_repo.get_remote_url("nonexistent")
|
|
|
|
assert url is None
|
|
|
|
|
|
class TestGitWrapperConfig:
|
|
"""Tests for git config operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_and_get_config(self, git_wrapper_with_repo):
|
|
"""Test setting and getting config value."""
|
|
await git_wrapper_with_repo.set_config("test.key", "test_value")
|
|
|
|
value = await git_wrapper_with_repo.get_config("test.key")
|
|
|
|
assert value == "test_value"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_config_nonexistent(self, git_wrapper_with_repo):
|
|
"""Test getting non-existent config value."""
|
|
value = await git_wrapper_with_repo.get_config("nonexistent.key")
|
|
|
|
assert value is None
|
|
|
|
|
|
class TestGitWrapperClone:
|
|
"""Tests for clone operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clone_success(self, temp_workspace, test_settings):
|
|
"""Test successful clone."""
|
|
wrapper = GitWrapper(temp_workspace, test_settings)
|
|
|
|
# Mock the clone operation
|
|
with patch("git_wrapper.GitRepo") as mock_repo_class:
|
|
mock_repo = MagicMock()
|
|
mock_repo.active_branch.name = "main"
|
|
mock_repo.head.commit.hexsha = "abc123"
|
|
mock_repo_class.clone_from.return_value = mock_repo
|
|
|
|
result = await wrapper.clone("https://github.com/test/repo.git")
|
|
|
|
assert result.success is True
|
|
assert result.branch == "main"
|
|
assert result.commit_sha == "abc123"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clone_with_auth_token(self, temp_workspace, test_settings):
|
|
"""Test clone with auth token."""
|
|
wrapper = GitWrapper(temp_workspace, test_settings)
|
|
|
|
with patch("git_wrapper.GitRepo") as mock_repo_class:
|
|
mock_repo = MagicMock()
|
|
mock_repo.active_branch.name = "main"
|
|
mock_repo.head.commit.hexsha = "abc123"
|
|
mock_repo_class.clone_from.return_value = mock_repo
|
|
|
|
result = await wrapper.clone(
|
|
"https://github.com/test/repo.git",
|
|
auth_token="test-token",
|
|
)
|
|
|
|
assert result.success is True
|
|
# Verify token was injected in URL
|
|
call_args = mock_repo_class.clone_from.call_args
|
|
assert "test-token@" in call_args.kwargs["url"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clone_with_branch_and_depth(self, temp_workspace, test_settings):
|
|
"""Test clone with branch and depth parameters."""
|
|
wrapper = GitWrapper(temp_workspace, test_settings)
|
|
|
|
with patch("git_wrapper.GitRepo") as mock_repo_class:
|
|
mock_repo = MagicMock()
|
|
mock_repo.active_branch.name = "develop"
|
|
mock_repo.head.commit.hexsha = "def456"
|
|
mock_repo_class.clone_from.return_value = mock_repo
|
|
|
|
result = await wrapper.clone(
|
|
"https://github.com/test/repo.git",
|
|
branch="develop",
|
|
depth=1,
|
|
)
|
|
|
|
assert result.success is True
|
|
call_args = mock_repo_class.clone_from.call_args
|
|
assert call_args.kwargs["branch"] == "develop"
|
|
assert call_args.kwargs["depth"] == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clone_failure(self, temp_workspace, test_settings):
|
|
"""Test clone failure raises CloneError."""
|
|
wrapper = GitWrapper(temp_workspace, test_settings)
|
|
|
|
with patch("git_wrapper.GitRepo") as mock_repo_class:
|
|
mock_repo_class.clone_from.side_effect = GitCommandError(
|
|
"git clone", 128, stderr="Authentication failed"
|
|
)
|
|
|
|
with pytest.raises(CloneError):
|
|
await wrapper.clone("https://github.com/test/repo.git")
|
|
|
|
|
|
class TestGitWrapperPush:
|
|
"""Tests for push operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_push_force_disabled(self, git_wrapper_with_repo, git_repo):
|
|
"""Test force push is disabled by default."""
|
|
git_repo.create_remote("origin", "https://github.com/test/repo.git")
|
|
|
|
with pytest.raises(PushError, match="Force push is disabled"):
|
|
await git_wrapper_with_repo.push(force=True)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_push_remote_not_found(self, git_wrapper_with_repo):
|
|
"""Test push to non-existent remote."""
|
|
with pytest.raises(PushError, match="Remote not found"):
|
|
await git_wrapper_with_repo.push(remote="nonexistent")
|
|
|
|
|
|
class TestGitWrapperPull:
|
|
"""Tests for pull operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pull_remote_not_found(self, git_wrapper_with_repo):
|
|
"""Test pull from non-existent remote."""
|
|
with pytest.raises(PullError, match="Remote not found"):
|
|
await git_wrapper_with_repo.pull(remote="nonexistent")
|
|
|
|
|
|
class TestGitWrapperFetch:
|
|
"""Tests for fetch operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fetch_remote_not_found(self, git_wrapper_with_repo):
|
|
"""Test fetch from non-existent remote."""
|
|
with pytest.raises(GitError, match="Remote not found"):
|
|
await git_wrapper_with_repo.fetch(remote="nonexistent")
|
|
|
|
|
|
class TestGitWrapperDiffHeadOnly:
|
|
"""Tests for diff with head ref only."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_head_only(self, git_wrapper_with_repo, git_repo):
|
|
"""Test diff with head ref only (working tree vs ref)."""
|
|
# Make some changes
|
|
readme = Path(git_repo.working_dir) / "README.md"
|
|
readme.write_text("modified content")
|
|
|
|
# This tests the head-only branch (base=None, head=specified)
|
|
result = await git_wrapper_with_repo.diff(head="HEAD")
|
|
|
|
assert isinstance(result.files_changed, int)
|
|
|
|
|
|
class TestGitWrapperRemoteWithUrl:
|
|
"""Tests for getting remote URL when remote exists."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_remote_url_exists(self, git_wrapper_with_repo, git_repo):
|
|
"""Test getting URL for existing remote."""
|
|
git_repo.create_remote("origin", "https://github.com/test/repo.git")
|
|
|
|
url = await git_wrapper_with_repo.get_remote_url("origin")
|
|
|
|
assert url == "https://github.com/test/repo.git"
|
|
|
|
|
|
class TestRunInExecutor:
|
|
"""Tests for run_in_executor utility."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_in_executor(self):
|
|
"""Test running function in executor."""
|
|
|
|
def blocking_func(x, y):
|
|
return x + y
|
|
|
|
result = await run_in_executor(blocking_func, 1, 2)
|
|
|
|
assert result == 3
|