From 06e66415699f53ad892d7892124619686b67cadf Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Mon, 12 Oct 2026 15:06:41 +0200 Subject: [PATCH 1/2] Keep the simulator hooks of a VUnit object when another one is created The simulator hooks registered by packages were kept in a module global registry, which every new VUnit object cleared. A run script creating a second VUnit object after adding a package therefore lost the hooks of the first one: its tests ran without the flags and environment the package registered, for example without the library search path of a native library, and failed with errors like GHDL's "cannot load VHPIDIRECT shared library". Every VUnit object now has a SimulatorHooks registry of its own, which the package context registers into and the VUnit object gives to the simulator interface it creates. The vsim based interfaces ask for the hooks of their persistent process when it is started, since the interface gets the hooks after it has been created. Co-Authored-By: Claude Opus 5.5 --- docs/vunit_packages.rst | 4 +- tests/unit/test_builtins.py | 5 +- tests/unit/test_sim_if_hooks.py | 132 ++++++++++++++--------- vunit/package_context.py | 34 +++++- vunit/sim_if/hooks.py | 151 +++++++++++++++++---------- vunit/sim_if/vsim_simulator_mixin.py | 11 +- vunit/ui/__init__.py | 10 +- 7 files changed, 225 insertions(+), 122 deletions(-) diff --git a/docs/vunit_packages.rst b/docs/vunit_packages.rst index 87b3c770e..f7549538a 100644 --- a/docs/vunit_packages.rst +++ b/docs/vunit_packages.rst @@ -188,7 +188,9 @@ and use these dependencies. Use ``context.register_simulator_hooks(simulator_name, ...)`` to register functions that supply these settings for a particular simulator. Hooks receive the simulator interface, so they can adapt their results to its -configuration. They apply only to the project in which they are registered. +configuration. They apply only to the project in which they are registered, +that is to the tests of the VUnit object the package is added to. Creating +another VUnit object neither removes them nor gives them to the new object. The following example builds a native library and registers hooks to load it with NVC or link it with GHDL: diff --git a/tests/unit/test_builtins.py b/tests/unit/test_builtins.py index abc33ec56..d289f53a3 100644 --- a/tests/unit/test_builtins.py +++ b/tests/unit/test_builtins.py @@ -20,6 +20,7 @@ from vunit.vhdl_standard import VHDL, VHDLStandard from vunit.project import Project from vunit.sim_if import hooks +from vunit.sim_if.hooks import SimulatorHooks from tests.common import create_tempdir from contextlib import contextmanager from importlib.machinery import ModuleSpec @@ -64,6 +65,7 @@ def add_library(name): self.vu._output_path = "output_path" self.vu._run_script_path = Path("run.py") + self.vu._simulator_hooks = SimulatorHooks() self.builtins = Builtins(self.vu, VHDLStandard("2002"), None) @@ -728,8 +730,6 @@ def setup(context): self.assertEqual(context.simulator_backend, backend) def test_setup_function_registers_simulator_hooks(self): - self.addCleanup(hooks.clear_hooks) - with ( create_tempdir() as tempdir, pkg_env(tempdir), @@ -760,6 +760,7 @@ def setup(context): simulator_interface = mock.Mock() simulator_interface.name = "ghdl" + simulator_interface.simulator_hooks = self.vu._simulator_hooks self.assertEqual(hooks.get_flags(simulator_interface, "elab_flags"), ["-Wl,-lfoo"]) self.assertEqual(hooks.get_flags(simulator_interface, "run_flags"), ["--load=foo"]) self.assertEqual(hooks.get_flags(simulator_interface, "process_flags"), ["-noautoldlibpath"]) diff --git a/tests/unit/test_sim_if_hooks.py b/tests/unit/test_sim_if_hooks.py index 22a56f4f9..d00695a74 100644 --- a/tests/unit/test_sim_if_hooks.py +++ b/tests/unit/test_sim_if_hooks.py @@ -15,7 +15,8 @@ from vunit import VUnit from vunit.project import Project -from vunit.sim_if import hooks +from vunit.sim_if import SimulatorInterface, hooks +from vunit.sim_if.hooks import SimulatorHooks from vunit.sim_if.activehdl import ActiveHDLInterface from vunit.sim_if.ghdl import GHDLInterface from vunit.sim_if.modelsim import ModelSimInterface @@ -51,14 +52,10 @@ class TestSimulatorHooks(unittest.TestCase): """ def setUp(self): - hooks.clear_hooks() + self.hooks = SimulatorHooks() - def tearDown(self): - hooks.clear_hooks() - - @staticmethod - def _simulator(name): - return SimpleNamespace(name=name) + def _simulator(self, name): + return SimpleNamespace(name=name, simulator_hooks=self.hooks) def test_no_hooks_by_default(self): simulator = self._simulator("ghdl") @@ -69,7 +66,7 @@ def test_no_hooks_by_default(self): def test_calls_hooks_with_the_simulator_interface(self): simulator = self._simulator("ghdl") - hooks.register_hooks( + self.hooks.register( "ghdl", elab_flags=lambda interface: [f"-Wl,-l{interface.name}"], run_flags=lambda interface: ["--load"], @@ -85,54 +82,89 @@ def test_calls_hooks_with_the_simulator_interface(self): def test_combines_hooks_in_registration_order(self): simulator = self._simulator("ghdl") for kind in FLAG_KINDS: - hooks.register_hooks("ghdl", **{kind: lambda interface: ["first"]}) - hooks.register_hooks("ghdl", **{kind: lambda interface: ["second"]}) + self.hooks.register("ghdl", **{kind: lambda interface: ["first"]}) + self.hooks.register("ghdl", **{kind: lambda interface: ["second"]}) self.assertEqual(hooks.get_flags(simulator, kind), ["first", "second"]) def test_hooks_are_specific_to_a_simulator(self): for kind in FLAG_KINDS: - hooks.register_hooks("ghdl", **{kind: lambda interface: ["--load"]}) + self.hooks.register("ghdl", **{kind: lambda interface: ["--load"]}) self.assertEqual(hooks.get_flags(self._simulator("ghdl"), kind), ["--load"]) self.assertEqual(hooks.get_flags(self._simulator("nvc"), kind), []) def test_copies_the_environment_when_there_is_none(self): simulator = self._simulator("ghdl") - hooks.register_hooks("ghdl", run_env=lambda interface, env: dict(env, FOO="1")) + self.hooks.register("ghdl", run_env=lambda interface, env: dict(env, FOO="1")) env = hooks.get_run_env(simulator) self.assertEqual(env["FOO"], "1") self.assertIn("PATH", env) - def test_clears_hooks(self): - simulator = self._simulator("ghdl") - hooks.register_hooks("ghdl", elab_flags=lambda interface: ["--load"]) - hooks.clear_hooks() - - self.assertEqual(hooks.get_flags(simulator, "elab_flags"), []) - def test_raises_if_hook_is_not_callable(self): with self.assertRaisesRegex(ValueError, "run_flags hook for simulator ghdl is not callable."): - hooks.register_hooks("ghdl", run_flags="--load") + self.hooks.register("ghdl", run_flags="--load") with self.assertRaisesRegex(ValueError, "process_flags hook for simulator ghdl is not callable."): - hooks.register_hooks("ghdl", process_flags="-noautoldlibpath") + self.hooks.register("ghdl", process_flags="-noautoldlibpath") def test_raises_if_simulator_name_is_invalid(self): with self.assertRaisesRegex(ValueError, "Simulator name must be a non-empty string."): - hooks.register_hooks("", run_flags=lambda interface: []) + self.hooks.register("", run_flags=lambda interface: []) - def test_hooks_are_cleared_when_a_vunit_object_is_created(self): - hooks.register_hooks("ghdl", elab_flags=lambda interface: ["--load"]) + def test_no_hooks_for_an_interface_not_created_by_a_vunit_object(self): + self.hooks.register("ghdl", elab_flags=lambda interface: ["--load"]) + simulator = SimpleNamespace(name="ghdl") + + self.assertEqual(hooks.get_flags(simulator, "elab_flags"), []) + self.assertEqual(hooks.get_run_env(simulator, {"FOO": "1"}), {"FOO": "1"}) - with ( - create_tempdir() as tempdir, - mock.patch("vunit.sim_if.factory.SIMULATOR_FACTORY.select_simulator", new=lambda: None), - ): - VUnit.from_argv(argv=["--output-path=%s" % tempdir]) - self.assertEqual(hooks.get_flags(self._simulator("ghdl"), "elab_flags"), []) +class _FakeSimulator(SimulatorInterface): + """ + A simulator class creating a plain interface + """ + + name = "ghdl" + + @classmethod + def from_args(cls, args, output_path): # pylint: disable=unused-argument + return SimpleNamespace(name=cls.name) + + +class TestSimulatorHooksOfVUnitObjects(unittest.TestCase): + """ + Test that the hooks belong to the VUnit object of the packages registering them + """ + + @staticmethod + def _vunit(output_path): + with mock.patch("vunit.sim_if.factory.SIMULATOR_FACTORY.select_simulator", new=lambda: _FakeSimulator): + return VUnit.from_argv(argv=[f"--output-path={output_path!s}"]) + + def test_simulator_interface_gets_the_hooks_of_its_vunit_object(self): + with create_tempdir() as tempdir: + vu = self._vunit(tempdir) + vu._simulator_hooks.register( # pylint: disable=protected-access + "ghdl", elab_flags=lambda interface: ["--load"] + ) + simulator_if = vu._create_simulator_if() # pylint: disable=protected-access + + self.assertEqual(hooks.get_flags(simulator_if, "elab_flags"), ["--load"]) + + def test_hooks_are_kept_when_another_vunit_object_is_created(self): + with create_tempdir() as tempdir: + first = self._vunit(Path(tempdir) / "first") + first._simulator_hooks.register( # pylint: disable=protected-access + "ghdl", elab_flags=lambda interface: ["--load"] + ) + second = self._vunit(Path(tempdir) / "second") + first_if = first._create_simulator_if() # pylint: disable=protected-access + second_if = second._create_simulator_if() # pylint: disable=protected-access + + self.assertEqual(hooks.get_flags(first_if, "elab_flags"), ["--load"]) + self.assertEqual(hooks.get_flags(second_if, "elab_flags"), []) class TestSimulatorHooksAreUsed(unittest.TestCase): @@ -141,10 +173,7 @@ class TestSimulatorHooksAreUsed(unittest.TestCase): """ def setUp(self): - hooks.clear_hooks() - - def tearDown(self): - hooks.clear_hooks() + self.hooks = SimulatorHooks() @staticmethod def _project(): @@ -154,7 +183,7 @@ def _project(): @mock.patch.object(GHDLInterface, "determine_version", return_value=5.0) def test_ghdl_uses_hooks(self, determine_version): - hooks.register_hooks( + self.hooks.register( "ghdl", elab_flags=lambda interface: ["-Wl,-lfoo"], run_flags=lambda interface: ["--load=foo"], @@ -162,6 +191,7 @@ def test_ghdl_uses_hooks(self, determine_version): ) simif = GHDLInterface(prefix="prefix", output_path="") + simif.simulator_hooks = self.hooks simif._project = self._project() # pylint: disable=protected-access simif._vhdl_standard = VHDL.standard("2008") # pylint: disable=protected-access @@ -182,7 +212,7 @@ def test_ghdl_uses_hooks(self, determine_version): @mock.patch.object(NVCInterface, "determine_version", return_value=(1, 16)) def test_nvc_uses_hooks(self, determine_version): - hooks.register_hooks( + self.hooks.register( "nvc", elab_flags=lambda interface: ["--jit"], run_flags=lambda interface: ["--load=foo"], @@ -190,6 +220,7 @@ def test_nvc_uses_hooks(self, determine_version): ) simif = NVCInterface(prefix="prefix", output_path="", num_threads=1) + simif.simulator_hooks = self.hooks simif._project = self._project() # pylint: disable=protected-access simif._vhdl_standard = VHDL.standard("2008") # pylint: disable=protected-access @@ -207,26 +238,26 @@ def test_nvc_uses_hooks(self, determine_version): self.assertEqual(process.call_args[1]["env"]["FOO"], "1") def test_modelsim_uses_hooks(self): - hooks.register_hooks( + self.hooks.register( "modelsim", elab_flags=lambda interface: ["-foo"], run_flags=lambda interface: ["-bar"], ) - simif = SimpleNamespace(name=ModelSimInterface.name, _gui=False) + simif = SimpleNamespace(name=ModelSimInterface.name, _gui=False, simulator_hooks=self.hooks) self.assertEqual(ModelSimInterface._vopt_extra_args(simif, make_config()), "-foo") self.assertEqual(ModelSimInterface._vsim_extra_args(simif, make_config()), "-bar") def test_rivierapro_uses_hooks(self): - hooks.register_hooks("rivierapro", run_flags=lambda interface: ["-bar"]) + self.hooks.register("rivierapro", run_flags=lambda interface: ["-bar"]) - simif = SimpleNamespace(name=RivieraProInterface.name, _gui=False) + simif = SimpleNamespace(name=RivieraProInterface.name, _gui=False, simulator_hooks=self.hooks) self.assertEqual(RivieraProInterface._vsim_extra_args(simif, make_config()), "-bar") def test_activehdl_uses_hooks(self): - hooks.register_hooks("activehdl", run_flags=lambda interface: ["-bar"]) + self.hooks.register("activehdl", run_flags=lambda interface: ["-bar"]) - simif = SimpleNamespace(name=ActiveHDLInterface.name, _gui=False) + simif = SimpleNamespace(name=ActiveHDLInterface.name, _gui=False, simulator_hooks=self.hooks) self.assertEqual(ActiveHDLInterface._vsim_extra_args(simif, make_config()), "-bar") @@ -252,10 +283,7 @@ class TestVsimProcessHooks(unittest.TestCase): """ def setUp(self): - hooks.clear_hooks() - - def tearDown(self): - hooks.clear_hooks() + self.hooks = SimulatorHooks() def test_no_process_hooks_by_default(self): with ( @@ -263,6 +291,7 @@ def test_no_process_hooks_by_default(self): mock.patch("vunit.sim_if.vsim_simulator_mixin.Process", autospec=True) as process, ): simif = _VsimSimulator(str(Path(tempdir) / "modelsim.ini"), persistent=True) + simif.simulator_hooks = self.hooks simif._persistent_shell._create_process(1) # pylint: disable=protected-access self.assertNotIn("-noautoldlibpath", process.call_args[0][0]) self.assertEqual(process.call_args[1]["env"], {"BASE": "1"}) @@ -271,7 +300,7 @@ def test_no_process_hooks_by_default(self): self.assertIsNone(process.call_args[1]["env"]) def test_persistent_vsim_process_uses_hooks(self): - hooks.register_hooks( + self.hooks.register( "modelsim", process_flags=lambda interface: ["-noautoldlibpath"], run_env=lambda interface, env: dict(env, FOO="1"), @@ -282,6 +311,7 @@ def test_persistent_vsim_process_uses_hooks(self): mock.patch("vunit.sim_if.vsim_simulator_mixin.Process", autospec=True) as process, ): simif = _VsimSimulator(str(Path(tempdir) / "modelsim.ini"), persistent=True) + simif.simulator_hooks = self.hooks simif._persistent_shell._create_process(1) # pylint: disable=protected-access args = process.call_args[0][0] @@ -289,7 +319,7 @@ def test_persistent_vsim_process_uses_hooks(self): self.assertEqual(process.call_args[1]["env"], {"BASE": "1", "FOO": "1"}) def test_batch_vsim_process_uses_hooks(self): - hooks.register_hooks( + self.hooks.register( "modelsim", process_flags=lambda interface: ["-noautoldlibpath"], run_env=lambda interface, env: dict(env, FOO="1"), @@ -300,6 +330,7 @@ def test_batch_vsim_process_uses_hooks(self): mock.patch("vunit.sim_if.vsim_simulator_mixin.Process", autospec=True) as process, ): simif = _VsimSimulator(str(Path(tempdir) / "modelsim.ini"), persistent=False) + simif.simulator_hooks = self.hooks simif._run_batch_file(str(Path(tempdir) / "batch.do")) # pylint: disable=protected-access args = process.call_args[0][0] @@ -307,13 +338,14 @@ def test_batch_vsim_process_uses_hooks(self): self.assertEqual(process.call_args[1]["env"]["FOO"], "1") def test_process_flags_are_specific_to_a_simulator(self): - hooks.register_hooks("rivierapro", process_flags=lambda interface: ["-noautoldlibpath"]) + self.hooks.register("rivierapro", process_flags=lambda interface: ["-noautoldlibpath"]) with ( create_tempdir() as tempdir, mock.patch("vunit.sim_if.vsim_simulator_mixin.Process", autospec=True) as process, ): simif = _VsimSimulator(str(Path(tempdir) / "modelsim.ini"), persistent=False) + simif.simulator_hooks = self.hooks simif._run_batch_file(str(Path(tempdir) / "batch.do")) # pylint: disable=protected-access self.assertNotIn("-noautoldlibpath", process.call_args[0][0]) diff --git a/vunit/package_context.py b/vunit/package_context.py index e8b05d1bb..da93fec78 100644 --- a/vunit/package_context.py +++ b/vunit/package_context.py @@ -12,7 +12,7 @@ from pathlib import Path from typing import Any, Optional -from vunit.sim_if.hooks import register_hooks +from vunit.sim_if.hooks import EnvHook, FlagsHook from vunit.vhdl_standard import VHDLStandard @@ -43,8 +43,36 @@ class PackageContext: simulator_class: Any _vunit_obj: Any - #: Register hooks extending what a simulator does for a test, see :ref:`vunit_packages`. - register_simulator_hooks = staticmethod(register_hooks) + def register_simulator_hooks( + self, + simulator_name: str, + *, + elab_flags: Optional[FlagsHook] = None, + run_flags: Optional[FlagsHook] = None, + process_flags: Optional[FlagsHook] = None, + run_env: Optional[EnvHook] = None, + ) -> None: + """ + Register hooks extending what a simulator does for a test, see :ref:`vunit_packages`. The + hooks apply to the tests of the VUnit object the package is added to. + + :param simulator_name: The name of the simulator the hooks apply to, for example "ghdl". + :param elab_flags: A ``elab_flags(simulator_interface)`` function returning extra flags for + the elaboration of a test. + :param run_flags: A ``run_flags(simulator_interface)`` function returning extra flags for the + simulation of a test. + :param process_flags: A ``process_flags(simulator_interface)`` function returning extra flags + for the simulator process VUnit starts. + :param run_env: A ``run_env(simulator_interface, env)`` function returning the environment + of the simulation of a test, given the environment it would otherwise have. + """ + self._vunit_obj._simulator_hooks.register( # pylint: disable=protected-access + simulator_name, + elab_flags=elab_flags, + run_flags=run_flags, + process_flags=process_flags, + run_env=run_env, + ) @property def simulator_name(self) -> Optional[str]: diff --git a/vunit/sim_if/hooks.py b/vunit/sim_if/hooks.py index a11fffed4..7aa6528ab 100644 --- a/vunit/sim_if/hooks.py +++ b/vunit/sim_if/hooks.py @@ -5,15 +5,16 @@ # Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com """ -Registry of simulator hooks. +Simulator hooks. A hook is a callable provided by a VUnit package to extend what a simulator interface does for a test. The hooks are registered per simulator name and are called by the simulator interfaces with the interface instance such that a hook can base its result on how the simulator was configured. -The registry is cleared when a new VUnit object is created, that is the hooks registered for -a project do not leak into the next one. +Every VUnit object has a registry of its own, holding the hooks registered by its packages, +and gives it to the simulator interface it creates. The hooks of a project therefore neither +leak into another VUnit object nor are lost when another VUnit object is created. """ from os import environ @@ -22,75 +23,109 @@ FlagsHook = Callable[..., List[str]] EnvHook = Callable[..., Dict[str, str]] -# The hooks registered for a simulator name and a kind of hook -_HOOKS: Dict[Tuple[str, str], List[Callable]] = {} - -def register_hooks( - simulator_name: str, - *, - elab_flags: Optional[FlagsHook] = None, - run_flags: Optional[FlagsHook] = None, - process_flags: Optional[FlagsHook] = None, - run_env: Optional[EnvHook] = None, -) -> None: +class SimulatorHooks: """ - Register hooks for a simulator. - - :param simulator_name: The name of the simulator the hooks apply to, for example "ghdl". - :param elab_flags: A ``elab_flags(simulator_interface)`` function returning extra flags for - the elaboration of a test. Simulators without a separate elaboration step - do not call this hook. - :param run_flags: A ``run_flags(simulator_interface)`` function returning extra flags for the - simulation of a test. - :param process_flags: A ``process_flags(simulator_interface)`` function returning extra flags for - the simulator process VUnit starts. Only simulators driving the simulation - from a separate process, that is the vsim based ones, call this hook. - :param run_env: A ``run_env(simulator_interface, env)`` function returning the environment - of the simulation of a test, given the environment it would otherwise have. + The hooks registered by the packages of a VUnit object. """ - if not isinstance(simulator_name, str) or not simulator_name: - raise ValueError("Simulator name must be a non-empty string.") - - new_hooks = {"elab_flags": elab_flags, "run_flags": run_flags, "process_flags": process_flags, "run_env": run_env} - for kind, hook in new_hooks.items(): - if hook is not None and not callable(hook): - raise ValueError(f"{kind} hook for simulator {simulator_name} is not callable.") - - for kind, hook in new_hooks.items(): - if hook is not None: - _HOOKS.setdefault((simulator_name, kind), []).append(hook) - -def clear_hooks() -> None: + def __init__(self): + # The hooks registered for a simulator name and a kind of hook + self._hooks: Dict[Tuple[str, str], List[Callable]] = {} + + def register( + self, + simulator_name: str, + *, + elab_flags: Optional[FlagsHook] = None, + run_flags: Optional[FlagsHook] = None, + process_flags: Optional[FlagsHook] = None, + run_env: Optional[EnvHook] = None, + ) -> None: + """ + Register hooks for a simulator. + + :param simulator_name: The name of the simulator the hooks apply to, for example "ghdl". + :param elab_flags: A ``elab_flags(simulator_interface)`` function returning extra flags for + the elaboration of a test. Simulators without a separate elaboration step + do not call this hook. + :param run_flags: A ``run_flags(simulator_interface)`` function returning extra flags for the + simulation of a test. + :param process_flags: A ``process_flags(simulator_interface)`` function returning extra flags + for the simulator process VUnit starts. Only simulators driving the + simulation from a separate process, that is the vsim based ones, call + this hook. + :param run_env: A ``run_env(simulator_interface, env)`` function returning the environment + of the simulation of a test, given the environment it would otherwise have. + """ + if not isinstance(simulator_name, str) or not simulator_name: + raise ValueError("Simulator name must be a non-empty string.") + + new_hooks = { + "elab_flags": elab_flags, + "run_flags": run_flags, + "process_flags": process_flags, + "run_env": run_env, + } + for kind, hook in new_hooks.items(): + if hook is not None and not callable(hook): + raise ValueError(f"{kind} hook for simulator {simulator_name} is not callable.") + + for kind, hook in new_hooks.items(): + if hook is not None: + self._hooks.setdefault((simulator_name, kind), []).append(hook) + + def get_flags(self, simulator_interface, kind: str) -> List[str]: + """ + Return the extra flags provided by the hooks of the simulator of a kind, that is + "elab_flags", "run_flags" or "process_flags". + """ + return [ + flag for hook in self._hooks.get((simulator_interface.name, kind), []) for flag in hook(simulator_interface) + ] + + def get_run_env(self, simulator_interface, env: Optional[Dict[str, str]] = None) -> Optional[Dict[str, str]]: + """ + Return the simulation environment transformed by the hooks of the simulator. + + The environment is returned unchanged if there are no hooks. None means that the + simulation inherits the environment of VUnit, in which case a copy of that environment is + given to the hooks. + """ + hooks = self._hooks.get((simulator_interface.name, "run_env"), []) + if not hooks: + return env + + result = environ.copy() if env is None else env + for hook in hooks: + result = hook(simulator_interface, result) + + return result + + +# The hooks of a simulator interface not created by a VUnit object +_NO_HOOKS = SimulatorHooks() + + +def _hooks_of(simulator_interface) -> SimulatorHooks: """ - Remove all registered hooks. + The hooks given to a simulator interface by the VUnit object that created it. """ - _HOOKS.clear() + hooks = getattr(simulator_interface, "simulator_hooks", None) + return hooks if isinstance(hooks, SimulatorHooks) else _NO_HOOKS def get_flags(simulator_interface, kind: str) -> List[str]: """ - Return the extra flags provided by the hooks of the simulator of a kind, that is + Return the extra flags provided by the hooks of the simulator interface of a kind, that is "elab_flags", "run_flags" or "process_flags". """ - return [flag for hook in _HOOKS.get((simulator_interface.name, kind), []) for flag in hook(simulator_interface)] + return _hooks_of(simulator_interface).get_flags(simulator_interface, kind) def get_run_env(simulator_interface, env: Optional[Dict[str, str]] = None) -> Optional[Dict[str, str]]: """ - Return the simulation environment transformed by the hooks of the simulator. - - The environment is returned unchanged if there are no hooks. None means that the simulation - inherits the environment of VUnit, in which case a copy of that environment is given to the - hooks. + Return the simulation environment transformed by the hooks of the simulator interface, see + :meth:`SimulatorHooks.get_run_env`. """ - hooks = _HOOKS.get((simulator_interface.name, "run_env"), []) - if not hooks: - return env - - result = environ.copy() if env is None else env - for hook in hooks: - result = hook(simulator_interface, result) - - return result + return _hooks_of(simulator_interface).get_run_env(simulator_interface, env) diff --git a/vunit/sim_if/vsim_simulator_mixin.py b/vunit/sim_if/vsim_simulator_mixin.py index 6048c113a..d52aac6f1 100644 --- a/vunit/sim_if/vsim_simulator_mixin.py +++ b/vunit/sim_if/vsim_simulator_mixin.py @@ -11,6 +11,7 @@ import sys import os +import weakref from pathlib import Path from ..ostools import write_file, Process from ..test.suites import get_result_file_name @@ -30,10 +31,12 @@ def __init__(self, prefix, persistent, sim_cfg_file_name): self._sim_cfg_file_name = sim_cfg_file_name prefix = self._prefix # Avoid circular dependency inhibiting process destruction - env = hooks.get_run_env(self, self.get_env()) - process_flags = hooks.get_flags(self, "process_flags") + # Likewise a weak reference. The hooks are asked when a process is created rather than + # here, since the VUnit object gives its hooks to the interface after creating it. + simulator_if = weakref.ref(self) def create_process(ident): + simulator = simulator_if() return Process( [ str(Path(prefix) / "vsim"), @@ -43,9 +46,9 @@ def create_process(ident): "-do", str((Path(__file__).parent / "tcl_read_eval_loop.tcl").resolve()), ] - + process_flags, + + hooks.get_flags(simulator, "process_flags"), cwd=str(Path(sim_cfg_file_name).parent), - env=env, + env=hooks.get_run_env(simulator, simulator.get_env()), ) if persistent: diff --git a/vunit/ui/__init__.py b/vunit/ui/__init__.py index 28943a3c5..17503a6c8 100644 --- a/vunit/ui/__init__.py +++ b/vunit/ui/__init__.py @@ -28,7 +28,7 @@ from ..vunit_cli import VUnitCLI from ..sim_if.factory import SIMULATOR_FACTORY from ..sim_if import SimulatorInterface -from ..sim_if.hooks import clear_hooks as clear_simulator_hooks +from ..sim_if.hooks import SimulatorHooks from ..color_printer import COLOR_PRINTER, NO_COLOR_PRINTER from ..project import Project @@ -154,8 +154,8 @@ def __init__( self._output_path = str(Path(args.output_path).resolve()) self._run_script_path = _find_run_script_path() - # Simulator hooks are registered by the packages of this project - clear_simulator_hooks() + # The simulator hooks registered by the packages of this project + self._simulator_hooks = SimulatorHooks() if args.no_color: self._printer = NO_COLOR_PRINTER @@ -861,7 +861,9 @@ def _create_simulator_if(self): if not Path(self._simulator_output_path).exists(): os.makedirs(self._simulator_output_path) - return self._simulator_class.from_args(args=self._args, output_path=self._simulator_output_path) + simulator_if = self._simulator_class.from_args(args=self._args, output_path=self._simulator_output_path) + simulator_if.simulator_hooks = self._simulator_hooks + return simulator_if def _get_test_history(self, simulator_if): """ From f431b9d96346d4637d508b8dcc6cb2ed594450e8 Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Mon, 12 Oct 2026 15:09:46 +0200 Subject: [PATCH 2/2] Add the news fragment of #1234 Co-Authored-By: Claude Opus 5.5 --- docs/news.d/1234.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/news.d/1234.bugfix.rst diff --git a/docs/news.d/1234.bugfix.rst b/docs/news.d/1234.bugfix.rst new file mode 100644 index 000000000..bc5eb0180 --- /dev/null +++ b/docs/news.d/1234.bugfix.rst @@ -0,0 +1 @@ +The simulator hooks registered by the packages of a VUnit object are no longer lost when another VUnit object is created.