Merge pull request #2801 from nicoddemus/capture-fixture

Allow fixtures to use capsys and capfd
This commit is contained in:
Ronny Pfannschmidt 2017-10-09 15:26:50 +02:00 committed by GitHub
commit 9b0ce535c9
6 changed files with 153 additions and 63 deletions

View File

@ -43,7 +43,7 @@ def pytest_load_initial_conftests(early_config, parser, args):
pluginmanager.register(capman, "capturemanager") pluginmanager.register(capman, "capturemanager")
# make sure that capturemanager is properly reset at final shutdown # make sure that capturemanager is properly reset at final shutdown
early_config.add_cleanup(capman.reset_capturings) early_config.add_cleanup(capman.stop_global_capturing)
# make sure logging does not raise exceptions at the end # make sure logging does not raise exceptions at the end
def silence_logging_at_shutdown(): def silence_logging_at_shutdown():
@ -52,17 +52,30 @@ def pytest_load_initial_conftests(early_config, parser, args):
early_config.add_cleanup(silence_logging_at_shutdown) early_config.add_cleanup(silence_logging_at_shutdown)
# finally trigger conftest loading but while capturing (issue93) # finally trigger conftest loading but while capturing (issue93)
capman.init_capturings() capman.start_global_capturing()
outcome = yield outcome = yield
out, err = capman.suspendcapture() out, err = capman.suspend_global_capture()
if outcome.excinfo is not None: if outcome.excinfo is not None:
sys.stdout.write(out) sys.stdout.write(out)
sys.stderr.write(err) sys.stderr.write(err)
class CaptureManager: class CaptureManager:
"""
Capture plugin, manages that the appropriate capture method is enabled/disabled during collection and each
test phase (setup, call, teardown). After each of those points, the captured output is obtained and
attached to the collection/runtest report.
There are two levels of capture:
* global: which is enabled by default and can be suppressed by the ``-s`` option. This is always enabled/disabled
during collection and each test phase.
* fixture: when a test function or one of its fixture depend on the ``capsys`` or ``capfd`` fixtures. In this
case special handling is needed to ensure the fixtures take precedence over the global capture.
"""
def __init__(self, method): def __init__(self, method):
self._method = method self._method = method
self._global_capturing = None
def _getcapture(self, method): def _getcapture(self, method):
if method == "fd": if method == "fd":
@ -74,23 +87,24 @@ class CaptureManager:
else: else:
raise ValueError("unknown capturing method: %r" % method) raise ValueError("unknown capturing method: %r" % method)
def init_capturings(self): def start_global_capturing(self):
assert not hasattr(self, "_capturing") assert self._global_capturing is None
self._capturing = self._getcapture(self._method) self._global_capturing = self._getcapture(self._method)
self._capturing.start_capturing() self._global_capturing.start_capturing()
def reset_capturings(self): def stop_global_capturing(self):
cap = self.__dict__.pop("_capturing", None) if self._global_capturing is not None:
if cap is not None: self._global_capturing.pop_outerr_to_orig()
cap.pop_outerr_to_orig() self._global_capturing.stop_capturing()
cap.stop_capturing() self._global_capturing = None
def resumecapture(self): def resume_global_capture(self):
self._capturing.resume_capturing() self._global_capturing.resume_capturing()
def suspendcapture(self, in_=False): def suspend_global_capture(self, item=None, in_=False):
self.deactivate_funcargs() if item is not None:
cap = getattr(self, "_capturing", None) self.deactivate_fixture(item)
cap = getattr(self, "_global_capturing", None)
if cap is not None: if cap is not None:
try: try:
outerr = cap.readouterr() outerr = cap.readouterr()
@ -98,23 +112,26 @@ class CaptureManager:
cap.suspend_capturing(in_=in_) cap.suspend_capturing(in_=in_)
return outerr return outerr
def activate_funcargs(self, pyfuncitem): def activate_fixture(self, item):
capfuncarg = pyfuncitem.__dict__.pop("_capfuncarg", None) """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over
if capfuncarg is not None: the global capture.
capfuncarg._start() """
self._capfuncarg = capfuncarg fixture = getattr(item, "_capture_fixture", None)
if fixture is not None:
fixture._start()
def deactivate_funcargs(self): def deactivate_fixture(self, item):
capfuncarg = self.__dict__.pop("_capfuncarg", None) """Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any."""
if capfuncarg is not None: fixture = getattr(item, "_capture_fixture", None)
capfuncarg.close() if fixture is not None:
fixture.close()
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector): def pytest_make_collect_report(self, collector):
if isinstance(collector, pytest.File): if isinstance(collector, pytest.File):
self.resumecapture() self.resume_global_capture()
outcome = yield outcome = yield
out, err = self.suspendcapture() out, err = self.suspend_global_capture()
rep = outcome.get_result() rep = outcome.get_result()
if out: if out:
rep.sections.append(("Captured stdout", out)) rep.sections.append(("Captured stdout", out))
@ -125,34 +142,39 @@ class CaptureManager:
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_setup(self, item): def pytest_runtest_setup(self, item):
self.resumecapture() self.resume_global_capture()
# no need to activate a capture fixture because they activate themselves during creation; this
# only makes sense when a fixture uses a capture fixture, otherwise the capture fixture will
# be activated during pytest_runtest_call
yield yield
self.suspendcapture_item(item, "setup") self.suspend_capture_item(item, "setup")
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(self, item): def pytest_runtest_call(self, item):
self.resumecapture() self.resume_global_capture()
self.activate_funcargs(item) # it is important to activate this fixture during the call phase so it overwrites the "global"
# capture
self.activate_fixture(item)
yield yield
# self.deactivate_funcargs() called from suspendcapture() self.suspend_capture_item(item, "call")
self.suspendcapture_item(item, "call")
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_teardown(self, item): def pytest_runtest_teardown(self, item):
self.resumecapture() self.resume_global_capture()
self.activate_fixture(item)
yield yield
self.suspendcapture_item(item, "teardown") self.suspend_capture_item(item, "teardown")
@pytest.hookimpl(tryfirst=True) @pytest.hookimpl(tryfirst=True)
def pytest_keyboard_interrupt(self, excinfo): def pytest_keyboard_interrupt(self, excinfo):
self.reset_capturings() self.stop_global_capturing()
@pytest.hookimpl(tryfirst=True) @pytest.hookimpl(tryfirst=True)
def pytest_internalerror(self, excinfo): def pytest_internalerror(self, excinfo):
self.reset_capturings() self.stop_global_capturing()
def suspendcapture_item(self, item, when, in_=False): def suspend_capture_item(self, item, when, in_=False):
out, err = self.suspendcapture(in_=in_) out, err = self.suspend_global_capture(item, in_=in_)
item.add_report_section(when, "stdout", out) item.add_report_section(when, "stdout", out)
item.add_report_section(when, "stderr", err) item.add_report_section(when, "stderr", err)
@ -168,8 +190,8 @@ def capsys(request):
""" """
if "capfd" in request.fixturenames: if "capfd" in request.fixturenames:
raise request.raiseerror(error_capsysfderror) raise request.raiseerror(error_capsysfderror)
request.node._capfuncarg = c = CaptureFixture(SysCapture, request) with _install_capture_fixture_on_item(request, SysCapture) as fixture:
return c yield fixture
@pytest.fixture @pytest.fixture
@ -181,9 +203,29 @@ def capfd(request):
if "capsys" in request.fixturenames: if "capsys" in request.fixturenames:
request.raiseerror(error_capsysfderror) request.raiseerror(error_capsysfderror)
if not hasattr(os, 'dup'): if not hasattr(os, 'dup'):
pytest.skip("capfd funcarg needs os.dup") pytest.skip("capfd fixture needs os.dup function which is not available in this system")
request.node._capfuncarg = c = CaptureFixture(FDCapture, request) with _install_capture_fixture_on_item(request, FDCapture) as fixture:
return c yield fixture
@contextlib.contextmanager
def _install_capture_fixture_on_item(request, capture_class):
"""
Context manager which creates a ``CaptureFixture`` instance and "installs" it on
the item/node of the given request. Used by ``capsys`` and ``capfd``.
The CaptureFixture is added as attribute of the item because it needs to accessed
by ``CaptureManager`` during its ``pytest_runtest_*`` hooks.
"""
request.node._capture_fixture = fixture = CaptureFixture(capture_class, request)
capmanager = request.config.pluginmanager.getplugin('capturemanager')
# need to active this fixture right away in case it is being used by another fixture (setup phase)
# if this fixture is being used only by a test function (call phase), then we wouldn't need this
# activation, but it doesn't hurt
capmanager.activate_fixture(request.node)
yield fixture
fixture.close()
del request.node._capture_fixture
class CaptureFixture: class CaptureFixture:
@ -210,12 +252,14 @@ class CaptureFixture:
@contextlib.contextmanager @contextlib.contextmanager
def disabled(self): def disabled(self):
self._capture.suspend_capturing()
capmanager = self.request.config.pluginmanager.getplugin('capturemanager') capmanager = self.request.config.pluginmanager.getplugin('capturemanager')
capmanager.suspendcapture_item(self.request.node, "call", in_=True) capmanager.suspend_global_capture(item=None, in_=False)
try: try:
yield yield
finally: finally:
capmanager.resumecapture() capmanager.resume_global_capture()
self._capture.resume_capturing()
def safe_text_dupfile(f, mode, default_encoding="UTF8"): def safe_text_dupfile(f, mode, default_encoding="UTF8"):

