Skip to content
Open
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
1 change: 1 addition & 0 deletions changelog/13453.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When ``BdbQuit`` is raised during a pdb debugging session -- for example when a test directly raises it or when quitting pdb on Python versions older than 3.14 -- pytest now fails the current test and stops the entire test run. Previously the session would continue after ``BdbQuit``, silently skipping teardown. Note: on Python 3.14+ the pdb "quit" path calls ``os._exit(0)`` instead of raising ``BdbQuit``, which pytest cannot intercept -- by :user:`EternalRights`
4 changes: 4 additions & 0 deletions src/_pytest/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ def call_and_report(
reraise=get_reraise_exceptions(item.config),
)
report: TestReport = ihook.pytest_runtest_makereport(item=item, call=call)
# A BdbQuit (e.g. from Ctrl+D in pdb) should stop the session, not just
# fail the current test. See #13453.
if call.excinfo is not None and isinstance(call.excinfo.value, bdb.BdbQuit):
item.session.shouldstop = "Debugger quit (BdbQuit)"
if log:
ihook.pytest_runtest_logreport(report=report)
if check_interactive_exception(call, report):
Expand Down
18 changes: 13 additions & 5 deletions testing/test_debugging.py
Original file line number Diff line number Diff line change
Expand Up @@ -1376,10 +1376,15 @@ def do_quit(self, *args):

@pytest.mark.xfail(
sys.version_info >= (3, 14),
reason="C-D now quits the test session, rather than failing the test. See https://github.com/python/cpython/issues/124703",
reason=(
"Python 3.14+ pdb calls os._exit(0) on quit instead of raising "
"BdbQuit, so pytest never sees the exception and cannot set "
"shouldstop. The fix in call_and_report handles BdbQuit raised "
"directly, but os._exit cannot be intercepted."
),
)
def test_raises_bdbquit_with_eoferror(pytester: Pytester) -> None:
"""It is not guaranteed that DontReadFromInput's read is called."""
"""BdbQuit from Ctrl+D / EOFError should fail the current test and stop the session."""
p1 = pytester.makepyfile(
"""
def input_without_read(*args, **kwargs):
Expand All @@ -1389,12 +1394,15 @@ def test(monkeypatch):
import builtins
monkeypatch.setattr(builtins, "input", input_without_read)
__import__('pdb').set_trace()

def test_after_bdbquit():
# This test should not run because BdbQuit stops the session.
pass
"""
)
result = pytester.runpytest(str(p1))
result.assert_outcomes(failed=1)
result.stdout.fnmatch_lines(["E *BdbQuit", "*= 1 failed in*"])
assert result.ret == 1
result.stdout.fnmatch_lines(["E *BdbQuit", "*Interrupted: Debugger quit*"])
assert result.ret == 2


def test_pdb_wrapper_class_is_reused(pytester: Pytester) -> None:
Expand Down
3 changes: 2 additions & 1 deletion testing/test_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1225,7 +1225,8 @@ def test_should_not_run(self):
"""
)
reprec = pytester.inline_run()
reprec.assertoutcome(failed=1, passed=1)
# BdbQuit now stops the session, so test_should_not_run doesn't run.
reprec.assertoutcome(failed=1)


def test_exit_outcome(pytester: Pytester) -> None:
Expand Down