Add tests for new git branch functionality

This commit is contained in:
Alex Selimov 2025-06-12 21:56:59 -04:00
parent 05e9390bdc
commit 9173399ce8
4 changed files with 465 additions and 0 deletions

0
tests/__init__.py Normal file
View file

410
tests/test_git_diff.py Normal file
View file

@ -0,0 +1,410 @@
"""
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, "")
# Integration test
def test_git_diff_dataclass():
"""Test GitDiff dataclass creation and immutability."""
diff = GitDiff(
file_path="test.py",
old_content="old",
new_content="new",
diff_text="diff",
change_type="modified"
)
assert diff.file_path == "test.py"
assert diff.old_content == "old"
assert diff.new_content == "new"
assert diff.diff_text == "diff"
assert diff.change_type == "modified"
# Test immutability
with pytest.raises(AttributeError):
diff.file_path = "changed.py"
def test_git_diff_dataclass():
"""Test GitAnalysis dataclass creation and immutability."""
diffs = [GitDiff("test.py", "old", "new", "diff", "modified")]
analysis = GitAnalysis(
repository_path=Path("/project"),
current_branch="feature",
base_branch="master",
diffs=diffs,
total_files_changed=1
)
assert analysis.repository_path == Path("/project")
assert analysis.current_branch == "feature"
assert analysis.base_branch == "master"
assert len(analysis.diffs) == 1
assert analysis.total_files_changed == 1
# Test immutability
with pytest.raises(AttributeError):
analysis.current_branch = "changed"