Source code for hbutils.testing.generator.func

"""
Test matrix generation utilities for parameterized test cases.

This module provides a compact interface for generating parameter combinations
used in test parameterization. It supports two modes of generation:

* :class:`MatrixMode` - Enumeration of available generation modes.
* :func:`tmatrix` - Generate parameter names and test case tuples for pytest.

The primary entry point is :func:`tmatrix`, which converts a parameter range
specification into a list of parameter names and a list of case tuples. These
results are compatible with ``pytest.mark.parametrize`` and can be used for
both full Cartesian matrix generation and AETG-based combinatorial testing.

.. note::
   The AETG algorithm can reduce the number of test cases while preserving
   combinatorial coverage of parameter interactions.

Example::

    >>> from hbutils.testing.generator.func import tmatrix
    >>> names, cases = tmatrix({
    ...     'a': [1, 2],
    ...     'b': ['x', 'y'],
    ...     ('c', 'd'): [(0, 1), (1, 2)]
    ... }, mode='aetg', level=2)
    >>> names
    ['a', 'b', 'c', 'd']
    >>> cases  # doctest: +ELLIPSIS
    [(1, 'x', 0, 1), ...]
"""

from enum import IntEnum, auto
from typing import List, Mapping, Union, Tuple, Optional, Any

from .aetg import AETGGenerator
from .matrix import MatrixGenerator
from ...model import int_enum_loads
from ...reflection import progressive_for

__all__ = ['tmatrix']


[docs] @int_enum_loads(enable_int=False, name_preprocess=str.upper) class MatrixMode(IntEnum): """ Enumeration of matrix generation modes. :cvar AETG: AETG (Automatic Efficient Test Generator) mode for combinatorial testing. :cvar MATRIX: Full Cartesian product matrix mode. """ AETG = auto() MATRIX = auto()
[docs] def tmatrix(ranges: Mapping[Union[str, Tuple[str, ...]], List[Any]], mode: Union[str, MatrixMode] = 'aetg', seed: Optional[int] = 0, level: int = 2) -> Tuple[List[str], List[Tuple[Any, ...]]]: """ Generate test matrix for parameterized testing. This function creates a test matrix that can be directly used with pytest's parametrize decorator. It supports two generation modes: AETG for efficient combinatorial testing and MATRIX for full Cartesian product generation. :param ranges: Mapping of parameter names to their possible values. Keys can be either a single string (for one parameter) or a tuple of strings (for multiple parameters that should be varied together). Values are lists of possible values for the parameter(s). :type ranges: Mapping[Union[str, Tuple[str, ...]], List[Any]] :param mode: Generation mode, accepts ``'aetg'``/``'matrix'`` (case-insensitive) or a :class:`MatrixMode` instance. Default is ``'aetg'``. :type mode: Union[str, MatrixMode] :param seed: Random seed for AETG mode. Default is ``0`` which produces deterministic results. Set to ``None`` for non-deterministic generation. :type seed: Optional[int] :param level: Coverage level for AETG algorithm, indicating the strength of combinatorial coverage (e.g., ``2`` for pairwise coverage). Default is ``2``. :type level: int :return: A tuple containing ``(parameter_names, test_cases)`` where ``parameter_names`` is a list of parameter names and ``test_cases`` is a list of tuples, each representing one test case. :rtype: Tuple[List[str], List[Tuple[Any, ...]]] :raises ValueError: If an invalid mode is specified. Examples:: >>> from hbutils.testing import tmatrix >>> names, values = tmatrix( ... { ... 'a': [2, 3], ... 'e': ['a', 'b', 'c'], ... ('b', 'c'): [(1, 7), (4, 6), (9, 12)], ... } ... ) >>> print(names) ['a', 'e', 'b', 'c'] >>> for i, v in enumerate(values): ... print(i, v) 0 (2, 'c', 9, 12) 1 (3, 'c', 4, 6) 2 (2, 'c', 1, 7) 3 (3, 'b', 9, 12) 4 (2, 'b', 4, 6) 5 (3, 'b', 1, 7) 6 (3, 'a', 9, 12) 7 (2, 'a', 4, 6) 8 (3, 'a', 1, 7) .. note:: This can be directly used in ``pytest.mark.parametrize`` function. >>> import pytest >>> @pytest.mark.unittest ... class TestTestingGeneratorFunc: ... @pytest.mark.parametrize(*tmatrix({ ... 'a': [2, 3], ... 'e': ['a', 'b', 'c'], ... ('b', 'c'): [(1, 7), (4, 6), (9, 12)], ... })) ... def test_tmatrix_usage(self, a, e, b, c): ... print(a, e, b, c) """ mode = MatrixMode.loads(mode) # Create internal key mapping for processing key_map: Mapping[str, Union[str, Tuple[str, ...]]] = {} final_names: List[str] = [] final_values: Mapping[str, List[Any]] = {} for ki, (key, value) in enumerate(ranges.items()): kname = f'key-{ki}' key_map[kname] = key final_names.append(kname) final_values[kname] = value # Extract all parameter names from the ranges names: List[str] = [] for key in ranges.keys(): if isinstance(key, str): names.append(key) elif isinstance(key, tuple): for k in key: names.append(k) # Create appropriate generator based on mode if mode == MatrixMode.MATRIX: generator = MatrixGenerator(final_values, final_names) elif mode == MatrixMode.AETG: generator = AETGGenerator( final_values, final_names, rnd=seed, pairs=list(progressive_for(final_names, min(level, len(names)))), ) else: raise ValueError(f'Invalid mode - {mode!r}.') # pragma: no cover # Generate test cases and transform them to final format pairs: List[Tuple[Any, ...]] = [] for case in generator.cases(): _v_case: Mapping[str, Any] = {} for name in final_names: key = key_map[name] if isinstance(key, str): _v_case[key] = case[name] elif isinstance(key, tuple): for ikey, ivalue in zip(key, case[name]): _v_case[ikey] = ivalue pairs.append(tuple(_v_case[name] for name in names)) return names, pairs