Skip to content

pdbpp: fix capturing with recursive debugging #4347

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 12, 2019
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
1 change: 1 addition & 0 deletions changelog/4347.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix output capturing when using pdb++ with recursive debugging.
35 changes: 22 additions & 13 deletions src/_pytest/debugging.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class pytestPDB(object):
_config = None
_pdb_cls = pdb.Pdb
_saved = []
_recursive_debug = 0

@classmethod
def _init_pdb(cls, *args, **kwargs):
Expand All @@ -87,29 +88,37 @@ def _init_pdb(cls, *args, **kwargs):
capman.suspend_global_capture(in_=True)
tw = _pytest.config.create_terminal_writer(cls._config)
tw.line()
# Handle header similar to pdb.set_trace in py37+.
header = kwargs.pop("header", None)
if header is not None:
tw.sep(">", header)
elif capman and capman.is_globally_capturing():
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
else:
tw.sep(">", "PDB set_trace")
if cls._recursive_debug == 0:
# Handle header similar to pdb.set_trace in py37+.
header = kwargs.pop("header", None)
if header is not None:
tw.sep(">", header)
elif capman and capman.is_globally_capturing():
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
else:
tw.sep(">", "PDB set_trace")

class _PdbWrapper(cls._pdb_cls, object):
_pytest_capman = capman
_continued = False

def do_debug(self, arg):
cls._recursive_debug += 1
ret = super(_PdbWrapper, self).do_debug(arg)
cls._recursive_debug -= 1
return ret

def do_continue(self, arg):
ret = super(_PdbWrapper, self).do_continue(arg)
if self._pytest_capman:
tw = _pytest.config.create_terminal_writer(cls._config)
tw.line()
if self._pytest_capman.is_globally_capturing():
tw.sep(">", "PDB continue (IO-capturing resumed)")
else:
tw.sep(">", "PDB continue")
self._pytest_capman.resume_global_capture()
if cls._recursive_debug == 0:
if self._pytest_capman.is_globally_capturing():
tw.sep(">", "PDB continue (IO-capturing resumed)")
else:
tw.sep(">", "PDB continue")
self._pytest_capman.resume_global_capture()
cls._pluginmanager.hook.pytest_leave_pdb(
config=cls._config, pdb=self
)
Expand Down
70 changes: 70 additions & 0 deletions testing/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,76 @@ def test_1():
assert "1 failed" in rest
self.flush(child)

