diff --git a/changelog/12204.bugfix.rst b/changelog/12204.bugfix.rst new file mode 100644 index 000000000..b89a04827 --- /dev/null +++ b/changelog/12204.bugfix.rst @@ -0,0 +1,4 @@ +Fix a regression in pytest 8.0 where tracebacks get longer and longer when multiple tests fail due to a shared higher-scope fixture which raised. + +The fix necessitated internal changes which may affect some plugins: +- ``FixtureDef.cached_result[2]`` is now a tuple ``(exc, tb)`` instead of ``exc``. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 09fd07422..5f10d565f 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -8,6 +8,7 @@ import inspect import os from pathlib import Path import sys +import types from typing import AbstractSet from typing import Any from typing import Callable @@ -104,8 +105,8 @@ _FixtureCachedResult = Union[ None, # Cache key. object, - # Exception if raised. - BaseException, + # The exception and the original traceback. + Tuple[BaseException, Optional[types.TracebackType]], ], ] @@ -1049,8 +1050,8 @@ class FixtureDef(Generic[FixtureValue]): # numpy arrays (#6497). if my_cache_key is cache_key: if self.cached_result[2] is not None: - exc = self.cached_result[2] - raise exc + exc, exc_tb = self.cached_result[2] + raise exc.with_traceback(exc_tb) else: result = self.cached_result[0] return result @@ -1126,7 +1127,7 @@ def pytest_fixture_setup( # Don't show the fixture as the skip location, as then the user # wouldn't know which test skipped. e._use_item_location = True - fixturedef.cached_result = (None, my_cache_key, e) + fixturedef.cached_result = (None, my_cache_key, (e, e.__traceback__)) raise fixturedef.cached_result = (result, my_cache_key, None) return result diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 12ca6e926..77914fed7 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3397,6 +3397,28 @@ class TestErrors: ["*def gen(qwe123):*", "*fixture*qwe123*not found*", "*1 error*"] ) + def test_cached_exception_doesnt_get_longer(self, pytester: Pytester) -> None: + """Regression test for #12204.""" + pytester.makepyfile( + """ + import pytest + @pytest.fixture(scope="session") + def bad(): 1 / 0 + + def test_1(bad): pass + def test_2(bad): pass + def test_3(bad): pass + """ + ) + + result = pytester.runpytest_inprocess("--tb=native") + assert result.ret == ExitCode.TESTS_FAILED + failures = result.reprec.getfailures() # type: ignore[attr-defined] + assert len(failures) == 3 + lines1 = failures[1].longrepr.reprtraceback.reprentries[0].lines + lines2 = failures[2].longrepr.reprtraceback.reprentries[0].lines + assert len(lines1) == len(lines2) + class TestShowFixtures: def test_funcarg_compat(self, pytester: Pytester) -> None: