Skip to content

Commit 9918093

Browse files
committed
fixtures register finalizers with all fixtures before them in the stack
1 parent 1ec5bef commit 9918093

File tree

4 files changed

+192
-3
lines changed

4 files changed

+192
-3
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Ceridwen
5454
Charles Cloud
5555
Charnjit SiNGH (CCSJ)
5656
Chris Lamb
57+
Chris NeJame
5758
Christian Boelsen
5859
Christian Fetzer
5960
Christian Neumüller

changelog/6436.bugfix.rst

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:class:`FixtureDef <_pytest.fixtures.FixtureDef>` objects now properly register their finalizers with autouse and
2+
parameterized fixtures that execute before them in the fixture stack so they are torn
3+
down at the right times, and in the right order.

src/_pytest/fixtures.py

+56-3
Original file line numberDiff line numberDiff line change
@@ -882,9 +882,7 @@ def finish(self, request):
882882
self._finalizers = []
883883

884884
def execute(self, request):
885-
# get required arguments and register our own finish()
886-
# with their finalization
887-
for argname in self.argnames:
885+
for argname in self._dependee_fixture_argnames(request):
888886
fixturedef = request._get_active_fixturedef(argname)
889887
if argname != "request":
890888
fixturedef.addfinalizer(functools.partial(self.finish, request=request))
@@ -907,6 +905,61 @@ def execute(self, request):
907905
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
908906
return hook.pytest_fixture_setup(fixturedef=self, request=request)
909907

908+
def _dependee_fixture_argnames(self, request):
909+
"""A list of argnames for fixtures that this fixture depends on.
910+
911+
Given a request, this looks at the currently known list of fixture argnames, and
912+
attempts to determine what slice of the list contains fixtures that it can know
913+
should execute before it. This information is necessary so that this fixture can
914+
know what fixtures to register its finalizer with to make sure that if they
915+
would be torn down, they would tear down this fixture before themselves. It's
916+
crucial for fixtures to be torn down in the inverse order that they were set up
917+
in so that they don't try to clean up something that another fixture is still
918+
depending on.
919+
920+
When autouse fixtures are involved, it can be tricky to figure out when fixtures
921+
should be torn down. To solve this, this method leverages the ``fixturenames``
922+
list provided by the ``request`` object, as this list is at least somewhat
923+
sorted (in terms of the order fixtures are set up in) by the time this method is
924+
reached. It's sorted enough that the starting point of fixtures that depend on
925+
this one can be found using the ``self._parent_request`` stack.
926+
927+
If a request in the ``self._parent_request`` stack has a ``:class:FixtureDef``
928+
associated with it, then that fixture is dependent on this one, so any fixture
929+
names that appear in the list of fixture argnames that come after it can also be
930+
ruled out. The argnames of all fixtures associated with a request in the
931+
``self._parent_request`` stack are found, and the lowest index argname is
932+
considered the earliest point in the list of fixture argnames where everything
933+
from that point onward can be considered to execute after this fixture.
934+
Everything before this point can be considered fixtures that this fixture
935+
depends on, and so this fixture should register its finalizer with all of them
936+
to ensure that if any of them are to be torn down, they will tear this fixture
937+
down first.
938+
939+
This is the first part of the list of fixture argnames that is returned. The last
940+
part of the list is everything in ``self.argnames`` as those are explicit
941+
dependees of this fixture, so this fixture should definitely register its
942+
finalizer with them.
943+
"""
944+
all_fix_names = request.fixturenames
945+
try:
946+
current_fix_index = all_fix_names.index(self.argname)
947+
except ValueError:
948+
current_fix_index = len(request.fixturenames)
949+
parent_fixture_indexes = set()
950+
951+
parent_request = request._parent_request
952+
while hasattr(parent_request, "_parent_request"):
953+
if hasattr(parent_request, "_fixturedef"):
954+
parent_fix_name = parent_request._fixturedef.argname
955+
if parent_fix_name in all_fix_names:
956+
parent_fixture_indexes.add(all_fix_names.index(parent_fix_name))
957+
parent_request = parent_request._parent_request
958+
959+
stack_slice_index = min([current_fix_index, *parent_fixture_indexes])
960+
active_fixture_argnames = all_fix_names[:stack_slice_index]
961+
return {*active_fixture_argnames, *self.argnames}
962+
910963
def cache_key(self, request):
911964
return request.param_index if not hasattr(request, "param") else request.param
912965

testing/python/fixtures.py

+132
Original file line numberDiff line numberDiff line change
@@ -1716,6 +1716,138 @@ def test_world(self):
17161716
reprec.assertoutcome(passed=3)
17171717

17181718

