diff --git a/changelog/4038.bugfix.rst b/changelog/4038.bugfix.rst new file mode 100644 index 00000000000..372be13f327 --- /dev/null +++ b/changelog/4038.bugfix.rst @@ -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. diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 8357991fe17..e9e76b0e67e 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -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