Skip to content

Commit ce8678e

Browse files
committed
remove non-documented per-conftest capturing option and simplify/refactor all code accordingly. Also make capturing more robust against tests closing FD1/2 and against pdb.set_trace() calls.
1 parent 2e1f6c8 commit ce8678e

File tree

4 files changed

+98
-133
lines changed

4 files changed

+98
-133
lines changed

_pytest/capture.py

Lines changed: 49 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
def pytest_addoption(parser):
2222
group = parser.getgroup("general")
2323
group._addoption(
24-
'--capture', action="store", default=None,
24+
'--capture', action="store",
25+
default="fd" if hasattr(os, "dup") else "sys",
2526
metavar="method", choices=['fd', 'sys', 'no'],
26-
help="per-test capturing method: one of fd (default)|sys|no.")
27+
help="per-test capturing method: one of fd|sys|no.")
2728
group._addoption(
2829
'-s', action="store_const", const="no", dest="capture",
2930
help="shortcut for --capture=no.")
@@ -32,16 +33,13 @@ def pytest_addoption(parser):
3233
@pytest.mark.tryfirst
3334
def pytest_load_initial_conftests(early_config, parser, args, __multicall__):
3435
ns = parser.parse_known_args(args)
35-
method = ns.capture
36-
if not method:
37-
method = "fd"
38-
if method == "fd" and not hasattr(os, "dup"):
39-
method = "sys"
4036
pluginmanager = early_config.pluginmanager
37+
method = ns.capture
4138
if method != "no":
4239
dupped_stdout = safe_text_dupfile(sys.stdout, "wb")
4340
pluginmanager.register(dupped_stdout, "dupped_stdout")
4441
#pluginmanager.add_shutdown(dupped_stdout.close)
42+
4543
capman = CaptureManager(method)
4644
pluginmanager.register(capman, "capturemanager")
4745

@@ -55,7 +53,7 @@ def silence_logging_at_shutdown():
5553
pluginmanager.add_shutdown(silence_logging_at_shutdown)
5654

5755
# finally trigger conftest loading but while capturing (issue93)
58-
capman.resumecapture()
56+
capman.init_capturings()
5957
try:
6058
try:
6159
return __multicall__.execute()
@@ -67,11 +65,9 @@ def silence_logging_at_shutdown():
6765
raise
6866

6967

70-
7168
class CaptureManager:
72-
def __init__(self, defaultmethod=None):
73-
self._method2capture = {}
74-
self._defaultmethod = defaultmethod
69+
def __init__(self, method):
70+
self._method = method
7571

7672
def _getcapture(self, method):
7773
if method == "fd":
@@ -83,53 +79,27 @@ def _getcapture(self, method):
8379
else:
8480
raise ValueError("unknown capturing method: %r" % method)
8581

86-
def _getmethod(self, config, fspath):
87-
if config.option.capture:
88-
method = config.option.capture
89-
else:
90-
try:
91-
method = config._conftest.rget("option_capture", path=fspath)
92-
except KeyError:
93-
method = "fd"
94-
if method == "fd" and not hasattr(os, 'dup'): # e.g. jython
95-
method = "sys"
96-
return method
82+
def init_capturings(self):
83+
assert not hasattr(self, "_capturing")
84+
self._capturing = self._getcapture(self._method)
85+
self._capturing.start_capturing()
9786

9887
def reset_capturings(self):
99-
for cap in self._method2capture.values():
88+
cap = self.__dict__.pop("_capturing", None)
89+
if cap is not None:
10090
cap.pop_outerr_to_orig()
10191
cap.stop_capturing()
102-
self._method2capture.clear()
103-
104-
def resumecapture_item(self, item):
105-
method = self._getmethod(item.config, item.fspath)
106-
return self.resumecapture(method)
107-
108-
def resumecapture(self, method=None):
109-
if hasattr(self, '_capturing'):
110-
raise ValueError(
111-
"cannot resume, already capturing with %r" %
112-
(self._capturing,))
113-
if method is None:
114-
method = self._defaultmethod
115-
cap = self._method2capture.get(method)
116-
self._capturing = method
117-
if cap is None:
118-
self._method2capture[method] = cap = self._getcapture(method)
119-
cap.start_capturing()
120-
else:
121-
cap.resume_capturing()
12292

123-
def suspendcapture(self, item=None):
93+
def resumecapture(self):
94+
self._capturing.resume_capturing()
95+
96+
def suspendcapture(self, in_=False):
12497
self.deactivate_funcargs()
125-
method = self.__dict__.pop("_capturing", None)
126-
outerr = "", ""
127-
if method is not None:
128-
cap = self._method2capture.get(method)
129-
if cap is not None:
130-
outerr = cap.readouterr()
131-
cap.suspend_capturing()
132-
return outerr
98+
cap = getattr(self, "_capturing", None)
99+
if cap is not None:
100+
outerr = cap.readouterr()
101+
cap.suspend_capturing(in_=in_)
102+
return outerr
133103

