Skip to content

Commit 43657f2

Browse files
authored
Merge pull request #3830 from nicoddemus/capfd-fixture-capture
Fixtures during teardown can use capsys and capfd to get output from tests
2 parents d3bdfc7 + f4c5994 commit 43657f2

File tree

3 files changed

+56
-8
lines changed

3 files changed

+56
-8
lines changed

changelog/3033.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixtures during teardown can again use ``capsys`` and ``cafd`` to inspect output captured during tests.

src/_pytest/capture.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,9 @@ class CaptureFixture(object):
309309
def __init__(self, captureclass, request):
310310
self.captureclass = captureclass
311311
self.request = request
312+
self._capture = None
313+
self._captured_out = self.captureclass.EMPTY_BUFFER
314+
self._captured_err = self.captureclass.EMPTY_BUFFER
312315

313316
def _start(self):
314317
self._capture = MultiCapture(
@@ -317,20 +320,26 @@ def _start(self):
317320
self._capture.start_capturing()
318321

319322
def close(self):
320-
cap = self.__dict__.pop("_capture", None)
321-
if cap is not None:
322-
self._outerr = cap.pop_outerr_to_orig()
323-
cap.stop_capturing()
323+
if self._capture is not None:
324+
out, err = self._capture.pop_outerr_to_orig()
325+
self._captured_out += out
326+
self._captured_err += err
327+
self._capture.stop_capturing()
328+
self._capture = None
324329

325330
def readouterr(self):
326331
"""Read and return the captured output so far, resetting the internal buffer.
327332
328333
:return: captured content as a namedtuple with ``out`` and ``err`` string attributes
329334
"""
330-
try:
331-
return self._capture.readouterr()
332-
except AttributeError:
333-
return self._outerr
335+
captured_out, captured_err = self._captured_out, self._captured_err
336+
if self._capture is not None:
337+
out, err = self._capture.readouterr()
338+
captured_out += out
339+
captured_err += err
340+
self._captured_out = self.captureclass.EMPTY_BUFFER
341+
self._captured_err = self.captureclass.EMPTY_BUFFER
342+
return CaptureResult(captured_out, captured_err)
334343

335344
@contextlib.contextmanager
336345
def _suspend(self):
@@ -463,6 +472,7 @@ def readouterr(self):
463472

464473

465474
class NoCapture(object):
475+
EMPTY_BUFFER = None
466476
__init__ = start = done = suspend = resume = lambda *args: None
467477

468478

@@ -472,6 +482,8 @@ class FDCaptureBinary(object):
472482
snap() produces `bytes`
473483
"""
474484

485+
EMPTY_BUFFER = bytes()
486+
475487
def __init__(self, targetfd, tmpfile=None):
476488
self.targetfd = targetfd
477489
try:
@@ -545,6 +557,8 @@ class FDCapture(FDCaptureBinary):
545557
snap() produces text
546558
"""
547559

560+
EMPTY_BUFFER = str()
561+
548562
def snap(self):
549563
res = FDCaptureBinary.snap(self)
550564
enc = getattr(self.tmpfile, "encoding", None)
@@ -554,6 +568,9 @@ def snap(self):
554568

555569

556570
class SysCapture(object):
571+
572+
EMPTY_BUFFER = str()
573+
557574
def __init__(self, fd, tmpfile=None):
558575
name = patchsysdict[fd]
559576
self._old = getattr(sys, name)
@@ -591,6 +608,8 @@ def writeorg(self, data):
591608

592609

593610
class SysCaptureBinary(SysCapture):
611+
EMPTY_BUFFER = bytes()
612+
594613
def snap(self):
595614
res = self.tmpfile.buffer.getvalue()
596615
self.tmpfile.seek(0)

testing/test_capture.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,34 @@ def test_captured_print(captured_print):
647647
assert "stdout contents begin" not in result.stdout.str()
648648
assert "stderr contents begin" not in result.stdout.str()
649649

650+
@pytest.mark.parametrize("cap", ["capsys", "capfd"])
651+
def test_fixture_use_by_other_fixtures_teardown(self, testdir, cap):
652+
"""Ensure we can access setup and teardown buffers from teardown when using capsys/capfd (##3033)"""
653+
testdir.makepyfile(
654+
"""
655+
import sys
656+
import pytest
657+
import os
658+
659+
@pytest.fixture()
660+
def fix({cap}):
661+
print("setup out")
662+
sys.stderr.write("setup err\\n")
663+
yield
664+
out, err = {cap}.readouterr()
665+
assert out == 'setup out\\ncall out\\n'
666+
assert err == 'setup err\\ncall err\\n'
667+
668+
def test_a(fix):
669+
print("call out")
670+
sys.stderr.write("call err\\n")
671+
""".format(
672+
cap=cap
673+
)
674+
)
675+
reprec = testdir.inline_run()
676+
reprec.assertoutcome(passed=1)
677+
650678

651679
def test_setup_failure_does_not_kill_capturing(testdir):
652680
sub1 = testdir.mkpydir("sub1")

0 commit comments

Comments
 (0)