* reworked per-test output capturing into the pytest_iocapture.py plugin

* removed all capturing code from config object and pytest_default plugins

* item.repr_failure(excinfo) instead of item.repr_failure(excinfo, outerr)

* added a few logging tests

--HG--
branch : 1.0.x
This commit is contained in:
holger krekel 2009-07-25 18:09:01 +02:00
parent 875ebc18ef
commit 04e9197fd6
13 changed files with 118 additions and 191 deletions

View File

@ -1,6 +1,11 @@
Changes between 1.0.0b8 and 1.0.0b9
=====================================
* reworked per-test output capturing into the pytest_iocapture.py plugin
and thus removed capturing code from config object
* item.repr_failure(excinfo) instead of item.repr_failure(excinfo, outerr)
Changes between 1.0.0b7 and 1.0.0b8
=====================================

View File

@ -247,7 +247,8 @@ class Node(object):
return col._getitembynames(names)
_fromtrail = staticmethod(_fromtrail)
def _repr_failure_py(self, excinfo, outerr):
def _repr_failure_py(self, excinfo, outerr=None):
assert outerr is None, "XXX deprecated"
excinfo.traceback = self._prunetraceback(excinfo.traceback)
# XXX temporary hack: getrepr() should not take a 'style' argument
# at all; it should record all data in all cases, and the style
@ -256,13 +257,9 @@ class Node(object):
style = "short"
else:
style = "long"
repr = excinfo.getrepr(funcargs=True,
return excinfo.getrepr(funcargs=True,
showlocals=self.config.option.showlocals,
style=style)
for secname, content in zip(["out", "err"], outerr):
if content:
repr.addsection("Captured std%s" % secname, content.rstrip())
return repr
repr_failure = _repr_failure_py
shortfailurerepr = "F"
@ -291,9 +288,10 @@ class Collector(Node):
if colitem.name == name:
return colitem
def repr_failure(self, excinfo, outerr):
def repr_failure(self, excinfo, outerr=None):
""" represent a failure. """
return self._repr_failure_py(excinfo, outerr)
assert outerr is None, "XXX deprecated"
return self._repr_failure_py(excinfo)
def _memocollect(self):
""" internal helper method to cache results of calling collect(). """

View File

@ -240,20 +240,6 @@ class Config(object):
finally:
config_per_process = py.test.config = oldconfig
def _getcapture(self, path=None):
if self.option.nocapture:
iocapture = "no"
else:
iocapture = self.getvalue("iocapture", path=path)
if iocapture == "fd":
return py.io.StdCaptureFD()
elif iocapture == "sys":
return py.io.StdCapture()
elif iocapture == "no":
return py.io.StdCapture(out=False, err=False, in_=False)
else:
raise self.Error("unknown io capturing: " + iocapture)
def getxspecs(self):
xspeclist = []
for xspec in self.getvalue("tx"):
@ -286,29 +272,6 @@ class Config(object):
if pydir is not None:
roots.append(pydir)
return roots
def guardedcall(self, func):
excinfo = result = None
capture = self._getcapture()
try:
try:
result = func()
except KeyboardInterrupt:
raise
except:
excinfo = py.code.ExceptionInfo()
finally:
stdout, stderr = capture.reset()
return CallResult(result, excinfo, stdout, stderr)
class CallResult:
def __init__(self, result, excinfo, stdout, stderr):
self.stdout = stdout
self.stderr = stderr
self.outerr = (self.stdout, self.stderr)
self.excinfo = excinfo
if excinfo is None:
self.result = result
#
# helpers

View File

@ -10,5 +10,5 @@ Generator = py.test.collect.Generator
Function = py.test.collect.Function
Instance = py.test.collect.Instance
pytest_plugins = "default runner terminal keyword xfail tmpdir execnetcleanup monkeypatch recwarn pdb unittest".split()
pytest_plugins = "default iocapture runner terminal keyword xfail tmpdir execnetcleanup monkeypatch recwarn pdb unittest".split()

View File

@ -7,7 +7,7 @@ XSpec = py.execnet.XSpec
def run(item, node, excinfo=None):
runner = item.config.pluginmanager.getplugin("runner")
rep = runner.ItemTestReport(item=item,
excinfo=excinfo, when="call", outerr=("", ""))
excinfo=excinfo, when="call")
rep.node = node
return rep

View File

