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 @@ -352,6 +352,11 @@ be marked as failed.
Compatibility
-------------

* This plugin is compatible with
`pytest-timeout <https://pypi.org/project/pytest-timeout/>`_: each attempt
gets the full configured timeout. With ``timeout_func_only`` the timeout
covers the test function body only; otherwise it covers the whole
per-attempt protocol (setup, call and teardown).
* This plugin is *not* compatible with pytest-xdist's --looponfail flag.
* This plugin is *not* compatible with the core --pdb flag.
* This plugin is *not* compatible with the plugin
Expand Down
2 changes: 2 additions & 0 deletions changes/99.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Re-arm the ``pytest-timeout`` timer for each attempt so that the configured
timeout also applies to re-runs, not only to the first attempt.
35 changes: 34 additions & 1 deletion src/pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,25 @@ def _restore_suspended_finalizers(item):
suspended_finalizers.clear()


def _get_pytest_timeout_settings(item):
"""Return pytest-timeout's settings when its whole-protocol timer applies.

The ``func_only`` mode hooks ``pytest_runtest_call`` and already works
per attempt; only the default mode needs the timer re-armed here.
"""
pm = item.session.config.pluginmanager
if not (pm.hasplugin("timeout") or pm.hasplugin("pytest_timeout")):
return None
try:
from pytest_timeout import _get_item_settings
except ImportError:
return None
settings = _get_item_settings(item)
if settings.timeout and settings.timeout > 0 and settings.func_only is False:
return settings
return None


def _is_rerun_path_excluded(item):
excluded_paths = item.config.getoption("rerun_exclude_path") or []
return any(
Expand Down Expand Up @@ -1258,11 +1277,25 @@ def pytest_runtest_protocol(item, nextitem):
if item.execution_count > reruns and item.execution_count > 0:
return True

timeout_settings = _get_pytest_timeout_settings(item)
need_to_run = True
while need_to_run:
item.execution_count += 1
if timeout_settings is not None:
# pytest-timeout arms one timer around pytest_runtest_protocol, so
# without a re-arm the rerun would run unbounded after the first
# timeout fired. Re-arm the per-item timeout for each attempt.
hooks = item.config.pluginmanager.hook
hooks.pytest_timeout_cancel_timer(item=item)
hooks.pytest_timeout_set_timer(item=item, settings=timeout_settings)
item.ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
reports = runtestprotocol(item, nextitem=nextitem, log=False)
try:
reports = runtestprotocol(item, nextitem=nextitem, log=False)
finally:
if timeout_settings is not None:
# the per-attempt timer must not stay armed while reports are
# processed or reruns_delay sleeps before the next attempt
item.config.pluginmanager.hook.pytest_timeout_cancel_timer(item=item)

condition = get_reruns_condition(item, _get_reruns_condition_failures(item))
rerun_triggered = False
Expand Down
31 changes: 31 additions & 0 deletions tests/test_pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,37 @@ def test_pass():
assert_outcomes(result, passed=1, rerun=2)


def test_pytest_timeout_applies_to_each_rerun(testdir):
pytest.importorskip("pytest_timeout")
testdir.makepyfile(
"""
import time

def test_slow():
time.sleep(5)
"""
)
start = time.time()
result = testdir.runpytest("--reruns", "1", "--timeout", "1")
elapsed = time.time() - start
# Both attempts must be timed out (~2s), not just the first one.
assert elapsed < 4.5
assert_outcomes(result, passed=0, failed=1, rerun=1)


def test_pytest_timeout_does_not_fire_during_rerun_delay(testdir):
pytest.importorskip("pytest_timeout")
testdir.makepyfile(
f"""
def test_fail():
{temporary_failure(1)}"""
)
# The per-attempt timer must be cancelled before the rerun delay,
# otherwise a delay longer than the timeout kills the next attempt.
result = testdir.runpytest("--reruns", "1", "--reruns-delay", "2", "--timeout", "1")
assert_outcomes(result, passed=1, rerun=1)


def test_no_extra_test_summary_for_reruns_by_default(testdir):
testdir.makepyfile(
f"""
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ minversion = 4.0
commands = pytest tests/ {posargs}
deps =
xdist: pytest-xdist
pytest-timeout
pytest82: pytest==8.2.*
pytest83: pytest==8.3.*
pytest84: pytest==8.4.*
Expand Down
Loading