134104
def activate_funcargs(self, pyfuncitem):
135105
capfuncarg = pyfuncitem.__dict__.pop("_capfuncarg", None)
@@ -142,28 +112,20 @@ def deactivate_funcargs(self):
142112
if capfuncarg is not None:
143113
capfuncarg.close()
144114

145-
@pytest.mark.hookwrapper
115+
@pytest.mark.tryfirst
146116
def pytest_make_collect_report(self, __multicall__, collector):
147-
method = self._getmethod(collector.config, collector.fspath)
148-
try:
149-
self.resumecapture(method)
150-
except ValueError:
151-
yield
152-
# recursive collect, XXX refactor capturing
153-
# to allow for more lightweight recursive capturing
117+
if not isinstance(collector, pytest.File):
154118
return
155-
yield
156-
out, err = self.suspendcapture()
157-
# XXX getting the report from the ongoing hook call is a bit
158-
# of a hack. We need to think about capturing during collection
159-
# and find out if it's really needed fine-grained (per
160-
# collector).
161-
if __multicall__.results:
162-
rep = __multicall__.results[0]
163-
if out:
164-
rep.sections.append(("Captured stdout", out))
165-
if err:
166-
rep.sections.append(("Captured stderr", err))
119+
self.resumecapture()
120+
try:
121+
rep = __multicall__.execute()
122+
finally:
123+
out, err = self.suspendcapture()
124+
if out:
125+
rep.sections.append(("Captured stdout", out))
126+
if err:
127+
rep.sections.append(("Captured stderr", err))
128+
return rep
167129

168130
@pytest.mark.hookwrapper
169131
def pytest_runtest_setup(self, item):
@@ -192,9 +154,9 @@ def pytest_internalerror(self, excinfo):
192154

193155
@contextlib.contextmanager
194156
def item_capture_wrapper(self, item, when):
195-
self.resumecapture_item(item)
157+
self.resumecapture()
196158
yield
197-
out, err = self.suspendcapture(item)
159+
out, err = self.suspendcapture()
198160
item.add_report_section(when, "out", out)
199161
item.add_report_section(when, "err", err)
200162

@@ -238,14 +200,14 @@ def _start(self):
238200
def close(self):
239201
cap = self.__dict__.pop("_capture", None)
240202
if cap is not None:
241-
cap.pop_outerr_to_orig()
203+
self._outerr = cap.pop_outerr_to_orig()
242204
cap.stop_capturing()
243205

244206
def readouterr(self):
245207
try:
246208
return self._capture.readouterr()
247209
except AttributeError:
248-
return "", ""
210+
return self._outerr
249211

250212

251213
def safe_text_dupfile(f, mode, default_encoding="UTF8"):
@@ -311,18 +273,25 @@ def pop_outerr_to_orig(self):
311273
self.out.writeorg(out)
312274
if err:
313275
self.err.writeorg(err)
276+
return out, err
314277

315-
def suspend_capturing(self):
278+
def suspend_capturing(self, in_=False):
316279
if self.out:
317280
self.out.suspend()
318281
if self.err:
319282
self.err.suspend()
283+
if in_ and self.in_:
284+
self.in_.suspend()
285+
self._in_suspended = True
320286

321287
def resume_capturing(self):
322288
if self.out:
323289
self.out.resume()
324290
if self.err:
325291
self.err.resume()
292+
if hasattr(self, "_in_suspended"):
293+
self.in_.resume()
294+
del self._in_suspended
326295

327296
def stop_capturing(self):
328297
""" stop capturing and reset capturing streams """
@@ -393,7 +362,8 @@ def snap(self):
393362
res = py.builtin._totext(res, enc, "replace")
394363
f.truncate(0)
395364
f.seek(0)
396-
return res
365+
return res
366+
return ''
397367