@ -60,9 +60,6 @@ def pytest_addoption(parser):
action="store", dest="tbstyle", default='long',
type="choice", choices=['long', 'short', 'no'],
help="traceback verboseness (long/short/no).")
group._addoption('-s',
action="store_true", dest="nocapture", default=False,
help="disable catching of stdout/stderr during test run.")
group._addoption('-p', action="append", dest="plugin", default = [],
help=("load the specified plugin after command line parsing. "))
group._addoption('-f', '--looponfail',

View File

@ -45,7 +45,7 @@ class DoctestItem(py.test.collect.Item):
super(DoctestItem, self).__init__(name=name, parent=parent)
self.fspath = path
def repr_failure(self, excinfo, outerr):
def repr_failure(self, excinfo):
if excinfo.errisinstance(py.compat.doctest.DocTestFailure):
doctestfailure = excinfo.value
example = doctestfailure.example
@ -67,9 +67,9 @@ class DoctestItem(py.test.collect.Item):
return ReprFailDoctest(reprlocation, lines)
elif excinfo.errisinstance(py.compat.doctest.UnexpectedException):
excinfo = py.code.ExceptionInfo(excinfo.value.exc_info)
return super(DoctestItem, self).repr_failure(excinfo, outerr)
return super(DoctestItem, self).repr_failure(excinfo)
else:
return super(DoctestItem, self).repr_failure(excinfo, outerr)
return super(DoctestItem, self).repr_failure(excinfo)
class DoctestTextfile(DoctestItem):
def runtest(self):

View File

@ -28,6 +28,64 @@ will be restored.
import py
def pytest_addoption(parser):
group = parser.getgroup("general")
group._addoption('-s',
action="store_true", dest="nocapture", default=False,
help="disable catching of stdout/stderr during test run.")
def pytest_configure(config):
if not config.option.nocapture:
config.pluginmanager.register(CapturePerTest())
def determine_capturing(config, path=None):
iocapture = config.getvalue("iocapture", path=path)
if iocapture == "fd":
return py.io.StdCaptureFD()
elif iocapture == "sys":
return py.io.StdCapture()
elif iocapture == "no":
return py.io.StdCapture(out=False, err=False, in_=False)
else:
# how to raise errors here?
raise config.Error("unknown io capturing: " + iocapture)
class CapturePerTest:
def __init__(self):
self.item2capture = {}
def _setcapture(self, item):
assert item not in self.item2capture
cap = determine_capturing(item.config, path=item.fspath)
self.item2capture[item] = cap
def pytest_runtest_setup(self, item):
self._setcapture(item)
def pytest_runtest_call(self, item):
self._setcapture(item)
def pytest_runtest_teardown(self, item):
self._setcapture(item)
def pytest_keyboard_interrupt(self, excinfo):
for cap in self.item2capture.values():
cap.reset()
self.item2capture.clear()
def pytest_runtest_makereport(self, __call__, item, call):
capture = self.item2capture.pop(item)
outerr = capture.reset()
# XXX shift reporting elsewhere
rep = __call__.execute(firstresult=True)
if hasattr(rep, 'longrepr'):
repr = rep.longrepr
if hasattr(repr, 'addsection'):
for secname, content in zip(["out", "err"], outerr):
if content:
repr.addsection("Captured std%s" % secname, content.rstrip())
return rep
def pytest_funcarg__capsys(request):
"""captures writes to sys.stdout/sys.stderr and makes
them available successively via a ``capsys.reset()`` method
@ -52,7 +110,7 @@ def pytest_pyfunc_call(pyfuncitem):
if funcarg == "capsys" or funcarg == "capfd":
value.reset()
class Capture:
class Capture: # funcarg
_capture = None
def __init__(self, captureclass):
self._captureclass = captureclass

View File

@ -31,15 +31,15 @@ def pytest_sessionfinish(session, exitstatus):
mod.raiseExceptions = False
def pytest_make_collect_report(collector):
call = collector.config.guardedcall(
lambda: collector._memocollect()
)
result = None
if not call.excinfo:
result = call.result
return CollectReport(collector, result, call.excinfo, call.outerr)
return report
# XXX capturing is missing
result = excinfo = None
try:
result = collector._memocollect()
except KeyboardInterrupt:
raise
except:
excinfo = py.code.ExceptionInfo()
return CollectReport(collector, result, excinfo)
def pytest_runtest_protocol(item):
if item.config.getvalue("boxed"):
@ -66,7 +66,7 @@ def pytest_runtest_call(item):
item.runtest()
def pytest_runtest_makereport(item, call):
return ItemTestReport(item, call.excinfo, call.when, call.outerr)
return ItemTestReport(item, call.excinfo, call.when)
def pytest_runtest_teardown(item):
item.config._setupstate.teardown_exact(item)
@ -82,7 +82,6 @@ def call_and_report(item, when, log=True):
hook.pytest_runtest_logreport(rep=report)
return report
class RuntestHookCall:
excinfo = None
_prefix = "pytest_runtest_"
@ -90,16 +89,12 @@ class RuntestHookCall:
self.when = when
hookname = self._prefix + when
hook = getattr(item.config.hook, hookname)
capture = item.config._getcapture()
try:
try:
self.result = hook(item=item)
except KeyboardInterrupt:
raise
except:
self.excinfo = py.code.ExceptionInfo()
finally:
self.outerr = capture.reset()
self.result = hook(item=item)
except KeyboardInterrupt:
raise
except:
self.excinfo = py.code.ExceptionInfo()
def forked_run_report(item):
# for now, we run setup/teardown in the subprocess
@ -149,10 +144,9 @@ class BaseReport(object):
class ItemTestReport(BaseReport):
failed = passed = skipped = False
def __init__(self, item, excinfo=None, when=None, outerr=None):
def __init__(self, item, excinfo=None, when=None):
self.item = item
self.when = when
self.outerr = outerr
if item and when != "setup":
self.keywords = item.readkeywords()
else:
@ -173,14 +167,14 @@ class ItemTestReport(BaseReport):
elif excinfo.errisinstance(Skipped):
self.skipped = True
shortrepr = "s"
longrepr = self.item._repr_failure_py(excinfo, outerr)
longrepr = self.item._repr_failure_py(excinfo)
else:
self.failed = True
shortrepr = self.item.shortfailurerepr
if self.when == "call":
longrepr = self.item.repr_failure(excinfo, outerr)
longrepr = self.item.repr_failure(excinfo)
else: # exception in setup or teardown
longrepr = self.item._repr_failure_py(excinfo, outerr)
longrepr = self.item._repr_failure_py(excinfo)
shortrepr = shortrepr.lower()
self.shortrepr = shortrepr
self.longrepr = longrepr
@ -191,14 +185,13 @@ class ItemTestReport(BaseReport):
class CollectReport(BaseReport):
skipped = failed = passed = False
def __init__(self, collector, result, excinfo=None, outerr=None):
def __init__(self, collector, result, excinfo=None):
self.collector = collector
if not excinfo:
self.passed = True
self.result = result
else:
self.outerr = outerr
self.longrepr = self.collector._repr_failure_py(excinfo, outerr)
self.longrepr = self.collector._repr_failure_py(excinfo)
if excinfo.errisinstance(Skipped):
self.skipped = True
self.reason = str(excinfo.value)

View File

@ -126,7 +126,7 @@ class BaseFunctionalTests:
testdir.makepyfile(conftest="""
import py
class Function(py.test.collect.Function):
def repr_failure(self, excinfo, outerr):
def repr_failure(self, excinfo):
return "hello"
""")
reports = testdir.runitem("""
@ -143,7 +143,7 @@ class BaseFunctionalTests:
#assert rep.failed.where.path.basename == "test_func.py"
#assert rep.failed.failurerepr == "hello"
def test_failure_in_setup_function_ignores_custom_failure_repr(self, testdir):
def test_failure_in_setup_function_ignores_custom_repr(self, testdir):
testdir.makepyfile(conftest="""
import py
class Function(py.test.collect.Function):
@ -168,21 +168,6 @@ class BaseFunctionalTests:
#assert rep.outcome.where.path.basename == "test_func.py"
#assert instanace(rep.failed.failurerepr, PythonFailureRepr)
def test_capture_in_func(self, testdir):
reports = testdir.runitem("""
import sys
def setup_function(func):
print "in setup"
def test_func():
print "in function"
assert 0
def teardown_function(func):
print "in teardown"
""")
assert reports[0].outerr[0] == "in setup\n"
assert reports[1].outerr[0] == "in function\n"
assert reports[2].outerr[0] == "in teardown\n"
def test_systemexit_does_not_bail_out(self, testdir):
try:
reports = testdir.runitem("""
@ -208,6 +193,23 @@ class BaseFunctionalTests:
else:
py.test.fail("did not raise")
@py.test.mark.xfail
def test_capture_per_func(self, testdir):
reports = testdir.runitem("""
import sys
def setup_function(func):
print "in setup"
def test_func():
print "in function"
assert 0
def teardown_function(func):
print "in teardown"
""")
assert reports[0].outerr[0] == "in setup\n"
assert reports[1].outerr[0] == "in function\n"
assert reports[2].outerr[0] == "in teardown\n"
class TestExecutionNonForked(BaseFunctionalTests):
def getrunner(self):
@ -287,16 +289,3 @@ def test_functional_boxed(testdir):
"*1 failed*"
])
def test_logging_interaction(testdir):
p = testdir.makepyfile("""
def test_logging():
import logging
import StringIO
stream = StringIO.StringIO()
logging.basicConfig(stream=stream)
stream.close() # to free memory/release resources
""")
result = testdir.runpytest(p)
assert result.stderr.str().find("atexit") == -1

