Skip to content

Commit e2657b2

Browse files
LouisDeconinckdevin-ai-integration[bot]icemac
authored
Disable reruns with a warning instead of erroring under --pdb (#374)
* Disable reruns with a warning instead of erroring under --pdb Combining --pdb with --reruns/--force-reruns/a flaky marker previously raised a UsageError. Someone reaching for the debugger almost never wants reruns, so warn and run each test once instead. Closes #248. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Do not let -W error escalate the --pdb reruns warning Escalated PytestWarning instances (from -W error or filterwarnings = error) are caught and re-emitted under an "always" filter so the run continues with reruns disabled instead of aborting with an INTERNALERROR, and the warning is still recorded for the warnings summary. Also bump the issue_config_time_warning stacklevel to 3 so the warning points at check_options() rather than the helper's internals. Co-Authored-By: Louis Deconinck <louis.dck@gmail.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Michael Howitz <icemac@gmx.net>
1 parent 6f843b6 commit e2657b2

4 files changed

Lines changed: 139 additions & 27 deletions

File tree

‎README.rst‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,8 @@ Compatibility
357357
`pytest-forked <https://pypi.org/project/pytest-forked/>`_: both plugins
358358
override ``pytest_runtest_protocol``, and whichever runs first prevents
359359
the other from working, so tests are not re-run.
360-
* This plugin is *not* compatible with the core --pdb flag.
360+
* When the core --pdb flag is used, reruns are disabled (a warning is
361+
emitted and tests are run once, as if ``--reruns 0`` was passed).
361362
* This plugin is *not* compatible with the plugin
362363
`flaky <https://pypi.org/project/flaky/>`_, you can only have
363364
``pytest-rerunfailures`` or ``flaky`` but not both.

‎changes/248.feature.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Emit a warning and disable reruns instead of raising a ``UsageError`` when
2+
``--pdb`` is combined with ``--reruns``, ``--force-reruns`` or the ``flaky``
3+
marker.

‎src/pytest_rerunfailures.py‎

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ def check_options(config):
229229
reruns = config.getoption("force_reruns") or _get_global_reruns(config)
230230
if not config.getoption("collectonly") and reruns:
231231
if config.option.usepdb: # a core option
232-
raise pytest.UsageError("--reruns incompatible with --pdb")
232+
_warn_pdb_disables_reruns(config)
233233

234234
for name in ("only_rerun", "rerun_except"):
235235
for pattern in getattr(config.option, name) or config.getini(name):
@@ -245,9 +245,46 @@ def _get_marker(item):
245245
return item.get_closest_marker("flaky")
246246

247247

248+
def _warn_pdb_disables_reruns(config, item=None):
249+
"""Warn once that --pdb disables reruns.
250+
251+
Called at config time for command-line/ini reruns and per item for
252+
marker-requested reruns, so emit through the channel that pytest records
253+
in each case. ``-W error`` / ``filterwarnings = error`` must not escalate
254+
the warning into an INTERNALERROR, so an escalated warning is re-emitted
255+
under an "always" filter and recorded through pytest's warning hook.
256+
"""
257+
if getattr(config, "_pdb_reruns_warning_issued", False):
258+
return
259+
config._pdb_reruns_warning_issued = True
260+
warning = pytest.PytestWarning(
261+
"--reruns incompatible with --pdb: reruns are disabled"
262+
)
263+
try:
264+
if item is not None:
265+
item.warn(warning)
266+
else:
267+
config.issue_config_time_warning(warning, stacklevel=3)
268+
except Warning:
269+
with warnings.catch_warnings(record=True) as records:
270+
warnings.simplefilter("always", type(warning))
271+
warnings.warn(warning, stacklevel=3)
272+
for record in records:
273+
config.hook.pytest_warning_recorded.call_historic(
274+
kwargs=dict(
275+
warning_message=record,
276+
when="config" if item is None else "runtest",
277+
nodeid="" if item is None else item.nodeid,
278+
location=None,
279+
)
280+
)
281+
282+
248283
def get_reruns_count(item):
249284
reruns = item.session.config.getoption("force_reruns")
250285
if reruns is not None:
286+
if item.session.config.option.usepdb:
287+
return 0
251288
return reruns
252289

253290
rerun_marker = _get_marker(item)
@@ -265,10 +302,18 @@ def get_reruns_count(item):
265302
if item.session.config.getoption("reruns_mode") == "append":
266303
global_reruns = _get_global_reruns(item.session.config)
267304
if global_reruns is not None:
268-
return marker_reruns + global_reruns
269-
return marker_reruns
305+
marker_reruns += global_reruns
306+
reruns = marker_reruns
307+
else:
308+
reruns = _get_global_reruns(item.session.config)
270309

271-
return _get_global_reruns(item.session.config)
310+
if reruns and item.session.config.option.usepdb:
311+
# --pdb disables reruns; the global/force-reruns case already warned
312+
# in check_options(), this catches marker-requested reruns.
313+
_warn_pdb_disables_reruns(item.session.config, item=item)
314+
return 0
315+
316+
return reruns
272317

273318

274319
def get_reruns_delay(item):
@@ -1241,12 +1286,6 @@ def pytest_runtest_protocol(item, nextitem):
12411286
# flaky
12421287
return
12431288

1244-
if reruns and item.session.config.option.usepdb:
1245-
# the global options are already rejected in check_options(); this
1246-
# catches reruns requested via the flaky marker, which are only
1247-
# known once the item is available
1248-
raise pytest.UsageError("--reruns incompatible with --pdb")
1249-
12501289
delay = get_reruns_delay(item)
12511290
delay_backoff_factor = get_reruns_delay_backoff_factor(item)
12521291
parallel = not is_master(item.config)

‎tests/test_pytest_rerunfailures.py‎

Lines changed: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,33 @@ def assert_outcomes(
9191
check_outcome_field(outcomes, "rerun", rerun)
9292

9393

94-
def test_error_when_run_with_pdb(testdir):
95-
testdir.makepyfile("def test_pass(): pass")
96-
result = testdir.runpytest("--reruns", "1", "--pdb")
97-
result.stderr.fnmatch_lines_random("ERROR: --reruns incompatible with --pdb")
94+
def make_dummy_pdb(testdir):
95+
"""Create a pdb class that does not enter an interactive session."""
96+
testdir.makepyfile(
97+
nopdb="""
98+
class DummyPdb:
99+
quitting = False
100+
101+
def __init__(self, *args, **kwargs):
102+
pass
103+
104+
def reset(self):
105+
pass
106+
107+
def interaction(self, *args, **kwargs):
108+
pass
109+
"""
110+
)
111+
112+
113+
def test_pdb_disables_reruns(testdir):
114+
make_dummy_pdb(testdir)
115+
testdir.makepyfile("def test_fail(): assert False")
116+
result = testdir.runpytest("--reruns", "1", "--pdb", "--pdbcls=nopdb:DummyPdb")
117+
assert_outcomes(result, passed=0, failed=1, rerun=0)
118+
result.stdout.fnmatch_lines_random(
119+
"*--reruns incompatible with --pdb: reruns are disabled"
120+
)
98121

99122

100123
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):
109132
assert_outcomes(result)
110133

111134

112-
def test_error_when_run_with_pdb_and_reruns_ini(testdir):
113-
testdir.makepyfile("def test_pass(): pass")
135+
def test_pdb_disables_reruns_ini(testdir):
136+
make_dummy_pdb(testdir)
137+
testdir.makepyfile("def test_fail(): assert False")
114138
testdir.makeini("[pytest]\nreruns = 1\n")
115-
result = testdir.runpytest("--pdb")
116-
result.stderr.fnmatch_lines_random("ERROR: --reruns incompatible with --pdb")
139+
result = testdir.runpytest("--pdb", "--pdbcls=nopdb:DummyPdb")
140+
assert_outcomes(result, passed=0, failed=1, rerun=0)
141+
result.stdout.fnmatch_lines_random(
142+
"*--reruns incompatible with --pdb: reruns are disabled"
143+
)
117144

118145

119-
def test_error_when_run_with_pdb_and_force_reruns(testdir):
120-
testdir.makepyfile("def test_pass(): pass")
121-
result = testdir.runpytest("--force-reruns", "1", "--pdb")
122-
result.stderr.fnmatch_lines_random("ERROR: --reruns incompatible with --pdb")
146+
def test_pdb_disables_force_reruns(testdir):
147+
make_dummy_pdb(testdir)
148+
testdir.makepyfile("def test_fail(): assert False")
149+
result = testdir.runpytest(
150+
"--force-reruns", "1", "--pdb", "--pdbcls=nopdb:DummyPdb"
151+
)
152+
assert_outcomes(result, passed=0, failed=1, rerun=0)
153+
result.stdout.fnmatch_lines_random(
154+
"*--reruns incompatible with --pdb: reruns are disabled"
155+
)
123156

124157

125-
def test_error_when_run_with_pdb_and_flaky_marker(testdir):
158+
def test_pdb_disables_reruns_with_flaky_marker(testdir):
126159
testdir.makepyfile(
127160
"""
128161
import pytest
129162
130163
@pytest.mark.flaky(reruns=1)
131-
def test_pass(): pass
164+
def test_fail(): assert False
132165
"""
133166
)
134-
result = testdir.runpytest("--pdb")
135-
result.stderr.fnmatch_lines_random("*--reruns incompatible with --pdb")
167+
make_dummy_pdb(testdir)
168+
result = testdir.runpytest("--pdb", "--pdbcls=nopdb:DummyPdb")
169+
assert_outcomes(result, passed=0, failed=1, rerun=0)
170+
result.stdout.fnmatch_lines_random(
171+
"*--reruns incompatible with --pdb: reruns are disabled"
172+
)
173+
174+
175+
def test_pdb_disables_reruns_with_warnings_as_errors(testdir):
176+
"""`-W error` must not escalate the warning into an INTERNALERROR."""
177+
make_dummy_pdb(testdir)
178+
testdir.makepyfile("def test_fail(): assert False")
179+
result = testdir.runpytest(
180+
"-W", "error", "--reruns", "1", "--pdb", "--pdbcls=nopdb:DummyPdb"
181+
)
182+
assert_outcomes(result, passed=0, failed=1, rerun=0)
183+
result.stdout.no_fnmatch_line("INTERNALERROR*")
184+
result.stdout.fnmatch_lines_random(
185+
"*--reruns incompatible with --pdb: reruns are disabled"
186+
)
187+
188+
189+
def test_pdb_disables_reruns_marker_with_warnings_as_errors(testdir):
190+
make_dummy_pdb(testdir)
191+
testdir.makepyfile(
192+
"""
193+
import pytest
194+
195+
@pytest.mark.flaky(reruns=1)
196+
def test_fail(): assert False
197+
"""
198+
)
199+
result = testdir.runpytest("-W", "error", "--pdb", "--pdbcls=nopdb:DummyPdb")
200+
assert_outcomes(result, passed=0, failed=1, rerun=0)
201+
result.stdout.no_fnmatch_line("INTERNALERROR*")
202+
result.stdout.fnmatch_lines_random(
203+
"*--reruns incompatible with --pdb: reruns are disabled"
204+
)
136205

137206

138207
def test_no_rerun_on_pass(testdir):

0 commit comments

Comments
 (0)