From cde970be6924c8d39ee02cc31c6ebeae55fbb82b Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 14 Mar 2014 12:49:34 +0100 Subject: [PATCH 01/11] remove unneccessary indirections and options --- _pytest/capture.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/_pytest/capture.py b/_pytest/capture.py index 04fcbbd0d..12410026e 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -120,9 +120,6 @@ class CaptureManager: f.close() return newf - def _makestringio(self): - return TextIO() - def _getcapture(self, method): if method == "fd": return StdCaptureFD( @@ -130,10 +127,7 @@ class CaptureManager: err=self._maketempfile(), ) elif method == "sys": - return StdCapture( - out=self._makestringio(), - err=self._makestringio(), - ) + return StdCapture(out=TextIO(), err=TextIO()) elif method == "no": return NoCapture() else: From b47fdbe0a73666e65fa66df72c0ee0ce48c9dd03 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 14 Mar 2014 12:49:34 +0100 Subject: [PATCH 02/11] remove externally setting and dealing with "item.outerr" from capturing in favor of a direct interface for adding reporting sections to items. * * * refactor makereport implementation to avoid recursion with __multicall__ --- _pytest/capture.py | 28 ++++++++-------------------- _pytest/junitxml.py | 14 ++++++++------ _pytest/main.py | 9 +++++++++ _pytest/pdb.py | 4 ++-- _pytest/runner.py | 14 +++++++++++--- testing/test_capture.py | 4 ++-- testing/test_junitxml.py | 4 +++- 7 files changed, 43 insertions(+), 34 deletions(-) diff --git a/_pytest/capture.py b/_pytest/capture.py index 12410026e..f5259f2be 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -151,8 +151,6 @@ class CaptureManager: def resumecapture_item(self, item): method = self._getmethod(item.config, item.fspath) - if not hasattr(item, 'outerr'): - item.outerr = ('', '') # we accumulate outerr on the item return self.resumecapture(method) def resumecapture(self, method=None): @@ -174,16 +172,10 @@ class CaptureManager: self.deactivate_funcargs() if hasattr(self, '_capturing'): method = self._capturing + del self._capturing cap = self._method2capture.get(method) if cap is not None: - outerr = cap.suspend() - del self._capturing - if item: - outerr = (item.outerr[0] + outerr[0], - item.outerr[1] + outerr[1]) - return outerr - if hasattr(item, 'outerr'): - return item.outerr + return cap.suspend() return "", "" def activate_funcargs(self, pyfuncitem): @@ -235,18 +227,14 @@ class CaptureManager: self.suspendcapture() @pytest.mark.tryfirst - def pytest_runtest_makereport(self, __multicall__, item, call): + def pytest_runtest_makereport(self, item, call): funcarg_outerr = self.deactivate_funcargs() - rep = __multicall__.execute() - outerr = self.suspendcapture(item) + out, err = self.suspendcapture(item) if funcarg_outerr is not None: - outerr = (outerr[0] + funcarg_outerr[0], - outerr[1] + funcarg_outerr[1]) - addouterr(rep, outerr) - if not rep.passed or rep.when == "teardown": - outerr = ('', '') - item.outerr = outerr - return rep + out += funcarg_outerr[0] + err += funcarg_outerr[1] + item.add_report_section(call.when, "out", out) + item.add_report_section(call.when, "err", err) error_capsysfderror = "cannot use capsys and capfd at the same time" diff --git a/_pytest/junitxml.py b/_pytest/junitxml.py index 2d330870f..c87e7f34f 100644 --- a/_pytest/junitxml.py +++ b/_pytest/junitxml.py @@ -108,12 +108,14 @@ class LogXML(object): )) def _write_captured_output(self, report): - sec = dict(report.sections) - for name in ('out', 'err'): - content = sec.get("Captured std%s" % name) - if content: - tag = getattr(Junit, 'system-'+name) - self.append(tag(bin_xml_escape(content))) + for capname in ('out', 'err'): + allcontent = "" + for name, content in report.get_sections("Captured std%s" % + capname): + allcontent += content + if allcontent: + tag = getattr(Junit, 'system-'+capname) + self.append(tag(bin_xml_escape(allcontent))) def append(self, obj): self.tests[-1].append(obj) diff --git a/_pytest/main.py b/_pytest/main.py index f7060bf6f..9ad861fe3 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -233,6 +233,7 @@ class Node(object): # used for storing artificial fixturedefs for direct parametrization self._name2pseudofixturedef = {} + #self.extrainit() @property @@ -469,6 +470,14 @@ class Item(Node): """ nextitem = None + def __init__(self, name, parent=None, config=None, session=None): + super(Item, self).__init__(name, parent, config, session) + self._report_sections = [] + + def add_report_section(self, when, key, content): + if content: + self._report_sections.append((when, key, content)) + def reportinfo(self): return self.fspath, None, "" diff --git a/_pytest/pdb.py b/_pytest/pdb.py index 6405773f8..eef24e43e 100644 --- a/_pytest/pdb.py +++ b/_pytest/pdb.py @@ -35,8 +35,8 @@ class pytestPDB: if item is not None: capman = item.config.pluginmanager.getplugin("capturemanager") out, err = capman.suspendcapture() - if hasattr(item, 'outerr'): - item.outerr = (item.outerr[0] + out, item.outerr[1] + err) + #if hasattr(item, 'outerr'): + # item.outerr = (item.outerr[0] + out, item.outerr[1] + err) tw = py.io.TerminalWriter() tw.line() tw.sep(">", "PDB set_trace (IO-capturing turned off)") diff --git a/_pytest/runner.py b/_pytest/runner.py index 539248117..ed3c8e9dc 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -178,6 +178,11 @@ class BaseReport(object): except UnicodeEncodeError: out.line("") + def get_sections(self, prefix): + for name, content in self.sections: + if name.startswith(prefix): + yield prefix, content + passed = property(lambda x: x.outcome == "passed") failed = property(lambda x: x.outcome == "failed") skipped = property(lambda x: x.outcome == "skipped") @@ -191,6 +196,7 @@ def pytest_runtest_makereport(item, call): duration = call.stop-call.start keywords = dict([(x,1) for x in item.keywords]) excinfo = call.excinfo + sections = [] if not call.excinfo: outcome = "passed" longrepr = None @@ -209,16 +215,18 @@ def pytest_runtest_makereport(item, call): else: # exception in setup or teardown longrepr = item._repr_failure_py(excinfo, style=item.config.option.tbstyle) + for rwhen, key, content in item._report_sections: + sections.append(("Captured std%s %s" %(key, rwhen), content)) return TestReport(item.nodeid, item.location, keywords, outcome, longrepr, when, - duration=duration) + sections, duration) class TestReport(BaseReport): """ Basic test report object (also used for setup and teardown calls if they fail). """ - def __init__(self, nodeid, location, - keywords, outcome, longrepr, when, sections=(), duration=0, **extra): + def __init__(self, nodeid, location, keywords, outcome, + longrepr, when, sections=(), duration=0, **extra): #: normalized collection node id self.nodeid = nodeid diff --git a/testing/test_capture.py b/testing/test_capture.py index e3fe6cfcf..5e697fd2a 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -282,9 +282,9 @@ class TestPerTestCapturing: "====* FAILURES *====", "____*____", "*test_capturing_outerr.py:8: ValueError", - "*--- Captured stdout ---*", + "*--- Captured stdout *call*", "1", - "*--- Captured stderr ---*", + "*--- Captured stderr *call*", "2", ]) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 05573ad68..965c444bf 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -478,10 +478,12 @@ def test_unicode_issue368(testdir): path = testdir.tmpdir.join("test.xml") log = LogXML(str(path), None) ustr = py.builtin._totext("ВНИ!", "utf-8") - class report: + from _pytest.runner import BaseReport + class Report(BaseReport): longrepr = ustr sections = [] nodeid = "something" + report = Report() # hopefully this is not too brittle ... log.pytest_sessionstart() From f43cda96817f87c110218b086e1c6b814d3f4f22 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 14 Mar 2014 12:49:35 +0100 Subject: [PATCH 03/11] implement a new hook type: hook wrappers using a "yield" to distinguish between working at the front and at the end of a hook call chain. The idea is to make it easier for a plugin to "wrap" a certain hook call and use context managers, in particular allow a major cleanup of capturing. --- _pytest/core.py | 67 +++++++++++++++++++++++++-------- testing/test_core.py | 89 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 16 deletions(-) diff --git a/_pytest/core.py b/_pytest/core.py index 43d2801c0..50af3188d 100644 --- a/_pytest/core.py +++ b/_pytest/core.py @@ -240,18 +240,22 @@ class PluginManager(object): pass l = [] last = [] + wrappers = [] for plugin in plugins: try: meth = getattr(plugin, attrname) - if hasattr(meth, 'tryfirst'): - last.append(meth) - elif hasattr(meth, 'trylast'): - l.insert(0, meth) - else: - l.append(meth) except AttributeError: continue + if hasattr(meth, 'hookwrapper'): + wrappers.append(meth) + elif hasattr(meth, 'tryfirst'): + last.append(meth) + elif hasattr(meth, 'trylast'): + l.insert(0, meth) + else: + l.append(meth) l.extend(last) + l.extend(wrappers) self._listattrcache[key] = list(l) return l @@ -272,6 +276,14 @@ def importplugin(importspec): class MultiCall: """ execute a call into multiple python functions/methods. """ + + class WrongHookWrapper(Exception): + """ a hook wrapper does not behave correctly. """ + def __init__(self, func, message): + Exception.__init__(self, func, message) + self.func = func + self.message = message + def __init__(self, methods, kwargs, firstresult=False): self.methods = list(methods) self.kwargs = kwargs @@ -283,16 +295,39 @@ class MultiCall: return "" %(status, self.kwargs) def execute(self): - while self.methods: - method = self.methods.pop() - kwargs = self.getkwargs(method) - res = method(**kwargs) - if res is not None: - self.results.append(res) - if self.firstresult: - return res - if not self.firstresult: - return self.results + next_finalizers = [] + try: + while self.methods: + method = self.methods.pop() + kwargs = self.getkwargs(method) + if hasattr(method, "hookwrapper"): + it = method(**kwargs) + next = getattr(it, "next", None) + if next is None: + next = getattr(it, "__next__", None) + if next is None: + raise self.WrongHookWrapper(method, + "wrapper does not contain a yield") + res = next() + next_finalizers.append((method, next)) + else: + res = method(**kwargs) + if res is not None: + self.results.append(res) + if self.firstresult: + return res + if not self.firstresult: + return self.results + finally: + for method, fin in reversed(next_finalizers): + try: + fin() + except StopIteration: + pass + else: + raise self.WrongHookWrapper(method, + "wrapper contain more than one yield") + def getkwargs(self, method): kwargs = {} diff --git a/testing/test_core.py b/testing/test_core.py index 7ec8d6519..e04720bb5 100644 --- a/testing/test_core.py +++ b/testing/test_core.py @@ -523,6 +523,95 @@ class TestMultiCall: res = MultiCall([m1, m2], {}).execute() assert res == [1] + def test_hookwrapper(self): + l = [] + def m1(): + l.append("m1 init") + yield None + l.append("m1 finish") + m1.hookwrapper = True + + def m2(): + l.append("m2") + return 2 + res = MultiCall([m2, m1], {}).execute() + assert res == [2] + assert l == ["m1 init", "m2", "m1 finish"] + l[:] = [] + res = MultiCall([m2, m1], {}, firstresult=True).execute() + assert res == 2 + assert l == ["m1 init", "m2", "m1 finish"] + + def test_hookwrapper_order(self): + l = [] + def m1(): + l.append("m1 init") + yield 1 + l.append("m1 finish") + m1.hookwrapper = True + + def m2(): + l.append("m2 init") + yield 2 + l.append("m2 finish") + m2.hookwrapper = True + res = MultiCall([m2, m1], {}).execute() + assert res == [1, 2] + assert l == ["m1 init", "m2 init", "m2 finish", "m1 finish"] + + def test_listattr_hookwrapper_ordering(self): + class P1: + @pytest.mark.hookwrapper + def m(self): + return 17 + + class P2: + def m(self): + return 23 + + class P3: + @pytest.mark.tryfirst + def m(self): + return 19 + + pluginmanager = PluginManager() + p1 = P1() + p2 = P2() + p3 = P3() + pluginmanager.register(p1) + pluginmanager.register(p2) + pluginmanager.register(p3) + methods = pluginmanager.listattr('m') + assert methods == [p2.m, p3.m, p1.m] + ## listattr keeps a cache and deleting + ## a function attribute requires clearing it + #pluginmanager._listattrcache.clear() + #del P1.m.__dict__['tryfirst'] + + def test_hookwrapper_not_yield(self): + def m1(): + pass + m1.hookwrapper = True + + mc = MultiCall([m1], {}) + with pytest.raises(mc.WrongHookWrapper) as ex: + mc.execute() + assert ex.value.func == m1 + assert ex.value.message + + def test_hookwrapper_too_many_yield(self): + def m1(): + yield 1 + yield 2 + m1.hookwrapper = True + + mc = MultiCall([m1], {}) + with pytest.raises(mc.WrongHookWrapper) as ex: + mc.execute() + assert ex.value.func == m1 + assert ex.value.message + + class TestHookRelay: def test_happypath(self): pm = PluginManager() From 9777703e0355dd4f068147519e7a4bceff1eded8 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 14 Mar 2014 12:49:36 +0100 Subject: [PATCH 04/11] - turn on capturing before early conftest loading and make terminal writer use the original stream. - avoid resetting capture FDs/sys.stdout for each test by keeping capturing always turned on and looking at snapshotted capturing data during runtest and collection phases. --- _pytest/capture.py | 175 ++++++++++++++++++++++++---------------- _pytest/config.py | 17 ++-- _pytest/genscript.py | 1 + _pytest/helpconfig.py | 4 +- _pytest/mark.py | 2 +- _pytest/pdb.py | 22 ++--- _pytest/python.py | 2 +- _pytest/runner.py | 16 ++-- _pytest/terminal.py | 10 ++- bench/bench.py | 4 +- testing/test_capture.py | 50 ++++++------ 11 files changed, 181 insertions(+), 122 deletions(-) diff --git a/_pytest/capture.py b/_pytest/capture.py index f5259f2be..a4c1535e4 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -1,12 +1,11 @@ """ - per-test stdout/stderr capturing mechanisms, - ``capsys`` and ``capfd`` function arguments. +per-test stdout/stderr capturing mechanism. + """ -# note: py.io capture was where copied from -# pylib 1.4.20.dev2 (rev 13d9af95547e) import sys import os import tempfile +import contextlib import py import pytest @@ -58,8 +57,18 @@ def pytest_load_initial_conftests(early_config, parser, args, __multicall__): method = "fd" if method == "fd" and not hasattr(os, "dup"): method = "sys" + pluginmanager = early_config.pluginmanager + if method != "no": + try: + sys.stdout.fileno() + except Exception: + dupped_stdout = sys.stdout + else: + dupped_stdout = dupfile(sys.stdout, buffering=1) + pluginmanager.register(dupped_stdout, "dupped_stdout") + #pluginmanager.add_shutdown(dupped_stdout.close) capman = CaptureManager(method) - early_config.pluginmanager.register(capman, "capturemanager") + pluginmanager.register(capman, "capturemanager") # make sure that capturemanager is properly reset at final shutdown def teardown(): @@ -68,13 +77,13 @@ def pytest_load_initial_conftests(early_config, parser, args, __multicall__): except ValueError: pass - early_config.pluginmanager.add_shutdown(teardown) + pluginmanager.add_shutdown(teardown) # make sure logging does not raise exceptions at the end def silence_logging_at_shutdown(): if "logging" in sys.modules: sys.modules["logging"].raiseExceptions = False - early_config.pluginmanager.add_shutdown(silence_logging_at_shutdown) + pluginmanager.add_shutdown(silence_logging_at_shutdown) # finally trigger conftest loading but while capturing (issue93) capman.resumecapture() @@ -89,23 +98,20 @@ def pytest_load_initial_conftests(early_config, parser, args, __multicall__): raise -def addouterr(rep, outerr): - for secname, content in zip(["out", "err"], outerr): - if content: - rep.sections.append(("Captured std%s" % secname, content)) - - class NoCapture: - def startall(self): + def start_capturing(self): pass - def resume(self): + def stop_capturing(self): + pass + + def pop_outerr_to_orig(self): pass def reset(self): pass - def suspend(self): + def readouterr(self): return "", "" @@ -147,7 +153,9 @@ class CaptureManager: def reset_capturings(self): for cap in self._method2capture.values(): + cap.pop_outerr_to_orig() cap.reset() + self._method2capture.clear() def resumecapture_item(self, item): method = self._getmethod(item.config, item.fspath) @@ -164,9 +172,9 @@ class CaptureManager: self._capturing = method if cap is None: self._method2capture[method] = cap = self._getcapture(method) - cap.startall() + cap.start_capturing() else: - cap.resume() + cap.pop_outerr_to_orig() def suspendcapture(self, item=None): self.deactivate_funcargs() @@ -175,7 +183,7 @@ class CaptureManager: del self._capturing cap = self._method2capture.get(method) if cap is not None: - return cap.suspend() + return cap.readouterr() return "", "" def activate_funcargs(self, pyfuncitem): @@ -194,47 +202,68 @@ class CaptureManager: del self._capturing_funcarg return outerr + @pytest.mark.hookwrapper def pytest_make_collect_report(self, __multicall__, collector): method = self._getmethod(collector.config, collector.fspath) try: self.resumecapture(method) except ValueError: + yield # recursive collect, XXX refactor capturing # to allow for more lightweight recursive capturing return - try: - rep = __multicall__.execute() - finally: - outerr = self.suspendcapture() - addouterr(rep, outerr) - return rep + yield + 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: + rep.sections.append(("Captured stdout", out)) + if err: + rep.sections.append(("Captured stderr", err)) - @pytest.mark.tryfirst + @pytest.mark.hookwrapper def pytest_runtest_setup(self, item): - self.resumecapture_item(item) + with self.item_capture_wrapper(item, "setup"): + yield - @pytest.mark.tryfirst + @pytest.mark.hookwrapper def pytest_runtest_call(self, item): - self.resumecapture_item(item) - self.activate_funcargs(item) + with self.item_capture_wrapper(item, "call"): + yield - @pytest.mark.tryfirst + @pytest.mark.hookwrapper def pytest_runtest_teardown(self, item): - self.resumecapture_item(item) - - def pytest_keyboard_interrupt(self, excinfo): - if hasattr(self, '_capturing'): - self.suspendcapture() + with self.item_capture_wrapper(item, "teardown"): + yield @pytest.mark.tryfirst - def pytest_runtest_makereport(self, item, call): - funcarg_outerr = self.deactivate_funcargs() + def pytest_keyboard_interrupt(self, excinfo): + self.reset_capturings() + + @pytest.mark.tryfirst + def pytest_internalerror(self, excinfo): + self.reset_capturings() + + @contextlib.contextmanager + def item_capture_wrapper(self, item, when): + self.resumecapture_item(item) + if when == "call": + self.activate_funcargs(item) + yield + funcarg_outerr = self.deactivate_funcargs() + else: + yield + funcarg_outerr = None out, err = self.suspendcapture(item) if funcarg_outerr is not None: out += funcarg_outerr[0] err += funcarg_outerr[1] - item.add_report_section(call.when, "out", out) - item.add_report_section(call.when, "err", err) + item.add_report_section(when, "out", out) + item.add_report_section(when, "err", err) error_capsysfderror = "cannot use capsys and capfd at the same time" @@ -263,10 +292,10 @@ def pytest_funcarg__capfd(request): class CaptureFixture: def __init__(self, captureclass): - self._capture = captureclass() + self._capture = captureclass(in_=False) def _start(self): - self._capture.startall() + self._capture.start_capturing() def _finalize(self): if hasattr(self, '_capture'): @@ -295,6 +324,8 @@ class FDCapture: """ self.targetfd = targetfd if tmpfile is None and targetfd != 0: + # this code path is covered in the tests + # but not used by a regular pytest run f = tempfile.TemporaryFile('wb+') tmpfile = dupfile(f, encoding="UTF-8") f.close() @@ -390,13 +421,13 @@ class EncodedFile(object): return getattr(self._stream, name) -class Capture(object): +class StdCaptureBase(object): def reset(self): """ reset sys.stdout/stderr and return captured output as strings. """ if hasattr(self, '_reset'): raise ValueError("was already reset") self._reset = True - outfile, errfile = self.done(save=False) + outfile, errfile = self.stop_capturing(save=False) out, err = "", "" if outfile and not outfile.closed: out = outfile.read() @@ -406,14 +437,16 @@ class Capture(object): errfile.close() return out, err - def suspend(self): - """ return current snapshot captures, memorize tempfiles. """ - outerr = self.readouterr() - outfile, errfile = self.done() - return outerr + def pop_outerr_to_orig(self): + """ pop current snapshot out/err capture and flush to orig streams. """ + out, err = self.readouterr() + if out: + self.out.writeorg(out) + if err: + self.err.writeorg(err) -class StdCaptureFD(Capture): +class StdCaptureFD(StdCaptureBase): """ This class allows to capture writes to FD1 and FD2 and may connect a NULL file to FD0 (and prevent reads from sys.stdin). If any of the 0,1,2 file descriptors @@ -464,7 +497,7 @@ class StdCaptureFD(Capture): except OSError: pass - def startall(self): + def start_capturing(self): if hasattr(self, 'in_'): self.in_.start() if hasattr(self, 'out'): @@ -472,11 +505,10 @@ class StdCaptureFD(Capture): if hasattr(self, 'err'): self.err.start() - def resume(self): - """ resume capturing with original temp files. """ - self.startall() + #def pytest_sessionfinish(self): + # self.reset_capturings() - def done(self, save=True): + def stop_capturing(self, save=True): """ return (outfile, errfile) and stop capturing. """ outfile = errfile = None if hasattr(self, 'out') and not self.out.tmpfile.closed: @@ -491,16 +523,15 @@ class StdCaptureFD(Capture): def readouterr(self): """ return snapshot value of stdout/stderr capturings. """ - out = self._readsnapshot('out') - err = self._readsnapshot('err') - return out, err + return self._readsnapshot('out'), self._readsnapshot('err') def _readsnapshot(self, name): - if hasattr(self, name): + try: f = getattr(self, name).tmpfile - else: + except AttributeError: + return '' + if f.tell() == 0: return '' - f.seek(0) res = f.read() enc = getattr(f, "encoding", None) @@ -510,8 +541,17 @@ class StdCaptureFD(Capture): f.seek(0) return res +class TextCapture(TextIO): + def __init__(self, oldout): + super(TextCapture, self).__init__() + self._oldout = oldout -class StdCapture(Capture): + def writeorg(self, data): + self._oldout.write(data) + self._oldout.flush() + + +class StdCapture(StdCaptureBase): """ This class allows to capture writes to sys.stdout|stderr "in-memory" and will raise errors on tries to read from sys.stdin. It only modifies sys.stdout|stderr|stdin attributes and does not @@ -522,15 +562,15 @@ class StdCapture(Capture): self._olderr = sys.stderr self._oldin = sys.stdin if out and not hasattr(out, 'file'): - out = TextIO() + out = TextCapture(self._oldout) self.out = out if err: if not hasattr(err, 'write'): - err = TextIO() + err = TextCapture(self._olderr) self.err = err self.in_ = in_ - def startall(self): + def start_capturing(self): if self.out: sys.stdout = self.out if self.err: @@ -538,7 +578,7 @@ class StdCapture(Capture): if self.in_: sys.stdin = self.in_ = DontReadFromInput() - def done(self, save=True): + def stop_capturing(self, save=True): """ return (outfile, errfile) and stop capturing. """ outfile = errfile = None if self.out and not self.out.closed: @@ -553,9 +593,6 @@ class StdCapture(Capture): sys.stdin = self._oldin return outfile, errfile - def resume(self): - """ resume capturing with original temp files. """ - self.startall() def readouterr(self): """ return snapshot value of stdout/stderr capturings. """ diff --git a/_pytest/config.py b/_pytest/config.py index c22716bb3..78ac915d2 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -56,11 +56,15 @@ def _prepareconfig(args=None, plugins=None): raise ValueError("not a string or argument list: %r" % (args,)) args = py.std.shlex.split(args) pluginmanager = get_plugin_manager() - if plugins: - for plugin in plugins: - pluginmanager.register(plugin) - return pluginmanager.hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args) + try: + if plugins: + for plugin in plugins: + pluginmanager.register(plugin) + return pluginmanager.hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args) + except Exception: + pluginmanager.ensure_shutdown() + raise class PytestPluginManager(PluginManager): def __init__(self, hookspecs=[hookspec]): @@ -612,6 +616,9 @@ class Config(object): self.hook.pytest_logwarning(code=code, message=message, fslocation=None, nodeid=None) + def get_terminal_writer(self): + return self.pluginmanager.getplugin("terminalreporter")._tw + def pytest_cmdline_parse(self, pluginmanager, args): assert self == pluginmanager.config, (self, pluginmanager.config) self.parse(args) diff --git a/_pytest/genscript.py b/_pytest/genscript.py index 25e8e6a0f..b58798aec 100755 --- a/_pytest/genscript.py +++ b/_pytest/genscript.py @@ -60,6 +60,7 @@ def pytest_addoption(parser): def pytest_cmdline_main(config): genscript = config.getvalue("genscript") if genscript: + #tw = config.get_terminal_writer() tw = py.io.TerminalWriter() deps = ['py', '_pytest', 'pytest'] if sys.version_info < (2,7): diff --git a/_pytest/helpconfig.py b/_pytest/helpconfig.py index 9e31c21e7..f3cb10fe7 100644 --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -47,6 +47,8 @@ def pytest_unconfigure(config): def pytest_cmdline_main(config): if config.option.version: + capman = config.pluginmanager.getplugin("capturemanager") + capman.reset_capturings() p = py.path.local(pytest.__file__) sys.stderr.write("This is pytest version %s, imported from %s\n" % (pytest.__version__, p)) @@ -62,7 +64,7 @@ def pytest_cmdline_main(config): return 0 def showhelp(config): - tw = py.io.TerminalWriter() + tw = config.get_terminal_writer() tw.write(config._parser.optparser.format_help()) tw.line() tw.line() diff --git a/_pytest/mark.py b/_pytest/mark.py index 6b66b1876..0d241625e 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -40,7 +40,7 @@ def pytest_addoption(parser): def pytest_cmdline_main(config): if config.option.markers: config.do_configure() - tw = py.io.TerminalWriter() + tw = config.get_terminal_writer() for line in config.getini("markers"): name, rest = line.split(":", 1) tw.write("@pytest.mark.%s:" % name, bold=True) diff --git a/_pytest/pdb.py b/_pytest/pdb.py index eef24e43e..38366ff9a 100644 --- a/_pytest/pdb.py +++ b/_pytest/pdb.py @@ -34,10 +34,9 @@ class pytestPDB: if item is not None: capman = item.config.pluginmanager.getplugin("capturemanager") - out, err = capman.suspendcapture() - #if hasattr(item, 'outerr'): - # item.outerr = (item.outerr[0] + out, item.outerr[1] + err) - tw = py.io.TerminalWriter() + if capman: + capman.reset_capturings() + tw = item.config.get_terminal_writer() tw.line() tw.sep(">", "PDB set_trace (IO-capturing turned off)") py.std.pdb.Pdb().set_trace(frame) @@ -46,19 +45,20 @@ def pdbitem(item): pytestPDB.item = item pytest_runtest_setup = pytest_runtest_call = pytest_runtest_teardown = pdbitem -@pytest.mark.tryfirst -def pytest_make_collect_report(__multicall__, collector): - try: - pytestPDB.collector = collector - return __multicall__.execute() - finally: - pytestPDB.collector = None +@pytest.mark.hookwrapper +def pytest_make_collect_report(collector): + pytestPDB.collector = collector + yield + pytestPDB.collector = None def pytest_runtest_makereport(): pytestPDB.item = None class PdbInvoke: def pytest_exception_interact(self, node, call, report): + capman = node.config.pluginmanager.getplugin("capturemanager") + if capman: + capman.reset_capturings() return _enter_pdb(node, call.excinfo, report) def pytest_internalerror(self, excrepr, excinfo): diff --git a/_pytest/python.py b/_pytest/python.py index 89be2598a..68e7465d5 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -885,7 +885,7 @@ def _showfixtures_main(config, session): nodeid = "::".join(map(str, [curdir.bestrelpath(part[0])] + part[1:])) nodeid.replace(session.fspath.sep, "/") - tw = py.io.TerminalWriter() + tw = config.get_terminal_writer() verbose = config.getvalue("verbose") fm = session._fixturemanager diff --git a/_pytest/runner.py b/_pytest/runner.py index ed3c8e9dc..47f368878 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -135,14 +135,13 @@ class CallInfo: self.when = when self.start = time() try: - try: - self.result = func() - except KeyboardInterrupt: - raise - except: - self.excinfo = py.code.ExceptionInfo() - finally: + self.result = func() + except KeyboardInterrupt: self.stop = time() + raise + except: + self.excinfo = py.code.ExceptionInfo() + self.stop = time() def __repr__(self): if self.excinfo: @@ -292,7 +291,8 @@ def pytest_make_collect_report(collector): class CollectReport(BaseReport): - def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra): + def __init__(self, nodeid, outcome, longrepr, result, + sections=(), **extra): self.nodeid = nodeid self.outcome = outcome self.longrepr = longrepr diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 9b64ba336..fb3e92517 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -36,7 +36,10 @@ def pytest_addoption(parser): def pytest_configure(config): config.option.verbose -= config.option.quiet - reporter = TerminalReporter(config, sys.stdout) + out = config.pluginmanager.getplugin("dupped_stdout") + #if out is None: + # out = sys.stdout + reporter = TerminalReporter(config, out) config.pluginmanager.register(reporter, 'terminalreporter') if config.option.debug or config.option.traceconfig: def mywriter(tags, args): @@ -44,6 +47,11 @@ def pytest_configure(config): reporter.write_line("[traceconfig] " + msg) config.trace.root.setprocessor("pytest:config", mywriter) +def get_terminal_writer(config): + tr = config.pluginmanager.getplugin("terminalreporter") + return tr._tw + + def getreportopt(config): reportopts = "" optvalue = config.option.report diff --git a/bench/bench.py b/bench/bench.py index bb95f86fc..c99bc3234 100644 --- a/bench/bench.py +++ b/bench/bench.py @@ -4,8 +4,8 @@ if __name__ == '__main__': import cProfile import pytest import pstats - script = sys.argv[1] if len(sys.argv) > 1 else "empty.py" - stats = cProfile.run('pytest.cmdline.main([%r])' % script, 'prof') + script = sys.argv[1:] if len(sys.argv) > 1 else "empty.py" + stats = cProfile.run('pytest.cmdline.main(%r)' % script, 'prof') p = pstats.Stats("prof") p.strip_dirs() p.sort_stats('cumulative') diff --git a/testing/test_capture.py b/testing/test_capture.py index 5e697fd2a..c9b99f89f 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -736,14 +736,14 @@ class TestFDCapture: class TestStdCapture: def getcapture(self, **kw): cap = capture.StdCapture(**kw) - cap.startall() + cap.start_capturing() return cap def test_capturing_done_simple(self): cap = self.getcapture() sys.stdout.write("hello") sys.stderr.write("world") - outfile, errfile = cap.done() + outfile, errfile = cap.stop_capturing() s = outfile.read() assert s == "hello" s = errfile.read() @@ -787,6 +787,7 @@ class TestStdCapture: print('\xa6') out, err = cap.readouterr() assert out == py.builtin._totext('\ufffd\n', 'unicode-escape') + cap.reset() def test_reset_twice_error(self): cap = self.getcapture() @@ -859,12 +860,13 @@ class TestStdCapture: try: print ("hello") sys.stderr.write("error\n") - out, err = cap.suspend() + out, err = cap.readouterr() + cap.stop_capturing() assert out == "hello\n" assert not err print ("in between") sys.stderr.write("in between\n") - cap.resume() + cap.start_capturing() print ("after") sys.stderr.write("error_after\n") finally: @@ -878,7 +880,7 @@ class TestStdCaptureFD(TestStdCapture): def getcapture(self, **kw): cap = capture.StdCaptureFD(**kw) - cap.startall() + cap.start_capturing() return cap def test_intermingling(self): @@ -908,7 +910,7 @@ def test_stdcapture_fd_tmpfile(tmpfile): try: os.write(1, "hello".encode("ascii")) os.write(2, "world".encode("ascii")) - outf, errf = capfd.done() + outf, errf = capfd.stop_capturing() finally: capfd.reset() assert outf == tmpfile @@ -924,15 +926,15 @@ class TestStdCaptureFDinvalidFD: def test_stdout(): os.close(1) cap = StdCaptureFD(out=True, err=False, in_=False) - cap.done() + cap.stop_capturing() def test_stderr(): os.close(2) cap = StdCaptureFD(out=False, err=True, in_=False) - cap.done() + cap.stop_capturing() def test_stdin(): os.close(0) cap = StdCaptureFD(out=False, err=False, in_=True) - cap.done() + cap.stop_capturing() """) result = testdir.runpytest("--capture=fd") assert result.ret == 0 @@ -941,8 +943,8 @@ class TestStdCaptureFDinvalidFD: def test_capture_not_started_but_reset(): capsys = capture.StdCapture() - capsys.done() - capsys.done() + capsys.stop_capturing() + capsys.stop_capturing() capsys.reset() @@ -951,7 +953,7 @@ def test_capture_no_sys(): capsys = capture.StdCapture() try: cap = capture.StdCaptureFD(patchsys=False) - cap.startall() + cap.start_capturing() sys.stdout.write("hello") sys.stderr.write("world") oswritebytes(1, "1") @@ -970,10 +972,9 @@ def test_fdcapture_tmpfile_remains_the_same(tmpfile, use): tmpfile = True cap = capture.StdCaptureFD(out=False, err=tmpfile) try: - cap.startall() + cap.start_capturing() capfile = cap.err.tmpfile - cap.suspend() - cap.resume() + cap.readouterr() finally: cap.reset() capfile2 = cap.err.tmpfile @@ -990,22 +991,25 @@ def test_capturing_and_logging_fundamentals(testdir, method): import py, logging from _pytest import capture cap = capture.%s(out=False, in_=False) - cap.startall() + cap.start_capturing() logging.warn("hello1") - outerr = cap.suspend() + outerr = cap.readouterr() print ("suspend, captured %%s" %%(outerr,)) logging.warn("hello2") - cap.resume() + cap.pop_outerr_to_orig() logging.warn("hello3") - outerr = cap.suspend() + outerr = cap.readouterr() print ("suspend2, captured %%s" %% (outerr,)) """ % (method,)) result = testdir.runpython(p) - result.stdout.fnmatch_lines([ - "suspend, captured*hello1*", - "suspend2, captured*hello2*WARNING:root:hello3*", - ]) + result.stdout.fnmatch_lines(""" + suspend, captured*hello1* + suspend2, captured*WARNING:root:hello3* + """) + result.stderr.fnmatch_lines(""" + WARNING:root:hello2 + """) assert "atexit" not in result.stderr.str() From ac1d277225980f52503bdb6ae322dc09dbb67ce4 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 14 Mar 2014 12:49:37 +0100 Subject: [PATCH 05/11] simplify pdb disabling of capturing, also accomodate the new semantics that capturing is always on during a test session. --- _pytest/pdb.py | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/_pytest/pdb.py b/_pytest/pdb.py index 38366ff9a..33a892184 100644 --- a/_pytest/pdb.py +++ b/_pytest/pdb.py @@ -16,43 +16,30 @@ def pytest_configure(config): if config.getvalue("usepdb"): config.pluginmanager.register(PdbInvoke(), 'pdbinvoke') - old_trace = py.std.pdb.set_trace + old = (py.std.pdb.set_trace, pytestPDB._pluginmanager) def fin(): - py.std.pdb.set_trace = old_trace + py.std.pdb.set_trace, pytestPDB._pluginmanager = old py.std.pdb.set_trace = pytest.set_trace + pytestPDB._pluginmanager = config.pluginmanager config._cleanup.append(fin) class pytestPDB: """ Pseudo PDB that defers to the real pdb. """ - item = None - collector = None + _pluginmanager = None def set_trace(self): """ invoke PDB set_trace debugging, dropping any IO capturing. """ frame = sys._getframe().f_back - item = self.item or self.collector - - if item is not None: - capman = item.config.pluginmanager.getplugin("capturemanager") + capman = None + if self._pluginmanager is not None: + capman = self._pluginmanager.getplugin("capturemanager") if capman: capman.reset_capturings() - tw = item.config.get_terminal_writer() + tw = py.io.TerminalWriter() tw.line() tw.sep(">", "PDB set_trace (IO-capturing turned off)") py.std.pdb.Pdb().set_trace(frame) -def pdbitem(item): - pytestPDB.item = item -pytest_runtest_setup = pytest_runtest_call = pytest_runtest_teardown = pdbitem - -@pytest.mark.hookwrapper -def pytest_make_collect_report(collector): - pytestPDB.collector = collector - yield - pytestPDB.collector = None - -def pytest_runtest_makereport(): - pytestPDB.item = None class PdbInvoke: def pytest_exception_interact(self, node, call, report): From d27c37781733607d37af3e89c0ebc86f1d80c0c1 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 14 Mar 2014 15:58:16 +0100 Subject: [PATCH 06/11] tentatively fix py33 and py25 compat --- _pytest/capture.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/_pytest/capture.py b/_pytest/capture.py index a4c1535e4..a63738315 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -2,6 +2,8 @@ per-test stdout/stderr capturing mechanism. """ +from __future__ import with_statement + import sys import os import tempfile @@ -366,12 +368,9 @@ class FDCapture: def writeorg(self, data): """ write a string to the original file descriptor """ - tempfp = tempfile.TemporaryFile() - try: - os.dup2(self._savefd, tempfp.fileno()) - tempfp.write(data) - finally: - tempfp.close() + if py.builtin._istext(data): + data = data.encode("utf8") # XXX use encoding of original stream + os.write(self._savefd, data) def dupfile(f, mode=None, buffering=0, raising=False, encoding=None): From 2263fcf6b7c97346cfc9ee1b38693fe7bd1d93f4 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 28 Mar 2014 07:03:34 +0100 Subject: [PATCH 07/11] remove unused "suspend/resume" on capturing, some formatting cleanup --- _pytest/capture.py | 35 ++++++----------------------------- testing/test_capture.py | 28 +++------------------------- 2 files changed, 9 insertions(+), 54 deletions(-) diff --git a/_pytest/capture.py b/_pytest/capture.py index a63738315..c5ad88f2f 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -426,7 +426,7 @@ class StdCaptureBase(object): if hasattr(self, '_reset'): raise ValueError("was already reset") self._reset = True - outfile, errfile = self.stop_capturing(save=False) + outfile, errfile = self.stop_capturing() out, err = "", "" if outfile and not outfile.closed: out = outfile.read() @@ -452,24 +452,9 @@ class StdCaptureFD(StdCaptureBase): is invalid it will not be captured. """ def __init__(self, out=True, err=True, in_=True, patchsys=True): - self._options = { - "out": out, - "err": err, - "in_": in_, - "patchsys": patchsys, - } - self._save() - - def _save(self): - in_ = self._options['in_'] - out = self._options['out'] - err = self._options['err'] - patchsys = self._options['patchsys'] if in_: try: - self.in_ = FDCapture( - 0, tmpfile=None, - patchsys=patchsys) + self.in_ = FDCapture(0, tmpfile=None, patchsys=patchsys) except OSError: pass if out: @@ -477,10 +462,7 @@ class StdCaptureFD(StdCaptureBase): if hasattr(out, 'write'): tmpfile = out try: - self.out = FDCapture( - 1, tmpfile=tmpfile, - patchsys=patchsys) - self._options['out'] = self.out.tmpfile + self.out = FDCapture(1, tmpfile=tmpfile, patchsys=patchsys) except OSError: pass if err: @@ -489,10 +471,7 @@ class StdCaptureFD(StdCaptureBase): else: tmpfile = None try: - self.err = FDCapture( - 2, tmpfile=tmpfile, - patchsys=patchsys) - self._options['err'] = self.err.tmpfile + self.err = FDCapture(2, tmpfile=tmpfile, patchsys=patchsys) except OSError: pass @@ -507,7 +486,7 @@ class StdCaptureFD(StdCaptureBase): #def pytest_sessionfinish(self): # self.reset_capturings() - def stop_capturing(self, save=True): + def stop_capturing(self): """ return (outfile, errfile) and stop capturing. """ outfile = errfile = None if hasattr(self, 'out') and not self.out.tmpfile.closed: @@ -516,8 +495,6 @@ class StdCaptureFD(StdCaptureBase): errfile = self.err.done() if hasattr(self, 'in_'): self.in_.done() - if save: - self._save() return outfile, errfile def readouterr(self): @@ -577,7 +554,7 @@ class StdCapture(StdCaptureBase): if self.in_: sys.stdin = self.in_ = DontReadFromInput() - def stop_capturing(self, save=True): + def stop_capturing(self): """ return (outfile, errfile) and stop capturing. """ outfile = errfile = None if self.out and not self.out.closed: diff --git a/testing/test_capture.py b/testing/test_capture.py index ed5e6c7d8..1a98b556d 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -873,25 +873,6 @@ class TestStdCapture: pytest.raises(IOError, "sys.stdin.read()") out, err = cap.reset() - def test_suspend_resume(self): - cap = self.getcapture(out=True, err=False, in_=False) - try: - print ("hello") - sys.stderr.write("error\n") - out, err = cap.readouterr() - cap.stop_capturing() - assert out == "hello\n" - assert not err - print ("in between") - sys.stderr.write("in between\n") - cap.start_capturing() - print ("after") - sys.stderr.write("error_after\n") - finally: - out, err = cap.reset() - assert out == "after\n" - assert not err - class TestStdCaptureFD(TestStdCapture): pytestmark = needsosdup @@ -925,12 +906,9 @@ class TestStdCaptureFD(TestStdCapture): @needsosdup def test_stdcapture_fd_tmpfile(tmpfile): capfd = capture.StdCaptureFD(out=tmpfile) - try: - os.write(1, "hello".encode("ascii")) - os.write(2, "world".encode("ascii")) - outf, errf = capfd.stop_capturing() - finally: - capfd.reset() + os.write(1, "hello".encode("ascii")) + os.write(2, "world".encode("ascii")) + outf, errf = capfd.stop_capturing() assert outf == tmpfile From e18c3ed494921d64ff85a5554b5b4123c9c84933 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 28 Mar 2014 07:03:37 +0100 Subject: [PATCH 08/11] unify and normalize Sys/FD Capturing classes * * * more unification --- _pytest/capture.py | 310 ++++++++++++++-------------------------- testing/test_capture.py | 59 ++++---- 2 files changed, 134 insertions(+), 235 deletions(-) diff --git a/_pytest/capture.py b/_pytest/capture.py index c5ad88f2f..38460e240 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -100,44 +100,25 @@ def pytest_load_initial_conftests(early_config, parser, args, __multicall__): raise -class NoCapture: - def start_capturing(self): - pass - - def stop_capturing(self): - pass - - def pop_outerr_to_orig(self): - pass - - def reset(self): - pass - - def readouterr(self): - return "", "" +def maketmpfile(): + f = py.std.tempfile.TemporaryFile() + newf = dupfile(f, encoding="UTF-8") + f.close() + return newf class CaptureManager: def __init__(self, defaultmethod=None): self._method2capture = {} self._defaultmethod = defaultmethod - def _maketempfile(self): - f = py.std.tempfile.TemporaryFile() - newf = dupfile(f, encoding="UTF-8") - f.close() - return newf - def _getcapture(self, method): if method == "fd": - return StdCaptureFD( - out=self._maketempfile(), - err=self._maketempfile(), - ) + return StdCaptureBase(out=True, err=True, Capture=FDCapture) elif method == "sys": - return StdCapture(out=TextIO(), err=TextIO()) + return StdCaptureBase(out=True, err=True, Capture=SysCapture) elif method == "no": - return NoCapture() + return StdCaptureBase(out=False, err=False, in_=False) else: raise ValueError("unknown capturing method: %r" % method) @@ -277,8 +258,7 @@ def pytest_funcarg__capsys(request): """ if "capfd" in request._funcargs: raise request.raiseerror(error_capsysfderror) - return CaptureFixture(StdCapture) - + return CaptureFixture(SysCapture) def pytest_funcarg__capfd(request): """enables capturing of writes to file descriptors 1 and 2 and makes @@ -289,12 +269,13 @@ def pytest_funcarg__capfd(request): request.raiseerror(error_capsysfderror) if not hasattr(os, 'dup'): pytest.skip("capfd funcarg needs os.dup") - return CaptureFixture(StdCaptureFD) + return CaptureFixture(FDCapture) class CaptureFixture: def __init__(self, captureclass): - self._capture = captureclass(in_=False) + self._capture = StdCaptureBase(out=True, err=True, in_=False, + Capture=captureclass) def _start(self): self._capture.start_capturing() @@ -315,63 +296,6 @@ class CaptureFixture: self._finalize() -class FDCapture: - """ Capture IO to/from a given os-level filedescriptor. """ - - def __init__(self, targetfd, tmpfile=None, patchsys=False): - """ save targetfd descriptor, and open a new - temporary file there. If no tmpfile is - specified a tempfile.Tempfile() will be opened - in text mode. - """ - self.targetfd = targetfd - if tmpfile is None and targetfd != 0: - # this code path is covered in the tests - # but not used by a regular pytest run - f = tempfile.TemporaryFile('wb+') - tmpfile = dupfile(f, encoding="UTF-8") - f.close() - self.tmpfile = tmpfile - self._savefd = os.dup(self.targetfd) - if patchsys: - self._oldsys = getattr(sys, patchsysdict[targetfd]) - - def start(self): - try: - os.fstat(self._savefd) - except OSError: - raise ValueError( - "saved filedescriptor not valid, " - "did you call start() twice?") - if self.targetfd == 0 and not self.tmpfile: - fd = os.open(os.devnull, os.O_RDONLY) - os.dup2(fd, 0) - os.close(fd) - if hasattr(self, '_oldsys'): - setattr(sys, patchsysdict[self.targetfd], DontReadFromInput()) - else: - os.dup2(self.tmpfile.fileno(), self.targetfd) - if hasattr(self, '_oldsys'): - setattr(sys, patchsysdict[self.targetfd], self.tmpfile) - - def done(self): - """ unpatch and clean up, returns the self.tmpfile (file object) - """ - os.dup2(self._savefd, self.targetfd) - os.close(self._savefd) - if self.targetfd != 0: - self.tmpfile.seek(0) - if hasattr(self, '_oldsys'): - setattr(sys, patchsysdict[self.targetfd], self._oldsys) - return self.tmpfile - - def writeorg(self, data): - """ write a string to the original file descriptor - """ - if py.builtin._istext(data): - data = data.encode("utf8") # XXX use encoding of original stream - os.write(self._savefd, data) - def dupfile(f, mode=None, buffering=0, raising=False, encoding=None): """ return a new open file object that's a duplicate of f @@ -421,6 +345,16 @@ class EncodedFile(object): class StdCaptureBase(object): + out = err = in_ = None + + def __init__(self, out=True, err=True, in_=True, Capture=None): + if in_: + self.in_ = Capture(0) + if out: + self.out = Capture(1) + if err: + self.err = Capture(2) + def reset(self): """ reset sys.stdout/stderr and return captured output as strings. """ if hasattr(self, '_reset'): @@ -436,6 +370,25 @@ class StdCaptureBase(object): errfile.close() return out, err + def start_capturing(self): + if self.in_: + self.in_.start() + if self.out: + self.out.start() + if self.err: + self.err.start() + + def stop_capturing(self): + """ return (outfile, errfile) and stop capturing. """ + outfile = errfile = None + if self.out: + outfile = self.out.done() + if self.err: + errfile = self.err.done() + if self.in_: + self.in_.done() + return outfile, errfile + def pop_outerr_to_orig(self): """ pop current snapshot out/err capture and flush to orig streams. """ out, err = self.readouterr() @@ -444,61 +397,8 @@ class StdCaptureBase(object): if err: self.err.writeorg(err) - -class StdCaptureFD(StdCaptureBase): - """ This class allows to capture writes to FD1 and FD2 - and may connect a NULL file to FD0 (and prevent - reads from sys.stdin). If any of the 0,1,2 file descriptors - is invalid it will not be captured. - """ - def __init__(self, out=True, err=True, in_=True, patchsys=True): - if in_: - try: - self.in_ = FDCapture(0, tmpfile=None, patchsys=patchsys) - except OSError: - pass - if out: - tmpfile = None - if hasattr(out, 'write'): - tmpfile = out - try: - self.out = FDCapture(1, tmpfile=tmpfile, patchsys=patchsys) - except OSError: - pass - if err: - if hasattr(err, 'write'): - tmpfile = err - else: - tmpfile = None - try: - self.err = FDCapture(2, tmpfile=tmpfile, patchsys=patchsys) - except OSError: - pass - - def start_capturing(self): - if hasattr(self, 'in_'): - self.in_.start() - if hasattr(self, 'out'): - self.out.start() - if hasattr(self, 'err'): - self.err.start() - - #def pytest_sessionfinish(self): - # self.reset_capturings() - - def stop_capturing(self): - """ return (outfile, errfile) and stop capturing. """ - outfile = errfile = None - if hasattr(self, 'out') and not self.out.tmpfile.closed: - outfile = self.out.done() - if hasattr(self, 'err') and not self.err.tmpfile.closed: - errfile = self.err.done() - if hasattr(self, 'in_'): - self.in_.done() - return outfile, errfile - def readouterr(self): - """ return snapshot value of stdout/stderr capturings. """ + """ return snapshot unicode value of stdout/stderr capturings. """ return self._readsnapshot('out'), self._readsnapshot('err') def _readsnapshot(self, name): @@ -511,77 +411,87 @@ class StdCaptureFD(StdCaptureBase): f.seek(0) res = f.read() enc = getattr(f, "encoding", None) - if enc: + if enc and isinstance(res, bytes): res = py.builtin._totext(res, enc, "replace") f.truncate(0) f.seek(0) return res -class TextCapture(TextIO): - def __init__(self, oldout): - super(TextCapture, self).__init__() - self._oldout = oldout + +class FDCapture: + """ Capture IO to/from a given os-level filedescriptor. """ + + def __init__(self, targetfd, tmpfile=None): + self.targetfd = targetfd + try: + self._savefd = os.dup(self.targetfd) + except OSError: + self.start = lambda: None + self.done = lambda: None + else: + if tmpfile is None: + if targetfd == 0: + tmpfile = open(os.devnull, "r") + else: + tmpfile = maketmpfile() + self.tmpfile = tmpfile + if targetfd in patchsysdict: + self._oldsys = getattr(sys, patchsysdict[targetfd]) + + def start(self): + """ Start capturing on targetfd using memorized tmpfile. """ + try: + os.fstat(self._savefd) + except OSError: + raise ValueError("saved filedescriptor not valid anymore") + targetfd = self.targetfd + os.dup2(self.tmpfile.fileno(), targetfd) + if hasattr(self, '_oldsys'): + subst = self.tmpfile if targetfd != 0 else DontReadFromInput() + setattr(sys, patchsysdict[targetfd], subst) + + def done(self): + """ stop capturing, restore streams, return original capture file, + seeked to position zero. """ + os.dup2(self._savefd, self.targetfd) + os.close(self._savefd) + if self.targetfd != 0: + self.tmpfile.seek(0) + if hasattr(self, '_oldsys'): + setattr(sys, patchsysdict[self.targetfd], self._oldsys) + return self.tmpfile def writeorg(self, data): - self._oldout.write(data) - self._oldout.flush() + """ write a string to the original file descriptor + """ + if py.builtin._istext(data): + data = data.encode("utf8") # XXX use encoding of original stream + os.write(self._savefd, data) -class StdCapture(StdCaptureBase): - """ This class allows to capture writes to sys.stdout|stderr "in-memory" - and will raise errors on tries to read from sys.stdin. It only - modifies sys.stdout|stderr|stdin attributes and does not - touch underlying File Descriptors (use StdCaptureFD for that). - """ - def __init__(self, out=True, err=True, in_=True): - self._oldout = sys.stdout - self._olderr = sys.stderr - self._oldin = sys.stdin - if out and not hasattr(out, 'file'): - out = TextCapture(self._oldout) - self.out = out - if err: - if not hasattr(err, 'write'): - err = TextCapture(self._olderr) - self.err = err - self.in_ = in_ +class SysCapture: + def __init__(self, fd): + name = patchsysdict[fd] + self._old = getattr(sys, name) + self.name = name + if name == "stdin": + self.tmpfile = DontReadFromInput() + else: + self.tmpfile = TextIO() - def start_capturing(self): - if self.out: - sys.stdout = self.out - if self.err: - sys.stderr = self.err - if self.in_: - sys.stdin = self.in_ = DontReadFromInput() + def start(self): + setattr(sys, self.name, self.tmpfile) - def stop_capturing(self): - """ return (outfile, errfile) and stop capturing. """ - outfile = errfile = None - if self.out and not self.out.closed: - sys.stdout = self._oldout - outfile = self.out - outfile.seek(0) - if self.err and not self.err.closed: - sys.stderr = self._olderr - errfile = self.err - errfile.seek(0) - if self.in_: - sys.stdin = self._oldin - return outfile, errfile + def done(self): + setattr(sys, self.name, self._old) + if self.name != "stdin": + self.tmpfile.seek(0) + return self.tmpfile + def writeorg(self, data): + self._old.write(data) + self._old.flush() - def readouterr(self): - """ return snapshot value of stdout/stderr capturings. """ - out = err = "" - if self.out: - out = self.out.getvalue() - self.out.truncate(0) - self.out.seek(0) - if self.err: - err = self.err.getvalue() - self.err.truncate(0) - self.err.seek(0) - return out, err class DontReadFromInput: diff --git a/testing/test_capture.py b/testing/test_capture.py index 1a98b556d..77092eaef 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -4,6 +4,7 @@ from __future__ import with_statement import os import sys import py +import tempfile import pytest import contextlib @@ -44,6 +45,13 @@ def oswritebytes(fd, obj): +def StdCaptureFD(out=True, err=True, in_=True): + return capture.StdCaptureBase(out, err, in_, Capture=capture.FDCapture) + +def StdCapture(out=True, err=True, in_=True): + return capture.StdCaptureBase(out, err, in_, Capture=capture.SysCapture) + + class TestCaptureManager: def test_getmethod_default_no_fd(self, testdir, monkeypatch): config = testdir.parseconfig(testdir.tmpdir) @@ -75,7 +83,7 @@ class TestCaptureManager: @needsosdup @pytest.mark.parametrize("method", ['no', 'fd', 'sys']) def test_capturing_basic_api(self, method): - capouter = capture.StdCaptureFD() + capouter = StdCaptureFD() old = sys.stdout, sys.stderr, sys.stdin try: capman = CaptureManager() @@ -99,7 +107,7 @@ class TestCaptureManager: @needsosdup def test_juggle_capturings(self, testdir): - capouter = capture.StdCaptureFD() + capouter = StdCaptureFD() try: #config = testdir.parseconfig(testdir.tmpdir) capman = CaptureManager() @@ -717,7 +725,7 @@ class TestFDCapture: f.close() def test_stderr(self): - cap = capture.FDCapture(2, patchsys=True) + cap = capture.FDCapture(2) cap.start() print_("hello", file=sys.stderr) f = cap.done() @@ -727,7 +735,7 @@ class TestFDCapture: def test_stdin(self, tmpfile): tmpfile.write(tobytes("3")) tmpfile.seek(0) - cap = capture.FDCapture(0, tmpfile=tmpfile) + cap = capture.FDCapture(0, tmpfile) cap.start() # check with os.read() directly instead of raw_input(), because # sys.stdin itself may be redirected (as pytest now does by default) @@ -753,7 +761,7 @@ class TestFDCapture: class TestStdCapture: def getcapture(self, **kw): - cap = capture.StdCapture(**kw) + cap = StdCapture(**kw) cap.start_capturing() return cap @@ -878,7 +886,7 @@ class TestStdCaptureFD(TestStdCapture): pytestmark = needsosdup def getcapture(self, **kw): - cap = capture.StdCaptureFD(**kw) + cap = StdCaptureFD(**kw) cap.start_capturing() return cap @@ -899,18 +907,10 @@ class TestStdCaptureFD(TestStdCapture): def test_many(self, capfd): with lsof_check(): for i in range(10): - cap = capture.StdCaptureFD() + cap = StdCaptureFD() cap.reset() -@needsosdup -def test_stdcapture_fd_tmpfile(tmpfile): - capfd = capture.StdCaptureFD(out=tmpfile) - os.write(1, "hello".encode("ascii")) - os.write(2, "world".encode("ascii")) - outf, errf = capfd.stop_capturing() - assert outf == tmpfile - class TestStdCaptureFDinvalidFD: pytestmark = needsosdup @@ -918,7 +918,10 @@ class TestStdCaptureFDinvalidFD: def test_stdcapture_fd_invalid_fd(self, testdir): testdir.makepyfile(""" import os - from _pytest.capture import StdCaptureFD + from _pytest import capture + def StdCaptureFD(out=True, err=True, in_=True): + return capture.StdCaptureBase(out, err, in_, + Capture=capture.FDCapture) def test_stdout(): os.close(1) cap = StdCaptureFD(out=True, err=False, in_=False) @@ -938,27 +941,12 @@ class TestStdCaptureFDinvalidFD: def test_capture_not_started_but_reset(): - capsys = capture.StdCapture() + capsys = StdCapture() capsys.stop_capturing() capsys.stop_capturing() capsys.reset() -@needsosdup -def test_capture_no_sys(): - capsys = capture.StdCapture() - try: - cap = capture.StdCaptureFD(patchsys=False) - cap.start_capturing() - sys.stdout.write("hello") - sys.stderr.write("world") - oswritebytes(1, "1") - oswritebytes(2, "2") - out, err = cap.reset() - assert out == "1" - assert err == "2" - finally: - capsys.reset() @needsosdup @@ -966,7 +954,7 @@ def test_capture_no_sys(): def test_fdcapture_tmpfile_remains_the_same(tmpfile, use): if not use: tmpfile = True - cap = capture.StdCaptureFD(out=False, err=tmpfile) + cap = StdCaptureFD(out=False, err=tmpfile) try: cap.start_capturing() capfile = cap.err.tmpfile @@ -977,7 +965,7 @@ def test_fdcapture_tmpfile_remains_the_same(tmpfile, use): assert capfile2 == capfile -@pytest.mark.parametrize('method', ['StdCapture', 'StdCaptureFD']) +@pytest.mark.parametrize('method', ['SysCapture', 'FDCapture']) def test_capturing_and_logging_fundamentals(testdir, method): if method == "StdCaptureFD" and not hasattr(os, 'dup'): pytest.skip("need os.dup") @@ -986,7 +974,8 @@ def test_capturing_and_logging_fundamentals(testdir, method): import sys, os import py, logging from _pytest import capture - cap = capture.%s(out=False, in_=False) + cap = capture.StdCaptureBase(out=False, in_=False, + Capture=capture.%s) cap.start_capturing() logging.warn("hello1") From a8f4f49a82e6c8d1685a5bff6fdb0f74a82e1657 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 28 Mar 2014 07:11:25 +0100 Subject: [PATCH 09/11] simplify reset/stop_capturing and fix capturing wrt to capturing simple os.write() calls --- _pytest/capture.py | 108 ++++++++++++++++++---------------------- testing/test_capture.py | 93 +++++++++++++++++++--------------- 2 files changed, 103 insertions(+), 98 deletions(-) diff --git a/_pytest/capture.py b/_pytest/capture.py index 38460e240..203044892 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -6,7 +6,7 @@ from __future__ import with_statement import sys import os -import tempfile +from tempfile import TemporaryFile import contextlib import py @@ -101,12 +101,6 @@ def pytest_load_initial_conftests(early_config, parser, args, __multicall__): -def maketmpfile(): - f = py.std.tempfile.TemporaryFile() - newf = dupfile(f, encoding="UTF-8") - f.close() - return newf - class CaptureManager: def __init__(self, defaultmethod=None): self._method2capture = {} @@ -137,7 +131,7 @@ class CaptureManager: def reset_capturings(self): for cap in self._method2capture.values(): cap.pop_outerr_to_orig() - cap.reset() + cap.stop_capturing() self._method2capture.clear() def resumecapture_item(self, item): @@ -274,15 +268,16 @@ def pytest_funcarg__capfd(request): class CaptureFixture: def __init__(self, captureclass): - self._capture = StdCaptureBase(out=True, err=True, in_=False, - Capture=captureclass) + self.captureclass = captureclass def _start(self): + self._capture = StdCaptureBase(out=True, err=True, in_=False, + Capture=self.captureclass) self._capture.start_capturing() def _finalize(self): if hasattr(self, '_capture'): - outerr = self._outerr = self._capture.reset() + outerr = self._outerr = self._capture.stop_capturing() del self._capture return outerr @@ -355,21 +350,6 @@ class StdCaptureBase(object): if err: self.err = Capture(2) - def reset(self): - """ reset sys.stdout/stderr and return captured output as strings. """ - if hasattr(self, '_reset'): - raise ValueError("was already reset") - self._reset = True - outfile, errfile = self.stop_capturing() - out, err = "", "" - if outfile and not outfile.closed: - out = outfile.read() - outfile.close() - if errfile and errfile != outfile and not errfile.closed: - err = errfile.read() - errfile.close() - return out, err - def start_capturing(self): if self.in_: self.in_.start() @@ -378,17 +358,6 @@ class StdCaptureBase(object): if self.err: self.err.start() - def stop_capturing(self): - """ return (outfile, errfile) and stop capturing. """ - outfile = errfile = None - if self.out: - outfile = self.out.done() - if self.err: - errfile = self.err.done() - if self.in_: - self.in_.done() - return outfile, errfile - def pop_outerr_to_orig(self): """ pop current snapshot out/err capture and flush to orig streams. """ out, err = self.readouterr() @@ -397,25 +366,27 @@ class StdCaptureBase(object): if err: self.err.writeorg(err) + def stop_capturing(self): + """ stop capturing and reset capturing streams """ + if hasattr(self, '_reset'): + raise ValueError("was already stopped") + self._reset = True + if self.out: + self.out.done() + if self.err: + self.err.done() + if self.in_: + self.in_.done() + def readouterr(self): """ return snapshot unicode value of stdout/stderr capturings. """ return self._readsnapshot('out'), self._readsnapshot('err') def _readsnapshot(self, name): - try: - f = getattr(self, name).tmpfile - except AttributeError: - return '' - if f.tell() == 0: - return '' - f.seek(0) - res = f.read() - enc = getattr(f, "encoding", None) - if enc and isinstance(res, bytes): - res = py.builtin._totext(res, enc, "replace") - f.truncate(0) - f.seek(0) - return res + cap = getattr(self, name, None) + if cap is None: + return "" + return cap.snap() class FDCapture: @@ -433,11 +404,16 @@ class FDCapture: if targetfd == 0: tmpfile = open(os.devnull, "r") else: - tmpfile = maketmpfile() + f = TemporaryFile() + with f: + tmpfile = dupfile(f, encoding="UTF-8") self.tmpfile = tmpfile if targetfd in patchsysdict: self._oldsys = getattr(sys, patchsysdict[targetfd]) + def __repr__(self): + return "" % (self.targetfd, self._savefd) + def start(self): """ Start capturing on targetfd using memorized tmpfile. """ try: @@ -450,16 +426,26 @@ class FDCapture: subst = self.tmpfile if targetfd != 0 else DontReadFromInput() setattr(sys, patchsysdict[targetfd], subst) + def snap(self): + f = self.tmpfile + f.seek(0) + res = f.read() + if res: + enc = getattr(f, "encoding", None) + if enc and isinstance(res, bytes): + res = py.builtin._totext(res, enc, "replace") + f.truncate(0) + f.seek(0) + return res + def done(self): """ stop capturing, restore streams, return original capture file, seeked to position zero. """ os.dup2(self._savefd, self.targetfd) os.close(self._savefd) - if self.targetfd != 0: - self.tmpfile.seek(0) if hasattr(self, '_oldsys'): setattr(sys, patchsysdict[self.targetfd], self._oldsys) - return self.tmpfile + self.tmpfile.close() def writeorg(self, data): """ write a string to the original file descriptor @@ -482,18 +468,22 @@ class SysCapture: def start(self): setattr(sys, self.name, self.tmpfile) + def snap(self): + f = self.tmpfile + res = f.getvalue() + f.truncate(0) + f.seek(0) + return res + def done(self): setattr(sys, self.name, self._old) - if self.name != "stdin": - self.tmpfile.seek(0) - return self.tmpfile + self.tmpfile.close() def writeorg(self, data): self._old.write(data) self._old.flush() - class DontReadFromInput: """Temporary stub class. Ideally when stdin is accessed, the capturing should be turned off, with possibly all data captured diff --git a/testing/test_capture.py b/testing/test_capture.py index 77092eaef..ef391b913 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -103,7 +103,7 @@ class TestCaptureManager: assert not out and not err capman.reset_capturings() finally: - capouter.reset() + capouter.stop_capturing() @needsosdup def test_juggle_capturings(self, testdir): @@ -127,7 +127,7 @@ class TestCaptureManager: finally: capman.reset_capturings() finally: - capouter.reset() + capouter.stop_capturing() @pytest.mark.parametrize("method", ['fd', 'sys']) @@ -696,17 +696,15 @@ class TestFDCapture: cap = capture.FDCapture(fd) data = tobytes("hello") os.write(fd, data) - f = cap.done() - s = f.read() - f.close() + s = cap.snap() + cap.done() assert not s cap = capture.FDCapture(fd) cap.start() os.write(fd, data) - f = cap.done() - s = f.read() + s = cap.snap() + cap.done() assert s == "hello" - f.close() def test_simple_many(self, tmpfile): for i in range(10): @@ -720,16 +718,15 @@ class TestFDCapture: def test_simple_fail_second_start(self, tmpfile): fd = tmpfile.fileno() cap = capture.FDCapture(fd) - f = cap.done() + cap.done() pytest.raises(ValueError, cap.start) - f.close() def test_stderr(self): cap = capture.FDCapture(2) cap.start() print_("hello", file=sys.stderr) - f = cap.done() - s = f.read() + s = cap.snap() + cap.done() assert s == "hello\n" def test_stdin(self, tmpfile): @@ -752,8 +749,8 @@ class TestFDCapture: cap.writeorg(data2) finally: tmpfile.close() - f = cap.done() - scap = f.read() + scap = cap.snap() + cap.done() assert scap == totext(data1) stmp = open(tmpfile.name, 'rb').read() assert stmp == data2 @@ -769,17 +766,17 @@ class TestStdCapture: cap = self.getcapture() sys.stdout.write("hello") sys.stderr.write("world") - outfile, errfile = cap.stop_capturing() - s = outfile.read() - assert s == "hello" - s = errfile.read() - assert s == "world" + out, err = cap.readouterr() + cap.stop_capturing() + assert out == "hello" + assert err == "world" def test_capturing_reset_simple(self): cap = self.getcapture() print("hello world") sys.stderr.write("hello error\n") - out, err = cap.reset() + out, err = cap.readouterr() + cap.stop_capturing() assert out == "hello world\n" assert err == "hello error\n" @@ -792,8 +789,9 @@ class TestStdCapture: assert out == "hello world\n" assert err == "hello error\n" sys.stderr.write("error2") + out, err = cap.readouterr() finally: - out, err = cap.reset() + cap.stop_capturing() assert err == "error2" def test_capturing_readouterr_unicode(self): @@ -802,7 +800,7 @@ class TestStdCapture: print ("hx\xc4\x85\xc4\x87") out, err = cap.readouterr() finally: - cap.reset() + cap.stop_capturing() assert out == py.builtin._totext("hx\xc4\x85\xc4\x87\n", "utf8") @pytest.mark.skipif('sys.version_info >= (3,)', @@ -813,13 +811,14 @@ class TestStdCapture: print('\xa6') out, err = cap.readouterr() assert out == py.builtin._totext('\ufffd\n', 'unicode-escape') - cap.reset() + cap.stop_capturing() def test_reset_twice_error(self): cap = self.getcapture() print ("hello") - out, err = cap.reset() - pytest.raises(ValueError, cap.reset) + out, err = cap.readouterr() + cap.stop_capturing() + pytest.raises(ValueError, cap.stop_capturing) assert out == "hello\n" assert not err @@ -833,7 +832,8 @@ class TestStdCapture: sys.stderr = capture.TextIO() print ("not seen") sys.stderr.write("not seen\n") - out, err = cap.reset() + out, err = cap.readouterr() + cap.stop_capturing() assert out == "hello" assert err == "world" assert sys.stdout == oldout @@ -844,8 +844,10 @@ class TestStdCapture: print ("cap1") cap2 = self.getcapture() print ("cap2") - out2, err2 = cap2.reset() - out1, err1 = cap1.reset() + out2, err2 = cap2.readouterr() + out1, err1 = cap1.readouterr() + cap2.stop_capturing() + cap1.stop_capturing() assert out1 == "cap1\n" assert out2 == "cap2\n" @@ -853,7 +855,8 @@ class TestStdCapture: cap = self.getcapture(out=True, err=False) sys.stdout.write("hello") sys.stderr.write("world") - out, err = cap.reset() + out, err = cap.readouterr() + cap.stop_capturing() assert out == "hello" assert not err @@ -861,7 +864,8 @@ class TestStdCapture: cap = self.getcapture(out=False, err=True) sys.stdout.write("hello") sys.stderr.write("world") - out, err = cap.reset() + out, err = cap.readouterr() + cap.stop_capturing() assert err == "world" assert not out @@ -869,7 +873,7 @@ class TestStdCapture: old = sys.stdin cap = self.getcapture(in_=True) newstdin = sys.stdin - out, err = cap.reset() + cap.stop_capturing() assert newstdin != sys.stdin assert sys.stdin is old @@ -879,7 +883,7 @@ class TestStdCapture: print ("XXX mechanisms") cap = self.getcapture() pytest.raises(IOError, "sys.stdin.read()") - out, err = cap.reset() + cap.stop_capturing() class TestStdCaptureFD(TestStdCapture): @@ -890,6 +894,20 @@ class TestStdCaptureFD(TestStdCapture): cap.start_capturing() return cap + def test_simple_only_fd(self, testdir): + testdir.makepyfile(""" + import os + def test_x(): + os.write(1, "hello\\n".encode("ascii")) + assert 0 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(""" + *test_x* + *assert 0* + *Captured stdout* + """) + def test_intermingling(self): cap = self.getcapture() oswritebytes(1, "1") @@ -900,7 +918,8 @@ class TestStdCaptureFD(TestStdCapture): sys.stderr.write("b") sys.stderr.flush() oswritebytes(2, "c") - out, err = cap.reset() + out, err = cap.readouterr() + cap.stop_capturing() assert out == "123" assert err == "abc" @@ -908,7 +927,7 @@ class TestStdCaptureFD(TestStdCapture): with lsof_check(): for i in range(10): cap = StdCaptureFD() - cap.reset() + cap.stop_capturing() @@ -943,10 +962,6 @@ class TestStdCaptureFDinvalidFD: def test_capture_not_started_but_reset(): capsys = StdCapture() capsys.stop_capturing() - capsys.stop_capturing() - capsys.reset() - - @needsosdup @@ -960,7 +975,7 @@ def test_fdcapture_tmpfile_remains_the_same(tmpfile, use): capfile = cap.err.tmpfile cap.readouterr() finally: - cap.reset() + cap.stop_capturing() capfile2 = cap.err.tmpfile assert capfile2 == capfile From 859915dc5ea2a59d64b0af362891b25f87bcfb36 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 28 Mar 2014 07:13:08 +0100 Subject: [PATCH 10/11] simplify capturing funcarg handling --- _pytest/capture.py | 62 +++++++++++++++++----------------------------- 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/_pytest/capture.py b/_pytest/capture.py index 203044892..60771cdf6 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -155,29 +155,23 @@ class CaptureManager: def suspendcapture(self, item=None): self.deactivate_funcargs() - if hasattr(self, '_capturing'): - method = self._capturing - del self._capturing + method = self.__dict__.pop("_capturing", None) + if method is not None: cap = self._method2capture.get(method) if cap is not None: return cap.readouterr() return "", "" def activate_funcargs(self, pyfuncitem): - funcargs = getattr(pyfuncitem, "funcargs", None) - if funcargs is not None: - for name, capfuncarg in funcargs.items(): - if name in ('capsys', 'capfd'): - assert not hasattr(self, '_capturing_funcarg') - self._capturing_funcarg = capfuncarg - capfuncarg._start() + capfuncarg = pyfuncitem.__dict__.pop("_capfuncarg", None) + if capfuncarg is not None: + capfuncarg._start() + self._capfuncarg = capfuncarg def deactivate_funcargs(self): - capturing_funcarg = getattr(self, '_capturing_funcarg', None) - if capturing_funcarg: - outerr = capturing_funcarg._finalize() - del self._capturing_funcarg - return outerr + capfuncarg = self.__dict__.pop("_capfuncarg", None) + if capfuncarg is not None: + capfuncarg.close() @pytest.mark.hookwrapper def pytest_make_collect_report(self, __multicall__, collector): @@ -210,7 +204,9 @@ class CaptureManager: @pytest.mark.hookwrapper def pytest_runtest_call(self, item): with self.item_capture_wrapper(item, "call"): + self.activate_funcargs(item) yield + #self.deactivate_funcargs() called from ctx's suspendcapture() @pytest.mark.hookwrapper def pytest_runtest_teardown(self, item): @@ -228,17 +224,8 @@ class CaptureManager: @contextlib.contextmanager def item_capture_wrapper(self, item, when): self.resumecapture_item(item) - if when == "call": - self.activate_funcargs(item) - yield - funcarg_outerr = self.deactivate_funcargs() - else: - yield - funcarg_outerr = None + yield out, err = self.suspendcapture(item) - if funcarg_outerr is not None: - out += funcarg_outerr[0] - err += funcarg_outerr[1] item.add_report_section(when, "out", out) item.add_report_section(when, "err", err) @@ -252,7 +239,8 @@ def pytest_funcarg__capsys(request): """ if "capfd" in request._funcargs: raise request.raiseerror(error_capsysfderror) - return CaptureFixture(SysCapture) + request.node._capfuncarg = c = CaptureFixture(SysCapture) + return c def pytest_funcarg__capfd(request): """enables capturing of writes to file descriptors 1 and 2 and makes @@ -263,7 +251,8 @@ def pytest_funcarg__capfd(request): request.raiseerror(error_capsysfderror) if not hasattr(os, 'dup'): pytest.skip("capfd funcarg needs os.dup") - return CaptureFixture(FDCapture) + request.node._capfuncarg = c = CaptureFixture(FDCapture) + return c class CaptureFixture: @@ -275,21 +264,17 @@ class CaptureFixture: Capture=self.captureclass) self._capture.start_capturing() - def _finalize(self): - if hasattr(self, '_capture'): - outerr = self._outerr = self._capture.stop_capturing() - del self._capture - return outerr + def close(self): + cap = self.__dict__.pop("_capture", None) + if cap is not None: + cap.pop_outerr_to_orig() + cap.stop_capturing() def readouterr(self): try: return self._capture.readouterr() except AttributeError: - return self._outerr - - def close(self): - self._finalize() - + return "", "" def dupfile(f, mode=None, buffering=0, raising=False, encoding=None): @@ -448,8 +433,7 @@ class FDCapture: self.tmpfile.close() def writeorg(self, data): - """ write a string to the original file descriptor - """ + """ write to original file descriptor. """ if py.builtin._istext(data): data = data.encode("utf8") # XXX use encoding of original stream os.write(self._savefd, data) From 0b340aa1f698e96c1f1285f7dbae0155513c6913 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 28 Mar 2014 07:55:07 +0100 Subject: [PATCH 11/11] simplify some capturing tests --- testing/test_capture.py | 141 ++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 79 deletions(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index ef391b913..483f2f770 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -757,32 +757,35 @@ class TestFDCapture: class TestStdCapture: + captureclass = staticmethod(StdCapture) + + @contextlib.contextmanager def getcapture(self, **kw): - cap = StdCapture(**kw) + cap = self.__class__.captureclass(**kw) cap.start_capturing() - return cap + try: + yield cap + finally: + cap.stop_capturing() def test_capturing_done_simple(self): - cap = self.getcapture() - sys.stdout.write("hello") - sys.stderr.write("world") - out, err = cap.readouterr() - cap.stop_capturing() + with self.getcapture() as cap: + sys.stdout.write("hello") + sys.stderr.write("world") + out, err = cap.readouterr() assert out == "hello" assert err == "world" def test_capturing_reset_simple(self): - cap = self.getcapture() - print("hello world") - sys.stderr.write("hello error\n") - out, err = cap.readouterr() - cap.stop_capturing() + with self.getcapture() as cap: + print("hello world") + sys.stderr.write("hello error\n") + out, err = cap.readouterr() assert out == "hello world\n" assert err == "hello error\n" def test_capturing_readouterr(self): - cap = self.getcapture() - try: + with self.getcapture() as cap: print ("hello world") sys.stderr.write("hello error\n") out, err = cap.readouterr() @@ -790,34 +793,27 @@ class TestStdCapture: assert err == "hello error\n" sys.stderr.write("error2") out, err = cap.readouterr() - finally: - cap.stop_capturing() assert err == "error2" def test_capturing_readouterr_unicode(self): - cap = self.getcapture() - try: + with self.getcapture() as cap: print ("hx\xc4\x85\xc4\x87") out, err = cap.readouterr() - finally: - cap.stop_capturing() assert out == py.builtin._totext("hx\xc4\x85\xc4\x87\n", "utf8") @pytest.mark.skipif('sys.version_info >= (3,)', reason='text output different for bytes on python3') def test_capturing_readouterr_decode_error_handling(self): - cap = self.getcapture() - # triggered a internal error in pytest - print('\xa6') - out, err = cap.readouterr() + with self.getcapture() as cap: + # triggered a internal error in pytest + print('\xa6') + out, err = cap.readouterr() assert out == py.builtin._totext('\ufffd\n', 'unicode-escape') - cap.stop_capturing() def test_reset_twice_error(self): - cap = self.getcapture() - print ("hello") - out, err = cap.readouterr() - cap.stop_capturing() + with self.getcapture() as cap: + print ("hello") + out, err = cap.readouterr() pytest.raises(ValueError, cap.stop_capturing) assert out == "hello\n" assert not err @@ -825,55 +821,49 @@ class TestStdCapture: def test_capturing_modify_sysouterr_in_between(self): oldout = sys.stdout olderr = sys.stderr - cap = self.getcapture() - sys.stdout.write("hello") - sys.stderr.write("world") - sys.stdout = capture.TextIO() - sys.stderr = capture.TextIO() - print ("not seen") - sys.stderr.write("not seen\n") - out, err = cap.readouterr() - cap.stop_capturing() + with self.getcapture() as cap: + sys.stdout.write("hello") + sys.stderr.write("world") + sys.stdout = capture.TextIO() + sys.stderr = capture.TextIO() + print ("not seen") + sys.stderr.write("not seen\n") + out, err = cap.readouterr() assert out == "hello" assert err == "world" assert sys.stdout == oldout assert sys.stderr == olderr def test_capturing_error_recursive(self): - cap1 = self.getcapture() - print ("cap1") - cap2 = self.getcapture() - print ("cap2") - out2, err2 = cap2.readouterr() - out1, err1 = cap1.readouterr() - cap2.stop_capturing() - cap1.stop_capturing() + with self.getcapture() as cap1: + print ("cap1") + with self.getcapture() as cap2: + print ("cap2") + out2, err2 = cap2.readouterr() + out1, err1 = cap1.readouterr() assert out1 == "cap1\n" assert out2 == "cap2\n" def test_just_out_capture(self): - cap = self.getcapture(out=True, err=False) - sys.stdout.write("hello") - sys.stderr.write("world") - out, err = cap.readouterr() - cap.stop_capturing() + with self.getcapture(out=True, err=False) as cap: + sys.stdout.write("hello") + sys.stderr.write("world") + out, err = cap.readouterr() assert out == "hello" assert not err def test_just_err_capture(self): - cap = self.getcapture(out=False, err=True) - sys.stdout.write("hello") - sys.stderr.write("world") - out, err = cap.readouterr() - cap.stop_capturing() + with self.getcapture(out=False, err=True) as cap: + sys.stdout.write("hello") + sys.stderr.write("world") + out, err = cap.readouterr() assert err == "world" assert not out def test_stdin_restored(self): old = sys.stdin - cap = self.getcapture(in_=True) - newstdin = sys.stdin - cap.stop_capturing() + with self.getcapture(in_=True) as cap: + newstdin = sys.stdin assert newstdin != sys.stdin assert sys.stdin is old @@ -881,18 +871,13 @@ class TestStdCapture: print ("XXX this test may well hang instead of crashing") print ("XXX which indicates an error in the underlying capturing") print ("XXX mechanisms") - cap = self.getcapture() - pytest.raises(IOError, "sys.stdin.read()") - cap.stop_capturing() + with self.getcapture() as cap: + pytest.raises(IOError, "sys.stdin.read()") class TestStdCaptureFD(TestStdCapture): pytestmark = needsosdup - - def getcapture(self, **kw): - cap = StdCaptureFD(**kw) - cap.start_capturing() - return cap + captureclass = staticmethod(StdCaptureFD) def test_simple_only_fd(self, testdir): testdir.makepyfile(""" @@ -909,17 +894,16 @@ class TestStdCaptureFD(TestStdCapture): """) def test_intermingling(self): - cap = self.getcapture() - oswritebytes(1, "1") - sys.stdout.write(str(2)) - sys.stdout.flush() - oswritebytes(1, "3") - oswritebytes(2, "a") - sys.stderr.write("b") - sys.stderr.flush() - oswritebytes(2, "c") - out, err = cap.readouterr() - cap.stop_capturing() + with self.getcapture() as cap: + oswritebytes(1, "1") + sys.stdout.write(str(2)) + sys.stdout.flush() + oswritebytes(1, "3") + oswritebytes(2, "a") + sys.stderr.write("b") + sys.stderr.flush() + oswritebytes(2, "c") + out, err = cap.readouterr() assert out == "123" assert err == "abc" @@ -930,7 +914,6 @@ class TestStdCaptureFD(TestStdCapture): cap.stop_capturing() - class TestStdCaptureFDinvalidFD: pytestmark = needsosdup