There have been a refactor going on in Cocotb in the last few months. So, a quick write-up would be good.
C++ bootstrap Link to heading
Starting with entry point to python in file src/pygpi/entry.pysrc/pygpi/entry.py
load_entry
loads cocotb:_initialise_testbench
but will get back to that later.
def load_entry() -> Tuple[ModuleType, Callable]:
"""Gather entry point information by parsing :envvar:`PYGPI_ENTRY_POINT`."""
entry_point_str = os.environ.get(
"PYGPI_ENTRY_POINT", "cocotb:_initialise_testbench"
)
try:
if ":" not in entry_point_str:
raise ValueError(
"Invalid PYGPI_ENTRY_POINT, missing entry function (no colon)."
)
entry_module_str, entry_func_str = entry_point_str.split(":", 1)
entry_module = importlib.import_module(entry_module_str)
entry_func = reduce(getattr, entry_func_str.split("."), entry_module)
except Exception as e:
raise RuntimeError(
f"Failure to parse PYGPI_ENTRY_POINT ('{entry_point_str}')"
) from e
else:
return entry_module, entry_func
Tracing who calls first, load_entry
is called from _embed_sim_init
and rest of call trace all the way to VPI bootstrap.
extern "C" COCOTB_EXPORT int _embed_sim_init(int argc,
char const *const *argv) {
...
auto gstate = PyGILState_Ensure();
DEFER(PyGILState_Release(gstate));
to_python();
DEFER(to_simulator());
auto entry_utility_module = PyImport_ImportModule("pygpi.entry");
...
DEFER(Py_DECREF(entry_utility_module));
auto entry_info_tuple =
PyObject_CallMethod(entry_utility_module, "load_entry", NULL);
extern "C" int embed_sim_init(int argc, char const *const *argv) {
if (init_failed) {
// LCOV_EXCL_START
return -1;
// LCOV_EXCL_STOP
} else {
return _embed_sim_init(argc, argv);
}
}
void gpi_embed_init(int argc, char const *const *argv) {
if (embed_sim_init(argc, argv)) gpi_embed_end();
}
int VpiStartupCbHdl::run_callback() {
s_vpi_vlog_info info;
if (!vpi_get_vlog_info(&info)) {
LOG_WARN("Unable to get argv and argc from simulator");
info.argc = 0;
info.argv = nullptr;
}
gpi_embed_init(info.argc, info.argv);
return 0;
}
int32_t handle_vpi_callback(p_cb_data cb_data) {
gpi_to_user();
int rv = 0;
VpiCbHdl *cb_hdl = (VpiCbHdl *)cb_data->user_data;
if (!cb_hdl) {
LOG_CRITICAL("VPI: Callback data corrupted: ABORTING");
gpi_embed_end();
return -1;
}
gpi_cb_state_e old_state = cb_hdl->get_call_state();
if (old_state == GPI_PRIMED) {
cb_hdl->set_call_state(GPI_CALL);
cb_hdl->run_callback();
VpiCbHdl::VpiCbHdl(GpiImplInterface *impl) : GpiCbHdl(impl) {
vpi_time.high = 0;
vpi_time.low = 0;
vpi_time.type = vpiSimTime;
cb_data.reason = 0;
cb_data.cb_rtn = handle_vpi_callback;
cb_data.obj = NULL;
cb_data.time = &vpi_time;
cb_data.value = NULL;
cb_data.index = 0;
cb_data.user_data = (char *)this;
}
static void register_initial_callback() {
sim_init_cb = new VpiStartupCbHdl(vpi_table);
sim_init_cb->arm_callback();
}
And finally, there is the VPIvlog_startup_routines
in src/cocotb/share/lib/vpi/VpiImpl.cpp
.
COCOTBVPI_EXPORT void (*vlog_startup_routines[])() = {
register_impl, gpi_entry_point, register_initial_callback,
register_final_callback, nullptr};
Python Side Link to heading
back to load_entry
which calls _initialise_testbench
in src/cocotb/__init__.py
def _initialise_testbench(argv_): # pragma: no cover
try:
_start_library_coverage()
_initialise_testbench_(argv_)
And starts the whole trace to load tests and initialization regressionManager
def _initialise_testbench_(argv_):
# The body of this function is split in two because no coverage is collected on
# the function that starts the coverage. By splitting it in two we get coverage
# on most of the function.
global is_simulation
is_simulation = True
global argv
argv = argv_
global SIM_NAME, SIM_VERSION
SIM_NAME = simulator.get_simulator_product().strip()
SIM_VERSION = simulator.get_simulator_version().strip()
cocotb.log.info(f"Running on {SIM_NAME} version {SIM_VERSION}")
log.info(
f"Running tests with cocotb v{__version__} from {os.path.dirname(__file__)}"
)
_process_plusargs()
_process_packages()
_setup_random_seed()
_setup_root_handle()
_start_user_coverage()
_setup_regression_manager()
# setup global scheduler system
global _scheduler
_scheduler = Scheduler(test_complete_cb=regression_manager._test_complete)
# start Regression Manager
regression_manager.start_regression()
_setup_root_handle
stored top variable handle.
def _setup_root_handle() -> None:
root_name = os.getenv("TOPLEVEL")
if root_name is not None:
root_name = root_name.strip()
if root_name == "":
root_name = None
elif "." in root_name:
# Skip any library component of the toplevel
root_name = root_name.split(".", 1)[1]
from cocotb import simulator
handle = simulator.get_root_handle(root_name)
if not handle:
raise RuntimeError(f"Can not find root handle {root_name!r}")
global top
top = cocotb.handle.SimHandle(handle)
_setup_regression_manager
create RegressionManager
to discover Cocotb Test
in the loaded module.
def _setup_regression_manager() -> None:
global regression_manager
regression_manager = RegressionManager()
# discover tests
module_str = os.getenv("MODULE", "").strip()
if not module_str:
raise RuntimeError(
"Environment variable MODULE, which defines the module(s) to execute, is not defined or empty."
)
modules = [s.strip() for s in module_str.split(",") if s.strip()]
regression_manager.setup_pytest_assertion_rewriting()
regression_manager.discover_tests(*modules)
# filter tests
testcase_str = os.getenv("TESTCASE", "").strip()
test_filter_str = os.getenv("COCOTB_TEST_FILTER", "").strip()
if testcase_str and test_filter_str:
raise RuntimeError("Specify only one of TESTCASE or COCOTB_TEST_FILTER")
elif testcase_str:
warnings.warn(
"TESTCASE is deprecated in favor of COCOTB_TEST_FILTER",
DeprecationWarning,
stacklevel=2,
)
filters = [f"{s.strip()}$" for s in testcase_str.split(",") if s.strip()]
regression_manager.add_filters(*filters)
regression_manager.set_mode(RegressionMode.TESTCASE)
elif test_filter_str:
regression_manager.add_filters(test_filter_str)
regression_manager.set_mode(RegressionMode.TESTCASE)