Skip to content

Commit 693bd56

Browse files
committed
feat: summary report more dashboard like
Introduce a CoverageMetrics model instead of the dict. Create a new method on the tool plugins to convert from eda tool specific coverage fields to generic names that match the opentitan-site dashboard fields. Update the report summary and block report templates to use the new model. Add the extra fields in the summary report to make it more like the dashboard (contain the same information). Signed-off-by: James McCorrie <[email protected]>
1 parent bdb4ef7 commit 693bd56

File tree

7 files changed

+227
-32
lines changed

7 files changed

+227
-32
lines changed

src/dvsim/flow/sim.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from dvsim.sim_results import SimResults
3030
from dvsim.test import Test
3131
from dvsim.testplan import Testplan
32+
from dvsim.tool.utils import get_sim_tool_plugin
3233
from dvsim.utils import TS_FORMAT, rm_path
3334

3435
# This affects the bucketizer failure report.
@@ -684,20 +685,25 @@ def make_test_result(tr) -> TestResult | None:
684685

685686
# --- Coverage ---
686687
coverage: dict[str, float | None] = {}
688+
coverage_model = None
687689
if self.cov_report_deploy:
688690
for k, v in self.cov_report_deploy.cov_results_dict.items():
689691
try:
690692
coverage[k.lower()] = float(v.rstrip("% "))
691693
except (ValueError, TypeError, AttributeError):
692694
coverage[k.lower()] = None
693695

696+
coverage_model = get_sim_tool_plugin(self.tool).get_coverage_metrics(
697+
raw_metrics=coverage,
698+
)
699+
694700
# --- Final result ---
695701
return FlowResults(
696702
block=block,
697703
tool=tool,
698704
timestamp=timestamp,
699705
stages=stages,
700-
coverage=coverage,
706+
coverage=coverage_model,
701707
passed=total_passed,
702708
total=total_runs,
703709
percent=100.0 * total_passed / total_runs if total_runs else 0.0,

src/dvsim/report/data.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,75 @@ class TestStage(BaseModel):
9595
"""Percentage test pass rate."""
9696

9797

98+
class CodeCoverageMetrics(BaseModel):
99+
"""CodeCoverage metrics."""
100+
101+
model_config = ConfigDict(frozen=True, extra="forbid")
102+
103+
block: float | None
104+
"""Block Coverage (%) - did this part of the code execute?"""
105+
line_statement: float | None
106+
"""Line/Statement Coverage (%) - did this part of the code execute?"""
107+
branch: float | None
108+
"""Branch Coverage (%) - did this if/case take all paths?"""
109+
condition_expression: float | None
110+
"""Condition/Expression Coverage (%) - did the logic evaluate to 0 & 1?"""
111+
toggle: float | None
112+
"""Toggle Coverage (%) - did the signal wiggle?"""
113+
fsm: float | None
114+
"""FSM Coverage (%) - did the state machine transition?"""
115+
116+
@property
117+
def average(self) -> float | None:
118+
"""Average code coverage (%)."""
119+
all_cov = [
120+
c
121+
for c in [
122+
self.line_statement,
123+
self.branch,
124+
self.condition_expression,
125+
self.toggle,
126+
self.fsm,
127+
]
128+
if c is not None
129+
]
130+
131+
if len(all_cov) == 0:
132+
return None
133+
134+
return sum(all_cov) / len(all_cov)
135+
136+
137+
class CoverageMetrics(BaseModel):
138+
"""Coverage metrics."""
139+
140+
code: CodeCoverageMetrics | None
141+
"""Code Coverage."""
142+
assertion: float | None
143+
"""Assertion Coverage."""
144+
functional: float | None
145+
"""Functional coverage."""
146+
147+
@property
148+
def average(self) -> float | None:
149+
"""Average code coverage (%) or None if there is no coverage."""
150+
code = self.code.average if self.code is not None else None
151+
all_cov = [
152+
c
153+
for c in [
154+
code,
155+
self.assertion,
156+
self.functional,
157+
]
158+
if c is not None
159+
]
160+
161+
if len(all_cov) == 0:
162+
return None
163+
164+
return sum(all_cov) / len(all_cov)
165+
166+
98167
class FlowResults(BaseModel):
99168
"""Flow results data."""
100169

@@ -109,7 +178,7 @@ class FlowResults(BaseModel):
109178

110179
stages: Mapping[str, TestStage]
111180
"""Results per test stage."""
112-
coverage: Mapping[str, float | None]
181+
coverage: CoverageMetrics | None
113182
"""Coverage metrics."""
114183

115184
passed: int

src/dvsim/templates/reports/block_report.html

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,21 +91,39 @@ <h2>Simulation Results: {{ block.name }}</h2>
9191
</span>
9292
</div>
9393

94+
{% macro coverage_stat(cov, kind, label) %}
95+
{% if cov and cov|attr(kind) is not none %}
96+
{% set value = cov|attr(kind) %}
97+
<div class="col">
98+
<ul class="list-group list-group-horizontal">
99+
<li class="list-group-item list-group-item-secondary px-2 py-1">
100+
{{ label }}
101+
</li>
102+
<li class="list-group-item py-1">{{ "%.2f"|format(value) }} %</li>
103+
</ul>
104+
</div>
105+
{% endif %}
106+
{% endmacro %}
107+
108+
94109
{% if coverage %}
95110
<div class="col-4 col-md-6">
96111
<div class="text-center mb-2">Coverage statistics</div>
97112
<div class="container small">
98113
<div class="row g-0 row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4">
99-
{% for name, val in coverage.items() %}
100-
<div class="col">
101-
<ul class="list-group list-group-horizontal">
102-
<li class="list-group-item list-group-item-secondary px-2 py-1">
103-
{{ name }}
104-
</li>
105-
<li class="list-group-item py-1">{{ val }}</li>
106-
</ul>
107-
</div>
108-
{% endfor %}
114+
{{ coverage_stat(coverage, "average", "Total") }}
115+
116+
{% set code = coverage.code %}
117+
{{ coverage_stat(code, "average", "code") }}
118+
{{ coverage_stat(coverage, "assertion", "assert") }}
119+
{{ coverage_stat(coverage, "functional", "func") }}
120+
121+
{{ coverage_stat(code, "block", "block") }}
122+
{{ coverage_stat(code, "line_statement", "line") }}
123+
{{ coverage_stat(code, "branch", "branch") }}
124+
{{ coverage_stat(code, "condition_expression", "cond") }}
125+
{{ coverage_stat(code, "toggle", "toggle") }}
126+
{{ coverage_stat(code, "fsm", "FSM") }}
109127
</div>
110128
</div>
111129
</div>

src/dvsim/templates/reports/summary_report.html

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,44 @@ <h2>Simulation Results: {{ top.name }}</h2>
8888
</div>
8989
</div>
9090

91+
{% macro coverage_cell(cov, kind) %}
92+
{% if cov and cov|attr(kind) is not none %}
93+
{% set value = cov|attr(kind) %}
94+
<td class="c{{ (value / 10)|int }}">{{ "%.2f"|format(value) }}</td>
95+
{% else %}
96+
<td class="text-muted">-</td>
97+
{% endif %}
98+
{% endmacro %}
99+
91100
<div class="row py-3">
92101
<div class="col">
93-
<table class="table table-sm table-hover">
94-
<thead>
102+
<table class="table table-sm table-hover text-center">
103+
<thead class="table-light align-middle">
95104
<tr>
96-
<th>Block</th>
105+
<th rowspan="2">Block</th>
106+
<th colspan="3">Tests</th>
107+
<th colspan="4" class="bg-secondary text-white">Coverage Summary</th>
108+
<th colspan="6">Code Coverage</th>
109+
</tr>
110+
<tr>
111+
<!-- Tests sub-headers -->
97112
<th>Pass</th>
98113
<th>Total</th>
99114
<th>%</th>
100-
<th>Cov</th>
115+
116+
<!-- Coverage Summary sub-headers -->
117+
<th class="bg-secondary text-white">Overall</th>
118+
<th class="bg-secondary text-white">Code</th>
119+
<th class="bg-secondary text-white">Functional</th>
120+
<th class="bg-secondary text-white">Assertion</th>
121+
122+
<!-- Code Coverage sub-headers -->
123+
<th>Block</th>
124+
<th>Line</th>
125+
<th>Branch</th>
126+
<th>Condition</th>
127+
<th>Toggle</th>
128+
<th>FSM</th>
101129
</tr>
102130
</thead>
103131
<tbody>
@@ -114,19 +142,20 @@ <h2>Simulation Results: {{ top.name }}</h2>
114142
<td class="c{{ (flow.percent / 10)|int }}">
115143
{{ "%.2f" | format(flow.percent) }}
116144
</td>
117-
<td class="
118-
{% if flow.coverage and flow.coverage.get('score') is not none %}
119-
c{{ (flow.coverage.score / 10)|int }}
120-
{% else %}
121-
text-muted
122-
{% endif %}
123-
">
124-
{% if flow.coverage and flow.coverage.get('score') is not none %}
125-
{{ "%.2f" | format(flow.coverage.score) }}
126-
{% else %}
127-
-
128-
{% endif %}
129-
</td>
145+
{% set cov = flow.coverage %}
146+
{{ coverage_cell(cov, "average") }}
147+
148+
{% set code = cov|attr("code") %}
149+
{{ coverage_cell(code, "average") }}
150+
{{ coverage_cell(cov, "functional") }}
151+
{{ coverage_cell(cov, "assertion") }}
152+
153+
{{ coverage_cell(code, "block") }}
154+
{{ coverage_cell(code, "line_statement") }}
155+
{{ coverage_cell(code, "branch") }}
156+
{{ coverage_cell(code, "condition_expression") }}
157+
{{ coverage_cell(code, "toggle") }}
158+
{{ coverage_cell(code, "fsm") }}
130159
</tr>
131160
{% endfor %}
132161
</tbody>

src/dvsim/tool/sim.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
"""EDA simulation tool interface."""
66

7-
from collections.abc import Sequence
7+
from collections.abc import Mapping, Sequence
88
from pathlib import Path
99
from typing import Protocol, runtime_checkable
1010

11+
from dvsim.report.data import CoverageMetrics
12+
1113
__all__ = ("SimTool",)
1214

1315

@@ -67,3 +69,16 @@ def get_simulated_time(log_text: Sequence[str]) -> tuple[float, str]:
6769
6870
"""
6971
...
72+
73+
@staticmethod
74+
def get_coverage_metrics(raw_metrics: Mapping[str, float | None] | None) -> CoverageMetrics:
75+
"""Get a CoverageMetrics model from raw coverage data.
76+
77+
Args:
78+
raw_metrics: raw coverage metrics as parsed from the tool.
79+
80+
Returns:
81+
CoverageMetrics model.
82+
83+
"""
84+
...

src/dvsim/tool/vcs.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
"""EDA tool plugin providing VCS support to DVSim."""
66

77
import re
8-
from collections.abc import Sequence
8+
from collections.abc import Mapping, Sequence
99
from pathlib import Path
1010

11+
from dvsim.report.data import CodeCoverageMetrics, CoverageMetrics
12+
1113
__all__ = ("VCS",)
1214

1315

@@ -103,3 +105,30 @@ def get_simulated_time(log_text: Sequence[str]) -> tuple[float, str]:
103105

104106
msg = "Simulated time not found in the log."
105107
raise RuntimeError(msg)
108+
109+
@staticmethod
110+
def get_coverage_metrics(raw_metrics: Mapping[str, float | None] | None) -> CoverageMetrics:
111+
"""Get a CoverageMetrics model from raw coverage data.
112+
113+
Args:
114+
raw_metrics: raw coverage metrics as parsed from the tool.
115+
116+
Returns:
117+
CoverageMetrics model.
118+
119+
"""
120+
if raw_metrics is None:
121+
return CoverageMetrics(code=None, assertion=None, functional=None)
122+
123+
return CoverageMetrics(
124+
functional=raw_metrics.get("group"),
125+
assertion=raw_metrics.get("assert"),
126+
code=CodeCoverageMetrics(
127+
block=None,
128+
line_statement=raw_metrics.get("line"),
129+
branch=raw_metrics.get("branch"),
130+
condition_expression=raw_metrics.get("cond"),
131+
toggle=raw_metrics.get("toggle"),
132+
fsm=raw_metrics.get("fsm"),
133+
),
134+
)

src/dvsim/tool/xcelium.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66

77
import re
88
from collections import OrderedDict
9-
from collections.abc import Sequence
9+
from collections.abc import Mapping, Sequence
1010
from pathlib import Path
1111

12+
from dvsim.report.data import CodeCoverageMetrics, CoverageMetrics
13+
1214
__all__ = ("Xcelium",)
1315

1416

@@ -128,3 +130,30 @@ def get_simulated_time(log_text: Sequence[str]) -> tuple[float, str]:
128130

129131
msg = "Simulated time not found in the log."
130132
raise RuntimeError(msg)
133+
134+
@staticmethod
135+
def get_coverage_metrics(raw_metrics: Mapping[str, float | None] | None) -> CoverageMetrics:
136+
"""Get a CoverageMetrics model from raw coverage data.
137+
138+
Args:
139+
raw_metrics: raw coverage metrics as parsed from the tool.
140+
141+
Returns:
142+
CoverageMetrics model.
143+
144+
"""
145+
if raw_metrics is None:
146+
return CoverageMetrics(code=None, assertion=None, functional=None)
147+
148+
return CoverageMetrics(
149+
functional=raw_metrics.get("covergroup"),
150+
assertion=raw_metrics.get("assertion"),
151+
code=CodeCoverageMetrics(
152+
block=raw_metrics.get("block"),
153+
line_statement=raw_metrics.get("statement"),
154+
branch=raw_metrics.get("branch"),
155+
condition_expression=raw_metrics.get("cond"),
156+
toggle=raw_metrics.get("toggle"),
157+
fsm=raw_metrics.get("fsm"),
158+
),
159+
)

0 commit comments

Comments
 (0)