1"""Unit tests for the Verilator cosim backend.
3These tests exercise the command-generation and CMake-template logic of the
4Verilator class *without* requiring the compiled ``esiCppAccel`` C++
5extension. We achieve this by inserting a ``MagicMock`` for the extension
6module before the real package is imported.
12from pathlib
import Path
13from unittest
import mock
14from unittest.mock
import MagicMock
22_accel_mock = MagicMock()
23sys.modules[
"esiaccel.esiCppAccel"] = _accel_mock
26from esiaccel.cosim.verilator
import Verilator
27from esiaccel.cosim.simulator
import (
29 is_simulator_available,
38 """Create a Verilator instance with minimal setup."""
39 sources = SourceFiles(top)
40 if dpi_so
is not None:
41 sources.dpi_so = dpi_so
46 make_default_logs=
False,
47 macro_definitions=macros,
51requires_verilator_bin = pytest.mark.skipif(
52 not is_simulator_available(
"verilator"), reason=
"verilator not found")
58 with pytest.raises(ValueError):
59 is_simulator_available(
"bogus")
62 monkeypatch.delenv(
"VERILATOR_PATH", raising=
False)
63 monkeypatch.delenv(
"VERILATOR_ROOT", raising=
False)
64 monkeypatch.setattr(shutil,
"which",
lambda name:
None)
65 assert not is_simulator_available(
"verilator")
66 assert "verilator" not in available_simulators()
69 root = tmp_path /
"verilator"
70 (root /
"bin").mkdir(parents=
True)
71 pkg_root = root /
"share" /
"verilator"
72 (pkg_root /
"include").mkdir(parents=
True)
73 (pkg_root /
"include" /
"verilated.h").touch()
74 fake_bin = root /
"bin" /
"verilator_bin"
77 monkeypatch.setenv(
"VERILATOR_PATH", str(fake_bin))
78 monkeypatch.delenv(
"VERILATOR_ROOT", raising=
False)
79 monkeypatch.setattr(shutil,
"which",
lambda name:
None)
80 assert is_simulator_available(
"verilator")
81 assert "verilator" in available_simulators()
84 monkeypatch.setenv(
"VERILATOR_PATH",
85 str(tmp_path /
"missing" /
"verilator_bin"))
86 monkeypatch.delenv(
"VERILATOR_ROOT", raising=
False)
87 monkeypatch.setattr(shutil,
"which",
lambda name:
None)
88 with pytest.raises(RuntimeError, match=
"VERILATOR_PATH"):
89 is_simulator_available(
"verilator")
92 root = tmp_path /
"verilator"
93 (root /
"bin").mkdir(parents=
True)
94 pkg_root = root /
"share" /
"verilator"
95 pkg_root.mkdir(parents=
True)
96 fake_bin = root /
"bin" /
"verilator_bin"
99 monkeypatch.setenv(
"VERILATOR_PATH", str(fake_bin))
100 monkeypatch.setenv(
"VERILATOR_ROOT", str(pkg_root))
101 monkeypatch.setattr(shutil,
"which",
lambda name:
None)
102 with pytest.raises(RuntimeError, match=
"VERILATOR_ROOT"):
103 is_simulator_available(
"verilator")
106 monkeypatch.setattr(shutil,
"which",
lambda name:
None)
107 assert not is_simulator_available(
"questa")
110 monkeypatch.delenv(
"VERILATOR_PATH", raising=
False)
111 monkeypatch.delenv(
"VERILATOR_ROOT", raising=
False)
115 return "C:/questa/vsim.exe"
118 monkeypatch.setattr(shutil,
"which", _which)
119 assert is_simulator_available(
"questa")
120 assert available_simulators() == [
"questa"]
125 @requires_verilator_bin
128 cmds = v.compile_commands()
129 assert Path(cmds[0][0]).stem ==
"verilator_bin"
130 assert Path(cmds[0][0]) == v.verilator_bin
132 @requires_verilator_bin
135 cmds = v.compile_commands()
138 assert len(cmds) == 4
139 assert callable(cmds[1])
140 assert cmds[2][0] ==
"cmake"
141 assert "-G" in cmds[2]
and "Ninja" in cmds[2]
142 assert cmds[3][0] ==
"ninja"
144 @requires_verilator_bin
146 """When using cmake, --exe and --build should not appear."""
149 pytest.skip(
"cmake+ninja not available")
150 cmd = v.compile_commands()[0]
151 assert "--exe" not in cmd
152 assert "--build" not in cmd
154 @requires_verilator_bin
156 """When using cmake, -CFLAGS and -LDFLAGS should not appear."""
159 pytest.skip(
"cmake+ninja not available")
160 cmd = v.compile_commands()[0]
161 assert "-CFLAGS" not in cmd
162 assert "-LDFLAGS" not in cmd
164 @requires_verilator_bin
166 """When using cmake, driver.cpp should not be in the verilator command."""
169 pytest.skip(
"cmake+ninja not available")
170 cmd = v.compile_commands()[0]
171 assert not any(
"driver.cpp" in str(c)
for c
in cmd)
173 @requires_verilator_bin
176 cmd = v.compile_commands()[0]
177 assert "--trace-fst" in cmd
178 assert "--trace-structs" in cmd
179 assert "--trace-underscore" in cmd
182 fake_bin = tmp_path /
"custom" /
"verilator_bin"
183 fake_bin.parent.mkdir()
185 with mock.patch.dict(os.environ, {
"VERILATOR_PATH": str(fake_bin)}):
187 assert v.verilator_bin == fake_bin.resolve()
190 fake_wrapper = tmp_path /
"usr" /
"bin" /
"verilator"
191 fake_bin = fake_wrapper.parent /
"verilator_bin"
192 fake_wrapper.parent.mkdir(parents=
True)
195 with mock.patch.dict(os.environ, {
"VERILATOR_PATH": str(fake_wrapper)}):
197 assert v.verilator_bin == fake_bin.resolve()
200 env_root = tmp_path /
"env-verilator"
201 path_root = tmp_path /
"path-verilator"
202 (env_root /
"bin").mkdir(parents=
True)
203 (path_root /
"bin").mkdir(parents=
True)
204 env_bin = env_root /
"bin" /
"verilator_bin"
205 path_bin = path_root /
"bin" /
"verilator_bin"
209 with mock.patch.dict(os.environ, {
"VERILATOR_PATH": str(env_bin)}):
210 with mock.patch(
"shutil.which", return_value=str(path_bin)):
211 assert Verilator._find_verilator_bin() == env_bin.resolve()
214 with mock.patch.dict(os.environ, {}, clear=
False):
215 os.environ.pop(
"VERILATOR_ROOT",
None)
216 os.environ.pop(
"VERILATOR_PATH",
None)
217 with mock.patch(
"shutil.which", return_value=
None):
219 with pytest.raises(RuntimeError, match=
"Cannot find verilator_bin"):
222 @requires_verilator_bin
225 cmd = v.compile_commands()[0]
226 assert "+define+FOO=BAR" in cmd
227 assert "+define+BAZ" in cmd
230@requires_verilator_bin
232 """Tests for the make fallback when cmake/ninja are not available."""
235 """Create a Verilator instance that thinks cmake/ninja are missing."""
239 @pytest.fixture(autouse=True)
241 """Patch shutil.which so cmake and ninja appear absent."""
242 original_which = shutil.which
244 def _which_no_cmake(name, *args, **kwargs):
245 if name
in (
"cmake",
"ninja"):
247 return original_which(name, *args, **kwargs)
249 with mock.patch(
"shutil.which", side_effect=_which_no_cmake):
254 cmds = v.compile_commands()
255 assert len(cmds) == 2
256 assert cmds[1][0] ==
"make"
260 cmd = v.compile_commands()[0]
261 assert "--exe" in cmd
265 cmd = v.compile_commands()[0]
266 assert "-CFLAGS" in cmd
267 idx = cmd.index(
"-CFLAGS")
268 assert "-DTOP_MODULE=TestTop" in cmd[idx + 1]
272 cmd = v.compile_commands()[0]
273 assert any(
"driver.cpp" in str(c)
for c
in cmd)
277 cmd = v.compile_commands()[0]
278 assert "-LDFLAGS" in cmd
279 idx = cmd.index(
"-LDFLAGS")
280 assert "-lEsiCosimDpiServer" in cmd[idx + 1]
284 cmd = v.compile_commands()[0]
285 assert "-LDFLAGS" not in cmd
289 cmd = v.compile_commands()[0]
290 idx = cmd.index(
"-CFLAGS")
291 assert "-DTRACE" in cmd[idx + 1]
295 cmds = v.compile_commands()
297 assert make_cmd[0] ==
"make"
298 assert "-C" in make_cmd
299 assert "obj_dir" in make_cmd
300 assert "-f" in make_cmd
301 assert "VMyTop.mk" in make_cmd
305 exe_name =
"VMyTop.exe" if os.name ==
"nt" else "VMyTop"
306 with mock.patch.object(Path,
"cwd", return_value=tmp_path):
307 cmd = v.run_command(gui=
False)
308 assert cmd == [str(tmp_path /
"obj_dir" / exe_name)]
314 root = tmp_path /
"verilator"
316 (root /
"include").mkdir()
317 (root /
"include" /
"verilated.h").touch()
318 with mock.patch.dict(os.environ, {
"VERILATOR_ROOT": str(root)}):
320 assert v._find_verilator_root() == root
323 root = tmp_path /
"verilator"
324 (root /
"bin").mkdir(parents=
True)
325 pkg_root = root /
"share" /
"verilator"
326 (pkg_root /
"include").mkdir(parents=
True)
327 (pkg_root /
"include" /
"verilated.h").touch()
328 fake_bin = root /
"bin" /
"verilator_bin"
330 fake_bin.chmod(0o755)
331 with mock.patch.dict(os.environ, {}, clear=
False):
334 os.environ.pop(
"VERILATOR_ROOT",
None)
335 os.environ.pop(
"VERILATOR_PATH",
None)
336 with mock.patch(
"shutil.which", return_value=str(fake_bin)):
338 found = v._find_verilator_root()
339 assert found == pkg_root
342 with mock.patch.dict(os.environ, {}, clear=
False):
345 os.environ.pop(
"VERILATOR_ROOT",
None)
346 os.environ.pop(
"VERILATOR_PATH",
None)
347 with mock.patch(
"shutil.which", return_value=
None):
349 assert v._find_verilator_root()
is None
352 root = tmp_path /
"verilator"
354 with mock.patch.dict(os.environ, {
"VERILATOR_ROOT": str(root)}):
356 with pytest.raises(RuntimeError, match=
"VERILATOR_ROOT"):
357 v._find_verilator_root()
363 obj_dir = tmp_path /
"obj_dir"
365 generated_sources = [obj_dir /
"VTestTop.cpp"]
366 root = tmp_path /
"verilator"
367 (root /
"include").mkdir(parents=
True)
368 (root /
"include" /
"verilated.h").touch()
369 with mock.patch.dict(os.environ, {
"VERILATOR_ROOT": str(root)}):
371 build_dir = v._write_cmake(obj_dir, generated_sources)
372 assert (build_dir /
"CMakeLists.txt").exists()
373 content = (build_dir /
"CMakeLists.txt").read_text()
374 assert "VTestTop" in content
375 assert generated_sources[0].as_posix()
in content
376 assert "verilated.cpp" in content
377 assert "verilated_threads.cpp" in content
378 assert "driver.cpp" in content
381 obj_dir = tmp_path /
"obj_dir"
383 generated_sources = [obj_dir /
"VTestTop.cpp"]
384 root = tmp_path /
"verilator"
385 (root /
"include").mkdir(parents=
True)
386 (root /
"include" /
"verilated.h").touch()
387 with mock.patch.dict(os.environ, {
"VERILATOR_ROOT": str(root)}):
389 build_dir = v._write_cmake(obj_dir, generated_sources)
390 content = (build_dir /
"CMakeLists.txt").read_text()
391 assert "verilated_fst_c.cpp" in content
392 assert "TRACE" in content
395 obj_dir = tmp_path /
"obj_dir"
397 generated_sources = [obj_dir /
"VTestTop.cpp"]
398 pch_header = obj_dir /
"VTestTop__pch.h"
399 root = tmp_path /
"verilator"
400 (root /
"include").mkdir(parents=
True)
401 (root /
"include" /
"verilated.h").touch()
402 with mock.patch.dict(os.environ, {
"VERILATOR_ROOT": str(root)}):
404 build_dir = v._write_cmake(obj_dir, generated_sources, pch_header)
405 content = (build_dir /
"CMakeLists.txt").read_text()
406 assert "target_precompile_headers(VTestTop PRIVATE" in content
407 assert "VTestTop__pch.h" in content
408 assert "SKIP_PRECOMPILE_HEADERS ON" in content
409 assert "verilated.cpp" in content
410 assert "driver.cpp" in content
418 pytest.skip(
"cmake+ninja not available")
419 exe_name =
"VMyTop.exe" if os.name ==
"nt" else "VMyTop"
420 with mock.patch.object(Path,
"cwd", return_value=tmp_path):
421 cmd = v.run_command(gui=
False)
422 assert cmd == [str(tmp_path /
"obj_dir" /
"cmake_build" / exe_name)]
test_macro_definitions(self, tmp_path)
test_respects_verilator_path_env(self, tmp_path)
test_verilator_path_overrides_path(self, tmp_path)
test_driver_not_in_verilator_cmd_cmake(self, tmp_path)
test_trace_flags_in_debug(self, tmp_path)
test_uses_verilator_bin(self, tmp_path)
test_no_cflags_or_ldflags_cmake(self, tmp_path)
test_compile_commands_requires_verilator_bin(self, tmp_path)
test_no_exe_or_build_flags_cmake(self, tmp_path)
test_cmake_and_ninja_commands(self, tmp_path)
test_verilator_path_redirects_perl_wrapper(self, tmp_path)
test_from_env(self, tmp_path)
test_from_bin_in_path(self, tmp_path)
test_invalid_env_raises(self, tmp_path)
test_returns_none_when_not_found(self, tmp_path)
test_fallback_has_cflags(self, tmp_path)
test_fallback_exe_path(self, tmp_path)
test_fallback_trace_cflags_in_debug(self, tmp_path)
test_fallback_has_driver(self, tmp_path)
_make_no_cmake(self, tmp_path, **kwargs)
test_fallback_no_ldflags_without_dpi(self, tmp_path)
test_fallback_uses_make(self, tmp_path)
test_fallback_has_exe_flag(self, tmp_path)
test_fallback_make_command(self, tmp_path)
test_fallback_has_ldflags_with_dpi(self, tmp_path)
test_exe_path_cmake(self, tmp_path)
test_verilator_unavailable_without_bin(self, monkeypatch)
test_questa_available_from_path(self, monkeypatch)
test_invalid_verilator_root_env_raises(self, monkeypatch, tmp_path)
test_verilator_available_from_env_path(self, monkeypatch, tmp_path)
test_unknown_simulator_raises(self)
test_questa_unavailable_without_vsim(self, monkeypatch)
test_invalid_verilator_path_env_raises(self, monkeypatch, tmp_path)
test_generates_cmake(self, tmp_path)
test_trace_sources_in_debug(self, tmp_path)
test_enables_pch_when_generated_header_exists(self, tmp_path)
_make_verilator(run_dir, top="TestTop", debug=False, dpi_so=None, macros=None)