diff --git a/README.rst b/README.rst
index f473a3c..7739eb4 100644
--- a/README.rst
+++ b/README.rst
@@ -357,7 +357,8 @@ Compatibility
`pytest-forked `_: both plugins
override ``pytest_runtest_protocol``, and whichever runs first prevents
the other from working, so tests are not re-run.
-* This plugin is *not* compatible with the core --pdb flag.
+* When the core --pdb flag is used, reruns are disabled (a warning is
+ emitted and tests are run once, as if ``--reruns 0`` was passed).
* This plugin is *not* compatible with the plugin
`flaky `_, you can only have
``pytest-rerunfailures`` or ``flaky`` but not both.
diff --git a/changes/248.feature.rst b/changes/248.feature.rst
new file mode 100644
index 0000000..8a92778
--- /dev/null
+++ b/changes/248.feature.rst
@@ -0,0 +1,3 @@
+Emit a warning and disable reruns instead of raising a ``UsageError`` when
+``--pdb`` is combined with ``--reruns``, ``--force-reruns`` or the ``flaky``
+marker.
diff --git a/src/pytest_rerunfailures.py b/src/pytest_rerunfailures.py
index 616e658..c2103fd 100644
--- a/src/pytest_rerunfailures.py
+++ b/src/pytest_rerunfailures.py
@@ -229,7 +229,7 @@ def check_options(config):
reruns = config.getoption("force_reruns") or _get_global_reruns(config)
if not config.getoption("collectonly") and reruns:
if config.option.usepdb: # a core option
- raise pytest.UsageError("--reruns incompatible with --pdb")
+ _warn_pdb_disables_reruns(config)
for name in ("only_rerun", "rerun_except"):
for pattern in getattr(config.option, name) or config.getini(name):
@@ -245,9 +245,46 @@ def _get_marker(item):
return item.get_closest_marker("flaky")
+def _warn_pdb_disables_reruns(config, item=None):
+ """Warn once that --pdb disables reruns.
+
+ Called at config time for command-line/ini reruns and per item for
+ marker-requested reruns, so emit through the channel that pytest records
+ in each case. ``-W error`` / ``filterwarnings = error`` must not escalate
+ the warning into an INTERNALERROR, so an escalated warning is re-emitted
+ under an "always" filter and recorded through pytest's warning hook.
+ """
+ if getattr(config, "_pdb_reruns_warning_issued", False):
+ return
+ config._pdb_reruns_warning_issued = True
+ warning = pytest.PytestWarning(
+ "--reruns incompatible with --pdb: reruns are disabled"
+ )
+ try:
+ if item is not None:
+ item.warn(warning)
+ else:
+ config.issue_config_time_warning(warning, stacklevel=3)
+ except Warning:
+ with warnings.catch_warnings(record=True) as records:
+ warnings.simplefilter("always", type(warning))
+ warnings.warn(warning, stacklevel=3)
+ for record in records:
+ config.hook.pytest_warning_recorded.call_historic(
+ kwargs=dict(
+ warning_message=record,
+ when="config" if item is None else "runtest",
+ nodeid="" if item is None else item.nodeid,
+ location=None,
+ )
+ )
+
+
def get_reruns_count(item):
reruns = item.session.config.getoption("force_reruns")
if reruns is not None:
+ if item.session.config.option.usepdb:
+ return 0
return reruns
rerun_marker = _get_marker(item)
@@ -265,10 +302,18 @@ def get_reruns_count(item):
if item.session.config.getoption("reruns_mode") == "append":
global_reruns = _get_global_reruns(item.session.config)
if global_reruns is not None:
- return marker_reruns + global_reruns
- return marker_reruns
+ marker_reruns += global_reruns
+ reruns = marker_reruns
+ else:
+ reruns = _get_global_reruns(item.session.config)
- return _get_global_reruns(item.session.config)
+ if reruns and item.session.config.option.usepdb:
+ # --pdb disables reruns; the global/force-reruns case already warned
+ # in check_options(), this catches marker-requested reruns.
+ _warn_pdb_disables_reruns(item.session.config, item=item)
+ return 0
+
+ return reruns
def get_reruns_delay(item):
@@ -1241,12 +1286,6 @@ 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 a4208c5..892b513 100644
--- a/tests/test_pytest_rerunfailures.py
+++ b/tests/test_pytest_rerunfailures.py
@@ -91,10 +91,33 @@ def assert_outcomes(
check_outcome_field(outcomes, "rerun", rerun)
-def test_error_when_run_with_pdb(testdir):
- testdir.makepyfile("def test_pass(): pass")
- result = testdir.runpytest("--reruns", "1", "--pdb")
- result.stderr.fnmatch_lines_random("ERROR: --reruns incompatible with --pdb")
+def make_dummy_pdb(testdir):
+ """Create a pdb class that does not enter an interactive session."""
+ testdir.makepyfile(
+ nopdb="""
+ class DummyPdb:
+ quitting = False
+
+ def __init__(self, *args, **kwargs):
+ pass
+
+ def reset(self):
+ pass
+
+ def interaction(self, *args, **kwargs):
+ pass
+ """
+ )
+
+
+def test_pdb_disables_reruns(testdir):
+ make_dummy_pdb(testdir)
+ testdir.makepyfile("def test_fail(): assert False")
+ result = testdir.runpytest("--reruns", "1", "--pdb", "--pdbcls=nopdb:DummyPdb")
+ assert_outcomes(result, passed=0, failed=1, rerun=0)
+ result.stdout.fnmatch_lines_random(
+ "*--reruns incompatible with --pdb: reruns are disabled"
+ )
def test_no_error_when_run_with_pdb_without_reruns(testdir):
@@ -109,30 +132,76 @@ def test_no_error_when_run_with_pdb_and_zero_reruns(testdir):
assert_outcomes(result)
-def test_error_when_run_with_pdb_and_reruns_ini(testdir):
- testdir.makepyfile("def test_pass(): pass")
+def test_pdb_disables_reruns_ini(testdir):
+ make_dummy_pdb(testdir)
+ testdir.makepyfile("def test_fail(): assert False")
testdir.makeini("[pytest]\nreruns = 1\n")
- result = testdir.runpytest("--pdb")
- result.stderr.fnmatch_lines_random("ERROR: --reruns incompatible with --pdb")
+ result = testdir.runpytest("--pdb", "--pdbcls=nopdb:DummyPdb")
+ assert_outcomes(result, passed=0, failed=1, rerun=0)
+ result.stdout.fnmatch_lines_random(
+ "*--reruns incompatible with --pdb: reruns are disabled"
+ )
-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_pdb_disables_force_reruns(testdir):
+ make_dummy_pdb(testdir)
+ testdir.makepyfile("def test_fail(): assert False")
+ result = testdir.runpytest(
+ "--force-reruns", "1", "--pdb", "--pdbcls=nopdb:DummyPdb"
+ )
+ assert_outcomes(result, passed=0, failed=1, rerun=0)
+ result.stdout.fnmatch_lines_random(
+ "*--reruns incompatible with --pdb: reruns are disabled"
+ )
-def test_error_when_run_with_pdb_and_flaky_marker(testdir):
+def test_pdb_disables_reruns_with_flaky_marker(testdir):
testdir.makepyfile(
"""
import pytest
@pytest.mark.flaky(reruns=1)
- def test_pass(): pass
+ def test_fail(): assert False
"""
)
- result = testdir.runpytest("--pdb")
- result.stderr.fnmatch_lines_random("*--reruns incompatible with --pdb")
+ make_dummy_pdb(testdir)
+ result = testdir.runpytest("--pdb", "--pdbcls=nopdb:DummyPdb")
+ assert_outcomes(result, passed=0, failed=1, rerun=0)
+ result.stdout.fnmatch_lines_random(
+ "*--reruns incompatible with --pdb: reruns are disabled"
+ )
+
+
+def test_pdb_disables_reruns_with_warnings_as_errors(testdir):
+ """`-W error` must not escalate the warning into an INTERNALERROR."""
+ make_dummy_pdb(testdir)
+ testdir.makepyfile("def test_fail(): assert False")
+ result = testdir.runpytest(
+ "-W", "error", "--reruns", "1", "--pdb", "--pdbcls=nopdb:DummyPdb"
+ )
+ assert_outcomes(result, passed=0, failed=1, rerun=0)
+ result.stdout.no_fnmatch_line("INTERNALERROR*")
+ result.stdout.fnmatch_lines_random(
+ "*--reruns incompatible with --pdb: reruns are disabled"
+ )
+
+
+def test_pdb_disables_reruns_marker_with_warnings_as_errors(testdir):
+ make_dummy_pdb(testdir)
+ testdir.makepyfile(
+ """
+ import pytest
+
+ @pytest.mark.flaky(reruns=1)
+ def test_fail(): assert False
+ """
+ )
+ result = testdir.runpytest("-W", "error", "--pdb", "--pdbcls=nopdb:DummyPdb")
+ assert_outcomes(result, passed=0, failed=1, rerun=0)
+ result.stdout.no_fnmatch_line("INTERNALERROR*")
+ result.stdout.fnmatch_lines_random(
+ "*--reruns incompatible with --pdb: reruns are disabled"
+ )
def test_no_rerun_on_pass(testdir):