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.
13from pathlib
import Path
14from unittest
import mock
15from unittest.mock
import MagicMock
23_accel_mock = MagicMock()
24sys.modules[
"esiaccel.esiCppAccel"] = _accel_mock
27from esiaccel.cosim.verilator
import Verilator
28from esiaccel.cosim.simulator
import (
31 is_simulator_available,
40 """Create a Verilator instance with minimal setup."""
41 sources = SourceFiles(top)
42 if dpi_so
is not None:
43 sources.dpi_so = dpi_so
48 make_default_logs=
False,
49 macro_definitions=macros,
54 root = tmp_path /
"verilator"
55 fake_bin = root /
"bin" /
"verilator_bin"
56 fake_bin.parent.mkdir(parents=
True)
58 (root /
"include").mkdir()
59 (root /
"include" /
"verilated.h").touch()
60 monkeypatch.setenv(
"VERILATOR_PATH", str(fake_bin))
61 monkeypatch.setenv(
"VERILATOR_ROOT", str(root))
62 monkeypatch.chdir(tmp_path)
63 monkeypatch.setattr(Verilator,
"_use_cmake", property(
lambda self:
True))
64 monkeypatch.setattr(Verilator,
"_raise_stack_limit",
65 staticmethod(
lambda:
None))
69requires_verilator_bin = pytest.mark.skipif(
70 not is_simulator_available(
"verilator"), reason=
"verilator not found")
76 with pytest.raises(ValueError):
77 is_simulator_available(
"bogus")
80 monkeypatch.delenv(
"VERILATOR_PATH", raising=
False)
81 monkeypatch.delenv(
"VERILATOR_ROOT", raising=
False)
82 monkeypatch.setattr(shutil,
"which",
lambda name:
None)
83 assert not is_simulator_available(
"verilator")
84 assert "verilator" not in available_simulators()
87 root = tmp_path /
"verilator"
88 (root /
"bin").mkdir(parents=
True)
89 pkg_root = root /
"share" /
"verilator"
90 (pkg_root /
"include").mkdir(parents=
True)
91 (pkg_root /
"include" /
"verilated.h").touch()
92 fake_bin = root /
"bin" /
"verilator_bin"
95 monkeypatch.setenv(
"VERILATOR_PATH", str(fake_bin))
96 monkeypatch.delenv(
"VERILATOR_ROOT", raising=
False)
97 monkeypatch.setattr(shutil,
"which",
lambda name:
None)
98 assert is_simulator_available(
"verilator")
99 assert "verilator" in available_simulators()
102 monkeypatch.setenv(
"VERILATOR_PATH",
103 str(tmp_path /
"missing" /
"verilator_bin"))
104 monkeypatch.delenv(
"VERILATOR_ROOT", raising=
False)
105 monkeypatch.setattr(shutil,
"which",
lambda name:
None)
106 with pytest.raises(RuntimeError, match=
"VERILATOR_PATH"):
107 is_simulator_available(
"verilator")
110 root = tmp_path /
"verilator"
111 (root /
"bin").mkdir(parents=
True)
112 pkg_root = root /
"share" /
"verilator"
113 pkg_root.mkdir(parents=
True)
114 fake_bin = root /
"bin" /
"verilator_bin"
117 monkeypatch.setenv(
"VERILATOR_PATH", str(fake_bin))
118 monkeypatch.setenv(
"VERILATOR_ROOT", str(pkg_root))
119 monkeypatch.setattr(shutil,
"which",
lambda name:
None)
120 with pytest.raises(RuntimeError, match=
"VERILATOR_ROOT"):
121 is_simulator_available(
"verilator")
124 monkeypatch.setattr(shutil,
"which",
lambda name:
None)
125 assert not is_simulator_available(
"questa")
128 monkeypatch.delenv(
"VERILATOR_PATH", raising=
False)
129 monkeypatch.delenv(
"VERILATOR_ROOT", raising=
False)
133 return "C:/questa/vsim.exe"
136 monkeypatch.setattr(shutil,
"which", _which)
137 assert is_simulator_available(
"questa")
138 assert available_simulators() == [
"questa"]
143 @requires_verilator_bin
146 cmds = v.compile_commands()
147 assert Path(cmds[0][0]).stem ==
"verilator_bin"
148 assert Path(cmds[0][0]) == v.verilator_bin
152 build_dir = tmp_path /
"obj_dir" /
"cmake_build"
153 build_dir.mkdir(parents=
True)
154 with mock.patch.object(v,
"_run_compile_command", return_value=0)
as run:
155 cmds = v.compile_commands()
156 assert len(cmds) == 4
157 assert callable(cmds[1])
158 assert callable(cmds[2])
159 assert cmds[2]() == 0
160 assert cmds[3][0] ==
"ninja"
161 cmake_cmd = run.call_args.args[0]
162 assert cmake_cmd[0] ==
"cmake"
163 assert "-G" in cmake_cmd
and "Ninja" in cmake_cmd
167 build_dir = tmp_path /
"obj_dir" /
"cmake_build"
168 build_dir.mkdir(parents=
True)
169 (build_dir /
"build.ninja").touch()
170 v._cmake_dirty =
False
172 with mock.patch.object(v,
"_run_compile_command", return_value=0)
as run:
173 configure = v.compile_commands()[2]
174 assert configure() == 0
175 assert configure() == 0
177 assert run.call_count == 1
178 assert (build_dir / Verilator._CMakeSignatureFilename).exists()
183 build_dir = tmp_path /
"obj_dir" /
"cmake_build"
184 build_dir.mkdir(parents=
True)
185 (build_dir /
"build.ninja").touch()
187 with mock.patch.object(v,
"_run_compile_command", return_value=0)
as run:
188 configure = v.compile_commands()[2]
189 assert configure() == 0
191 v._cmake_dirty =
True
192 assert configure() == 0
194 run.assert_not_called()
198 build_dir = tmp_path /
"obj_dir" /
"cmake_build"
199 build_dir.mkdir(parents=
True)
200 (build_dir /
"build.ninja").touch()
201 v._cmake_dirty =
False
203 with mock.patch.object(v,
"_run_compile_command", return_value=0)
as run:
204 monkeypatch.setenv(
"CXX",
"first-cxx")
205 assert v.compile_commands()[2]() == 0
206 monkeypatch.setenv(
"CXX",
"second-cxx")
207 assert v.compile_commands()[2]() == 0
209 assert run.call_count == 2
210 assert run.call_args_list[0].args[0] == run.call_args_list[1].args[0]
214 build_dir = tmp_path /
"obj_dir" /
"cmake_build"
215 build_dir.mkdir(parents=
True)
216 (build_dir /
"build.ninja").touch()
217 v._cmake_dirty =
False
218 signature_file = build_dir / Verilator._CMakeSignatureFilename
220 with mock.patch.object(v,
"_run_compile_command",
221 side_effect=(1, 0))
as run:
222 configure = v.compile_commands()[2]
223 assert configure() == 1
224 assert not signature_file.exists()
225 assert configure() == 0
227 assert run.call_count == 2
228 assert signature_file.exists()
230 @requires_verilator_bin
232 """When using cmake, --exe and --build should not appear."""
235 pytest.skip(
"cmake+ninja not available")
236 cmd = v.compile_commands()[0]
237 assert "--exe" not in cmd
238 assert "--build" not in cmd
240 @requires_verilator_bin
242 """When using cmake, -CFLAGS and -LDFLAGS should not appear."""
245 pytest.skip(
"cmake+ninja not available")
246 cmd = v.compile_commands()[0]
247 assert "-CFLAGS" not in cmd
248 assert "-LDFLAGS" not in cmd
250 @requires_verilator_bin
252 """When using cmake, driver.cpp should not be in the verilator command."""
255 pytest.skip(
"cmake+ninja not available")
256 cmd = v.compile_commands()[0]
257 assert not any(
"driver.cpp" in str(c)
for c
in cmd)
259 @requires_verilator_bin
262 cmd = v.compile_commands()[0]
263 assert "--trace-fst" in cmd
264 assert "--trace-structs" in cmd
265 assert "--trace-underscore" in cmd
268 fake_bin = tmp_path /
"custom" /
"verilator_bin"
269 fake_bin.parent.mkdir()
271 with mock.patch.dict(os.environ, {
"VERILATOR_PATH": str(fake_bin)}):
273 assert v.verilator_bin == fake_bin.resolve()
276 fake_wrapper = tmp_path /
"usr" /
"bin" /
"verilator"
277 fake_bin = fake_wrapper.parent /
"verilator_bin"
278 fake_wrapper.parent.mkdir(parents=
True)
281 with mock.patch.dict(os.environ, {
"VERILATOR_PATH": str(fake_wrapper)}):
283 assert v.verilator_bin == fake_bin.resolve()
286 env_root = tmp_path /
"env-verilator"
287 path_root = tmp_path /
"path-verilator"
288 (env_root /
"bin").mkdir(parents=
True)
289 (path_root /
"bin").mkdir(parents=
True)
290 env_bin = env_root /
"bin" /
"verilator_bin"
291 path_bin = path_root /
"bin" /
"verilator_bin"
295 with mock.patch.dict(os.environ, {
"VERILATOR_PATH": str(env_bin)}):
296 with mock.patch(
"shutil.which", return_value=str(path_bin)):
297 assert Verilator._find_verilator_bin() == env_bin.resolve()
300 with mock.patch.dict(os.environ, {}, clear=
False):
301 os.environ.pop(
"VERILATOR_ROOT",
None)
302 os.environ.pop(
"VERILATOR_PATH",
None)
303 with mock.patch(
"shutil.which", return_value=
None):
305 with pytest.raises(RuntimeError, match=
"Cannot find verilator_bin"):
308 @requires_verilator_bin
311 cmd = v.compile_commands()[0]
312 assert "+define+FOO=BAR" in cmd
313 assert "+define+BAZ" in cmd
315 @pytest.mark.parametrize("name", ("verilator", "questa"))
317 sources = SourceFiles(
"TestTop")
318 simulator = get_simulator(name,
326 assert simulator.macro_definitions == {
"FOO":
"BAR",
"BAZ":
None}
332 for name
in (
"A.sv",
"zPkg.sv",
"B.v",
"notes.txt"):
333 (tmp_path / name).touch()
334 (tmp_path /
"sub").mkdir()
335 (tmp_path /
"sub" /
"C.sv").touch()
336 (tmp_path /
"filelist.f").write_text(
"zPkg.sv\n\nB.v\nmissing.sv\n")
338 sources = SourceFiles(
"TestTop")
339 sources.add_dir(tmp_path)
340 assert sources.user == [
341 tmp_path /
"zPkg.sv", tmp_path /
"B.v", tmp_path /
"A.sv",
342 tmp_path /
"sub" /
"C.sv"
347 for name
in (
"b.sv",
"A.sv"):
348 (tmp_path / name).touch()
349 sources = SourceFiles(
"TestTop")
350 sources.add_dir(tmp_path)
351 assert sources.user == [tmp_path /
"A.sv", tmp_path /
"b.sv"]
355 script = (Path(__file__).parents[2] /
"cosim_dpi_server" /
"esi-cosim.py")
356 script_globals = runpy.run_path(str(script))
357 main = script_globals[
"__main__"]
358 simulator = MagicMock()
359 simulator.run.return_value = 0
360 sources = MagicMock()
361 source_files_factory = MagicMock(return_value=sources)
362 simulator_factory = MagicMock(return_value=simulator)
364 with mock.patch.dict(main.__globals__, {
365 "SourceFiles": source_files_factory,
366 "get_simulator": simulator_factory,
369 "esi-cosim",
"-DFOO=BAR",
"--define",
"BAZ",
"--source",
370 str(tmp_path),
"--no-compile",
"--server-only"
373 simulator_factory.assert_called_once_with(
"verilator",
384@requires_verilator_bin
386 """Tests for the make fallback when cmake/ninja are not available."""
389 """Create a Verilator instance that thinks cmake/ninja are missing."""
393 @pytest.fixture(autouse=True)
395 """Patch shutil.which so cmake and ninja appear absent."""
396 original_which = shutil.which
398 def _which_no_cmake(name, *args, **kwargs):
399 if name
in (
"cmake",
"ninja"):
401 return original_which(name, *args, **kwargs)
403 with mock.patch(
"shutil.which", side_effect=_which_no_cmake):
408 cmds = v.compile_commands()
409 assert len(cmds) == 2
410 assert cmds[1][0] ==
"make"
414 cmd = v.compile_commands()[0]
415 assert "--exe" in cmd
419 cmd = v.compile_commands()[0]
420 assert "-CFLAGS" in cmd
421 idx = cmd.index(
"-CFLAGS")
422 assert "-DTOP_MODULE=TestTop" in cmd[idx + 1]
426 cmd = v.compile_commands()[0]
427 assert any(
"driver.cpp" in str(c)
for c
in cmd)
431 cmd = v.compile_commands()[0]
432 assert "-LDFLAGS" in cmd
433 idx = cmd.index(
"-LDFLAGS")
434 assert "-lEsiCosimDpiServer" in cmd[idx + 1]
438 cmd = v.compile_commands()[0]
439 assert "-LDFLAGS" not in cmd
443 cmd = v.compile_commands()[0]
444 idx = cmd.index(
"-CFLAGS")
445 assert "-DTRACE" in cmd[idx + 1]
449 cmds = v.compile_commands()
451 assert make_cmd[0] ==
"make"
452 assert "-C" in make_cmd
453 assert "obj_dir" in make_cmd
454 assert "-f" in make_cmd
455 assert "VMyTop.mk" in make_cmd
459 exe_name =
"VMyTop.exe" if os.name ==
"nt" else "VMyTop"
460 with mock.patch.object(Path,
"cwd", return_value=tmp_path):
461 cmd = v.run_command(gui=
False)
462 assert cmd == [str(tmp_path /
"obj_dir" / exe_name)]
468 root = tmp_path /
"verilator"
470 (root /
"include").mkdir()
471 (root /
"include" /
"verilated.h").touch()
472 with mock.patch.dict(os.environ, {
"VERILATOR_ROOT": str(root)}):
474 assert v._find_verilator_root() == root
477 root = tmp_path /
"verilator"
478 (root /
"bin").mkdir(parents=
True)
479 pkg_root = root /
"share" /
"verilator"
480 (pkg_root /
"include").mkdir(parents=
True)
481 (pkg_root /
"include" /
"verilated.h").touch()
482 fake_bin = root /
"bin" /
"verilator_bin"
484 fake_bin.chmod(0o755)
485 with mock.patch.dict(os.environ, {}, clear=
False):
488 os.environ.pop(
"VERILATOR_ROOT",
None)
489 os.environ.pop(
"VERILATOR_PATH",
None)
490 with mock.patch(
"shutil.which", return_value=str(fake_bin)):
492 found = v._find_verilator_root()
493 assert found == pkg_root
496 with mock.patch.dict(os.environ, {}, clear=
False):
499 os.environ.pop(
"VERILATOR_ROOT",
None)
500 os.environ.pop(
"VERILATOR_PATH",
None)
501 with mock.patch(
"shutil.which", return_value=
None):
503 assert v._find_verilator_root()
is None
506 root = tmp_path /
"verilator"
508 with mock.patch.dict(os.environ, {
"VERILATOR_ROOT": str(root)}):
510 with pytest.raises(RuntimeError, match=
"VERILATOR_ROOT"):
511 v._find_verilator_root()
517 obj_dir = tmp_path /
"obj_dir"
519 generated_sources = [obj_dir /
"VTestTop.cpp"]
520 root = tmp_path /
"verilator"
521 (root /
"include").mkdir(parents=
True)
522 (root /
"include" /
"verilated.h").touch()
523 with mock.patch.dict(os.environ, {
"VERILATOR_ROOT": str(root)}):
525 build_dir = v._write_cmake(obj_dir, generated_sources)
526 assert (build_dir /
"CMakeLists.txt").exists()
527 content = (build_dir /
"CMakeLists.txt").read_text()
528 assert "VTestTop" in content
529 assert generated_sources[0].as_posix()
in content
530 assert "verilated.cpp" in content
531 assert "verilated_threads.cpp" in content
532 assert "driver.cpp" in content
535 obj_dir = tmp_path /
"obj_dir"
537 generated_sources = [obj_dir /
"VTestTop.cpp"]
538 root = tmp_path /
"verilator"
539 (root /
"include").mkdir(parents=
True)
540 (root /
"include" /
"verilated.h").touch()
541 with mock.patch.dict(os.environ, {
"VERILATOR_ROOT": str(root)}):
543 build_dir = v._write_cmake(obj_dir, generated_sources)
544 content = (build_dir /
"CMakeLists.txt").read_text()
545 assert "verilated_fst_c.cpp" in content
546 assert "TRACE" in content
548 @pytest.mark.parametrize(
549 (
"filename",
"expected"),
551 (
"VTestTop___024root__Slow.cpp",
True),
552 (
"VTestTop__Syms__Slow.cpp",
True),
553 (
"VTestTop__Syms__ctor__0__Slow.cpp",
True),
554 (
"VTestTop__ConstPool__0__Slow.cpp",
True),
555 (
"VTestTop.cpp",
False),
556 (
"VTestTop___024root.cpp",
False),
560 assert Verilator._is_slow(Path(filename))
is expected
563 obj_dir = tmp_path /
"obj_dir"
565 generated_sources = [
566 obj_dir /
"VTestTop.cpp", obj_dir /
"VTestTop__Slow.cpp"
568 pch_header = obj_dir /
"VTestTop__pch.h"
569 root = tmp_path /
"verilator"
570 (root /
"include").mkdir(parents=
True)
571 (root /
"include" /
"verilated.h").touch()
572 with mock.patch.dict(os.environ, {
"VERILATOR_ROOT": str(root)}):
574 build_dir = v._write_cmake(obj_dir, generated_sources, pch_header)
575 content = (build_dir /
"CMakeLists.txt").read_text()
576 assert "target_precompile_headers(vl_fast PRIVATE" in content
577 assert "target_precompile_headers(vl_slow PRIVATE" in content
578 assert "target_precompile_headers(VTestTop PRIVATE" not in content
579 assert "VTestTop__pch.h" in content
580 assert "SKIP_PRECOMPILE_HEADERS ON" not in content
581 assert "verilated.cpp" in content
582 assert "driver.cpp" in content
590 pytest.skip(
"cmake+ninja not available")
591 exe_name =
"VMyTop.exe" if os.name ==
"nt" else "VMyTop"
592 with mock.patch.object(Path,
"cwd", return_value=tmp_path):
593 cmd = v.run_command(gui=
False)
594 assert cmd == [str(tmp_path /
"obj_dir" /
"cmake_build" / exe_name)]
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_get_simulator_macro_definitions(self, tmp_path, name)
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)
test_esi_cosim_macro_definitions(tmp_path)
test_source_files_add_dir_without_filelist(tmp_path)
test_source_files_add_dir_uses_filelist_order(tmp_path)
_make_cmake_verilator(tmp_path, monkeypatch)
_make_verilator(run_dir, top="TestTop", debug=False, dpi_so=None, macros=None)