Skip to content

Commit 6b5cddc

Browse files
authored
Merge pull request #4951 from blueyed/fix-pdb-capfix
pdb: handle capturing with fixtures only
2 parents d8ef86a + 46d9243 commit 6b5cddc

File tree

4 files changed

+198
-15
lines changed

4 files changed

+198
-15
lines changed

changelog/4951.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Output capturing is handled correctly when only capturing via fixtures (capsys, capfs) with ``pdb.set_trace()``.

src/_pytest/capture.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,16 @@ def _getcapture(self, method):
107107
return MultiCapture(out=False, err=False, in_=False)
108108
raise ValueError("unknown capturing method: %r" % method) # pragma: no cover
109109

110+
def is_capturing(self):
111+
if self.is_globally_capturing():
112+
return "global"
113+
capture_fixture = getattr(self._current_item, "_capture_fixture", None)
114+
if capture_fixture is not None:
115+
return (
116+
"fixture %s" % self._current_item._capture_fixture.request.fixturename
117+
)
118+
return False
119+
110120
# Global capturing control
111121

112122
def is_globally_capturing(self):
@@ -134,6 +144,15 @@ def suspend_global_capture(self, in_=False):
134144
if cap is not None:
135145
cap.suspend_capturing(in_=in_)
136146

147+
def suspend(self, in_=False):
148+
# Need to undo local capsys-et-al if it exists before disabling global capture.
149+
self.suspend_fixture(self._current_item)
150+
self.suspend_global_capture(in_)
151+
152+
def resume(self):
153+
self.resume_global_capture()
154+
self.resume_fixture(self._current_item)
155+
137156
def read_global_capture(self):
138157
return self._global_capturing.readouterr()
139158

@@ -168,14 +187,11 @@ def resume_fixture(self, item):
168187
@contextlib.contextmanager
169188
def global_and_fixture_disabled(self):
170189
"""Context manager to temporarily disable global and current fixture capturing."""
171-
# Need to undo local capsys-et-al if it exists before disabling global capture.
172-
self.suspend_fixture(self._current_item)
173-
self.suspend_global_capture(in_=False)
190+
self.suspend()
174191
try:
175192
yield
176193
finally:
177-
self.resume_global_capture()
178-
self.resume_fixture(self._current_item)
194+
self.resume()
179195

180196
@contextlib.contextmanager
181197
def item_capture(self, when, item):

src/_pytest/debugging.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ class pytestPDB(object):
101101
_saved = []
102102
_recursive_debug = 0
103103

104+
@classmethod
105+
def _is_capturing(cls, capman):
106+
if capman:
107+
return capman.is_capturing()
108+
return False
109+
104110
@classmethod
105111
def _init_pdb(cls, *args, **kwargs):
106112
""" Initialize PDB debugging, dropping any IO capturing. """
@@ -109,18 +115,27 @@ def _init_pdb(cls, *args, **kwargs):
109115
if cls._pluginmanager is not None:
110116
capman = cls._pluginmanager.getplugin("capturemanager")
111117
if capman:
112-
capman.suspend_global_capture(in_=True)
118+
capman.suspend(in_=True)
113119
tw = _pytest.config.create_terminal_writer(cls._config)
114120
tw.line()
115121
if cls._recursive_debug == 0:
116122
# Handle header similar to pdb.set_trace in py37+.
117123
header = kwargs.pop("header", None)
118124
if header is not None:
119125
tw.sep(">", header)
120-
elif capman and capman.is_globally_capturing():
121-
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
122126
else:
123-
tw.sep(">", "PDB set_trace")
127+
capturing = cls._is_capturing(capman)
128+
if capturing:
129+
if capturing == "global":
130+
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
131+
else:
132+
tw.sep(
133+
">",
134+
"PDB set_trace (IO-capturing turned off for %s)"
135+
% capturing,
136+
)
137+
else:
138+
tw.sep(">", "PDB set_trace")
124139

