fix: update CI workflow for repohealth-cli with separate lint, test, and build jobs
This commit is contained in:
1
repohealth-cli/tests/__init__.py
Normal file
1
repohealth-cli/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for the RepoHealth CLI."""
|
||||
133
repohealth-cli/tests/conftest.py
Normal file
133
repohealth-cli/tests/conftest.py
Normal 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
repohealth-cli/tests/test_analyzers.py
Normal file
267
repohealth-cli/tests/test_analyzers.py
Normal 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
repohealth-cli/tests/test_cli.py
Normal file
200
repohealth-cli/tests/test_cli.py
Normal 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
repohealth-cli/tests/test_models.py
Normal file
202
repohealth-cli/tests/test_models.py
Normal 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
repohealth-cli/tests/test_reporters.py
Normal file
261
repohealth-cli/tests/test_reporters.py
Normal 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"
|
||||
Reference in New Issue
Block a user