fix: Add Gitea Actions CI workflow and fix linting issues
This commit is contained in:
240
tests/test_generator.py
Normal file
240
tests/test_generator.py
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
"""Tests for commit message generator."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.analyzer import ChangeSet, ChangeType, StagedChange
|
||||||
|
from src.generator import (
|
||||||
|
GenerationError,
|
||||||
|
detect_commit_type,
|
||||||
|
detect_scope,
|
||||||
|
generate_commit_message,
|
||||||
|
generate_description,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDetectCommitType:
|
||||||
|
"""Tests for commit type detection."""
|
||||||
|
|
||||||
|
def test_detects_feat_for_src_files(self):
|
||||||
|
"""Test detecting 'feat' for source files."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("src/main.py", ChangeType.ADDED),
|
||||||
|
StagedChange("src/cli.py", ChangeType.MODIFIED),
|
||||||
|
])
|
||||||
|
result = detect_commit_type(change_set)
|
||||||
|
assert result == "feat"
|
||||||
|
|
||||||
|
def test_detects_fix_for_bug_fixes(self):
|
||||||
|
"""Test detecting 'fix' for bug-related changes."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("bug_fix.py", ChangeType.MODIFIED),
|
||||||
|
StagedChange("fix_login.py", ChangeType.MODIFIED),
|
||||||
|
])
|
||||||
|
result = detect_commit_type(change_set)
|
||||||
|
assert result == "fix"
|
||||||
|
|
||||||
|
def test_detects_docs_for_markdown(self):
|
||||||
|
"""Test detecting 'docs' for markdown files."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("README.md", ChangeType.MODIFIED),
|
||||||
|
StagedChange("docs/guide.md", ChangeType.ADDED),
|
||||||
|
])
|
||||||
|
result = detect_commit_type(change_set)
|
||||||
|
assert result == "docs"
|
||||||
|
|
||||||
|
def test_detects_test_for_test_files(self):
|
||||||
|
"""Test detecting 'test' for test files."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("tests/test_main.py", ChangeType.ADDED),
|
||||||
|
StagedChange("test_main.spec.js", ChangeType.MODIFIED),
|
||||||
|
])
|
||||||
|
result = detect_commit_type(change_set)
|
||||||
|
assert result == "test"
|
||||||
|
|
||||||
|
def test_detects_chore_for_config(self):
|
||||||
|
"""Test detecting 'chore' for configuration files."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("package.json", ChangeType.MODIFIED),
|
||||||
|
StagedChange(".gitignore", ChangeType.ADDED),
|
||||||
|
])
|
||||||
|
result = detect_commit_type(change_set)
|
||||||
|
assert result == "chore"
|
||||||
|
|
||||||
|
def test_falls_back_to_chore(self):
|
||||||
|
"""Test falling back to 'chore' when no pattern matches."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("unknown/file.xyz", ChangeType.ADDED),
|
||||||
|
])
|
||||||
|
result = detect_commit_type(change_set)
|
||||||
|
assert result == "chore"
|
||||||
|
|
||||||
|
def test_custom_type_rules(self):
|
||||||
|
"""Test using custom type rules."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("custom/feature.py", ChangeType.ADDED),
|
||||||
|
])
|
||||||
|
custom_rules = {"custom_type": ["custom/"]}
|
||||||
|
result = detect_commit_type(change_set, custom_rules)
|
||||||
|
assert result == "custom_type"
|
||||||
|
|
||||||
|
|
||||||
|
class TestDetectScope:
|
||||||
|
"""Tests for scope detection."""
|
||||||
|
|
||||||
|
def test_detects_single_scope(self):
|
||||||
|
"""Test detecting single scope from directory."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("src/cli/main.py", ChangeType.ADDED),
|
||||||
|
StagedChange("src/cli/commands.py", ChangeType.MODIFIED),
|
||||||
|
])
|
||||||
|
result = detect_scope(change_set)
|
||||||
|
assert result == "src"
|
||||||
|
|
||||||
|
def test_detects_multiple_scopes(self):
|
||||||
|
"""Test detecting multiple scopes."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("src/a.py", ChangeType.ADDED),
|
||||||
|
StagedChange("lib/b.py", ChangeType.MODIFIED),
|
||||||
|
])
|
||||||
|
result = detect_scope(change_set)
|
||||||
|
assert "src" in result and "lib" in result
|
||||||
|
|
||||||
|
def test_empty_for_no_changes(self):
|
||||||
|
"""Test returning empty string for no changes."""
|
||||||
|
change_set = ChangeSet([])
|
||||||
|
result = detect_scope(change_set)
|
||||||
|
assert result == ""
|
||||||
|
|
||||||
|
def test_root_file_no_scope(self):
|
||||||
|
"""Test no scope for root-level files."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("main.py", ChangeType.ADDED),
|
||||||
|
])
|
||||||
|
result = detect_scope(change_set)
|
||||||
|
assert result == ""
|
||||||
|
|
||||||
|
|
||||||
|
class TestGenerateDescription:
|
||||||
|
"""Tests for description generation."""
|
||||||
|
|
||||||
|
def test_describes_single_added(self):
|
||||||
|
"""Test generating description for single added file."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("new_file.py", ChangeType.ADDED),
|
||||||
|
])
|
||||||
|
result = generate_description(change_set)
|
||||||
|
assert result == "add new_file.py"
|
||||||
|
|
||||||
|
def test_describes_single_deleted(self):
|
||||||
|
"""Test generating description for deleted file."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("old_file.py", ChangeType.DELETED),
|
||||||
|
])
|
||||||
|
result = generate_description(change_set)
|
||||||
|
assert result == "remove old_file.py"
|
||||||
|
|
||||||
|
def test_describes_single_modified(self):
|
||||||
|
"""Test generating description for modified file."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("existing.py", ChangeType.MODIFIED),
|
||||||
|
])
|
||||||
|
result = generate_description(change_set)
|
||||||
|
assert result == "update existing.py"
|
||||||
|
|
||||||
|
def test_describes_multiple_added(self):
|
||||||
|
"""Test generating description for multiple added files."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("a.py", ChangeType.ADDED),
|
||||||
|
StagedChange("b.py", ChangeType.ADDED),
|
||||||
|
])
|
||||||
|
result = generate_description(change_set)
|
||||||
|
assert result == "add 2 files"
|
||||||
|
|
||||||
|
def test_describes_multiple_modified(self):
|
||||||
|
"""Test generating description for multiple modified files."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("a.py", ChangeType.MODIFIED),
|
||||||
|
StagedChange("b.py", ChangeType.MODIFIED),
|
||||||
|
])
|
||||||
|
result = generate_description(change_set)
|
||||||
|
assert result == "update 2 files"
|
||||||
|
|
||||||
|
def test_default_for_empty(self):
|
||||||
|
"""Test default description for empty change set."""
|
||||||
|
change_set = ChangeSet([])
|
||||||
|
result = generate_description(change_set)
|
||||||
|
assert result == "update"
|
||||||
|
|
||||||
|
|
||||||
|
class TestGenerateCommitMessage:
|
||||||
|
"""Tests for full message generation."""
|
||||||
|
|
||||||
|
@patch("src.generator.ChangeAnalyzer")
|
||||||
|
def test_generates_valid_message(self, mock_analyzer):
|
||||||
|
"""Test generating a complete commit message."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("src/main.py", ChangeType.ADDED),
|
||||||
|
])
|
||||||
|
mock_analyzer.return_value.get_staged_changes.return_value = change_set
|
||||||
|
|
||||||
|
result = generate_commit_message()
|
||||||
|
|
||||||
|
assert "feat" in result
|
||||||
|
assert "src" in result
|
||||||
|
|
||||||
|
def test_raises_error_on_no_changes(self):
|
||||||
|
"""Test raising error when no staged changes."""
|
||||||
|
with patch("src.generator.ChangeAnalyzer") as mock_analyzer:
|
||||||
|
mock_analyzer.return_value.get_staged_changes.return_value = ChangeSet([])
|
||||||
|
|
||||||
|
with pytest.raises(GenerationError, match="No staged changes"):
|
||||||
|
generate_commit_message()
|
||||||
|
|
||||||
|
def test_raises_error_on_invalid_repo(self):
|
||||||
|
"""Test raising error for invalid repository."""
|
||||||
|
with patch("src.generator.ChangeAnalyzer") as mock_analyzer:
|
||||||
|
mock_analyzer.return_value.get_staged_changes.side_effect = ValueError(
|
||||||
|
"Not a git repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(GenerationError, match="Not a git repository"):
|
||||||
|
generate_commit_message()
|
||||||
|
|
||||||
|
@patch("src.generator.ChangeAnalyzer")
|
||||||
|
def test_respects_config_template(self, mock_analyzer):
|
||||||
|
"""Test using custom template from config."""
|
||||||
|
change_set = ChangeSet([
|
||||||
|
StagedChange("src/test.py", ChangeType.ADDED),
|
||||||
|
])
|
||||||
|
custom_config = {"template": "CUSTOM: {description}"}
|
||||||
|
|
||||||
|
mock_analyzer.return_value.get_staged_changes.return_value = change_set
|
||||||
|
|
||||||
|
with patch("src.generator.load_config") as mock_load_config:
|
||||||
|
mock_load_config.return_value = custom_config
|
||||||
|
|
||||||
|
result = generate_commit_message()
|
||||||
|
|
||||||
|
assert result.startswith("CUSTOM:")
|
||||||
|
|
||||||
|
@patch("src.generator.ChangeAnalyzer")
|
||||||
|
def test_max_files_limit(self, mock_analyzer):
|
||||||
|
"""Test respecting max files configuration."""
|
||||||
|
changes = [StagedChange(f"file{i}.py", ChangeType.ADDED) for i in range(10)]
|
||||||
|
change_set = ChangeSet(changes)
|
||||||
|
custom_config = {
|
||||||
|
"include_file_list": True,
|
||||||
|
"max_files": 3,
|
||||||
|
"template": "{files}"
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_analyzer.return_value.get_staged_changes.return_value = change_set
|
||||||
|
|
||||||
|
with patch("src.generator.load_config") as mock_load_config:
|
||||||
|
mock_load_config.return_value = custom_config
|
||||||
|
|
||||||
|
result = generate_commit_message()
|
||||||
|
file_count = result.count("file")
|
||||||
|
assert file_count <= 3
|
||||||
Reference in New Issue
Block a user