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):