Merge 26ae39866a
into 04cf8db1d7
This commit is contained in:
commit
5622a6c4ed
1
AUTHORS
1
AUTHORS
|
@ -322,6 +322,7 @@ Thomas Grainger
|
||||||
Thomas Hisch
|
Thomas Hisch
|
||||||
Tim Hoffmann
|
Tim Hoffmann
|
||||||
Tim Strazny
|
Tim Strazny
|
||||||
|
Tobias Deiminger
|
||||||
Tom Dalton
|
Tom Dalton
|
||||||
Tom Viner
|
Tom Viner
|
||||||
Tomáš Gavenčiak
|
Tomáš Gavenčiak
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
If fixtures had been indirectly parameterized via test function, e.g. using the
|
||||||
|
``@pytest.mark.parametrize(indirect=True)`` marker, reordering of tests for the least possible fixture setup/teardown
|
||||||
|
cycles did not work. Optimized test groups can now be determined either explicitly by passing parameter ids, or
|
||||||
|
implicitly if the parameter value is hashable.
|
|
@ -5,6 +5,7 @@ import sys
|
||||||
import warnings
|
import warnings
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from collections.abc import Hashable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
|
@ -248,21 +249,21 @@ def get_parametrized_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[_K
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
cs: CallSpec2 = callspec
|
cs: CallSpec2 = callspec
|
||||||
# cs.indices.items() is random order of argnames. Need to
|
# cs.param_keys.items() is random order of argnames. Need to
|
||||||
# sort this so that different calls to
|
# sort this so that different calls to
|
||||||
# get_parametrized_fixture_keys will be deterministic.
|
# get_parametrized_fixture_keys will be deterministic.
|
||||||
for argname, param_index in sorted(cs.indices.items()):
|
for argname, param_key in sorted(cs.param_keys.items()):
|
||||||
if cs._arg2scope[argname] != scope:
|
if cs._arg2scope[argname] != scope:
|
||||||
continue
|
continue
|
||||||
if scope is Scope.Session:
|
if scope is Scope.Session:
|
||||||
key: _Key = (argname, param_index)
|
key: _Key = (argname, param_key)
|
||||||
elif scope is Scope.Package:
|
elif scope is Scope.Package:
|
||||||
key = (argname, param_index, item.path.parent)
|
key = (argname, param_key, item.path.parent)
|
||||||
elif scope is Scope.Module:
|
elif scope is Scope.Module:
|
||||||
key = (argname, param_index, item.path)
|
key = (argname, param_key, item.path)
|
||||||
elif scope is Scope.Class:
|
elif scope is Scope.Class:
|
||||||
item_cls = item.cls # type: ignore[attr-defined]
|
item_cls = item.cls # type: ignore[attr-defined]
|
||||||
key = (argname, param_index, item.path, item_cls)
|
key = (argname, param_key, item.path, item_cls)
|
||||||
else:
|
else:
|
||||||
assert_never(scope)
|
assert_never(scope)
|
||||||
yield key
|
yield key
|
||||||
|
@ -601,6 +602,7 @@ class FixtureRequest:
|
||||||
except (AttributeError, ValueError):
|
except (AttributeError, ValueError):
|
||||||
param = NOTSET
|
param = NOTSET
|
||||||
param_index = 0
|
param_index = 0
|
||||||
|
param_key = ""
|
||||||
has_params = fixturedef.params is not None
|
has_params = fixturedef.params is not None
|
||||||
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
|
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
|
||||||
if has_params and fixtures_not_supported:
|
if has_params and fixtures_not_supported:
|
||||||
|
@ -640,13 +642,14 @@ class FixtureRequest:
|
||||||
fail(msg, pytrace=False)
|
fail(msg, pytrace=False)
|
||||||
else:
|
else:
|
||||||
param_index = funcitem.callspec.indices[argname]
|
param_index = funcitem.callspec.indices[argname]
|
||||||
|
param_key = funcitem.callspec.param_keys[argname]
|
||||||
# If a parametrize invocation set a scope it will override
|
# If a parametrize invocation set a scope it will override
|
||||||
# the static scope defined with the fixture function.
|
# the static scope defined with the fixture function.
|
||||||
with suppress(KeyError):
|
with suppress(KeyError):
|
||||||
scope = funcitem.callspec._arg2scope[argname]
|
scope = funcitem.callspec._arg2scope[argname]
|
||||||
|
|
||||||
subrequest = SubRequest(
|
subrequest = SubRequest(
|
||||||
self, scope, param, param_index, fixturedef, _ispytest=True
|
self, scope, param, param_index, param_key, fixturedef, _ispytest=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if a higher-level scoped fixture accesses a lower level one.
|
# Check if a higher-level scoped fixture accesses a lower level one.
|
||||||
|
@ -731,6 +734,7 @@ class SubRequest(FixtureRequest):
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
param: Any,
|
param: Any,
|
||||||
param_index: int,
|
param_index: int,
|
||||||
|
param_key: Hashable,
|
||||||
fixturedef: "FixtureDef[object]",
|
fixturedef: "FixtureDef[object]",
|
||||||
*,
|
*,
|
||||||
_ispytest: bool = False,
|
_ispytest: bool = False,
|
||||||
|
@ -741,6 +745,7 @@ class SubRequest(FixtureRequest):
|
||||||
if param is not NOTSET:
|
if param is not NOTSET:
|
||||||
self.param = param
|
self.param = param
|
||||||
self.param_index = param_index
|
self.param_index = param_index
|
||||||
|
self.param_key = param_key
|
||||||
self._scope = scope
|
self._scope = scope
|
||||||
self._fixturedef = fixturedef
|
self._fixturedef = fixturedef
|
||||||
self._pyfuncitem = request._pyfuncitem
|
self._pyfuncitem = request._pyfuncitem
|
||||||
|
@ -1012,10 +1017,10 @@ class FixtureDef(Generic[FixtureValue]):
|
||||||
|
|
||||||
my_cache_key = self.cache_key(request)
|
my_cache_key = self.cache_key(request)
|
||||||
if self.cached_result is not None:
|
if self.cached_result is not None:
|
||||||
# note: comparison with `==` can fail (or be expensive) for e.g.
|
|
||||||
# numpy arrays (#6497).
|
|
||||||
cache_key = self.cached_result[1]
|
cache_key = self.cached_result[1]
|
||||||
if my_cache_key is cache_key:
|
# Note: Comparison with `==` may be implemented as (possibly expensive)
|
||||||
|
# deep by-value comparison. See _pytest.python.SafeHashWrapper for details.
|
||||||
|
if my_cache_key == cache_key:
|
||||||
if self.cached_result[2] is not None:
|
if self.cached_result[2] is not None:
|
||||||
_, val, tb = self.cached_result[2]
|
_, val, tb = self.cached_result[2]
|
||||||
raise val.with_traceback(tb)
|
raise val.with_traceback(tb)
|
||||||
|
@ -1032,7 +1037,7 @@ class FixtureDef(Generic[FixtureValue]):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def cache_key(self, request: SubRequest) -> object:
|
def cache_key(self, request: SubRequest) -> object:
|
||||||
return request.param_index if not hasattr(request, "param") else request.param
|
return request.param_key
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "<FixtureDef argname={!r} scope={!r} baseid={!r}>".format(
|
return "<FixtureDef argname={!r} scope={!r} baseid={!r}>".format(
|
||||||
|
|
|
@ -9,6 +9,7 @@ import types
|
||||||
import warnings
|
import warnings
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from collections.abc import Hashable
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@ -927,6 +928,38 @@ def hasnew(obj: object) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(auto_attribs=True, eq=False, slots=True)
|
||||||
|
class SafeHashWrapper:
|
||||||
|
"""Wrap an arbitrary type so that it becomes comparable with guaranteed constraints.
|
||||||
|
|
||||||
|
Constraints:
|
||||||
|
- SafeHashWrapper(a) == SafeHashWrapper(b) will never raise an exception
|
||||||
|
- SafeHashWrapper(a) == SafeHashWrapper(b) will always return bool
|
||||||
|
(oddly some inner types wouldn't, e.g. numpy.array([0]) == numpy.array([0]) returns List)
|
||||||
|
- SafeHashWrapper(a) is always hashable
|
||||||
|
- if SafeHashWrapper(a) == SafeHashWrapper(b),
|
||||||
|
then hash(SafeHashWrapper(a)) == hash(SafeHashWrapper(b))
|
||||||
|
|
||||||
|
It works by falling back to identity compare in case constraints couldn't be met otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
obj: Any
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if isinstance(self.obj, Hashable) and isinstance(other, Hashable):
|
||||||
|
try:
|
||||||
|
res = self.obj == other
|
||||||
|
return bool(res)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return self.obj is other
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
if isinstance(self.obj, Hashable):
|
||||||
|
return hash(self.obj)
|
||||||
|
return hash(id(self.obj))
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@attr.s(frozen=True, auto_attribs=True, slots=True)
|
@attr.s(frozen=True, auto_attribs=True, slots=True)
|
||||||
class IdMaker:
|
class IdMaker:
|
||||||
|
@ -974,6 +1007,27 @@ class IdMaker:
|
||||||
id_suffixes[id] += 1
|
id_suffixes[id] += 1
|
||||||
return resolved_ids
|
return resolved_ids
|
||||||
|
|
||||||
|
def make_parameter_keys(self) -> Iterable[Dict[str, Hashable]]:
|
||||||
|
"""Make hashable parameter keys for each ParameterSet.
|
||||||
|
|
||||||
|
For each ParameterSet, generates a dict mapping each parameter to its key.
|
||||||
|
|
||||||
|
This key will be considered (along with the arguments name) to determine
|
||||||
|
if parameters are the same in the sense of reorder_items() and the
|
||||||
|
FixtureDef cache. The key is guaranteed to be hashable and comparable.
|
||||||
|
It's not intended for printing and therefore not ASCII escaped.
|
||||||
|
"""
|
||||||
|
for idx, parameterset in enumerate(self.parametersets):
|
||||||
|
if parameterset.id is not None:
|
||||||
|
# ID provided directly - pytest.param(..., id="...")
|
||||||
|
yield {argname: parameterset.id for argname in self.argnames}
|
||||||
|
elif self.ids and idx < len(self.ids) and self.ids[idx] is not None:
|
||||||
|
# ID provided in the IDs list - parametrize(..., ids=[...]).
|
||||||
|
yield {argname: self.ids[idx] for argname in self.argnames}
|
||||||
|
else:
|
||||||
|
# ID not provided - generate it.
|
||||||
|
yield self._parameter_keys_from_parameterset(parameterset, idx)
|
||||||
|
|
||||||
def _resolve_ids(self) -> Iterable[str]:
|
def _resolve_ids(self) -> Iterable[str]:
|
||||||
"""Resolve IDs for all ParameterSets (may contain duplicates)."""
|
"""Resolve IDs for all ParameterSets (may contain duplicates)."""
|
||||||
for idx, parameterset in enumerate(self.parametersets):
|
for idx, parameterset in enumerate(self.parametersets):
|
||||||
|
@ -992,6 +1046,20 @@ class IdMaker:
|
||||||
for val, argname in zip(parameterset.values, self.argnames)
|
for val, argname in zip(parameterset.values, self.argnames)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _parameter_keys_from_parameterset(
|
||||||
|
self, parameterset: ParameterSet, idx: int
|
||||||
|
) -> Dict[str, Hashable]:
|
||||||
|
"""Make parameter keys for all parameters in a ParameterSet."""
|
||||||
|
param_keys: Dict[str, Hashable] = {}
|
||||||
|
for val, argname in zip(parameterset.values, self.argnames):
|
||||||
|
evaluated_id = self._idval_from_function(val, argname, idx)
|
||||||
|
if evaluated_id is not None:
|
||||||
|
param_keys[argname] = evaluated_id
|
||||||
|
else:
|
||||||
|
# Wrapping ensures val becomes comparable and hashable.
|
||||||
|
param_keys[argname] = SafeHashWrapper(val)
|
||||||
|
return param_keys
|
||||||
|
|
||||||
def _idval(self, val: object, argname: str, idx: int) -> str:
|
def _idval(self, val: object, argname: str, idx: int) -> str:
|
||||||
"""Make an ID for a parameter in a ParameterSet."""
|
"""Make an ID for a parameter in a ParameterSet."""
|
||||||
idval = self._idval_from_function(val, argname, idx)
|
idval = self._idval_from_function(val, argname, idx)
|
||||||
|
@ -1076,6 +1144,8 @@ class CallSpec2:
|
||||||
# arg name -> arg value which will be passed to a fixture of the same name
|
# arg name -> arg value which will be passed to a fixture of the same name
|
||||||
# (indirect parametrization).
|
# (indirect parametrization).
|
||||||
params: Dict[str, object] = attr.Factory(dict)
|
params: Dict[str, object] = attr.Factory(dict)
|
||||||
|
# arg name -> parameter key.
|
||||||
|
param_keys: Dict[str, Hashable] = attr.Factory(dict)
|
||||||
# arg name -> arg index.
|
# arg name -> arg index.
|
||||||
indices: Dict[str, int] = attr.Factory(dict)
|
indices: Dict[str, int] = attr.Factory(dict)
|
||||||
# Used for sorting parametrized resources.
|
# Used for sorting parametrized resources.
|
||||||
|
@ -1095,9 +1165,12 @@ class CallSpec2:
|
||||||
marks: Iterable[Union[Mark, MarkDecorator]],
|
marks: Iterable[Union[Mark, MarkDecorator]],
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
param_index: int,
|
param_index: int,
|
||||||
|
param_set_keys: Dict[str, Hashable],
|
||||||
) -> "CallSpec2":
|
) -> "CallSpec2":
|
||||||
|
"""Extend an existing callspec with new parameters during multiple invocation of Metafunc.parametrize."""
|
||||||
funcargs = self.funcargs.copy()
|
funcargs = self.funcargs.copy()
|
||||||
params = self.params.copy()
|
params = self.params.copy()
|
||||||
|
param_keys = self.param_keys.copy()
|
||||||
indices = self.indices.copy()
|
indices = self.indices.copy()
|
||||||
arg2scope = self._arg2scope.copy()
|
arg2scope = self._arg2scope.copy()
|
||||||
for arg, val in zip(argnames, valset):
|
for arg, val in zip(argnames, valset):
|
||||||
|
@ -1111,10 +1184,12 @@ class CallSpec2:
|
||||||
else:
|
else:
|
||||||
assert_never(valtype_for_arg)
|
assert_never(valtype_for_arg)
|
||||||
indices[arg] = param_index
|
indices[arg] = param_index
|
||||||
|
param_keys[arg] = param_set_keys[arg]
|
||||||
arg2scope[arg] = scope
|
arg2scope[arg] = scope
|
||||||
return CallSpec2(
|
return CallSpec2(
|
||||||
funcargs=funcargs,
|
funcargs=funcargs,
|
||||||
params=params,
|
params=params,
|
||||||
|
param_keys=param_keys,
|
||||||
arg2scope=arg2scope,
|
arg2scope=arg2scope,
|
||||||
indices=indices,
|
indices=indices,
|
||||||
idlist=[*self._idlist, id],
|
idlist=[*self._idlist, id],
|
||||||
|
@ -1284,7 +1359,7 @@ class Metafunc:
|
||||||
if generated_ids is not None:
|
if generated_ids is not None:
|
||||||
ids = generated_ids
|
ids = generated_ids
|
||||||
|
|
||||||
ids = self._resolve_parameter_set_ids(
|
ids, parameters_keys = self._resolve_parameter_set_ids(
|
||||||
argnames, ids, parametersets, nodeid=self.definition.nodeid
|
argnames, ids, parametersets, nodeid=self.definition.nodeid
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1297,17 +1372,18 @@ class Metafunc:
|
||||||
# of all calls.
|
# of all calls.
|
||||||
newcalls = []
|
newcalls = []
|
||||||
for callspec in self._calls or [CallSpec2()]:
|
for callspec in self._calls or [CallSpec2()]:
|
||||||
for param_index, (param_id, param_set) in enumerate(
|
for param_index, (param_id, parameterset, param_set_keys) in enumerate(
|
||||||
zip(ids, parametersets)
|
zip(ids, parametersets, parameters_keys)
|
||||||
):
|
):
|
||||||
newcallspec = callspec.setmulti(
|
newcallspec = callspec.setmulti(
|
||||||
valtypes=arg_values_types,
|
valtypes=arg_values_types,
|
||||||
argnames=argnames,
|
argnames=argnames,
|
||||||
valset=param_set.values,
|
valset=parameterset.values,
|
||||||
id=param_id,
|
id=param_id,
|
||||||
marks=param_set.marks,
|
marks=parameterset.marks,
|
||||||
scope=scope_,
|
scope=scope_,
|
||||||
param_index=param_index,
|
param_index=param_index,
|
||||||
|
param_set_keys=param_set_keys,
|
||||||
)
|
)
|
||||||
newcalls.append(newcallspec)
|
newcalls.append(newcallspec)
|
||||||
self._calls = newcalls
|
self._calls = newcalls
|
||||||
|
@ -1323,9 +1399,8 @@ class Metafunc:
|
||||||
],
|
],
|
||||||
parametersets: Sequence[ParameterSet],
|
parametersets: Sequence[ParameterSet],
|
||||||
nodeid: str,
|
nodeid: str,
|
||||||
) -> List[str]:
|
) -> Tuple[List[str], List[Dict[str, Hashable]]]:
|
||||||
"""Resolve the actual ids for the given parameter sets.
|
"""Resolve the actual ids for the given parameter sets.
|
||||||
|
|
||||||
:param argnames:
|
:param argnames:
|
||||||
Argument names passed to ``parametrize()``.
|
Argument names passed to ``parametrize()``.
|
||||||
:param ids:
|
:param ids:
|
||||||
|
@ -1337,7 +1412,9 @@ class Metafunc:
|
||||||
The nodeid of the definition item that generated this
|
The nodeid of the definition item that generated this
|
||||||
parametrization.
|
parametrization.
|
||||||
:returns:
|
:returns:
|
||||||
List with ids for each parameter set given.
|
Tuple, where
|
||||||
|
1st entry is a list with ids for each parameter set given used to name test invocations, and
|
||||||
|
2nd entry is a list with keys to support distinction of parameters to support fixture reuse.
|
||||||
"""
|
"""
|
||||||
if ids is None:
|
if ids is None:
|
||||||
idfn = None
|
idfn = None
|
||||||
|
@ -1351,7 +1428,9 @@ class Metafunc:
|
||||||
id_maker = IdMaker(
|
id_maker = IdMaker(
|
||||||
argnames, parametersets, idfn, ids_, self.config, nodeid=nodeid
|
argnames, parametersets, idfn, ids_, self.config, nodeid=nodeid
|
||||||
)
|
)
|
||||||
return id_maker.make_unique_parameterset_ids()
|
return id_maker.make_unique_parameterset_ids(), list(
|
||||||
|
id_maker.make_parameter_keys()
|
||||||
|
)
|
||||||
|
|
||||||
def _validate_ids(
|
def _validate_ids(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -22,13 +22,13 @@ def checked_order():
|
||||||
assert order == [
|
assert order == [
|
||||||
("issue_519.py", "fix1", "arg1v1"),
|
("issue_519.py", "fix1", "arg1v1"),
|
||||||
("test_one[arg1v1-arg2v1]", "fix2", "arg2v1"),
|
("test_one[arg1v1-arg2v1]", "fix2", "arg2v1"),
|
||||||
("test_two[arg1v1-arg2v1]", "fix2", "arg2v1"),
|
|
||||||
("test_one[arg1v1-arg2v2]", "fix2", "arg2v2"),
|
("test_one[arg1v1-arg2v2]", "fix2", "arg2v2"),
|
||||||
|
("test_two[arg1v1-arg2v1]", "fix2", "arg2v1"),
|
||||||
("test_two[arg1v1-arg2v2]", "fix2", "arg2v2"),
|
("test_two[arg1v1-arg2v2]", "fix2", "arg2v2"),
|
||||||
("issue_519.py", "fix1", "arg1v2"),
|
("issue_519.py", "fix1", "arg1v2"),
|
||||||
("test_one[arg1v2-arg2v1]", "fix2", "arg2v1"),
|
("test_one[arg1v2-arg2v1]", "fix2", "arg2v1"),
|
||||||
("test_two[arg1v2-arg2v1]", "fix2", "arg2v1"),
|
|
||||||
("test_one[arg1v2-arg2v2]", "fix2", "arg2v2"),
|
("test_one[arg1v2-arg2v2]", "fix2", "arg2v2"),
|
||||||
|
("test_two[arg1v2-arg2v1]", "fix2", "arg2v1"),
|
||||||
("test_two[arg1v2-arg2v2]", "fix2", "arg2v2"),
|
("test_two[arg1v2-arg2v2]", "fix2", "arg2v2"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1308,6 +1308,59 @@ class TestFixtureUsages:
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
result.stdout.fnmatch_lines(["*4 passed*"])
|
result.stdout.fnmatch_lines(["*4 passed*"])
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("parametrize1", "parametrize2"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
'"fix", [1, 2], indirect=True',
|
||||||
|
'"fix", [2, 1], indirect=True',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'"fix", [1, pytest.param({"data": 2}, id="2")], indirect=True',
|
||||||
|
'"fix", [pytest.param({"data": 2}, id="2"), 1], indirect=True',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'"fix", [{"data": 1}, {"data": 2}], indirect=True, ids=lambda d: MyEnum(d["data"])',
|
||||||
|
'"fix", [{"data": 2}, {"data": 1}], indirect=True, ids=lambda d: MyEnum(d["data"])',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'"fix", [{"data": 1}, {"data": 2}], indirect=True, ids=[1, "two"]',
|
||||||
|
'"fix", [{"data": 2}, {"data": 1}], indirect=True, ids=["two", 1]',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_reorder_and_cache(
|
||||||
|
self, pytester: Pytester, parametrize1, parametrize2
|
||||||
|
) -> None:
|
||||||
|
"""Test optimization for minimal setup/teardown with indirectly parametrized fixtures. See #8914, #9420."""
|
||||||
|
pytester.makepyfile(
|
||||||
|
f"""
|
||||||
|
import pytest
|
||||||
|
from enum import Enum
|
||||||
|
class MyEnum(Enum):
|
||||||
|
Id1 = 1
|
||||||
|
Id2 = 2
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def fix(request):
|
||||||
|
value = request.param["data"] if isinstance(request.param, dict) else request.param
|
||||||
|
print(f'prepare foo-%s' % value)
|
||||||
|
yield value
|
||||||
|
print(f'teardown foo-%s' % value)
|
||||||
|
@pytest.mark.parametrize({parametrize1})
|
||||||
|
def test1(fix):
|
||||||
|
pass
|
||||||
|
@pytest.mark.parametrize({parametrize2})
|
||||||
|
def test2(fix):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest("-s")
|
||||||
|
output = result.stdout.str()
|
||||||
|
assert output.count("prepare foo-1") == 1
|
||||||
|
assert output.count("prepare foo-2") == 1
|
||||||
|
assert output.count("teardown foo-1") == 1
|
||||||
|
assert output.count("teardown foo-2") == 1
|
||||||
|
|
||||||
def test_funcarg_parametrized_and_used_twice(self, pytester: Pytester) -> None:
|
def test_funcarg_parametrized_and_used_twice(self, pytester: Pytester) -> None:
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue