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 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ 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"]