Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/3033.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixtures during teardown can again use ``capsys`` and ``cafd`` to inspect output captured during tests.
35 changes: 27 additions & 8 deletions src/_pytest/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,9 @@ class CaptureFixture(object):
def __init__(self, captureclass, request):
self.captureclass = captureclass
self.request = request
self._capture = None
self._captured_out = self.captureclass.EMPTY_BUFFER
self._captured_err = self.captureclass.EMPTY_BUFFER

def _start(self):
self._capture = MultiCapture(
Expand All @@ -317,20 +320,26 @@ def _start(self):
self._capture.start_capturing()

def close(self):
cap = self.__dict__.pop("_capture", None)
if cap is not None:
self._outerr = cap.pop_outerr_to_orig()
cap.stop_capturing()
if self._capture is not None:
out, err = self._capture.pop_outerr_to_orig()
self._captured_out += out
self._captured_err += err
self._capture.stop_capturing()
self._capture = None

def readouterr(self):
"""Read and return the captured output so far, resetting the internal buffer.

:return: captured content as a namedtuple with ``out`` and ``err`` string attributes
"""
try:
return self._capture.readouterr()
except AttributeError:
return self._outerr
captured_out, captured_err = self._captured_out, self._captured_err
if self._capture is not None:
out, err = self._capture.readouterr()
captured_out += out
captured_err += err
self._captured_out = self.captureclass.EMPTY_BUFFER
self._captured_err = self.captureclass.EMPTY_BUFFER
return CaptureResult(captured_out, captured_err)

@contextlib.contextmanager
def _suspend(self):
Expand Down Expand Up @@ -463,6 +472,7 @@ def readouterr(self):


class NoCapture(object):
EMPTY_BUFFER = None
__init__ = start = done = suspend = resume = lambda *args: None


Expand All @@ -472,6 +482,8 @@ class FDCaptureBinary(object):
snap() produces `bytes`
"""

EMPTY_BUFFER = bytes()

def __init__(self, targetfd, tmpfile=None):
self.targetfd = targetfd
try:
Expand Down Expand Up @@ -545,6 +557,8 @@ class FDCapture(FDCaptureBinary):
snap() produces text
"""

EMPTY_BUFFER = str()

def snap(self):
res = FDCaptureBinary.snap(self)
enc = getattr(self.tmpfile, "encoding", None)
Expand All @@ -554,6 +568,9 @@ def snap(self):


class SysCapture(object):

EMPTY_BUFFER = str()

def __init__(self, fd, tmpfile=None):
name = patchsysdict[fd]
self._old = getattr(sys, name)
Expand Down Expand Up @@ -591,6 +608,8 @@ def writeorg(self, data):


class SysCaptureBinary(SysCapture):
EMPTY_BUFFER = bytes()

def snap(self):
res = self.tmpfile.buffer.getvalue()
self.tmpfile.seek(0)
Expand Down
28 changes: 28 additions & 0 deletions testing/test_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,34 @@ def test_captured_print(captured_print):
assert "stdout contents begin" not in result.stdout.str()
assert "stderr contents begin" not in result.stdout.str()

@pytest.mark.parametrize("cap", ["capsys", "capfd"])
def test_fixture_use_by_other_fixtures_teardown(self, testdir, cap):
"""Ensure we can access setup and teardown buffers from teardown when using capsys/capfd (##3033)"""
testdir.makepyfile(
"""
import sys
import pytest
import os

@pytest.fixture()
def fix({cap}):
print("setup out")
sys.stderr.write("setup err\\n")
yield
out, err = {cap}.readouterr()
assert out == 'setup out\\ncall out\\n'
assert err == 'setup err\\ncall err\\n'

def test_a(fix):
print("call out")
sys.stderr.write("call err\\n")
""".format(
cap=cap
)
)
reprec = testdir.inline_run()
reprec.assertoutcome(passed=1)


def test_setup_failure_does_not_kill_capturing(testdir):
sub1 = testdir.mkpydir("sub1")
Expand Down