Skip to content
Closed
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
5 changes: 5 additions & 0 deletions changelog/4038.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Changes teardown behavior for parameterized fixtures as follows:

1. Teardown code will be called from within the pytest_runtest_teardown hook.
2. If a fixture is parameterized and the next item requires the next index, all lower scoped fixtures are torn down.
3. If a fixture of equal scope needs to be torn down and the next item requires the next index, all fixtures that were setup after the fixture are torn down, but not fixtures of equal scope that were set up earlier.
62 changes: 60 additions & 2 deletions src/_pytest/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,9 +363,67 @@ def teardown_all(self):
self._teardown_with_finalization(key)
assert not self._finalizers

def _callfinalizer(self, colitem, finalizer_index):
fin = self._finalizers[colitem].pop(finalizer_index)
try:
fin()
except TEST_OUTCOME:
if self.exc is None:
self.exc = sys.exc_info()

def _teardown_to_finalizer(self, colitem_index, finalizer_index):
colitem = self.stack[colitem_index]
finalizer = self._finalizers[colitem][finalizer_index]
while self.stack[colitem_index + 1 :] != []:
self._pop_and_teardown()
while finalizer in self._finalizers[colitem]:
self._callfinalizer(colitem, finalizer_index)
if len(self._finalizers[colitem]) == 0:
self._teardown_with_finalization(colitem)

def _get_fixture_index(self, item_object, finalizer_fix_name):
if not hasattr(item_object, "callspec"):
return None
if not hasattr(item_object.callspec, "indices"):
return None
if finalizer_fix_name not in item_object.callspec.indices:
return None
return item_object.callspec.indices[finalizer_fix_name]

def teardown_exact(self, item, nextitem):
needed_collectors = nextitem and nextitem.listchain() or []
self._teardown_towards(needed_collectors)
self.exc = None
for colitem_index in range(len(item.listchain()) - 1, -1, -1):
colitem = item.listchain()[colitem_index]
if nextitem is None:
self._teardown_towards([])
break
elif colitem not in nextitem.listchain():
while colitem in self.stack:
self._pop_and_teardown()
elif colitem in self._finalizers.keys():
for finalizer_index in range(
len(self._finalizers[colitem]) - 1, -1, -1
):
finalizer = self._finalizers[colitem][finalizer_index]
if (
not hasattr(finalizer, "keywords")
or "request" not in finalizer.keywords.keys()
or not hasattr(finalizer.keywords["request"], "fixturename")
):
continue
finalizer_fix_name = finalizer.keywords["request"].fixturename
if (
not hasattr(nextitem, "fixturenames")
or finalizer_fix_name not in nextitem.fixturenames
):
self._teardown_to_finalizer(colitem_index, finalizer_index)
elif finalizer_fix_name in nextitem.fixturenames:
if self._get_fixture_index(
item, finalizer_fix_name
) != self._get_fixture_index(nextitem, finalizer_fix_name):
self._teardown_to_finalizer(colitem_index, finalizer_index)
if self.exc:
six.reraise(*self.exc)

def _teardown_towards(self, needed_collectors):
exc = None
Expand Down