diff --git a/README.md b/README.md index 16eeb07..887c520 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,10 @@ check(call_integer_vector("gain", arg(integer_vector'(1, 2))) = integer_vector'( | Questa/ModelSim | FLI | Tested manually on Linux; the Windows build is untested | | Riviera-PRO | VHPI | Tested manually on Linux and Windows | +A simulator provided by a VUnit package says how it loads the bridge +itself, by two attributes of its simulator interface class: see +[Simulator packages](https://vunit-python-bridge.readthedocs.io/en/latest/simulator_packages.html). + ## Requirements - VUnit 5.0.0.dev15 or later (`vunit_hdl` on PyPI), which pip installs with the package. diff --git a/docs/index.rst b/docs/index.rst index abc2cd2..e3e35a3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -62,6 +62,9 @@ Questa/ModelSim FLI Tested manually on Linux; the Win Riviera-PRO VHPI Tested manually on Linux and Windows ================================= ========== ======================================================== +A simulator provided by a VUnit package says how it loads the bridge itself, see +:doc:`simulator_packages`. + Getting started --------------- @@ -131,6 +134,7 @@ become available through ``python_context``: :hidden: how_it_works + simulator_packages .. toctree:: :caption: About diff --git a/docs/requirements.rst b/docs/requirements.rst index cf0a35e..3b0111e 100644 --- a/docs/requirements.rst +++ b/docs/requirements.rst @@ -3,8 +3,9 @@ Requirements * A VUnit with support for packages and simulator hooks, see the `README `__. -* NVC, GHDL, Questa/ModelSim or Riviera-PRO, see :ref:`python_bridge:native`. - Any other simulator raises a ``RuntimeError``. +* NVC, GHDL, Questa/ModelSim or Riviera-PRO, see :ref:`python_bridge:native`, + or a simulator from a VUnit package that describes how it loads the bridge, see + :ref:`python_bridge:simulator_packages`. Any other simulator raises a ``RuntimeError``. * VHDL-2008 or later. * CPython 3.10 or later with the standard (GIL) build. Free-threaded builds are rejected with an error. diff --git a/docs/simulator_packages.rst b/docs/simulator_packages.rst new file mode 100644 index 0000000..e21f256 --- /dev/null +++ b/docs/simulator_packages.rst @@ -0,0 +1,56 @@ +.. _python_bridge:simulator_packages: + +Simulator packages +------------------ + +A simulator can be provided by a VUnit package, whose ``vunit_pkg.toml`` names +its simulator interface class. The bridge does not list such simulators. +It collects them from VUnit, which knows the simulator interface classes of the +packages added before the bridge. A class says how the bridge is served with +two attributes: + +``foreign_language_interface`` + How the simulator calls the bridge library: ``"VHPIDIRECT"``, ``"FLI"`` or + ``"VHPI"``. + + * ``"VHPIDIRECT"``: the foreign attributes are + ``VHPIDIRECT ``, with the file name + only, so the simulator has to load the library itself, from the flags or + the environment the hooks below give it. + * ``"FLI"``: the attributes of the FLI variant of the library, built against + the ``mti.h`` of the simulator installation, as for Questa/ModelSim. + * ``"VHPI"``: the attributes of the VHPI variant, built against the + ``vhpi_user.h`` of the simulator installation, as for Riviera-PRO. + +``native_library_hooks(library_file)`` + A static or class method returning how the simulator loads the given + library: a ``dict`` of keyword arguments for + ``context.register_simulator_hooks``, that is any of ``elab_flags``, + ``run_flags``, ``process_flags`` and ``run_env``, each a function as + described under "Simulator Hooks" in the VUnit documentation of packages. + The bridge registers them for the simulator, with its own ``run_env`` on top + of the one returned, which adds ``VUNIT_PYTHON_BRIDGE_CONFIG`` to the + environment. The simulator interface must therefore apply the run + environment hooks to the simulation. + +For example, for a simulator that loads a library given with ``--load`` when +it runs a test: + +.. code-block:: python + + class MySimulatorInterface(SimulatorInterface): + name = "mysim" + foreign_language_interface = "VHPIDIRECT" + + @staticmethod + def native_library_hooks(library_file): + return {"run_flags": lambda simulator_interface: [f"--load={library_file}"]} + +The simulators of these classes are added to the built-in ones the bridge +serves, and take precedence over a built-in simulator of the same ``name``. +Only the selected simulator is checked and has its hooks called. Selecting a +simulator that is not among them fails when the package is added, with an +error naming the simulators the bridge serves and the two attributes. So do +an unknown interface name, only one of the two attributes, and hooks that are +not a ``dict`` of the keys above. A class defining only one of the attributes +is ignored when another simulator is selected. diff --git a/release_notes/news.d/41.feature.md b/release_notes/news.d/41.feature.md new file mode 100644 index 0000000..a24f13d --- /dev/null +++ b/release_notes/news.d/41.feature.md @@ -0,0 +1 @@ +Simulators provided by VUnit packages can be used with the bridge, see "Simulator packages" in the documentation. diff --git a/src/vunit_python_bridge/__init__.py b/src/vunit_python_bridge/__init__.py index eaf8a39..771b329 100644 --- a/src/vunit_python_bridge/__init__.py +++ b/src/vunit_python_bridge/__init__.py @@ -14,7 +14,8 @@ the VUnit Python bridge, a small C library (native/*.c) embedding CPython in the simulator process, called through VHPIDIRECT (NVC, GHDL), the FLI (Questa/ModelSim) or VHPI -(Riviera-PRO). +(Riviera-PRO). A simulator provided by a VUnit package names its interface itself, see +bridge.bridge_simulators. Modules: @@ -40,20 +41,6 @@ VHDL_PATH = Path(__file__).parent.resolve() / "hdl" / "src" -# The foreign language interface implementing the package for a simulator. VUnit says which -# simulator was selected, the package says how it is served: NVC and GHDL both go through -# VHPIDIRECT but bind the library differently, Questa/ModelSim goes through the FLI and -# Riviera-PRO through VHPI. -FOREIGN_LANGUAGE_INTERFACES = { - "nvc": "VHPIDIRECT_NVC", - "ghdl": "VHPIDIRECT_GHDL", - "modelsim": "FLI", - "rivierapro": "VHPI", -} - -# The simulators of the table, named as a user knows them -SUPPORTED_SIMULATORS = "NVC, GHDL, Questa/ModelSim or Riviera-PRO" - def setup(context): """ @@ -63,21 +50,18 @@ def setup(context): foreign language interface of the selected simulator. """ # pylint: disable=import-outside-toplevel - from .bridge import setup as setup_bridge + from .bridge import bridge_simulators, select_simulator, setup as setup_bridge from . import simulator_hooks simulator_name = context.simulator_name + simulators = bridge_simulators(simulator_name) if simulator_name is None: raise RuntimeError( - f"The vunit-python-bridge package requires {SUPPORTED_SIMULATORS} but no simulator was found" + f"The vunit-python-bridge package requires {', '.join(simulators)} but no simulator was found" ) - interface = FOREIGN_LANGUAGE_INTERFACES.get(simulator_name) - if interface is None: - raise RuntimeError( - f"The vunit-python-bridge package requires {SUPPORTED_SIMULATORS}, " - f"it has no foreign language interface for {simulator_name}" - ) + # Before anything is built, so a simulator the bridge cannot serve fails first + interface = select_simulator(simulators, simulator_name).interface add_file_base_package(context) @@ -87,8 +71,9 @@ def setup(context): simulator_name, context.simulator_prefix, context.simulator_backend, + interface, ) context.add_source_files(context.library.name, list(bridge.vhdl_files)) - simulator_hooks.register(context, bridge) + simulator_hooks.register(context, bridge, simulators) diff --git a/src/vunit_python_bridge/bridge.py b/src/vunit_python_bridge/bridge.py index 8f6d60a..1a6661b 100644 --- a/src/vunit_python_bridge/bridge.py +++ b/src/vunit_python_bridge/bridge.py @@ -13,7 +13,9 @@ import re import sys from pathlib import Path -from typing import List, NamedTuple, Optional +from typing import Callable, Dict, List, NamedTuple, Optional + +from vunit.sim_if.factory import SIMULATOR_FACTORY from .native_library import ( LIBRARY_DIR_VARIABLE, @@ -31,10 +33,20 @@ BRIDGE_PACKAGE_TEMPLATE = VHDL_SOURCE_PATH / "python_bridge_pkg.vhd.in" CONFIG_FILE_NAME = "vunit_python_bridge.cfg" -# The simulators the bridge serves, reaching it through VHPIDIRECT, the FLI and VHPI. -FLI_SIMULATORS = ("modelsim",) -VHPI_SIMULATORS = ("rivierapro",) -BRIDGE_SIMULATORS = ("nvc", "ghdl") + FLI_SIMULATORS + VHPI_SIMULATORS +# The foreign language interface of each built-in VUnit simulator. VHPIDIRECT names the library +# by file name and the simulator hooks make the simulator load it, which GHDL does in its own way. +BUILTIN_SIMULATORS = { + "nvc": "VHPIDIRECT", + "ghdl": "VHPIDIRECT_GHDL", + "modelsim": "FLI", + "rivierapro": "VHPI", +} + +# What the interface class of a simulator from a VUnit package defines instead, see +# docs/simulator_packages.rst +INTERFACE_ATTRIBUTE = "foreign_language_interface" +HOOKS_ATTRIBUTE = "native_library_hooks" +PACKAGE_INTERFACES = ("VHPIDIRECT", "FLI", "VHPI") # GHDL backends that link the design ahead of time. For these the library # token in the VHPIDIRECT attribute is passed to the linker instead of being @@ -47,6 +59,16 @@ SUBPROGRAM_PATTERN = re.compile(r"^ (impure function|procedure) (\w+)(\([^)]*\))?( return \w+)?;", re.MULTILINE) +class BridgeSimulator(NamedTuple): + """ + How the bridge serves a simulator: its foreign language interface and, for a simulator from a + VUnit package, the native_library_hooks that make it load the library (None for a built-in one). + """ + + interface: str + native_library_hooks: Optional[Callable] + + class PythonBridge(NamedTuple): """ A prepared bridge: the native library and the generated VHDL. @@ -63,6 +85,7 @@ def setup( simulator_name: Optional[str] = None, simulator_prefix: Optional[str] = None, simulator_backend: Optional[str] = None, + interface: Optional[str] = None, ) -> PythonBridge: """ Prepare the Python bridge for a project. Called by the package setup function. @@ -73,19 +96,18 @@ def setup( :param simulator_prefix: The path its executables were found in. :param simulator_backend: How the installation found there was built, which for GHDL is the code generator deciding how the library is bound. + :param interface: The foreign language interface, see bridge_simulators. None takes it from + there by simulator name. :returns: The bridge. Its vhdl_files are to be added to the package library. """ - if simulator_name is not None and simulator_name not in BRIDGE_SIMULATORS: - raise PythonBridgeError( - f"The vunit-python-bridge package requires NVC, GHDL, Questa/ModelSim or Riviera-PRO, " - f"it is not supported for {simulator_name}" - ) + if interface is None and simulator_name is not None: + interface = select_simulator(bridge_simulators(simulator_name), simulator_name).interface check_python_build() check_windows_64bit_simulator(simulator_name, simulator_prefix) - is_fli = simulator_name in FLI_SIMULATORS - is_vhpi = simulator_name in VHPI_SIMULATORS + is_fli = interface == "FLI" + is_vhpi = interface == "VHPI" if (is_fli or is_vhpi) and simulator_prefix is None: raise PythonBridgeError( f"The {'FLI' if is_fli else 'VHPI'} variant of the bridge is built against the {simulator_name} " @@ -105,13 +127,13 @@ def setup( # and the library, by absolute path since Questa accepts it, for the FLI; the library by # absolute path without its suffix, which Riviera-PRO adds, and the entry point registered # by native/vhpi.c for VHPI; the VHPIDIRECT library token and the entry point itself for - # NVC and GHDL. + # VHPIDIRECT. if is_fli: foreign = f"fli_{{entry_point}} {library_file!s}" elif is_vhpi: foreign = f"VHPI {_vhpi_library_token(library_file)} {{entry_point}}" else: - foreign = f"VHPIDIRECT {_vhpidirect_token(simulator_name, simulator_backend, library_file)} {{entry_point}}" + foreign = f"VHPIDIRECT {_vhpidirect_token(interface, simulator_backend, library_file)} {{entry_point}}" bridge_package = root / "vhdl" / "python_bridge_pkg.vhd" _write_if_changed(bridge_package, _render_bridge_package(foreign)) @@ -125,6 +147,67 @@ def setup( ) +def bridge_simulators(selected: Optional[str] = None) -> Dict[str, BridgeSimulator]: + """ + The simulators the bridge serves, by name: the built-in ones and every simulator VUnit knows, + from a simulator package, whose interface class defines foreign_language_interface and + native_library_hooks. The simulator packages are added to VUnit before the bridge. Only the + attributes of the selected simulator are checked, a class defining just one of them is + otherwise not a simulator the bridge serves. + """ + simulators = {name: BridgeSimulator(interface, None) for name, interface in BUILTIN_SIMULATORS.items()} + for simulator_class in SIMULATOR_FACTORY.supported_simulators(): + interface = getattr(simulator_class, INTERFACE_ATTRIBUTE, None) + hooks = getattr(simulator_class, HOOKS_ATTRIBUTE, None) + if interface is None and hooks is None: + continue + if simulator_class.name == selected: + _check_attributes(simulator_class.name, interface, hooks) + elif interface is None or hooks is None: + continue + simulators[simulator_class.name] = BridgeSimulator(interface, hooks) + return simulators + + +def select_simulator(simulators: Dict[str, BridgeSimulator], name: str) -> BridgeSimulator: + """ + The simulator of the given name from bridge_simulators. + """ + if name not in simulators: + raise PythonBridgeError( + f"The vunit-python-bridge package requires {', '.join(simulators)}, or a simulator whose interface " + f"class says how it loads a native library, which {name} does not. Its simulator interface class must " + f"define {INTERFACE_ATTRIBUTE} ({', '.join(map(repr, PACKAGE_INTERFACES))}) and " + f"{HOOKS_ATTRIBUTE}(library_file), see 'Simulator packages' in the vunit-python-bridge documentation." + ) + return simulators[name] + + +def _check_attributes(name: str, interface, hooks) -> None: + """ + Check the attributes a simulator interface class defines to be served by the bridge. + """ + for defined, missing, value in ( + (INTERFACE_ATTRIBUTE, HOOKS_ATTRIBUTE, hooks), + (HOOKS_ATTRIBUTE, INTERFACE_ATTRIBUTE, interface), + ): + if value is None: + raise PythonBridgeError( + f"The {name} simulator interface defines {defined} but not {missing}, " + "the vunit-python-bridge package needs both. Define it in the simulator interface class." + ) + if interface not in PACKAGE_INTERFACES: + raise PythonBridgeError( + f"{INTERFACE_ATTRIBUTE} of the {name} simulator interface is {interface!r}, " + f"the vunit-python-bridge package knows {', '.join(map(repr, PACKAGE_INTERFACES))}" + ) + if not callable(hooks): + raise PythonBridgeError( + f"{HOOKS_ATTRIBUTE} of the {name} simulator interface is not callable, it must be a static or " + "class method taking the library file" + ) + + def _prepare_library(simulator_prefix: Optional[Path], front_end: Optional[str]) -> Path: """ Prepare the library in the library directory, failing with a pointer to the variable @@ -141,12 +224,12 @@ def _prepare_library(simulator_prefix: Optional[Path], front_end: Optional[str]) ) from exc -def _vhpidirect_token(simulator_name, simulator_backend: Optional[str], library_file: Path) -> str: +def _vhpidirect_token(interface: Optional[str], simulator_backend: Optional[str], library_file: Path) -> str: """ The library token of the VHPIDIRECT attributes: the file name the simulator dlopen()s, or the linker flag of the GHDL backends that link the design ahead of time. """ - if simulator_name == "ghdl" and simulator_backend in GHDL_LINKING_BACKENDS: + if interface == "VHPIDIRECT_GHDL" and simulator_backend in GHDL_LINKING_BACKENDS: return "-lvunit_python_bridge" return library_file.name diff --git a/src/vunit_python_bridge/native_library.py b/src/vunit_python_bridge/native_library.py index 2c51fef..9b748a1 100644 --- a/src/vunit_python_bridge/native_library.py +++ b/src/vunit_python_bridge/native_library.py @@ -162,6 +162,8 @@ def _check_windows_python() -> None: # Machine field values of the PE (Windows executable) header _PE_MACHINE_X64 = 0x8664 _PE_MACHINE_NAMES = {_PE_MACHINE_X64: "64-bit x86 (x64)", 0x014C: "32-bit x86", 0xAA64: "ARM64"} +# The environment variables VUnit reads for the executable of NVC and GHDL +EXECUTABLE_VARIABLES = {"nvc": "NVC", "ghdl": "GHDL"} def pe_machine(path: Path) -> Optional[int]: @@ -192,8 +194,14 @@ def check_windows_64bit_simulator(simulator_name: str, simulator_prefix: Optiona if simulator_prefix is None: return # NVC and GHDL name their executable, which VUnit lets the NVC and GHDL environment variables - # override, the Questa/ModelSim and Aldec interfaces use vsim - name = os.environ.get(simulator_name.upper(), simulator_name) if simulator_name in ("nvc", "ghdl") else "vsim" + # override, the Questa/ModelSim and Aldec interfaces use vsim. The executable of a simulator + # from a VUnit package is not known here. + if simulator_name in EXECUTABLE_VARIABLES: + name = os.environ.get(EXECUTABLE_VARIABLES[simulator_name], simulator_name) + elif simulator_name in ("modelsim", "rivierapro"): + name = "vsim" + else: + return executable = Path(simulator_prefix) / (name if name.lower().endswith(".exe") else f"{name}.exe") machine = pe_machine(executable) if machine is not None and machine != _PE_MACHINE_X64: diff --git a/src/vunit_python_bridge/simulator_hooks.py b/src/vunit_python_bridge/simulator_hooks.py index e68af8e..0943fb1 100644 --- a/src/vunit_python_bridge/simulator_hooks.py +++ b/src/vunit_python_bridge/simulator_hooks.py @@ -21,7 +21,8 @@ from pathlib import Path from typing import Dict, List, Optional -from .bridge import GHDL_LINKING_BACKENDS, PythonBridge +from .bridge import GHDL_LINKING_BACKENDS, HOOKS_ATTRIBUTE, BridgeSimulator, PythonBridge +from .native_library import PythonBridgeError # The vsim flag keeping the runtime libraries Questa bundles out of the dynamic library # search path, and the help category listing every vsim flag. @@ -31,32 +32,60 @@ # Read by native/config.c, keep in sync CONFIG_FILE_VARIABLE = "VUNIT_PYTHON_BRIDGE_CONFIG" +# The keyword arguments of register_simulator_hooks of the VUnit package context +HOOK_KINDS = ("elab_flags", "run_flags", "process_flags", "run_env") -def register(context, bridge: PythonBridge) -> None: + +def register(context, bridge: PythonBridge, simulators: Dict[str, BridgeSimulator]) -> None: """ - Register the hooks of every simulator the bridge serves with the package context. + Register the hooks of every built-in simulator with the package context. The hooks the selected + simulator interface gives by native_library_hooks replace the built-in ones. """ - context.register_simulator_hooks("nvc", run_flags=_nvc_run_flags(bridge), run_env=_config_run_env(bridge)) - context.register_simulator_hooks( - "ghdl", - elab_flags=_ghdl_elab_flags(bridge), - run_env=_ghdl_run_env(bridge), - ) - context.register_simulator_hooks( - "modelsim", process_flags=_modelsim_process_flags(), run_env=_config_run_env(bridge) - ) + hooks = { + "nvc": dict(run_flags=_nvc_run_flags(bridge), run_env=_config_run_env(bridge)), + "ghdl": dict(elab_flags=_ghdl_elab_flags(bridge), run_env=_ghdl_run_env(bridge)), + "modelsim": dict(process_flags=_modelsim_process_flags(), run_env=_config_run_env(bridge)), + } + selected = simulators.get(context.simulator_name) + if selected is not None and selected.native_library_hooks is not None: + hooks[context.simulator_name] = _package_hooks(context.simulator_name, selected.native_library_hooks, bridge) + for simulator_name, kinds in hooks.items(): + context.register_simulator_hooks(simulator_name, **kinds) if context.simulator_name == "rivierapro": # Its interface has no run environment hook, but starts vsim with the environment of this # process, after the setup os.environ[CONFIG_FILE_VARIABLE] = str(bridge.config_file) -def _config_run_env(bridge: PythonBridge): +def _package_hooks(simulator_name: str, native_library_hooks, bridge: PythonBridge) -> Dict: + """ + The hooks of a simulator interface loading the library, with the configuration added to its + run environment. + """ + hooks = native_library_hooks(bridge.library_file) + if not isinstance(hooks, dict): + raise PythonBridgeError( + f"{HOOKS_ATTRIBUTE} of the {simulator_name} simulator interface returned {type(hooks).__name__}, " + f"it must return a dict with any of the keys {', '.join(HOOK_KINDS)}" + ) + unknown = sorted(str(kind) for kind in hooks if kind not in HOOK_KINDS) + if unknown: + raise PythonBridgeError( + f"{HOOKS_ATTRIBUTE} of the {simulator_name} simulator interface returned the unknown keys " + f"{', '.join(unknown)}, the keys are {', '.join(HOOK_KINDS)}" + ) + return dict(hooks, run_env=_config_run_env(bridge, hooks.get("run_env"))) + + +def _config_run_env(bridge: PythonBridge, simulator_run_env=None): """ - Tell the bridge library where the configuration of the project is. + Tell the bridge library where the configuration of the project is, on top of the environment + a simulator_run_env hook gives. """ - def hook(simulator_interface, env: Dict[str, str]) -> Dict[str, str]: # pylint: disable=unused-argument + def hook(simulator_interface, env: Dict[str, str]) -> Dict[str, str]: + if simulator_run_env is not None: + env = simulator_run_env(simulator_interface, env) env = dict(env) env[CONFIG_FILE_VARIABLE] = str(bridge.config_file) return env diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index 8e781f9..7947b81 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -30,6 +30,8 @@ else: import tomli as tomllib # A dependency of VUnit before Python 3.11 +from vunit.sim_if.factory import SimulatorFactory + import vunit_python_bridge from vunit_python_bridge import bridge as bridge_setup, file_base, native_library, simulator_hooks @@ -257,32 +259,26 @@ def test_generated_packages_are_up_to_date(self): self.assertEqual((generator.SRC_PATH / name).read_text(encoding="utf-8"), text, f"regenerate {name}") -class TestForeignLanguageInterfaces(unittest.TestCase): +class TestBuiltinSimulators(unittest.TestCase): """ - The table deciding how a simulator is served. VUnit only says which simulator was - selected, so the package has to know the foreign language interface of each of them. + The table of the built-in simulators. VUnit only says which simulator was selected, so the + package has to know the foreign language interface of each of them. """ def test_every_supported_simulator_has_its_interface(self): self.assertEqual( - vunit_python_bridge.FOREIGN_LANGUAGE_INTERFACES, + bridge_setup.BUILTIN_SIMULATORS, { - "nvc": "VHPIDIRECT_NVC", + "nvc": "VHPIDIRECT", "ghdl": "VHPIDIRECT_GHDL", "modelsim": "FLI", "rivierapro": "VHPI", }, ) - def test_the_simulators_the_bridge_serves_are_the_ones_it_is_set_up_for(self): - # Every simulator is served by the bridge, which has to know the same simulators. - self.assertEqual( - set(vunit_python_bridge.FOREIGN_LANGUAGE_INTERFACES), set(bridge_setup.BRIDGE_SIMULATORS) - ) - def test_the_message_names_every_simulator(self): - for simulator in ("NVC", "GHDL", "Questa/ModelSim", "Riviera-PRO"): - self.assertIn(simulator, vunit_python_bridge.SUPPORTED_SIMULATORS) + with self.assertRaisesRegex(RuntimeError, "nvc, ghdl, modelsim, rivierapro, or a simulator"): + bridge_setup.select_simulator(bridge_setup.bridge_simulators(), "xsim") class TestPackageSetup(unittest.TestCase): @@ -306,9 +302,7 @@ def _simulator(name): """ A simulator interface class as the setup function sees it, that is by name. """ - simulator = mock.Mock() - simulator.name = name - return simulator + return types.SimpleNamespace(name=name) def _added_names(self, context): return {path.name for _, path in context.added_files} @@ -319,10 +313,12 @@ def test_rejects_simulator_class_none(self): def test_rejects_unsupported_simulator(self): context = self._context(self._simulator("some_simulator")) - with self.assertRaisesRegex(RuntimeError, "no foreign language interface for some_simulator") as ctx: + with self.assertRaisesRegex(RuntimeError, "which some_simulator does not") as ctx: vunit_python_bridge.setup(context) - # The message names the simulators so a user knows what is supported. - self.assertIn(vunit_python_bridge.SUPPORTED_SIMULATORS, str(ctx.exception)) + # The message names the simulators and what a simulator package has to define. + self.assertIn(", ".join(bridge_setup.BUILTIN_SIMULATORS), str(ctx.exception)) + self.assertIn("foreign_language_interface", str(ctx.exception)) + self.assertIn("native_library_hooks(library_file)", str(ctx.exception)) def _fake_bridge(self): return bridge_setup.PythonBridge( @@ -334,7 +330,7 @@ def _fake_bridge(self): ], ) - def _check_bridge_files_added(self, simulator, backend=None): + def _check_bridge_files_added(self, simulator, backend=None, interface=None): """ The generated bridge files are added for a simulator. """ @@ -352,6 +348,7 @@ def _check_bridge_files_added(self, simulator, backend=None): simulator.name, context.simulator_prefix, context.simulator_backend, + bridge_setup.BUILTIN_SIMULATORS[simulator.name] if interface is None else interface, ) for expected in fake_bridge.vhdl_files: @@ -417,6 +414,187 @@ def listing(): self.assertEqual(listing(), before) +class _PackageSimulator: + """ + The interface class of a simulator provided by a VUnit package, saying how it loads a native library. + """ + + name = "pkgsim" + compile_options = [] + sim_options = [] + foreign_language_interface = "VHPIDIRECT" + + @staticmethod + def native_library_hooks(library_file): + return { + "run_flags": lambda simulator_interface: [f"--load={library_file!s}"], + "run_env": lambda simulator_interface, env: dict(env, PKGSIM="1"), + } + + +def _package_simulator(**attributes): + """ + A _PackageSimulator with some of its attributes replaced, None removing one. + """ + return type("Simulator", (_PackageSimulator,), attributes) + + +def _factory(*simulator_classes): + """ + A stand-in for the VUnit simulator factory knowing the given simulator classes. + """ + return types.SimpleNamespace(supported_simulators=lambda: list(simulator_classes)) + + +def _simulators(*simulator_classes, selected=None): + """ + The simulators the bridge serves when VUnit knows the given simulator classes, the built-in + VUnit simulators left out. + """ + with mock.patch.object(bridge_setup, "SIMULATOR_FACTORY", _factory(*simulator_classes)): + return bridge_setup.bridge_simulators(selected) + + +class TestSimulatorPackages(unittest.TestCase): + """ + The bridge serves the simulators VUnit knows whose interface class says how by + foreign_language_interface and native_library_hooks(library_file), as well as the built-in ones. + """ + + library_file = Path("/cache/libvunit_python_bridge.so") + config_file = Path("/out/python_bridge/vunit_python_bridge.cfg") + + def _register(self, simulator_class, *other_classes): + """ + The hooks registered with simulator_class selected. + """ + context = _FakeContext(simulator_class, Path("/out"), Path("/run.py")) + bridge = bridge_setup.PythonBridge(self.library_file, self.config_file, []) + simulators = _simulators(simulator_class, *other_classes, selected=simulator_class.name) + simulator_hooks.register(context, bridge, simulators) + return context.hooks + + @unittest.skipUnless(hasattr(SimulatorFactory, "register_simulator"), "VUnit has no simulator packages") + def test_the_simulators_are_collected_from_vunit(self): + # A simulator registered in a VUnit simulator factory, as a package does + factory = SimulatorFactory() + factory.register_simulator(_PackageSimulator) + with mock.patch.object(bridge_setup, "SIMULATOR_FACTORY", factory): + simulators = bridge_setup.bridge_simulators() + self.assertEqual(list(simulators), [*bridge_setup.BUILTIN_SIMULATORS, "pkgsim"]) + self.assertEqual(simulators["pkgsim"].interface, "VHPIDIRECT") + self.assertIs(simulators["pkgsim"].native_library_hooks, _PackageSimulator.native_library_hooks) + + def test_the_built_in_simulators_are_served_without_hooks(self): + # Also when VUnit knows simulators that do not define the attributes + simulators = _simulators(types.SimpleNamespace(name="xsim")) + interfaces = {name: simulator.interface for name, simulator in simulators.items()} + self.assertEqual(interfaces, bridge_setup.BUILTIN_SIMULATORS) + self.assertEqual({simulator.native_library_hooks for simulator in simulators.values()}, {None}) + + def test_the_interface_is_taken_from_the_simulator_class(self): + for interface in ("VHPIDIRECT", "FLI", "VHPI"): + simulators = _simulators(_package_simulator(foreign_language_interface=interface), selected="pkgsim") + self.assertEqual(bridge_setup.select_simulator(simulators, "pkgsim").interface, interface) + + def test_the_simulator_class_takes_precedence_over_the_built_in_simulator(self): + simulator = _package_simulator(name="nvc", foreign_language_interface="FLI") + self.assertEqual(_simulators(simulator, selected="nvc")["nvc"].interface, "FLI") + hooks = self._register(simulator)["nvc"] + # The hooks of the simulator replace the built-in ones, so the library is loaded once + self.assertEqual(hooks["run_flags"](mock.Mock()), [f"--load={self.library_file!s}"]) + self.assertEqual(hooks["run_env"](mock.Mock(), {})["PKGSIM"], "1") + + def test_the_hooks_of_the_simulator_are_registered_with_the_configuration(self): + hooks = self._register(_PackageSimulator) + self.assertEqual(sorted(hooks), ["ghdl", "modelsim", "nvc", "pkgsim"]) + self.assertEqual(hooks["pkgsim"]["run_flags"](mock.Mock()), [f"--load={self.library_file!s}"]) + self.assertIsNone(hooks["pkgsim"]["elab_flags"]) + env = {"KEPT": "1"} + result = hooks["pkgsim"]["run_env"](mock.Mock(), env) + self.assertEqual( + result, {"KEPT": "1", "PKGSIM": "1", simulator_hooks.CONFIG_FILE_VARIABLE: str(self.config_file)} + ) + self.assertEqual(env, {"KEPT": "1"}) + + def test_a_simulator_without_a_run_env_hook_is_told_the_configuration(self): + simulator = _package_simulator(native_library_hooks=staticmethod(lambda library_file: {})) + result = self._register(simulator)["pkgsim"]["run_env"](mock.Mock(), {"KEPT": "1"}) + self.assertEqual(result, {"KEPT": "1", simulator_hooks.CONFIG_FILE_VARIABLE: str(self.config_file)}) + + def test_a_simulator_without_the_attributes_is_rejected_with_what_to_define(self): + simulators = _simulators(types.SimpleNamespace(name="xsim"), _PackageSimulator) + with self.assertRaisesRegex( + RuntimeError, + "requires nvc, ghdl, modelsim, rivierapro, pkgsim, .*which xsim does not.*define " + "foreign_language_interface.*native_library_hooks", + ): + bridge_setup.select_simulator(simulators, "xsim") + + def test_both_attributes_are_needed(self): + for missing in ("foreign_language_interface", "native_library_hooks"): + with self.assertRaisesRegex(RuntimeError, f"pkgsim simulator interface defines .* but not {missing}"): + _simulators(_package_simulator(**{missing: None}), selected="pkgsim") + + def test_a_simulator_that_is_not_selected_is_not_checked(self): + broken = ( + _package_simulator(name="half", native_library_hooks=None), + _package_simulator(name="unknown", foreign_language_interface="VHPI_DIRECT"), + _package_simulator(name="uncallable", native_library_hooks="--load"), + ) + simulators = _simulators(*broken, selected="nvc") + self.assertNotIn("half", simulators) + # Only the hooks of the selected simulator are called + called = [] + counting = _package_simulator( + name="counting", native_library_hooks=staticmethod(lambda library_file: called.append(1) or {}) + ) + hooks = self._register(_PackageSimulator, counting, *broken) + self.assertEqual(sorted(hooks), ["ghdl", "modelsim", "nvc", "pkgsim"]) + self.assertEqual(called, []) + + def test_an_unknown_interface_is_rejected(self): + with self.assertRaisesRegex(RuntimeError, "foreign_language_interface of the pkgsim .* is 'VHPI_DIRECT'"): + _simulators(_package_simulator(foreign_language_interface="VHPI_DIRECT"), selected="pkgsim") + + def test_hooks_that_cannot_be_called_are_rejected(self): + with self.assertRaisesRegex(RuntimeError, "native_library_hooks of the pkgsim .* is not callable"): + _simulators(_package_simulator(native_library_hooks="--load"), selected="pkgsim") + + def test_hooks_that_are_not_a_dict_are_rejected(self): + simulator = _package_simulator(native_library_hooks=staticmethod(lambda library_file: ["--load"])) + with self.assertRaisesRegex(RuntimeError, "returned list, it must return a dict"): + self._register(simulator) + + def test_unknown_hook_kinds_are_rejected(self): + simulator = _package_simulator(native_library_hooks=staticmethod(lambda library_file: {"sim_flags": list})) + with self.assertRaisesRegex(RuntimeError, "unknown keys sim_flags, the keys are elab_flags, run_flags"): + self._register(simulator) + + def test_vhpidirect_names_the_library_file(self): + with create_tempdir() as tempdir: + run_script = _write_run_script(tempdir / "run.py") + with mock.patch("vunit_python_bridge.bridge.prepare_library", return_value=self.library_file) as prepare: + # A backend that GHDL links ahead of time does not matter to another simulator + bridge = bridge_setup.setup(tempdir / "out", run_script, "pkgsim", "/sim/bin", "llvm", "VHPIDIRECT") + text = bridge.vhdl_files[0].read_text(encoding="utf-8") + self.assertIsNone(prepare.call_args.args[1]) + self.assertIn('"VHPIDIRECT libvunit_python_bridge.so vpy_begin"', text) + + def test_the_package_setup_serves_the_simulator(self): + with create_tempdir() as tempdir: + run_script = _write_run_script(tempdir / "run.py") + context = _FakeContext(_PackageSimulator, tempdir / "out", run_script, "/sim/bin") + fake_bridge = bridge_setup.PythonBridge(self.library_file, self.config_file, []) + with ( + mock.patch("vunit_python_bridge.bridge.setup", return_value=fake_bridge) as setup_mock, + mock.patch.object(bridge_setup, "SIMULATOR_FACTORY", _factory(_PackageSimulator)), + ): + vunit_python_bridge.setup(context) + self.assertEqual(setup_mock.call_args.args[2:], ("pkgsim", "/sim/bin", None, "VHPIDIRECT")) + self.assertEqual(context.hooks["pkgsim"]["run_flags"](mock.Mock()), [f"--load={self.library_file!s}"]) + + class TestRelativeFileBase(unittest.TestCase): """ set_relative_file_base() and the python_file_base_pkg generated by setup(), giving @@ -446,9 +624,7 @@ def _setup(self, name="nvc"): Set the package up for a simulator, with bridge.setup() stubbed, and return the context and the generated package. """ - simulator = mock.Mock() - simulator.name = name - context = _FakeContext(simulator, self.tempdir / "out", self.run_script, "/sim/bin") + context = _FakeContext(types.SimpleNamespace(name=name), self.tempdir / "out", self.run_script, "/sim/bin") fake_bridge = bridge_setup.PythonBridge( library_file=Path("/fake/libvunit_python_bridge.so"), config_file=Path("/fake/vunit_python_bridge.cfg"), @@ -697,7 +873,7 @@ def test_no_simulator_prefix_for_vhpidirect(self): self.assertIsNone(prepare_mock.call_args.args[1]) def test_unsupported_simulator_raises(self): - with self.assertRaisesRegex(RuntimeError, "Questa/ModelSim or Riviera-PRO, it is not supported for xsim"): + with self.assertRaisesRegex(RuntimeError, "which xsim does not"): self._setup("xsim") def test_vhpi_attributes_name_the_library_without_its_suffix_and_the_entry_point(self): @@ -1391,6 +1567,12 @@ def test_vsim_is_checked_for_questa(self): with self.assertRaisesRegex(native_library.PythonBridgeError, "vsim.exe is 32-bit x86"): self._check("modelsim", str(tempdir)) + def test_the_executable_of_a_simulator_package_is_not_checked(self): + with create_tempdir() as tempdir: + for name in ("pkgsim.exe", "vsim.exe"): + self._write_executable(tempdir / name, 0x014C) + self._check("pkgsim", str(tempdir)) + def test_not_checked_outside_windows(self): with create_tempdir() as tempdir: self._write_executable(tempdir / "ghdl.exe", 0x014C) @@ -1426,7 +1608,7 @@ def test_every_simulator_is_told_where_the_configuration_is(self): def _hooks(self, library_file): bridge = bridge_setup.PythonBridge(library_file=library_file, config_file=self.config_file, vhdl_files=[]) context = _FakeContext(None, Path("/out"), Path("/run.py")) - simulator_hooks.register(context, bridge) + simulator_hooks.register(context, bridge, bridge_setup.bridge_simulators()) return context.hooks @staticmethod