View File

@ -54,7 +54,7 @@ class pytestPDB:
if cls._pluginmanager is not None: if cls._pluginmanager is not None:
capman = cls._pluginmanager.getplugin("capturemanager") capman = cls._pluginmanager.getplugin("capturemanager")
if capman: if capman:
capman.suspendcapture(in_=True) capman.suspend_global_capture(in_=True)
tw = _pytest.config.create_terminal_writer(cls._config) tw = _pytest.config.create_terminal_writer(cls._config)
tw.line() tw.line()
tw.sep(">", "PDB set_trace (IO-capturing turned off)") tw.sep(">", "PDB set_trace (IO-capturing turned off)")
@ -66,7 +66,7 @@ class PdbInvoke:
def pytest_exception_interact(self, node, call, report): def pytest_exception_interact(self, node, call, report):
capman = node.config.pluginmanager.getplugin("capturemanager") capman = node.config.pluginmanager.getplugin("capturemanager")
if capman: if capman:
out, err = capman.suspendcapture(in_=True) out, err = capman.suspend_global_capture(in_=True)
sys.stdout.write(out) sys.stdout.write(out)
sys.stdout.write(err) sys.stdout.write(err)
_enter_pdb(node, call.excinfo, report) _enter_pdb(node, call.excinfo, report)

