diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 1a6e245c75b..64d21b9f696 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -733,10 +733,19 @@ def addfinalizer(self, finalizer): self._finalizer.append(finalizer) def finish(self): + exceptions = [] try: while self._finalizer: - func = self._finalizer.pop() - func() + try: + func = self._finalizer.pop() + func() + except: + exceptions.append(sys.exc_info()) + if exceptions: + e = exceptions[0] + del exceptions # ensure we don't keep all frames alive because of the traceback + py.builtin._reraise(*e) + finally: ihook = self._fixturemanager.session.ihook ihook.pytest_fixture_post_finalizer(fixturedef=self) diff --git a/changelog/2440.bugfix b/changelog/2440.bugfix new file mode 100644 index 00000000000..7f1f7d50406 --- /dev/null +++ b/changelog/2440.bugfix @@ -0,0 +1 @@ +Exceptions raised during teardown by finalizers are now suppressed until all finalizers are called, with the initial exception reraised. diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 4c9ad7a9185..1e58a555046 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -657,6 +657,39 @@ def test_second(): "*1 error*" # XXX the whole module collection fails ]) + def test_request_subrequest_addfinalizer_exceptions(self, testdir): + """ + Ensure exceptions raised during teardown by a finalizer are suppressed + until all finalizers are called, re-raising the first exception (#2440) + """ + testdir.makepyfile(""" + import pytest + l = [] + def _excepts(where): + raise Exception('Error in %s fixture' % where) + @pytest.fixture + def subrequest(request): + return request + @pytest.fixture + def something(subrequest): + subrequest.addfinalizer(lambda: l.append(1)) + subrequest.addfinalizer(lambda: l.append(2)) + subrequest.addfinalizer(lambda: _excepts('something')) + @pytest.fixture + def excepts(subrequest): + subrequest.addfinalizer(lambda: _excepts('excepts')) + subrequest.addfinalizer(lambda: l.append(3)) + def test_first(something, excepts): + pass + def test_second(): + assert l == [3, 2, 1] + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + '*Exception: Error in excepts fixture', + '* 2 passed, 1 error in *', + ]) + def test_request_getmodulepath(self, testdir): modcol = testdir.getmodulecol("def test_somefunc(): pass") item, = testdir.genitems([modcol])