Merge branch 'main' into Improvement-catch-duplicate-values-when-determining-param-indices-in-metafunc-parametrize

This commit is contained in:
Sadra Barikbin 2024-01-09 22:53:08 +03:30 committed by GitHub
commit 909f695b9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 237 additions and 257 deletions

View File

@ -29,7 +29,7 @@ repos:
language: python
files: \.py$
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
rev: 7.0.0
hooks:
- id: flake8
language_version: python3

View File

@ -0,0 +1,7 @@
Some changes were made to private functions which may affect plugins which access them:
- ``FixtureManager._getautousenames()`` now takes a ``Node`` itself instead of the nodeid.
- ``FixtureManager.getfixturedefs()`` now takes the ``Node`` itself instead of the nodeid.
- The ``_pytest.nodes.iterparentnodeids()`` function is removed without replacement.
Prefer to traverse the node hierarchy itself instead.
If you really need to, copy the function from the previous pytest release.

View File

@ -424,9 +424,9 @@ class FixtureRequest(abc.ABC):
# We arrive here because of a dynamic call to
# getfixturevalue(argname) usage which was naturally
# not known at parsing/collection time.
assert self._pyfuncitem.parent is not None
parentid = self._pyfuncitem.parent.nodeid
fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
parent = self._pyfuncitem.parent
assert parent is not None
fixturedefs = self._fixturemanager.getfixturedefs(argname, parent)
if fixturedefs is not None:
self._arg2fixturedefs[argname] = fixturedefs
# No fixtures defined with this name.
@ -846,9 +846,8 @@ class FixtureLookupError(LookupError):
available = set()
parent = self.request._pyfuncitem.parent
assert parent is not None
parentid = parent.nodeid
for name, fixturedefs in fm._arg2fixturedefs.items():
faclist = list(fm._matchfactories(fixturedefs, parentid))
faclist = list(fm._matchfactories(fixturedefs, parent))
if faclist:
available.add(name)
if self.argname in available:
@ -989,9 +988,8 @@ class FixtureDef(Generic[FixtureValue]):
# The "base" node ID for the fixture.
#
# This is a node ID prefix. A fixture is only available to a node (e.g.
# a `Function` item) if the fixture's baseid is a parent of the node's
# nodeid (see the `iterparentnodeids` function for what constitutes a
# "parent" and a "prefix" in this context).
# a `Function` item) if the fixture's baseid is a nodeid of a parent of
# node.
#
# For a fixture found in a Collector's object (e.g. a `Module`s module,
# a `Class`'s class), the baseid is the Collector's nodeid.
@ -1482,7 +1480,7 @@ class FixtureManager:
else:
argnames = ()
usefixturesnames = self._getusefixturesnames(node)
autousenames = self._getautousenames(node.nodeid)
autousenames = self._getautousenames(node)
initialnames = deduplicate_names(autousenames, usefixturesnames, argnames)
direct_parametrize_args = _get_direct_parametrize_args(node)
@ -1517,10 +1515,10 @@ class FixtureManager:
self.parsefactories(plugin, nodeid)
def _getautousenames(self, nodeid: str) -> Iterator[str]:
"""Return the names of autouse fixtures applicable to nodeid."""
for parentnodeid in nodes.iterparentnodeids(nodeid):
basenames = self._nodeid_autousenames.get(parentnodeid)
def _getautousenames(self, node: nodes.Node) -> Iterator[str]:
"""Return the names of autouse fixtures applicable to node."""
for parentnode in reversed(list(nodes.iterparentnodes(node))):
basenames = self._nodeid_autousenames.get(parentnode.nodeid)
if basenames:
yield from basenames
@ -1542,7 +1540,6 @@ class FixtureManager:
# to re-discover fixturedefs again for each fixturename
# (discovering matching fixtures for a given name/node is expensive).
parentid = parentnode.nodeid
fixturenames_closure = list(initialnames)
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
@ -1554,7 +1551,7 @@ class FixtureManager:
continue
if argname in arg2fixturedefs:
continue
fixturedefs = self.getfixturedefs(argname, parentid)
fixturedefs = self.getfixturedefs(argname, parentnode)
if fixturedefs:
arg2fixturedefs[argname] = fixturedefs
for arg in fixturedefs[-1].argnames:
@ -1621,6 +1618,69 @@ class FixtureManager:
# Separate parametrized setups.
items[:] = reorder_items(items)
def _register_fixture(
self,
*,
name: str,
func: "_FixtureFunc[object]",
nodeid: Optional[str],
scope: Union[
Scope, _ScopeName, Callable[[str, Config], _ScopeName], None
] = "function",
params: Optional[Sequence[object]] = None,
ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = None,
autouse: bool = False,
unittest: bool = False,
) -> None:
"""Register a fixture
:param name:
The fixture's name.
:param func:
The fixture's implementation function.
:param nodeid:
The visibility of the fixture. The fixture will be available to the
node with this nodeid and its children in the collection tree.
None means that the fixture is visible to the entire collection tree,
e.g. a fixture defined for general use in a plugin.
:param scope:
The fixture's scope.
:param params:
The fixture's parametrization params.
:param ids:
The fixture's IDs.
:param autouse:
Whether this is an autouse fixture.
:param unittest:
Set this if this is a unittest fixture.
"""
fixture_def = FixtureDef(
fixturemanager=self,
baseid=nodeid,
argname=name,
func=func,
scope=scope,
params=params,
unittest=unittest,
ids=ids,
_ispytest=True,
)
faclist = self._arg2fixturedefs.setdefault(name, [])
if fixture_def.has_location:
faclist.append(fixture_def)
else:
# fixturedefs with no location are at the front
# so this inserts the current fixturedef after the
# existing fixturedefs from external plugins but
# before the fixturedefs provided in conftests.
i = len([f for f in faclist if not f.has_location])
faclist.insert(i, fixture_def)
if autouse:
self._nodeid_autousenames.setdefault(nodeid or "", []).append(name)
@overload
def parsefactories(
self,
@ -1672,13 +1732,7 @@ class FixtureManager:
return
self._holderobjseen.add(holderobj)
autousenames = []
for name in dir(holderobj):
# ugly workaround for one of the fspath deprecated property of node
# todo: safely generalize
if isinstance(holderobj, nodes.Node) and name == "fspath":
continue
# The attribute can be an arbitrary descriptor, so the attribute
# access below can raise. safe_getatt() ignores such exceptions.
obj = safe_getattr(holderobj, name, None)
@ -1695,38 +1749,21 @@ class FixtureManager:
# to issue a warning if called directly, so here we unwrap it in
# order to not emit the warning when pytest itself calls the
# fixture function.
obj = get_real_method(obj, holderobj)
func = get_real_method(obj, holderobj)
fixture_def = FixtureDef(
fixturemanager=self,
baseid=nodeid,
argname=name,
func=obj,
self._register_fixture(
name=name,
nodeid=nodeid,
func=func,
scope=marker.scope,
params=marker.params,
unittest=unittest,
ids=marker.ids,
_ispytest=True,
autouse=marker.autouse,
)
faclist = self._arg2fixturedefs.setdefault(name, [])
if fixture_def.has_location:
faclist.append(fixture_def)
else:
# fixturedefs with no location are at the front
# so this inserts the current fixturedef after the
# existing fixturedefs from external plugins but
# before the fixturedefs provided in conftests.
i = len([f for f in faclist if not f.has_location])
faclist.insert(i, fixture_def)
if marker.autouse:
autousenames.append(name)
if autousenames:
self._nodeid_autousenames.setdefault(nodeid or "", []).extend(autousenames)
def getfixturedefs(
self, argname: str, nodeid: str
self, argname: str, node: nodes.Node
) -> Optional[Sequence[FixtureDef[Any]]]:
"""Get FixtureDefs for a fixture name which are applicable
to a given node.
@ -1737,18 +1774,18 @@ class FixtureManager:
an empty result is returned).
:param argname: Name of the fixture to search for.
:param nodeid: Full node id of the requesting test.
:param node: The requesting Node.
"""
try:
fixturedefs = self._arg2fixturedefs[argname]
except KeyError:
return None
return tuple(self._matchfactories(fixturedefs, nodeid))
return tuple(self._matchfactories(fixturedefs, node))
def _matchfactories(
self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str
self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node
) -> Iterator[FixtureDef[Any]]:
parentnodeids = set(nodes.iterparentnodeids(nodeid))
parentnodeids = {n.nodeid for n in nodes.iterparentnodes(node)}
for fixturedef in fixturedefs:
if fixturedef.baseid in parentnodeids:
yield fixturedef

View File

@ -49,49 +49,13 @@ SEP = "/"
tracebackcutdir = Path(_pytest.__file__).parent
def iterparentnodeids(nodeid: str) -> Iterator[str]:
"""Return the parent node IDs of a given node ID, inclusive.
For the node ID
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
the result would be
""
"testing"
"testing/code"
"testing/code/test_excinfo.py"
"testing/code/test_excinfo.py::TestFormattedExcinfo"
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
Note that / components are only considered until the first ::.
"""
pos = 0
first_colons: Optional[int] = nodeid.find("::")
if first_colons == -1:
first_colons = None
# The root Session node - always present.
yield ""
# Eagerly consume SEP parts until first colons.
while True:
at = nodeid.find(SEP, pos, first_colons)
if at == -1:
break
if at > 0:
yield nodeid[:at]
pos = at + len(SEP)
# Eagerly consume :: parts.
while True:
at = nodeid.find("::", pos)
if at == -1:
break
if at > 0:
yield nodeid[:at]
pos = at + len("::")
# The node ID itself.
if nodeid:
yield nodeid
def iterparentnodes(node: "Node") -> Iterator["Node"]:
"""Return the parent nodes, including the node itself, from the node
upwards."""
parent: Optional[Node] = node
while parent is not None:
yield parent
parent = parent.parent
_NodeType = TypeVar("_NodeType", bound="Node")
@ -488,7 +452,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
* "location": a pair (path, lineno)
* "obj": a Python object that the node wraps.
* "fspath": just a path
* "path": just a path
:rtype: A tuple of (str|Path, int) with filename and 0-based line number.
"""
@ -499,7 +463,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
obj = getattr(node, "obj", None)
if obj is not None:
return getfslineno(obj)
return getattr(node, "fspath", "unknown location"), -1
return getattr(node, "path", "unknown location"), -1
class Collector(Node, abc.ABC):

View File

@ -582,13 +582,13 @@ class Module(nodes.File, PyCollector):
return importtestmodule(self.path, self.config)
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
self._inject_setup_module_fixture()
self._inject_setup_function_fixture()
self._register_setup_module_fixture()
self._register_setup_function_fixture()
self.session._fixturemanager.parsefactories(self)
return super().collect()
def _inject_setup_module_fixture(self) -> None:
"""Inject a hidden autouse, module scoped fixture into the collected module object
def _register_setup_module_fixture(self) -> None:
"""Register an autouse, module-scoped fixture for the collected module object
that invokes setUpModule/tearDownModule if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
@ -604,23 +604,25 @@ class Module(nodes.File, PyCollector):
if setup_module is None and teardown_module is None:
return
@fixtures.fixture(
autouse=True,
scope="module",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_module_fixture_{self.obj.__name__}",
)
def xunit_setup_module_fixture(request) -> Generator[None, None, None]:
module = request.module
if setup_module is not None:
_call_with_optional_argument(setup_module, request.module)
_call_with_optional_argument(setup_module, module)
yield
if teardown_module is not None:
_call_with_optional_argument(teardown_module, request.module)
_call_with_optional_argument(teardown_module, module)
self.obj.__pytest_setup_module = xunit_setup_module_fixture
self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
name=f"_xunit_setup_module_fixture_{self.obj.__name__}",
func=xunit_setup_module_fixture,
nodeid=self.nodeid,
scope="module",
autouse=True,
)
def _inject_setup_function_fixture(self) -> None:
"""Inject a hidden autouse, function scoped fixture into the collected module object
def _register_setup_function_fixture(self) -> None:
"""Register an autouse, function-scoped fixture for the collected module object
that invokes setup_function/teardown_function if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
@ -633,25 +635,27 @@ class Module(nodes.File, PyCollector):
if setup_function is None and teardown_function is None:
return
@fixtures.fixture(
autouse=True,
scope="function",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_function_fixture_{self.obj.__name__}",
)
def xunit_setup_function_fixture(request) -> Generator[None, None, None]:
if request.instance is not None:
# in this case we are bound to an instance, so we need to let
# setup_method handle this
yield
return
function = request.function
if setup_function is not None:
_call_with_optional_argument(setup_function, request.function)
_call_with_optional_argument(setup_function, function)
yield
if teardown_function is not None:
_call_with_optional_argument(teardown_function, request.function)
_call_with_optional_argument(teardown_function, function)
self.obj.__pytest_setup_function = xunit_setup_function_fixture
self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
name=f"_xunit_setup_function_fixture_{self.obj.__name__}",
func=xunit_setup_function_fixture,
nodeid=self.nodeid,
scope="function",
autouse=True,
)
class Package(nodes.Directory):
@ -795,15 +799,15 @@ class Class(PyCollector):
)
return []
self._inject_setup_class_fixture()
self._inject_setup_method_fixture()
self._register_setup_class_fixture()
self._register_setup_method_fixture()
self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid)
return super().collect()
def _inject_setup_class_fixture(self) -> None:
"""Inject a hidden autouse, class scoped fixture into the collected class object
def _register_setup_class_fixture(self) -> None:
"""Register an autouse, class scoped fixture into the collected class object
that invokes setup_class/teardown_class if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
@ -814,25 +818,27 @@ class Class(PyCollector):
if setup_class is None and teardown_class is None:
return
@fixtures.fixture(
autouse=True,
scope="class",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}",
)
def xunit_setup_class_fixture(cls) -> Generator[None, None, None]:
def xunit_setup_class_fixture(request) -> Generator[None, None, None]:
cls = request.cls
if setup_class is not None:
func = getimfunc(setup_class)
_call_with_optional_argument(func, self.obj)
_call_with_optional_argument(func, cls)
yield
if teardown_class is not None:
func = getimfunc(teardown_class)
_call_with_optional_argument(func, self.obj)
_call_with_optional_argument(func, cls)
self.obj.__pytest_setup_class = xunit_setup_class_fixture
self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}",
func=xunit_setup_class_fixture,
nodeid=self.nodeid,
scope="class",
autouse=True,
)
def _inject_setup_method_fixture(self) -> None:
"""Inject a hidden autouse, function scoped fixture into the collected class object
def _register_setup_method_fixture(self) -> None:
"""Register an autouse, function scoped fixture into the collected class object
that invokes setup_method/teardown_method if either or both are available.
Using a fixture to invoke these methods ensures we play nicely and unsurprisingly with
@ -845,23 +851,25 @@ class Class(PyCollector):
if setup_method is None and teardown_method is None:
return
@fixtures.fixture(
autouse=True,
scope="function",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}",
)
def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
def xunit_setup_method_fixture(request) -> Generator[None, None, None]:
instance = request.instance
method = request.function
if setup_method is not None:
func = getattr(self, setup_name)
func = getattr(instance, setup_name)
_call_with_optional_argument(func, method)
yield
if teardown_method is not None:
func = getattr(self, teardown_name)
func = getattr(instance, teardown_name)
_call_with_optional_argument(func, method)
self.obj.__pytest_setup_method = xunit_setup_method_fixture
self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}",
func=xunit_setup_method_fixture,
nodeid=self.nodeid,
scope="function",
autouse=True,
)
def hasinit(obj: object) -> bool:

View File

@ -29,7 +29,6 @@ from _pytest.python import Class
from _pytest.python import Function
from _pytest.python import Module
from _pytest.runner import CallInfo
from _pytest.scope import Scope
if TYPE_CHECKING:
import unittest
@ -71,8 +70,9 @@ class UnitTestCase(Class):
skipped = _is_skipped(cls)
if not skipped:
self._inject_setup_teardown_fixtures(cls)
self._inject_setup_class_fixture()
self._register_unittest_setup_method_fixture(cls)
self._register_unittest_setup_class_fixture(cls)
self._register_setup_class_fixture()
self.session._fixturemanager.parsefactories(self, unittest=True)
loader = TestLoader()
@ -93,91 +93,75 @@ class UnitTestCase(Class):
if ut is None or runtest != ut.TestCase.runTest: # type: ignore
yield TestCaseFunction.from_parent(self, name="runTest")
def _inject_setup_teardown_fixtures(self, cls: type) -> None:
"""Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding
teardown functions (#517)."""
class_fixture = _make_xunit_fixture(
cls,
"setUpClass",
"tearDownClass",
"doClassCleanups",
scope=Scope.Class,
pass_self=False,
)
if class_fixture:
cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined]
def _register_unittest_setup_class_fixture(self, cls: type) -> None:
"""Register an auto-use fixture to invoke setUpClass and
tearDownClass (#517)."""
setup = getattr(cls, "setUpClass", None)
teardown = getattr(cls, "tearDownClass", None)
if setup is None and teardown is None:
return None
cleanup = getattr(cls, "doClassCleanups", lambda: None)
method_fixture = _make_xunit_fixture(
cls,
"setup_method",
"teardown_method",
None,
scope=Scope.Function,
pass_self=True,
)
if method_fixture:
cls.__pytest_method_setup = method_fixture # type: ignore[attr-defined]
def _make_xunit_fixture(
obj: type,
setup_name: str,
teardown_name: str,
cleanup_name: Optional[str],
scope: Scope,
pass_self: bool,
):
setup = getattr(obj, setup_name, None)
teardown = getattr(obj, teardown_name, None)
if setup is None and teardown is None:
return None
if cleanup_name:
cleanup = getattr(obj, cleanup_name, lambda *args: None)
else:
def cleanup(*args):
pass
@pytest.fixture(
scope=scope.value,
autouse=True,
# Use a unique name to speed up lookup.
name=f"_unittest_{setup_name}_fixture_{obj.__qualname__}",
)
def fixture(self, request: FixtureRequest) -> Generator[None, None, None]:
if _is_skipped(self):
reason = self.__unittest_skip_why__
raise pytest.skip.Exception(reason, _use_item_location=True)
if setup is not None:
try:
if pass_self:
setup(self, request.function)
else:
def unittest_setup_class_fixture(
request: FixtureRequest,
) -> Generator[None, None, None]:
cls = request.cls
if _is_skipped(cls):
reason = cls.__unittest_skip_why__
raise pytest.skip.Exception(reason, _use_item_location=True)
if setup is not None:
try:
setup()
# unittest does not call the cleanup function for every BaseException, so we
# follow this here.
except Exception:
if pass_self:
cleanup(self)
else:
# unittest does not call the cleanup function for every BaseException, so we
# follow this here.
except Exception:
cleanup()
raise
yield
try:
if teardown is not None:
if pass_self:
teardown(self, request.function)
else:
raise
yield
try:
if teardown is not None:
teardown()
finally:
if pass_self:
cleanup(self)
else:
finally:
cleanup()
return fixture
self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
name=f"_unittest_setUpClass_fixture_{cls.__qualname__}",
func=unittest_setup_class_fixture,
nodeid=self.nodeid,
scope="class",
autouse=True,
)
def _register_unittest_setup_method_fixture(self, cls: type) -> None:
"""Register an auto-use fixture to invoke setup_method and
teardown_method (#517)."""
setup = getattr(cls, "setup_method", None)
teardown = getattr(cls, "teardown_method", None)
if setup is None and teardown is None:
return None
def unittest_setup_method_fixture(
request: FixtureRequest,
) -> Generator[None, None, None]:
self = request.instance
if _is_skipped(self):
reason = self.__unittest_skip_why__
raise pytest.skip.Exception(reason, _use_item_location=True)
if setup is not None:
setup(self, request.function)
yield
if teardown is not None:
teardown(self, request.function)
self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
name=f"_unittest_setup_method_fixture_{cls.__qualname__}",
func=unittest_setup_method_fixture,
nodeid=self.nodeid,
scope="function",
autouse=True,
)
class TestCaseFunction(Function):

