This commit is contained in:
Sadra Barikbin 2024-06-20 13:54:22 +02:00 committed by GitHub
commit d2bd3bae13
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 110 additions and 8 deletions

View File

@ -1054,12 +1054,12 @@ class CallSpec2:
id: str,
marks: Iterable[Union[Mark, MarkDecorator]],
scope: Scope,
param_index: int,
param_indices: Tuple[int, ...],
) -> "CallSpec2":
params = self.params.copy()
indices = self.indices.copy()
arg2scope = dict(self._arg2scope)
for arg, val in zip(argnames, valset):
for arg, val, param_index in zip(argnames, valset, param_indices):
if arg in params:
raise ValueError(f"duplicate parametrization of {arg!r}")
params[arg] = val
@ -1088,7 +1088,57 @@ def get_direct_param_fixture_func(request: FixtureRequest) -> Any:
return request.param
# Used for storing pseudo fixturedefs for direct parametrization.
def resolve_values_indices_in_parametersets(
argnames: Sequence[str],
parametersets: Sequence[ParameterSet],
) -> List[Tuple[int, ...]]:
"""Resolve indices of the values in parameter sets. The index of a value is determined by
where the value first appears in the existing values of the argname. In other words, given
a subset of the cross-product of some ordered sets, it substitues the values in the subset
members with their index in the respective sets. For example, given ``argnames`` and
``parametersets`` below, the result would be:
::
argnames = ["A", "B", "C"]
parametersets = [("a1", "b1", "c1"), ("a1", "b2", "c1"), ("a1", "b3", "c2")]
result = [(0, 0, 0), (0, 1, 0), (0, 2, 1)]
Result is used in reordering tests to keep items using the same fixture close together.
:param argnames:
Argument names passed to ``metafunc.parametrize()``.
:param parametersets:
The parameter sets, each containing a set of values corresponding
to ``argnames``.
:returns:
List of tuples of indices, each tuple for a parameter set.
"""
indices: List[List[int]] = []
argname_value_indices_for_hashable_ones: Dict[str, Dict[object, int]] = defaultdict(
dict
)
argvalues_count: Dict[str, int] = defaultdict(int)
for i, argname in enumerate(argnames):
argname_indices = []
for parameterset in parametersets:
value = parameterset.values[i]
try:
argname_indices.append(
argname_value_indices_for_hashable_ones[argname][value]
)
except KeyError: # New unique value
argname_value_indices_for_hashable_ones[argname][value] = (
argvalues_count[argname]
)
argname_indices.append(argvalues_count[argname])
argvalues_count[argname] += 1
except TypeError: # `value` is not hashable
argname_indices.append(argvalues_count[argname])
argvalues_count[argname] += 1
indices.append(argname_indices)
return list(zip(*indices))
# Used for storing artificial fixturedefs for direct parametrization.
name2pseudofixturedef_key = StashKey[Dict[str, FixtureDef[Any]]]()
@ -1243,13 +1293,15 @@ class Metafunc:
ids = self._resolve_parameter_set_ids(
argnames, ids, parametersets, nodeid=self.definition.nodeid
)
param_indices_list = resolve_values_indices_in_parametersets(
argnames, parametersets
)
# Store used (possibly generated) ids with parametrize Marks.
if _param_mark and _param_mark._param_ids_from and generated_ids is None:
object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids)
# Add funcargs as fixturedefs to fixtureinfo.arg2fixturedefs by registering
# artificial "pseudo" FixtureDef's so that later at test execution time we can
# artificial FixtureDef's so that later at test execution time we can
# rely on a proper FixtureDef to exist for fixture setup.
node = None
# If we have a scope that is higher than function, we need
@ -1305,8 +1357,8 @@ class Metafunc:
# of all calls.
newcalls = []
for callspec in self._calls or [CallSpec2()]:
for param_index, (param_id, param_set) in enumerate(
zip(ids, parametersets)
for param_id, param_set, param_indices in zip(
ids, parametersets, param_indices_list
):
newcallspec = callspec.setmulti(
argnames=argnames,
@ -1314,7 +1366,7 @@ class Metafunc:
id=param_id,
marks=param_set.marks,
scope=scope_,
param_index=param_index,
param_indices=param_indices,
)
newcalls.append(newcallspec)
self._calls = newcalls

View File

@ -21,10 +21,12 @@ from _pytest import fixtures
from _pytest import python
from _pytest.compat import getfuncargnames
from _pytest.compat import NOTSET
from _pytest.mark import ParameterSet
from _pytest.outcomes import fail
from _pytest.pytester import Pytester
from _pytest.python import Function
from _pytest.python import IdMaker
from _pytest.python import resolve_values_indices_in_parametersets
from _pytest.scope import Scope
import pytest
@ -985,6 +987,15 @@ class TestMetafunc:
assert metafunc._calls[1].params == dict(x=3, y=4)
assert metafunc._calls[1].id == "3-4"
def test_parametrize_with_duplicate_values(self) -> None:
metafunc = self.Metafunc(lambda x, y: None)
metafunc.parametrize(("x", "y"), [(1, 2), (3, 4), (1, 5), (2, 2)])
assert len(metafunc._calls) == 4
assert metafunc._calls[0].indices == dict(x=0, y=0)
assert metafunc._calls[1].indices == dict(x=1, y=1)
assert metafunc._calls[2].indices == dict(x=0, y=2)
assert metafunc._calls[3].indices == dict(x=2, y=0)
def test_high_scoped_parametrize_reordering(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
@ -1019,6 +1030,35 @@ class TestMetafunc:
]
)
def test_high_scoped_parametrize_with_duplicate_values_reordering(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
import pytest
@pytest.fixture(scope='module')
def fixture1(request):
pass
@pytest.fixture(scope='module')
def fixture2(request):
pass
@pytest.mark.parametrize("fixture1, fixture2", [("a", 0), ("b", 1), ("a", 2)], indirect=True)
def test(fixture1, fixture2):
pass
"""
)
result = pytester.runpytest("--collect-only")
result.stdout.re_match_lines(
[
r" <Function test\[a-0\]>",
r" <Function test\[a-2\]>",
r" <Function test\[b-1\]>",
]
)
def test_parametrize_multiple_times(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
@ -1650,6 +1690,16 @@ class TestMetafuncFunctional:
]
)
def test_resolve_values_indices_in_parametersets(self, pytester: Pytester) -> None:
indices = resolve_values_indices_in_parametersets(
("a", "b"),
[
ParameterSet.extract_from((a, b))
for a, b in [(1, 1), (1, 2), (2, 3), ([2], 4), ([2], 1)]
],
)
assert indices == [(0, 0), (0, 1), (1, 2), (2, 3), (3, 0)]
class TestMetafuncFunctionalAuto:
"""Tests related to automatically find out the correct scope for