This is a write-up of Cocotb test discovery mechanism including the decorator and regression runner infrastructure.

Part1: Discovery with regression runner Link to heading

Starting with __init__.py where from_discovery is called to lookup the tests.

    # start Regression Manager
    global regression_manager
    regression_manager = RegressionManager.from_discovery(top)
    regression_manager.execute()

And from_discorvery calls _discover_tests (static class method in RegressionManager)

        tests = cls._discover_tests()

_discover_tests loads test module (or modules) and detects classes derived from Test


        module_str = os.getenv('MODULE')

        ...
        ...
        modules = [s.strip() for s in module_str.split(',') if s.strip()]
        ...
        ...

        for module_name in modules:
            try:
                for thing in vars(module).values():
                    if isinstance(thing, Test): # Checks  classes derived from Test.
                        yield thing

Note that Test class is imported from cocotb.decorators

from cocotb.decorators import test as Test

Makefile must define MODULE variable, so auto-detect can load the module and extract the tests.

MODULE   := test

Part2: Decorator @cocotb.test() Link to heading

Here is an example of a simple cocotb test. which uses cocotb.test() decorator.

@cocotb.test()
async def test_foo_bar(dut):

   # clock
    c = Clock(dut.clk, 10, 'ns')
    cocotb.fork(c.start())

decorator class test is defined in decorators.py(this is difference from base class Test). test_foo_bar will be instance of class test which gets picked up by discovery above.

Looking at __init__, test routine gets wrapped with decorator before passed to super().__init__(f).

class test(coroutine, metaclass=_decorator_helper):

    def __init__(self, f, timeout_time=None, timeout_unit="step",
                 expect_fail=False, expect_error=(),
                 skip=False, stage=None):

                
            co = coroutine(f)

            # wraps the passed f. See https://stackoverflow.com/questions/308999/what-does-functools-wraps-do
            @functools.wraps(f)
            async def f(*args, **kwargs):
                running_co = co(*args, **kwargs) # pass args, kargs to coroutine

                try:
                    res = await cocotb.triggers.with_timeout(running_co, self.timeout_time, self.timeout_unit)
                except cocotb.result.SimTimeoutError:
                    running_co.kill()
                    raise
                else:
                    return res

        super().__init__(f) 

There are two important things about coroutine:

  • __init__ updates meta data of wrapped function
  • __call__ called by running_co = co(*args, **kwargs)
class coroutine:

    def __init__(self, func):
        self._func = func
        functools.update_wrapper(self, func)


    def __call__(self, *args, **kwargs):
        return RunningCoroutine(self._func(*args, **kwargs), self)