CIRCT 24.0.0git
Loading...
Searching...
No Matches
test_verilator.py
Go to the documentation of this file.
1"""Unit tests for the Verilator cosim backend.
2
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.
7"""
8
9import os
10import shutil
11import sys
12from pathlib import Path
13from unittest import mock
14from unittest.mock import MagicMock
15
16import pytest
17
18# ---------------------------------------------------------------------------
19# Provide a comprehensive mock for the native extension so we can import the
20# pure-Python cosim modules without a full C++ build.
21# ---------------------------------------------------------------------------
22_accel_mock = MagicMock()
23sys.modules["esiaccel.esiCppAccel"] = _accel_mock
24
25# Now we can safely import the cosim modules.
26from esiaccel.cosim.verilator import Verilator # noqa: E402
27from esiaccel.cosim.simulator import (
28 available_simulators, # noqa: E402
29 is_simulator_available,
30 SourceFiles)
31
32
33def _make_verilator(run_dir,
34 top="TestTop",
35 debug=False,
36 dpi_so=None,
37 macros=None):
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
42 return Verilator(
43 sources=sources,
44 run_dir=run_dir,
45 debug=debug,
46 make_default_logs=False,
47 macro_definitions=macros,
48 )
49
50
51def _make_cmake_verilator(tmp_path, monkeypatch):
52 root = tmp_path / "verilator"
53 fake_bin = root / "bin" / "verilator_bin"
54 fake_bin.parent.mkdir(parents=True)
55 fake_bin.touch()
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))
64 return _make_verilator(tmp_path)
65
66
67requires_verilator_bin = pytest.mark.skipif(
68 not is_simulator_available("verilator"), reason="verilator not found")
69
70
72
74 with pytest.raises(ValueError):
75 is_simulator_available("bogus")
76
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()
83
84 def test_verilator_available_from_env_path(self, monkeypatch, tmp_path):
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"
91 fake_bin.touch()
92
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()
98
99 def test_invalid_verilator_path_env_raises(self, monkeypatch, tmp_path):
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")
106
107 def test_invalid_verilator_root_env_raises(self, monkeypatch, tmp_path):
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"
113 fake_bin.touch()
114
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")
120
122 monkeypatch.setattr(shutil, "which", lambda name: None)
123 assert not is_simulator_available("questa")
124
125 def test_questa_available_from_path(self, monkeypatch):
126 monkeypatch.delenv("VERILATOR_PATH", raising=False)
127 monkeypatch.delenv("VERILATOR_ROOT", raising=False)
128
129 def _which(name):
130 if name == "vsim":
131 return "C:/questa/vsim.exe"
132 return None
133
134 monkeypatch.setattr(shutil, "which", _which)
135 assert is_simulator_available("questa")
136 assert available_simulators() == ["questa"]
137
138
140
141 @requires_verilator_bin
142 def test_uses_verilator_bin(self, tmp_path):
143 v = _make_verilator(tmp_path)
144 cmds = v.compile_commands()
145 assert Path(cmds[0][0]).stem == "verilator_bin"
146 assert Path(cmds[0][0]) == v.verilator_bin
147
148 def test_cmake_and_ninja_commands(self, tmp_path, monkeypatch):
149 v = _make_cmake_verilator(tmp_path, monkeypatch)
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
162
163 def test_configure_skips_when_inputs_unchanged(self, tmp_path, monkeypatch):
164 v = _make_cmake_verilator(tmp_path, monkeypatch)
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
169
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
174
175 assert run.call_count == 1
176 assert (build_dir / Verilator._CMakeSignatureFilename).exists()
177
179 monkeypatch):
180 v = _make_cmake_verilator(tmp_path, monkeypatch)
181 build_dir = tmp_path / "obj_dir" / "cmake_build"
182 build_dir.mkdir(parents=True)
183 (build_dir / "build.ninja").touch()
184
185 with mock.patch.object(v, "_run_compile_command", return_value=0) as run:
186 configure = v.compile_commands()[2]
187 assert configure() == 0
188 run.reset_mock()
189 v._cmake_dirty = True
190 assert configure() == 0
191
192 run.assert_not_called()
193
194 def test_configure_runs_when_environment_changes(self, tmp_path, monkeypatch):
195 v = _make_cmake_verilator(tmp_path, monkeypatch)
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
200
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
206
207 assert run.call_count == 2
208 assert run.call_args_list[0].args[0] == run.call_args_list[1].args[0]
209
210 def test_failed_configure_is_not_cached(self, tmp_path, monkeypatch):
211 v = _make_cmake_verilator(tmp_path, monkeypatch)
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
217
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
224
225 assert run.call_count == 2
226 assert signature_file.exists()
227
228 @requires_verilator_bin
230 """When using cmake, --exe and --build should not appear."""
231 v = _make_verilator(tmp_path)
232 if not v._use_cmake:
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
237
238 @requires_verilator_bin
240 """When using cmake, -CFLAGS and -LDFLAGS should not appear."""
241 v = _make_verilator(tmp_path)
242 if not v._use_cmake:
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
247
248 @requires_verilator_bin
250 """When using cmake, driver.cpp should not be in the verilator command."""
251 v = _make_verilator(tmp_path)
252 if not v._use_cmake:
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)
256
257 @requires_verilator_bin
258 def test_trace_flags_in_debug(self, tmp_path):
259 v = _make_verilator(tmp_path, debug=True)
260 cmd = v.compile_commands()[0]
261 assert "--trace-fst" in cmd
262 assert "--trace-structs" in cmd
263 assert "--trace-underscore" in cmd
264
266 fake_bin = tmp_path / "custom" / "verilator_bin"
267 fake_bin.parent.mkdir()
268 fake_bin.touch()
269 with mock.patch.dict(os.environ, {"VERILATOR_PATH": str(fake_bin)}):
270 v = _make_verilator(tmp_path)
271 assert v.verilator_bin == fake_bin.resolve()
272
274 fake_wrapper = tmp_path / "usr" / "bin" / "verilator"
275 fake_bin = fake_wrapper.parent / "verilator_bin"
276 fake_wrapper.parent.mkdir(parents=True)
277 fake_wrapper.touch()
278 fake_bin.touch()
279 with mock.patch.dict(os.environ, {"VERILATOR_PATH": str(fake_wrapper)}):
280 v = _make_verilator(tmp_path)
281 assert v.verilator_bin == fake_bin.resolve()
282
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"
290 env_bin.touch()
291 path_bin.touch()
292
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()
296
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):
302 v = _make_verilator(tmp_path)
303 with pytest.raises(RuntimeError, match="Cannot find verilator_bin"):
304 v.compile_commands()
305
306 @requires_verilator_bin
307 def test_macro_definitions(self, tmp_path):
308 v = _make_verilator(tmp_path, macros={"FOO": "BAR", "BAZ": None})
309 cmd = v.compile_commands()[0]
310 assert "+define+FOO=BAR" in cmd
311 assert "+define+BAZ" in cmd
312
313
314@requires_verilator_bin
316 """Tests for the make fallback when cmake/ninja are not available."""
317
318 def _make_no_cmake(self, tmp_path, **kwargs):
319 """Create a Verilator instance that thinks cmake/ninja are missing."""
320 v = _make_verilator(tmp_path, **kwargs)
321 return v
322
323 @pytest.fixture(autouse=True)
324 def _hide_cmake(self):
325 """Patch shutil.which so cmake and ninja appear absent."""
326 original_which = shutil.which
327
328 def _which_no_cmake(name, *args, **kwargs):
329 if name in ("cmake", "ninja"):
330 return None
331 return original_which(name, *args, **kwargs)
332
333 with mock.patch("shutil.which", side_effect=_which_no_cmake):
334 yield
335
336 def test_fallback_uses_make(self, tmp_path):
337 v = self._make_no_cmake(tmp_path)
338 cmds = v.compile_commands()
339 assert len(cmds) == 2
340 assert cmds[1][0] == "make"
341
342 def test_fallback_has_exe_flag(self, tmp_path):
343 v = self._make_no_cmake(tmp_path)
344 cmd = v.compile_commands()[0]
345 assert "--exe" in cmd
346
347 def test_fallback_has_cflags(self, tmp_path):
348 v = self._make_no_cmake(tmp_path)
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]
353
354 def test_fallback_has_driver(self, tmp_path):
355 v = self._make_no_cmake(tmp_path)
356 cmd = v.compile_commands()[0]
357 assert any("driver.cpp" in str(c) for c in cmd)
358
360 v = self._make_no_cmake(tmp_path, dpi_so=["EsiCosimDpiServer"])
361 cmd = v.compile_commands()[0]
362 assert "-LDFLAGS" in cmd
363 idx = cmd.index("-LDFLAGS")
364 assert "-lEsiCosimDpiServer" in cmd[idx + 1]
365
367 v = self._make_no_cmake(tmp_path, dpi_so=[])
368 cmd = v.compile_commands()[0]
369 assert "-LDFLAGS" not in cmd
370
372 v = self._make_no_cmake(tmp_path, debug=True)
373 cmd = v.compile_commands()[0]
374 idx = cmd.index("-CFLAGS")
375 assert "-DTRACE" in cmd[idx + 1]
376
377 def test_fallback_make_command(self, tmp_path):
378 v = self._make_no_cmake(tmp_path, top="MyTop")
379 cmds = v.compile_commands()
380 make_cmd = cmds[1]
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
386
387 def test_fallback_exe_path(self, tmp_path):
388 v = self._make_no_cmake(tmp_path, top="MyTop")
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)]
393
394
396
397 def test_from_env(self, tmp_path):
398 root = tmp_path / "verilator"
399 root.mkdir()
400 (root / "include").mkdir()
401 (root / "include" / "verilated.h").touch()
402 with mock.patch.dict(os.environ, {"VERILATOR_ROOT": str(root)}):
403 v = _make_verilator(tmp_path)
404 assert v._find_verilator_root() == root
405
406 def test_from_bin_in_path(self, tmp_path):
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"
413 fake_bin.touch()
414 fake_bin.chmod(0o755)
415 with mock.patch.dict(os.environ, {}, clear=False):
416 # Clear both root and path env vars so the real Verilator install
417 # doesn't shadow the fake bin created for this test.
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)):
421 v = _make_verilator(tmp_path)
422 found = v._find_verilator_root()
423 assert found == pkg_root
424
426 with mock.patch.dict(os.environ, {}, clear=False):
427 # Clear both env vars so the real Verilator install doesn't satisfy
428 # root detection before the RuntimeError can be raised.
429 os.environ.pop("VERILATOR_ROOT", None)
430 os.environ.pop("VERILATOR_PATH", None)
431 with mock.patch("shutil.which", return_value=None):
432 v = _make_verilator(tmp_path)
433 assert v._find_verilator_root() is None
434
435 def test_invalid_env_raises(self, tmp_path):
436 root = tmp_path / "verilator"
437 root.mkdir()
438 with mock.patch.dict(os.environ, {"VERILATOR_ROOT": str(root)}):
439 v = _make_verilator(tmp_path)
440 with pytest.raises(RuntimeError, match="VERILATOR_ROOT"):
441 v._find_verilator_root()
442
443
445
446 def test_generates_cmake(self, tmp_path):
447 obj_dir = tmp_path / "obj_dir"
448 obj_dir.mkdir()
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)}):
454 v = _make_verilator(tmp_path, dpi_so=[])
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
463
464 def test_trace_sources_in_debug(self, tmp_path):
465 obj_dir = tmp_path / "obj_dir"
466 obj_dir.mkdir()
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)}):
472 v = _make_verilator(tmp_path, debug=True, dpi_so=[])
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
477
478 @pytest.mark.parametrize(
479 ("filename", "expected"),
480 [
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),
487 ],
488 )
489 def test_classifies_slow_generated_sources(self, filename, expected):
490 assert Verilator._is_slow(Path(filename)) is expected
491
493 obj_dir = tmp_path / "obj_dir"
494 obj_dir.mkdir()
495 generated_sources = [
496 obj_dir / "VTestTop.cpp", obj_dir / "VTestTop__Slow.cpp"
497 ]
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)}):
503 v = _make_verilator(tmp_path, dpi_so=[])
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
513
514
516
517 def test_exe_path_cmake(self, tmp_path):
518 v = _make_verilator(tmp_path, top="MyTop")
519 if not v._use_cmake:
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_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_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_fallback_trace_cflags_in_debug(self, tmp_path)
_make_no_cmake(self, tmp_path, **kwargs)
test_fallback_no_ldflags_without_dpi(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_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_questa_unavailable_without_vsim(self, monkeypatch)
test_invalid_verilator_path_env_raises(self, monkeypatch, 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)