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
7 changes: 7 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ Features
and later. The pytest-subtests plugin is *not* supported.
Fixes `#315 <https://github.com/pytest-dev/pytest-rerunfailures/issues/315>`_.

- Add ``--reruns-delay-backoff-factor`` option (and the matching
``reruns_delay_backoff_factor`` marker kwarg / ini setting) to grow the rerun
delay after each attempt for an exponential backoff. The delay before the
*n*-th re-run is ``reruns_delay * reruns_delay_backoff_factor ** (n - 1)``.
The default factor is ``1.0``, so existing behaviour (a constant delay) is
unchanged.


16.3 (2026-05-22)
-----------------
Expand Down
20 changes: 19 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ test re-run is launched:

$ pytest --reruns 5 --reruns-delay 1

To grow the delay after every attempt (exponential backoff), use the
``--reruns-delay-backoff-factor`` option. The delay before the *n*-th re-run is
``reruns_delay * reruns_delay_backoff_factor ** (n - 1)``. The default factor is
``1.0``, which keeps the delay constant. For example, the following waits 1, 2
and 4 seconds before the three re-runs:

.. code-block:: bash

$ pytest --reruns 3 --reruns-delay 1 --reruns-delay-backoff-factor 2

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

Expand Down Expand Up @@ -135,7 +145,8 @@ test to run:
Note that when teardown fails, two reports are generated for the case, one for
the test case and the other for the teardown error.

You can also specify the re-run delay time in the marker:
You can also specify the re-run delay time in the marker, as well as the
backoff factor for an exponential backoff:

.. code-block:: python

Expand All @@ -144,6 +155,13 @@ You can also specify the re-run delay time in the marker:
import random
assert random.choice([True, False])

.. code-block:: python

@pytest.mark.flaky(reruns=5, reruns_delay=2, reruns_delay_backoff_factor=2)
def test_example():
import random
assert random.choice([True, False])

You can also specify an optional ``condition`` in the re-run marker:

.. code-block:: python
Expand Down
57 changes: 53 additions & 4 deletions src/pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ def works_with_current_xdist():

RERUNS_DESC = "number of times to re-run failed tests. defaults to 0."
RERUNS_DELAY_DESC = "add time (seconds) delay between reruns."
RERUNS_DELAY_BACKOFF_FACTOR_DESC = (
"multiply the rerun delay by this factor after each attempt, for an "
"exponential backoff (delay * factor ** (attempt - 1)). defaults to 1.0, "
"i.e. a constant delay."
)


