Merge pull request #12264 from bluetech/reraise-with-original-tb
fixtures,runner: fix tracebacks getting longer and longer
This commit is contained in:
commit
feaae2fb35
|
@ -0,0 +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``.
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue