Skip to content

Commit c772af2

Browse files
authored
Merge pull request #13774 from bluetech/fixture-closure-overrides
fixtures: fix fixture closure calculation to properly consider overridden fixtures
2 parents 527b46d + 72ae3db commit c772af2

File tree

3 files changed

+142
-3
lines changed

3 files changed

+142
-3
lines changed

changelog/13773.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed the static fixture closure calculation to properly consider transitive dependencies requested by overridden fixtures.

src/_pytest/fixtures.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1645,9 +1645,18 @@ def getfixtureclosure(
16451645
fixturedefs = self.getfixturedefs(argname, parentnode)
16461646
if fixturedefs:
16471647
arg2fixturedefs[argname] = fixturedefs
1648-
for arg in fixturedefs[-1].argnames:
1649-
if arg not in fixturenames_closure:
1650-
fixturenames_closure.append(arg)
1648+
1649+
# Add dependencies from this fixture.
1650+
# If it overrides a fixture with the same name and requests
1651+
# it, also add dependencies from the overridden fixtures in
1652+
# the chain. See also similar dealing in _get_active_fixturedef().
1653+
for fixturedef in reversed(fixturedefs): # pragma: no cover
1654+
for arg in fixturedef.argnames:
1655+
if arg not in fixturenames_closure:
1656+
fixturenames_closure.append(arg)
1657+
if argname not in fixturedef.argnames:
1658+
# Overrides, but doesn't request super.
1659+
break
16511660

16521661
def sort_by_scope(arg_name: str) -> Scope:
16531662
try:

testing/python/fixtures.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5069,3 +5069,132 @@ def test_method(self, /, fix):
50695069
)
50705070
result = pytester.runpytest()
50715071
result.assert_outcomes(passed=1)
5072+
5073+
5074+
def test_fixture_closure_with_overrides(pytester: Pytester) -> None:
5075+
"""Test that an item's static fixture closure properly includes transitive
5076+
dependencies through overridden fixtures (#13773)."""
5077+
pytester.makeconftest(
5078+
"""
5079+
import pytest
5080+
5081+
@pytest.fixture
5082+
def db(): pass
5083+
5084+
@pytest.fixture
5085+
def app(db): pass
5086+
"""
5087+
)
5088+
pytester.makepyfile(
5089+
"""
5090+
import pytest
5091+
5092+
# Overrides conftest-level `app` and requests it.
5093+
@pytest.fixture
5094+
def app(app): pass
5095+
5096+
class TestClass:
5097+
# Overrides module-level `app` and requests it.
5098+
@pytest.fixture
5099+
def app(self, app): pass
5100+
5101+
def test_something(self, request, app):
5102+
# Both dynamic and static fixture closures should include 'db'.
5103+
assert 'db' in request.fixturenames
5104+
assert 'db' in request.node.fixturenames
5105+
# No dynamic dependencies, should be equal.
5106+
assert set(request.fixturenames) == set(request.node.fixturenames)
5107+
"""
5108+
)
5109+
result = pytester.runpytest("-v")
5110+
result.assert_outcomes(passed=1)
5111+
5112+
5113+
@pytest.mark.xfail(reason="not currently handled correctly")
5114+
def test_fixture_closure_with_overrides_and_intermediary(pytester: Pytester) -> None:
5115+
"""Test that an item's static fixture closure properly includes transitive
5116+
dependencies through overridden fixtures (#13773).
5117+
5118+
A more complicated case than test_fixture_closure_with_overrides, adds an
5119+
intermediary so the override chain is not direct.
5120+
"""
5121+
pytester.makeconftest(
5122+
"""
5123+
import pytest
5124+
5125+
@pytest.fixture
5126+
def db(): pass
5127+
5128+
@pytest.fixture
5129+
def app(db): pass
5130+
5131+
@pytest.fixture
5132+
def intermediate(app): pass
5133+
"""
5134+
)
5135+
pytester.makepyfile(
5136+
"""
5137+
import pytest
5138+
5139+
# Overrides conftest-level `app` and requests it.
5140+
@pytest.fixture
5141+
def app(intermediate): pass
5142+
5143+
class TestClass:
5144+
# Overrides module-level `app` and requests it.
5145+
@pytest.fixture
5146+
def app(self, app): pass
5147+
5148+
def test_something(self, request, app):
5149+
# Both dynamic and static fixture closures should include 'db'.
5150+
assert 'db' in request.fixturenames
5151+
assert 'db' in request.node.fixturenames
5152+
# No dynamic dependencies, should be equal.
5153+
assert set(request.fixturenames) == set(request.node.fixturenames)
5154+
"""
5155+
)
5156+
result = pytester.runpytest("-v")
5157+
result.assert_outcomes(passed=1)
5158+
5159+
5160+
def test_fixture_closure_with_broken_override_chain(pytester: Pytester) -> None:
5161+
"""Test that an item's static fixture closure properly includes transitive
5162+
dependencies through overridden fixtures (#13773).
5163+
5164+
A more complicated case than test_fixture_closure_with_overrides, one of the
5165+
fixtures in the chain doesn't call its super, so it shouldn't be included.
5166+
"""
5167+
pytester.makeconftest(
5168+
"""
5169+
import pytest
5170+
5171+
@pytest.fixture
5172+
def db(): pass
5173+
5174+
@pytest.fixture
5175+
def app(db): pass
5176+
"""
5177+
)
5178+
pytester.makepyfile(
5179+
"""
5180+
import pytest
5181+
5182+
# Overrides conftest-level `app` and *doesn't* request it.
5183+
@pytest.fixture
5184+
def app(): pass
5185+
5186+
class TestClass:
5187+
# Overrides module-level `app` and requests it.
5188+
@pytest.fixture
5189+
def app(self, app): pass
5190+
5191+
def test_something(self, request, app):
5192+
# Both dynamic and static fixture closures should include 'db'.
5193+
assert 'db' not in request.fixturenames
5194+
assert 'db' not in request.node.fixturenames
5195+
# No dynamic dependencies, should be equal.
5196+
assert set(request.fixturenames) == set(request.node.fixturenames)
5197+
"""
5198+
)
5199+
result = pytester.runpytest("-v")
5200+
result.assert_outcomes(passed=1)

0 commit comments

Comments
 (0)