fix: update CI workflow with proper checkout paths
Some checks failed
CI / lint (push) Successful in 9m27s
CI / test (push) Failing after 4m46s
CI / build (push) Has been skipped

This commit is contained in:
Developer
2026-02-05 18:03:00 +00:00
commit 5b74fccad8
28 changed files with 3461 additions and 0 deletions

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Tests for the RepoHealth CLI."""

133
tests/conftest.py Normal file
View File

@@ -0,0 +1,133 @@
"""Pytest configuration and fixtures."""
import shutil
import tempfile
from pathlib import Path
import pytest
from git import Repo
@pytest.fixture
def sample_git_repo():
"""Create a sample Git repository for testing.
Creates a temporary directory with a Git repository containing
multiple files and commits from different authors.
Returns:
Path to the temporary repository.
"""
temp_dir = tempfile.mkdtemp(prefix="repohealth_test_")
repo_path = Path(temp_dir)
repo = Repo.init(repo_path)
config = repo.config_writer()
config.set_value("user", "name", "Test Author 1")
config.set_value("user", "email", "author1@example.com")
config.release()
(repo_path / "main.py").write_text("# Main module\n\ndef hello():\n return 'Hello'\n")
(repo_path / "utils.py").write_text("# Utility functions\n\ndef helper():\n return True\n")
(repo_path / "test_main.py").write_text("# Tests for main\n\ndef test_hello():\n assert hello() == 'Hello'\n")
repo.index.add(["main.py", "utils.py", "test_main.py"])
repo.index.commit("Initial commit with main files")
config = repo.config_writer()
config.set_value("user", "name", "Test Author 2")
config.set_value("user", "email", "author2@example.com")
config.release()
(repo_path / "main.py").write_text("# Main module\n\ndef hello():\n return 'Hello'\n\ndef goodbye():\n return 'Goodbye'\n")
(repo_path / "utils.py").write_text("# Utility functions\n\ndef helper():\n return True\n\ndef complex_func():\n return 42\n")
repo.index.add(["main.py", "utils.py"])
repo.index.commit("Add goodbye function and complex_func")
config = repo.config_writer()
config.set_value("user", "name", "Test Author 1")
config.set_value("user", "email", "author1@example.com")
config.release()
(repo_path / "main.py").write_text("# Main module\n\ndef hello():\n return 'Hello'\n\ndef goodbye():\n return 'Goodbye'\n\ndef greet(name):\n return f'Hello, {name}'\n")
repo.index.add(["main.py"])
repo.index.commit("Add greet function")
config = repo.config_writer()
config.set_value("user", "name", "Test Author 3")
config.set_value("user", "email", "author3@example.com")
config.release()
(repo_path / "helpers.py").write_text("# Additional helpers\n\ndef new_helper():\n return False\n")
(repo_path / "test_helpers.py").write_text("# Tests for helpers\n\ndef test_new_helper():\n assert new_helper() == False\n")
repo.index.add(["helpers.py", "test_helpers.py"])
repo.index.commit("Add helpers module")
(repo_path / "core.py").write_text("# Core module - critical file\n\nclass CoreClass:\n def __init__(self):\n self.data = []\n\n def process(self, item):\n self.data.append(item)\n")
repo.index.add(["core.py"])
repo.index.commit("Add core module")
yield repo_path
shutil.rmtree(temp_dir)
@pytest.fixture
def single_author_repo():
"""Create a repository with single author for critical risk testing.
Returns:
Path to the temporary repository.
"""
temp_dir = tempfile.mkdtemp(prefix="repohealth_single_")
repo_path = Path(temp_dir)
repo = Repo.init(repo_path)
config = repo.config_writer()
config.set_value("user", "name", "Solo Author")
config.set_value("user", "email", "solo@example.com")
config.release()
for i in range(10):
(repo_path / f"module_{i}.py").write_text(f"# Module {i}\n\ndef func_{i}():\n return {i}\n")
repo.index.add([f"module_{i}.py"])
repo.index.commit(f"Add module {i}")
yield repo_path
shutil.rmtree(temp_dir)
@pytest.fixture
def empty_repo():
"""Create an empty Git repository.
Returns:
Path to the empty repository.
"""
temp_dir = tempfile.mkdtemp(prefix="repohealth_empty_")
repo_path = Path(temp_dir)
Repo.init(repo_path)
yield repo_path
shutil.rmtree(temp_dir)
@pytest.fixture
def temp_dir():
"""Provide a temporary directory for test artifacts.
Returns:
Path to a temporary directory.
"""
temp_dir = tempfile.mkdtemp(prefix="repohealth_artifacts_")
yield Path(temp_dir)
shutil.rmtree(temp_dir)

267
tests/test_analyzers.py Normal file
View File

@@ -0,0 +1,267 @@
"""Tests for analyzer modules."""
from repohealth.analyzers.bus_factor import BusFactorCalculator
from repohealth.analyzers.risk_analyzer import RiskAnalyzer
from repohealth.models.file_stats import FileAnalysis
class TestBusFactorCalculator:
"""Tests for BusFactorCalculator."""
def setup_method(self):
"""Set up test fixtures."""
self.calculator = BusFactorCalculator()
def test_calculate_gini_equal_distribution(self):
"""Test Gini coefficient with equal distribution."""
values = [10, 10, 10, 10]
gini = self.calculator.calculate_gini(values)
assert gini == 0.0
def test_calculate_gini_unequal_distribution(self):
"""Test Gini coefficient with unequal distribution."""
values = [100, 0, 0, 0]
gini = self.calculator.calculate_gini(values)
assert gini > 0.5
assert gini <= 1.0
def test_calculate_gini_single_value(self):
"""Test Gini coefficient with single value."""
values = [100]
gini = self.calculator.calculate_gini(values)
assert gini == 0.0
def test_calculate_gini_empty_list(self):
"""Test Gini coefficient with empty list."""
gini = self.calculator.calculate_gini([])
assert gini == 0.0
def test_calculate_file_bus_factor_single_author(self):
"""Test bus factor with single author."""
analysis = FileAnalysis(
path="test.py",
total_commits=10,
author_commits={"author@example.com": 10}
)
bus_factor = self.calculator.calculate_file_bus_factor(analysis)
assert bus_factor == 1.0
def test_calculate_file_bus_factor_multiple_authors(self):
"""Test bus factor with multiple authors."""
analysis = FileAnalysis(
path="test.py",
total_commits=10,
author_commits={"a@x.com": 5, "b@x.com": 5}
)
bus_factor = self.calculator.calculate_file_bus_factor(analysis)
assert bus_factor > 1.0
def test_calculate_file_bus_factor_no_commits(self):
"""Test bus factor with no commits."""
analysis = FileAnalysis(
path="test.py",
total_commits=0,
author_commits={}
)
bus_factor = self.calculator.calculate_file_bus_factor(analysis)
assert bus_factor == 1.0
def test_calculate_repository_bus_factor(self):
"""Test repository-level bus factor calculation."""
files = [
FileAnalysis(
path="file1.py",
total_commits=10,
author_commits={"a@x.com": 10}
),
FileAnalysis(
path="file2.py",
total_commits=10,
author_commits={"a@x.com": 5, "b@x.com": 5}
)
]
bus_factor = self.calculator.calculate_repository_bus_factor(files)
assert bus_factor > 1.0
def test_assign_risk_levels(self):
"""Test risk level assignment."""
files = [
FileAnalysis(
path="critical.py",
total_commits=10,
author_commits={"a@x.com": 10}
),
FileAnalysis(
path="low_risk.py",
total_commits=10,
author_commits={"a@x.com": 3, "b@x.com": 3, "c@x.com": 4}
)
]
assigned = self.calculator.assign_risk_levels(files)
assert assigned[0].risk_level == "critical"
assert assigned[1].risk_level == "low"
def test_calculate_repository_gini(self):
"""Test repository-wide Gini coefficient."""
files = [
FileAnalysis(
path="file1.py",
total_commits=10,
author_commits={"a@x.com": 10}
),
FileAnalysis(
path="file2.py",
total_commits=10,
author_commits={"b@x.com": 10}
)
]
gini = self.calculator.calculate_repository_gini(files)
assert gini > 0
class TestRiskAnalyzer:
"""Tests for RiskAnalyzer."""
def setup_method(self):
"""Set up test fixtures."""
self.analyzer = RiskAnalyzer()
def test_identify_hotspots_critical(self):
"""Test hotspot identification for critical files."""
files = [
FileAnalysis(
path="critical.py",
total_commits=10,
author_commits={"a@x.com": 9, "b@x.com": 1},
bus_factor=1.1
),
FileAnalysis(
path="safe.py",
total_commits=10,
author_commits={"a@x.com": 4, "b@x.com": 6},
bus_factor=2.0
)
]
hotspots = self.analyzer.identify_hotspots(files)
assert len(hotspots) >= 1
assert any(h.risk_level == "critical" for h in hotspots)
def test_identify_hotspots_limit(self):
"""Test hotspot limit parameter."""
files = [
FileAnalysis(
path=f"file{i}.py",
total_commits=10,
author_commits={"a@x.com": 9, "b@x.com": 1},
bus_factor=1.1
)
for i in range(25)
]
hotspots = self.analyzer.identify_hotspots(files, limit=10)
assert len(hotspots) == 10
def test_generate_suggestions(self):
"""Test diversification suggestions generation."""
files = [
FileAnalysis(
path="file1.py",
total_commits=10,
author_commits={"a@x.com": 9, "b@x.com": 1}
),
FileAnalysis(
path="file2.py",
total_commits=10,
author_commits={"a@x.com": 5, "b@x.com": 5}
)
]
suggestions = self.analyzer.generate_suggestions(files)
assert len(suggestions) > 0
def test_calculate_risk_summary(self):
"""Test risk summary calculation."""
files = [
FileAnalysis(
path="f1.py",
total_commits=10,
author_commits={"a@x.com": 10},
risk_level="critical"
),
FileAnalysis(
path="f2.py",
total_commits=10,
author_commits={"a@x.com": 8, "b@x.com": 2},
risk_level="high"
),
FileAnalysis(
path="f3.py",
total_commits=10,
author_commits={"a@x.com": 4, "b@x.com": 6},
risk_level="medium"
)
]
summary = self.analyzer.calculate_risk_summary(files)
assert summary["critical"] == 1
assert summary["high"] == 1
assert summary["medium"] == 1
assert "overall_risk" in summary
def test_calculate_risk_summary_empty(self):
"""Test risk summary with empty files."""
summary = self.analyzer.calculate_risk_summary([])
assert summary["overall_risk"] == "unknown"
def test_analyze_module_risk(self):
"""Test module-level risk analysis."""
files = [
FileAnalysis(
path="core/main.py",
total_commits=10,
author_commits={"a@x.com": 10},
module="core",
risk_level="critical"
),
FileAnalysis(
path="core/utils.py",
total_commits=10,
author_commits={"a@x.com": 10},
module="core",
risk_level="critical"
),
FileAnalysis(
path="tests/test.py",
total_commits=10,
author_commits={"a@x.com": 5, "b@x.com": 5},
module="tests",
risk_level="medium"
)
]
module_risk = self.analyzer.analyze_module_risk(files)
assert "core" in module_risk
assert "tests" in module_risk

200
tests/test_cli.py Normal file
View File

@@ -0,0 +1,200 @@
"""Tests for CLI interface."""
import json
import tempfile
from click.testing import CliRunner
from repohealth.cli.cli import analyze, health, main, report
class TestCLI:
"""Tests for CLI commands."""
def test_main_help(self):
"""Test main command help."""
runner = CliRunner()
result = runner.invoke(main, ["--help"])
assert result.exit_code == 0
assert "RepoHealth CLI" in result.output
assert "analyze" in result.output
assert "report" in result.output
assert "health" in result.output
def test_analyze_help(self):
"""Test analyze command help."""
runner = CliRunner()
result = runner.invoke(analyze, ["--help"])
assert result.exit_code == 0
assert "--depth" in result.output
assert "--path" in result.output
assert "--extensions" in result.output
assert "--json" in result.output
def test_report_help(self):
"""Test report command help."""
runner = CliRunner()
result = runner.invoke(report, ["--help"])
assert result.exit_code == 0
assert "--format" in result.output
assert "--output" in result.output
def test_health_help(self):
"""Test health command help."""
runner = CliRunner()
result = runner.invoke(health, ["--help"])
assert result.exit_code == 0
def test_analyze_invalid_repo(self):
"""Test analyze with invalid repository path."""
runner = CliRunner()
result = runner.invoke(analyze, ["/nonexistent/path"])
assert result.exit_code != 0
assert "not a valid Git repository" in result.output
def test_health_invalid_repo(self):
"""Test health with invalid repository path."""
runner = CliRunner()
result = runner.invoke(health, ["/nonexistent/path"])
assert result.exit_code != 0
def test_analyze_negative_depth(self):
"""Test analyze with negative depth option."""
runner = CliRunner()
with tempfile.TemporaryDirectory() as tmpdir:
result = runner.invoke(analyze, [tmpdir, "--depth", "-5"])
assert result.exit_code != 0
assert "positive integer" in result.output
def test_analyze_json_output(self, sample_git_repo, temp_dir):
"""Test analyze with JSON output."""
runner = CliRunner()
result = runner.invoke(analyze, [str(sample_git_repo), "--json"])
assert result.exit_code == 0
output = json.loads(result.output)
assert "repository" in output
assert "summary" in output
assert "files" in output
def test_analyze_json_to_file(self, sample_git_repo, temp_dir):
"""Test analyze saving JSON to file."""
runner = CliRunner()
output_file = temp_dir / "output.json"
result = runner.invoke(
analyze,
[str(sample_git_repo), "--output", str(output_file)]
)
assert result.exit_code == 0
assert output_file.exists()
content = json.loads(output_file.read_text())
assert "repository" in content
def test_report_html_output(self, sample_git_repo, temp_dir):
"""Test report generating HTML output."""
runner = CliRunner()
output_file = temp_dir / "report.html"
result = runner.invoke(
report,
[str(sample_git_repo), "--format", "html", "--output", str(output_file)]
)
assert result.exit_code == 0
assert output_file.exists()
html_content = output_file.read_text()
assert "<html>" in html_content
assert "Repository Health Report" in html_content
def test_health_display(self, sample_git_repo):
"""Test health command display output."""
runner = CliRunner()
result = runner.invoke(health, [str(sample_git_repo)])
assert result.exit_code == 0
assert "Repository Health" in result.output or "Bus Factor" in result.output
def test_analyze_with_extensions(self, sample_git_repo):
"""Test analyze with file extension filter."""
runner = CliRunner()
result = runner.invoke(
analyze,
[str(sample_git_repo), "--extensions", "py", "--json"]
)
assert result.exit_code == 0
output = json.loads(result.output)
assert output["files_analyzed"] >= 0
def test_analyze_with_depth(self, sample_git_repo):
"""Test analyze with commit depth limit."""
runner = CliRunner()
result = runner.invoke(
analyze,
[str(sample_git_repo), "--depth", "2", "--json"]
)
assert result.exit_code == 0
output = json.loads(result.output)
assert "files_analyzed" in output
class TestRepoHealthCLI:
"""Unit tests for RepoHealthCLI class."""
def test_cli_initialization(self):
"""Test CLI class initialization."""
from repohealth.cli.cli import RepoHealthCLI
cli = RepoHealthCLI()
assert cli.terminal_reporter is not None
assert cli.json_reporter is not None
assert cli.html_reporter is not None
def test_analyze_repository_result_structure(self, sample_git_repo):
"""Test that analyze produces valid result structure."""
from repohealth.cli.cli import RepoHealthCLI
cli = RepoHealthCLI()
result = cli.analyze_repository(str(sample_git_repo))
assert result.repository_path is not None
assert isinstance(result.files_analyzed, int)
assert isinstance(result.total_commits, int)
assert isinstance(result.unique_authors, int)
assert isinstance(result.overall_bus_factor, float)
assert result.files is not None
assert result.risk_summary is not None
def test_analyze_repository_min_commits(self, sample_git_repo):
"""Test analyze with min_commits filter."""
from repohealth.cli.cli import RepoHealthCLI
cli = RepoHealthCLI()
result_all = cli.analyze_repository(
str(sample_git_repo),
min_commits=1
)
result_filtered = cli.analyze_repository(
str(sample_git_repo),
min_commits=100
)
assert result_all.files_analyzed >= result_filtered.files_analyzed

202
tests/test_models.py Normal file
View File

@@ -0,0 +1,202 @@
"""Tests for data models."""
from repohealth.models.author import AuthorStats
from repohealth.models.file_stats import FileAnalysis
from repohealth.models.result import RepositoryResult
class TestFileAnalysis:
"""Tests for FileAnalysis model."""
def test_file_analysis_creation(self):
"""Test creating a FileAnalysis instance."""
analysis = FileAnalysis(
path="src/main.py",
total_commits=10,
author_commits={"author1@example.com": 6, "author2@example.com": 4}
)
assert analysis.path == "src/main.py"
assert analysis.total_commits == 10
assert analysis.num_authors == 2
assert analysis.bus_factor == 1.0
def test_num_authors(self):
"""Test num_authors property."""
analysis = FileAnalysis(
path="test.py",
total_commits=5,
author_commits={"a@x.com": 3, "b@x.com": 2}
)
assert analysis.num_authors == 2
def test_num_authors_empty(self):
"""Test num_authors with empty commits."""
analysis = FileAnalysis(
path="test.py",
total_commits=0,
author_commits={}
)
assert analysis.num_authors == 0
def test_top_author(self):
"""Test top_author property."""
analysis = FileAnalysis(
path="test.py",
total_commits=10,
author_commits={"a@x.com": 7, "b@x.com": 3}
)
top_author, count = analysis.top_author
assert top_author == "a@x.com"
assert count == 7
def test_top_author_empty(self):
"""Test top_author with empty commits."""
analysis = FileAnalysis(
path="test.py",
total_commits=0,
author_commits={}
)
assert analysis.top_author is None
def test_top_author_share(self):
"""Test top_author_share property."""
analysis = FileAnalysis(
path="test.py",
total_commits=10,
author_commits={"a@x.com": 8, "b@x.com": 2}
)
assert analysis.top_author_share == 0.8
def test_top_author_share_empty(self):
"""Test top_author_share with no commits."""
analysis = FileAnalysis(
path="test.py",
total_commits=0,
author_commits={}
)
assert analysis.top_author_share == 0.0
def test_get_author_share(self):
"""Test get_author_share method."""
analysis = FileAnalysis(
path="test.py",
total_commits=10,
author_commits={"a@x.com": 5, "b@x.com": 5}
)
assert analysis.get_author_share("a@x.com") == 0.5
assert analysis.get_author_share("b@x.com") == 0.50
assert analysis.get_author_share("c@x.com") == 0.0
def test_module_and_extension(self):
"""Test module and extension extraction."""
analysis = FileAnalysis(
path="src/core/main.py",
total_commits=5,
author_commits={},
module="src/core",
extension="py"
)
assert analysis.module == "src/core"
assert analysis.extension == "py"
class TestAuthorStats:
"""Tests for AuthorStats model."""
def test_author_stats_creation(self):
"""Test creating an AuthorStats instance."""
stats = AuthorStats(
name="Test Author",
email="test@example.com",
total_commits=100
)
assert stats.name == "Test Author"
assert stats.email == "test@example.com"
assert stats.total_commits == 100
assert len(stats.files_touched) == 0
def test_add_file(self):
"""Test adding a file contribution."""
stats = AuthorStats(name="Test", email="test@test.com")
stats.add_file("src/main.py", "src")
assert "src/main.py" in stats.files_touched
assert "src" in stats.modules_contributed
assert stats.total_contributions == 1
def test_merge(self):
"""Test merging two AuthorStats."""
stats1 = AuthorStats(name="Test", email="test@test.com")
stats1.total_commits = 10
stats1.files_touched = {"file1.py"}
stats2 = AuthorStats(name="Test", email="test@test.com")
stats2.total_commits = 5
stats2.files_touched = {"file2.py"}
stats1.merge(stats2)
assert stats1.total_commits == 15
assert "file1.py" in stats1.files_touched
assert "file2.py" in stats1.files_touched
class TestRepositoryResult:
"""Tests for RepositoryResult model."""
def test_repository_result_creation(self):
"""Test creating a RepositoryResult instance."""
result = RepositoryResult(
repository_path="/test/repo",
files_analyzed=100,
total_commits=500,
unique_authors=5
)
assert result.repository_path == "/test/repo"
assert result.files_analyzed == 100
assert result.total_commits == 500
assert result.unique_authors == 5
def test_risk_count_properties(self):
"""Test risk count properties."""
result = RepositoryResult(
repository_path="/test/repo",
files=[
{"risk_level": "critical"},
{"risk_level": "critical"},
{"risk_level": "high"},
{"risk_level": "high"},
{"risk_level": "medium"},
{"risk_level": "low"}
]
)
assert result.high_risk_count == 2
assert result.medium_risk_count == 1
assert result.low_risk_count == 1
def test_to_dict(self):
"""Test to_dict serialization."""
result = RepositoryResult(
repository_path="/test/repo",
files_analyzed=10,
total_commits=50,
unique_authors=3
)
result_dict = result.to_dict()
assert result_dict["repository"] == "/test/repo"
assert result_dict["files_analyzed"] == 10
assert "analyzed_at" in result_dict

261
tests/test_reporters.py Normal file
View File

@@ -0,0 +1,261 @@
"""Tests for reporter modules."""
import json
from repohealth.models.file_stats import FileAnalysis
from repohealth.models.result import RepositoryResult
from repohealth.reporters.html_reporter import HTMLReporter
from repohealth.reporters.json_reporter import JSONReporter
class TestJSONReporter:
"""Tests for JSONReporter."""
def setup_method(self):
"""Set up test fixtures."""
self.reporter = JSONReporter()
self.sample_result = RepositoryResult(
repository_path="/test/repo",
files_analyzed=10,
total_commits=100,
unique_authors=5,
overall_bus_factor=2.5,
gini_coefficient=0.35,
files=[
{
"path": "src/main.py",
"total_commits": 20,
"num_authors": 2,
"author_commits": {"a@x.com": 15, "b@x.com": 5},
"bus_factor": 1.5,
"risk_level": "high",
"top_author_share": 0.75,
"module": "src",
"extension": "py"
}
],
hotspots=[
{
"file_path": "src/main.py",
"risk_level": "high",
"bus_factor": 1.5,
"top_author": "a@x.com",
"top_author_share": 0.75,
"total_commits": 20,
"num_authors": 2,
"module": "src",
"suggestion": "Consider code reviews"
}
],
suggestions=[
{
"file_path": "src/main.py",
"current_author": "a@x.com",
"suggested_authors": ["b@x.com"],
"priority": "high",
"reason": "High ownership concentration",
"action": "Assign reviews to b@x.com"
}
],
risk_summary={
"critical": 0,
"high": 1,
"medium": 3,
"low": 6,
"percentage_critical": 0.0,
"percentage_high": 10.0,
"overall_risk": "low"
}
)
def test_generate_json(self):
"""Test JSON generation."""
json_output = self.reporter.generate(self.sample_result)
assert isinstance(json_output, str)
parsed = json.loads(json_output)
assert parsed["repository"] == "/test/repo"
assert parsed["summary"]["overall_bus_factor"] == 2.5
def test_generate_file_dict(self):
"""Test file analysis to dictionary conversion."""
analysis = FileAnalysis(
path="src/main.py",
total_commits=20,
author_commits={"a@x.com": 15, "b@x.com": 5},
bus_factor=1.5,
risk_level="high",
module="src",
extension="py"
)
file_dict = self.reporter.generate_file_dict(analysis)
assert file_dict["path"] == "src/main.py"
assert file_dict["total_commits"] == 20
assert file_dict["num_authors"] == 2
assert file_dict["bus_factor"] == 1.5
def test_save_json(self, temp_dir):
"""Test saving JSON to file."""
output_file = temp_dir / "output.json"
self.reporter.save(self.sample_result, str(output_file))
assert output_file.exists()
content = json.loads(output_file.read_text())
assert content["repository"] == "/test/repo"
def test_indent_parameter(self):
"""Test JSON indentation."""
reporter_no_indent = JSONReporter(indent=0)
json_output = reporter_no_indent.generate(self.sample_result)
lines = json_output.strip().split("\n")
assert len(lines) <= 2
def test_json_contains_required_fields(self):
"""Test that JSON output contains all required fields."""
json_output = self.reporter.generate(self.sample_result)
parsed = json.loads(json_output)
assert "version" in parsed
assert "repository" in parsed
assert "analyzed_at" in parsed
assert "summary" in parsed
assert "files" in parsed
assert "hotspots" in parsed
assert "suggestions" in parsed
class TestHTMLReporter:
"""Tests for HTMLReporter."""
def setup_method(self):
"""Set up test fixtures."""
self.reporter = HTMLReporter()
self.sample_result = RepositoryResult(
repository_path="/test/repo",
files_analyzed=10,
total_commits=100,
unique_authors=5,
overall_bus_factor=2.5,
gini_coefficient=0.35,
files=[
{
"path": "src/main.py",
"total_commits": 20,
"num_authors": 2,
"author_commits": {"a@x.com": 15, "b@x.com": 5},
"bus_factor": 1.5,
"risk_level": "high",
"top_author_share": 0.75,
"module": "src",
"extension": "py"
}
],
hotspots=[
{
"file_path": "src/main.py",
"risk_level": "high",
"bus_factor": 1.5,
"top_author": "a@x.com",
"top_author_share": 0.75,
"total_commits": 20,
"num_authors": 2,
"module": "src",
"suggestion": "Consider code reviews"
}
],
suggestions=[
{
"file_path": "src/main.py",
"current_author": "a@x.com",
"suggested_authors": ["b@x.com"],
"priority": "high",
"reason": "High ownership concentration",
"action": "Assign reviews to b@x.com"
}
],
risk_summary={
"critical": 0,
"high": 1,
"medium": 3,
"low": 6,
"percentage_critical": 0.0,
"percentage_high": 10.0,
"overall_risk": "low"
}
)
def test_generate_standalone(self):
"""Test standalone HTML generation."""
html_output = self.reporter.generate_standalone(self.sample_result)
assert isinstance(html_output, str)
assert "<!doctype html>" in html_output.lower() or "<html>" in html_output.lower()
assert "</html>" in html_output
def test_standalone_contains_summary(self):
"""Test that standalone HTML contains summary section."""
html_output = self.reporter.generate_standalone(self.sample_result)
assert "repository health report" in html_output.lower()
def test_standalone_contains_chart_data(self):
"""Test that standalone HTML includes Chart.js."""
html_output = self.reporter.generate_standalone(self.sample_result)
assert "chart.js" in html_output.lower()
def test_save_standalone(self, temp_dir):
"""Test saving standalone HTML to file."""
output_file = temp_dir / "report.html"
self.reporter.save_standalone(self.sample_result, str(output_file))
assert output_file.exists()
content = output_file.read_text()
assert "<!doctype html>" in content.lower() or "<html>" in content.lower()
def test_generate_charts_data(self):
"""Test chart data generation."""
charts_data = self.reporter.generate_charts_data(self.sample_result)
assert "risk_distribution" in charts_data
assert "top_hotspots" in charts_data
assert "file_data" in charts_data
assert "summary" in charts_data
def test_risk_colors_defined(self):
"""Test that risk colors are properly defined."""
assert "critical" in self.reporter.RISK_COLORS
assert "high" in self.reporter.RISK_COLORS
assert "medium" in self.reporter.RISK_COLORS
assert "low" in self.reporter.RISK_COLORS
class TestTerminalReporter:
"""Tests for TerminalReporter."""
def test_reporter_initialization(self):
"""Test terminal reporter initialization."""
from repohealth.reporters.terminal import TerminalReporter
reporter = TerminalReporter()
assert reporter.RISK_COLORS is not None
def test_risk_colors_mapping(self):
"""Test risk color mappings."""
from repohealth.reporters.terminal import TerminalReporter
reporter = TerminalReporter()
assert reporter.RISK_COLORS["critical"] == "red"
assert reporter.RISK_COLORS["high"] == "orange3"
assert reporter.RISK_COLORS["medium"] == "yellow"
assert reporter.RISK_COLORS["low"] == "green"