1719+
class TestMultiLevelAutouseAndParameterization:
1720+
def test_setup_and_teardown_order(self, testdir):
1721+
"""Tests that parameterized fixtures effect subsequent fixtures. (#6436)
1722+
1723+
If a fixture uses a parameterized fixture, or, for any other reason, is executed
1724+
after the parameterized fixture in the fixture stack, then it should be affected
1725+
by the parameterization, and as a result, should be torn down before the
1726+
parameterized fixture, every time the parameterized fixture is torn down. This
1727+
should be the case even if autouse is involved and/or the linear order of
1728+
fixture execution isn't deterministic. In other words, before any fixture can be
1729+
torn down, every fixture that was executed after it must also be torn down.
1730+
"""
1731+
testdir.makepyfile(
1732+
test_auto="""
1733+
import pytest
1734+
def f(param):
1735+
return param
1736+
@pytest.fixture(scope="session", autouse=True)
1737+
def s_fix(request):
1738+
yield
1739+
@pytest.fixture(scope="package", params=["p1", "p2"], ids=f, autouse=True)
1740+
def p_fix(request):
1741+
yield
1742+
@pytest.fixture(scope="module", params=["m1", "m2"], ids=f, autouse=True)
1743+
def m_fix(request):
1744+
yield
1745+
@pytest.fixture(scope="class", autouse=True)
1746+
def another_c_fix(m_fix):
1747+
yield
1748+
@pytest.fixture(scope="class")
1749+
def c_fix():
1750+
yield
1751+
@pytest.fixture(scope="function", params=["f1", "f2"], ids=f, autouse=True)
1752+
def f_fix(request):
1753+
yield
1754+
class TestFixtures:
1755+
def test_a(self, c_fix):
1756+
pass
1757+
def test_b(self, c_fix):
1758+
pass
1759+
"""
1760+
)
1761+
result = testdir.runpytest("--setup-plan")
1762+
test_fixtures_used = (
1763+
"(fixtures used: another_c_fix, c_fix, f_fix, m_fix, p_fix, request, s_fix)"
1764+
)
1765+
result.stdout.fnmatch_lines(
1766+
"""
1767+
SETUP S s_fix
1768+
SETUP P p_fix[p1]
1769+
SETUP M m_fix[m1]
1770+
SETUP C another_c_fix (fixtures used: m_fix)
1771+
SETUP C c_fix
1772+
SETUP F f_fix[f1]
1773+
test_auto.py::TestFixtures::test_a[p1-m1-f1] {0}
1774+
TEARDOWN F f_fix[f1]
1775+
SETUP F f_fix[f2]
1776+
test_auto.py::TestFixtures::test_a[p1-m1-f2] {0}
1777+
TEARDOWN F f_fix[f2]
1778+
SETUP F f_fix[f1]
1779+
test_auto.py::TestFixtures::test_b[p1-m1-f1] {0}
1780+
TEARDOWN F f_fix[f1]
1781+
SETUP F f_fix[f2]
1782+
test_auto.py::TestFixtures::test_b[p1-m1-f2] {0}
1783+
TEARDOWN F f_fix[f2]
1784+
TEARDOWN C c_fix
1785+
TEARDOWN C another_c_fix
1786+
TEARDOWN M m_fix[m1]
1787+
SETUP M m_fix[m2]
1788+
SETUP C another_c_fix (fixtures used: m_fix)
1789+
SETUP C c_fix
1790+
SETUP F f_fix[f1]
1791+
test_auto.py::TestFixtures::test_a[p1-m2-f1] {0}
1792+
TEARDOWN F f_fix[f1]
1793+
SETUP F f_fix[f2]
1794+
test_auto.py::TestFixtures::test_a[p1-m2-f2] {0}
1795+
TEARDOWN F f_fix[f2]
1796+
SETUP F f_fix[f1]
1797+
test_auto.py::TestFixtures::test_b[p1-m2-f1] {0}
1798+
TEARDOWN F f_fix[f1]
1799+
SETUP F f_fix[f2]
1800+
test_auto.py::TestFixtures::test_b[p1-m2-f2] {0}
1801+
TEARDOWN F f_fix[f2]
1802+
TEARDOWN C c_fix
1803+
TEARDOWN C another_c_fix
1804+
TEARDOWN M m_fix[m2]
1805+
TEARDOWN P p_fix[p1]
1806+
SETUP P p_fix[p2]
1807+
SETUP M m_fix[m1]
1808+
SETUP C another_c_fix (fixtures used: m_fix)
1809+
SETUP C c_fix
1810+
SETUP F f_fix[f1]
1811+
test_auto.py::TestFixtures::test_a[p2-m1-f1] {0}
1812+
TEARDOWN F f_fix[f1]
1813+
SETUP F f_fix[f2]
1814+
test_auto.py::TestFixtures::test_a[p2-m1-f2] {0}
1815+
TEARDOWN F f_fix[f2]
1816+
SETUP F f_fix[f1]
1817+
test_auto.py::TestFixtures::test_b[p2-m1-f1] {0}
1818+
TEARDOWN F f_fix[f1]
1819+
SETUP F f_fix[f2]
1820+
test_auto.py::TestFixtures::test_b[p2-m1-f2] {0}
1821+
TEARDOWN F f_fix[f2]
1822+
TEARDOWN C c_fix
1823+
TEARDOWN C another_c_fix
1824+
TEARDOWN M m_fix[m1]
1825+
SETUP M m_fix[m2]
1826+
SETUP C another_c_fix (fixtures used: m_fix)
1827+
SETUP C c_fix
1828+
SETUP F f_fix[f1]
1829+
test_auto.py::TestFixtures::test_a[p2-m2-f1] {0}
1830+
TEARDOWN F f_fix[f1]
1831+
SETUP F f_fix[f2]
1832+
test_auto.py::TestFixtures::test_a[p2-m2-f2] {0}
1833+
TEARDOWN F f_fix[f2]
1834+
SETUP F f_fix[f1]
1835+
test_auto.py::TestFixtures::test_b[p2-m2-f1] {0}
1836+
TEARDOWN F f_fix[f1]
1837+
SETUP F f_fix[f2]
1838+
test_auto.py::TestFixtures::test_b[p2-m2-f2] {0}
1839+
TEARDOWN F f_fix[f2]
1840+
TEARDOWN C c_fix
1841+
TEARDOWN C another_c_fix
1842+
TEARDOWN M m_fix[m2]
1843+
TEARDOWN P p_fix[p2]
1844+
TEARDOWN S s_fix
1845+
""".format(
1846+
test_fixtures_used
1847+
)
1848+
)
1849+
1850+
17191851
class TestAutouseManagement:
17201852
def test_autouse_conftest_mid_directory(self, testdir):
17211853
pkgdir = testdir.mkpydir("xyz123")

0 commit comments

Comments
 (0)