不要使用 from cocotb_test.simulator import run
# test_runner.py
import os
from pathlib import Path
from cocotb.runner import get_runner
def test_my_design_runner():
sim = os.getenv("SIM", "icarus")
proj_path = Path(__file__).resolve().parent
sources = [proj_path / "my_design.sv"]
runner = get_runner(sim)
runner.build(
sources=sources,
hdl_toplevel="my_design",
)
runner.test(hdl_toplevel="my_design", test_module="test_my_design,")
if __name__ == "__main__":
test_my_design_runner()
https://docs.cocotb.org/en/latest/_modules/cocotb_tools/runner.html#Runner
class Runner(ABC):
supported_gpi_interfaces: Dict[str, List[str]] = {}
def __init__(self) -> None:
self._simulator_in_path()
self.env: Dict[str, str] = {}
# for running test() independently of build()
self.build_dir: Path = get_abs_path("sim_build")
self.parameters: Mapping[str, object] = {}
self.log = logging.getLogger(type(self).__qualname__)
@abstractmethod
def _simulator_in_path(self) -> None:
"""Raise exception if the simulator executable does not exist in :envvar:`PATH`.
Raises:
SystemExit: Simulator executable does not exist in :envvar:`PATH`.
"""
def _check_hdl_toplevel_lang(self, hdl_toplevel_lang: Optional[str]) -> str:
"""Return *hdl_toplevel_lang* if supported by simulator, raise exception otherwise.
Returns:
*hdl_toplevel_lang* if supported by the simulator.
Raises:
ValueError: *hdl_toplevel_lang* is not supported by the simulator.
"""
if hdl_toplevel_lang is None:
if self.vhdl_sources and not self.verilog_sources and not self.sources:
lang = "vhdl"
elif self.verilog_sources and not self.vhdl_sources and not self.sources:
lang = "verilog"
elif self.sources and not self.vhdl_sources and not self.verilog_sources:
if is_vhdl_source(self.sources[-1]):
lang = "vhdl"
elif is_verilog_source(self.sources[-1]):
lang = "verilog"
else:
raise UnknownFileExtension(self.sources[-1])
else:
raise ValueError(
f"{type(self).__qualname__}: Must specify a hdl_toplevel_lang in a mixed-language design"
)
else:
lang = hdl_toplevel_lang
if lang in self.supported_gpi_interfaces.keys():
return lang
else:
raise ValueError(
f"{type(self).__qualname__}: hdl_toplevel_lang {hdl_toplevel_lang!r} is not "
f"in supported list: {', '.join(self.supported_gpi_interfaces.keys())}"
)
def _set_env(self) -> None:
"""Set environment variables for sub-processes."""
for e in os.environ:
self.env[e] = os.environ[e]
if "LIBPYTHON_LOC" not in self.env:
libpython_path = find_libpython.find_libpython()
if not libpython_path:
raise ValueError(
"Unable to find libpython, please make sure the appropriate libpython is installed"
)
self.env["LIBPYTHON_LOC"] = libpython_path
self.env["PATH"] += os.pathsep + str(cocotb_tools.config.libs_dir)
self.env["PYTHONPATH"] = os.pathsep.join(sys.path)
self.env["PYTHONHOME"] = sys.prefix
self.env["COCOTB_TOPLEVEL"] = self.sim_hdl_toplevel
self.env["COCOTB_TEST_MODULES"] = self.test_module
self.env["TOPLEVEL_LANG"] = self.hdl_toplevel_lang
@abstractmethod
def _build_command(self) -> Sequence[_Command]:
"""Return command to build the HDL sources."""
@abstractmethod
def _test_command(self) -> Sequence[_Command]:
"""Return command to run a test."""
[docs]
def build(
self,
hdl_library: str = "top",
verilog_sources: Sequence[PathLike] = [],
vhdl_sources: Sequence[PathLike] = [],
sources: Sequence[Union[PathLike, VHDL, Verilog]] = [],
includes: Sequence[PathLike] = [],
defines: Mapping[str, object] = {},
parameters: Mapping[str, object] = {},
build_args: Sequence[Union[str, VHDL, Verilog]] = [],
hdl_toplevel: Optional[str] = None,
always: bool = False,
build_dir: PathLike = "sim_build",
clean: bool = False,
verbose: bool = False,
timescale: Optional[Tuple[str, str]] = None,
waves: bool = False,
log_file: Optional[PathLike] = None,
) -> None:
"""Build the HDL sources.
With mixed language simulators, *sources* will be built,
followed by *vhdl_sources*, then *verilog_sources*.
With simulators that only support either VHDL or Verilog, *sources* will be built,
followed by *vhdl_sources* and *verilog_sources*, respectively.
If your source files use an atypical file extension,
use :class:`VHDL` and :class:`Verilog` to tag the path as a VHDL or Verilog source file, respectively.
If the filepaths aren't tagged, the extension is used to determine if they are VHDL or Verilog files.
+----------+------------------------------------+
| Language | File Extensions |
+==========+====================================+
| VHDL | ``.vhd``, ``.vhdl`` |
+----------+------------------------------------+
| Verilog | ``.v``, ``.sv``, ``.vh``, ``.svh`` |
+----------+------------------------------------+
.. code-block:: python3
runner.build(
sources=[
VHDL("/my/file.is_actually_vhdl"),
Verilog("/other/file.verilog"),
],
)
The same tagging works for *build_args*.
Tagged *build_args* only supply that option to the compiler when building the source file for the tagged language.
Non-tagged *build_args* are supplied when compiling any language.
Args:
hdl_library: The library name to compile into.
verilog_sources: Verilog source files to build.
vhdl_sources: VHDL source files to build.
sources: Language-agnostic list of source files to build.
includes: Verilog include directories.
defines: Defines to set.
parameters: Verilog parameters or VHDL generics.
build_args: Extra build arguments for the simulator.
hdl_toplevel: The name of the HDL toplevel module.
always: Always run the build step.
build_dir: Directory to run the build step in.
clean: Delete *build_dir* before building.
verbose: Enable verbose messages.
timescale: Tuple containing time unit and time precision for simulation.
waves: Record signal traces.
log_file: File to write the build log to.
.. deprecated:: 2.0
Uses of the *verilog_sources* and *vhdl_sources* parameters should be replaced with the language-agnostic *sources* argument.
"""
self.clean: bool = clean
self.build_dir = get_abs_path(build_dir)
if self.clean:
self.rm_build_folder(self.build_dir)
os.makedirs(self.build_dir, exist_ok=True)
# note: to avoid mutating argument defaults, we ensure that no value
# is written without a copy. This is much more concise and leads to
# a better docstring than using `None` as a default in the parameters
# list.
self.hdl_library: str = hdl_library
if verilog_sources:
warnings.warn(
"Simulator.build *verilog_sources* parameter is deprecated. Use the language-agnostic *sources* parameter instead.",
DeprecationWarning,
stacklevel=2,
)
self.verilog_sources: List[Path] = get_abs_paths(verilog_sources)
if vhdl_sources:
warnings.warn(
"Simulator.build *vhdl_sources* parameter is deprecated. Use the language-agnostic *sources* parameter instead.",
DeprecationWarning,
stacklevel=2,
)
self.vhdl_sources: List[Path] = get_abs_paths(vhdl_sources)
self.sources: List[Path] = get_abs_paths(sources)
self.includes: List[Path] = get_abs_paths(includes)
self.defines = dict(defines)
self.parameters = dict(parameters)
self.build_args = list(build_args)
self.always: bool = always
self.hdl_toplevel: Optional[str] = hdl_toplevel
self.verbose: bool = verbose
self.timescale: Optional[Tuple[str, str]] = timescale
self.log_file: Optional[PathLike] = log_file
self.waves = waves
self.env.update(os.environ)
cmds: Sequence[_Command] = self._build_command()
self._execute(cmds, cwd=self.build_dir)
[docs]
def test(
self,
test_module: Union[str, Sequence[str]],
hdl_toplevel: str,
hdl_toplevel_library: str = "top",
hdl_toplevel_lang: Optional[str] = None,
gpi_interfaces: Optional[List[str]] = None,
testcase: Optional[Union[str, Sequence[str]]] = None,
seed: Optional[Union[str, int]] = None,
elab_args: Sequence[str] = [],
test_args: Sequence[str] = [],
plusargs: Sequence[str] = [],
extra_env: Mapping[str, str] = {},
waves: bool = False,
gui: bool = False,
parameters: Optional[Mapping[str, object]] = None,
build_dir: Optional[PathLike] = None,
test_dir: Optional[PathLike] = None,
results_xml: Optional[str] = None,
pre_cmd: Optional[List[str]] = None,
verbose: bool = False,
timescale: Optional[Tuple[str, str]] = None,
log_file: Optional[PathLike] = None,
test_filter: Optional[str] = None,
) -> Path:
"""Run the tests.
Args:
test_module: Name(s) of the Python module(s) containing the tests to run.
Can be a comma-separated list.
hdl_toplevel: Name of the HDL toplevel module.
hdl_toplevel_library: The library name for HDL toplevel module.
hdl_toplevel_lang: Language of the HDL toplevel module.
gpi_interfaces: List of GPI interfaces to use, with the first one being the entry point.
testcase: Name(s) of a specific testcase(s) to run.
If not set, run all testcases found in *test_module*.
Can be a comma-separated list.
seed: A specific random seed to use.
elab_args: A list of elaboration arguments for the simulator.
test_args: A list of extra arguments for the simulator.
plusargs: 'plusargs' to set for the simulator.
extra_env: Extra environment variables to set.
waves: Record signal traces.
gui: Run with simulator GUI.
parameters: Verilog parameters or VHDL generics.
build_dir: Directory the build step has been run in.
test_dir: Directory to run the tests in.
results_xml: Name of xUnit XML file to store test results in.
If an absolute path is provided it will be used as-is,
``{build_dir}/results.xml`` otherwise.
This argument should not be set when run with ``pytest``.
verbose: Enable verbose messages.
pre_cmd: Commands to run before simulation begins.
Typically Tcl commands for simulators that support them.
timescale: Tuple containing time unit and time precision for simulation.
log_file: File to write the test log to.
test_filter: Regular expression which matches test names.
Only matched tests are run if this argument if given.
Returns:
The absolute location of the results XML file which can be
defined by the *results_xml* argument.
"""
__tracebackhide__ = True # Hide the traceback when using pytest
if build_dir is not None:
self.build_dir = get_abs_path(build_dir)
if parameters is not None:
self.parameters = dict(parameters)
if test_dir is None:
self.test_dir = self.build_dir
else:
self.test_dir = get_abs_path(test_dir)
os.makedirs(self.test_dir, exist_ok=True)
if isinstance(test_module, str):
self.test_module = test_module
else:
self.test_module = ",".join(test_module)
# note: to avoid mutating argument defaults, we ensure that no value
# is written without a copy. This is much more concise and leads to
# a better docstring than using `None` as a default in the parameters
# list.
self.sim_hdl_toplevel = hdl_toplevel
self.hdl_toplevel_library: str = hdl_toplevel_library
self.hdl_toplevel_lang = self._check_hdl_toplevel_lang(hdl_toplevel_lang)
if gpi_interfaces:
self.gpi_interfaces = gpi_interfaces
else:
self.gpi_interfaces = []
for gpi_if in self.supported_gpi_interfaces.values():
self.gpi_interfaces.append(gpi_if[0])
self.pre_cmd = pre_cmd
self.elab_args = list(elab_args)
self.test_args = list(test_args)
self.plusargs = list(plusargs)
self.env = dict(extra_env)
if testcase is not None:
if isinstance(testcase, str):
self.env["COCOTB_TESTCASE"] = testcase
else:
self.env["COCOTB_TESTCASE"] = ",".join(testcase)
if test_filter is not None:
self.env["COCOTB_TEST_FILTER"] = test_filter
if seed is not None:
self.env["COCOTB_RANDOM_SEED"] = str(seed)
self.log_file = log_file
self.waves = waves
self.gui = gui
self.timescale = timescale
if verbose is not None:
self.verbose = verbose
# When using pytest, use test name as result file name
pytest_current_test = os.getenv("PYTEST_CURRENT_TEST", None)
test_dir_path = Path(self.test_dir)
self.current_test_name = "test"
if results_xml is not None:
# PYTEST_CURRENT_TEST only allowed when results_xml is not set
assert not pytest_current_test
results_xml_path = Path(results_xml)
if results_xml_path.is_absolute():
results_xml_file = results_xml_path
else:
results_xml_file = test_dir_path / results_xml_path
elif pytest_current_test is not None:
self.current_test_name = pytest_current_test.split(":")[-1].split(" ")[0]
results_xml_file = test_dir_path / f"{self.current_test_name}.{results_xml}"
else:
results_xml_file = test_dir_path / "results.xml"
with suppress(OSError):
os.remove(results_xml_file)
# transport the settings to cocotb via environment variables
self._set_env()
self.env["COCOTB_RESULTS_FILE"] = str(results_xml_file)
cmds: Sequence[_Command] = self._test_command()
simulator_exit_code: int = 0
try:
self._execute(cmds, cwd=self.test_dir)
except subprocess.CalledProcessError as e:
# It is possible for the simulator to fail but still leave results.
self.log.error("Simulation failed: %d", e.returncode)
simulator_exit_code = e.returncode
# Only when running under pytest, check the results file here,
# potentially raising an exception with failing testcases,
# otherwise return the results file for later analysis.
if pytest_current_test:
try:
(num_tests, num_failed) = get_results(results_xml_file)
except RuntimeError as e:
self.log.error("%s", e.args[0])
sys.exit(simulator_exit_code)
else:
if num_failed:
self.log.error(
"ERROR: Failed %d of %d tests.", num_failed, num_tests
)
sys.exit(1 if simulator_exit_code == 0 else simulator_exit_code)
if simulator_exit_code != 0:
sys.exit(simulator_exit_code)
self.log.info("Results file: %s", results_xml_file)
return results_xml_file
@abstractmethod
def _get_include_options(self, includes: Sequence[PathLike]) -> _Command:
"""Return simulator-specific formatted option strings with *includes* directories."""
@abstractmethod
def _get_define_options(self, defines: Mapping[str, object]) -> _Command:
"""Return simulator-specific formatted option strings with *defines* macros."""
@abstractmethod
def _get_parameter_options(self, parameters: Mapping[str, object]) -> _Command:
"""Return simulator-specific formatted option strings with *parameters*/generics."""
def _execute(self, cmds: Sequence[_Command], cwd: PathLike) -> None:
__tracebackhide__ = True # Hide the traceback when using PyTest.
if self.log_file is None:
self._execute_cmds(cmds, cwd)
else:
with open(self.log_file, "w") as f:
self._execute_cmds(cmds, cwd, f)
def _execute_cmds(
self, cmds: Sequence[_Command], cwd: PathLike, stdout: Optional[TextIO] = None
) -> None:
__tracebackhide__ = True # Hide the traceback when using PyTest.
for cmd in cmds:
self.log.info("Running command %s in directory %s", _shlex_join(cmd), cwd)
# TODO: create a thread to handle stderr and log as error?
# TODO: log forwarding
stderr = None if stdout is None else subprocess.STDOUT
subprocess.run(
cmd, cwd=cwd, env=self.env, check=True, stdout=stdout, stderr=stderr
)
def rm_build_folder(self, build_dir: Path) -> None:
if os.path.isdir(build_dir):
self.log.info("Removing: %s", build_dir)
shutil.rmtree(build_dir, ignore_errors=True)