CIRCT 23.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
51requires_verilator_bin = pytest.mark.skipif(
52 not is_simulator_available("verilator"), reason="verilator not found")
53
54
56
58 with pytest.raises(ValueError):
59 is_simulator_available("bogus")
60
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()
67
68 def test_verilator_available_from_env_path(self, monkeypatch, tmp_path):
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"
75 fake_bin.touch()
76
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()
82
83 def test_invalid_verilator_path_env_raises(self, monkeypatch, tmp_path):
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")
90
91 def test_invalid_verilator_root_env_raises(self, monkeypatch, tmp_path):
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"
97 fake_bin.touch()
98
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")
104
106 monkeypatch.setattr(shutil, "which", lambda name: None)
107 assert not is_simulator_available("questa")
108
109 def test_questa_available_from_path(self, monkeypatch):
110 monkeypatch.delenv("VERILATOR_PATH", raising=False)
111 monkeypatch.delenv("VERILATOR_ROOT", raising=False)
112
113 def _which(name):
114 if name == "vsim":
115 return "C:/questa/vsim.exe"
116 return None
117
118 monkeypatch.setattr(shutil, "which", _which)
119 assert is_simulator_available("questa")
120 assert available_simulators() == ["questa"]
121
122
124
125 @requires_verilator_bin
126 def test_uses_verilator_bin(self, tmp_path):
127 v = _make_verilator(tmp_path)
128 cmds = v.compile_commands()
129 assert Path(cmds[0][0]).stem == "verilator_bin"
130 assert Path(cmds[0][0]) == v.verilator_bin
131
132 @requires_verilator_bin
133 def test_cmake_and_ninja_commands(self, tmp_path):
134 v = _make_verilator(tmp_path)
135 cmds = v.compile_commands()
136 # cmake+ninja present => 4 steps, including a Python callback.
137 if v._use_cmake:
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"
143
144 @requires_verilator_bin
146 """When using cmake, --exe and --build should not appear."""
147 v = _make_verilator(tmp_path)
148 if not v._use_cmake:
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
153
154 @requires_verilator_bin
156 """When using cmake, -CFLAGS and -LDFLAGS should not appear."""
157 v = _make_verilator(tmp_path)
158 if not v._use_cmake:
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
163
164 @requires_verilator_bin
166 """When using cmake, driver.cpp should not be in the verilator command."""
167 v = _make_verilator(tmp_path)
168 if not v._use_cmake:
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)
172
173 @requires_verilator_bin
174 def test_trace_flags_in_debug(self, tmp_path):
175 v = _make_verilator(tmp_path, debug=True)
176 cmd = v.compile_commands()[0]
177 assert "--trace-fst" in cmd
178 assert "--trace-structs" in cmd
179 assert "--trace-underscore" in cmd
180
182 fake_bin = tmp_path / "custom" / "verilator_bin"
183 fake_bin.parent.mkdir()
184 fake_bin.touch()
185 with mock.patch.dict(os.environ, {"VERILATOR_PATH": str(fake_bin)}):
186 v = _make_verilator(tmp_path)
187 assert v.verilator_bin == fake_bin.resolve()
188
190 fake_wrapper = tmp_path / "usr" / "bin" / "verilator"
191 fake_bin = fake_wrapper.parent / "verilator_bin"
192 fake_wrapper.parent.mkdir(parents=True)
193 fake_wrapper.touch()
194 fake_bin.touch()
195 with mock.patch.dict(os.environ, {"VERILATOR_PATH": str(fake_wrapper)}):
196 v = _make_verilator(tmp_path)
197 assert v.verilator_bin == fake_bin.resolve()
198
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"
206 env_bin.touch()
207 path_bin.touch()
208
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()
212
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):
218 v = _make_verilator(tmp_path)
219 with pytest.raises(RuntimeError, match="Cannot find verilator_bin"):
220 v.compile_commands()
221
222 @requires_verilator_bin
223 def test_macro_definitions(self, tmp_path):
224 v = _make_verilator(tmp_path, macros={"FOO": "BAR", "BAZ": None})
225 cmd = v.compile_commands()[0]
226 assert "+define+FOO=BAR" in cmd
227 assert "+define+BAZ" in cmd
228
229
230@requires_verilator_bin
232 """Tests for the make fallback when cmake/ninja are not available."""
233
234 def _make_no_cmake(self, tmp_path, **kwargs):
235 """Create a Verilator instance that thinks cmake/ninja are missing."""
236 v = _make_verilator(tmp_path, **kwargs)
237 return v
238
239 @pytest.fixture(autouse=True)
240 def _hide_cmake(self):
241 """Patch shutil.which so cmake and ninja appear absent."""
242 original_which = shutil.which
243
244 def _which_no_cmake(name, *args, **kwargs):
245 if name in ("cmake", "ninja"):
246 return None
247 return original_which(name, *args, **kwargs)
248
249 with mock.patch("shutil.which", side_effect=_which_no_cmake):
250 yield
251
252 def test_fallback_uses_make(self, tmp_path):
253 v = self._make_no_cmake(tmp_path)
254 cmds = v.compile_commands()
255 assert len(cmds) == 2
256 assert cmds[1][0] == "make"
257
258 def test_fallback_has_exe_flag(self, tmp_path):
259 v = self._make_no_cmake(tmp_path)
260 cmd = v.compile_commands()[0]
261 assert "--exe" in cmd
262
263 def test_fallback_has_cflags(self, tmp_path):
264 v = self._make_no_cmake(tmp_path)
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]
269
270 def test_fallback_has_driver(self, tmp_path):
271 v = self._make_no_cmake(tmp_path)
272 cmd = v.compile_commands()[0]
273 assert any("driver.cpp" in str(c) for c in cmd)
274
276 v = self._make_no_cmake(tmp_path, dpi_so=["EsiCosimDpiServer"])
277 cmd = v.compile_commands()[0]
278 assert "-LDFLAGS" in cmd
279 idx = cmd.index("-LDFLAGS")
280 assert "-lEsiCosimDpiServer" in cmd[idx + 1]
281
283 v = self._make_no_cmake(tmp_path, dpi_so=[])
284 cmd = v.compile_commands()[0]
285 assert "-LDFLAGS" not in cmd
286
288 v = self._make_no_cmake(tmp_path, debug=True)
289 cmd = v.compile_commands()[0]
290 idx = cmd.index("-CFLAGS")
291 assert "-DTRACE" in cmd[idx + 1]
292
293 def test_fallback_make_command(self, tmp_path):
294 v = self._make_no_cmake(tmp_path, top="MyTop")
295 cmds = v.compile_commands()
296 make_cmd = cmds[1]
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
302
303 def test_fallback_exe_path(self, tmp_path):
304 v = self._make_no_cmake(tmp_path, top="MyTop")
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)]
309
310
312
313 def test_from_env(self, tmp_path):
314 root = tmp_path / "verilator"
315 root.mkdir()
316 (root / "include").mkdir()
317 (root / "include" / "verilated.h").touch()
318 with mock.patch.dict(os.environ, {"VERILATOR_ROOT": str(root)}):
319 v = _make_verilator(tmp_path)
320 assert v._find_verilator_root() == root
321
322 def test_from_bin_in_path(self, tmp_path):
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"
329 fake_bin.touch()
330 fake_bin.chmod(0o755)
331 with mock.patch.dict(os.environ, {}, clear=False):
332 # Clear both root and path env vars so the real Verilator install
333 # doesn't shadow the fake bin created for this test.
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)):
337 v = _make_verilator(tmp_path)
338 found = v._find_verilator_root()
339 assert found == pkg_root
340
342 with mock.patch.dict(os.environ, {}, clear=False):
343 # Clear both env vars so the real Verilator install doesn't satisfy
344 # root detection before the RuntimeError can be raised.
345 os.environ.pop("VERILATOR_ROOT", None)
346 os.environ.pop("VERILATOR_PATH", None)
347 with mock.patch("shutil.which", return_value=None):
348 v = _make_verilator(tmp_path)
349 assert v._find_verilator_root() is None
350
351 def test_invalid_env_raises(self, tmp_path):
352 root = tmp_path / "verilator"
353 root.mkdir()
354 with mock.patch.dict(os.environ, {"VERILATOR_ROOT": str(root)}):
355 v = _make_verilator(tmp_path)
356 with pytest.raises(RuntimeError, match="VERILATOR_ROOT"):
357 v._find_verilator_root()
358
359
361
362 def test_generates_cmake(self, tmp_path):
363 obj_dir = tmp_path / "obj_dir"
364 obj_dir.mkdir()
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)}):
370 v = _make_verilator(tmp_path, dpi_so=[])
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
379
380 def test_trace_sources_in_debug(self, tmp_path):
381 obj_dir = tmp_path / "obj_dir"
382 obj_dir.mkdir()
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)}):
388 v = _make_verilator(tmp_path, debug=True, dpi_so=[])
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
393
395 obj_dir = tmp_path / "obj_dir"
396 obj_dir.mkdir()
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)}):
403 v = _make_verilator(tmp_path, dpi_so=[])
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
411
412
414
415 def test_exe_path_cmake(self, tmp_path):
416 v = _make_verilator(tmp_path, top="MyTop")
417 if not v._use_cmake:
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_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_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_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_enables_pch_when_generated_header_exists(self, tmp_path)
_make_verilator(run_dir, top="TestTop", debug=False, dpi_so=None, macros=None)