Skip to content

Commit e20e376

Browse files
authored
Merge pull request #4347 from blueyed/pdb-recursive-capture
pdbpp: fix capturing with recursive debugging
2 parents 8052d01 + 9feb494 commit e20e376

File tree

3 files changed

+93
-13
lines changed

3 files changed

+93
-13
lines changed

changelog/4347.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix output capturing when using pdb++ with recursive debugging.

src/_pytest/debugging.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class pytestPDB(object):
7575
_config = None
7676
_pdb_cls = pdb.Pdb
7777
_saved = []
78+
_recursive_debug = 0
7879

7980
@classmethod
8081
def _init_pdb(cls, *args, **kwargs):
@@ -87,29 +88,37 @@ def _init_pdb(cls, *args, **kwargs):
8788
capman.suspend_global_capture(in_=True)
8889
tw = _pytest.config.create_terminal_writer(cls._config)
8990
tw.line()
90-
# Handle header similar to pdb.set_trace in py37+.
91-
header = kwargs.pop("header", None)
92-
if header is not None:
93-
tw.sep(">", header)
94-
elif capman and capman.is_globally_capturing():
95-
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
96-
else:
97-
tw.sep(">", "PDB set_trace")
91+
if cls._recursive_debug == 0:
92+
# Handle header similar to pdb.set_trace in py37+.
93+
header = kwargs.pop("header", None)
94+
if header is not None:
95+
tw.sep(">", header)
96+
elif capman and capman.is_globally_capturing():
97+
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
98+
else:
99+
tw.sep(">", "PDB set_trace")
98100

99101
class _PdbWrapper(cls._pdb_cls, object):
100102
_pytest_capman = capman
101103
_continued = False
102104

105+
def do_debug(self, arg):
106+
cls._recursive_debug += 1
107+
ret = super(_PdbWrapper, self).do_debug(arg)
108+
cls._recursive_debug -= 1
109+
return ret
110+
103111
def do_continue(self, arg):
104112
ret = super(_PdbWrapper, self).do_continue(arg)
105113
if self._pytest_capman:
106114
tw = _pytest.config.create_terminal_writer(cls._config)
107115
tw.line()
108-
if self._pytest_capman.is_globally_capturing():
109-
tw.sep(">", "PDB continue (IO-capturing resumed)")
110-
else:
111-
tw.sep(">", "PDB continue")
112-
self._pytest_capman.resume_global_capture()
116+
if cls._recursive_debug == 0:
117+
if self._pytest_capman.is_globally_capturing():
118+
tw.sep(">", "PDB continue (IO-capturing resumed)")
119+
else:
120+
tw.sep(">", "PDB continue")
121+
self._pytest_capman.resume_global_capture()
113122
cls._pluginmanager.hook.pytest_leave_pdb(
114123
config=cls._config, pdb=self
115124
)

testing/test_pdb.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,76 @@ def test_1():
513513
assert "1 failed" in rest
514514
self.flush(child)
515515

516+
def test_pdb_interaction_continue_recursive(self, testdir):
517+
p1 = testdir.makepyfile(
518+
mytest="""
519+
import pdb
520+
import pytest
521+
522+
count_continue = 0
523+
524+
# Simulates pdbpp, which injects Pdb into do_debug, and uses
525+
# self.__class__ in do_continue.
526+
class CustomPdb(pdb.Pdb, object):
527+
def do_debug(self, arg):
528+
import sys
529+
import types
530+
531+
newglobals = {
532+
'Pdb': self.__class__, # NOTE: different with pdb.Pdb
533+
'sys': sys,
534+
}
535+
if sys.version_info < (3, ):
536+
do_debug_func = pdb.Pdb.do_debug.im_func
537+
else:
538+
do_debug_func = pdb.Pdb.do_debug
539+
540+
orig_do_debug = types.FunctionType(
541+
do_debug_func.__code__, newglobals,
542+
do_debug_func.__name__, do_debug_func.__defaults__,
543+
)
544+
return orig_do_debug(self, arg)
545+
do_debug.__doc__ = pdb.Pdb.do_debug.__doc__
546+
547+
def do_continue(self, *args, **kwargs):
548+
global count_continue
549+
count_continue += 1
550+
return super(CustomPdb, self).do_continue(*args, **kwargs)
551+
552+
def foo():
553+
print("print_from_foo")
554+
555+
def test_1():
556+
i = 0
557+
print("hello17")
558+
pytest.set_trace()
559+
x = 3
560+
print("hello18")
561+
562+
assert count_continue == 2, "unexpected_failure: %d != 2" % count_continue
563+
pytest.fail("expected_failure")
564+
"""
565+
)
566+
child = testdir.spawn_pytest("--pdbcls=mytest:CustomPdb %s" % str(p1))
567+
child.expect(r"PDB set_trace \(IO-capturing turned off\)")
568+
child.expect(r"\n\(Pdb")
569+
child.sendline("debug foo()")
570+
child.expect("ENTERING RECURSIVE DEBUGGER")
571+
child.expect(r"\n\(\(Pdb")
572+
child.sendline("c")
573+
child.expect("LEAVING RECURSIVE DEBUGGER")
574+
assert b"PDB continue" not in child.before
575+
assert b"print_from_foo" in child.before
576+
child.sendline("c")
577+
child.expect(r"PDB continue \(IO-capturing resumed\)")
578+
rest = child.read().decode("utf8")
579+
assert "hello17" in rest # out is captured
580+
assert "hello18" in rest # out is captured
581+
assert "1 failed" in rest
582+
assert "Failed: expected_failure" in rest
583+
assert "AssertionError: unexpected_failure" not in rest
584+
self.flush(child)
585+
516586
def test_pdb_without_capture(self, testdir):
517587
p1 = testdir.makepyfile(
518588
"""

0 commit comments

Comments
 (0)