View File

@ -44,7 +44,7 @@ def _show_fixture_action(fixturedef, msg):
config = fixturedef._fixturemanager.config config = fixturedef._fixturemanager.config
capman = config.pluginmanager.getplugin('capturemanager') capman = config.pluginmanager.getplugin('capturemanager')
if capman: if capman:
out, err = capman.suspendcapture() out, err = capman.suspend_global_capture()
tw = config.get_terminal_writer() tw = config.get_terminal_writer()
tw.line() tw.line()
@ -63,7 +63,7 @@ def _show_fixture_action(fixturedef, msg):
tw.write('[{0}]'.format(fixturedef.cached_param)) tw.write('[{0}]'.format(fixturedef.cached_param))
if capman: if capman:
capman.resumecapture() capman.resume_global_capture()
sys.stdout.write(out) sys.stdout.write(out)
sys.stderr.write(err) sys.stderr.write(err)

1
changelog/1993.bugfix Normal file
View File

@ -0,0 +1 @@
Resume output capturing after ``capsys/capfd.disabled()`` context manager.

1
changelog/2709.bugfix Normal file
View File

@ -0,0 +1 @@
``capsys`` and ``capfd`` can now be used by other fixtures.

View File

@ -78,23 +78,23 @@ class TestCaptureManager(object):
old = sys.stdout, sys.stderr, sys.stdin old = sys.stdout, sys.stderr, sys.stdin
try: try:
capman = CaptureManager(method) capman = CaptureManager(method)
capman.init_capturings() capman.start_global_capturing()
outerr = capman.suspendcapture() outerr = capman.suspend_global_capture()
assert outerr == ("", "") assert outerr == ("", "")
outerr = capman.suspendcapture() outerr = capman.suspend_global_capture()
assert outerr == ("", "") assert outerr == ("", "")
print("hello") print("hello")
out, err = capman.suspendcapture() out, err = capman.suspend_global_capture()
if method == "no": if method == "no":
assert old == (sys.stdout, sys.stderr, sys.stdin) assert old == (sys.stdout, sys.stderr, sys.stdin)
else: else:
assert not out assert not out
capman.resumecapture() capman.resume_global_capture()
print("hello") print("hello")
out, err = capman.suspendcapture() out, err = capman.suspend_global_capture()
if method != "no": if method != "no":
assert out == "hello\n" assert out == "hello\n"
capman.reset_capturings() capman.stop_global_capturing()
finally: finally:
capouter.stop_capturing() capouter.stop_capturing()
@ -103,9 +103,9 @@ class TestCaptureManager(object):
capouter = StdCaptureFD() capouter = StdCaptureFD()
try: try:
capman = CaptureManager("fd") capman = CaptureManager("fd")
capman.init_capturings() capman.start_global_capturing()
pytest.raises(AssertionError, "capman.init_capturings()") pytest.raises(AssertionError, "capman.start_global_capturing()")
capman.reset_capturings() capman.stop_global_capturing()
finally: finally:
capouter.stop_capturing() capouter.stop_capturing()
@ -502,20 +502,64 @@ class TestCaptureFixture(object):
assert 'closed' not in result.stderr.str() assert 'closed' not in result.stderr.str()
@pytest.mark.parametrize('fixture', ['capsys', 'capfd']) @pytest.mark.parametrize('fixture', ['capsys', 'capfd'])
def test_disabled_capture_fixture(self, testdir, fixture): @pytest.mark.parametrize('no_capture', [True, False])
def test_disabled_capture_fixture(self, testdir, fixture, no_capture):
testdir.makepyfile(""" testdir.makepyfile("""
def test_disabled({fixture}): def test_disabled({fixture}):
print('captured before') print('captured before')
with {fixture}.disabled(): with {fixture}.disabled():
print('while capture is disabled') print('while capture is disabled')
print('captured after') print('captured after')
assert {fixture}.readouterr() == ('captured before\\ncaptured after\\n', '')
def test_normal():
print('test_normal executed')
""".format(fixture=fixture)) """.format(fixture=fixture))
result = testdir.runpytest_subprocess() args = ('-s',) if no_capture else ()
result = testdir.runpytest_subprocess(*args)
result.stdout.fnmatch_lines(""" result.stdout.fnmatch_lines("""
*while capture is disabled* *while capture is disabled*
""") """)
assert 'captured before' not in result.stdout.str() assert 'captured before' not in result.stdout.str()
assert 'captured after' not in result.stdout.str() assert 'captured after' not in result.stdout.str()
if no_capture:
assert 'test_normal executed' in result.stdout.str()
else:
assert 'test_normal executed' not in result.stdout.str()
@pytest.mark.parametrize('fixture', ['capsys', 'capfd'])
def test_fixture_use_by_other_fixtures(self, testdir, fixture):
"""
Ensure that capsys and capfd can be used by other fixtures during setup and teardown.
"""
testdir.makepyfile("""
from __future__ import print_function
import sys
import pytest
@pytest.fixture
def captured_print({fixture}):
print('stdout contents begin')
print('stderr contents begin', file=sys.stderr)
out, err = {fixture}.readouterr()
yield out, err
print('stdout contents end')
print('stderr contents end', file=sys.stderr)
out, err = {fixture}.readouterr()
assert out == 'stdout contents end\\n'
assert err == 'stderr contents end\\n'
def test_captured_print(captured_print):
out, err = captured_print
assert out == 'stdout contents begin\\n'
assert err == 'stderr contents begin\\n'
""".format(fixture=fixture))
result = testdir.runpytest_subprocess()
result.stdout.fnmatch_lines("*1 passed*")
assert 'stdout contents begin' not in result.stdout.str()
assert 'stderr contents begin' not in result.stdout.str()
def test_setup_failure_does_not_kill_capturing(testdir): def test_setup_failure_does_not_kill_capturing(testdir):