|
1 | | -#!/usr/bin/env python3 |
2 | 1 | ################################## |
3 | | -# covergroupgen.py |
| 2 | +# generate.py |
4 | 3 | # |
5 | 4 | # David_Harris@hmc.edu 15 August 2025 |
6 | 5 | # SPDX-License-Identifier: Apache-2.0 |
|
9 | 8 | ################################## |
10 | 9 |
|
11 | 10 | import csv |
| 11 | +import importlib.resources |
12 | 12 | import math |
13 | 13 | import re |
| 14 | +from difflib import get_close_matches |
14 | 15 | from pathlib import Path |
15 | 16 | from typing import TextIO |
16 | 17 |
|
| 18 | +from rich.progress import track |
| 19 | + |
17 | 20 | ################################## |
18 | 21 | # Functions |
19 | 22 | ################################## |
|
25 | 28 | # the value being a list of covergroups for that instruction |
26 | 29 |
|
27 | 30 |
|
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]]: |
29 | 36 | """ |
30 | 37 | Iterates over all of the CSV testplan files in the provided directory. It populates a dictionary of dictionaries with |
31 | 38 | the top level key being the architecture/extension (e.g. RV64I), the second level key being a tuple of |
32 | 39 | (instruction mnemonic, type) to allow duplicate instructions with different types, and the value being a list |
33 | 40 | of coverpoints for that instruction. |
34 | 41 | """ |
| 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 | + |
35 | 50 | testplans: dict[str, dict[tuple[str, str], list[str]]] = {} |
36 | 51 | arch_sources: dict[str, str] = {} |
37 | | - coverplan_dirs = [(testplans_dir, "unpriv")] |
| 52 | + coverplan_dirs = [(testplan_dir, "unpriv")] |
38 | 53 | for coverplan_dir, source in coverplan_dirs: |
39 | 54 | if not coverplan_dir.exists(): |
40 | 55 | continue # Skip missing directories |
41 | 56 | for file in coverplan_dir.rglob("*.csv"): |
42 | 57 | 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 |
43 | 63 | with file.open() as csvfile: |
44 | 64 | reader = csv.DictReader(csvfile) |
45 | 65 | tp: dict[tuple[str, str], list[str]] = {} |
@@ -83,19 +103,28 @@ def read_testplans(testplans_dir: Path) -> tuple[dict[str, dict[tuple[str, str], |
83 | 103 | return testplans, arch_sources |
84 | 104 |
|
85 | 105 |
|
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 |
93 | 115 |
|
94 | 116 |
|
95 | 117 | def customize_template(covergroup_templates: dict[str, str], name: str, arch: str, instr: str, effew: str = "") -> str: |
96 | 118 | """Customize the covergroup template with the given parameters and pick from RV32/RV64 as necessary.""" |
97 | 119 | 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) |
99 | 128 | template = covergroup_templates[name] |
100 | 129 | instr_nodot = instr.replace(".", "_") |
101 | 130 | template = ( |
@@ -305,14 +334,13 @@ def write_covergroups( |
305 | 334 |
|
306 | 335 | with (coverageHeaderDir / "RISCV_instruction_sample.svh").open("w") as fsample: |
307 | 336 | 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)): |
309 | 338 | covergroupSubDir = arch_sources.get(arch, "unpriv") |
310 | 339 | covergroup_out_dir = covergroup_dir / covergroupSubDir |
311 | 340 | covergroup_out_dir.mkdir(parents=True, exist_ok=True) |
312 | 341 |
|
313 | 342 | file = arch + "_coverage.svh" |
314 | 343 | initfile = arch + "_coverage_init.svh" |
315 | | - print("***** Writing " + file) |
316 | 344 |
|
317 | 345 | vector = arch.startswith(("Vx", "Zv", "Vls", "Vf")) |
318 | 346 | effew = get_effew(arch) if vector else "" |
@@ -386,15 +414,7 @@ def write_covergroups( |
386 | 414 | ################################## |
387 | 415 | # Main Python Script |
388 | 416 | ################################## |
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() |
394 | 420 | 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) |
|
0 commit comments