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,
52 root = tmp_path /
"verilator"
53 fake_bin = root /
"bin" /
"verilator_bin"
54 fake_bin.parent.mkdir(parents=
True)
56 (root /
"include").mkdir()
57 (root /
"include" /
"verilated.h").touch()
58 monkeypatch.setenv(
"VERILATOR_PATH", str(fake_bin))
59 monkeypatch.setenv(
"VERILATOR_ROOT", str(root))
60 monkeypatch.chdir(tmp_path)
61 monkeypatch.setattr(Verilator,
"_use_cmake", property(
lambda self:
True))
62 monkeypatch.setattr(Verilator,
"_raise_stack_limit",
63 staticmethod(
lambda:
None))
67requires_verilator_bin = pytest.mark.skipif(
68 not is_simulator_available(
"verilator"), reason=
"verilator not found")
74 with pytest.raises(ValueError):
75 is_simulator_available(
"bogus")
78 monkeypatch.delenv(
"VERILATOR_PATH", raising=
False)
79 monkeypatch.delenv(
"VERILATOR_ROOT", raising=
False)
80 monkeypatch.setattr(shutil,
"which",
lambda name:
None)
81 assert not is_simulator_available(
"verilator")
82 assert "verilator" not in available_simulators()
85 root = tmp_path /
"verilator"
86 (root /
"bin").mkdir(parents=
True)
87 pkg_root = root /
"share" /
"verilator"
88 (pkg_root /
"include").mkdir(parents=
True)
89 (pkg_root /
"include" /
"verilated.h").touch()
90 fake_bin = root /
"bin" /
"verilator_bin"
93 monkeypatch.setenv(
"VERILATOR_PATH", str(fake_bin))
94 monkeypatch.delenv(
"VERILATOR_ROOT", raising=
False)
95 monkeypatch.setattr(shutil,
"which",
lambda name:
None)
96 assert is_simulator_available(
"verilator")
97 assert "verilator" in available_simulators()
100 monkeypatch.setenv(
"VERILATOR_PATH",
101 str(tmp_path /
"missing" /
"verilator_bin"))
102 monkeypatch.delenv(
"VERILATOR_ROOT", raising=
False)
103 monkeypatch.setattr(shutil,
"which",
lambda name:
None)
104 with pytest.raises(RuntimeError, match=
"VERILATOR_PATH"):
105 is_simulator_available(
"verilator")
108 root = tmp_path /
"verilator"
109 (root /
"bin").mkdir(parents=
True)
110 pkg_root = root /
"share" /
"verilator"
111 pkg_root.mkdir(parents=
True)
112 fake_bin = root /
"bin" /
"verilator_bin"
115 monkeypatch.setenv(
"VERILATOR_PATH", str(fake_bin))
116 monkeypatch.setenv(
"VERILATOR_ROOT", str(pkg_root))
117 monkeypatch.setattr(shutil,
"which",
lambda name:
None)
118 with pytest.raises(RuntimeError, match=
"VERILATOR_ROOT"):
119 is_simulator_available(
"verilator")
122 monkeypatch.setattr(shutil,
"which",
lambda name:
None)
123 assert not is_simulator_available(
"questa")
126 monkeypatch.delenv(
"VERILATOR_PATH", raising=
False)
127 monkeypatch.delenv(
"VERILATOR_ROOT", raising=
False)
131 return "C:/questa/vsim.exe"
134 monkeypatch.setattr(shutil,
"which", _which)
135 assert is_simulator_available(
"questa")
136 assert available_simulators() == [
"questa"]
141 @requires_verilator_bin
144 cmds = v.compile_commands()
145 assert Path(cmds[0][0]).stem ==
"verilator_bin"
146 assert Path(cmds[0][0]) == v.verilator_bin
150 build_dir = tmp_path /
"obj_dir" /
"cmake_build"
151 build_dir.mkdir(parents=
True)
152 with mock.patch.object(v,
"_run_compile_command", return_value=0)
as run:
153 cmds = v.compile_commands()
154 assert len(cmds) == 4
155 assert callable(cmds[1])
156 assert callable(cmds[2])
157 assert cmds[2]() == 0
158 assert cmds[3][0] ==
"ninja"
159 cmake_cmd = run.call_args.args[0]
160 assert cmake_cmd[0] ==
"cmake"
161 assert "-G" in cmake_cmd
and "Ninja" in cmake_cmd
165 build_dir = tmp_path /
"obj_dir" /
"cmake_build"
166 build_dir.mkdir(parents=
True)
167 (build_dir /
"build.ninja").touch()
168 v._cmake_dirty =
False
170 with mock.patch.object(v,
"_run_compile_command", return_value=0)
as run:
171 configure = v.compile_commands()[2]
172 assert configure() == 0
173 assert configure() == 0
175 assert run.call_count == 1
176 assert (build_dir / Verilator._CMakeSignatureFilename).exists()
181 build_dir = tmp_path /
"obj_dir" /
"cmake_build"
182 build_dir.mkdir(parents=
True)
183 (build_dir /
"build.ninja").touch()
185 with mock.patch.object(v,
"_run_compile_command", return_value=0)
as run:
186 configure = v.compile_commands()[2]
187 assert configure() == 0
189 v._cmake_dirty =
True
190 assert configure() == 0
192 run.assert_not_called()
196 build_dir = tmp_path /
"obj_dir" /
"cmake_build"
197 build_dir.mkdir(parents=
True)
198 (build_dir /
"build.ninja").touch()
199 v._cmake_dirty =
False
201 with mock.patch.object(v,
"_run_compile_command", return_value=0)
as run:
202 monkeypatch.setenv(
"CXX",
"first-cxx")
203 assert v.compile_commands()[2]() == 0
204 monkeypatch.setenv(
"CXX",
"second-cxx")
205 assert v.compile_commands()[2]() == 0
207 assert run.call_count == 2
208 assert run.call_args_list[0].args[0] == run.call_args_list[1].args[0]
212 build_dir = tmp_path /
"obj_dir" /
"cmake_build"
213 build_dir.mkdir(parents=
True)
214 (build_dir /
"build.ninja").touch()
215 v._cmake_dirty =
False
216 signature_file = build_dir / Verilator._CMakeSignatureFilename
218 with mock.patch.object(v,
"_run_compile_command",
219 side_effect=(1, 0))
as run:
220 configure = v.compile_commands()[2]
221 assert configure() == 1
222 assert not signature_file.exists()
223 assert configure() == 0
225 assert run.call_count == 2
226 assert signature_file.exists()
228 @requires_verilator_bin
230 """When using cmake, --exe and --build should not appear."""
233 pytest.skip(
"cmake+ninja not available")
234 cmd = v.compile_commands()[0]
235 assert "--exe" not in cmd
236 assert "--build" not in cmd
238 @requires_verilator_bin
240 """When using cmake, -CFLAGS and -LDFLAGS should not appear."""
243 pytest.skip(
"cmake+ninja not available")
244 cmd = v.compile_commands()[0]
245 assert "-CFLAGS" not in cmd
246 assert "-LDFLAGS" not in cmd
248 @requires_verilator_bin
250 """When using cmake, driver.cpp should not be in the verilator command."""
253 pytest.skip(
"cmake+ninja not available")
254 cmd = v.compile_commands()[0]
255 assert not any(
"driver.cpp" in str(c)
for c
in cmd)
257 @requires_verilator_bin
260 cmd = v.compile_commands()[0]
261 assert "--trace-fst" in cmd
262 assert "--trace-structs" in cmd
263 assert "--trace-underscore" in cmd
266 fake_bin = tmp_path /
"custom" /
"verilator_bin"
267 fake_bin.parent.mkdir()
269 with mock.patch.dict(os.environ, {
"VERILATOR_PATH": str(fake_bin)}):
271 assert v.verilator_bin == fake_bin.resolve()
274 fake_wrapper = tmp_path /
"usr" /
"bin" /
"verilator"
275 fake_bin = fake_wrapper.parent /
"verilator_bin"
276 fake_wrapper.parent.mkdir(parents=
True)
279 with mock.patch.dict(os.environ, {
"VERILATOR_PATH": str(fake_wrapper)}):
281 assert v.verilator_bin == fake_bin.resolve()
284 env_root = tmp_path /
"env-verilator"
285 path_root = tmp_path /
"path-verilator"
286 (env_root /
"bin").mkdir(parents=
True)
287 (path_root /
"bin").mkdir(parents=
True)
288 env_bin = env_root /
"bin" /
"verilator_bin"
289 path_bin = path_root /
"bin" /
"verilator_bin"
293 with mock.patch.dict(os.environ, {
"VERILATOR_PATH": str(env_bin)}):
294 with mock.patch(
"shutil.which", return_value=str(path_bin)):
295 assert Verilator._find_verilator_bin() == env_bin.resolve()
298 with mock.patch.dict(os.environ, {}, clear=
False):
299 os.environ.pop(
"VERILATOR_ROOT",
None)
300 os.environ.pop(
"VERILATOR_PATH",
None)
301 with mock.patch(
"shutil.which", return_value=
None):
303 with pytest.raises(RuntimeError, match=
"Cannot find verilator_bin"):
306 @requires_verilator_bin
309 cmd = v.compile_commands()[0]
310 assert "+define+FOO=BAR" in cmd
311 assert "+define+BAZ" in cmd
314@requires_verilator_bin
316 """Tests for the make fallback when cmake/ninja are not available."""
319 """Create a Verilator instance that thinks cmake/ninja are missing."""
323 @pytest.fixture(autouse=True)
325 """Patch shutil.which so cmake and ninja appear absent."""
326 original_which = shutil.which
328 def _which_no_cmake(name, *args, **kwargs):
329 if name
in (
"cmake",
"ninja"):
331 return original_which(name, *args, **kwargs)
333 with mock.patch(
"shutil.which", side_effect=_which_no_cmake):
338 cmds = v.compile_commands()
339 assert len(cmds) == 2
340 assert cmds[1][0] ==
"make"
344 cmd = v.compile_commands()[0]
345 assert "--exe" in cmd
349 cmd = v.compile_commands()[0]
350 assert "-CFLAGS" in cmd
351 idx = cmd.index(
"-CFLAGS")
352 assert "-DTOP_MODULE=TestTop" in cmd[idx + 1]
356 cmd = v.compile_commands()[0]
357 assert any(
"driver.cpp" in str(c)
for c
in cmd)
361 cmd = v.compile_commands()[0]
362 assert "-LDFLAGS" in cmd
363 idx = cmd.index(
"-LDFLAGS")
364 assert "-lEsiCosimDpiServer" in cmd[idx + 1]
368 cmd = v.compile_commands()[0]
369 assert "-LDFLAGS" not in cmd
373 cmd = v.compile_commands()[0]
374 idx = cmd.index(
"-CFLAGS")
375 assert "-DTRACE" in cmd[idx + 1]
379 cmds = v.compile_commands()
381 assert make_cmd[0] ==
"make"
382 assert "-C" in make_cmd
383 assert "obj_dir" in make_cmd
384 assert "-f" in make_cmd
385 assert "VMyTop.mk" in make_cmd
389 exe_name =
"VMyTop.exe" if os.name ==
"nt" else "VMyTop"
390 with mock.patch.object(Path,
"cwd", return_value=tmp_path):
391 cmd = v.run_command(gui=
False)
392 assert cmd == [str(tmp_path /
"obj_dir" / exe_name)]
398 root = tmp_path /
"verilator"
400 (root /
"include").mkdir()
401 (root /
"include" /
"verilated.h").touch()
402 with mock.patch.dict(os.environ, {
"VERILATOR_ROOT": str(root)}):
404 assert v._find_verilator_root() == root
407 root = tmp_path /
"verilator"
408 (root /
"bin").mkdir(parents=
True)
409 pkg_root = root /
"share" /
"verilator"
410 (pkg_root /
"include").mkdir(parents=
True)
411 (pkg_root /
"include" /
"verilated.h").touch()
412 fake_bin = root /
"bin" /
"verilator_bin"
414 fake_bin.chmod(0o755)
415 with mock.patch.dict(os.environ, {}, clear=
False):
418 os.environ.pop(
"VERILATOR_ROOT",
None)
419 os.environ.pop(
"VERILATOR_PATH",
None)
420 with mock.patch(
"shutil.which", return_value=str(fake_bin)):
422 found = v._find_verilator_root()
423 assert found == pkg_root
426 with mock.patch.dict(os.environ, {}, clear=
False):
429 os.environ.pop(
"VERILATOR_ROOT",
None)
430 os.environ.pop(
"VERILATOR_PATH",
None)
431 with mock.patch(
"shutil.which", return_value=
None):
433 assert v._find_verilator_root()
is None
436 root = tmp_path /
"verilator"
438 with mock.patch.dict(os.environ, {
"VERILATOR_ROOT": str(root)}):
440 with pytest.raises(RuntimeError, match=
"VERILATOR_ROOT"):
441 v._find_verilator_root()
447 obj_dir = tmp_path /
"obj_dir"
449 generated_sources = [obj_dir /
"VTestTop.cpp"]
450 root = tmp_path /
"verilator"
451 (root /
"include").mkdir(parents=
True)
452 (root /
"include" /
"verilated.h").touch()
453 with mock.patch.dict(os.environ, {
"VERILATOR_ROOT": str(root)}):
455 build_dir = v._write_cmake(obj_dir, generated_sources)
456 assert (build_dir /
"CMakeLists.txt").exists()
457 content = (build_dir /
"CMakeLists.txt").read_text()
458 assert "VTestTop" in content
459 assert generated_sources[0].as_posix()
in content
460 assert "verilated.cpp" in content
461 assert "verilated_threads.cpp" in content
462 assert "driver.cpp" in content
465 obj_dir = tmp_path /
"obj_dir"
467 generated_sources = [obj_dir /
"VTestTop.cpp"]
468 root = tmp_path /
"verilator"
469 (root /
"include").mkdir(parents=
True)
470 (root /
"include" /
"verilated.h").touch()
471 with mock.patch.dict(os.environ, {
"VERILATOR_ROOT": str(root)}):
473 build_dir = v._write_cmake(obj_dir, generated_sources)
474 content = (build_dir /
"CMakeLists.txt").read_text()
475 assert "verilated_fst_c.cpp" in content
476 assert "TRACE" in content
478 @pytest.mark.parametrize(
479 (
"filename",
"expected"),
481 (
"VTestTop___024root__Slow.cpp",
True),
482 (
"VTestTop__Syms__Slow.cpp",
True),
483 (
"VTestTop__Syms__ctor__0__Slow.cpp",
True),
484 (
"VTestTop__ConstPool__0__Slow.cpp",
True),
485 (
"VTestTop.cpp",
False),
486 (
"VTestTop___024root.cpp",
False),
490 assert Verilator._is_slow(Path(filename))
is expected
493 obj_dir = tmp_path /
"obj_dir"
495 generated_sources = [
496 obj_dir /
"VTestTop.cpp", obj_dir /
"VTestTop__Slow.cpp"
498 pch_header = obj_dir /
"VTestTop__pch.h"
499 root = tmp_path /
"verilator"
500 (root /
"include").mkdir(parents=
True)
501 (root /
"include" /
"verilated.h").touch()
502 with mock.patch.dict(os.environ, {
"VERILATOR_ROOT": str(root)}):
504 build_dir = v._write_cmake(obj_dir, generated_sources, pch_header)
505 content = (build_dir /
"CMakeLists.txt").read_text()
506 assert "target_precompile_headers(vl_fast PRIVATE" in content
507 assert "target_precompile_headers(vl_slow PRIVATE" in content
508 assert "target_precompile_headers(VTestTop PRIVATE" not in content
509 assert "VTestTop__pch.h" in content
510 assert "SKIP_PRECOMPILE_HEADERS ON" not in content
511 assert "verilated.cpp" in content
512 assert "driver.cpp" in content
520 pytest.skip(
"cmake+ninja not available")
521 exe_name =
"VMyTop.exe" if os.name ==
"nt" else "VMyTop"
522 with mock.patch.object(Path,
"cwd", return_value=tmp_path):
523 cmd = v.run_command(gui=
False)
524 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_configure_runs_when_environment_changes(self, tmp_path, monkeypatch)
test_driver_not_in_verilator_cmd_cmake(self, tmp_path)
test_configure_skips_when_inputs_unchanged(self, tmp_path, monkeypatch)
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, monkeypatch)
test_configure_leaves_cmakelists_changes_to_ninja(self, tmp_path, monkeypatch)
test_failed_configure_is_not_cached(self, tmp_path, monkeypatch)
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_classifies_slow_generated_sources(self, filename, expected)
test_enables_pch_for_generated_source_groups(self, tmp_path)
_make_cmake_verilator(tmp_path, monkeypatch)
_make_verilator(run_dir, top="TestTop", debug=False, dpi_so=None, macros=None)