# command line options
Expand Down Expand Up @@ -91,6 +96,13 @@ def pytest_addoption(parser):
type=float,
help="add time (seconds) delay between reruns.",
)
group._addoption(
"--reruns-delay-backoff-factor",
action="store",
dest="reruns_delay_backoff_factor",
type=float,
help=RERUNS_DELAY_BACKOFF_FACTOR_DESC,
)
group._addoption(
"--rerun-except",
action="append",
Expand Down Expand Up @@ -132,6 +144,11 @@ def pytest_addoption(parser):
arg_type = "string"
parser.addini("reruns", RERUNS_DESC, type=arg_type)
parser.addini("reruns_delay", RERUNS_DELAY_DESC, type=arg_type)
parser.addini(
"reruns_delay_backoff_factor",
RERUNS_DELAY_BACKOFF_FACTOR_DESC,
type=arg_type,
)


# making sure the options make sense
Expand Down Expand Up @@ -213,6 +230,36 @@ def get_reruns_delay(item):
return delay


def get_reruns_delay_backoff_factor(item):
rerun_marker = _get_marker(item)

if rerun_marker is not None:
if "reruns_delay_backoff_factor" in rerun_marker.kwargs:
factor = rerun_marker.kwargs["reruns_delay_backoff_factor"]
elif len(rerun_marker.args) > 2:
# check for arguments
factor = rerun_marker.args[2]
else:
factor = 1.0
else:
factor = item.session.config.getvalue("reruns_delay_backoff_factor")
if factor is None:
try:
factor = float(
item.session.config.getini("reruns_delay_backoff_factor")
)
except (TypeError, ValueError):
factor = 1.0

if factor < 0:
factor = 1.0
warnings.warn(
"Rerun delay backoff factor cannot be < 0. Using default value: 1.0"
)

Comment on lines +246 to +251

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

same behaviour as in delay, not introducing new layer of complexity

return factor


def get_reruns_condition(item):
rerun_marker = _get_marker(item)

Expand Down Expand Up @@ -476,9 +523,10 @@ def pytest_configure(config):
# add flaky marker
config.addinivalue_line(
"markers",
"flaky(reruns=1, reruns_delay=0): mark test to re-run up "
"to 'reruns' times. Add a delay of 'reruns_delay' seconds "
"between re-runs.",
"flaky(reruns=1, reruns_delay=0, reruns_delay_backoff_factor=1.0): mark "
"test to re-run up to 'reruns' times. Add a delay of 'reruns_delay' "
"seconds between re-runs, multiplied by 'reruns_delay_backoff_factor' "
"after each attempt for an exponential backoff.",
)

if config.pluginmanager.hasplugin("xdist") and HAS_PYTEST_HANDLECRASHITEM:
Expand Down Expand Up @@ -718,6 +766,7 @@ def pytest_runtest_protocol(item, nextitem):
# first item if necessary
check_options(item.session.config)
delay = get_reruns_delay(item)
delay_backoff_factor = get_reruns_delay_backoff_factor(item)
parallel = not is_master(item.config)
db = item.session.config.failures_db
item.execution_count = db.get_test_failures(item.nodeid)
Expand All @@ -740,7 +789,7 @@ def pytest_runtest_protocol(item, nextitem):
else:
# failure detected and reruns not exhausted, since i < reruns
report.outcome = "rerun"
time.sleep(delay)
time.sleep(delay * delay_backoff_factor ** (item.execution_count - 1))

if not parallel or works_with_current_xdist():
# will rerun test, log intermediate result
Expand Down
91 changes: 91 additions & 0 deletions tests/test_pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,97 @@ def test_fail_two():
assert_outcomes(result, passed=0, failed=1, rerun=2)


def test_reruns_with_delay_backoff_factor(testdir):
testdir.makepyfile(
"""
def test_fail():
assert False"""
)

time.sleep = mock.MagicMock()

result = testdir.runpytest(
"--reruns",
"3",
"--reruns-delay",
"1",
"--reruns-delay-backoff-factor",
"2",
)

# delay * factor ** (attempt - 1) -> 1, 2, 4
assert time.sleep.call_args_list == [mock.call(1), mock.call(2), mock.call(4)]

assert_outcomes(result, passed=0, failed=1, rerun=3)


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

@pytest.mark.flaky(reruns=3, reruns_delay=1, reruns_delay_backoff_factor=2)
def test_fail():
assert False"""
)

time.sleep = mock.MagicMock()

result = testdir.runpytest()

assert time.sleep.call_args_list == [mock.call(1), mock.call(2), mock.call(4)]

assert_outcomes(result, passed=0, failed=1, rerun=3)


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

@pytest.mark.flaky(3, 1, 2)
def test_fail():
assert False"""
)

time.sleep = mock.MagicMock()

result = testdir.runpytest()

assert time.sleep.call_args_list == [mock.call(1), mock.call(2), mock.call(4)]

assert_outcomes(result, passed=0, failed=1, rerun=3)


def test_reruns_with_negative_delay_backoff_factor(testdir):
testdir.makepyfile(
"""
def test_fail():
assert False"""
)

time.sleep = mock.MagicMock()

result = testdir.runpytest(
"--reruns",
"2",
"--reruns-delay",
"1",
"--reruns-delay-backoff-factor",
"-1",
)

result.stdout.fnmatch_lines(
"*UserWarning: Rerun delay backoff factor cannot be < 0. "
"Using default value: 1.0"
)

# factor falls back to 1.0 -> constant delay of 1
assert time.sleep.call_args_list == [mock.call(1), mock.call(1)]

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


def test_rerun_on_setup_class_with_error_with_reruns(testdir):
"""
Case: setup_class throwing error on the first execution for parametrized test
Expand Down