from __future__ import annotations import json from dataclasses import dataclass, field from pathlib import Path from typing import Dict, List, Optional, Set, Tuple @dataclass class CoverageBin: name: str hit_count: int goal: int = 1 @property def covered(self) -> bool: return self.hit_count >= self.goal @dataclass class SimResult: passed: bool total_bins: int = 0 covered_bins: int = 0 bins: List[CoverageBin] = field(default_factory=list) errors: List[str] = field(default_factory=list) log_output: str = "" seed: int = 0 @property def coverage_pct(self) -> float: if self.total_bins == 0: return 0.0 return (self.covered_bins / self.total_bins) * 100.0 @property def uncovered_bins(self) -> List[CoverageBin]: return [b for b in self.bins if not b.covered] class CoverageDB: """Merges coverage results across multiple seeds for regression.""" def __init__(self): self.seed_results: List[SimResult] = [] self.merged_bins: Dict[str, CoverageBin] = {} def add_seed_result(self, result: SimResult) -> None: self.seed_results.append(result) for b in result.bins: key = b.name if key in self.merged_bins: existing = self.merged_bins[key] existing.hit_count = max(existing.hit_count, b.hit_count) existing.goal = max(existing.goal, b.goal) else: self.merged_bins[key] = CoverageBin(name=b.name, hit_count=b.hit_count, goal=b.goal) def merge(self) -> SimResult: bins = list(self.merged_bins.values()) covered = sum(1 for b in bins if b.covered) total = len(bins) return SimResult( passed=covered == total, total_bins=total, covered_bins=covered, bins=bins, errors=[], log_output=self._format_summary(bins, covered, total), ) def _format_summary(self, bins: List[CoverageBin], covered: int, total: int) -> str: pct = (covered / total * 100) if total else 0 lines = [ f"--- CoverageDB merged ({len(self.seed_results)} seeds) ---", ] for b in bins: status = "HIT" if b.covered else "MISS" lines.append(f"COVERAGE: {b.name} {b.hit_count}/{b.goal} [{status}]") lines.append(f"--- Merged: {covered}/{total} ({pct:.1f}%) ---") return "\n".join(lines) def save(self, path: str) -> None: data = { "num_seeds": len(self.seed_results), "merged": {k: {"hit_count": v.hit_count, "goal": v.goal} for k, v in self.merged_bins.items()}, } Path(path).write_text(json.dumps(data, indent=2), encoding="utf-8") @classmethod def load(cls, path: str) -> CoverageDB: data = json.loads(Path(path).read_text(encoding="utf-8")) db = cls() for name, bdata in data.get("merged", {}).items(): db.merged_bins[name] = CoverageBin(name=name, **bdata) return db @property def uncovered(self) -> List[CoverageBin]: return [b for b in self.merged_bins.values() if not b.covered] class Simulator: def __init__(self, work_dir: str = "sim_output"): self.work_dir = work_dir def run(self, files: List[str], top: str = "testbench", plusargs: Optional[List[str]] = None) -> SimResult: raise NotImplementedError def run_multi_seed(self, files: List[str], num_seeds: int = 3, top: str = "testbench") -> Tuple[SimResult, CoverageDB]: db = CoverageDB() merged = None for s in range(num_seeds): result = self.run(files, top=top, plusargs=[f"+seed={s+1}"]) result.seed = s + 1 db.add_seed_result(result) merged = db.merge() return merged, db def parse_coverage(self, log: str) -> SimResult: raise NotImplementedError def name(self) -> str: return self.__class__.__name__