From 3fbb93460c5966053e7a66c6c5a505f73434d403 Mon Sep 17 00:00:00 2001 From: martin-velay Date: Wed, 25 Mar 2026 16:31:07 +0100 Subject: [PATCH] [src] Integrate DVPlan tool commands - Integrate DVPlan subcommands. These commands will be run only if: - DVPlan tool is available - Coverage is enabled - A "".vplan" file is found in the cuurent sim_cfg.hjson file - When the condition are met, at the end the regression/simulation, the DVPlan tool will: - Check the coverage of the specification - Generate a derived vPlan in the simulation directory - Perform back-annotation of the vPlan with the coverage data - Compute the coverage of the vPlan - Generate a report based on obtained annotated vPlan Signed-off-by: martin-velay --- src/dvsim/job/deploy.py | 87 ++++++++++++++++++++++++++++++++++- src/dvsim/sim/data.py | 2 + src/dvsim/sim/flow.py | 16 +++++++ src/dvsim/sim/report.py | 4 +- src/dvsim/sim/tool/base.py | 24 ++++++++++ src/dvsim/sim/tool/vcs.py | 24 ++++++++++ src/dvsim/sim/tool/xcelium.py | 24 ++++++++++ 7 files changed, 179 insertions(+), 2 deletions(-) diff --git a/src/dvsim/job/deploy.py b/src/dvsim/job/deploy.py index 71247b85..a4d57f77 100644 --- a/src/dvsim/job/deploy.py +++ b/src/dvsim/job/deploy.py @@ -4,6 +4,7 @@ """Job deployment mechanism.""" +import os import pprint import random import shlex @@ -28,9 +29,9 @@ from dvsim.flow.sim import SimCfg from dvsim.modes import BuildMode - __all__ = ( "CompileSim", + "CovVPlan", "Deploy", ) @@ -876,3 +877,87 @@ 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 + 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 = self.sim_cfg.cov_vplan_dir + if not self.cov_vplan_dir: + 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 + + # Determine paths for the intermediate HJSON and the final HTML report + self.annotated_hjson = f"{self.odir}/{self.sim_cfg.name}_annotated_vplan.hjson" + self.gen_html = f"{self.odir}/{self.sim_cfg.name}_annotated_vplan.html" + self.output_dirs = [self.odir] + + # Calculate IP root + vplan_path = Path(self.vplan) + self.ip_root = str(vplan_path.parent.parent) + + 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"/bin/bash -c {shlex.quote(fallback)}" + + plugin = get_sim_tool_plugin(tool=self.sim_cfg.tool) + + 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 = plugin.get_dvplan_parser_name() + cov_report_txt = getattr(self.report_job, "cov_report_txt", "") + report_path = plugin.get_dvplan_report_path( + cov_report_dir=self.report_job.cov_report_dir, cov_report_txt=cov_report_txt + ) + + 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"/bin/bash -c {shlex.quote(full_command)}" diff --git a/src/dvsim/sim/data.py b/src/dvsim/sim/data.py index f9427da4..7b40dbff 100644 --- a/src/dvsim/sim/data.py +++ b/src/dvsim/sim/data.py @@ -192,6 +192,8 @@ 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 + """Optional path linking to the generated verification plan (vPlan) reports.""" failed_jobs: BucketedFailures """Bucketed failed job overview.""" diff --git a/src/dvsim/sim/flow.py b/src/dvsim/sim/flow.py index 2ba51873..d533b4da 100644 --- a/src/dvsim/sim/flow.py +++ b/src/dvsim/sim/flow.py @@ -21,6 +21,7 @@ CovMerge, CovReport, CovUnr, + CovVPlan, RunTest, ) from dvsim.job.status import JobStatus @@ -158,6 +159,12 @@ def __init__(self, flow_cfg_file, hjson_data, args, mk_config) -> None: self.cov_report_dir = "" self.cov_report_page = "" + # Options for vPlan processing + self.cov_vplan_dir = "" + self.dut_instance = "" + 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 = "" @@ -560,6 +567,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 hasattr(self, "vplan") and self.vplan: + self.cov_vplan_deploy = CovVPlan(self.cov_report_deploy, self) + self.deploy.append(self.cov_vplan_deploy) + # Create initial set of directories before kicking off the regression. self._create_dirs() @@ -839,6 +850,10 @@ 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 + if getattr(self, "cov_vplan_deploy", None): + vplan_report_page = Path(self.cov_vplan_deploy.gen_html) + failures = BucketedFailures.from_job_status(results=run_results) if failures.buckets: self.errors_seen = True @@ -853,6 +868,7 @@ def make_test_result(tr) -> TestResult | None: stages=stages, coverage=coverage_model, cov_report_page=cov_report_page, + vplan_report_page=vplan_report_page, failed_jobs=failures, passed=total_passed, total=total_runs, diff --git a/src/dvsim/sim/report.py b/src/dvsim/sim/report.py index 56824ac3..60b0a268 100644 --- a/src/dvsim/sim/report.py +++ b/src/dvsim/sim/report.py @@ -329,10 +329,12 @@ 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.cov_report_page: report_md += f"\n### [Coverage Dashboard]({results.cov_report_page})" if cov_results: diff --git a/src/dvsim/sim/tool/base.py b/src/dvsim/sim/tool/base.py index 896ac20a..31d1fd9b 100644 --- a/src/dvsim/sim/tool/base.py +++ b/src/dvsim/sim/tool/base.py @@ -82,3 +82,27 @@ def get_coverage_metrics(raw_metrics: Mapping[str, float | None] | None) -> Cove """ ... + + @staticmethod + def get_dvplan_parser_name() -> str: + """Return the name of the DVPlan parser for this tool. + + Returns: + The DVPlan coverage parser string. + + """ + return "synopsys_report" + + @staticmethod + def get_dvplan_report_path(cov_report_dir: str, cov_report_txt: str) -> str: + """Return the path to the coverage report required by DVPlan. + + Args: + cov_report_dir: Path to the coverage report directory. + cov_report_txt: Path to the text-based coverage report. + + Returns: + Path to the target coverage report file or directory. + + """ + return cov_report_txt or cov_report_dir diff --git a/src/dvsim/sim/tool/vcs.py b/src/dvsim/sim/tool/vcs.py index 4948416f..437f33cc 100644 --- a/src/dvsim/sim/tool/vcs.py +++ b/src/dvsim/sim/tool/vcs.py @@ -132,3 +132,27 @@ def get_coverage_metrics(raw_metrics: Mapping[str, float | None] | None) -> Cove fsm=raw_metrics.get("fsm"), ), ) + + @staticmethod + def get_dvplan_parser_name() -> str: + """Return the name of the DVPlan parser for this tool. + + Returns: + The DVPlan coverage parser string. + + """ + return "synopsys_report" + + @staticmethod + def get_dvplan_report_path(cov_report_dir: str, cov_report_txt: str) -> str: + """Return the path to the coverage report required by DVPlan. + + Args: + cov_report_dir: Path to the coverage report directory. + cov_report_txt: Path to the text-based coverage report. + + Returns: + Path to the target coverage report file or directory. + + """ + return cov_report_dir diff --git a/src/dvsim/sim/tool/xcelium.py b/src/dvsim/sim/tool/xcelium.py index 0362f48d..030fa0aa 100644 --- a/src/dvsim/sim/tool/xcelium.py +++ b/src/dvsim/sim/tool/xcelium.py @@ -157,3 +157,27 @@ def get_coverage_metrics(raw_metrics: Mapping[str, float | None] | None) -> Cove fsm=raw_metrics.get("fsm"), ), ) + + @staticmethod + def get_dvplan_parser_name() -> str: + """Return the name of the DVPlan parser for this tool. + + Returns: + The DVPlan coverage parser string. + + """ + return "cadence_report" + + @staticmethod + def get_dvplan_report_path(cov_report_dir: str, cov_report_txt: str) -> str: + """Return the path to the coverage report required by DVPlan. + + Args: + cov_report_dir: Path to the coverage report directory. + cov_report_txt: Path to the text-based coverage report. + + Returns: + Path to the target coverage report file or directory. + + """ + return f"$(ls -t {cov_report_dir}/report_data/*.report | head -n 1)"