ReviewLlama/tests/test_git_diff.py

385 lines
12 KiB
Python

"""
Unit tests for git analysis functionality using pytest.
"""
from pathlib import Path
from unittest.mock import Mock, PropertyMock, patch
import pytest
from git.exc import GitCommandError, InvalidGitRepositoryError
from reviewllama.git_diff import (
GitAnalysis,
GitDiff,
analyze_git_repository,
branch_exists,
create_git_diff,
determine_change_type,
extract_file_content,
filter_reviewable_diffs,
find_git_repository,
get_base_branch,
get_current_branch_name,
get_diff_between_branches,
process_diff_items,
)
# Fixtures
@pytest.fixture
def mock_repo():
"""Create a mock git repository."""
repo = Mock()
repo.active_branch.name = "feature-branch"
repo.head.commit.hexsha = "abc123def456"
return repo
@pytest.fixture
def mock_diff_item():
"""Create a mock git diff item."""
diff = Mock()
diff.a_path = "src/main.py"
diff.b_path = "src/main.py"
diff.new_file = False
diff.deleted_file = False
diff.a_blob = Mock()
diff.b_blob = Mock()
diff.a_blob.data_stream.read.return_value = b"old content"
diff.b_blob.data_stream.read.return_value = b"new content"
diff.__str__ = Mock(return_value="diff content")
return diff
@pytest.fixture
def sample_git_diffs():
"""Create sample GitDiff objects for testing."""
return [
GitDiff("src/main.py", "old", "new", "diff", "modified"),
GitDiff("test.js", "old", "new", "diff", "added"),
GitDiff("README.md", "old", "new", "diff", "modified"),
GitDiff("image.png", "old", "new", "diff", "added"),
]
# Tests for find_git_repository
@patch("reviewllama.git_diff.Repo")
def test_find_git_repository_success(mock_repo_class):
"""Test successful git repository detection."""
mock_repo_instance = Mock()
mock_repo_class.return_value = mock_repo_instance
path = Path("/project")
result = find_git_repository(path)
mock_repo_class.assert_called_once_with(path, search_parent_directories=True)
assert result == mock_repo_instance
@patch("reviewllama.git_diff.Repo")
def test_find_git_repository_not_found(mock_repo_class):
"""Test git repository not found."""
mock_repo_class.side_effect = InvalidGitRepositoryError()
path = Path("/not-a-repo")
with pytest.raises(ValueError, match="No git repository found"):
find_git_repository(path)
# Tests for get_current_branch_name
def test_get_current_branch_name_active_branch(mock_repo):
"""Test getting current branch name from active branch."""
result = get_current_branch_name(mock_repo)
assert result == "feature-branch"
def test_get_current_branch_name_detached_head(mock_repo):
"""Test getting current branch name in detached HEAD state."""
type(mock_repo.active_branch).name = PropertyMock(side_effect=TypeError())
result = get_current_branch_name(mock_repo)
assert result == "abc123de" # First 8 chars of hexsha
# Tests for branch_exists
def test_branch_exists_true(mock_repo):
"""Test branch exists returns True when branch found."""
mock_repo.commit.return_value = Mock()
result = branch_exists(mock_repo, "main")
assert result is True
mock_repo.commit.assert_called_once_with("main")
def test_branch_exists_false(mock_repo):
"""Test branch exists returns False when branch not found."""
mock_repo.commit.side_effect = Exception("Branch not found")
result = branch_exists(mock_repo, "nonexistent")
assert result is False
# Tests for get_base_branch
@patch("reviewllama.git_diff.branch_exists")
def test_get_base_branch_requested_exists(mock_branch_exists, mock_repo):
"""Test get_base_branch returns requested branch when it exists."""
mock_branch_exists.return_value = True
result = get_base_branch(mock_repo, "develop")
assert result == "develop"
mock_branch_exists.assert_called_once_with(mock_repo, "develop")
@patch("reviewllama.git_diff.branch_exists")
def test_get_base_branch_fallback_to_master(mock_branch_exists, mock_repo):
"""Test get_base_branch falls back to master when requested not found."""
mock_branch_exists.side_effect = [False, True] # requested=False, master=True
result = get_base_branch(mock_repo, "nonexistent")
assert result == "master"
@patch("reviewllama.git_diff.branch_exists")
def test_get_base_branch_fallback_to_main(mock_branch_exists, mock_repo):
"""Test get_base_branch falls back to main when master not found."""
mock_branch_exists.side_effect = [
False,
False,
True,
] # requested=False, master=False, main=True
result = get_base_branch(mock_repo, "nonexistent")
assert result == "main"
@patch("reviewllama.git_diff.branch_exists")
def test_get_base_branch_no_fallback_found(mock_branch_exists, mock_repo):
"""Test get_base_branch raises error when no fallback found."""
mock_branch_exists.return_value = False
with pytest.raises(ValueError, match="Base branch 'nonexistent' not found"):
get_base_branch(mock_repo, "nonexistent")
# Tests for get_diff_between_branches
def test_get_diff_between_branches_success(mock_repo):
"""Test successful diff between branches."""
base_commit = Mock()
current_commit = Mock()
mock_diff = Mock()
mock_repo.commit.side_effect = [base_commit, current_commit]
base_commit.diff.return_value = mock_diff
result = get_diff_between_branches(mock_repo, "master", "feature")
assert result == mock_diff
base_commit.diff.assert_called_once_with(current_commit)
def test_get_diff_between_branches_git_error(mock_repo):
"""Test diff between branches with git error."""
mock_repo.commit.side_effect = GitCommandError("git", "error")
with pytest.raises(ValueError, match="Failed to get diff"):
get_diff_between_branches(mock_repo, "master", "feature")
# Tests for determine_change_type
def test_determine_change_type_added():
"""Test determine_change_type for added file."""
diff = Mock()
diff.new_file = True
diff.deleted_file = False
result = determine_change_type(diff)
assert result == "added"
def test_determine_change_type_deleted():
"""Test determine_change_type for deleted file."""
diff = Mock()
diff.new_file = False
diff.deleted_file = True
result = determine_change_type(diff)
assert result == "deleted"
def test_determine_change_type_modified():
"""Test determine_change_type for modified file."""
diff = Mock()
diff.new_file = False
diff.deleted_file = False
result = determine_change_type(diff)
assert result == "modified"
# Tests for extract_file_content
def test_extract_file_content_old(mock_diff_item):
"""Test extracting old file content."""
result = extract_file_content(mock_diff_item, is_old=True)
assert result == "old content"
mock_diff_item.a_blob.data_stream.read.assert_called_once()
def test_extract_file_content_new(mock_diff_item):
"""Test extracting new file content."""
result = extract_file_content(mock_diff_item, is_old=False)
assert result == "new content"
mock_diff_item.b_blob.data_stream.read.assert_called_once()
def test_extract_file_content_none_blob():
"""Test extracting content when blob is None."""
diff = Mock()
diff.a_blob = None
result = extract_file_content(diff, is_old=True)
assert result == ""
def test_extract_file_content_decode_error():
"""Test extracting content with decode error."""
diff = Mock()
diff.a_blob.data_stream.read.side_effect = UnicodeDecodeError(
"utf-8", b"", 0, 1, "error"
)
result = extract_file_content(diff, is_old=True)
assert result == ""
# Tests for create_git_diff
def test_create_git_diff(mock_diff_item):
"""Test creating GitDiff from git diff item."""
result = create_git_diff(mock_diff_item)
assert isinstance(result, GitDiff)
assert result.file_path == "src/main.py"
assert result.old_content == "old content"
assert result.new_content == "new content"
assert result.diff_text == "diff content"
assert result.change_type == "modified"
def test_create_git_diff_unknown_path():
"""Test creating GitDiff with unknown file path."""
diff = Mock()
diff.a_path = None
diff.b_path = None
diff.new_file = False
diff.deleted_file = False
diff.a_blob = None
diff.b_blob = None
diff.__str__ = Mock(return_value="diff")
result = create_git_diff(diff)
assert result.file_path == "unknown"
# Tests for process_diff_items
def test_process_diff_items():
"""Test processing multiple diff items."""
diff1 = Mock()
diff1.a_path = "file1.py"
diff1.b_path = "file1.py"
diff1.new_file = False
diff1.deleted_file = False
diff1.a_blob = None
diff1.b_blob = None
diff1.__str__ = Mock(return_value="diff1")
diff2 = Mock()
diff2.a_path = "file2.py"
diff2.b_path = "file2.py"
diff2.new_file = True
diff2.deleted_file = False
diff2.a_blob = None
diff2.b_blob = None
diff2.__str__ = Mock(return_value="diff2")
diff_index = [diff1, diff2]
result = process_diff_items(diff_index)
assert len(result) == 2
assert all(isinstance(diff, GitDiff) for diff in result)
assert result[0].file_path == "file1.py"
assert result[1].file_path == "file2.py"
# Tests for filter_reviewable_diffs
def test_filter_reviewable_diffs(sample_git_diffs):
"""Test filtering reviewable diffs."""
result = filter_reviewable_diffs(sample_git_diffs)
# Should include .py and .js files, exclude .md and .png
assert len(result) == 2
file_paths = [diff.file_path for diff in result]
assert "src/main.py" in file_paths
assert "test.js" in file_paths
assert "README.md" not in file_paths
assert "image.png" not in file_paths
def test_filter_reviewable_diffs_empty():
"""Test filtering reviewable diffs with empty input."""
result = filter_reviewable_diffs([])
assert result == []
# Tests for analyze_git_repository
@patch("reviewllama.git_diff.find_git_repository")
@patch("reviewllama.git_diff.get_current_branch_name")
@patch("reviewllama.git_diff.get_base_branch")
@patch("reviewllama.git_diff.get_diff_between_branches")
@patch("reviewllama.git_diff.process_diff_items")
@patch("reviewllama.git_diff.filter_reviewable_diffs")
def test_analyze_git_repository_success(
mock_filter,
mock_process,
mock_get_diff,
mock_get_base,
mock_get_current,
mock_find_repo,
sample_git_diffs,
):
"""Test successful git repository analysis."""
# Setup mocks
mock_repo = Mock()
mock_find_repo.return_value = mock_repo
mock_get_current.return_value = "feature-branch"
mock_get_base.return_value = "master"
mock_get_diff.return_value = Mock()
mock_process.return_value = sample_git_diffs
mock_filter.return_value = sample_git_diffs[:2] # Filter to 2 items
path = Path("/project")
result = analyze_git_repository(path, "main")
# Verify result
assert isinstance(result, GitAnalysis)
assert result.repository_path == path
assert result.current_branch == "feature-branch"
assert result.base_branch == "master"
assert len(result.diffs) == 2
assert result.total_files_changed == 2
# Verify function calls
mock_find_repo.assert_called_once_with(path)
mock_get_current.assert_called_once_with(mock_repo)
mock_get_base.assert_called_once_with(mock_repo, "main")
mock_get_diff.assert_called_once_with(mock_repo, "master", "feature-branch")
@patch("reviewllama.git_diff.find_git_repository")
def test_analyze_git_repository_not_found(mock_find_repo):
"""Test git repository analysis when repo not found."""
mock_find_repo.side_effect = ValueError("No git repository found")
path = Path("/not-a-repo")
with pytest.raises(ValueError, match="No git repository found"):
analyze_git_repository(path, "")