View File

@ -1,7 +1,9 @@
anyio[curio,trio]==4.2.0
django==5.0
pytest-asyncio==0.23.2
pytest-bdd==7.0.1
pytest-asyncio==0.23.3
# Temporarily not installed until pytest-bdd is fixed:
# https://github.com/pytest-dev/pytest/pull/11785
# pytest-bdd==7.0.1
pytest-cov==4.1.0
pytest-django==4.7.0
pytest-flakes==4.0.5

View File

@ -1574,7 +1574,7 @@ class TestFixtureManagerParseFactories:
"""
def test_hello(item, fm):
for name in ("fm", "hello", "item"):
faclist = fm.getfixturedefs(name, item.nodeid)
faclist = fm.getfixturedefs(name, item)
assert len(faclist) == 1
fac = faclist[0]
assert fac.func.__name__ == name
@ -1598,7 +1598,7 @@ class TestFixtureManagerParseFactories:
def hello(self, request):
return "class"
def test_hello(self, item, fm):
faclist = fm.getfixturedefs("hello", item.nodeid)
faclist = fm.getfixturedefs("hello", item)
print(faclist)
assert len(faclist) == 3
@ -1804,7 +1804,7 @@ class TestAutouseDiscovery:
"""
from _pytest.pytester import get_public_names
def test_check_setup(item, fm):
autousenames = list(fm._getautousenames(item.nodeid))
autousenames = list(fm._getautousenames(item))
assert len(get_public_names(autousenames)) == 2
assert "perfunction2" in autousenames
assert "perfunction" in autousenames

