diff --git a/changelog/1895.bugfix.rst b/changelog/1895.bugfix.rst new file mode 100644 index 000000000..44b921ad9 --- /dev/null +++ b/changelog/1895.bugfix.rst @@ -0,0 +1,2 @@ +Fix bug where fixtures requested dynamically via ``request.getfixturevalue()`` might be teardown +before the requesting fixture. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index b2ad9aae3..2635d095e 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -585,11 +585,13 @@ class FixtureRequest(FuncargnamesCompatAttr): # call the fixture function fixturedef.execute(request=subrequest) finally: - # if fixture function failed it might have registered finalizers - self.session._setupstate.addfinalizer( - functools.partial(fixturedef.finish, request=subrequest), - subrequest.node, - ) + self._schedule_finalizers(fixturedef, subrequest) + + def _schedule_finalizers(self, fixturedef, subrequest): + # if fixture function failed it might have registered finalizers + self.session._setupstate.addfinalizer( + functools.partial(fixturedef.finish, request=subrequest), subrequest.node + ) def _check_scope(self, argname, invoking_scope, requested_scope): if argname == "request": @@ -659,6 +661,16 @@ class SubRequest(FixtureRequest): def addfinalizer(self, finalizer): self._fixturedef.addfinalizer(finalizer) + def _schedule_finalizers(self, fixturedef, subrequest): + # if the executing fixturedef was not explicitly requested in the argument list (via + # getfixturevalue inside the fixture call) then ensure this fixture def will be finished + # first + if fixturedef.argname not in self.funcargnames: + fixturedef.addfinalizer( + functools.partial(self._fixturedef.finish, request=self) + ) + super(SubRequest, self)._schedule_finalizers(fixturedef, subrequest) + scopes = "session package module class function".split() scopenum_function = scopes.index("function") diff --git a/testing/python/fixture.py b/testing/python/fixtures.py similarity index 98% rename from testing/python/fixture.py rename to testing/python/fixtures.py index 3d557cec8..1ea37f85c 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixtures.py @@ -562,6 +562,44 @@ class TestRequestBasic(object): reprec = testdir.inline_run() reprec.assertoutcome(passed=1) + def test_getfixturevalue_teardown(self, testdir): + """ + Issue #1895 + + `test_inner` requests `inner` fixture, which in turn requests `resource` + using `getfixturevalue`. `test_func` then requests `resource`. + + `resource` is teardown before `inner` because the fixture mechanism won't consider + `inner` dependent on `resource` when it is used via `getfixturevalue`: `test_func` + will then cause the `resource`'s finalizer to be called first because of this. + """ + testdir.makepyfile( + """ + import pytest + + @pytest.fixture(scope='session') + def resource(): + r = ['value'] + yield r + r.pop() + + @pytest.fixture(scope='session') + def inner(request): + resource = request.getfixturevalue('resource') + assert resource == ['value'] + yield + assert resource == ['value'] + + def test_inner(inner): + pass + + def test_func(resource): + pass + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines("* 2 passed in *") + @pytest.mark.parametrize("getfixmethod", ("getfixturevalue", "getfuncargvalue")) def test_getfixturevalue(self, testdir, getfixmethod): item = testdir.getitem(