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
20 changes: 20 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,19 @@ The same matching is applied to each exception in the ``__cause__`` /
``raise RuntimeError(...) from MemoryError(...)`` is still rerun by
``--only-rerun MemoryError``.

The default list can also live in the ``pytest.ini`` (or ``pyproject.toml``)
file, one expression per line:

.. code-block:: ini

[pytest]
only_rerun =
AssertionError
ValueError

A ``--only-rerun`` flag on the command line replaces the ini list rather than
accumulating with it.

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

Expand All @@ -139,6 +152,9 @@ 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``.

The exclusion list can also live in the ``pytest.ini`` (or
``pyproject.toml``) file as ``rerun_except``, one expression per line.

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

Expand Down Expand Up @@ -345,6 +361,10 @@ which one takes priority?
* Second priority is what's specified on the command line, like ``--reruns=2``
* Last priority is the ``pyproject.toml`` (or ``pytest.ini``) file setting, like ``reruns = 3``

The same order applies to the rerun filters: a marker's ``only_rerun`` /
``rerun_except`` beats ``--only-rerun`` / ``--rerun-except`` on the command
line, which in turn beats the ``only_rerun`` / ``rerun_except`` ini settings.

Additionally, all three can be overridden by passing ``--force-reruns`` argument
on the command line. Passing ``--reruns-mode=append`` makes the marker count and
the global ``--reruns`` / ``reruns`` ini setting additive instead of strict.
Expand Down
1 change: 1 addition & 0 deletions changes/165.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow configuring ``only_rerun`` regular expressions in ``pytest.ini`` files.
32 changes: 32 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,36 @@ Below are the ``pytest.ini`` options supported by the plugin:
[pytest]
reruns_delay = 2.5

``only_rerun``
Comment thread
icemac marked this conversation as resolved.
^^^^^^^^^^^^^^

- **Description**: Sets regular expressions for errors that should be rerun. Add one expression per line. The ``--only-rerun`` command-line flag replaces this list rather than adding to it, and a project-wide ``only_rerun`` also applies to tests carrying a plain ``@pytest.mark.flaky(reruns=...)`` marker — a marked test raising a non-matching error stops rerunning.
- **Type**: List of strings
- **Default**: Not set (all errors are eligible for reruns).
- **Example**:

.. code-block:: ini

[pytest]
only_rerun =
AssertionError
ValueError

``rerun_except``
^^^^^^^^^^^^^^^^

- **Description**: Sets regular expressions for errors that should not be rerun. Add one expression per line. The ``--rerun-except`` command-line flag replaces this list rather than adding to it.
- **Type**: List of strings
- **Default**: Not set (all errors are eligible for reruns).
- **Example**:

.. code-block:: ini

[pytest]
rerun_except =
AssertionError
ValueError

Example
-------

Expand All @@ -45,11 +75,13 @@ To configure your test environment for consistent retries and delays, add the fo
[pytest]
reruns = 3
reruns_delay = 2.0
only_rerun = AssertionError
Comment thread
icemac marked this conversation as resolved.

This setup ensures that:

- Failed tests will be retried up to 3 times.
- There will be a 2-second delay between each retry.
- Only failures matching ``AssertionError`` are retried; other errors fail without rerunning.

Overriding ``pytest.ini`` Options
---------------------------------
Expand Down
6 changes: 4 additions & 2 deletions docs/mark.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ warning and does not re-run for that failure.
^^^^^^^^^^^^^^

Re-run the test only for specific exception types or patterns.
That overrides the :option:`--only-rerun` command-line option.
That overrides the :option:`--only-rerun` command-line option and the
``only_rerun`` ini setting.

.. code-block:: python

Expand All @@ -109,7 +110,8 @@ That overrides the :option:`--only-rerun` command-line option.
^^^^^^^^^^^^^^^^

Exclude specific exception types or patterns from triggering a re-run.
That overrides the :option:`--rerun-except` command-line option.
That overrides the :option:`--rerun-except` command-line option and the
``rerun_except`` ini setting.

.. code-block:: python

Expand Down
39 changes: 33 additions & 6 deletions src/pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ def works_with_current_xdist():
"exponential backoff (delay * factor ** (attempt - 1)). defaults to 1.0, "
"i.e. a constant delay."
)
ONLY_RERUN_DESC = (
"If passed, only rerun errors matching the regex provided. "
"Pass this flag multiple times (or list one regex per line in the ini "
"file) to accumulate a list of regexes to match"
)
RERUN_EXCEPT_DESC = (
"If passed, only rerun errors other than matching the regex provided. "
"Pass this flag multiple times (or list one regex per line in the ini "
"file) to accumulate a list of regexes to match"
)


