Skip to content
Merged
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
11 changes: 11 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``.
Comment thread
icemac marked this conversation as resolved.

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

Expand All @@ -128,6 +133,12 @@ would only rerun those errors that does not match with ``AssertionError`` or ``O

$ pytest --reruns 5 --rerun-except AssertionError --rerun-except OSError

Matching for ``--rerun-except`` follows explicit ``__cause__`` links
(``raise ... from ...``), so ``raise RuntimeError(...) from ValueError(...)``
is excluded by ``--rerun-except ValueError``. Implicit ``__context__`` from
``except`` / ``finally`` is not walked, so a ``ConnectionError`` raised inside
``except AssertionError`` is still rerun by ``--rerun-except AssertionError``.

Exclude test paths from re-runs
--------------------------------

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`` against the full exception chain (``__cause__`` and ``__context__``), not only the outermost exception. Match ``rerun_except`` against explicit ``__cause__`` links only, so implicit ``except`` / ``finally`` context does not suppress reruns.
27 changes: 21 additions & 6 deletions src/pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,19 +596,34 @@ def _get_rerun_filter_regex(item, regex_name):


def _matches_any_rerun_error(rerun_errors, excinfo):
return _try_match_error(rerun_errors, excinfo)
return _try_match_error(rerun_errors, excinfo, follow_context=True)


def _matches_any_rerun_except_error(rerun_except_errors, excinfo):
return _try_match_error(rerun_except_errors, excinfo)
return _try_match_error(rerun_except_errors, excinfo, follow_context=False)


def _iter_exception_chain(exc, *, follow_context=True):
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:
Comment thread
icemac marked this conversation as resolved.
exc = exc.__cause__
elif follow_context and 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}"
def _try_match_error(rerun_errors, excinfo, *, follow_context=True):
if not excinfo:
return False
for exc in _iter_exception_chain(excinfo.value, follow_context=follow_context):
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):
Comment thread
icemac marked this conversation as resolved.
return True
elif re.search(rerun_error, err):
return True
Expand Down
116 changes: 116 additions & 0 deletions tests/test_pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,122 @@ 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_explicit_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_rerun_except_does_not_match_implicit_context(testdir):
testdir.makepyfile(
"""
def test_wrapped():
try:
assert False, "genuine failure"
except AssertionError:
raise ConnectionError("network blip")
"""
)
result = testdir.runpytest("--reruns", "2", "--rerun-except", "AssertionError")
assert_outcomes(result, passed=0, failed=1, rerun=2)


def test_only_rerun_and_rerun_except_implicit_context(testdir):
testdir.makepyfile(
"""
def test_wrapped():
try:
assert False, "genuine failure"
except AssertionError:
raise ConnectionError("network blip")
"""
)
result = testdir.runpytest(
"--reruns",
"2",
"--only-rerun",
"ConnectionError",
"--rerun-except",
"AssertionError",
)
assert_outcomes(result, passed=0, failed=1, rerun=2)


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