Skip to content

Commit ed57854

Browse files
committed
Make covergroupgen a first-class package with better error messages
1 parent 38c964a commit ed57854

File tree

8 files changed

+139
-28
lines changed

8 files changed

+139
-28
lines changed

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ clean: clean-tests
117117
.PHONY: covergroupgen
118118
covergroupgen: $(STAMP_DIR)/covergroupgen.stamp
119119
$(STAMP_DIR)/covergroupgen.stamp: $(COVERGROUPGEN_DEPS) $(TESTPLANS) Makefile | $(STAMP_DIR)
120-
$(UV_RUN) generators/coverage/covergroupgen.py
120+
$(UV_RUN) covergroupgen testplans $(if $(EXTENSIONS),--extensions $(EXTENSIONS)) $(if $(EXCLUDE_EXTENSIONS),--exclude $(EXCLUDE_EXTENSIONS))
121121
@touch $@
122122

123123
.PHONY: testgen
@@ -149,7 +149,8 @@ tests: covergroupgen testgen privheaders
149149
.PHONY: clean-tests
150150
clean-tests:
151151
rm -rf $(SRCDIR64) $(SRCDIR32) $(SRCDIR64E) $(SRCDIR32E) $(PRIVHEADERSDIR)
152-
rm -rf fcov/unpriv/*
152+
rm -rf coverpoints/unpriv/*
153+
rm -rf coverpoints/coverage/*
153154
rm -rf $(STAMP_DIR)
154155

155156
$(PRIVHEADERSDIR) $(STAMP_DIR):

generators/coverage/pyproject.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# pyproject.toml
2+
# covergroupgen python project configuration file for riscv-arch-test
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
[project]
6+
name = "covergroupgen"
7+
version = "0.1.0"
8+
description = "RISC-V Architectural Certification Covergroup Generator"
9+
authors = [{ name = "Jordan Carlin", email = "jcarlin@hmc.edu" }]
10+
requires-python = ">=3.12"
11+
dependencies = ["typer>=0.20.0", "rich>=13.0.0"]
12+
13+
[project.scripts]
14+
covergroupgen = "covergroupgen.cli:main"
15+
16+
##### BUILD #####
17+
[build-system]
18+
requires = ["uv_build>=0.9.4,<0.10.0"]
19+
build-backend = "uv_build"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Deliberately empty file. Necessary for Python package recognition.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env python3
2+
3+
##################################
4+
# cli.py
5+
#
6+
# Command-line interface for covergroup generation.
7+
# jcarlin@hmc.edu March 2026
8+
# SPDX-License-Identifier: Apache-2.0
9+
##################################
10+
11+
"""Top-level command-line interface for covergroup generation."""
12+
13+
from pathlib import Path
14+
from typing import Annotated
15+
16+
import typer
17+
18+
from covergroupgen.generate import generate_covergroups
19+
20+
# CLI interface setup
21+
covergroupgen_app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]}, add_completion=False)
22+
23+
24+
@covergroupgen_app.command()
25+
def run(
26+
testplan_dir: Annotated[
27+
Path, typer.Argument(exists=True, file_okay=False, help="Directory containing testplan CSV files")
28+
],
29+
*,
30+
output_dir: Annotated[
31+
Path, typer.Option("-o", "--output", file_okay=False, help="Output directory for covergroups")
32+
] = Path("coverpoints"),
33+
extensions: Annotated[
34+
str, typer.Option("--extensions", "-e", help="Comma-separated list of extensions to generate covergroups for")
35+
] = "all",
36+
exclude: Annotated[
37+
str,
38+
typer.Option(
39+
"--exclude", "-x", help="Comma-separated list of extensions to exclude from covergroup generation"
40+
),
41+
] = "",
42+
) -> None:
43+
"""Generate functional covergroups for RISC-V instructions from CSV testplans."""
44+
generate_covergroups(testplan_dir, output_dir, extensions, exclude)
45+
46+
47+
def main() -> None:
48+
covergroupgen_app()
49+
50+
51+
if __name__ == "__main__":
52+
main()

generators/coverage/covergroupgen.py renamed to generators/coverage/src/covergroupgen/generate.py

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
#!/usr/bin/env python3
21
##################################
3-
# covergroupgen.py
2+
# generate.py
43
#
54
# David_Harris@hmc.edu 15 August 2025
65
# SPDX-License-Identifier: Apache-2.0
@@ -9,11 +8,15 @@
98
##################################
109

1110
import csv
11+
import importlib.resources
1212
import math
1313
import re
14+
from difflib import get_close_matches
1415
from pathlib import Path
1516
from typing import TextIO
1617

18+
from rich.progress import track
19+
1720
##################################
1821
# Functions
1922
##################################
@@ -25,21 +28,38 @@
2528
# the value being a list of covergroups for that instruction
2629

2730

28-
def read_testplans(testplans_dir: Path) -> tuple[dict[str, dict[tuple[str, str], list[str]]], dict[str, str]]:
31+
def read_testplans(
32+
testplan_dir: Path,
33+
extensions: str = "all",
34+
exclude: str = "",
35+
) -> tuple[dict[str, dict[tuple[str, str], list[str]]], dict[str, str]]:
2936
"""
3037
Iterates over all of the CSV testplan files in the provided directory. It populates a dictionary of dictionaries with
3138
the top level key being the architecture/extension (e.g. RV64I), the second level key being a tuple of
3239
(instruction mnemonic, type) to allow duplicate instructions with different types, and the value being a list
3340
of coverpoints for that instruction.
3441
"""
42+
# Parse extension filter lists
43+
include_set: set[str] | None = None
44+
if extensions != "all":
45+
include_set = {ext.strip() for ext in extensions.split(",") if ext.strip()}
46+
exclude_set: set[str] = set()
47+
if exclude:
48+
exclude_set = {ext.strip() for ext in exclude.split(",") if ext.strip()}
49+
3550
testplans: dict[str, dict[tuple[str, str], list[str]]] = {}
3651
arch_sources: dict[str, str] = {}
37-
coverplan_dirs = [(testplans_dir, "unpriv")]
52+
coverplan_dirs = [(testplan_dir, "unpriv")]
3853
for coverplan_dir, source in coverplan_dirs:
3954
if not coverplan_dir.exists():
4055
continue # Skip missing directories
4156
for file in coverplan_dir.rglob("*.csv"):
4257
arch = file.stem
58+
# Filter by extension name
59+
if include_set is not None and arch not in include_set:
60+
continue
61+
if arch in exclude_set:
62+
continue
4363
with file.open() as csvfile:
4464
reader = csv.DictReader(csvfile)
4565
tp: dict[tuple[str, str], list[str]] = {}
@@ -83,19 +103,28 @@ def read_testplans(testplans_dir: Path) -> tuple[dict[str, dict[tuple[str, str],
83103
return testplans, arch_sources
84104

85105

86-
def read_covergroup_templates(template_dir: Path) -> dict[str, str]:
87-
"""Read the covergroup templates from the templates directory."""
88-
covergroupTemplates: dict[str, str] = {}
89-
for file in template_dir.rglob("*.sv"):
90-
cg = file.stem
91-
covergroupTemplates[cg] = file.read_text()
92-
return covergroupTemplates
106+
def read_covergroup_templates(package: str = "covergroupgen.templates") -> dict[str, str]:
107+
"""Recursively read all .sv covergroup templates from the given package and its sub-packages."""
108+
templates: dict[str, str] = {}
109+
for item in importlib.resources.files(package).iterdir():
110+
if item.is_file() and item.name.endswith(".sv"):
111+
templates[item.name.removesuffix(".sv")] = item.read_text()
112+
elif item.is_dir() and not item.name.startswith("__"):
113+
templates.update(read_covergroup_templates(f"{package}.{item.name}"))
114+
return templates
93115

94116

95117
def customize_template(covergroup_templates: dict[str, str], name: str, arch: str, instr: str, effew: str = "") -> str:
96118
"""Customize the covergroup template with the given parameters and pick from RV32/RV64 as necessary."""
97119
if name not in covergroup_templates:
98-
raise ValueError(f"No template found for '{name}'. Check if there are spaces before or after coverpoint name.")
120+
available = list(covergroup_templates.keys())
121+
similar = get_close_matches(name, available, n=5, cutoff=0.4)
122+
msg = f"No template found for '{name}'. "
123+
if similar:
124+
msg += f"Similar templates: {', '.join(similar)}. "
125+
templates_dir = importlib.resources.files("covergroupgen.templates")
126+
msg += f"To add support, create a new .sv template in '{templates_dir}'."
127+
raise ValueError(msg)
99128
template = covergroup_templates[name]
100129
instr_nodot = instr.replace(".", "_")
101130
template = (
@@ -305,14 +334,13 @@ def write_covergroups(
305334

306335
with (coverageHeaderDir / "RISCV_instruction_sample.svh").open("w") as fsample:
307336
fsample.write(customize_template(covergroup_templates, "instruction_sample_header", "NA", "NA"))
308-
for arch, tp in test_plans.items():
337+
for arch, tp in track(test_plans.items(), description="[cyan]Generating covergroups...", total=len(test_plans)):
309338
covergroupSubDir = arch_sources.get(arch, "unpriv")
310339
covergroup_out_dir = covergroup_dir / covergroupSubDir
311340
covergroup_out_dir.mkdir(parents=True, exist_ok=True)
312341

313342
file = arch + "_coverage.svh"
314343
initfile = arch + "_coverage_init.svh"
315-
print("***** Writing " + file)
316344

317345
vector = arch.startswith(("Vx", "Zv", "Vls", "Vf"))
318346
effew = get_effew(arch) if vector else ""
@@ -386,15 +414,7 @@ def write_covergroups(
386414
##################################
387415
# Main Python Script
388416
##################################
389-
390-
391-
def main(testplan_dir: Path, output_dir: Path) -> None:
392-
test_plans, arch_sources = read_testplans(testplan_dir)
393-
covergroup_templates = read_covergroup_templates(Path(__file__).parent / "templates")
417+
def generate_covergroups(testplan_dir: Path, output_dir: Path, extensions: str = "all", exclude: str = "") -> None:
418+
test_plans, arch_sources = read_testplans(testplan_dir, extensions, exclude)
419+
covergroup_templates = read_covergroup_templates()
394420
write_covergroups(test_plans, covergroup_templates, arch_sources, output_dir)
395-
396-
397-
if __name__ == "__main__":
398-
test_plan_dir = (Path(__file__).parent / "../../testplans").resolve()
399-
output_dir = (Path(__file__).parent / "../../coverpoints").resolve()
400-
main(test_plan_dir, output_dir)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
# SPDX-License-Identifier: Apache-2.0
12
# Deliberately empty file. Necessary for Python package recognition.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# SPDX-License-Identifier: Apache-2.0
55

66
[tool.uv.workspace]
7-
members = ["framework", "generators/testgen"]
7+
members = ["framework", "generators/testgen", "generators/coverage"]
88

99
[dependency-groups]
1010
dev = [

uv.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)