fixtures register finalizers with all fixtures before them in the stack
This commit is contained in:
parent
1ec5befdb7
commit
99180939fe
1
AUTHORS
1
AUTHORS
|
@ -54,6 +54,7 @@ Ceridwen
|
||||||
Charles Cloud
|
Charles Cloud
|
||||||
Charnjit SiNGH (CCSJ)
|
Charnjit SiNGH (CCSJ)
|
||||||
Chris Lamb
|
Chris Lamb
|
||||||
|
Chris NeJame
|
||||||
Christian Boelsen
|
Christian Boelsen
|
||||||
Christian Fetzer
|
Christian Fetzer
|
||||||
Christian Neumüller
|
Christian Neumüller
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
:class:`FixtureDef <_pytest.fixtures.FixtureDef>` objects now properly register their finalizers with autouse and
|
||||||
|
parameterized fixtures that execute before them in the fixture stack so they are torn
|
||||||
|
down at the right times, and in the right order.
|
|
@ -882,9 +882,7 @@ class FixtureDef:
|
||||||
self._finalizers = []
|
self._finalizers = []
|
||||||
|
|
||||||
def execute(self, request):
|
def execute(self, request):
|
||||||
# get required arguments and register our own finish()
|
for argname in self._dependee_fixture_argnames(request):
|
||||||
# with their finalization
|
|
||||||
for argname in self.argnames:
|
|
||||||
fixturedef = request._get_active_fixturedef(argname)
|
fixturedef = request._get_active_fixturedef(argname)
|
||||||
if argname != "request":
|
if argname != "request":
|
||||||
fixturedef.addfinalizer(functools.partial(self.finish, request=request))
|
fixturedef.addfinalizer(functools.partial(self.finish, request=request))
|
||||||
|
@ -907,6 +905,61 @@ class FixtureDef:
|
||||||
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
|
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
|
||||||
return hook.pytest_fixture_setup(fixturedef=self, request=request)
|
return hook.pytest_fixture_setup(fixturedef=self, request=request)
|
||||||
|
|
||||||
|
def _dependee_fixture_argnames(self, request):
|
||||||
|
"""A list of argnames for fixtures that this fixture depends on.
|
||||||
|
|
||||||
|
Given a request, this looks at the currently known list of fixture argnames, and
|
||||||
|
attempts to determine what slice of the list contains fixtures that it can know
|
||||||
|
should execute before it. This information is necessary so that this fixture can
|
||||||
|
know what fixtures to register its finalizer with to make sure that if they
|
||||||
|
would be torn down, they would tear down this fixture before themselves. It's
|
||||||
|
crucial for fixtures to be torn down in the inverse order that they were set up
|
||||||
|
in so that they don't try to clean up something that another fixture is still
|
||||||
|
depending on.
|
||||||
|
|
||||||
|
When autouse fixtures are involved, it can be tricky to figure out when fixtures
|
||||||
|
should be torn down. To solve this, this method leverages the ``fixturenames``
|
||||||
|
list provided by the ``request`` object, as this list is at least somewhat
|
||||||
|
sorted (in terms of the order fixtures are set up in) by the time this method is
|
||||||
|
reached. It's sorted enough that the starting point of fixtures that depend on
|
||||||
|
this one can be found using the ``self._parent_request`` stack.
|
||||||
|
|
||||||
|
If a request in the ``self._parent_request`` stack has a ``:class:FixtureDef``
|
||||||
|
associated with it, then that fixture is dependent on this one, so any fixture
|
||||||
|
names that appear in the list of fixture argnames that come after it can also be
|
||||||
|
ruled out. The argnames of all fixtures associated with a request in the
|
||||||
|
``self._parent_request`` stack are found, and the lowest index argname is
|
||||||
|
considered the earliest point in the list of fixture argnames where everything
|
||||||
|
from that point onward can be considered to execute after this fixture.
|
||||||
|
Everything before this point can be considered fixtures that this fixture
|
||||||
|
depends on, and so this fixture should register its finalizer with all of them
|
||||||
|
to ensure that if any of them are to be torn down, they will tear this fixture
|
||||||
|
down first.
|
||||||
|
|
||||||
|
This is the first part of the list of fixture argnames that is returned. The last
|
||||||
|
part of the list is everything in ``self.argnames`` as those are explicit
|
||||||
|
dependees of this fixture, so this fixture should definitely register its
|
||||||
|
finalizer with them.
|
||||||
|
"""
|
||||||
|
all_fix_names = request.fixturenames
|
||||||
|
try:
|
||||||
|
current_fix_index = all_fix_names.index(self.argname)
|
||||||
|
except ValueError:
|
||||||
|
current_fix_index = len(request.fixturenames)
|
||||||
|
parent_fixture_indexes = set()
|
||||||
|
|
||||||
|
parent_request = request._parent_request
|
||||||
|
while hasattr(parent_request, "_parent_request"):
|
||||||
|
if hasattr(parent_request, "_fixturedef"):
|
||||||
|
parent_fix_name = parent_request._fixturedef.argname
|
||||||
|
if parent_fix_name in all_fix_names:
|
||||||
|
parent_fixture_indexes.add(all_fix_names.index(parent_fix_name))
|
||||||
|
parent_request = parent_request._parent_request
|
||||||
|
|
||||||
|
stack_slice_index = min([current_fix_index, *parent_fixture_indexes])
|
||||||
|
active_fixture_argnames = all_fix_names[:stack_slice_index]
|
||||||
|
return {*active_fixture_argnames, *self.argnames}
|
||||||
|
|
||||||
def cache_key(self, request):
|
def cache_key(self, request):
|
||||||
return request.param_index if not hasattr(request, "param") else request.param
|
return request.param_index if not hasattr(request, "param") else request.param
|
||||||
|
|
||||||
|
|
|
@ -1716,6 +1716,138 @@ class TestAutouseDiscovery:
|
||||||
reprec.assertoutcome(passed=3)
|
reprec.assertoutcome(passed=3)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMultiLevelAutouseAndParameterization:
|
||||||
|
def test_setup_and_teardown_order(self, testdir):
|
||||||
|
"""Tests that parameterized fixtures effect subsequent fixtures. (#6436)
|
||||||
|
|
||||||
|
If a fixture uses a parameterized fixture, or, for any other reason, is executed
|
||||||
|
after the parameterized fixture in the fixture stack, then it should be affected
|
||||||
|
by the parameterization, and as a result, should be torn down before the
|
||||||
|
parameterized fixture, every time the parameterized fixture is torn down. This
|
||||||
|
should be the case even if autouse is involved and/or the linear order of
|
||||||
|
fixture execution isn't deterministic. In other words, before any fixture can be
|
||||||
|
torn down, every fixture that was executed after it must also be torn down.
|
||||||
|
"""
|
||||||
|
testdir.makepyfile(
|
||||||
|
test_auto="""
|
||||||
|
import pytest
|
||||||
|
def f(param):
|
||||||
|
return param
|
||||||
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
|
def s_fix(request):
|
||||||
|
yield
|
||||||
|
@pytest.fixture(scope="package", params=["p1", "p2"], ids=f, autouse=True)
|
||||||
|
def p_fix(request):
|
||||||
|
yield
|
||||||
|
@pytest.fixture(scope="module", params=["m1", "m2"], ids=f, autouse=True)
|
||||||
|
def m_fix(request):
|
||||||
|
yield
|
||||||
|
@pytest.fixture(scope="class", autouse=True)
|
||||||
|
def another_c_fix(m_fix):
|
||||||
|
yield
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
def c_fix():
|
||||||
|
yield
|
||||||
|
@pytest.fixture(scope="function", params=["f1", "f2"], ids=f, autouse=True)
|
||||||
|
def f_fix(request):
|
||||||
|
yield
|
||||||
|
class TestFixtures:
|
||||||
|
def test_a(self, c_fix):
|
||||||
|
pass
|
||||||
|
def test_b(self, c_fix):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest("--setup-plan")
|
||||||
|
test_fixtures_used = (
|
||||||
|
"(fixtures used: another_c_fix, c_fix, f_fix, m_fix, p_fix, request, s_fix)"
|
||||||
|
)
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
"""
|
||||||
|
SETUP S s_fix
|
||||||
|
SETUP P p_fix[p1]
|
||||||
|
SETUP M m_fix[m1]
|
||||||
|
SETUP C another_c_fix (fixtures used: m_fix)
|
||||||
|
SETUP C c_fix
|
||||||
|
SETUP F f_fix[f1]
|
||||||
|
test_auto.py::TestFixtures::test_a[p1-m1-f1] {0}
|
||||||
|
TEARDOWN F f_fix[f1]
|
||||||
|
SETUP F f_fix[f2]
|
||||||
|
test_auto.py::TestFixtures::test_a[p1-m1-f2] {0}
|
||||||
|
TEARDOWN F f_fix[f2]
|
||||||
|
SETUP F f_fix[f1]
|
||||||
|
test_auto.py::TestFixtures::test_b[p1-m1-f1] {0}
|
||||||
|
TEARDOWN F f_fix[f1]
|
||||||
|
SETUP F f_fix[f2]
|
||||||
|
test_auto.py::TestFixtures::test_b[p1-m1-f2] {0}
|
||||||
|
TEARDOWN F f_fix[f2]
|
||||||
|
TEARDOWN C c_fix
|
||||||
|
TEARDOWN C another_c_fix
|
||||||
|
TEARDOWN M m_fix[m1]
|
||||||
|
SETUP M m_fix[m2]
|
||||||
|
SETUP C another_c_fix (fixtures used: m_fix)
|
||||||
|
SETUP C c_fix
|
||||||
|
SETUP F f_fix[f1]
|
||||||
|
test_auto.py::TestFixtures::test_a[p1-m2-f1] {0}
|
||||||
|
TEARDOWN F f_fix[f1]
|
||||||
|
SETUP F f_fix[f2]
|
||||||
|
test_auto.py::TestFixtures::test_a[p1-m2-f2] {0}
|
||||||
|
TEARDOWN F f_fix[f2]
|
||||||
|
SETUP F f_fix[f1]
|
||||||
|
test_auto.py::TestFixtures::test_b[p1-m2-f1] {0}
|
||||||
|
TEARDOWN F f_fix[f1]
|
||||||
|
SETUP F f_fix[f2]
|
||||||
|
test_auto.py::TestFixtures::test_b[p1-m2-f2] {0}
|
||||||
|
TEARDOWN F f_fix[f2]
|
||||||
|
TEARDOWN C c_fix
|
||||||
|
TEARDOWN C another_c_fix
|
||||||
|
TEARDOWN M m_fix[m2]
|
||||||
|
TEARDOWN P p_fix[p1]
|
||||||
|
SETUP P p_fix[p2]
|
||||||
|
SETUP M m_fix[m1]
|
||||||
|
SETUP C another_c_fix (fixtures used: m_fix)
|
||||||
|
SETUP C c_fix
|
||||||
|
SETUP F f_fix[f1]
|
||||||
|
test_auto.py::TestFixtures::test_a[p2-m1-f1] {0}
|
||||||
|
TEARDOWN F f_fix[f1]
|
||||||
|
SETUP F f_fix[f2]
|
||||||
|
test_auto.py::TestFixtures::test_a[p2-m1-f2] {0}
|
||||||
|
TEARDOWN F f_fix[f2]
|
||||||
|
SETUP F f_fix[f1]
|
||||||
|
test_auto.py::TestFixtures::test_b[p2-m1-f1] {0}
|
||||||
|
TEARDOWN F f_fix[f1]
|
||||||
|
SETUP F f_fix[f2]
|
||||||
|
test_auto.py::TestFixtures::test_b[p2-m1-f2] {0}
|
||||||
|
TEARDOWN F f_fix[f2]
|
||||||
|
TEARDOWN C c_fix
|
||||||
|
TEARDOWN C another_c_fix
|
||||||
|
TEARDOWN M m_fix[m1]
|
||||||
|
SETUP M m_fix[m2]
|
||||||
|
SETUP C another_c_fix (fixtures used: m_fix)
|
||||||
|
SETUP C c_fix
|
||||||
|
SETUP F f_fix[f1]
|
||||||
|
test_auto.py::TestFixtures::test_a[p2-m2-f1] {0}
|
||||||
|
TEARDOWN F f_fix[f1]
|
||||||
|
SETUP F f_fix[f2]
|
||||||
|
test_auto.py::TestFixtures::test_a[p2-m2-f2] {0}
|
||||||
|
TEARDOWN F f_fix[f2]
|
||||||
|
SETUP F f_fix[f1]
|
||||||
|
test_auto.py::TestFixtures::test_b[p2-m2-f1] {0}
|
||||||
|
TEARDOWN F f_fix[f1]
|
||||||
|
SETUP F f_fix[f2]
|
||||||
|
test_auto.py::TestFixtures::test_b[p2-m2-f2] {0}
|
||||||
|
TEARDOWN F f_fix[f2]
|
||||||
|
TEARDOWN C c_fix
|
||||||
|
TEARDOWN C another_c_fix
|
||||||
|
TEARDOWN M m_fix[m2]
|
||||||
|
TEARDOWN P p_fix[p2]
|
||||||
|
TEARDOWN S s_fix
|
||||||
|
""".format(
|
||||||
|
test_fixtures_used
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestAutouseManagement:
|
class TestAutouseManagement:
|
||||||
def test_autouse_conftest_mid_directory(self, testdir):
|
def test_autouse_conftest_mid_directory(self, testdir):
|
||||||
pkgdir = testdir.mkpydir("xyz123")
|
pkgdir = testdir.mkpydir("xyz123")
|
||||||
|
|
Loading…
Reference in New Issue