diff --git a/changelog/14992.bugfix.rst b/changelog/14992.bugfix.rst new file mode 100644 index 00000000000..f2762deaac0 --- /dev/null +++ b/changelog/14992.bugfix.rst @@ -0,0 +1 @@ +Fixed fixture setup order so fixtures requested by ``usefixtures`` remain ahead of dependencies of earlier autouse fixtures. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 05537ec01b2..446ef39f939 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1985,14 +1985,18 @@ def sort_by_scope(arg_name: str) -> Scope: else: return fixturedefs[-1]._scope - fixturenames_closure = sorted( - traverse_fixture_closure( + # Keep the initial fixtures at the front of the closure. Their order + # is observable by hooks such as pytest_generate_tests. + fixturenames_closure = list(initialnames) + fixturenames_closure.extend( + argname + for argname in traverse_fixture_closure( initialnames, getfixturedefs=getfixturedefs, - ), - key=sort_by_scope, - reverse=True, + ) + if argname not in initialnames ) + fixturenames_closure.sort(key=sort_by_scope, reverse=True) return fixturenames_closure, arg2fixturedefs diff --git a/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py b/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py index 5ea5e6ace79..0559905cea4 100644 --- a/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py +++ b/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py @@ -20,4 +20,4 @@ def b(a): def test(b, request): - assert request.fixturenames == ["b", "a", "request", "dynamic"] + assert request.fixturenames == ["b", "request", "a", "dynamic"] diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index c0b49948152..6e858005c20 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -2529,6 +2529,49 @@ def test_hello(arg1): reprec = pytester.inline_run() reprec.assertoutcome(passed=1) + def test_usefixtures_before_autouse_dependencies(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + order = [] + + @pytest.fixture + def precondition(): + order.append("precondition") + + @pytest.fixture + def subject_deps(precondition): + order.append("subject_deps") + + @pytest.fixture(autouse=True) + def subject(subject_deps): + order.append("subject") + + @pytest.fixture + def guard(): + order.append("guard") + + def pytest_generate_tests(metafunc): + names = metafunc.fixturenames + if "subject" in names: + names.remove("subject") + names.append("subject") + + @pytest.mark.usefixtures("guard") + class TestOrdering: + def test_guard_is_set_up_first(self): + assert order == [ + "guard", + "precondition", + "subject_deps", + "subject", + ] + """ + ) + result = pytester.runpytest() + result.assert_outcomes(passed=1) + @pytest.mark.parametrize("param1", ["", "params=[1]"], ids=["p00", "p01"]) @pytest.mark.parametrize("param2", ["", "params=[1]"], ids=["p10", "p11"]) def test_ordering_dependencies_torndown_first( @@ -4673,8 +4716,8 @@ def test_foo(f1, p1, m1, f2, s1): # Actual fixture execution differs from static order: dependent # fixtures must be created first ("my_tmp_path"). assert fixture_order == [ - "my_tmp_path_factory", "s1", + "my_tmp_path_factory", "p1", "m1", "my_tmp_path", @@ -4689,13 +4732,13 @@ def test_foo(f1, p1, m1, f2, s1): # Static order of fixtures based on their scope and position in the # parameter list. assert request.fixturenames == [ - "my_tmp_path_factory", "s1", + "my_tmp_path_factory", "p1", "m1", "f1", - "my_tmp_path", "f2", + "my_tmp_path", ] result = pytester.runpytest("-vv") result.assert_outcomes(passed=1) @@ -5871,7 +5914,7 @@ def test_circular_deps(fix_a, fix_x): ) items, _hookrec = pytester.inline_genitems() assert isinstance(items[0], Function) - assert items[0].fixturenames == ["fix_a", "fix_b", "fix_x", "fix_y", "fix_z"] + assert items[0].fixturenames == ["fix_a", "fix_x", "fix_b", "fix_y", "fix_z"] def test_fixture_closure_handles_diamond_dependencies(pytester: Pytester) -> None: