-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Description
Currently for each fixture which depends on another fixture, a "finalizer" is added to the list of the dependent fixture.
For example:
def test(tmpdir):
pass
tmpdir
, as we know, depends on tmpdir_path
to create the temporary directory. Each tmpdir
invocation ends up adding its finalization to the list of finalizers of tmpdir_path
. This is the mechanism used to finalize fixtures in the correct order (as thought we still have bugs in this area, as #1895 shows for example), and ensures that every tmpdir
will be destroyed before the requested tmpdir_path
fixture.
This then means that every high-scoped fixture might contain dozens, hundreds or thousands of "finalizers" attached to them. Fixture finalizers can be called multiple times without problems, but this consumes memory: each finalizer keeps its SubRequest
object alive, containing a number of small variables:
pytest/src/_pytest/fixtures.py
Lines 341 to 359 in ed68fcf
class FixtureRequest(FuncargnamesCompatAttr): | |
""" A request for a fixture from a test or fixture function. | |
A request object gives access to the requesting test context | |
and has an optional ``param`` attribute in case | |
the fixture is parametrized indirectly. | |
""" | |
def __init__(self, pyfuncitem): | |
self._pyfuncitem = pyfuncitem | |
#: fixture for which this request is being performed | |
self.fixturename = None | |
#: Scope string, one of "function", "class", "module", "session" | |
self.scope = "function" | |
self._fixture_defs = {} # argname -> FixtureDef | |
fixtureinfo = pyfuncitem._fixtureinfo | |
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() | |
self._arg2index = {} | |
self._fixturemanager = pyfuncitem.session._fixturemanager |
This can easily be demonstrated by applying this patch:
diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py
index 55dcd805..b3a94bc6 100644
--- a/src/_pytest/runner.py
+++ b/src/_pytest/runner.py
@@ -393,6 +393,8 @@ class SetupState(object):
for col in self.stack:
if hasattr(col, "_prepare_exc"):
six.reraise(*col._prepare_exc)
+ if self.stack:
+ print(len(self._finalizers.get(self.stack[0])))
for col in needed_collectors[len(self.stack) :]:
self.stack.append(col)
try:
(this prints the finalizers attached to the "Session" node, where the session fixtures attach their finalization to)
And running this test:
import pytest
@pytest.mark.parametrize('i', range(10))
def test(i, tmpdir):
pass
λ pytest .tmp\test-foo.py -qs
.1
.2
.3
.4
.5
.6
.7
.8
.9
.
10 passed in 0.16 seconds
I believe we can think of ways to refactor the fixture teardown mechanism to avoid this accumulation of finalizers on the objects. Ideally we should build a proper DAG of fixture dependencies which should be destroyed in the proper order. This would also make things more explicit and easier to follow IMHO.