Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 105 additions & 1 deletion src/dvsim/job/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
from dvsim.modes import BuildMode
from dvsim.sim.flow import SimCfg


__all__ = (
"CompileSim",
"CovVPlan",
"Deploy",
)

Expand Down Expand Up @@ -1028,3 +1028,107 @@ def _set_attrs(self) -> None:
self.qual_name = self.target
self.full_name = f"{self.sim_cfg.name}{self._variant_suffix}:{self.qual_name}"
self.input_dirs += [self.cov_merge_db_dir]


class CovVPlan(Deploy):
"""Abstraction for generating a Verification Plan (vPlan) report using DVPlan."""

target = "cov_vplan"
weight = 10

def __init__(self, cov_report_job, sim_cfg) -> None:
self.report_job = cov_report_job
# Populated by post_finish() once the job completes successfully.
self.vplan_coverage: float | None = None
super().__init__(sim_cfg)
self.dependencies.append(cov_report_job)

def _define_attrs(self) -> None:
super()._define_attrs()
self.mandatory_cmd_attrs.update(
{
"proj_root": False,
"vplan": False,
}
)
self.mandatory_misc_attrs.update(
{
"dut_instance": False,
}
)

def _set_attrs(self) -> None:
self.cov_vplan_dir = f"{self.sim_cfg.scratch_path}/{self.target}"

super()._set_attrs()
self.qual_name = self.target
self.full_name = f"{self.sim_cfg.name}{self._variant_suffix}:{self.qual_name}"

self.prepare_opts = self.sim_cfg.cov_vplan_prepare_opts
self.process_opts = self.sim_cfg.cov_vplan_process_opts

# Calculate IP root.
vplan_path = Path(self.vplan)
self.ip_root = str(vplan_path.parent.parent)

# Use fixed output filenames so the report location is always predictable.
self.annotated_hjson = f"{self.odir}/vplan_annotated.hjson"
self.gen_html = f"{self.odir}/vplan_annotated.html"
self.output_dirs = [self.odir]

def post_finish(self) -> Callable[[JobStatus], None]:
"""Get post finish callback."""

def callback(status: JobStatus) -> None:
"""Extract the overall vPlan normalised coverage from the annotated HJSON."""
if self.dry_run or status != JobStatus.PASSED:
return
hjson_path = Path(self.annotated_hjson)
if not hjson_path.exists():
return
try:
import hjson # noqa: PLC0415

with hjson_path.open() as f:
data = hjson.load(f)
# HJSON vPlans are keyed: {dut_name: {fields...}}
root_node = next(iter(data.values()), {})
raw = root_node.get("Normalized_Coverage")
if raw is not None:
self.vplan_coverage = float(str(raw).rstrip(" %"))
except Exception: # noqa: BLE001
log.debug("Could not extract vPlan coverage from '%s'.", hjson_path)

return callback

def _construct_cmd(self) -> str:
"""Construct the pure bash shell command, bypassing the base Makefile assumption."""
import shlex
import shutil

if shutil.which("dvplan") is None:
fallback = (
"echo 'WARNING: dvplan tool not installed in PATH. Skipping vPlan generation.'"
)
return f"/usr/bin/env bash -c {shlex.quote(fallback)}"

def format_opts(opts):
return " ".join(opts) if isinstance(opts, list) else str(opts)

prepare_opts_str = format_opts(self.prepare_opts)
process_opts_str = format_opts(self.process_opts)

prepare_cmd = f"dvplan prepare_vplan {prepare_opts_str} {self.ip_root} {self.vplan} {self.annotated_hjson}"
prepare_cmd = " ".join(prepare_cmd.split())

vendor_tool = f"{self.sim_cfg.tool}_report"
report_path = self.report_job.cov_report_dir

process_cmd = (
f"dvplan process_results {process_opts_str} --coverage {vendor_tool} {report_path} "
f"-R {self.gen_html} -s {self.sim_cfg.name} {self.dut_instance} {self.annotated_hjson}"
)
process_cmd = " ".join(process_cmd.split())

full_command = f"set -e; mkdir -p {self.odir}; {prepare_cmd} && {process_cmd}"
return f"/usr/bin/env bash -c {shlex.quote(full_command)}"
4 changes: 4 additions & 0 deletions src/dvsim/sim/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ class SimFlowResults(BaseModel):
"""Coverage metrics."""
cov_report_page: Path | None
"""Optional path linking to the generated coverage report dashboard page."""
vplan_report_page: Path | None
Comment thread
machshev marked this conversation as resolved.
"""Optional path linking to the generated verification plan (vPlan) reports."""
vplan_coverage: float | None = None
"""Overall normalised coverage (%) extracted from the back-annotated vPlan."""

failed_jobs: BucketedFailures
"""Bucketed failed job overview."""
Expand Down
20 changes: 20 additions & 0 deletions src/dvsim/sim/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
CovMerge,
CovReport,
CovUnr,
CovVPlan,
RunTest,
)
from dvsim.job.status import JobStatus
Expand Down Expand Up @@ -157,6 +158,13 @@ def __init__(self, flow_cfg_file, hjson_data, args, mk_config) -> None:
self.cov_report_dir = ""
self.cov_report_page = ""

# Options for vPlan processing
# dut_instance is the hierarchical testbench path to the DUT (e.g. "tb.dut"),
# distinct from `name`/`qual_name` which identify the sim config itself.
self.dut_instance = ""
Comment thread
machshev marked this conversation as resolved.
self.cov_vplan_prepare_opts = []
self.cov_vplan_process_opts = []

# Options from tools - for building and running tests
self.build_cmd = ""
self.flist_gen_cmd = ""
Expand Down Expand Up @@ -550,6 +558,10 @@ def _create_deploy_objects(self) -> None:
self.cov_report_deploy = CovReport(self.cov_merge_deploy, self)
self.deploy += [self.cov_merge_deploy, self.cov_report_deploy]

if getattr(self, "vplan", False):
self.cov_vplan_deploy = CovVPlan(self.cov_report_deploy, self)
self.deploy.append(self.cov_vplan_deploy)

def _cov_analyze(self) -> None:
"""Open GUI tool for coverage analysis.

Expand Down Expand Up @@ -821,6 +833,12 @@ def make_test_result(tr) -> TestResult | None:
cov_report_dir = self.cov_report_dir or "cov_report"
cov_report_page = Path(cov_report_dir, self.cov_report_page)

vplan_report_page = None
vplan_coverage = None
if getattr(self, "cov_vplan_deploy", None):
vplan_report_page = Path(self.scratch_path) / CovVPlan.target / "vplan_annotated.html"
vplan_coverage = self.cov_vplan_deploy.vplan_coverage

failures = BucketedFailures.from_job_status(results=run_results)
if failures.buckets:
self.errors_seen = True
Expand All @@ -835,6 +853,8 @@ def make_test_result(tr) -> TestResult | None:
stages=stages,
coverage=coverage_model,
cov_report_page=cov_report_page,
vplan_report_page=vplan_report_page,
Comment thread
machshev marked this conversation as resolved.
vplan_coverage=vplan_coverage,
failed_jobs=failures,
passed=total_passed,
total=total_runs,
Expand Down
6 changes: 5 additions & 1 deletion src/dvsim/sim/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,10 +329,14 @@ def render_coverage_table(self, results: SimFlowResults) -> str:
for k, v in results.coverage.flattened().items()
if v is not None
}
if not cov_results and not results.cov_report_page:
if not cov_results and not results.cov_report_page and not results.vplan_report_page:
return ""

report_md = "## Coverage Results"
if results.vplan_report_page:
report_md += f"\n### [vPlan Dashboard]({results.vplan_report_page})"
if results.vplan_coverage is not None:
report_md += f"\n\nVerification Plan Coverage: {results.vplan_coverage:.2f} %\n"
if results.cov_report_page:
report_md += f"\n### [Coverage Dashboard]({results.cov_report_page})"
if cov_results:
Expand Down
Loading