@dataclass
@register_rule("testing")
class TestCoverageRule(ProjectRule):
"""Check test coverage via pytest-cov.
Scoring: coverage percentage directly (e.g., 90% → score 90).
Pass threshold: 90%.
"""
min_coverage: float = 90.0
@property
def rule_id(self) -> str:
"""Unique identifier for this rule."""
return "QUALITY_COVERAGE"
def check(self, project_path: Path) -> CheckResult:
"""Check test coverage and capture failures with pytest-cov.
Delegates to ``run_tests(mode='compact')`` from the shared
test runner for structured output, then converts the result
to a ``CheckResult``.
"""
from axm_audit.core.test_runner import run_tests
report = run_tests(project_path, mode="compact", stop_on_first=False)
return self._report_to_result(report)
def _report_to_result(self, report: TestReport) -> CheckResult:
"""Convert a ``TestReport`` to a ``CheckResult``."""
coverage_pct = report.coverage if report.coverage is not None else 0.0
score = int(coverage_pct)
has_failures = report.failed > 0 or report.errors > 0
passed = coverage_pct >= self.min_coverage and not has_failures
# Build failure details for backwards-compatible format
failures: list[dict[str, str]] = [
{"test": f.test, "traceback": f.message} for f in report.failures
]
if report.coverage is None:
return CheckResult(
rule_id=self.rule_id,
passed=False,
message="No coverage data (pytest-cov not configured)",
severity=Severity.WARNING,
details={"coverage": 0.0, "score": 0, "failures": failures},
fix_hint="Add pytest-cov: uv add --dev pytest-cov",
)
if has_failures:
total_fails = report.failed + report.errors
message = (
f"Test coverage: {coverage_pct:.0f}% ({total_fails} test(s) failed)"
)
else:
message = f"Test coverage: {coverage_pct:.0f}% ({score}/100)"
fix_hints = self._generate_fix_hints(has_failures, coverage_pct)
return CheckResult(
rule_id=self.rule_id,
passed=passed,
message=message,
severity=Severity.WARNING if not passed else Severity.INFO,
details={
"coverage": coverage_pct,
"score": score,
"failures": failures,
},
fix_hint=fix_hints,
)
def _generate_fix_hints(
self, has_failures: bool, coverage_pct: float
) -> str | None:
"""Generate fix hints based on failures and coverage."""
fix_hints: list[str] = []
if has_failures:
fix_hints.append("Fix failing tests")
if coverage_pct < self.min_coverage:
fix_hints.append(f"Increase test coverage to >= {self.min_coverage:.0f}%")
return "; ".join(fix_hints) if fix_hints else None