def test_pdb_interaction_continue_recursive(self, testdir):
p1 = testdir.makepyfile(
mytest="""
import pdb
import pytest
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange but I get (apparently) the same interactive session you describe here in this tests without your patch:

======================== test session starts ========================
platform win32 -- Python 3.6.6, pytest-3.10.1.dev27+gf06fe436.d20181108, py-1.6.0, pluggy-0.8.0
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase('c:\\pytest\\.hypothesis\\examples')
rootdir: c:\pytest\.tmp, inifile: pytest.ini
plugins: xdist-1.23.2, parallel-0.0.6, forked-0.2, hypothesis-3.74.2
collected 1 item

.tmp\test-recursive-pdb.py
>>>>>>>>>>>>>> PDB set_trace (IO-capturing turned off) >>>>>>>>>>>>>>
> c:\pytest\.tmp\test-recursive-pdb.py(12)test_1()
-> x = 3
(Pdb) debug foo()
ENTERING RECURSIVE DEBUGGER
> <string>(1)<module>()
((Pdb)) c
print_from_foo
LEAVING RECURSIVE DEBUGGER
(Pdb) c

>>>>>>>>>>>>>>>> PDB continue (IO-capturing resumed) >>>>>>>>>>>>>>>>
F                                   [100%]

============================= FAILURES ==============================
______________________________ test_1 _______________________________

    def test_1():
        i = 0
        print("hello17")
        pytest.set_trace()
>       x = 3
E       assert 0

.tmp\test-recursive-pdb.py:12: AssertionError
----------------------- Captured stdout call ------------------------
hello17
hello18
===================== 1 failed in 61.35 seconds =====================

Am I missing something? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for testing.
That looks like it is expected, but did not work for me.
Maybe also due to pdbpp instead of pdb.

OTOH, the inner "c" without this patch should get handled with the "do_continue" really though (and then resume capturing, causing pdb to fail then).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least the new test is failing without the patch:

        child.expect("LEAVING RECURSIVE DEBUGGER")
>       assert b"PDB continue" not in child.before
E       AssertionError: assert b'PDB continue' not in b'++)) c\r\n\r\n>>>>>>>>>>>>>>>>>>>>>>>>>>>> PDB continue (IO-capturing resumed) >>>>>>>>>>>>>>>>>>>>>>>>>>>>\r\n\x1b[...--------------------------- Captured stdout call ------------------------------------\r\nhello17\r\nprint_from_foo\r\n'
E        +  where b'++)) c\r\n\r\n>>>>>>>>>>>>>>>>>>>>>>>>>>>> PDB continue (IO-capturing resumed) >>>>>>>>>>>>>>>>>>>>>>>>>>>>\r\n\x1b[...--------------------------- Captured stdout call ------------------------------------\r\nhello17\r\nprint_from_foo\r\n' = <pexpect.pty_spawn.spawn object at 0x7fd6ecdf8400>.before

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe capturing is stacked in some way on Windows already?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally I've thought about having some new attribute to see the level/stacking of suspend/resume on the capman, but it would not really help here - more relevant for #4331 probably.

This comment was marked as resolved.


count_continue = 0

# Simulates pdbpp, which injects Pdb into do_debug, and uses
# self.__class__ in do_continue.
class CustomPdb(pdb.Pdb, object):
def do_debug(self, arg):
import sys
import types

newglobals = {
'Pdb': self.__class__, # NOTE: different with pdb.Pdb
'sys': sys,
}
if sys.version_info < (3, ):
do_debug_func = pdb.Pdb.do_debug.im_func
else:
do_debug_func = pdb.Pdb.do_debug

orig_do_debug = types.FunctionType(
do_debug_func.__code__, newglobals,
do_debug_func.__name__, do_debug_func.__defaults__,
)
return orig_do_debug(self, arg)
do_debug.__doc__ = pdb.Pdb.do_debug.__doc__

def do_continue(self, *args, **kwargs):
global count_continue
count_continue += 1
return super(CustomPdb, self).do_continue(*args, **kwargs)

def foo():
print("print_from_foo")

def test_1():
i = 0
print("hello17")
pytest.set_trace()
x = 3
print("hello18")

assert count_continue == 2, "unexpected_failure: %d != 2" % count_continue
pytest.fail("expected_failure")
"""
)
child = testdir.spawn_pytest("--pdbcls=mytest:CustomPdb %s" % str(p1))
child.expect(r"PDB set_trace \(IO-capturing turned off\)")
child.expect(r"\n\(Pdb")
child.sendline("debug foo()")
child.expect("ENTERING RECURSIVE DEBUGGER")
child.expect(r"\n\(\(Pdb")
child.sendline("c")
child.expect("LEAVING RECURSIVE DEBUGGER")
assert b"PDB continue" not in child.before
assert b"print_from_foo" in child.before
child.sendline("c")
child.expect(r"PDB continue \(IO-capturing resumed\)")
rest = child.read().decode("utf8")
assert "hello17" in rest # out is captured
assert "hello18" in rest # out is captured
assert "1 failed" in rest
assert "Failed: expected_failure" in rest
assert "AssertionError: unexpected_failure" not in rest
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @blueyed, sorry for the delay.

I've tried your branch again on Windows, here's the full session:

>>>>>>>>>>>>>>>>>>>>>>>>>>>>> PDB set_trace (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> c:\pytest\.tmp\recursive-db\mytest.py(43)test_1()
-> x = 3
(Pdb) debug foo()
ENTERING RECURSIVE DEBUGGER
> <string>(1)<module>()
((Pdb)) c
print_from_foo
LEAVING RECURSIVE DEBUGGER
(Pdb) c

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> PDB continue (IO-capturing resumed) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
F                                                                                  [100%]

============================================ FAILURES =============================================
_____________________________________________ test_1 ______________________________________________

    def test_1():
        i = 0
        print("hello17")
        pytest.set_trace()
>       x = 3
E       AssertionError: unexpected_failure: 0 != 2
E       assert 0 == 2

mytest.py:43: AssertionError
-------------------------------------- Captured stdout call ---------------------------------------
hello17
hello18
==================================== 1 failed in 4.95 seconds =====================================

Really strange how count_continue is 0, even though I've hit c twice... 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this is solved?
The test changed by now a bit, but in case it is still failing, I suggest adding print(self) to do_continue.

self.flush(child)

def test_pdb_without_capture(self, testdir):
p1 = testdir.makepyfile(
"""
Expand Down