We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/jddunn/tenets'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""Tests for hotspot detection module."""
from datetime import datetime, timedelta
from unittest.mock import Mock, patch
import pytest
from tenets.config import TenetsConfig
from tenets.core.examiner.hotspots import (
FileHotspot,
HotspotDetector,
HotspotMetrics,
HotspotReport,
ModuleHotspot,
detect_hotspots,
)
@pytest.fixture
def config():
"""Create test configuration."""
return TenetsConfig()
@pytest.fixture
def hotspot_detector(config):
"""Create HotspotDetector instance."""
return HotspotDetector(config)
@pytest.fixture
def sample_file_changes():
"""Create sample file change data."""
return {
"hot_file.py": {
"commit_count": 50,
"authors": {"alice@example.com", "bob@example.com", "charlie@example.com"},
"commits": [
{
"sha": f"abc{i:03d}",
"date": datetime.now() - timedelta(days=30 - i),
"message": f"Fix bug #{i}" if i % 3 == 0 else f"Add feature {i}",
"author": f"Author{i % 3}",
}
for i in range(50)
],
"lines_added": 500,
"lines_removed": 300,
"bug_fixes": 15,
"refactors": 5,
"coupled_files": {"related1.py": 10, "related2.py": 8, "related3.py": 5},
"first_commit": datetime.now() - timedelta(days=90),
"last_commit": datetime.now() - timedelta(days=1),
},
"stable_file.py": {
"commit_count": 5,
"authors": {"alice@example.com"},
"commits": [],
"lines_added": 100,
"lines_removed": 20,
"bug_fixes": 1,
"refactors": 0,
"coupled_files": {},
"first_commit": datetime.now() - timedelta(days=180),
"last_commit": datetime.now() - timedelta(days=60),
},
"complex_file.py": {
"commit_count": 30,
"authors": {f"dev{i}@example.com" for i in range(15)},
"commits": [],
"lines_added": 1000,
"lines_removed": 800,
"bug_fixes": 20,
"refactors": 10,
"coupled_files": {f"file{i}.py": i + 1 for i in range(10)},
"first_commit": datetime.now() - timedelta(days=60),
"last_commit": datetime.now() - timedelta(days=2),
},
}
class TestHotspotMetrics:
"""Test suite for HotspotMetrics dataclass."""
def test_hotspot_metrics_creation(self):
"""Test creating HotspotMetrics."""
metrics = HotspotMetrics(
change_frequency=0.5, commit_count=50, author_count=5, complexity=15.0
)
assert metrics.change_frequency == 0.5
assert metrics.commit_count == 50
assert metrics.author_count == 5
assert metrics.complexity == 15.0
def test_hotspot_score(self):
"""Test hotspot_score calculation."""
# Low score (stable file)
stable = HotspotMetrics(
change_frequency=0.05,
commit_count=5,
author_count=2,
complexity=5.0,
bug_fix_commits=0,
coupling=1,
)
stable_score = stable.hotspot_score
assert 0 <= stable_score <= 100
assert stable_score < 30
# High score (problematic file)
problematic = HotspotMetrics(
change_frequency=2.0,
commit_count=100,
author_count=10,
complexity=25.0,
bug_fix_commits=40,
coupling=15,
)
problematic_score = problematic.hotspot_score
assert problematic_score > stable_score
assert problematic_score > 50
def test_risk_level(self):
"""Test risk_level property."""
# Critical risk
critical = HotspotMetrics()
critical.change_frequency = 3.0
critical.complexity = 30.0
critical.commit_count = 100
critical.bug_fix_commits = 50
assert critical.risk_level == "critical"
# High risk
high = HotspotMetrics()
high.change_frequency = 1.0
high.complexity = 15.0
high.commit_count = 50
high.bug_fix_commits = 15
assert high.risk_level == "high"
# Medium risk
medium = HotspotMetrics()
medium.change_frequency = 0.3
medium.complexity = 8.0
assert medium.risk_level == "medium"
# Low risk
low = HotspotMetrics()
assert low.risk_level == "low"
def test_needs_attention(self):
"""Test needs_attention property."""
# Needs attention
critical = HotspotMetrics()
critical.change_frequency = 2.0
critical.complexity = 25.0
critical.commit_count = 50
critical.bug_fix_commits = 20
assert critical.needs_attention == True
# Doesn't need immediate attention
stable = HotspotMetrics()
stable.change_frequency = 0.1
stable.complexity = 5.0
assert stable.needs_attention == False
class TestFileHotspot:
"""Test suite for FileHotspot dataclass."""
def test_file_hotspot_creation(self):
"""Test creating FileHotspot."""
hotspot = FileHotspot(path="src/main.py", name="main.py", size=500, language="Python")
assert hotspot.path == "src/main.py"
assert hotspot.name == "main.py"
assert hotspot.size == 500
assert hotspot.language == "Python"
def test_summary(self):
"""Test summary property."""
hotspot = FileHotspot(path="hot.py", name="hot.py")
# Set problematic metrics
hotspot.metrics.change_frequency = 1.5
hotspot.metrics.complexity = 25.0
hotspot.metrics.bug_fix_commits = 10
hotspot.metrics.author_count = 15
summary = hotspot.summary
assert "hot.py" in summary
assert "changes" in summary
assert "complexity" in summary
assert "bug fixes" in summary
assert "authors" in summary
# Stable file
stable = FileHotspot(path="stable.py", name="stable.py")
stable_summary = stable.summary
assert "stable" in stable_summary
class TestModuleHotspot:
"""Test suite for ModuleHotspot dataclass."""
def test_module_hotspot_creation(self):
"""Test creating ModuleHotspot."""
module = ModuleHotspot(path="src/auth", name="auth", file_count=10)
assert module.path == "src/auth"
assert module.name == "auth"
assert module.file_count == 10
def test_hotspot_density(self):
"""Test hotspot_density property."""
module = ModuleHotspot(path="src", name="src", file_count=10)
# Add hotspot files
for i in range(3):
module.hotspot_files.append(FileHotspot(path=f"file{i}.py", name=f"file{i}.py"))
assert module.hotspot_density == 0.3
# No files
empty_module = ModuleHotspot(path="empty", name="empty", file_count=0)
assert empty_module.hotspot_density == 0.0
def test_module_health(self):
"""Test module_health property."""
# Healthy module
healthy = ModuleHotspot(path="healthy", name="healthy", file_count=10, stability_score=80.0)
assert healthy.module_health == "healthy"
# Warning module
warning = ModuleHotspot(path="warning", name="warning", file_count=10, stability_score=50.0)
for i in range(4):
warning.hotspot_files.append(FileHotspot(path=f"file{i}.py", name=f"file{i}.py"))
assert warning.module_health == "warning"
# Unhealthy module
unhealthy = ModuleHotspot(
path="unhealthy", name="unhealthy", file_count=10, stability_score=30.0
)
for i in range(6):
unhealthy.hotspot_files.append(FileHotspot(path=f"file{i}.py", name=f"file{i}.py"))
assert unhealthy.module_health == "unhealthy"
class TestHotspotReport:
"""Test suite for HotspotReport dataclass."""
def test_hotspot_report_creation(self):
"""Test creating HotspotReport."""
report = HotspotReport(
total_files_analyzed=100, total_hotspots=15, critical_count=3, high_count=5
)
assert report.total_files_analyzed == 100
assert report.total_hotspots == 15
assert report.critical_count == 3
assert report.high_count == 5
def test_total_count(self):
"""Test total_count property."""
report = HotspotReport()
for i in range(5):
report.file_hotspots.append(FileHotspot(path=f"file{i}.py", name=f"file{i}.py"))
assert report.total_count == 5
def test_health_score(self):
"""Test health_score property."""
# Good health
good_report = HotspotReport(
total_files_analyzed=100, total_hotspots=5, critical_count=0, high_count=2
)
good_score = good_report.health_score
assert 60 <= good_score <= 100
# Poor health
poor_report = HotspotReport(
total_files_analyzed=100, total_hotspots=30, critical_count=10, high_count=15
)
poor_report.coupling_clusters = [["f1", "f2", "f3"] for _ in range(5)]
poor_score = poor_report.health_score
assert poor_score < good_score
assert 0 <= poor_score <= 100
def test_to_dict(self):
"""Test to_dict conversion."""
report = HotspotReport(
total_files_analyzed=50, total_hotspots=10, critical_count=2, high_count=3
)
# Add a hotspot
hotspot = FileHotspot(path="hot.py", name="hot.py")
hotspot.metrics.hotspot_score = 75.0
hotspot.metrics.risk_level = "high"
hotspot.problem_indicators = ["High complexity", "Many bugs"]
report.file_hotspots.append(hotspot)
# Add module
module = ModuleHotspot(path="src", name="src")
report.module_hotspots.append(module)
# Add recommendations
report.recommendations = ["Refactor hot.py"]
result = report.to_dict()
assert result["total_files_analyzed"] == 50
assert result["total_hotspots"] == 10
assert len(result["hotspot_summary"]) == 1
assert len(result["module_summary"]) == 1
assert len(result["recommendations"]) == 1
class TestHotspotDetector:
"""Test suite for HotspotDetector class."""
def test_initialization(self, config):
"""Test HotspotDetector initialization."""
detector = HotspotDetector(config)
assert detector.config == config
assert detector.git_analyzer is None
@patch("tenets.core.examiner.hotspots.GitAnalyzer")
def test_detect_no_repo(self, mock_git_class, hotspot_detector, tmp_path):
"""Test detection with no git repository."""
mock_git = Mock()
mock_git.is_repo.return_value = False
mock_git_class.return_value = mock_git
report = hotspot_detector.detect(tmp_path)
assert report.total_files_analyzed == 0
assert report.total_hotspots == 0
@patch("tenets.core.examiner.hotspots.GitAnalyzer")
def test_detect_basic(self, mock_git_class, hotspot_detector, tmp_path):
"""Test basic hotspot detection."""
mock_git = Mock()
mock_git.is_repo.return_value = True
# Create mock commits
commits = []
for i in range(20):
commit = Mock()
commit.hexsha = f"abc{i:03d}"
commit.committed_date = (datetime.now() - timedelta(days=20 - i)).timestamp()
commit.message = "Fix bug" if i % 3 == 0 else "Add feature"
commit.author = Mock(email=f"dev{i % 3}@example.com")
# Add file changes
commit.stats = Mock()
commit.stats.files = {
"hot_file.py": {"insertions": 10, "deletions": 5},
"normal.py": {"insertions": 5, "deletions": 2},
}
commits.append(commit)
mock_git.get_commits_since.return_value = commits
mock_git_class.return_value = mock_git
report = hotspot_detector.detect(tmp_path, since_days=30)
assert report.total_files_analyzed > 0
def test_analyze_file_changes(self, hotspot_detector):
"""Test file change analysis."""
hotspot_detector.git_analyzer = Mock()
# Create mock commits
commits = []
for i in range(10):
commit = Mock()
commit.hexsha = f"abc{i:03d}"
commit.committed_date = (datetime.now() - timedelta(days=10 - i)).timestamp()
commit.message = "Fix critical bug" if i % 2 == 0 else "Refactor code"
commit.author = Mock(email=f"dev{i % 2}@example.com", name=f"Dev{i % 2}")
commit.stats = Mock()
commit.stats.files = {
"file1.py": {"insertions": 20, "deletions": 10},
"file2.py": {"insertions": 15, "deletions": 5},
}
commits.append(commit)
hotspot_detector.git_analyzer.get_commits_since.return_value = commits
file_changes = hotspot_detector._analyze_file_changes(datetime.now() - timedelta(days=30))
assert "file1.py" in file_changes
assert "file2.py" in file_changes
assert file_changes["file1.py"]["commit_count"] == 10
assert file_changes["file1.py"]["bug_fixes"] == 5
assert file_changes["file1.py"]["refactors"] == 5
assert len(file_changes["file1.py"]["authors"]) == 2
def test_analyze_file_hotspot(self, hotspot_detector, sample_file_changes):
"""Test single file hotspot analysis."""
change_data = sample_file_changes["hot_file.py"]
# Create mock analyzed file
analyzed_file = Mock()
analyzed_file.path = "hot_file.py"
analyzed_file.complexity = Mock(cyclomatic=25)
analyzed_file.lines = 500
analyzed_file.language = "Python"
hotspot = hotspot_detector._analyze_file_hotspot(
"hot_file.py", change_data, [analyzed_file], since_days=90
)
assert hotspot.path == "hot_file.py"
assert hotspot.name == "hot_file.py"
assert hotspot.metrics.commit_count == 50
assert hotspot.metrics.author_count == 3
assert hotspot.metrics.bug_fix_commits == 15
assert hotspot.metrics.complexity == 25
assert hotspot.size == 500
assert hotspot.language == "Python"
assert len(hotspot.coupled_files) > 0
assert len(hotspot.problem_indicators) > 0
assert len(hotspot.recommended_actions) > 0
def test_identify_problems(self, hotspot_detector):
"""Test problem identification."""
hotspot = FileHotspot(path="test.py", name="test.py")
# Set various problematic metrics
hotspot.metrics.change_frequency = 1.0
hotspot.metrics.complexity = 25
hotspot.metrics.bug_fix_commits = 12
hotspot.metrics.author_count = 15
hotspot.metrics.coupling = 12
hotspot.metrics.churn_rate = 15
hotspot.metrics.recency_days = 3
hotspot.metrics.commit_count = 10
hotspot.size = 1200
problems = hotspot_detector._identify_problems(hotspot)
assert len(problems) > 0
assert any("change frequency" in p for p in problems)
assert any("complexity" in p for p in problems)
assert any("bug fixes" in p for p in problems)
assert any("contributors" in p for p in problems)
assert any("coupled" in p for p in problems)
assert any("churn" in p for p in problems)
assert any("large file" in p for p in problems)
def test_recommend_actions(self, hotspot_detector):
"""Test action recommendation."""
hotspot = FileHotspot(path="test.py", name="test.py")
# High complexity
hotspot.metrics.complexity = 25
actions = hotspot_detector._recommend_actions(hotspot)
assert any("refactor" in a.lower() for a in actions)
# Large file
hotspot.size = 1500
actions = hotspot_detector._recommend_actions(hotspot)
assert any("split" in a.lower() or "smaller" in a.lower() for a in actions)
# Many bugs
hotspot.metrics.bug_fix_commits = 10
actions = hotspot_detector._recommend_actions(hotspot)
assert any("test" in a.lower() or "review" in a.lower() for a in actions)
# High coupling
hotspot.metrics.coupling = 15
actions = hotspot_detector._recommend_actions(hotspot)
assert any("coupling" in a.lower() or "abstraction" in a.lower() for a in actions)
def test_calculate_stability(self, hotspot_detector):
"""Test stability score calculation."""
# Stable file
stable = FileHotspot(path="stable.py", name="stable.py")
stable.metrics.change_frequency = 0.1
stable.metrics.commit_count = 10
stable.metrics.bug_fix_commits = 1
stable.metrics.author_count = 2
stable.metrics.churn_rate = 1.0
stable.metrics.recency_days = 60
stability = hotspot_detector._calculate_stability(stable)
assert stability > 70
# Unstable file
unstable = FileHotspot(path="unstable.py", name="unstable.py")
unstable.metrics.change_frequency = 2.0
unstable.metrics.commit_count = 50
unstable.metrics.bug_fix_commits = 25
unstable.metrics.author_count = 10
unstable.metrics.churn_rate = 20.0
unstable.metrics.recency_days = 2
instability = hotspot_detector._calculate_stability(unstable)
assert instability < stability
assert instability < 50
def test_detect_coupling_clusters(self, hotspot_detector):
"""Test coupling cluster detection."""
file_changes = {
"file1.py": {"coupled_files": {"file2.py": 10, "file3.py": 8, "file4.py": 2}},
"file2.py": {"coupled_files": {"file1.py": 10, "file3.py": 7, "file4.py": 1}},
"file3.py": {"coupled_files": {"file1.py": 8, "file2.py": 7, "file4.py": 1}},
"file4.py": {"coupled_files": {"file1.py": 2, "file2.py": 1, "file3.py": 1}},
"isolated.py": {"coupled_files": {}},
}
clusters = hotspot_detector._detect_coupling_clusters(file_changes)
assert len(clusters) > 0
# Should find cluster with file1, file2, file3
assert any(
set(["file1.py", "file2.py", "file3.py"]).issubset(set(cluster)) for cluster in clusters
)
def test_estimate_remediation_effort(self, hotspot_detector):
"""Test effort estimation."""
report = HotspotReport()
# Add hotspots with different risk levels
for i in range(3):
hotspot = FileHotspot(path=f"critical{i}.py", name=f"critical{i}.py")
hotspot.metrics.risk_level = "critical"
hotspot.size = 800
hotspot.metrics.complexity = 25
hotspot.metrics.coupling = 12
report.file_hotspots.append(hotspot)
for i in range(5):
hotspot = FileHotspot(path=f"high{i}.py", name=f"high{i}.py")
hotspot.metrics.risk_level = "high"
hotspot.size = 400
hotspot.metrics.complexity = 15
report.file_hotspots.append(hotspot)
effort = hotspot_detector._estimate_remediation_effort(report)
assert effort > 0
# Should be reasonable estimate
assert 50 <= effort <= 500
def test_generate_recommendations(self, hotspot_detector):
"""Test recommendation generation."""
report = HotspotReport(critical_count=5, high_count=10)
# Add coupling clusters
report.coupling_clusters = [
["f1", "f2", "f3"],
["f4", "f5"],
["f6", "f7", "f8"],
["f9", "f10"],
]
# Add unhealthy modules
for i in range(3):
module = ModuleHotspot(path=f"module{i}", name=f"module{i}")
module.module_health = "unhealthy"
report.module_hotspots.append(module)
# Add top problems
report.top_problems = [("High Complexity", 20), ("Frequent Changes", 15), ("Bug Prone", 10)]
# Set poor health score
report.health_score = 30.0
# High effort estimate
report.estimated_effort = 200.0
recommendations = hotspot_detector._generate_recommendations(report)
assert len(recommendations) > 0
assert any("critical" in r.lower() for r in recommendations)
assert any("refactoring" in r.lower() for r in recommendations)
assert any("coupling" in r.lower() for r in recommendations)
class TestDetectHotspotsFunction:
"""Test the detect_hotspots convenience function."""
@patch("tenets.core.examiner.hotspots.HotspotDetector")
def test_detect_hotspots_basic(self, mock_detector_class, tmp_path):
"""Test basic hotspot detection."""
mock_detector = Mock()
mock_report = HotspotReport(total_hotspots=5)
mock_detector.detect.return_value = mock_report
mock_detector_class.return_value = mock_detector
report = detect_hotspots(tmp_path)
assert isinstance(report, HotspotReport)
assert report.total_hotspots == 5
mock_detector.detect.assert_called_once()
@patch("tenets.core.examiner.hotspots.HotspotDetector")
def test_detect_hotspots_with_options(self, mock_detector_class, tmp_path):
"""Test with various options."""
mock_detector = Mock()
mock_detector.detect.return_value = HotspotReport()
mock_detector_class.return_value = mock_detector
files = [Mock()]
report = detect_hotspots(tmp_path, files=files, threshold=15)
assert isinstance(report, HotspotReport)
mock_detector.detect.assert_called_with(tmp_path, files=files, threshold=15)
@patch("tenets.core.examiner.hotspots.HotspotDetector")
def test_detect_hotspots_with_config(self, mock_detector_class, tmp_path, config):
"""Test with custom config."""
mock_detector = Mock()
mock_detector.detect.return_value = HotspotReport()
mock_detector_class.return_value = mock_detector
report = detect_hotspots(tmp_path, config=config)
assert isinstance(report, HotspotReport)
mock_detector_class.assert_called_with(config)