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
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``

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README.rst is also the PyPI long description (see readme in pyproject.toml), and it is not touched here: the "Re-run all failures matching certain expressions" section still presents --only-rerun as CLI-only and the "Priority" section shows the pytest.ini layer holding only reruns = 3. Leaving it stale reproduces the confusion that led to #165. Previous option additions (e.g. --reruns-delay-backoff-factor) updated README together with the docs.

Comment created by Claude

^^^^^^^^^^^^^^

- **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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example gained a third key, but the lead-in above ("for consistent retries and delays") and the "This setup ensures that:" list below still cover only two. A third bullet would help, especially since only_rerun restricts reruns rather than adding behaviour.

Comment created by Claude


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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only only_rerun gets an ini option; its documented sibling rerun_except stays CLI-only. With rerun_except = ValueError in pytest.ini the setting is silently ignored, the only signal being PytestConfigWarning: Unknown config option: rerun_except in the warnings summary. Since the docs introduce the two as a mirrored pair, adding parser.addini("rerun_except", ..., type="linelist") here would keep them in sync and also lets the fallback below drop its hardcoded name check.

Comment created by Claude

"only_rerun",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not new to this PR, but worth considering now that the pattern can live in a checked-in file: an invalid regex such as only_rerun = [unclosed produces INTERNALERROR> re.error: unterminated character set at position 0 with a traceback into re/_parser.py and nothing naming only_rerun or the config file. A CLI typo breaks the one command you typed; a committed ini typo breaks every run in CI. check_options already raises pytest.UsageError for other option problems and would be the natural place to re.compile each entry.

Comment created by Claude

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The central claim of the PR, that the CLI still overrides the ini, has no test. The file pairs test_ini_file_parameters with test_ini_file_parameters_override for the existing ini options; this new option only gets the plain half. I confirmed manually that ini only_rerun = AssertionError plus --only-rerun ValueError reruns only the ValueError test, but a refactor that swaps the lookup order (or uses getattr(...) or getini(...)) would invert precedence and the suite would stay green. Also worth a test: marker only_rerun vs ini, and ini only_rerun combined with --rerun-except, which takes the four-way branch in _should_hard_fail_on_error.

Comment created by Claude

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