From 04e9197fd6138adaf953ba8fef37085b8f75e71e Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sat, 25 Jul 2009 18:09:01 +0200 Subject: [PATCH] * 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 --- CHANGELOG | 5 +++ py/test/collect.py | 14 +++---- py/test/config.py | 37 ----------------- py/test/defaultconftest.py | 2 +- py/test/dist/testing/test_dsession.py | 2 +- py/test/plugin/pytest_default.py | 3 -- py/test/plugin/pytest_doctest.py | 6 +-- py/test/plugin/pytest_iocapture.py | 60 ++++++++++++++++++++++++++- py/test/plugin/pytest_runner.py | 49 ++++++++++------------ py/test/plugin/test_pytest_runner.py | 49 +++++++++------------- py/test/pycollect.py | 5 ++- py/test/testing/acceptance_test.py | 22 ---------- py/test/testing/test_config.py | 55 ------------------------ 13 files changed, 118 insertions(+), 191 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1ef53b75e..1ed977434 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 ===================================== diff --git a/py/test/collect.py b/py/test/collect.py index a56ec496c..c7bbdb556 100644 --- a/py/test/collect.py +++ b/py/test/collect.py @@ -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(). """ diff --git a/py/test/config.py b/py/test/config.py index 4c468ae19..2ef394ba9 100644 --- a/py/test/config.py +++ b/py/test/config.py @@ -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 diff --git a/py/test/defaultconftest.py b/py/test/defaultconftest.py index 6ee22fb4e..91622956b 100644 --- a/py/test/defaultconftest.py +++ b/py/test/defaultconftest.py @@ -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() diff --git a/py/test/dist/testing/test_dsession.py b/py/test/dist/testing/test_dsession.py index 02679a75c..ec33526cc 100644 --- a/py/test/dist/testing/test_dsession.py +++ b/py/test/dist/testing/test_dsession.py @@ -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 diff --git a/py/test/plugin/pytest_default.py b/py/test/plugin/pytest_default.py index 7b9959339..83c53306d 100644 --- a/py/test/plugin/pytest_default.py +++ b/py/test/plugin/pytest_default.py @@ -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', diff --git a/py/test/plugin/pytest_doctest.py b/py/test/plugin/pytest_doctest.py index 79400b6df..4497bf874 100644 --- a/py/test/plugin/pytest_doctest.py +++ b/py/test/plugin/pytest_doctest.py @@ -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): diff --git a/py/test/plugin/pytest_iocapture.py b/py/test/plugin/pytest_iocapture.py index 5fa9bb4ea..46c031090 100644 --- a/py/test/plugin/pytest_iocapture.py +++ b/py/test/plugin/pytest_iocapture.py @@ -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 diff --git a/py/test/plugin/pytest_runner.py b/py/test/plugin/pytest_runner.py index eedb1a2fc..c43fd14f4 100644 --- a/py/test/plugin/pytest_runner.py +++ b/py/test/plugin/pytest_runner.py @@ -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) diff --git a/py/test/plugin/test_pytest_runner.py b/py/test/plugin/test_pytest_runner.py index e0e7c2348..4b279d56b 100644 --- a/py/test/plugin/test_pytest_runner.py +++ b/py/test/plugin/test_pytest_runner.py @@ -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 - - diff --git a/py/test/pycollect.py b/py/test/pycollect.py index 76394a588..25072c373 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -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" diff --git a/py/test/testing/acceptance_test.py b/py/test/testing/acceptance_test.py index 03075d75a..c78eebed7 100644 --- a/py/test/testing/acceptance_test.py +++ b/py/test/testing/acceptance_test.py @@ -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(""" diff --git a/py/test/testing/test_config.py b/py/test/testing/test_config.py index 221508d43..8299aa6e3 100644 --- a/py/test/testing/test_config.py +++ b/py/test/testing/test_config.py @@ -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