# command line options
Expand All @@ -99,9 +109,7 @@ def pytest_addoption(parser):
dest="only_rerun",
type=str,
default=None,
help="If passed, only rerun errors matching the regex provided. "
"Pass this flag multiple times to accumulate a list of regexes "
"to match",
help=ONLY_RERUN_DESC,
)
group._addoption(
"--reruns",
Expand Down Expand Up @@ -130,9 +138,7 @@ def pytest_addoption(parser):
dest="rerun_except",
type=str,
default=None,
help="If passed, only rerun errors other than matching the "
"regex provided. Pass this flag multiple times to accumulate a list "
"of regexes to match",
help=RERUN_EXCEPT_DESC,
)
group._addoption(
"--rerun-exclude-path",
Expand Down Expand Up @@ -189,6 +195,16 @@ def pytest_addoption(parser):
RERUNS_DELAY_BACKOFF_FACTOR_DESC,
type=arg_type,
)
parser.addini(
Comment thread
icemac marked this conversation as resolved.
"only_rerun",
Comment thread
icemac marked this conversation as resolved.
ONLY_RERUN_DESC,
type="linelist",
)
parser.addini(
"rerun_except",
RERUN_EXCEPT_DESC,
type="linelist",
)


def _get_global_reruns(config):
Expand All @@ -214,6 +230,15 @@ def check_options(config):
if config.option.usepdb: # a core option
raise pytest.UsageError("--reruns incompatible with --pdb")

for name in ("only_rerun", "rerun_except"):
for pattern in getattr(config.option, name) or config.getini(name):
try:
re.compile(pattern)
except re.error as error:
raise pytest.UsageError(
f"invalid regular expression for {name}: {pattern!r} ({error})"
) from error


def _get_marker(item):
return item.get_closest_marker("flaky")
Expand Down Expand Up @@ -591,6 +616,8 @@ def _get_rerun_filter_regex(item, regex_name):
regex = [regex]
else:
regex = getattr(item.session.config.option, regex_name)
if regex is None:
regex = item.session.config.getini(regex_name)

return regex

Expand Down
170 changes: 170 additions & 0 deletions tests/test_pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,176 @@ def test_only_rerun2():
)


def test_only_rerun_ini(testdir):
Comment thread
LouisDeconinck marked this conversation as resolved.
Comment thread
icemac marked this conversation as resolved.
testdir.makepyfile(
"""
def test_assertion_error():
raise AssertionError("ERR")

def test_value_error():
raise ValueError("ERR")
"""
)
testdir.makeini(
"""
[pytest]
reruns = 1
only_rerun = AssertionError
"""
)

result = testdir.runpytest()
assert_outcomes(result, passed=0, failed=2, rerun=1)


def test_only_rerun_ini_multiple(testdir):
testdir.makepyfile(
"""
def test_assertion_error():
raise AssertionError("ERR")

def test_value_error():
raise ValueError("ERR")

def test_key_error():
raise KeyError("ERR")
"""
)
testdir.makeini(
"""
[pytest]
reruns = 1
only_rerun =
AssertionError
ValueError
"""
)

result = testdir.runpytest()
assert_outcomes(result, passed=0, failed=3, rerun=2)


def test_only_rerun_ini_override(testdir):
testdir.makepyfile(
"""
def test_assertion_error():
raise AssertionError("ERR")

def test_value_error():
raise ValueError("ERR")
"""
)
testdir.makeini(
"""
[pytest]
reruns = 1
only_rerun = AssertionError
"""
)

result = testdir.runpytest("--only-rerun", "ValueError")
assert_outcomes(result, passed=0, failed=2, rerun=1)
# test_assertion_error fails outright; only test_value_error is rerun,
# so the progress line must read F-R-F in collection order.
result.stdout.fnmatch_lines(["test_only_rerun_ini_override.py FRF*"])


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

@pytest.mark.flaky(reruns=1, only_rerun="AssertionError")
def test_assertion_error():
raise AssertionError("ERR")

def test_value_error():
raise ValueError("ERR")
"""
)
testdir.makeini(
"""
[pytest]
reruns = 1
only_rerun = ValueError
"""
)

result = testdir.runpytest()
assert_outcomes(result, passed=0, failed=2, rerun=2)


def test_only_rerun_ini_with_rerun_except_flag(testdir):
testdir.makepyfile(
"""
def test_assertion_error():
raise AssertionError("ERR")

def test_value_error():
raise ValueError("ERR")

def test_os_error():
raise OSError("ERR")
"""
)
testdir.makeini(
"""
[pytest]
reruns = 1
only_rerun =
AssertionError
ValueError
"""
)

result = testdir.runpytest("--rerun-except", "ValueError")
assert_outcomes(result, passed=0, failed=3, rerun=1)


def test_rerun_except_ini(testdir):
testdir.makepyfile(
"""
def test_assertion_error():
raise AssertionError("ERR")

def test_value_error():
raise ValueError("ERR")
"""
)
testdir.makeini(
"""
[pytest]
reruns = 1
rerun_except = ValueError
"""
)

result = testdir.runpytest()
assert_outcomes(result, passed=0, failed=2, rerun=1)


@pytest.mark.parametrize("option_name", ["only_rerun", "rerun_except"])
def test_rerun_filter_ini_invalid_regex(testdir, option_name):
testdir.makepyfile(
"""
def test_foo():
raise AssertionError("ERR")
"""
)
testdir.makeini(
f"""
[pytest]
reruns = 1
{option_name} = [unclosed
"""
)

result = testdir.runpytest()
result.stderr.fnmatch_lines_random(
f"*invalid regular expression for {option_name}*"
)


@pytest.mark.parametrize(
"only_rerun,should_rerun",
[
Expand Down