125140
class _PdbWrapper(cls._pdb_cls, object):
126141
_pytest_capman = capman
@@ -134,15 +149,24 @@ def do_debug(self, arg):
134149

135150
def do_continue(self, arg):
136151
ret = super(_PdbWrapper, self).do_continue(arg)
137-
if self._pytest_capman:
152+
if cls._recursive_debug == 0:
138153
tw = _pytest.config.create_terminal_writer(cls._config)
139154
tw.line()
140-
if cls._recursive_debug == 0:
141-
if self._pytest_capman.is_globally_capturing():
155+
156+
capman = self._pytest_capman
157+
capturing = pytestPDB._is_capturing(capman)
158+
if capturing:
159+
if capturing == "global":
142160
tw.sep(">", "PDB continue (IO-capturing resumed)")
143161
else:
144-
tw.sep(">", "PDB continue")
145-
self._pytest_capman.resume_global_capture()
162+
tw.sep(
163+
">",
164+
"PDB continue (IO-capturing resumed for %s)"
165+
% capturing,
166+
)
167+
capman.resume()
168+
else:
169+
tw.sep(">", "PDB continue")
146170
cls._pluginmanager.hook.pytest_leave_pdb(
147171
config=cls._config, pdb=self
148172
)

testing/test_pdb.py

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,8 @@ def test_1():
576576
child.sendline("c")
577577
child.expect("LEAVING RECURSIVE DEBUGGER")
578578
assert b"PDB continue" not in child.before
579-
assert b"print_from_foo" in child.before
579+
# No extra newline.
580+
assert child.before.endswith(b"c\r\nprint_from_foo\r\n")
580581
child.sendline("c")
581582
child.expect(r"PDB continue \(IO-capturing resumed\)")
582583
rest = child.read().decode("utf8")
@@ -603,6 +604,98 @@ def test_1():
603604
child.expect("1 passed")
604605
self.flush(child)
605606

607+
@pytest.mark.parametrize("capture_arg", ("", "-s", "-p no:capture"))
608+
def test_pdb_continue_with_recursive_debug(self, capture_arg, testdir):
609+
"""Full coverage for do_debug without capturing.
610+
611+
This is very similar to test_pdb_interaction_continue_recursive in general,
612+
but mocks out ``pdb.set_trace`` for providing more coverage.
613+
"""
614+
p1 = testdir.makepyfile(
615+
"""
616+
try:
617+
input = raw_input
618+
except NameError:
619+
pass
620+
621+
def set_trace():
622+
__import__('pdb').set_trace()
623+
624+
def test_1(monkeypatch):
625+
import _pytest.debugging
626+
627+
class pytestPDBTest(_pytest.debugging.pytestPDB):
628+
@classmethod
629+
def set_trace(cls, *args, **kwargs):
630+
# Init _PdbWrapper to handle capturing.
631+
_pdb = cls._init_pdb(*args, **kwargs)
632+
633+
# Mock out pdb.Pdb.do_continue.
634+
import pdb
635+
pdb.Pdb.do_continue = lambda self, arg: None
636+
637+
print("=== SET_TRACE ===")
638+
assert input() == "debug set_trace()"
639+
640+
# Simulate _PdbWrapper.do_debug
641+
cls._recursive_debug += 1
642+
print("ENTERING RECURSIVE DEBUGGER")
643+
print("=== SET_TRACE_2 ===")
644+
645+
assert input() == "c"
646+
_pdb.do_continue("")
647+
print("=== SET_TRACE_3 ===")
648+
649+
# Simulate _PdbWrapper.do_debug
650+
print("LEAVING RECURSIVE DEBUGGER")
651+
cls._recursive_debug -= 1
652+
653+
print("=== SET_TRACE_4 ===")
654+
assert input() == "c"
655+
_pdb.do_continue("")
656+
657+
def do_continue(self, arg):
658+
print("=== do_continue")
659+
# _PdbWrapper.do_continue("")
660+
661+
monkeypatch.setattr(_pytest.debugging, "pytestPDB", pytestPDBTest)
662+
663+
import pdb
664+
monkeypatch.setattr(pdb, "set_trace", pytestPDBTest.set_trace)
665+
666+
set_trace()
667+
"""
668+
)
669+
child = testdir.spawn_pytest("%s %s" % (p1, capture_arg))
670+
child.expect("=== SET_TRACE ===")
671+
before = child.before.decode("utf8")
672+
if not capture_arg:
673+
assert ">>> PDB set_trace (IO-capturing turned off) >>>" in before
674+
else:
675+
assert ">>> PDB set_trace >>>" in before
676+
child.sendline("debug set_trace()")
677+
child.expect("=== SET_TRACE_2 ===")
678+
before = child.before.decode("utf8")
679+
assert "\r\nENTERING RECURSIVE DEBUGGER\r\n" in before
680+
child.sendline("c")
681+
child.expect("=== SET_TRACE_3 ===")
682+
683+
# No continue message with recursive debugging.
684+
before = child.before.decode("utf8")
685+
assert ">>> PDB continue " not in before
686+
687+
child.sendline("c")
688+
child.expect("=== SET_TRACE_4 ===")
689+
before = child.before.decode("utf8")
690+
assert "\r\nLEAVING RECURSIVE DEBUGGER\r\n" in before
691+
child.sendline("c")
692+
rest = child.read().decode("utf8")
693+
if not capture_arg:
694+
assert "> PDB continue (IO-capturing resumed) >" in rest
695+
else:
696+
assert "> PDB continue >" in rest
697+
assert "1 passed in" in rest
698+
606699
def test_pdb_used_outside_test(self, testdir):
607700
p1 = testdir.makepyfile(
608701
"""
@@ -970,3 +1063,52 @@ def test_2():
9701063
rest = child.read().decode("utf8")
9711064
assert "no tests ran" in rest
9721065
TestPDB.flush(child)
1066+
1067+
1068+
@pytest.mark.parametrize("fixture", ("capfd", "capsys"))
1069+
def test_pdb_suspends_fixture_capturing(testdir, fixture):
1070+
"""Using "-s" with pytest should suspend/resume fixture capturing."""
1071+
p1 = testdir.makepyfile(
1072+
"""
1073+
def test_inner({fixture}):
1074+
import sys
1075+
1076+
print("out_inner_before")
1077+
sys.stderr.write("err_inner_before\\n")
1078+
1079+
__import__("pdb").set_trace()
1080+
1081+
print("out_inner_after")
1082+
sys.stderr.write("err_inner_after\\n")
1083+
1084+
out, err = {fixture}.readouterr()
1085+
assert out =="out_inner_before\\nout_inner_after\\n"
1086+
assert err =="err_inner_before\\nerr_inner_after\\n"
1087+
""".format(
1088+
fixture=fixture
1089+
)
1090+
)
1091+
1092+
child = testdir.spawn_pytest(str(p1) + " -s")
1093+
1094+
child.expect("Pdb")
1095+
before = child.before.decode("utf8")
1096+
assert (
1097+
"> PDB set_trace (IO-capturing turned off for fixture %s) >" % (fixture)
1098+
in before
1099+
)
1100+
1101+
# Test that capturing is really suspended.
1102+
child.sendline("p 40 + 2")
1103+
child.expect("Pdb")
1104+
assert "\r\n42\r\n" in child.before.decode("utf8")
1105+
1106+
child.sendline("c")
1107+
rest = child.read().decode("utf8")
1108+
assert "out_inner" not in rest
1109+
assert "err_inner" not in rest
1110+
1111+
TestPDB.flush(child)
1112+
assert child.exitstatus == 0
1113+
assert "= 1 passed in " in rest
1114+
assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest

0 commit comments

Comments
 (0)