Source code for hbutils.testing.generator.matrix

"""
Matrix generator module for generating test cases based on matrix combinations.

This module provides a MatrixGenerator class that generates all possible combinations
of parameter values in a matrix, similar to GitHub Actions' matrix strategy. It supports
including additional cases and excluding specific combinations.
"""

from typing import Iterator, Mapping, Optional, List, Any

from .base import BaseGenerator, _single_dict_process, _single_to_tuple, _check_keys


[docs] class MatrixGenerator(BaseGenerator): """ Full matrix model, all the cases in this matrix will be iterated. This generator creates a cartesian product of all provided parameter values, with optional inclusions and exclusions to customize the generated test cases. """
[docs] def __init__(self, values: Mapping[str, Any], names: Optional[List[str]] = None, includes: Optional[List[Mapping[str, Any]]] = None, excludes: Optional[List[Mapping[str, Any]]] = None): """ Constructor of the :class:`MatrixGenerator` class. It is similar to GitHub Action's matrix strategy, generating all combinations of the provided values with support for custom inclusions and exclusions. :param values: Matrix values, such as ``{'a': [2, 3], 'b': ['b', 'c']}``. :type values: Mapping[str, Any] :param names: Names of the given generator, default is ``None`` which means use the sorted \ key set of the values. :type names: Optional[List[str]] :param includes: Include items, such as ``[{'a': 4, 'b': 'b'}]``, \ default is ``None`` which means no extra inclusions. :type includes: Optional[List[Mapping[str, Any]]] :param excludes: Exclude Items, such as ``[{'a': 2, 'b': 'c'}]``, \ default is ``None`` which means no extra exclusions. :type excludes: Optional[List[Mapping[str, Any]]] Example:: >>> gen = MatrixGenerator( ... {'a': [1, 2], 'b': ['x', 'y']}, ... includes=[{'a': 3, 'b': 'z'}], ... excludes=[{'a': 1, 'b': 'x'}] ... ) """ BaseGenerator.__init__(self, values, names) _name_set = set(self.names) self.__includes = [_single_dict_process(inc) for inc in (includes or [])] for includes in self.__includes: _check_keys(includes, _name_set) self.__excludes = [_single_dict_process(exc) for exc in (excludes or [])] for excludes in self.__excludes: _check_keys(excludes, _name_set)
@property def includes(self) -> List[Mapping[str, Any]]: """ Get the include items. Include items are additional parameter combinations that will be added to the generated cases, even if they don't fit the original matrix values. :return: List of include item mappings. :rtype: List[Mapping[str, Any]] """ return self.__includes @property def excludes(self) -> List[Mapping[str, Any]]: """ Get the exclude items. Exclude items are parameter combinations that will be filtered out from the generated cases. A case is excluded if it matches all key-value pairs in any exclude item. :return: List of exclude item mappings. :rtype: List[Mapping[str, Any]] """ return self.__excludes
[docs] def cases(self) -> Iterator[Mapping[str, Any]]: """ Get the cases in this matrix. Generates all possible combinations of the matrix values, applying exclusions and inclusions as specified. The method performs a cartesian product of all parameter values, then filters based on exclude rules, and finally adds any specified include cases. :return: Iterator yielding dictionaries representing each test case. :rtype: Iterator[Mapping[str, Any]] Examples:: >>> from hbutils.testing import MatrixGenerator >>> for p in MatrixGenerator( ... {'a': [1, 2, 3], 'b': ['a', 'b'], 'r': [3, 4, 5]}, ... includes=[{'a': 4, 'r': 7}], ... excludes=[{'a': 1, 'r': 3}, {'a': 3, 'b': 'b'}, {'a': 4, 'b': 'a', 'r': 7}] ... ).cases(): ... print(p) {'a': 1, 'b': 'a', 'r': 4} {'a': 1, 'b': 'a', 'r': 5} {'a': 1, 'b': 'b', 'r': 4} {'a': 1, 'b': 'b', 'r': 5} {'a': 2, 'b': 'a', 'r': 3} {'a': 2, 'b': 'a', 'r': 4} {'a': 2, 'b': 'a', 'r': 5} {'a': 2, 'b': 'b', 'r': 3} {'a': 2, 'b': 'b', 'r': 4} {'a': 2, 'b': 'b', 'r': 5} {'a': 3, 'b': 'a', 'r': 3} {'a': 3, 'b': 'a', 'r': 4} {'a': 3, 'b': 'a', 'r': 5} {'a': 4, 'b': 'b', 'r': 7} """ n = len(self.names) def _check_single_exclude(dict_value: Mapping[str, Any], exclude: Mapping[str, Any]) -> bool: """ Check if a single case matches an exclude pattern. :param dict_value: The case dictionary to check. :type dict_value: Mapping[str, Any] :param exclude: The exclude pattern to match against. :type exclude: Mapping[str, Any] :return: True if the case matches the exclude pattern, False otherwise. :rtype: bool """ for key, value in exclude.items(): if key not in dict_value or dict_value[key] not in value: return False return True def _check_exclude(dict_value: Mapping[str, Any], excludes: List[Mapping[str, Any]]) -> bool: """ Check if a case should be excluded based on all exclude patterns. :param dict_value: The case dictionary to check. :type dict_value: Mapping[str, Any] :param excludes: List of exclude patterns to check against. :type excludes: List[Mapping[str, Any]] :return: True if the case should be excluded, False otherwise. :rtype: bool """ for exclude in excludes: if _check_single_exclude(dict_value, exclude): return True return False def _matrix_recursion(depth: int, dict_value: Mapping[str, Any], values: Mapping[str, Any], excludes: List[Mapping[str, Any]]) -> Iterator[Mapping[str, Any]]: """ Recursively generate matrix combinations. This function performs a depth-first traversal to generate all combinations of parameter values, checking exclusions at each level. :param depth: Current recursion depth (parameter index). :type depth: int :param dict_value: Current accumulated parameter values. :type dict_value: Mapping[str, Any] :param values: Available values for each parameter. :type values: Mapping[str, Any] :param excludes: Exclude patterns to apply. :type excludes: List[Mapping[str, Any]] :return: Iterator yielding valid case dictionaries. :rtype: Iterator[Mapping[str, Any]] """ if _check_exclude(dict_value, excludes): return if depth < n: name = self.names[depth] for curitem in values[name]: yield from _matrix_recursion(depth + 1, {**dict_value, name: curitem}, values, excludes) else: yield dict_value value_items = [self.values, *({ name: _single_to_tuple(include[name]) if name in include else self.values[name] for name in self.names } for include in self.includes)] local_excludes = [*self.excludes] for vis in value_items: yield from _matrix_recursion(0, {}, vis, local_excludes) local_excludes.append(vis)