"""Per-file coverage threshold validation for quality-managed modules.""" from __future__ import annotations import xml.etree.ElementTree as ET from pathlib import Path from typing import Any def _load_rates(xml_path: Path, root: Path) -> dict[str, dict[str, float | None]]: """Load per-file line and branch percentages from a Cobertura XML report.""" tree = ET.parse(xml_path) xml_root = tree.getroot() source_roots = [ Path(node.text) for node in xml_root.findall("./sources/source") if node.text ] rates: dict[str, dict[str, float | None]] = {} for class_node in xml_root.findall(".//class"): filename = class_node.attrib.get("filename") line_rate = class_node.attrib.get("line-rate") if not filename or line_rate is None: continue normalized = filename.replace("\\", "/") if normalized.startswith("/"): key = Path(normalized).relative_to(root).as_posix() else: key = normalized for source_root in source_roots: candidate = source_root / filename if candidate.exists(): key = candidate.relative_to(root).as_posix() break branch_rate = class_node.attrib.get("branch-rate") rates[key] = { "line": float(line_rate) * 100.0, "branch": None if branch_rate is None else float(branch_rate) * 100.0, } return rates def run_check(contract: dict[str, Any], root: Path, xml_path: Path) -> list[str]: """Return human-readable issues for tracked files below the coverage floor. Line and branch rates are both enforced per file so a single weak module cannot hide behind aggregate suite coverage or line-only reporting. """ if not xml_path.exists(): return [f"coverage xml missing: {xml_path.relative_to(root)}"] rates = _load_rates(xml_path, root) minimum = float(contract.get("coverage", {}).get("minimum_percent", 95.0)) issues: list[str] = [] for relative_path in contract.get("coverage", {}).get("tracked_files", []): normalized = relative_path.replace("\\", "/") file_rates = rates.get(normalized) if file_rates is None: issues.append(f"coverage missing for tracked file: {relative_path}") continue for metric in ("line", "branch"): percent = file_rates[metric] if percent is None: issues.append( f"{metric} coverage missing for tracked file: {relative_path}" ) elif percent + 1e-9 < minimum: issues.append( f"{metric} coverage below {minimum:.1f}%: " f"{relative_path} ({percent:.1f}%)" ) return issues def compute_workspace_line_coverage( contract: dict[str, Any], root: Path, xml_path: Path, ) -> float: """Compute mean line coverage across tracked files present in the XML.""" if not xml_path.exists(): return 0.0 rates = _load_rates(xml_path, root) samples: list[float] = [] for relative_path in contract.get("coverage", {}).get("tracked_files", []): normalized = relative_path.replace("\\", "/") file_rates = rates.get(normalized) if file_rates is not None: samples.append(file_rates["line"]) if not samples: return 0.0 return round(sum(samples) / len(samples), 3)