From 0b91d5e3e869d00c01c614d827745a9ed4cddb43 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 28 Apr 2024 11:44:55 +0300 Subject: [PATCH 1/2] fixtures: fix tracebacks for higher-scoped failed fixtures getting longer and longer Fix #12204. --- changelog/12204.bugfix.rst | 4 ++++ src/_pytest/fixtures.py | 11 ++++++----- testing/python/fixtures.py | 22 ++++++++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 changelog/12204.bugfix.rst 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: From 3e81cb2f455fa091bc5a78742d190437f8281c68 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 28 Apr 2024 11:47:37 +0300 Subject: [PATCH 2/2] runner: fix tracebacks for failed collectors getting longer and longer Refs https://github.com/pytest-dev/pytest/issues/12204#issuecomment-2081239376 --- changelog/12204.bugfix.rst | 3 +++ src/_pytest/runner.py | 14 ++++++++++---- testing/test_runner.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/changelog/12204.bugfix.rst b/changelog/12204.bugfix.rst index b89a04827..9690f513a 100644 --- a/changelog/12204.bugfix.rst +++ b/changelog/12204.bugfix.rst @@ -1,4 +1,7 @@ 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. +Also fix a similar regression in pytest 5.4 for collectors which raise during setup. + The fix necessitated internal changes which may affect some plugins: - ``FixtureDef.cached_result[2]`` is now a tuple ``(exc, tb)`` instead of ``exc``. +- ``SetupState.stack`` failures are now a tuple ``(exc, tb)`` instead of ``exc``. diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index a551f715a..bf4d9a37f 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -5,6 +5,7 @@ import bdb import dataclasses import os import sys +import types from typing import Callable from typing import cast from typing import Dict @@ -488,8 +489,13 @@ class SetupState: Tuple[ # Node's finalizers. List[Callable[[], object]], - # Node's exception, if its setup raised. - Optional[Union[OutcomeException, Exception]], + # Node's exception and original traceback, if its setup raised. + Optional[ + Tuple[ + Union[OutcomeException, Exception], + Optional[types.TracebackType], + ] + ], ], ] = {} @@ -502,7 +508,7 @@ class SetupState: for col, (finalizers, exc) in self.stack.items(): assert col in needed_collectors, "previous item was not torn down properly" if exc: - raise exc + raise exc[0].with_traceback(exc[1]) for col in needed_collectors[len(self.stack) :]: assert col not in self.stack @@ -511,7 +517,7 @@ class SetupState: try: col.setup() except TEST_OUTCOME as exc: - self.stack[col] = (self.stack[col][0], exc) + self.stack[col] = (self.stack[col][0], (exc, exc.__traceback__)) raise def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None: diff --git a/testing/test_runner.py b/testing/test_runner.py index 8b41ec28a..ecb98f2ff 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -142,6 +142,43 @@ class TestSetupState: assert isinstance(func.exceptions[0], TypeError) # type: ignore assert isinstance(func.exceptions[1], ValueError) # type: ignore + def test_cached_exception_doesnt_get_longer(self, pytester: Pytester) -> None: + """Regression test for #12204 (the "BTW" case).""" + pytester.makepyfile(test="") + # If the collector.setup() raises, all collected items error with this + # exception. + pytester.makeconftest( + """ + import pytest + + class MyItem(pytest.Item): + def runtest(self) -> None: pass + + class MyBadCollector(pytest.Collector): + def collect(self): + return [ + MyItem.from_parent(self, name="one"), + MyItem.from_parent(self, name="two"), + MyItem.from_parent(self, name="three"), + ] + + def setup(self): + 1 / 0 + + def pytest_collect_file(file_path, parent): + if file_path.name == "test.py": + return MyBadCollector.from_parent(parent, name='bad') + """ + ) + + 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 BaseFunctionalTests: def test_passfunction(self, pytester: Pytester) -> None: