TestFactory
is cocotb-y way for test sweep with different configuration (ie parameters). As part of Cocotb 2.0, TestFactory
is deprecated and parameterize
is recommended instead.
Starting with the deprecation messages for TestFactory
def __init__(self, test_function: F, *args: Any, **kwargs: Any) -> None:
warnings.warn(
"TestFactory is deprecated, use `@cocotb.parameterize` instead",
DeprecationWarning,
stacklevel=2,
)
Jumping to parameterize
decorator where the documentation pretty much sums it up. The test takes the parameters which is cross combination of arg1
and arg2
.
@cocotb.test(
skip=False,
)
@cocotb.parameterize(
arg1=[0, 1],
arg2=["a", "b"],
)
async def my_test(arg1: int, arg2: str) -> None: ...
def parameterize(
*options_by_tuple: Union[
Tuple[str, Sequence[Any]], Tuple[Sequence[str], Sequence[Sequence[Any]]]
],
**options_by_name: Sequence[Any],
) -> Callable[[F], _Parameterized[F]]:
"""Decorator to generate parameterized tests from a single test function.
Decorates a test function with named test parameters.
The call to ``parameterize`` should include the name of each test parameter and the possible values each parameter can hold.
This will generate a test for each of the Cartesian products of the parameters and their values.
.. code-block:: python3
@cocotb.test(
skip=False,
)
@cocotb.parameterize(
arg1=[0, 1],
arg2=["a", "b"],
)
async def my_test(arg1: int, arg2: str) -> None: ...
The above is equivalent to the following.
.. code-block:: python3
@cocotb.test(skip=False)
async def my_test_0_a() -> None:
arg1, arg2 = 0, "a"
...
@cocotb.test(skip=False)
async def my_test_0_b() -> None:
arg1, arg2 = 0, "b"
...
@cocotb.test(skip=False)
async def my_test_1_a() -> None:
arg1, arg2 = 1, "a"
...
@cocotb.test(skip=False)
async def my_test_1_b() -> None:
arg1, arg2 = 1, "b"
...
Options can also be specified in much the same way that :meth:`TestFactory.add_option <cocotb.regression.TestFactory.add_option>` can,
either by supplying tuples of the parameter name to values,
or a sequence of variable names and a sequence of values.
.. code-block:: python3
@cocotb.parameterize(
("arg1", [0, 1]),
(("arg2", arg3"), [(1, 2), (3, 4)])
)
Args:
options_by_tuple:
Tuple of parameter name to sequence of values for that parameter,
or tuple of sequence of parameter names to sequence of sequences of values for that pack of parameters.
options_by_name:
Mapping of parameter name to sequence of values for that parameter.
.. versionadded:: 2.0
"""
# check good inputs
for i, option_by_tuple in enumerate(options_by_tuple):
if len(option_by_tuple) != 2:
raise ValueError(
f"Invalid option tuple {i}, expected exactly two fields `(name, values)`"
)
name, values = option_by_tuple
if not isinstance(name, str):
for n in name:
if not n.isidentifier():
raise ValueError("Option names must be valid Python identifiers")
values = cast(Sequence[Sequence[Any]], values)
for value in values:
if len(name) != len(value):
raise ValueError(
f"Invalid option tuple {i}, mismatching number of parameters ({name}) and values ({value})"
)
elif not name.isidentifier():
raise ValueError("Option names must be valid Python identifiers")
options = [*options_by_tuple, *options_by_name.items()]
def wrapper(f: F) -> _Parameterized[F]:
return _Parameterized(f, options)
return wrapper
Digging deeper into how parameterize
works, there is generate_tests
that yields Test
with test_kwargs
created from options
passed to parameterize
def generate_tests(
self,
*,
name: Optional[str] = None,
timeout_time: Optional[float] = None,
timeout_unit: str = "step",
expect_fail: bool = False,
expect_error: Union[Type[Exception], Sequence[Type[Exception]]] = (),
skip: bool = False,
stage: int = 0,
_expect_sim_failure: bool = False,
) -> Iterable[Test]:
test_func_name = self.test_function.__qualname__ if name is None else name
# this value is a list of ranges of the same length as each set of values in self.options for passing to itertools.product
option_indexes = [range(len(option[1])) for option in self.options]
# go through the cartesian product of all values of all options
for selected_options in product(*option_indexes):
test_kwargs: Dict[str, Sequence[Any]] = {}
test_name_pieces: List[str] = [test_func_name]
for option_idx, select_idx in enumerate(selected_options):
option_name, option_values = self.options[option_idx]
selected_value = option_values[select_idx]
if isinstance(option_name, str):
# single params per option
selected_value = cast(Sequence[Any], selected_value)
test_kwargs[option_name] = selected_value
test_name_pieces.append(
f"/{option_name}={self._option_reprs[option_name][select_idx]}"
)
else:
# multiple params per option
selected_value = cast(Sequence[Any], selected_value)
for n, v in zip(option_name, selected_value):
test_kwargs[n] = v
test_name_pieces.append(
f"/{n}={self._option_reprs[n][select_idx]}"
)
parameterized_test_name = "".join(test_name_pieces)
# create wrapper function to bind kwargs
@functools.wraps(self.test_function)
async def _my_test(dut, kwargs: Dict[str, Any] = test_kwargs) -> None:
await self.test_function(dut, **kwargs)
yield Test(
func=_my_test,
name=parameterized_test_name,
timeout_time=timeout_time,
timeout_unit=timeout_unit,
expect_fail=expect_fail,
expect_error=expect_error,
skip=skip,
stage=stage,
_expect_sim_failure=_expect_sim_failure,
)
So, where is generate_tests
is called? Jumping to test discovery, where _add_tests
is called to added the generated test to listed of discovered tests.
def _add_tests(module_name: str, *tests: Test) -> None:
mod = sys.modules[module_name]
if not hasattr(mod, "__cocotb_tests__"):
mod.__cocotb_tests__ = []
mod.__cocotb_tests__.extend(tests)
if _func is not None:
if isinstance(_func, _Parameterized):
test_func = _func.test_function
_add_tests(test_func.__module__, *_func.generate_tests())
return test_func
else:
_add_tests(_func.__module__, Test(func=_func))
return _func
Going one level higher to the test discovery where _add_tests
is called to start the whole thing.
def wrapper(f: Union[F, _Parameterized[F]]) -> F:
if isinstance(f, _Parameterized):
test_func = f.test_function
_add_tests(
test_func.__module__,
*f.generate_tests(
name=name,
timeout_time=timeout_time,
timeout_unit=timeout_unit,
expect_fail=expect_fail,
expect_error=expect_error,
skip=skip,
stage=stage,
_expect_sim_failure=_expect_sim_failure,
),
)
return test_func
else:
_add_tests(
f.__module__,
Test(
func=f,
name=name,
timeout_time=timeout_time,
timeout_unit=timeout_unit,
expect_fail=expect_fail,
expect_error=expect_error,
skip=skip,
stage=stage,
_expect_sim_failure=_expect_sim_failure,
),
)
return f
return wrapper