We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/UrbanDiver/local-deepwiki-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""Tests for the changelog module."""
import subprocess
from datetime import datetime
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from local_deepwiki.core.git_utils import GitRepoInfo
from local_deepwiki.generators.changelog import (
CommitInfo,
build_commit_url,
generate_changelog_content,
get_commit_history,
)
class TestGetCommitHistory:
"""Tests for get_commit_history function."""
def test_returns_commits_from_real_repo(self, tmp_path: Path) -> None:
"""Test getting commit history from a real git repo."""
# Initialize git repo
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=tmp_path,
capture_output=True,
)
subprocess.run(
["git", "config", "user.name", "Test Author"],
cwd=tmp_path,
capture_output=True,
)
# Create a file and commit
(tmp_path / "file1.py").write_text("# File 1")
subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "commit", "-m", "Initial commit"],
cwd=tmp_path,
capture_output=True,
)
# Create another file and commit
(tmp_path / "file2.py").write_text("# File 2")
subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "commit", "-m", "Add second file"],
cwd=tmp_path,
capture_output=True,
)
commits = get_commit_history(tmp_path, limit=10)
assert len(commits) == 2
# Newest commit first
assert commits[0].message == "Add second file"
assert commits[1].message == "Initial commit"
assert commits[0].author == "Test Author"
assert "file2.py" in commits[0].files
assert "file1.py" in commits[1].files
def test_returns_empty_for_non_git_dir(self, tmp_path: Path) -> None:
"""Test returns empty list for non-git directory."""
commits = get_commit_history(tmp_path)
assert commits == []
def test_respects_limit(self, tmp_path: Path) -> None:
"""Test that limit parameter is respected."""
# Initialize git repo
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=tmp_path,
capture_output=True,
)
subprocess.run(
["git", "config", "user.name", "Test"],
cwd=tmp_path,
capture_output=True,
)
# Create 5 commits
for i in range(5):
(tmp_path / f"file{i}.py").write_text(f"# File {i}")
subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "commit", "-m", f"Commit {i}"],
cwd=tmp_path,
capture_output=True,
)
commits = get_commit_history(tmp_path, limit=3)
assert len(commits) == 3
def test_handles_invalid_date_format(self, tmp_path: Path) -> None:
"""Test that invalid date format falls back to datetime.now()."""
# Mock subprocess.run to return output with invalid date
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "abc1234|abc1234567890abcdef|Test Author|invalid-date|Test message\nfile.py\n"
with patch("local_deepwiki.generators.changelog.subprocess.run", return_value=mock_result):
commits = get_commit_history(tmp_path, limit=10)
assert len(commits) == 1
assert commits[0].hash == "abc1234"
assert commits[0].message == "Test message"
# Date should be set to approximately now (within last minute)
time_diff = abs((datetime.now() - commits[0].date).total_seconds())
assert time_diff < 60
def test_handles_timeout(self, tmp_path: Path) -> None:
"""Test that TimeoutExpired returns empty list."""
with patch(
"local_deepwiki.generators.changelog.subprocess.run",
side_effect=subprocess.TimeoutExpired(cmd="git", timeout=30),
):
commits = get_commit_history(tmp_path, limit=10)
assert commits == []
def test_handles_file_not_found_error(self, tmp_path: Path) -> None:
"""Test that FileNotFoundError returns empty list."""
with patch(
"local_deepwiki.generators.changelog.subprocess.run",
side_effect=FileNotFoundError("git not found"),
):
commits = get_commit_history(tmp_path, limit=10)
assert commits == []
def test_handles_os_error(self, tmp_path: Path) -> None:
"""Test that OSError returns empty list."""
with patch(
"local_deepwiki.generators.changelog.subprocess.run",
side_effect=OSError("Permission denied"),
):
commits = get_commit_history(tmp_path, limit=10)
assert commits == []
class TestBuildCommitUrl:
"""Tests for build_commit_url function."""
def test_github_url(self) -> None:
"""Test building GitHub commit URL."""
repo_info = GitRepoInfo(
remote_url="https://github.com/owner/repo",
host="github.com",
owner="owner",
repo="repo",
default_branch="main",
)
result = build_commit_url(repo_info, "abc1234")
assert result == "https://github.com/owner/repo/commit/abc1234"
def test_gitlab_url(self) -> None:
"""Test building GitLab commit URL."""
repo_info = GitRepoInfo(
remote_url="https://gitlab.com/owner/repo",
host="gitlab.com",
owner="owner",
repo="repo",
default_branch="main",
)
result = build_commit_url(repo_info, "abc1234")
assert result == "https://gitlab.com/owner/repo/-/commit/abc1234"
def test_no_remote_returns_none(self) -> None:
"""Test returns None when no remote configured."""
repo_info = GitRepoInfo(
remote_url=None,
host=None,
owner=None,
repo=None,
default_branch="main",
)
result = build_commit_url(repo_info, "abc1234")
assert result is None
class TestGenerateChangelogContent:
"""Tests for generate_changelog_content function."""
def test_generates_markdown(self, tmp_path: Path) -> None:
"""Test generates valid markdown content."""
# Initialize git repo with remote
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=tmp_path,
capture_output=True,
)
subprocess.run(
["git", "config", "user.name", "Test Author"],
cwd=tmp_path,
capture_output=True,
)
subprocess.run(
["git", "remote", "add", "origin", "https://github.com/test/repo.git"],
cwd=tmp_path,
capture_output=True,
)
# Create commits
(tmp_path / "file.py").write_text("# Test")
subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "commit", "-m", "Initial commit"],
cwd=tmp_path,
capture_output=True,
)
content = generate_changelog_content(tmp_path)
assert content is not None
assert "# Changelog" in content
assert "## Recent Commits" in content
assert "Initial commit" in content
assert "## Statistics" in content
assert "https://github.com/test/repo/commit/" in content
def test_returns_none_for_non_git_dir(self, tmp_path: Path) -> None:
"""Test returns None for non-git directory."""
content = generate_changelog_content(tmp_path)
assert content is None
def test_groups_by_date(self, tmp_path: Path) -> None:
"""Test commits are grouped by date."""
# Initialize git repo
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=tmp_path,
capture_output=True,
)
subprocess.run(
["git", "config", "user.name", "Test"],
cwd=tmp_path,
capture_output=True,
)
# Create commit
(tmp_path / "file.py").write_text("# Test")
subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "commit", "-m", "Test commit"],
cwd=tmp_path,
capture_output=True,
)
content = generate_changelog_content(tmp_path)
assert content is not None
# Should have a date header (format: ### Month Day, Year)
assert "### " in content
def test_includes_file_changes(self, tmp_path: Path) -> None:
"""Test includes changed files in output."""
# Initialize git repo
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=tmp_path,
capture_output=True,
)
subprocess.run(
["git", "config", "user.name", "Test"],
cwd=tmp_path,
capture_output=True,
)
# Create commit with file
(tmp_path / "myfile.py").write_text("# Test")
subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "commit", "-m", "Add myfile"],
cwd=tmp_path,
capture_output=True,
)
content = generate_changelog_content(tmp_path)
assert content is not None
assert "myfile.py" in content
def test_respects_max_commits(self, tmp_path: Path) -> None:
"""Test max_commits parameter limits output."""
# Initialize git repo
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=tmp_path,
capture_output=True,
)
subprocess.run(
["git", "config", "user.name", "Test"],
cwd=tmp_path,
capture_output=True,
)
# Create 5 commits
for i in range(5):
(tmp_path / f"file{i}.py").write_text(f"# File {i}")
subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "commit", "-m", f"Commit number {i}"],
cwd=tmp_path,
capture_output=True,
)
content = generate_changelog_content(tmp_path, max_commits=2)
assert content is not None
# Should show 2 in statistics
assert "**Commits shown**: 2" in content
def test_shows_statistics(self, tmp_path: Path) -> None:
"""Test includes statistics section."""
# Initialize git repo
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=tmp_path,
capture_output=True,
)
subprocess.run(
["git", "config", "user.name", "Test Author"],
cwd=tmp_path,
capture_output=True,
)
(tmp_path / "file.py").write_text("# Test")
subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "commit", "-m", "Initial commit"],
cwd=tmp_path,
capture_output=True,
)
content = generate_changelog_content(tmp_path)
assert content is not None
assert "## Statistics" in content
assert "**Commits shown**:" in content
assert "**Contributors**:" in content
assert "**Latest commit**:" in content
def test_truncates_long_messages(self, tmp_path: Path) -> None:
"""Test that commit messages longer than 80 chars are truncated."""
# Initialize git repo
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=tmp_path,
capture_output=True,
)
subprocess.run(
["git", "config", "user.name", "Test"],
cwd=tmp_path,
capture_output=True,
)
# Create commit with very long message (>80 chars)
long_message = "A" * 100 # 100 character message
(tmp_path / "file.py").write_text("# Test")
subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "commit", "-m", long_message],
cwd=tmp_path,
capture_output=True,
)
content = generate_changelog_content(tmp_path)
assert content is not None
# Message should be truncated to 77 chars + "..."
assert "A" * 77 + "..." in content
# Full message should NOT be present
assert "A" * 100 not in content
def test_shows_more_files_indicator(self, tmp_path: Path) -> None:
"""Test that '+N more' is shown when commit has more than 5 files."""
# Initialize git repo
subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=tmp_path,
capture_output=True,
)
subprocess.run(
["git", "config", "user.name", "Test"],
cwd=tmp_path,
capture_output=True,
)
# Create commit with more than 5 files
for i in range(8):
(tmp_path / f"file{i}.py").write_text(f"# File {i}")
subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True)
subprocess.run(
["git", "commit", "-m", "Add multiple files"],
cwd=tmp_path,
capture_output=True,
)
content = generate_changelog_content(tmp_path)
assert content is not None
# Should show "+3 more" (8 files - 5 shown = 3 more)
assert "(+3 more)" in content
class TestCommitInfo:
"""Tests for CommitInfo dataclass."""
def test_create_commit_info(self) -> None:
"""Test creating CommitInfo object."""
commit = CommitInfo(
hash="abc1234",
full_hash="abc1234567890",
author="Test Author",
date=datetime(2026, 1, 13, 10, 30, 0),
message="Test commit",
files=["file1.py", "file2.py"],
)
assert commit.hash == "abc1234"
assert commit.author == "Test Author"
assert commit.message == "Test commit"
assert len(commit.files) == 2
def test_default_files_list(self) -> None:
"""Test that files defaults to empty list."""
commit = CommitInfo(
hash="abc1234",
full_hash="abc1234567890",
author="Test",
date=datetime.now(),
message="Test",
)
assert commit.files == []