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.
This commit is contained in:
parent
2e1f6c85f6
commit
ce8678e6d5
|
@ -21,9 +21,10 @@ patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'}
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
group = parser.getgroup("general")
|
group = parser.getgroup("general")
|
||||||
group._addoption(
|
group._addoption(
|
||||||
'--capture', action="store", default=None,
|
'--capture', action="store",
|
||||||
|
default="fd" if hasattr(os, "dup") else "sys",
|
||||||
metavar="method", choices=['fd', 'sys', 'no'],
|
metavar="method", choices=['fd', 'sys', 'no'],
|
||||||
help="per-test capturing method: one of fd (default)|sys|no.")
|
help="per-test capturing method: one of fd|sys|no.")
|
||||||
group._addoption(
|
group._addoption(
|
||||||
'-s', action="store_const", const="no", dest="capture",
|
'-s', action="store_const", const="no", dest="capture",
|
||||||
help="shortcut for --capture=no.")
|
help="shortcut for --capture=no.")
|
||||||
|
@ -32,16 +33,13 @@ def pytest_addoption(parser):
|
||||||
@pytest.mark.tryfirst
|
@pytest.mark.tryfirst
|
||||||
def pytest_load_initial_conftests(early_config, parser, args, __multicall__):
|
def pytest_load_initial_conftests(early_config, parser, args, __multicall__):
|
||||||
ns = parser.parse_known_args(args)
|
ns = parser.parse_known_args(args)
|
||||||
method = ns.capture
|
|
||||||
if not method:
|
|
||||||
method = "fd"
|
|
||||||
if method == "fd" and not hasattr(os, "dup"):
|
|
||||||
method = "sys"
|
|
||||||
pluginmanager = early_config.pluginmanager
|
pluginmanager = early_config.pluginmanager
|
||||||
|
method = ns.capture
|
||||||
if method != "no":
|
if method != "no":
|
||||||
dupped_stdout = safe_text_dupfile(sys.stdout, "wb")
|
dupped_stdout = safe_text_dupfile(sys.stdout, "wb")
|
||||||
pluginmanager.register(dupped_stdout, "dupped_stdout")
|
pluginmanager.register(dupped_stdout, "dupped_stdout")
|
||||||
#pluginmanager.add_shutdown(dupped_stdout.close)
|
#pluginmanager.add_shutdown(dupped_stdout.close)
|
||||||
|
|
||||||
capman = CaptureManager(method)
|
capman = CaptureManager(method)
|
||||||
pluginmanager.register(capman, "capturemanager")
|
pluginmanager.register(capman, "capturemanager")
|
||||||
|
|
||||||
|
@ -55,7 +53,7 @@ def pytest_load_initial_conftests(early_config, parser, args, __multicall__):
|
||||||
pluginmanager.add_shutdown(silence_logging_at_shutdown)
|
pluginmanager.add_shutdown(silence_logging_at_shutdown)
|
||||||
|
|
||||||
# finally trigger conftest loading but while capturing (issue93)
|
# finally trigger conftest loading but while capturing (issue93)
|
||||||
capman.resumecapture()
|
capman.init_capturings()
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
return __multicall__.execute()
|
return __multicall__.execute()
|
||||||
|
@ -67,11 +65,9 @@ def pytest_load_initial_conftests(early_config, parser, args, __multicall__):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CaptureManager:
|
class CaptureManager:
|
||||||
def __init__(self, defaultmethod=None):
|
def __init__(self, method):
|
||||||
self._method2capture = {}
|
self._method = method
|
||||||
self._defaultmethod = defaultmethod
|
|
||||||
|
|
||||||
def _getcapture(self, method):
|
def _getcapture(self, method):
|
||||||
if method == "fd":
|
if method == "fd":
|
||||||
|
@ -83,52 +79,26 @@ class CaptureManager:
|
||||||
else:
|
else:
|
||||||
raise ValueError("unknown capturing method: %r" % method)
|
raise ValueError("unknown capturing method: %r" % method)
|
||||||
|
|
||||||
def _getmethod(self, config, fspath):
|
def init_capturings(self):
|
||||||
if config.option.capture:
|
assert not hasattr(self, "_capturing")
|
||||||
method = config.option.capture
|
self._capturing = self._getcapture(self._method)
|
||||||
else:
|
self._capturing.start_capturing()
|
||||||
try:
|
|
||||||
method = config._conftest.rget("option_capture", path=fspath)
|
|
||||||
except KeyError:
|
|
||||||
method = "fd"
|
|
||||||
if method == "fd" and not hasattr(os, 'dup'): # e.g. jython
|
|
||||||
method = "sys"
|
|
||||||
return method
|
|
||||||
|
|
||||||
def reset_capturings(self):
|
def reset_capturings(self):
|
||||||
for cap in self._method2capture.values():
|
cap = self.__dict__.pop("_capturing", None)
|
||||||
|
if cap is not None:
|
||||||
cap.pop_outerr_to_orig()
|
cap.pop_outerr_to_orig()
|
||||||
cap.stop_capturing()
|
cap.stop_capturing()
|
||||||
self._method2capture.clear()
|
|
||||||
|
|
||||||
def resumecapture_item(self, item):
|
def resumecapture(self):
|
||||||
method = self._getmethod(item.config, item.fspath)
|
self._capturing.resume_capturing()
|
||||||
return self.resumecapture(method)
|
|
||||||
|
|
||||||
def resumecapture(self, method=None):
|
def suspendcapture(self, in_=False):
|
||||||
if hasattr(self, '_capturing'):
|
|
||||||
raise ValueError(
|
|
||||||
"cannot resume, already capturing with %r" %
|
|
||||||
(self._capturing,))
|
|
||||||
if method is None:
|
|
||||||
method = self._defaultmethod
|
|
||||||
cap = self._method2capture.get(method)
|
|
||||||
self._capturing = method
|
|
||||||
if cap is None:
|
|
||||||
self._method2capture[method] = cap = self._getcapture(method)
|
|
||||||
cap.start_capturing()
|
|
||||||
else:
|
|
||||||
cap.resume_capturing()
|
|
||||||
|
|
||||||
def suspendcapture(self, item=None):
|
|
||||||
self.deactivate_funcargs()
|
self.deactivate_funcargs()
|
||||||
method = self.__dict__.pop("_capturing", None)
|
cap = getattr(self, "_capturing", None)
|
||||||
outerr = "", ""
|
|
||||||
if method is not None:
|
|
||||||
cap = self._method2capture.get(method)
|
|
||||||
if cap is not None:
|
if cap is not None:
|
||||||
outerr = cap.readouterr()
|
outerr = cap.readouterr()
|
||||||
cap.suspend_capturing()
|
cap.suspend_capturing(in_=in_)
|
||||||
return outerr
|
return outerr
|
||||||
|
|
||||||
def activate_funcargs(self, pyfuncitem):
|
def activate_funcargs(self, pyfuncitem):
|
||||||
|
@ -142,28 +112,20 @@ class CaptureManager:
|
||||||
if capfuncarg is not None:
|
if capfuncarg is not None:
|
||||||
capfuncarg.close()
|
capfuncarg.close()
|
||||||
|
|
||||||
@pytest.mark.hookwrapper
|
@pytest.mark.tryfirst
|
||||||
def pytest_make_collect_report(self, __multicall__, collector):
|
def pytest_make_collect_report(self, __multicall__, collector):
|
||||||
method = self._getmethod(collector.config, collector.fspath)
|
if not isinstance(collector, pytest.File):
|
||||||
try:
|
|
||||||
self.resumecapture(method)
|
|
||||||
except ValueError:
|
|
||||||
yield
|
|
||||||
# recursive collect, XXX refactor capturing
|
|
||||||
# to allow for more lightweight recursive capturing
|
|
||||||
return
|
return
|
||||||
yield
|
self.resumecapture()
|
||||||
|
try:
|
||||||
|
rep = __multicall__.execute()
|
||||||
|
finally:
|
||||||
out, err = self.suspendcapture()
|
out, err = self.suspendcapture()
|
||||||
# XXX getting the report from the ongoing hook call is a bit
|
|
||||||
# of a hack. We need to think about capturing during collection
|
|
||||||
# and find out if it's really needed fine-grained (per
|
|
||||||
# collector).
|
|
||||||
if __multicall__.results:
|
|
||||||
rep = __multicall__.results[0]
|
|
||||||
if out:
|
if out:
|
||||||
rep.sections.append(("Captured stdout", out))
|
rep.sections.append(("Captured stdout", out))
|
||||||
if err:
|
if err:
|
||||||
rep.sections.append(("Captured stderr", err))
|
rep.sections.append(("Captured stderr", err))
|
||||||
|
return rep
|
||||||
|
|
||||||
@pytest.mark.hookwrapper
|
@pytest.mark.hookwrapper
|
||||||
def pytest_runtest_setup(self, item):
|
def pytest_runtest_setup(self, item):
|
||||||
|
@ -192,9 +154,9 @@ class CaptureManager:
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def item_capture_wrapper(self, item, when):
|
def item_capture_wrapper(self, item, when):
|
||||||
self.resumecapture_item(item)
|
self.resumecapture()
|
||||||
yield
|
yield
|
||||||
out, err = self.suspendcapture(item)
|
out, err = self.suspendcapture()
|
||||||
item.add_report_section(when, "out", out)
|
item.add_report_section(when, "out", out)
|
||||||
item.add_report_section(when, "err", err)
|
item.add_report_section(when, "err", err)
|
||||||
|
|
||||||
|
@ -238,14 +200,14 @@ class CaptureFixture:
|
||||||
def close(self):
|
def close(self):
|
||||||
cap = self.__dict__.pop("_capture", None)
|
cap = self.__dict__.pop("_capture", None)
|
||||||
if cap is not None:
|
if cap is not None:
|
||||||
cap.pop_outerr_to_orig()
|
self._outerr = cap.pop_outerr_to_orig()
|
||||||
cap.stop_capturing()
|
cap.stop_capturing()
|
||||||
|
|
||||||
def readouterr(self):
|
def readouterr(self):
|
||||||
try:
|
try:
|
||||||
return self._capture.readouterr()
|
return self._capture.readouterr()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return "", ""
|
return self._outerr
|
||||||
|
|
||||||
|
|
||||||
def safe_text_dupfile(f, mode, default_encoding="UTF8"):
|
def safe_text_dupfile(f, mode, default_encoding="UTF8"):
|
||||||
|
@ -311,18 +273,25 @@ class MultiCapture(object):
|
||||||
self.out.writeorg(out)
|
self.out.writeorg(out)
|
||||||
if err:
|
if err:
|
||||||
self.err.writeorg(err)
|
self.err.writeorg(err)
|
||||||
|
return out, err
|
||||||
|
|
||||||
def suspend_capturing(self):
|
def suspend_capturing(self, in_=False):
|
||||||
if self.out:
|
if self.out:
|
||||||
self.out.suspend()
|
self.out.suspend()
|
||||||
if self.err:
|
if self.err:
|
||||||
self.err.suspend()
|
self.err.suspend()
|
||||||
|
if in_ and self.in_:
|
||||||
|
self.in_.suspend()
|
||||||
|
self._in_suspended = True
|
||||||
|
|
||||||
def resume_capturing(self):
|
def resume_capturing(self):
|
||||||
if self.out:
|
if self.out:
|
||||||
self.out.resume()
|
self.out.resume()
|
||||||
if self.err:
|
if self.err:
|
||||||
self.err.resume()
|
self.err.resume()
|
||||||
|
if hasattr(self, "_in_suspended"):
|
||||||
|
self.in_.resume()
|
||||||
|
del self._in_suspended
|
||||||
|
|
||||||
def stop_capturing(self):
|
def stop_capturing(self):
|
||||||
""" stop capturing and reset capturing streams """
|
""" stop capturing and reset capturing streams """
|
||||||
|
@ -394,6 +363,7 @@ class FDCapture:
|
||||||
f.truncate(0)
|
f.truncate(0)
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
return res
|
return res
|
||||||
|
return ''
|
||||||
|
|
||||||
def done(self):
|
def done(self):
|
||||||
""" stop capturing, restore streams, return original capture file,
|
""" stop capturing, restore streams, return original capture file,
|
||||||
|
|
|
@ -34,7 +34,7 @@ class pytestPDB:
|
||||||
if self._pluginmanager is not None:
|
if self._pluginmanager is not None:
|
||||||
capman = self._pluginmanager.getplugin("capturemanager")
|
capman = self._pluginmanager.getplugin("capturemanager")
|
||||||
if capman:
|
if capman:
|
||||||
capman.reset_capturings()
|
capman.suspendcapture(in_=True)
|
||||||
tw = py.io.TerminalWriter()
|
tw = py.io.TerminalWriter()
|
||||||
tw.line()
|
tw.line()
|
||||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||||
|
@ -45,8 +45,8 @@ class PdbInvoke:
|
||||||
def pytest_exception_interact(self, node, call, report):
|
def pytest_exception_interact(self, node, call, report):
|
||||||
capman = node.config.pluginmanager.getplugin("capturemanager")
|
capman = node.config.pluginmanager.getplugin("capturemanager")
|
||||||
if capman:
|
if capman:
|
||||||
capman.reset_capturings()
|
capman.suspendcapture(in_=True)
|
||||||
return _enter_pdb(node, call.excinfo, report)
|
_enter_pdb(node, call.excinfo, report)
|
||||||
|
|
||||||
def pytest_internalerror(self, excrepr, excinfo):
|
def pytest_internalerror(self, excrepr, excinfo):
|
||||||
for line in str(excrepr).split("\n"):
|
for line in str(excrepr).split("\n"):
|
||||||
|
|
|
@ -53,78 +53,53 @@ def StdCapture(out=True, err=True, in_=True):
|
||||||
|
|
||||||
|
|
||||||
class TestCaptureManager:
|
class TestCaptureManager:
|
||||||
def test_getmethod_default_no_fd(self, testdir, monkeypatch):
|
def test_getmethod_default_no_fd(self, monkeypatch):
|
||||||
config = testdir.parseconfig(testdir.tmpdir)
|
from _pytest.capture import pytest_addoption
|
||||||
assert config.getvalue("capture") is None
|
from _pytest.config import Parser
|
||||||
capman = CaptureManager()
|
parser = Parser()
|
||||||
|
pytest_addoption(parser)
|
||||||
|
default = parser._groups[0].options[0].default
|
||||||
|
assert default == "fd" if hasattr(os, "dup") else "sys"
|
||||||
|
parser = Parser()
|
||||||
monkeypatch.delattr(os, 'dup', raising=False)
|
monkeypatch.delattr(os, 'dup', raising=False)
|
||||||
try:
|
pytest_addoption(parser)
|
||||||
assert capman._getmethod(config, None) == "sys"
|
assert parser._groups[0].options[0].default == "sys"
|
||||||
finally:
|
|
||||||
monkeypatch.undo()
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", "no fd sys".split())
|
|
||||||
def test_configure_per_fspath(self, testdir, mode):
|
|
||||||
config = testdir.parseconfig(testdir.tmpdir)
|
|
||||||
capman = CaptureManager()
|
|
||||||
hasfd = hasattr(os, 'dup')
|
|
||||||
if hasfd:
|
|
||||||
assert capman._getmethod(config, None) == "fd"
|
|
||||||
else:
|
|
||||||
assert capman._getmethod(config, None) == "sys"
|
|
||||||
|
|
||||||
if not hasfd and mode == 'fd':
|
|
||||||
return
|
|
||||||
sub = testdir.tmpdir.mkdir("dir" + mode)
|
|
||||||
sub.ensure("__init__.py")
|
|
||||||
sub.join("conftest.py").write('option_capture = %r' % mode)
|
|
||||||
assert capman._getmethod(config, sub.join("test_hello.py")) == mode
|
|
||||||
|
|
||||||
@needsosdup
|
@needsosdup
|
||||||
@pytest.mark.parametrize("method", ['no', 'fd', 'sys'])
|
@pytest.mark.parametrize("method",
|
||||||
|
['no', 'sys', pytest.mark.skipif('not hasattr(os, "dup")', 'fd')])
|
||||||
def test_capturing_basic_api(self, method):
|
def test_capturing_basic_api(self, method):
|
||||||
capouter = StdCaptureFD()
|
capouter = StdCaptureFD()
|
||||||
old = sys.stdout, sys.stderr, sys.stdin
|
old = sys.stdout, sys.stderr, sys.stdin
|
||||||
try:
|
try:
|
||||||
capman = CaptureManager()
|
capman = CaptureManager(method)
|
||||||
# call suspend without resume or start
|
capman.init_capturings()
|
||||||
outerr = capman.suspendcapture()
|
outerr = capman.suspendcapture()
|
||||||
|
assert outerr == ("", "")
|
||||||
outerr = capman.suspendcapture()
|
outerr = capman.suspendcapture()
|
||||||
assert outerr == ("", "")
|
assert outerr == ("", "")
|
||||||
capman.resumecapture(method)
|
|
||||||
print ("hello")
|
print ("hello")
|
||||||
out, err = capman.suspendcapture()
|
out, err = capman.suspendcapture()
|
||||||
if method == "no":
|
if method == "no":
|
||||||
assert old == (sys.stdout, sys.stderr, sys.stdin)
|
assert old == (sys.stdout, sys.stderr, sys.stdin)
|
||||||
else:
|
else:
|
||||||
assert out == "hello\n"
|
assert not out
|
||||||
capman.resumecapture(method)
|
capman.resumecapture()
|
||||||
|
print ("hello")
|
||||||
out, err = capman.suspendcapture()
|
out, err = capman.suspendcapture()
|
||||||
assert not out and not err
|
if method != "no":
|
||||||
|
assert out == "hello\n"
|
||||||
capman.reset_capturings()
|
capman.reset_capturings()
|
||||||
finally:
|
finally:
|
||||||
capouter.stop_capturing()
|
capouter.stop_capturing()
|
||||||
|
|
||||||
@needsosdup
|
@needsosdup
|
||||||
def test_juggle_capturings(self, testdir):
|
def test_init_capturing(self):
|
||||||
capouter = StdCaptureFD()
|
capouter = StdCaptureFD()
|
||||||
try:
|
try:
|
||||||
#config = testdir.parseconfig(testdir.tmpdir)
|
capman = CaptureManager("fd")
|
||||||
capman = CaptureManager()
|
capman.init_capturings()
|
||||||
try:
|
pytest.raises(AssertionError, "capman.init_capturings()")
|
||||||
capman.resumecapture("fd")
|
|
||||||
pytest.raises(ValueError, 'capman.resumecapture("fd")')
|
|
||||||
pytest.raises(ValueError, 'capman.resumecapture("sys")')
|
|
||||||
os.write(1, "hello\n".encode('ascii'))
|
|
||||||
out, err = capman.suspendcapture()
|
|
||||||
assert out == "hello\n"
|
|
||||||
capman.resumecapture("sys")
|
|
||||||
os.write(1, "hello\n".encode('ascii'))
|
|
||||||
py.builtin.print_("world", file=sys.stderr)
|
|
||||||
out, err = capman.suspendcapture()
|
|
||||||
assert not out
|
|
||||||
assert err == "world\n"
|
|
||||||
finally:
|
|
||||||
capman.reset_capturings()
|
capman.reset_capturings()
|
||||||
finally:
|
finally:
|
||||||
capouter.stop_capturing()
|
capouter.stop_capturing()
|
||||||
|
@ -991,7 +966,7 @@ def test_close_and_capture_again(testdir):
|
||||||
def test_close():
|
def test_close():
|
||||||
os.close(1)
|
os.close(1)
|
||||||
def test_capture_again():
|
def test_capture_again():
|
||||||
os.write(1, "hello\\n")
|
os.write(1, b"hello\\n")
|
||||||
assert 0
|
assert 0
|
||||||
""")
|
""")
|
||||||
result = testdir.runpytest()
|
result = testdir.runpytest()
|
||||||
|
|
|
@ -167,6 +167,26 @@ class TestPDB:
|
||||||
if child.isalive():
|
if child.isalive():
|
||||||
child.wait()
|
child.wait()
|
||||||
|
|
||||||
|
def test_set_trace_capturing_afterwards(self, testdir):
|
||||||
|
p1 = testdir.makepyfile("""
|
||||||
|
import pdb
|
||||||
|
def test_1():
|
||||||
|
pdb.set_trace()
|
||||||
|
def test_2():
|
||||||
|
print ("hello")
|
||||||
|
assert 0
|
||||||
|
""")
|
||||||
|
child = testdir.spawn_pytest(str(p1))
|
||||||
|
child.expect("test_1")
|
||||||
|
child.send("c\n")
|
||||||
|
child.expect("test_2")
|
||||||
|
child.expect("Captured")
|
||||||
|
child.expect("hello")
|
||||||
|
child.sendeof()
|
||||||
|
child.read()
|
||||||
|
if child.isalive():
|
||||||
|
child.wait()
|
||||||
|
|
||||||
@xfail_if_pdbpp_installed
|
@xfail_if_pdbpp_installed
|
||||||
def test_pdb_interaction_doctest(self, testdir):
|
def test_pdb_interaction_doctest(self, testdir):
|
||||||
p1 = testdir.makepyfile("""
|
p1 = testdir.makepyfile("""
|
||||||
|
|
Loading…
Reference in New Issue