View File

@ -271,8 +271,9 @@ class FunctionMixin(PyobjMixin):
traceback = ntraceback.filter()
return traceback
def repr_failure(self, excinfo, outerr):
return self._repr_failure_py(excinfo, outerr)
def repr_failure(self, excinfo, outerr=None):
assert outerr is None, "XXX outerr usage is deprecated"
return self._repr_failure_py(excinfo)
shortfailurerepr = "F"

View File

@ -227,28 +227,6 @@ class TestGeneralUsage:
"*test_traceback_failure.py:4: AssertionError"
])
def test_capturing_outerr(self, testdir):
p1 = testdir.makepyfile("""
import sys
def test_capturing():
print 42
print >>sys.stderr, 23
def test_capturing_error():
print 1
print >>sys.stderr, 2
raise ValueError
""")
result = testdir.runpytest(p1)
result.stdout.fnmatch_lines([
"*test_capturing_outerr.py .F",
"====* FAILURES *====",
"____*____",
"*test_capturing_outerr.py:8: ValueError",
"*--- Captured stdout ---*",
"1",
"*--- Captured stderr ---*",
"2",
])
def test_showlocals(self, testdir):
p1 = testdir.makepyfile("""

View File

@ -212,38 +212,6 @@ class TestConfigApi_getcolitems:
for col in col.listchain():
assert col.config is config
class TestGuardedCall:
def test_guardedcall_ok(self, testdir):
config = testdir.parseconfig()
def myfunc(x):
print x
print >>py.std.sys.stderr, "hello"
return 7
call = config.guardedcall(lambda: myfunc(3))
assert call.excinfo is None
assert call.result == 7
assert call.stdout.startswith("3")
assert call.stderr.startswith("hello")
def test_guardedcall_fail(self, testdir):
config = testdir.parseconfig()
def myfunc(x):
print x
raise ValueError(17)
call = config.guardedcall(lambda: myfunc(3))
assert call.excinfo
assert call.excinfo.type == ValueError
assert not hasattr(call, 'result')
assert call.stdout.startswith("3")
assert not call.stderr
def test_guardedcall_keyboardinterrupt(self, testdir):
config = testdir.parseconfig()
def myfunc():
raise KeyboardInterrupt
py.test.raises(KeyboardInterrupt, config.guardedcall, myfunc)
class TestOptionEffects:
def test_boxed_option_default(self, testdir):
tmpdir = testdir.tmpdir.ensure("subdir", dir=1)
@ -258,29 +226,6 @@ class TestOptionEffects:
config = py.test.config._reparse([testdir.tmpdir])
assert not config.option.boxed
def test_config_iocapturing(self, testdir):
config = testdir.parseconfig(testdir.tmpdir)
assert config.getvalue("iocapture")
tmpdir = testdir.tmpdir.ensure("sub-with-conftest", dir=1)
tmpdir.join("conftest.py").write(py.code.Source("""
pytest_option_iocapture = "no"
"""))
config = py.test.config._reparse([tmpdir])
assert config.getvalue("iocapture") == "no"
capture = config._getcapture()
assert isinstance(capture, py.io.StdCapture)
assert not capture._out
assert not capture._err
assert not capture._in
assert isinstance(capture, py.io.StdCapture)
for opt, cls in (("sys", py.io.StdCapture),
("fd", py.io.StdCaptureFD),
):
config.option.iocapture = opt
capture = config._getcapture()
assert isinstance(capture, cls)
class TestConfig_gettopdir:
def test_gettopdir(self, testdir):
from py.__.test.config import gettopdir