Merge f86aa30530
into f426c0b35a
This commit is contained in:
commit
d2bd3bae13
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue