Skip to content
Merged
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
3 changes: 2 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,8 @@ Compatibility
`pytest-forked <https://pypi.org/project/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 <https://pypi.org/project/flaky/>`_, you can only have
``pytest-rerunfailures`` or ``flaky`` but not both.
Expand Down
3 changes: 3 additions & 0 deletions changes/248.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Emit a warning and disable reruns instead of raising a ``UsageError`` when
Comment thread
icemac marked this conversation as resolved.
``--pdb`` is combined with ``--reruns``, ``--force-reruns`` or the ``flaky``
marker.
59 changes: 49 additions & 10 deletions src/pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
101 changes: 85 additions & 16 deletions tests/test_pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
Loading