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)