Skip to content

Commit d3adf46

Browse files
authored
Add capteesys capture fixture to bubble up output to --capture handler (#12854)
The config dict is passed alongside the class that the fixture will eventually initialize. It can use the config dict for optional arguments to the implementation's constructor.
1 parent 0646383 commit d3adf46

File tree

7 files changed

+95
-4
lines changed

7 files changed

+95
-4
lines changed

Diff for: AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Andras Tim
3232
Andrea Cimatoribus
3333
Andreas Motl
3434
Andreas Zeidler
35+
Andrew Pikul
3536
Andrew Shapton
3637
Andrey Paramonov
3738
Andrzej Klajnert

Diff for: changelog/12081.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added :fixture:`capteesys` to capture AND pass output to next handler set by ``--capture=``.

Diff for: doc/en/how-to/capture-stdout-stderr.rst

+8-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
How to capture stdout/stderr output
55
=========================================================
66

7+
Pytest intercepts stdout and stderr as configured by the ``--capture=``
8+
command-line argument or by using fixtures. The ``--capture=`` flag configures
9+
reporting, whereas the fixtures offer more granular control and allows
10+
inspection of output during testing. The reports can be customized with the
11+
`-r flag <../reference/reference.html#command-line-flags>`_.
12+
713
Default stdout/stderr/stdin capturing behaviour
814
---------------------------------------------------------
915

@@ -106,8 +112,8 @@ of the failing function and hide the other one:
106112
Accessing captured output from a test function
107113
---------------------------------------------------
108114

109-
The :fixture:`capsys`, :fixture:`capsysbinary`, :fixture:`capfd`, and :fixture:`capfdbinary` fixtures
110-
allow access to ``stdout``/``stderr`` output created during test execution.
115+
The :fixture:`capsys`, :fixture:`capteesys`, :fixture:`capsysbinary`, :fixture:`capfd`, and :fixture:`capfdbinary`
116+
fixtures allow access to ``stdout``/``stderr`` output created during test execution.
111117

112118
Here is an example test function that performs some output related checks:
113119

Diff for: doc/en/reference/fixtures.rst

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ Built-in fixtures
3232
:fixture:`capsys`
3333
Capture, as text, output to ``sys.stdout`` and ``sys.stderr``.
3434

35+
:fixture:`capteesys`
36+
Capture in the same manner as :fixture:`capsys`, but also pass text
37+
through according to ``--capture=``.
38+
3539
:fixture:`capsysbinary`
3640
Capture, as bytes, output to ``sys.stdout`` and ``sys.stderr``.
3741

Diff for: doc/en/reference/reference.rst

+10
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,16 @@ capsys
412412
.. autoclass:: pytest.CaptureFixture()
413413
:members:
414414

415+
.. fixture:: capteesys
416+
417+
capteesys
418+
~~~~~~~~~
419+
420+
**Tutorial**: :ref:`captures`
421+
422+
.. autofunction:: _pytest.capture.capteesys()
423+
:no-auto-options:
424+
415425
.. fixture:: capsysbinary
416426

417427
capsysbinary

Diff for: src/_pytest/capture.py

+39-2
Original file line numberDiff line numberDiff line change
@@ -922,11 +922,13 @@ def __init__(
922922
captureclass: type[CaptureBase[AnyStr]],
923923
request: SubRequest,
924924
*,
925+
config: dict[str, Any] | None = None,
925926
_ispytest: bool = False,
926927
) -> None:
927928
check_ispytest(_ispytest)
928929
self.captureclass: type[CaptureBase[AnyStr]] = captureclass
929930
self.request = request
931+
self._config = config if config else {}
930932
self._capture: MultiCapture[AnyStr] | None = None
931933
self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
932934
self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
@@ -935,8 +937,8 @@ def _start(self) -> None:
935937
if self._capture is None:
936938
self._capture = MultiCapture(
937939
in_=None,
938-
out=self.captureclass(1),
939-
err=self.captureclass(2),
940+
out=self.captureclass(1, **self._config),
941+
err=self.captureclass(2, **self._config),
940942
)
941943
self._capture.start_capturing()
942944

@@ -1022,6 +1024,41 @@ def test_output(capsys):
10221024
capman.unset_fixture()
10231025

10241026

1027+
@fixture
1028+
def capteesys(request: SubRequest) -> Generator[CaptureFixture[str]]:
1029+
r"""Enable simultaneous text capturing and pass-through of writes
1030+
to ``sys.stdout`` and ``sys.stderr`` as defined by ``--capture=``.
1031+
1032+
1033+
The captured output is made available via ``capteesys.readouterr()`` method
1034+
calls, which return a ``(out, err)`` namedtuple.
1035+
``out`` and ``err`` will be ``text`` objects.
1036+
1037+
The output is also passed-through, allowing it to be "live-printed",
1038+
reported, or both as defined by ``--capture=``.
1039+
1040+
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
1041+
1042+
Example:
1043+
1044+
.. code-block:: python
1045+
1046+
def test_output(capsys):
1047+
print("hello")
1048+
captured = capteesys.readouterr()
1049+
assert captured.out == "hello\n"
1050+
"""
1051+
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
1052+
capture_fixture = CaptureFixture(
1053+
SysCapture, request, config=dict(tee=True), _ispytest=True
1054+
)
1055+
capman.set_fixture(capture_fixture)
1056+
capture_fixture._start()
1057+
yield capture_fixture
1058+
capture_fixture.close()
1059+
capman.unset_fixture()
1060+
1061+
10251062
@fixture
10261063
def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]:
10271064
r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.

Diff for: testing/test_capture.py

+32
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,38 @@ def test_hello(capsys):
446446
)
447447
reprec.assertoutcome(passed=1)
448448

449+
def test_capteesys(self, pytester: Pytester) -> None:
450+
p = pytester.makepyfile(
451+
"""\
452+
import sys
453+
def test_one(capteesys):
454+
print("sTdoUt")
455+
print("sTdeRr", file=sys.stderr)
456+
out, err = capteesys.readouterr()
457+
assert out == "sTdoUt\\n"
458+
assert err == "sTdeRr\\n"
459+
"""
460+
)
461+
# -rN and --capture=tee-sys means we'll read them on stdout/stderr,
462+
# as opposed to both being reported on stdout
463+
result = pytester.runpytest(p, "--quiet", "--quiet", "-rN", "--capture=tee-sys")
464+
assert result.ret == ExitCode.OK
465+
result.stdout.fnmatch_lines(["sTdoUt"]) # tee'd out
466+
result.stderr.fnmatch_lines(["sTdeRr"]) # tee'd out
467+
468+
result = pytester.runpytest(p, "--quiet", "--quiet", "-rA", "--capture=tee-sys")
469+
assert result.ret == ExitCode.OK
470+
result.stdout.fnmatch_lines(
471+
["sTdoUt", "sTdoUt", "sTdeRr"]
472+
) # tee'd out, the next two reported
473+
result.stderr.fnmatch_lines(["sTdeRr"]) # tee'd out
474+
475+
# -rA and --capture=sys means we'll read them on stdout.
476+
result = pytester.runpytest(p, "--quiet", "--quiet", "-rA", "--capture=sys")
477+
assert result.ret == ExitCode.OK
478+
result.stdout.fnmatch_lines(["sTdoUt", "sTdeRr"]) # no tee, just reported
479+
assert not result.stderr.lines
480+
449481
def test_capsyscapfd(self, pytester: Pytester) -> None:
450482
p = pytester.makepyfile(
451483
"""\

0 commit comments

Comments
 (0)