Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ would only rerun those errors that match ``AssertionError`` or ``ValueError``:

$ pytest --reruns 5 --only-rerun AssertionError --only-rerun ValueError

The same matching is applied to each exception in the ``__cause__`` /
``__context__`` chain, so a wrapped error such as
``raise RuntimeError(...) from MemoryError(...)`` is still rerun by
``--only-rerun MemoryError``.

Re-run all failures other than matching certain expressions
-----------------------------------------------------------

Expand Down
1 change: 1 addition & 0 deletions changes/353.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Match ``only_rerun`` and ``rerun_except`` against the full exception chain (``__cause__`` and ``__context__``), not only the outermost exception.
21 changes: 18 additions & 3 deletions src/pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,12 +545,27 @@ def _matches_any_rerun_except_error(rerun_except_errors, excinfo):
return _try_match_error(rerun_except_errors, excinfo)


def _iter_exception_chain(exc):
seen = set()
while exc is not None and id(exc) not in seen:
seen.add(id(exc))
yield exc
if exc.__cause__ is not None:
exc = exc.__cause__
elif not getattr(exc, "__suppress_context__", False):
exc = exc.__context__
else:
exc = None


def _try_match_error(rerun_errors, excinfo):
if excinfo:
err = f"{excinfo.type.__name__}: {excinfo.value}"
if not excinfo:
return False
for exc in _iter_exception_chain(excinfo.value):
err = f"{type(exc).__name__}: {exc}"
for rerun_error in rerun_errors:
if isinstance(rerun_error, type) and issubclass(rerun_error, BaseException):
if issubclass(excinfo.type, rerun_error):
if isinstance(exc, rerun_error):
return True
elif re.search(rerun_error, err):
return True
Expand Down
81 changes: 81 additions & 0 deletions tests/test_pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,87 @@ def test_only_rerun2():
)


@pytest.mark.parametrize(
"only_rerun,should_rerun",
[
("MemoryError", True),
("out of memory", True),
("ValueError", False),
],
)
def test_only_rerun_matches_wrapped_cause(testdir, only_rerun, should_rerun):
testdir.makepyfile(
"""
def test_wrapped():
try:
raise MemoryError("out of memory")
except MemoryError as error:
raise RuntimeError("something failed") from error
"""
)
result = testdir.runpytest("--reruns", "1", "--only-rerun", only_rerun)
assert_outcomes(result, passed=0, failed=1, rerun=1 if should_rerun else 0)


def test_only_rerun_matches_implicit_context(testdir):
testdir.makepyfile(
"""
def test_wrapped():
try:
raise MemoryError("out of memory")
except MemoryError:
raise RuntimeError("something failed")
"""
)
result = testdir.runpytest("--reruns", "1", "--only-rerun", "MemoryError")
assert_outcomes(result, passed=0, failed=1, rerun=1)


def test_only_rerun_exception_class_matches_wrapped_cause(testdir):
testdir.makepyfile(
"""
import pytest

@pytest.mark.flaky(reruns=1, only_rerun=[MemoryError])
def test_wrapped():
try:
raise MemoryError("out of memory")
except MemoryError as error:
raise RuntimeError("something failed") from error
"""
)
result = testdir.runpytest()
assert_outcomes(result, passed=0, failed=1, rerun=1)


def test_only_rerun_ignores_suppressed_context(testdir):
testdir.makepyfile(
"""
def test_wrapped():
try:
raise MemoryError("out of memory")
except MemoryError:
raise RuntimeError("something failed") from None
"""
)
result = testdir.runpytest("--reruns", "1", "--only-rerun", "MemoryError")
assert_outcomes(result, passed=0, failed=1, rerun=0)


def test_rerun_except_matches_wrapped_cause(testdir):
testdir.makepyfile(
"""
def test_wrapped():
try:
raise ValueError("bad value")
except ValueError as error:
raise RuntimeError("something failed") from error
"""
)
result = testdir.runpytest("--reruns", "1", "--rerun-except", "ValueError")
assert_outcomes(result, passed=0, failed=1, rerun=0)


def test_no_rerun_on_strict_xfail_with_only_rerun_flag(testdir):
testdir.makepyfile(
"""
Expand Down