View File

@ -2,7 +2,6 @@ import re
import warnings
from pathlib import Path
from typing import cast
from typing import List
from typing import Type
import pytest
@ -12,29 +11,6 @@ from _pytest.pytester import Pytester
from _pytest.warning_types import PytestWarning
@pytest.mark.parametrize(
("nodeid", "expected"),
(
("", [""]),
("a", ["", "a"]),
("aa/b", ["", "aa", "aa/b"]),
("a/b/c", ["", "a", "a/b", "a/b/c"]),
("a/bbb/c::D", ["", "a", "a/bbb", "a/bbb/c", "a/bbb/c::D"]),
("a/b/c::D::eee", ["", "a", "a/b", "a/b/c", "a/b/c::D", "a/b/c::D::eee"]),
("::xx", ["", "::xx"]),
# / only considered until first ::
("a/b/c::D/d::e", ["", "a", "a/b", "a/b/c", "a/b/c::D/d", "a/b/c::D/d::e"]),
# : alone is not a separator.
("a/b::D:e:f::g", ["", "a", "a/b", "a/b::D:e:f", "a/b::D:e:f::g"]),
# / not considered if a part of a test name
("a/b::c/d::e[/test]", ["", "a", "a/b", "a/b::c/d", "a/b::c/d::e[/test]"]),
),
)
def test_iterparentnodeids(nodeid: str, expected: List[str]) -> None:
result = list(nodes.iterparentnodeids(nodeid))
assert result == expected
def test_node_from_parent_disallowed_arguments() -> None:
with pytest.raises(TypeError, match="session is"):
nodes.Node.from_parent(None, session=None) # type: ignore[arg-type]

View File

@ -134,9 +134,11 @@ changedir = testing/plugins_integration
deps = -rtesting/plugins_integration/requirements.txt
setenv =
PYTHONPATH=.
# Command temporarily removed until pytest-bdd is fixed:
# https://github.com/pytest-dev/pytest/pull/11785
# pytest bdd_wallet.py
commands =
pip check
pytest bdd_wallet.py
pytest --cov=. simple_integration.py
pytest --ds=django_settings simple_integration.py
pytest --html=simple.html simple_integration.py