398368
def done(self):
399369
""" stop capturing, restore streams, return original capture file,

_pytest/pdb.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def set_trace(self):
3434
if self._pluginmanager is not None:
3535
capman = self._pluginmanager.getplugin("capturemanager")
3636
if capman:
37-
capman.reset_capturings()
37+
capman.suspendcapture(in_=True)
3838
tw = py.io.TerminalWriter()
3939
tw.line()
4040
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
@@ -45,8 +45,8 @@ class PdbInvoke:
4545
def pytest_exception_interact(self, node, call, report):
4646
capman = node.config.pluginmanager.getplugin("capturemanager")
4747
if capman:
48-
capman.reset_capturings()
49-
return _enter_pdb(node, call.excinfo, report)
48+
capman.suspendcapture(in_=True)
49+
_enter_pdb(node, call.excinfo, report)
5050

5151
def pytest_internalerror(self, excrepr, excinfo):
5252
for line in str(excrepr).split("\n"):

testing/test_capture.py

Lines changed: 26 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -53,79 +53,54 @@ def StdCapture(out=True, err=True, in_=True):
5353

5454

5555
class TestCaptureManager:
56-
def test_getmethod_default_no_fd(self, testdir, monkeypatch):
57-
config = testdir.parseconfig(testdir.tmpdir)
58-
assert config.getvalue("capture") is None
59-
capman = CaptureManager()
56+
def test_getmethod_default_no_fd(self, monkeypatch):
57+
from _pytest.capture import pytest_addoption
58+
from _pytest.config import Parser
59+
parser = Parser()
60+
pytest_addoption(parser)
61+
default = parser._groups[0].options[0].default
62+
assert default == "fd" if hasattr(os, "dup") else "sys"
63+
parser = Parser()
6064
monkeypatch.delattr(os, 'dup', raising=False)
61-
try:
62-
assert capman._getmethod(config, None) == "sys"
63-
finally:
64-
monkeypatch.undo()
65-
66-
@pytest.mark.parametrize("mode", "no fd sys".split())
67-
def test_configure_per_fspath(self, testdir, mode):
68-
config = testdir.parseconfig(testdir.tmpdir)
69-
capman = CaptureManager()
70-
hasfd = hasattr(os, 'dup')
71-
if hasfd:
72-
assert capman._getmethod(config, None) == "fd"
73-
else:
74-
assert capman._getmethod(config, None) == "sys"
75-
76-
if not hasfd and mode == 'fd':
77-
return
78-
sub = testdir.tmpdir.mkdir("dir" + mode)
79-
sub.ensure("__init__.py")
80-
sub.join("conftest.py").write('option_capture = %r' % mode)
81-
assert capman._getmethod(config, sub.join("test_hello.py")) == mode
65+
pytest_addoption(parser)
66+
assert parser._groups[0].options[0].default == "sys"
8267

8368
@needsosdup
84-
@pytest.mark.parametrize("method", ['no', 'fd', 'sys'])
69+
@pytest.mark.parametrize("method",
70+
['no', 'sys', pytest.mark.skipif('not hasattr(os, "dup")', 'fd')])
8571
def test_capturing_basic_api(self, method):
8672
capouter = StdCaptureFD()
8773
old = sys.stdout, sys.stderr, sys.stdin
8874
try:
89-
capman = CaptureManager()
90-
# call suspend without resume or start
75+
capman = CaptureManager(method)
76+
capman.init_capturings()
9177
outerr = capman.suspendcapture()
78+
assert outerr == ("", "")
9279
outerr = capman.suspendcapture()
9380
assert outerr == ("", "")
94-
capman.resumecapture(method)
9581
print ("hello")
9682
out, err = capman.suspendcapture()
9783
if method == "no":
9884
assert old == (sys.stdout, sys.stderr, sys.stdin)
9985
else:
100-
assert out == "hello\n"
101-
capman.resumecapture(method)
86+
assert not out
87+
capman.resumecapture()
88+
print ("hello")
10289
out, err = capman.suspendcapture()
103-
assert not out and not err
90+
if method != "no":
91+
assert out == "hello\n"
10492
capman.reset_capturings()
10593
finally:
10694
capouter.stop_capturing()
10795

10896
@needsosdup
109-
def test_juggle_capturings(self, testdir):
97+
def test_init_capturing(self):
11098
capouter = StdCaptureFD()
11199
try:
112-
#config = testdir.parseconfig(testdir.tmpdir)
113-
capman = CaptureManager()
114-
try:
115-
capman.resumecapture("fd")
116-
pytest.raises(ValueError, 'capman.resumecapture("fd")')
117-
pytest.raises(ValueError, 'capman.resumecapture("sys")')
118-
os.write(1, "hello\n".encode('ascii'))
119-
out, err = capman.suspendcapture()
120-
assert out == "hello\n"
121-
capman.resumecapture("sys")
122-
os.write(1, "hello\n".encode('ascii'))
123-
py.builtin.print_("world", file=sys.stderr)
124-
out, err = capman.suspendcapture()
125-
assert not out
126-
assert err == "world\n"
127-
finally:
128-
capman.reset_capturings()
100+
capman = CaptureManager("fd")
101+
capman.init_capturings()
102+
pytest.raises(AssertionError, "capman.init_capturings()")
103+
capman.reset_capturings()
129104
finally:
130105
capouter.stop_capturing()
131106

@@ -991,7 +966,7 @@ def test_close_and_capture_again(testdir):
991966
def test_close():
992967
os.close(1)
993968
def test_capture_again():
994-
os.write(1, "hello\\n")
969+
os.write(1, b"hello\\n")
995970
assert 0
996971
""")
997972
result = testdir.runpytest()

testing/test_pdb.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,26 @@ def test_1(capsys):
167167
if child.isalive():
168168
child.wait()
169169

170+
def test_set_trace_capturing_afterwards(self, testdir):
171+
p1 = testdir.makepyfile("""
172+
import pdb
173+
def test_1():
174+
pdb.set_trace()
175+
def test_2():
176+
print ("hello")
177+
assert 0
178+
""")
179+
child = testdir.spawn_pytest(str(p1))
180+
child.expect("test_1")
181+
child.send("c\n")
182+
child.expect("test_2")
183+
child.expect("Captured")
184+
child.expect("hello")
185+
child.sendeof()
186+
child.read()
187+
if child.isalive():
188+
child.wait()
189+
170190
@xfail_if_pdbpp_installed
171191
def test_pdb_interaction_doctest(self, testdir):
172192
p1 = testdir.makepyfile("""

0 commit comments

Comments
 (0)