From bae195d988d058cae11185df0a21487a99de6541 Mon Sep 17 00:00:00 2001 From: Michael Howitz Date: Fri, 14 Aug 2026 08:12:47 +0200 Subject: [PATCH] Fix regression making --pdb unusable without configured reruns The --pdb incompatibility check in check_options() treated an unset --reruns option (None) as configured because of `None != 0`. This was harmless while the check only ran once reruns were resolved per item, but #332 moved it to pytest_configure, so it fired unconditionally. Resolve the actually configured global reruns (CLI, ini, --force-reruns) in check_options() and restore a per-item check for marker-driven reruns. Fixes #342. --- changes/342.bugfix.rst | 1 + src/pytest_rerunfailures.py | 35 ++++++++++++++++----------- tests/test_pytest_rerunfailures.py | 38 ++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 changes/342.bugfix.rst diff --git a/changes/342.bugfix.rst b/changes/342.bugfix.rst new file mode 100644 index 0000000..62ab5b6 --- /dev/null +++ b/changes/342.bugfix.rst @@ -0,0 +1 @@ +Fix a regression in version 16.5 which made ``--pdb`` unusable even when no reruns were configured. diff --git a/src/pytest_rerunfailures.py b/src/pytest_rerunfailures.py index 69c04c1..c00ff64 100644 --- a/src/pytest_rerunfailures.py +++ b/src/pytest_rerunfailures.py @@ -168,6 +168,17 @@ def pytest_addoption(parser): ) +def _get_global_reruns(config): + reruns = config.getvalue("reruns") + if reruns is not None: + return reruns + + reruns = None + with suppress(TypeError, ValueError): + reruns = int(config.getini("reruns")) + return reruns + + # making sure the options make sense # should run before / at the beginning of pytest_cmdline_main def check_options(config): @@ -177,7 +188,8 @@ def check_options(config): and config.option.max_suite_reruns < 0 ): raise pytest.UsageError("--max-suite-reruns must be >= 0") - if not val("collectonly") and config.option.reruns != 0: + reruns = config.getvalue("force_reruns") or _get_global_reruns(config) + if not val("collectonly") and reruns: if config.option.usepdb: # a core option raise pytest.UsageError("--reruns incompatible with --pdb") @@ -186,17 +198,6 @@ def _get_marker(item): return item.get_closest_marker("flaky") -def _get_global_reruns(item): - reruns = item.session.config.getvalue("reruns") - if reruns is not None: - return reruns - - reruns = None - with suppress(TypeError, ValueError): - reruns = int(item.session.config.getini("reruns")) - return reruns - - def get_reruns_count(item): reruns = item.session.config.getvalue("force_reruns") if reruns is not None: @@ -215,12 +216,12 @@ def get_reruns_count(item): marker_reruns = 1 if item.session.config.getvalue("reruns_mode") == "append": - global_reruns = _get_global_reruns(item) + global_reruns = _get_global_reruns(item.session.config) if global_reruns is not None: return marker_reruns + global_reruns return marker_reruns - return _get_global_reruns(item) + return _get_global_reruns(item.session.config) def get_reruns_delay(item): @@ -924,6 +925,12 @@ def pytest_runtest_protocol(item, nextitem): # flaky return + if reruns and item.session.config.option.usepdb: + # the global options are already rejected in check_options(); this + # catches reruns requested via the flaky marker, which are only + # known once the item is available + raise pytest.UsageError("--reruns incompatible with --pdb") + delay = get_reruns_delay(item) delay_backoff_factor = get_reruns_delay_backoff_factor(item) parallel = not is_master(item.config) diff --git a/tests/test_pytest_rerunfailures.py b/tests/test_pytest_rerunfailures.py index 24b92f3..34586ef 100644 --- a/tests/test_pytest_rerunfailures.py +++ b/tests/test_pytest_rerunfailures.py @@ -75,6 +75,44 @@ def test_error_when_run_with_pdb(testdir): result.stderr.fnmatch_lines_random("ERROR: --reruns incompatible with --pdb") +def test_no_error_when_run_with_pdb_without_reruns(testdir): + testdir.makepyfile("def test_pass(): pass") + result = testdir.runpytest("--pdb") + assert_outcomes(result) + + +def test_no_error_when_run_with_pdb_and_zero_reruns(testdir): + testdir.makepyfile("def test_pass(): pass") + result = testdir.runpytest("--reruns", "0", "--pdb") + assert_outcomes(result) + + +def test_error_when_run_with_pdb_and_reruns_ini(testdir): + testdir.makepyfile("def test_pass(): pass") + testdir.makeini("[pytest]\nreruns = 1\n") + result = testdir.runpytest("--pdb") + result.stderr.fnmatch_lines_random("ERROR: --reruns incompatible with --pdb") + + +def test_error_when_run_with_pdb_and_force_reruns(testdir): + testdir.makepyfile("def test_pass(): pass") + result = testdir.runpytest("--force-reruns", "1", "--pdb") + result.stderr.fnmatch_lines_random("ERROR: --reruns incompatible with --pdb") + + +def test_error_when_run_with_pdb_and_flaky_marker(testdir): + testdir.makepyfile( + """ + import pytest + + @pytest.mark.flaky(reruns=1) + def test_pass(): pass + """ + ) + result = testdir.runpytest("--pdb") + result.stderr.fnmatch_lines_random("*--reruns incompatible with --pdb") + + def test_no_rerun_on_pass(testdir): testdir.makepyfile("def test_pass(): pass") result = testdir.runpytest("--reruns", "1")