Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/14635.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed fixture resolution failures when specifying multiple test paths on the command line that share common parent directories. Parametrized fixtures defined in a ``conftest.py`` could be silently dropped depending on the order of the specified paths.
24 changes: 24 additions & 0 deletions src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,30 @@ def _collect_one_node(
return rep, True
else:
rep = collect_one_node(node)
if rep.passed and node in self._collection_cache:
# Re-collection (handle_dupes=False) creates fresh child nodes.
# Reuse previously-seen Directory children so that fixture
# registration (keyed by node identity) remains valid. (#14635)
#
# This post-processing reconciliation is intentional: the
# collection architecture is not ready for a session-global
# Directory node registry, and pushing deduplication into
# individual collectors (Dir.collect / Package.collect) would
# leak session-level concerns into node code and miss
# third-party Directory subclasses.
prev_result = self._collection_cache[node].result
prev_dirs = {
child.path: child
for child in prev_result
if isinstance(child, nodes.Directory)
}
if prev_dirs:
rep.result = [
prev_dirs.get(child.path, child)
if isinstance(child, nodes.Directory)
else child
for child in rep.result
]
self._collection_cache[node] = rep
return rep, False

Expand Down
109 changes: 109 additions & 0 deletions testing/test_conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,115 @@ def test_uses_ancestor(ancestor_fixture):
result.stdout.fnmatch_lines(["*test_uses_ancestor*PASSED*", "*1 passed*"])


def test_fixture_closure_order_independence_with_parametrize(
pytester: Pytester,
) -> None:
"""Regression test for #14635.

A test's fixture closure (and thus parametrize validation) should be
independent of which unrelated paths were collected earlier in the session.

The scenario: a test uses @pytest.mark.parametrize("fixture_param", [...])
where fixture_param is NOT a direct arg of the test but IS an argname of a
fixture the test depends on transitively. Collecting unrelated directories
before the test's directory should not cause the fixture_param to drop out
of the closure.
"""
root = pytester.path
tests = root / "tests"
tests.mkdir()
tests.joinpath("__init__.py").write_text("", encoding="utf-8")

# tests/conftest.py - empty (or with some unrelated fixture)
tests.joinpath("conftest.py").write_text(
textwrap.dedent("""\
import pytest
"""),
encoding="utf-8",
)

# tests/components/ with its conftest
components = tests / "components"
components.mkdir()
components.joinpath("__init__.py").write_text("", encoding="utf-8")
components.joinpath("conftest.py").write_text(
textwrap.dedent("""\
import pytest

@pytest.fixture
def cache_dir_side_effect():
return None

@pytest.fixture
def mock_init_cache_dir(cache_dir_side_effect):
return cache_dir_side_effect

@pytest.fixture
def mock_cache_dir(mock_init_cache_dir):
return mock_init_cache_dir
"""),
encoding="utf-8",
)

# tests/components/water_heater/ - unrelated test directory
water_heater = components / "water_heater"
water_heater.mkdir()
water_heater.joinpath("__init__.py").write_text("", encoding="utf-8")
water_heater.joinpath("test_water_heater.py").write_text(
textwrap.dedent("""\
def test_water_heater():
pass
"""),
encoding="utf-8",
)

# tests/components/tts/ - the problematic test directory
tts = components / "tts"
tts.mkdir()
tts.joinpath("__init__.py").write_text("", encoding="utf-8")
tts.joinpath("conftest.py").write_text(
textwrap.dedent("""\
import pytest

@pytest.fixture(autouse=True)
def mock_cache_dir(mock_cache_dir):
# Autouse override that requests the parent fixture of same name
return mock_cache_dir
"""),
encoding="utf-8",
)
tts.joinpath("test_init.py").write_text(
textwrap.dedent("""\
import pytest

@pytest.mark.parametrize("cache_dir_side_effect", ["error_value"])
async def test_setup_no_access(mock_init_cache_dir):
assert mock_init_cache_dir == "error_value"
"""),
encoding="utf-8",
)

# tests/test_config_entries.py - another unrelated test
tests.joinpath("test_config_entries.py").write_text(
textwrap.dedent("""\
def test_config():
pass
"""),
encoding="utf-8",
)

# This order triggers the bug: collecting water_heater and config_entries
# BEFORE tts causes the fixture closure to be wrong.
result = pytester.runpytest(
"--collect-only",
str(water_heater),
str(tests / "test_config_entries.py"),
str(tts / "test_init.py"),
)
result.stdout.fnmatch_lines(["*test_setup_no_access*"])
assert result.ret == ExitCode.OK


def test_required_option_help(pytester: Pytester) -> None:
pytester.makeconftest("assert 0")
x = pytester.mkdir("x")
Expand Down
Loading