svtb 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- svtb-0.1.0/LICENSE +21 -0
- svtb-0.1.0/PKG-INFO +70 -0
- svtb-0.1.0/README.md +52 -0
- svtb-0.1.0/pyproject.toml +32 -0
- svtb-0.1.0/setup.cfg +4 -0
- svtb-0.1.0/svtb/__init__.py +8 -0
- svtb-0.1.0/svtb/fault/LICENSE.txt +26 -0
- svtb-0.1.0/svtb/fault/__init__.py +12 -0
- svtb-0.1.0/svtb/fault/subprocess_run.py +65 -0
- svtb-0.1.0/svtb/fault/tester.py +432 -0
- svtb-0.1.0/svtb/magma/LICENSE.txt +21 -0
- svtb-0.1.0/svtb/magma/__init__.py +80 -0
- svtb-0.1.0/svtb.egg-info/PKG-INFO +70 -0
- svtb-0.1.0/svtb.egg-info/SOURCES.txt +15 -0
- svtb-0.1.0/svtb.egg-info/dependency_links.txt +1 -0
- svtb-0.1.0/svtb.egg-info/top_level.txt +1 -0
- svtb-0.1.0/tests/test_tester.py +191 -0
svtb-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Justin Correll
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
svtb-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: svtb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Self-checking SystemVerilog testbench generator: the subset of fault's Tester and magma's port-declaration API that svreal's and msdsl's tests use, for Python versions neither package installs on
|
|
5
|
+
Author: Justin Correll
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/correlljm/svtb
|
|
8
|
+
Project-URL: Repository, https://github.com/correlljm/svtb
|
|
9
|
+
Project-URL: Issues, https://github.com/correlljm/svtb/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/correlljm/svtb/blob/main/CHANGELOG.md
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Requires-Python: >=3.14
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
License-File: svtb/fault/LICENSE.txt
|
|
16
|
+
License-File: svtb/magma/LICENSE.txt
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# svtb
|
|
20
|
+
|
|
21
|
+
A self-checking SystemVerilog testbench generator: the subset of
|
|
22
|
+
[fault](https://github.com/leonardt/fault)'s `Tester` and [magma](https://github.com/phanrahan/magma)'s
|
|
23
|
+
port-declaration API that svreal's and msdsl's historical test suites use. Neither fault nor magma installs
|
|
24
|
+
on Python 3.14; this is a from-scratch implementation written to the same calling convention, so existing
|
|
25
|
+
test files do not change.
|
|
26
|
+
|
|
27
|
+
`svtb.fault.Tester` records `poke`, `eval`, `step`, `expect` and `get_value` calls and generates a
|
|
28
|
+
self-checking SystemVerilog testbench, run on iverilog, Verilator, Xcelium, Incisive, Vivado or VCS. The
|
|
29
|
+
commands for Xcelium, Incisive, Vivado and VCS follow fault 3.0.43's `SystemVerilogTarget`.
|
|
30
|
+
|
|
31
|
+
`svtb.magma` declares a DUT's name and port list only; no circuit construction or compilation.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```shell
|
|
36
|
+
pip install svtb
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
or, in a consuming package's `pyproject.toml`, as a dev dependency:
|
|
40
|
+
|
|
41
|
+
```toml
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = ["svtb", "pytest"]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then point `tests/conftest.py`'s import path at the installed package instead of a local directory:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import svtb
|
|
50
|
+
sys.path.insert(0, str(Path(svtb.__file__).parent))
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This makes `import fault` and `import magma as m` resolve to `svtb`'s subpackages as top-level modules,
|
|
54
|
+
exactly as they would from a local `tests/support/` directory.
|
|
55
|
+
|
|
56
|
+
## Licensing
|
|
57
|
+
|
|
58
|
+
- `fault` is BSD-3-Clause, Copyright 2018 Leonard Truong; see `svtb/fault/LICENSE.txt`.
|
|
59
|
+
- `magma` is MIT, Copyright (C) 2016 Stanford University; see `svtb/magma/LICENSE.txt`.
|
|
60
|
+
- svtb itself is MIT; see `LICENSE`.
|
|
61
|
+
|
|
62
|
+
## Running the tests
|
|
63
|
+
|
|
64
|
+
```shell
|
|
65
|
+
uv sync
|
|
66
|
+
uv run pytest -q tests
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`tests/test_tester.py` checks the generator on each simulator; `tests/fake_sim.py` stands in for Xcelium,
|
|
70
|
+
Incisive, Vivado and VCS, since only iverilog and Verilator have run here.
|
svtb-0.1.0/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# svtb
|
|
2
|
+
|
|
3
|
+
A self-checking SystemVerilog testbench generator: the subset of
|
|
4
|
+
[fault](https://github.com/leonardt/fault)'s `Tester` and [magma](https://github.com/phanrahan/magma)'s
|
|
5
|
+
port-declaration API that svreal's and msdsl's historical test suites use. Neither fault nor magma installs
|
|
6
|
+
on Python 3.14; this is a from-scratch implementation written to the same calling convention, so existing
|
|
7
|
+
test files do not change.
|
|
8
|
+
|
|
9
|
+
`svtb.fault.Tester` records `poke`, `eval`, `step`, `expect` and `get_value` calls and generates a
|
|
10
|
+
self-checking SystemVerilog testbench, run on iverilog, Verilator, Xcelium, Incisive, Vivado or VCS. The
|
|
11
|
+
commands for Xcelium, Incisive, Vivado and VCS follow fault 3.0.43's `SystemVerilogTarget`.
|
|
12
|
+
|
|
13
|
+
`svtb.magma` declares a DUT's name and port list only; no circuit construction or compilation.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```shell
|
|
18
|
+
pip install svtb
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
or, in a consuming package's `pyproject.toml`, as a dev dependency:
|
|
22
|
+
|
|
23
|
+
```toml
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = ["svtb", "pytest"]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then point `tests/conftest.py`'s import path at the installed package instead of a local directory:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import svtb
|
|
32
|
+
sys.path.insert(0, str(Path(svtb.__file__).parent))
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This makes `import fault` and `import magma as m` resolve to `svtb`'s subpackages as top-level modules,
|
|
36
|
+
exactly as they would from a local `tests/support/` directory.
|
|
37
|
+
|
|
38
|
+
## Licensing
|
|
39
|
+
|
|
40
|
+
- `fault` is BSD-3-Clause, Copyright 2018 Leonard Truong; see `svtb/fault/LICENSE.txt`.
|
|
41
|
+
- `magma` is MIT, Copyright (C) 2016 Stanford University; see `svtb/magma/LICENSE.txt`.
|
|
42
|
+
- svtb itself is MIT; see `LICENSE`.
|
|
43
|
+
|
|
44
|
+
## Running the tests
|
|
45
|
+
|
|
46
|
+
```shell
|
|
47
|
+
uv sync
|
|
48
|
+
uv run pytest -q tests
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`tests/test_tester.py` checks the generator on each simulator; `tests/fake_sim.py` stands in for Xcelium,
|
|
52
|
+
Incisive, Vivado and VCS, since only iverilog and Verilator have run here.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "svtb"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Self-checking SystemVerilog testbench generator: the subset of fault's Tester and magma's port-declaration API that svreal's and msdsl's tests use, for Python versions neither package installs on"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE", "svtb/fault/LICENSE.txt", "svtb/magma/LICENSE.txt"]
|
|
12
|
+
authors = [{ name = "Justin Correll" }]
|
|
13
|
+
requires-python = ">=3.14"
|
|
14
|
+
dependencies = []
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/correlljm/svtb"
|
|
21
|
+
Repository = "https://github.com/correlljm/svtb"
|
|
22
|
+
Issues = "https://github.com/correlljm/svtb/issues"
|
|
23
|
+
Changelog = "https://github.com/correlljm/svtb/blob/main/CHANGELOG.md"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
include = ["svtb*"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.package-data]
|
|
29
|
+
svtb = ["fault/LICENSE.txt", "magma/LICENSE.txt"]
|
|
30
|
+
|
|
31
|
+
[dependency-groups]
|
|
32
|
+
dev = ["pytest", "numpy"]
|
svtb-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""svtb: a self-checking SystemVerilog testbench generator.
|
|
2
|
+
|
|
3
|
+
Implements the subset of fault's (https://github.com/leonardt/fault) Tester and magma's
|
|
4
|
+
(https://github.com/phanrahan/magma) port-declaration API that svreal's and msdsl's historical test suites
|
|
5
|
+
use. Neither fault nor magma installs on Python 3.14; this is a from-scratch implementation, not a
|
|
6
|
+
compatibility patch, written to the same calling convention so existing test files do not change. See
|
|
7
|
+
README.md.
|
|
8
|
+
"""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Copyright 2018 Leonard Truong
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without
|
|
4
|
+
modification, are permitted provided that the following conditions are met:
|
|
5
|
+
|
|
6
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
7
|
+
list of conditions and the following disclaimer.
|
|
8
|
+
|
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
|
11
|
+
and/or other materials provided with the distribution.
|
|
12
|
+
|
|
13
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
14
|
+
may be used to endorse or promote products derived from this software without
|
|
15
|
+
specific prior written permission.
|
|
16
|
+
|
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
18
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
19
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
20
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
21
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
23
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
24
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
25
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
26
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Subset of the fault testbench API (https://github.com/leonardt/fault, BSD-3-Clause, see LICENSE.txt),
|
|
2
|
+
as svreal's and msdsl's tests use it."""
|
|
3
|
+
|
|
4
|
+
from magma import PortType
|
|
5
|
+
|
|
6
|
+
from . import subprocess_run
|
|
7
|
+
from .tester import GetValue, Tester
|
|
8
|
+
|
|
9
|
+
RealIn = PortType("real", direction="in")
|
|
10
|
+
RealOut = PortType("real", direction="out")
|
|
11
|
+
|
|
12
|
+
__all__ = ["GetValue", "RealIn", "RealOut", "Tester", "subprocess_run"]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""subprocess.run wrapper with fault's semantics: optional live display, error-string scan,
|
|
2
|
+
return-code check. Raises AssertionError on failure."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _error_detected(text: str, err_str) -> bool:
|
|
12
|
+
if isinstance(err_str, str):
|
|
13
|
+
return err_str in text
|
|
14
|
+
if isinstance(err_str, list):
|
|
15
|
+
return any(elem in text for elem in err_str)
|
|
16
|
+
if isinstance(err_str, re.Pattern):
|
|
17
|
+
return err_str.search(text) is not None
|
|
18
|
+
raise TypeError(f"Invalid err_str: {err_str!r}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def subprocess_run(
|
|
22
|
+
args,
|
|
23
|
+
cwd=None,
|
|
24
|
+
env=None,
|
|
25
|
+
disp_type: str = "on_error",
|
|
26
|
+
err_str=None,
|
|
27
|
+
chk_ret_code: bool = True,
|
|
28
|
+
shell: bool = False,
|
|
29
|
+
use_fault_cfg: bool = True,
|
|
30
|
+
) -> subprocess.CompletedProcess:
|
|
31
|
+
if disp_type not in ("on_error", "realtime"):
|
|
32
|
+
raise ValueError(f"Invalid disp_type: {disp_type!r}")
|
|
33
|
+
args = [str(arg) for arg in args]
|
|
34
|
+
cmd_str = " ".join(shlex.quote(arg) for arg in args)
|
|
35
|
+
if disp_type == "realtime":
|
|
36
|
+
print(f"Running command: {cmd_str}", flush=True)
|
|
37
|
+
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
cmd_str if shell else args,
|
|
40
|
+
cwd=cwd,
|
|
41
|
+
env=env,
|
|
42
|
+
shell=shell,
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
)
|
|
46
|
+
if disp_type == "realtime":
|
|
47
|
+
print(result.stdout, end="", flush=True)
|
|
48
|
+
print(result.stderr, end="", flush=True)
|
|
49
|
+
|
|
50
|
+
errors = []
|
|
51
|
+
if chk_ret_code and result.returncode:
|
|
52
|
+
errors.append(f"Got return code {result.returncode}.")
|
|
53
|
+
if err_str is not None:
|
|
54
|
+
if _error_detected(result.stdout, err_str):
|
|
55
|
+
errors.append(f'Found error pattern "{err_str}" in STDOUT.')
|
|
56
|
+
if _error_detected(result.stderr, err_str):
|
|
57
|
+
errors.append(f'Found error pattern "{err_str}" in STDERR.')
|
|
58
|
+
|
|
59
|
+
if errors:
|
|
60
|
+
if disp_type == "on_error":
|
|
61
|
+
print(f"Running command: {cmd_str}")
|
|
62
|
+
print(result.stdout, end="")
|
|
63
|
+
print(result.stderr, end="")
|
|
64
|
+
raise AssertionError(" ".join(errors))
|
|
65
|
+
return result
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""fault.Tester subset: record poke/expect/step/... actions, emit a SystemVerilog testbench,
|
|
2
|
+
run it under iverilog, Verilator, Xcelium (xrun), Incisive (irun), Vivado (xsim) or VCS.
|
|
3
|
+
|
|
4
|
+
The commands for Xcelium, Incisive, Vivado and VCS follow fault 3.0.43's SystemVerilogTarget
|
|
5
|
+
(BSD-3-Clause, see LICENSE.txt), with two changes: the Cadence Tcl runs to $finish rather than for
|
|
6
|
+
num_cycles, and every one of them also fails on the testbench's own expect message, which fault
|
|
7
|
+
did not check for Cadence.
|
|
8
|
+
|
|
9
|
+
Timing mirrors fault's SystemVerilogTarget: poke is a nonblocking assign, eval is #1, step
|
|
10
|
+
toggles the clock after #clock_step_delay, delay(t) is #(t*1s), and a failed expect raises
|
|
11
|
+
$error, which is detected by scanning simulator output.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import tempfile
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from magma import Port
|
|
21
|
+
|
|
22
|
+
from .subprocess_run import subprocess_run
|
|
23
|
+
|
|
24
|
+
# Printed as the testbench's last action so a simulation that stops early cannot pass.
|
|
25
|
+
_DONE_TOKEN = "__FAULTSHIM_DONE__"
|
|
26
|
+
_GET_VALUE_FILE = "get_value_file.txt"
|
|
27
|
+
_SIMULATORS = ("iverilog", "verilator", "xcelium", "ncsim", "vivado", "vcs")
|
|
28
|
+
# Start of the $error message a failed expect prints; see _render_expect.
|
|
29
|
+
_EXPECT_FAILED = "Failed on action="
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _plus_defines(defines: dict | None) -> list[str]:
|
|
33
|
+
"""`+define+NAME` or `+define+NAME=value`, as fault's def_args writes them for Cadence and VCS."""
|
|
34
|
+
return [f"+define+{k}" if v is None else f"+define+{k}={v}" for k, v in (defines or {}).items()]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _vivado_tcl(srcs: list[Path], incs: list[Path], defines: dict, top_module: str) -> str:
|
|
38
|
+
"""fault's write_vivado_tcl: a project with the sources as SystemVerilog, run to $finish."""
|
|
39
|
+
files = " ".join(f"{{{src}}}" for src in srcs)
|
|
40
|
+
cmds = ["create_project -force {project} {project}", f"add_files [list {files}]"]
|
|
41
|
+
cmds += [f"set_property file_type SystemVerilog [get_files [list {files}]]"]
|
|
42
|
+
if incs:
|
|
43
|
+
dirs = " ".join(f"{{{d}}}" for d in incs)
|
|
44
|
+
cmds += [f"set_property include_dirs [list {dirs}] [get_fileset sim_1]"]
|
|
45
|
+
if defines:
|
|
46
|
+
defs = " ".join(f"{{{k}}}" if v is None else f"{{{k}={v}}}" for k, v in defines.items())
|
|
47
|
+
cmds += [f"set_property -name verilog_define -value [list {defs}] -objects [get_fileset sim_1]"]
|
|
48
|
+
cmds += [f"set_property -name top -value {{{top_module}}} -objects [get_fileset sim_1]"]
|
|
49
|
+
cmds += ["set_property -name xsim.simulate.runtime -value -all -objects [get_fileset sim_1]"]
|
|
50
|
+
cmds += ["launch_simulation"]
|
|
51
|
+
return "\n".join(cmds)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class GetValue:
|
|
56
|
+
port: Port
|
|
57
|
+
value: float | int | None = None
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def is_real(self) -> bool:
|
|
61
|
+
return self.port.type_.kind == "real"
|
|
62
|
+
|
|
63
|
+
def update_from_line(self, line: str) -> None:
|
|
64
|
+
self.value = float(line) if self.is_real else int(line)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class _Action:
|
|
69
|
+
kind: str
|
|
70
|
+
args: tuple = ()
|
|
71
|
+
kwargs: dict = field(default_factory=dict)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _sv_real(value) -> str:
|
|
75
|
+
# float() first: NumPy 2 scalars repr as "np.float64(...)", which is not SV.
|
|
76
|
+
return repr(float(value))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _sv_value(port: Port, value) -> str:
|
|
80
|
+
type_ = port.type_
|
|
81
|
+
if type_.kind == "real":
|
|
82
|
+
return _sv_real(value)
|
|
83
|
+
width = type_.width
|
|
84
|
+
return f"{width}'d{int(value) & ((1 << width) - 1)}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _sv_string(text: str) -> str:
|
|
88
|
+
return text.replace('"', '\\"').replace("\n", "\\n")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Tester:
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
circuit,
|
|
95
|
+
clock: Port | None = None,
|
|
96
|
+
reset: Port | None = None,
|
|
97
|
+
poke_delay_default: float | None = None,
|
|
98
|
+
expect_strict_default: bool = True,
|
|
99
|
+
) -> None:
|
|
100
|
+
self._circuit = circuit
|
|
101
|
+
self.clock = clock
|
|
102
|
+
self.reset = reset
|
|
103
|
+
self.poke_delay_default = poke_delay_default
|
|
104
|
+
self.expect_strict_default = expect_strict_default
|
|
105
|
+
self.debug_mode = False
|
|
106
|
+
self.actions: list[_Action] = []
|
|
107
|
+
if clock is not None:
|
|
108
|
+
self.poke(clock, 0)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def circuit(self):
|
|
112
|
+
return self._circuit
|
|
113
|
+
|
|
114
|
+
# ---- actions ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
def poke(self, port: Port, value, delay: float | None = None) -> None:
|
|
117
|
+
if delay is None:
|
|
118
|
+
delay = self.poke_delay_default
|
|
119
|
+
self.actions.append(_Action("poke", (port, value), {"delay": delay}))
|
|
120
|
+
|
|
121
|
+
def expect(
|
|
122
|
+
self,
|
|
123
|
+
port: Port,
|
|
124
|
+
value,
|
|
125
|
+
strict: bool | None = None,
|
|
126
|
+
caller=None,
|
|
127
|
+
abs_tol: float | None = None,
|
|
128
|
+
rel_tol: float | None = None,
|
|
129
|
+
above: float | None = None,
|
|
130
|
+
below: float | None = None,
|
|
131
|
+
msg=None,
|
|
132
|
+
) -> None:
|
|
133
|
+
if strict is None:
|
|
134
|
+
strict = self.expect_strict_default
|
|
135
|
+
if abs_tol is not None or rel_tol is not None:
|
|
136
|
+
if above is not None or below is not None:
|
|
137
|
+
raise ValueError("Cannot provide both abs_tol/rel_tol and above/below.")
|
|
138
|
+
abs_tol = abs_tol or 0
|
|
139
|
+
rel_tol = rel_tol or 0
|
|
140
|
+
if abs_tol < 0 or rel_tol < 0:
|
|
141
|
+
raise ValueError("rel_tol and abs_tol must be non-negative.")
|
|
142
|
+
above = value - rel_tol * abs(value) - abs_tol
|
|
143
|
+
below = value + rel_tol * abs(value) + abs_tol
|
|
144
|
+
if msg is not None and not isinstance(msg, (str, tuple)):
|
|
145
|
+
raise ValueError("expect msg must be a str or a (format_str, *args) tuple")
|
|
146
|
+
self.actions.append(
|
|
147
|
+
_Action(
|
|
148
|
+
"expect",
|
|
149
|
+
(port, value),
|
|
150
|
+
{"strict": strict, "above": above, "below": below, "msg": msg},
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def eval(self) -> None:
|
|
155
|
+
self.actions.append(_Action("eval"))
|
|
156
|
+
|
|
157
|
+
def delay(self, time: float) -> None:
|
|
158
|
+
self.actions.append(_Action("delay", (time,)))
|
|
159
|
+
|
|
160
|
+
def step(self, steps: int = 1) -> None:
|
|
161
|
+
if self.clock is None:
|
|
162
|
+
raise RuntimeError("Stepping tester without a clock")
|
|
163
|
+
self.actions.append(_Action("step", (int(steps),)))
|
|
164
|
+
|
|
165
|
+
def print(self, format_str: str, *args) -> None:
|
|
166
|
+
self.actions.append(_Action("print", (format_str, *args)))
|
|
167
|
+
|
|
168
|
+
def get_value(self, port: Port) -> GetValue:
|
|
169
|
+
result = GetValue(port)
|
|
170
|
+
self.actions.append(_Action("get_value", (result,)))
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
def zero_inputs(self) -> None:
|
|
174
|
+
for name, type_ in self._circuit.IO.ports.items():
|
|
175
|
+
if type_.is_input():
|
|
176
|
+
self.poke(getattr(self._circuit, name), 0)
|
|
177
|
+
|
|
178
|
+
# ---- code generation -------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
def _render_action(self, i: int, action: _Action, clock_step_delay: float) -> list[str]:
|
|
181
|
+
kind, args, kw = action.kind, action.args, action.kwargs
|
|
182
|
+
if kind == "poke":
|
|
183
|
+
port, value = args
|
|
184
|
+
lines = [f"{port.name} <= {_sv_value(port, value)};"]
|
|
185
|
+
if kw["delay"] is not None:
|
|
186
|
+
lines.append(f"#({kw['delay']}*1s);")
|
|
187
|
+
return lines
|
|
188
|
+
if kind == "eval":
|
|
189
|
+
return ["#1;"]
|
|
190
|
+
if kind == "delay":
|
|
191
|
+
return [f"#({args[0]}*1s);"]
|
|
192
|
+
if kind == "step":
|
|
193
|
+
return [f"#{clock_step_delay} {self.clock.name} ^= 1;" for _ in range(args[0])]
|
|
194
|
+
if kind == "print":
|
|
195
|
+
fmt, *ports = args
|
|
196
|
+
items = [f'"{_sv_string(fmt)}"'] + [
|
|
197
|
+
p.name if isinstance(p, Port) else str(p) for p in ports
|
|
198
|
+
]
|
|
199
|
+
return [f"$write({', '.join(items)});"]
|
|
200
|
+
if kind == "get_value":
|
|
201
|
+
(result,) = args
|
|
202
|
+
fmt = "%0f" if result.is_real else "%0d"
|
|
203
|
+
return [f'$fwrite(__get_value_fd, "{fmt}\\n", {result.port.name});']
|
|
204
|
+
if kind == "expect":
|
|
205
|
+
return self._render_expect(i, *args, **kw)
|
|
206
|
+
raise AssertionError(f"unknown action {kind}")
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def _render_expect(i: int, port: Port, value, strict, above, below, msg) -> list[str]:
|
|
210
|
+
name = port.name
|
|
211
|
+
if above is not None and below is not None:
|
|
212
|
+
lo, hi = _sv_real(above), _sv_real(below)
|
|
213
|
+
cond = f"(({lo} <= {name}) && ({name} <= {hi}))"
|
|
214
|
+
err_msg, err_args = "Expected %0f to %0f, got %0f", [lo, hi, name]
|
|
215
|
+
elif above is not None:
|
|
216
|
+
lo = _sv_real(above)
|
|
217
|
+
cond = f"({lo} <= {name})"
|
|
218
|
+
err_msg, err_args = "Expected above %0f, got %0f", [lo, name]
|
|
219
|
+
elif below is not None:
|
|
220
|
+
hi = _sv_real(below)
|
|
221
|
+
cond = f"({name} <= {hi})"
|
|
222
|
+
err_msg, err_args = "Expected below %0f, got %0f", [hi, name]
|
|
223
|
+
else:
|
|
224
|
+
sv_value = _sv_value(port, value)
|
|
225
|
+
is_real = port.type_.kind == "real"
|
|
226
|
+
# SV forbids === on reals; fault emitted it anyway, which failed to compile.
|
|
227
|
+
cond = f"({name} {'===' if strict and not is_real else '=='} {sv_value})"
|
|
228
|
+
fmt = "%0f" if is_real else "%x"
|
|
229
|
+
err_msg, err_args = f"Expected {fmt}, got {fmt}", [sv_value, name]
|
|
230
|
+
if isinstance(msg, str):
|
|
231
|
+
err_msg += "\\n" + _sv_string(msg)
|
|
232
|
+
elif isinstance(msg, tuple):
|
|
233
|
+
err_msg += "\\n" + _sv_string(msg[0])
|
|
234
|
+
err_args += [p.name if isinstance(p, Port) else str(p) for p in msg[1:]]
|
|
235
|
+
body = ", ".join([f'"Failed on action={i} checking port {name}. {err_msg}."', *err_args])
|
|
236
|
+
return [f"if (!{cond}) begin", f" $error({body});", "end"]
|
|
237
|
+
|
|
238
|
+
def _render_testbench(
|
|
239
|
+
self,
|
|
240
|
+
top_module: str,
|
|
241
|
+
timescale: str,
|
|
242
|
+
clock_step_delay: float,
|
|
243
|
+
parameters: dict,
|
|
244
|
+
dump_waveforms: bool,
|
|
245
|
+
get_value_path: Path,
|
|
246
|
+
) -> str:
|
|
247
|
+
decls, conns = [], []
|
|
248
|
+
for name, type_ in self._circuit.IO.ports.items():
|
|
249
|
+
if type_.kind == "real":
|
|
250
|
+
sv_type = "real"
|
|
251
|
+
else:
|
|
252
|
+
sv_type = "reg" if type_.is_input() else "wire"
|
|
253
|
+
if type_.kind == "bits":
|
|
254
|
+
sv_type += f" [{type_.width - 1}:0]"
|
|
255
|
+
decls.append(f" {sv_type} {name};")
|
|
256
|
+
conns.append(f".{name}({name})")
|
|
257
|
+
|
|
258
|
+
body = []
|
|
259
|
+
if dump_waveforms:
|
|
260
|
+
body += ['$dumpfile("waveforms.vcd");', "$dumpvars(0, dut);"]
|
|
261
|
+
uses_get_value = any(a.kind == "get_value" for a in self.actions)
|
|
262
|
+
if uses_get_value:
|
|
263
|
+
body.append(f'__get_value_fd = $fopen("{get_value_path}", "w");')
|
|
264
|
+
for i, action in enumerate(self.actions):
|
|
265
|
+
body += self._render_action(i, action, clock_step_delay)
|
|
266
|
+
if uses_get_value:
|
|
267
|
+
body.append("$fclose(__get_value_fd);")
|
|
268
|
+
body += ["#20;", f'$display("{_DONE_TOKEN}");', "$finish;"]
|
|
269
|
+
|
|
270
|
+
params = ", ".join(f".{k}({v})" for k, v in parameters.items())
|
|
271
|
+
param_str = f"#({params}) " if params else ""
|
|
272
|
+
lines = [
|
|
273
|
+
f"`timescale {timescale}",
|
|
274
|
+
f"module {top_module};",
|
|
275
|
+
*decls,
|
|
276
|
+
" integer __get_value_fd;" if uses_get_value else "",
|
|
277
|
+
f" {self._circuit.name} {param_str}dut ({', '.join(conns)});",
|
|
278
|
+
" initial begin",
|
|
279
|
+
*[f" {line}" for line in body],
|
|
280
|
+
" end",
|
|
281
|
+
"endmodule",
|
|
282
|
+
"",
|
|
283
|
+
]
|
|
284
|
+
return "\n".join(lines)
|
|
285
|
+
|
|
286
|
+
# ---- compile and run -------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
def compile_and_run(self, target: str = "system-verilog", tmp_dir: bool = False, **kwargs):
|
|
289
|
+
if tmp_dir:
|
|
290
|
+
with tempfile.TemporaryDirectory(dir=".") as directory:
|
|
291
|
+
kwargs["directory"] = directory
|
|
292
|
+
self._compile_and_run(target, **kwargs)
|
|
293
|
+
else:
|
|
294
|
+
self._compile_and_run(target, **kwargs)
|
|
295
|
+
|
|
296
|
+
def _compile_and_run(
|
|
297
|
+
self,
|
|
298
|
+
target: str,
|
|
299
|
+
simulator: str | None = None,
|
|
300
|
+
directory="build/",
|
|
301
|
+
ext_srcs=None,
|
|
302
|
+
include_verilog_libraries=None,
|
|
303
|
+
ext_model_file=None,
|
|
304
|
+
ext_libs=None,
|
|
305
|
+
inc_dirs=None,
|
|
306
|
+
include_directories=None,
|
|
307
|
+
defines=None,
|
|
308
|
+
flags=None,
|
|
309
|
+
parameters=None,
|
|
310
|
+
timescale: str = "1ns/1ns",
|
|
311
|
+
clock_step_delay: float = 5,
|
|
312
|
+
num_cycles=None, # accepted for API compatibility; SV runs to $finish
|
|
313
|
+
dump_waveforms: bool = False,
|
|
314
|
+
disp_type: str = "on_error",
|
|
315
|
+
ext_test_bench: bool = False,
|
|
316
|
+
top_module: str | None = None,
|
|
317
|
+
) -> None:
|
|
318
|
+
if target == "verilator":
|
|
319
|
+
simulator = "verilator"
|
|
320
|
+
elif target != "system-verilog":
|
|
321
|
+
raise NotImplementedError(f"svtb supports only system-verilog, not {target!r}")
|
|
322
|
+
if simulator not in _SIMULATORS:
|
|
323
|
+
raise NotImplementedError(f"svtb supports {_SIMULATORS}, not {simulator!r}")
|
|
324
|
+
|
|
325
|
+
directory = Path(directory).resolve()
|
|
326
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
327
|
+
srcs = [Path(s).resolve() for s in (include_verilog_libraries or []) + (ext_srcs or [])]
|
|
328
|
+
if isinstance(ext_model_file, (str, Path)):
|
|
329
|
+
srcs.append(Path(ext_model_file).resolve())
|
|
330
|
+
elif not (ext_model_file or ext_test_bench):
|
|
331
|
+
raise NotImplementedError("svtb cannot compile magma circuits; pass ext_model_file")
|
|
332
|
+
incs = [Path(d).resolve() for d in (inc_dirs or []) + (include_directories or [])]
|
|
333
|
+
lib_dirs = list(dict.fromkeys(Path(lib).resolve().parent for lib in ext_libs or []))
|
|
334
|
+
def_args = [
|
|
335
|
+
f"-D{k}" if v is None else f"-D{k}={v}" for k, v in (defines or {}).items()
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
get_value_path = directory / _GET_VALUE_FILE
|
|
339
|
+
if ext_test_bench:
|
|
340
|
+
top_module = top_module or self._circuit.name
|
|
341
|
+
else:
|
|
342
|
+
top_module = top_module or f"{self._circuit.name}_tb"
|
|
343
|
+
tb_file = directory / f"{self._circuit.name}_tb.sv"
|
|
344
|
+
tb_file.write_text(
|
|
345
|
+
self._render_testbench(
|
|
346
|
+
top_module,
|
|
347
|
+
timescale,
|
|
348
|
+
clock_step_delay,
|
|
349
|
+
parameters or {},
|
|
350
|
+
dump_waveforms,
|
|
351
|
+
get_value_path,
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
srcs = [tb_file, *srcs]
|
|
355
|
+
|
|
356
|
+
bin_name = f"{self._circuit.name}_tb"
|
|
357
|
+
if simulator == "iverilog":
|
|
358
|
+
compile_cmd = ["iverilog", *(flags or []), f"-o{bin_name}"]
|
|
359
|
+
if lib_dirs:
|
|
360
|
+
compile_cmd += ["-Y.v", "-Y.sv", *[f"-y{d}" for d in lib_dirs]]
|
|
361
|
+
compile_cmd += [f"-I{d}" for d in incs] + def_args
|
|
362
|
+
compile_cmd += ["-g2012", "-s", top_module, *srcs]
|
|
363
|
+
compile_err = ["syntax error", "I give up."]
|
|
364
|
+
run_cmd = ["vvp", "-N", bin_name]
|
|
365
|
+
run_err = ["ERROR", "FATAL"]
|
|
366
|
+
elif simulator == "verilator":
|
|
367
|
+
compile_cmd = ["verilator", "--binary", "--timing", "-Wno-fatal"]
|
|
368
|
+
compile_cmd += ["--timescale", timescale, *(flags or [])]
|
|
369
|
+
compile_cmd += [f"-y{d}" for d in lib_dirs]
|
|
370
|
+
compile_cmd += [f"-I{d}" for d in incs] + def_args
|
|
371
|
+
compile_cmd += ["--top-module", top_module, "-o", bin_name, *srcs]
|
|
372
|
+
compile_err = None
|
|
373
|
+
run_cmd = [str(directory / "obj_dir" / bin_name)]
|
|
374
|
+
run_err = ["%Error", "ERROR", "FATAL"]
|
|
375
|
+
elif simulator in ("xcelium", "ncsim"):
|
|
376
|
+
# one command compiles and runs; fault's cadence_cmd and write_cadence_tcl
|
|
377
|
+
tcl = directory / f"{self._circuit.name}_cmd.tcl"
|
|
378
|
+
tcl.write_text("run\nassertion -summary -final\nquit")
|
|
379
|
+
compile_cmd = None
|
|
380
|
+
run_cmd = ["xrun" if simulator == "xcelium" else "irun", *(flags or [])]
|
|
381
|
+
run_cmd += ["-top", top_module, "-timescale", timescale, "-input", tcl, *srcs]
|
|
382
|
+
for lib in ext_libs or []:
|
|
383
|
+
run_cmd += ["-v", Path(lib).resolve()]
|
|
384
|
+
for d in incs:
|
|
385
|
+
run_cmd += ["-incdir", d]
|
|
386
|
+
run_cmd += _plus_defines(defines)
|
|
387
|
+
if dump_waveforms:
|
|
388
|
+
run_cmd += ["-access", "r"]
|
|
389
|
+
run_cmd += ["-notimingchecks"]
|
|
390
|
+
run_err = [_EXPECT_FAILED]
|
|
391
|
+
elif simulator == "vivado":
|
|
392
|
+
# a batch-mode project that compiles and runs; fault's write_vivado_tcl and vivado_cmd
|
|
393
|
+
tcl = directory / f"{self._circuit.name}_cmd.tcl"
|
|
394
|
+
tcl.write_text(
|
|
395
|
+
_vivado_tcl(srcs + [Path(lib).resolve() for lib in ext_libs or []], incs,
|
|
396
|
+
defines or {}, top_module)
|
|
397
|
+
)
|
|
398
|
+
compile_cmd = None
|
|
399
|
+
run_cmd = ["vivado", *(flags or []), "-mode", "batch", "-source", tcl]
|
|
400
|
+
run_cmd += ["-nolog", "-nojournal"]
|
|
401
|
+
run_err = ["CRITICAL WARNING", "ERROR", "Fatal", "Error", _EXPECT_FAILED]
|
|
402
|
+
else:
|
|
403
|
+
# compile to ./simv, then run it; fault's vcs_cmd
|
|
404
|
+
compile_cmd = ["vcs", *(flags or []), f"-timescale={timescale}", *srcs]
|
|
405
|
+
for lib in ext_libs or []:
|
|
406
|
+
compile_cmd += ["-v", Path(lib).resolve()]
|
|
407
|
+
compile_cmd += [f"+incdir+{d}" for d in incs] + _plus_defines(defines)
|
|
408
|
+
compile_cmd += ["-sverilog", "-full64", "+v2k", "-LDFLAGS", "-Wl,--no-as-needed"]
|
|
409
|
+
if dump_waveforms:
|
|
410
|
+
compile_cmd += ["-debug_pp"]
|
|
411
|
+
compile_cmd += ["-top", top_module]
|
|
412
|
+
compile_err = None
|
|
413
|
+
run_cmd = ["./simv"]
|
|
414
|
+
run_err = ["Error", "Fatal", _EXPECT_FAILED]
|
|
415
|
+
|
|
416
|
+
if compile_cmd is not None:
|
|
417
|
+
subprocess_run(compile_cmd, cwd=directory, err_str=compile_err, disp_type=disp_type)
|
|
418
|
+
result = subprocess_run(run_cmd, cwd=directory, err_str=run_err, disp_type=disp_type)
|
|
419
|
+
if not ext_test_bench and _DONE_TOKEN not in result.stdout:
|
|
420
|
+
print(result.stdout)
|
|
421
|
+
raise AssertionError("Testbench did not reach its final action.")
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
get_values = [a.args[0] for a in self.actions if a.kind == "get_value"]
|
|
425
|
+
if get_values:
|
|
426
|
+
lines = get_value_path.read_text().splitlines()
|
|
427
|
+
if len(lines) != len(get_values):
|
|
428
|
+
raise AssertionError(
|
|
429
|
+
f"Expected {len(get_values)} get_value results, found {len(lines)}."
|
|
430
|
+
)
|
|
431
|
+
for line, result_ in zip(lines, get_values):
|
|
432
|
+
result_.update_from_line(line)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (C) 2016 Stanford University.
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
7
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
8
|
+
the Software without restriction, including without limitation the rights to
|
|
9
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
10
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
11
|
+
subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
18
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
19
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
20
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
21
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Port-declaration subset of magma (https://github.com/phanrahan/magma, MIT, see LICENSE.txt), as svreal's
|
|
2
|
+
and msdsl's tests use it.
|
|
3
|
+
|
|
4
|
+
Only declares a DUT's name and port list; no circuit construction or compilation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, replace
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class PortType:
|
|
14
|
+
kind: str # 'bit', 'bits', 'clock' or 'real'
|
|
15
|
+
width: int = 1
|
|
16
|
+
signed: bool = False
|
|
17
|
+
direction: str | None = None # 'in', 'out' or None
|
|
18
|
+
|
|
19
|
+
def is_input(self) -> bool:
|
|
20
|
+
return self.direction == "in"
|
|
21
|
+
|
|
22
|
+
def is_output(self) -> bool:
|
|
23
|
+
return self.direction == "out"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _VectorFactory:
|
|
27
|
+
def __init__(self, signed: bool) -> None:
|
|
28
|
+
self._signed = signed
|
|
29
|
+
|
|
30
|
+
def __getitem__(self, width: int) -> PortType:
|
|
31
|
+
return PortType("bits", int(width), self._signed)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def In(type_: PortType) -> PortType: # noqa: N802 (magma API name)
|
|
35
|
+
return replace(type_, direction="in")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def Out(type_: PortType) -> PortType: # noqa: N802 (magma API name)
|
|
39
|
+
return replace(type_, direction="out")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
Bit = PortType("bit")
|
|
43
|
+
BitIn = In(Bit)
|
|
44
|
+
BitOut = Out(Bit)
|
|
45
|
+
Clock = PortType("clock")
|
|
46
|
+
ClockIn = In(Clock)
|
|
47
|
+
Bits = _VectorFactory(signed=False)
|
|
48
|
+
UInt = _VectorFactory(signed=False)
|
|
49
|
+
SInt = _VectorFactory(signed=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class Port:
|
|
54
|
+
name: str
|
|
55
|
+
type_: PortType
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
return self.name
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class IO:
|
|
62
|
+
def __init__(self, **ports: PortType) -> None:
|
|
63
|
+
self.ports = dict(ports)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _CircuitMeta(type):
|
|
67
|
+
def __init__(cls, name, bases, namespace):
|
|
68
|
+
super().__init__(name, bases, namespace)
|
|
69
|
+
io = namespace.get("io")
|
|
70
|
+
if io is None:
|
|
71
|
+
return
|
|
72
|
+
if "name" not in namespace:
|
|
73
|
+
cls.name = name
|
|
74
|
+
cls.IO = io
|
|
75
|
+
for port_name, type_ in io.ports.items():
|
|
76
|
+
setattr(cls, port_name, Port(port_name, type_))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Circuit(metaclass=_CircuitMeta):
|
|
80
|
+
pass
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: svtb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Self-checking SystemVerilog testbench generator: the subset of fault's Tester and magma's port-declaration API that svreal's and msdsl's tests use, for Python versions neither package installs on
|
|
5
|
+
Author: Justin Correll
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/correlljm/svtb
|
|
8
|
+
Project-URL: Repository, https://github.com/correlljm/svtb
|
|
9
|
+
Project-URL: Issues, https://github.com/correlljm/svtb/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/correlljm/svtb/blob/main/CHANGELOG.md
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Requires-Python: >=3.14
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
License-File: svtb/fault/LICENSE.txt
|
|
16
|
+
License-File: svtb/magma/LICENSE.txt
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# svtb
|
|
20
|
+
|
|
21
|
+
A self-checking SystemVerilog testbench generator: the subset of
|
|
22
|
+
[fault](https://github.com/leonardt/fault)'s `Tester` and [magma](https://github.com/phanrahan/magma)'s
|
|
23
|
+
port-declaration API that svreal's and msdsl's historical test suites use. Neither fault nor magma installs
|
|
24
|
+
on Python 3.14; this is a from-scratch implementation written to the same calling convention, so existing
|
|
25
|
+
test files do not change.
|
|
26
|
+
|
|
27
|
+
`svtb.fault.Tester` records `poke`, `eval`, `step`, `expect` and `get_value` calls and generates a
|
|
28
|
+
self-checking SystemVerilog testbench, run on iverilog, Verilator, Xcelium, Incisive, Vivado or VCS. The
|
|
29
|
+
commands for Xcelium, Incisive, Vivado and VCS follow fault 3.0.43's `SystemVerilogTarget`.
|
|
30
|
+
|
|
31
|
+
`svtb.magma` declares a DUT's name and port list only; no circuit construction or compilation.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```shell
|
|
36
|
+
pip install svtb
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
or, in a consuming package's `pyproject.toml`, as a dev dependency:
|
|
40
|
+
|
|
41
|
+
```toml
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = ["svtb", "pytest"]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then point `tests/conftest.py`'s import path at the installed package instead of a local directory:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import svtb
|
|
50
|
+
sys.path.insert(0, str(Path(svtb.__file__).parent))
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This makes `import fault` and `import magma as m` resolve to `svtb`'s subpackages as top-level modules,
|
|
54
|
+
exactly as they would from a local `tests/support/` directory.
|
|
55
|
+
|
|
56
|
+
## Licensing
|
|
57
|
+
|
|
58
|
+
- `fault` is BSD-3-Clause, Copyright 2018 Leonard Truong; see `svtb/fault/LICENSE.txt`.
|
|
59
|
+
- `magma` is MIT, Copyright (C) 2016 Stanford University; see `svtb/magma/LICENSE.txt`.
|
|
60
|
+
- svtb itself is MIT; see `LICENSE`.
|
|
61
|
+
|
|
62
|
+
## Running the tests
|
|
63
|
+
|
|
64
|
+
```shell
|
|
65
|
+
uv sync
|
|
66
|
+
uv run pytest -q tests
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`tests/test_tester.py` checks the generator on each simulator; `tests/fake_sim.py` stands in for Xcelium,
|
|
70
|
+
Incisive, Vivado and VCS, since only iverilog and Verilator have run here.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
svtb/__init__.py
|
|
5
|
+
svtb.egg-info/PKG-INFO
|
|
6
|
+
svtb.egg-info/SOURCES.txt
|
|
7
|
+
svtb.egg-info/dependency_links.txt
|
|
8
|
+
svtb.egg-info/top_level.txt
|
|
9
|
+
svtb/fault/LICENSE.txt
|
|
10
|
+
svtb/fault/__init__.py
|
|
11
|
+
svtb/fault/subprocess_run.py
|
|
12
|
+
svtb/fault/tester.py
|
|
13
|
+
svtb/magma/LICENSE.txt
|
|
14
|
+
svtb/magma/__init__.py
|
|
15
|
+
tests/test_tester.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
svtb
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from shutil import which
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import fault
|
|
10
|
+
import magma as m
|
|
11
|
+
|
|
12
|
+
DUT_FILE = Path(__file__).resolve().parent / "dut.sv"
|
|
13
|
+
FAKE_SIM = Path(__file__).resolve().parent / "fake_sim.py"
|
|
14
|
+
# simulator name -> the executable fake_sim.py stands in for
|
|
15
|
+
FAKES = {"xcelium": "xrun", "ncsim": "irun", "vivado": "vivado", "vcs": "vcs"}
|
|
16
|
+
REAL = [s for s in ("iverilog", "verilator") if which(s)]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture(params=REAL + [f"fake-{s}" for s in FAKES])
|
|
20
|
+
def simulator(request, tmp_path_factory, monkeypatch):
|
|
21
|
+
"""A real simulator, or a commercial one, stood in for by fake_sim.py on PATH."""
|
|
22
|
+
name = request.param
|
|
23
|
+
if not name.startswith("fake-"):
|
|
24
|
+
return name
|
|
25
|
+
bindir = tmp_path_factory.mktemp("fakebin")
|
|
26
|
+
for exe in FAKES.values():
|
|
27
|
+
(bindir / exe).symlink_to(FAKE_SIM)
|
|
28
|
+
monkeypatch.setenv("PATH", f"{bindir}{os.pathsep}{os.environ['PATH']}")
|
|
29
|
+
return name.removeprefix("fake-")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class dut(m.Circuit): # noqa: N801 (mirrors legacy test style)
|
|
33
|
+
name = "dut"
|
|
34
|
+
io = m.IO(
|
|
35
|
+
a=fault.RealIn,
|
|
36
|
+
y=fault.RealOut,
|
|
37
|
+
d=m.In(m.Bits[8]),
|
|
38
|
+
q_comb=m.Out(m.Bits[8]),
|
|
39
|
+
q_reg=m.Out(m.Bits[8]),
|
|
40
|
+
clk=m.ClockIn,
|
|
41
|
+
early_finish=m.BitIn,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def make_tester():
|
|
46
|
+
t = fault.Tester(dut, dut.clk)
|
|
47
|
+
t.zero_inputs()
|
|
48
|
+
t.eval()
|
|
49
|
+
return t
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run(t, simulator, tmp_path, **kwargs):
|
|
53
|
+
t.compile_and_run(
|
|
54
|
+
target="system-verilog",
|
|
55
|
+
simulator=simulator,
|
|
56
|
+
ext_srcs=[DUT_FILE],
|
|
57
|
+
ext_model_file=True,
|
|
58
|
+
directory=tmp_path,
|
|
59
|
+
**kwargs,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_correct_expects_pass(simulator, tmp_path):
|
|
64
|
+
t = make_tester()
|
|
65
|
+
t.poke(dut.a, 1.25)
|
|
66
|
+
t.poke(dut.d, 41)
|
|
67
|
+
t.eval()
|
|
68
|
+
t.expect(dut.y, 2.5, abs_tol=1e-9)
|
|
69
|
+
t.expect(dut.q_comb, 42)
|
|
70
|
+
t.expect(dut.q_reg, 0)
|
|
71
|
+
t.step(2)
|
|
72
|
+
t.expect(dut.q_reg, 41)
|
|
73
|
+
t.poke(dut.d, 255)
|
|
74
|
+
t.eval()
|
|
75
|
+
t.expect(dut.q_comb, 0) # 8-bit wrap
|
|
76
|
+
y = t.get_value(dut.y)
|
|
77
|
+
q = t.get_value(dut.q_reg)
|
|
78
|
+
t.print("q_reg=%0d\n", dut.q_reg)
|
|
79
|
+
run(t, simulator, tmp_path)
|
|
80
|
+
assert y.value == pytest.approx(2.5)
|
|
81
|
+
assert q.value == 41
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_numpy_scalar_values(simulator, tmp_path):
|
|
85
|
+
t = make_tester()
|
|
86
|
+
t.poke(dut.a, np.float64(1.25))
|
|
87
|
+
t.eval()
|
|
88
|
+
t.expect(dut.y, np.float64(2.5), abs_tol=np.float64(1e-9))
|
|
89
|
+
t.expect(dut.y, np.float64(2.5))
|
|
90
|
+
run(t, simulator, tmp_path)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_wrong_real_expect_fails(simulator, tmp_path, capsys):
|
|
94
|
+
t = make_tester()
|
|
95
|
+
t.poke(dut.a, 1.25)
|
|
96
|
+
t.eval()
|
|
97
|
+
t.expect(dut.y, 2.6, abs_tol=0.05)
|
|
98
|
+
with pytest.raises(AssertionError):
|
|
99
|
+
run(t, simulator, tmp_path)
|
|
100
|
+
assert "Failed on action=" in capsys.readouterr().out
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_wrong_digital_expect_fails(simulator, tmp_path, capsys):
|
|
104
|
+
t = make_tester()
|
|
105
|
+
t.poke(dut.d, 41)
|
|
106
|
+
t.eval()
|
|
107
|
+
t.expect(dut.q_comb, 41)
|
|
108
|
+
with pytest.raises(AssertionError):
|
|
109
|
+
run(t, simulator, tmp_path)
|
|
110
|
+
assert "Failed on action=" in capsys.readouterr().out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_above_only_expect_passes(simulator, tmp_path):
|
|
114
|
+
t = make_tester()
|
|
115
|
+
t.poke(dut.a, 1.25)
|
|
116
|
+
t.eval()
|
|
117
|
+
t.expect(dut.y, None, above=2.0) # y = 2.5
|
|
118
|
+
run(t, simulator, tmp_path)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_above_only_expect_fails(simulator, tmp_path, capsys):
|
|
122
|
+
t = make_tester()
|
|
123
|
+
t.poke(dut.a, 1.25)
|
|
124
|
+
t.eval()
|
|
125
|
+
t.expect(dut.y, None, above=3.0) # y = 2.5, not above 3.0
|
|
126
|
+
with pytest.raises(AssertionError):
|
|
127
|
+
run(t, simulator, tmp_path)
|
|
128
|
+
assert "Failed on action=" in capsys.readouterr().out
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_below_only_expect_passes(simulator, tmp_path):
|
|
132
|
+
t = make_tester()
|
|
133
|
+
t.poke(dut.a, 1.25)
|
|
134
|
+
t.eval()
|
|
135
|
+
t.expect(dut.y, None, below=3.0) # y = 2.5
|
|
136
|
+
run(t, simulator, tmp_path)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_below_only_expect_fails(simulator, tmp_path, capsys):
|
|
140
|
+
t = make_tester()
|
|
141
|
+
t.poke(dut.a, 1.25)
|
|
142
|
+
t.eval()
|
|
143
|
+
t.expect(dut.y, None, below=2.0) # y = 2.5, not below 2.0
|
|
144
|
+
with pytest.raises(AssertionError):
|
|
145
|
+
run(t, simulator, tmp_path)
|
|
146
|
+
assert "Failed on action=" in capsys.readouterr().out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_early_finish_fails(simulator, tmp_path):
|
|
150
|
+
t = make_tester()
|
|
151
|
+
t.poke(dut.early_finish, 1)
|
|
152
|
+
t.eval()
|
|
153
|
+
t.expect(dut.q_comb, 1)
|
|
154
|
+
with pytest.raises(AssertionError, match="did not reach"):
|
|
155
|
+
run(t, simulator, tmp_path)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@pytest.mark.parametrize("simulator", [f"fake-{s}" for s in FAKES], indirect=True)
|
|
159
|
+
def test_commands_follow_fault(simulator, tmp_path):
|
|
160
|
+
"""The command line and Tcl each tool receives, as fault 3.0.43's SystemVerilogTarget writes them."""
|
|
161
|
+
t = make_tester()
|
|
162
|
+
t.poke(dut.a, 1.25)
|
|
163
|
+
t.eval()
|
|
164
|
+
t.expect(dut.y, 2.5, abs_tol=1e-9)
|
|
165
|
+
inc = tmp_path / "inc"
|
|
166
|
+
inc.mkdir()
|
|
167
|
+
run(t, simulator, tmp_path, inc_dirs=[inc], defines={"FOO": None, "BAR": 2})
|
|
168
|
+
argv = json.loads((tmp_path / "fake_sim.json").read_text())["argv"]
|
|
169
|
+
tb, tcl = str(tmp_path / "dut_tb.sv"), str(tmp_path / "dut_cmd.tcl")
|
|
170
|
+
srcs = [tb, str(DUT_FILE)]
|
|
171
|
+
if simulator in ("xcelium", "ncsim"):
|
|
172
|
+
assert argv == ["-top", "dut_tb", "-timescale", "1ns/1ns", "-input", tcl, *srcs,
|
|
173
|
+
"-incdir", str(inc), "+define+FOO", "+define+BAR=2", "-notimingchecks"]
|
|
174
|
+
assert Path(tcl).read_text() == "run\nassertion -summary -final\nquit"
|
|
175
|
+
elif simulator == "vivado":
|
|
176
|
+
assert argv == ["-mode", "batch", "-source", tcl, "-nolog", "-nojournal"]
|
|
177
|
+
files = f"{{{tb}}} {{{DUT_FILE}}}"
|
|
178
|
+
assert Path(tcl).read_text().splitlines() == [
|
|
179
|
+
"create_project -force {project} {project}",
|
|
180
|
+
f"add_files [list {files}]",
|
|
181
|
+
f"set_property file_type SystemVerilog [get_files [list {files}]]",
|
|
182
|
+
f"set_property include_dirs [list {{{inc}}}] [get_fileset sim_1]",
|
|
183
|
+
"set_property -name verilog_define -value [list {FOO} {BAR=2}] -objects [get_fileset sim_1]",
|
|
184
|
+
"set_property -name top -value {dut_tb} -objects [get_fileset sim_1]",
|
|
185
|
+
"set_property -name xsim.simulate.runtime -value -all -objects [get_fileset sim_1]",
|
|
186
|
+
"launch_simulation",
|
|
187
|
+
]
|
|
188
|
+
else:
|
|
189
|
+
assert argv == ["-timescale=1ns/1ns", *srcs, f"+incdir+{inc}", "+define+FOO", "+define+BAR=2",
|
|
190
|
+
"-sverilog", "-full64", "+v2k", "-LDFLAGS", "-Wl,--no-as-needed",
|
|
191
|
+
"-top", "dut_tb"]
|