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 byrunning_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)