Cocotb 2.1 introduced several new decorators, so I wanted to document both the old and new ones in one place for future reference.

@test Link to heading

In src/cocotb/_decorators.py, test is the original decorator for marking an async function as a cocotb test. Cocotb uses it to discover tests at startup.

def test(
    obj: TestFuncType | TestGenerator | None = None,
    *,
    timeout_time: float | None = None,
    timeout_unit: TimeUnit = "step",
    expect_fail: bool | None = None,
    expect_error: type[BaseException] | tuple[type[BaseException], ...] | None = None,
    skip: bool | None = None,
    stage: int | None = None,
    name: str | None = None,
) -> TestGenerator | Callable[[TestFuncType | TestGenerator], TestGenerator]:

timeout_time and timeout_unit Link to heading

The timeout options can stop a test that hangs. Set them long enough for the stimulus to propagate through the DUT.

            @cocotb.test(timeout_time=10, timeout_unit="ns")
            async def test1(dut):
              ...

expect_fail Link to heading

For a negative test case, or when the DUT does not yet support a feature, mark the test as expected to fail with expect_fail=True.

            @cocotb.test(expect_fail=True)
            async def test1(dut):
              ...

The documentation describes what cocotb considers a test failure:

        expect_fail:
            If ``True`` and the test fails a functional check via an :keyword:`assert` statement, :class:`pytest.raises`,
            :class:`pytest.warns`, or :class:`pytest.deprecated_call` the test is considered to have passed.
            If ``True`` and the test passes successfully, the test is considered to have failed.

skip Link to heading

skip=True excludes a test from normal discovery and execution. You can still run it explicitly by setting TEST=<test_name>.

            @cocotb.test(skip=True)
            async def test1(dut):
              ...

stage Link to heading

I haven’t tried this one yet, but it seems to group tests into stages and run them in order. This could be useful for separating basic tests, mission-mode tests, and corner cases.

            @cocotb.test(stage=0)
            async def test1(dut):
              ...

            @cocotb.test(stage=1)
            async def test1(dut):
              ...

@parametrize and Param Link to heading

@cocotb.parametrize generates a test for each parameter value. For example, mode=[0, 1] generates tests named test_mode/0 and test_mode/1. Use cocotb.Param to give each value a readable name, such as test_mode/basic and test_mode/advanced.

        @cocotb.test
        @cocotb.parametrize(
            mode=[
                cocotb.Param(value=0, name="basic"),
                cocotb.Param(value=1, name="advanced"),
            ],
        )
        async def test_mode(dut, mode: int) -> None:
            ...

@skipif Link to heading

Introduced in 2.1, skipif lets you skip a test conditionally and provide a reason. The condition can depend on signals from the design, and the reason can explain why a feature is unsupported.

        @cocotb.skipif(
            cocotb.top.USE_MY_FEATURE.value != 1,
            reason="The design doesn't support my feature.",
        )
        @cocotb.test
        async def test_my_feature(dut) -> None: ...
    # Set cocotb.top if the root_name matches one of the handles
    if root_name is not None:
        target_name = root_name.casefold()
        for name in cocotb.tops:
            if name.casefold() == target_name:
                cocotb.top = cocotb.tops[name]
                return

@xfail Link to heading

Like skipif, xfail is conditional. It lets you mark a test as expected to fail and optionally specify a reason and the exception type that qualifies as an expected failure.

def xfail(
    condition: bool = True,
    *,
    reason: str | None = None,
    raises: type[BaseException]
    | pytest.RaisesExc
    | pytest.RaisesGroup
    | Iterable[type[BaseException]]
    | None = None,
) -> Callable[[TestFuncType | TestGenerator], TestGenerator]:
        @cocotb.xfail(
            cocotb.top.USE_MY_FEATURE.value != 0,
            raises=IndexError,
            reason="My design is not configured to use the new feature, so the model will eventually fail",
        )
        @cocotb.test
        async def test_design_feature(dut): ...

Note: The conditions in xfail and skipif need access to the design during test discovery, before the test function is called and the dut argument is passed in. For this, cocotb provides cocotb.top, a singleton handle to the design’s top-level, and cocotb.tops, a dictionary that can be used when there are multiple top-levels.