Skip to content

Commit

Permalink
Add capteesys capture fixture to bubble up output to --capture hand…
Browse files Browse the repository at this point in the history
…ler (#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.
  • Loading branch information
ayjayt authored Mar 1, 2025
1 parent 0646383 commit d3adf46
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 4 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Andras Tim
Andrea Cimatoribus
Andreas Motl
Andreas Zeidler
Andrew Pikul
Andrew Shapton
Andrey Paramonov
Andrzej Klajnert
Expand Down
1 change: 1 addition & 0 deletions changelog/12081.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added :fixture:`capteesys` to capture AND pass output to next handler set by ``--capture=``.
10 changes: 8 additions & 2 deletions doc/en/how-to/capture-stdout-stderr.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
How to capture stdout/stderr output
=========================================================

Pytest intercepts stdout and stderr as configured by the ``--capture=``
command-line argument or by using fixtures. The ``--capture=`` flag configures
reporting, whereas the fixtures offer more granular control and allows
inspection of output during testing. The reports can be customized with the
`-r flag <../reference/reference.html#command-line-flags>`_.

Default stdout/stderr/stdin capturing behaviour
---------------------------------------------------------

Expand Down Expand Up @@ -106,8 +112,8 @@ of the failing function and hide the other one:
Accessing captured output from a test function
---------------------------------------------------

The :fixture:`capsys`, :fixture:`capsysbinary`, :fixture:`capfd`, and :fixture:`capfdbinary` fixtures
allow access to ``stdout``/``stderr`` output created during test execution.
The :fixture:`capsys`, :fixture:`capteesys`, :fixture:`capsysbinary`, :fixture:`capfd`, and :fixture:`capfdbinary`
fixtures allow access to ``stdout``/``stderr`` output created during test execution.

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

Expand Down
4 changes: 4 additions & 0 deletions doc/en/reference/fixtures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ Built-in fixtures
:fixture:`capsys`
Capture, as text, output to ``sys.stdout`` and ``sys.stderr``.

:fixture:`capteesys`
Capture in the same manner as :fixture:`capsys`, but also pass text
through according to ``--capture=``.

:fixture:`capsysbinary`
Capture, as bytes, output to ``sys.stdout`` and ``sys.stderr``.

Expand Down
10 changes: 10 additions & 0 deletions doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,16 @@ capsys
.. autoclass:: pytest.CaptureFixture()
:members:

.. fixture:: capteesys

capteesys
~~~~~~~~~

**Tutorial**: :ref:`captures`

.. autofunction:: _pytest.capture.capteesys()
:no-auto-options:

.. fixture:: capsysbinary

capsysbinary
Expand Down
41 changes: 39 additions & 2 deletions src/_pytest/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -922,11 +922,13 @@ def __init__(
captureclass: type[CaptureBase[AnyStr]],
request: SubRequest,
*,
config: dict[str, Any] | None = None,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self.captureclass: type[CaptureBase[AnyStr]] = captureclass
self.request = request
self._config = config if config else {}
self._capture: MultiCapture[AnyStr] | None = None
self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
Expand All @@ -935,8 +937,8 @@ def _start(self) -> None:
if self._capture is None:
self._capture = MultiCapture(
in_=None,
out=self.captureclass(1),
err=self.captureclass(2),
out=self.captureclass(1, **self._config),
err=self.captureclass(2, **self._config),
)
self._capture.start_capturing()

Expand Down Expand Up @@ -1022,6 +1024,41 @@ def test_output(capsys):
capman.unset_fixture()


@fixture
def capteesys(request: SubRequest) -> Generator[CaptureFixture[str]]:
r"""Enable simultaneous text capturing and pass-through of writes
to ``sys.stdout`` and ``sys.stderr`` as defined by ``--capture=``.
The captured output is made available via ``capteesys.readouterr()`` method
calls, which return a ``(out, err)`` namedtuple.
``out`` and ``err`` will be ``text`` objects.
The output is also passed-through, allowing it to be "live-printed",
reported, or both as defined by ``--capture=``.
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsys):
print("hello")
captured = capteesys.readouterr()
assert captured.out == "hello\n"
"""
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture(
SysCapture, request, config=dict(tee=True), _ispytest=True
)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
capture_fixture.close()
capman.unset_fixture()


@fixture
def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]:
r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
Expand Down
32 changes: 32 additions & 0 deletions testing/test_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,38 @@ def test_hello(capsys):
)
reprec.assertoutcome(passed=1)

def test_capteesys(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""\
import sys
def test_one(capteesys):
print("sTdoUt")
print("sTdeRr", file=sys.stderr)
out, err = capteesys.readouterr()
assert out == "sTdoUt\\n"
assert err == "sTdeRr\\n"
"""
)
# -rN and --capture=tee-sys means we'll read them on stdout/stderr,
# as opposed to both being reported on stdout
result = pytester.runpytest(p, "--quiet", "--quiet", "-rN", "--capture=tee-sys")
assert result.ret == ExitCode.OK
result.stdout.fnmatch_lines(["sTdoUt"]) # tee'd out
result.stderr.fnmatch_lines(["sTdeRr"]) # tee'd out

result = pytester.runpytest(p, "--quiet", "--quiet", "-rA", "--capture=tee-sys")
assert result.ret == ExitCode.OK
result.stdout.fnmatch_lines(
["sTdoUt", "sTdoUt", "sTdeRr"]
) # tee'd out, the next two reported
result.stderr.fnmatch_lines(["sTdeRr"]) # tee'd out

# -rA and --capture=sys means we'll read them on stdout.
result = pytester.runpytest(p, "--quiet", "--quiet", "-rA", "--capture=sys")
assert result.ret == ExitCode.OK
result.stdout.fnmatch_lines(["sTdoUt", "sTdeRr"]) # no tee, just reported
assert not result.stderr.lines

def test_capsyscapfd(self, pytester: Pytester) -> None:
p = pytester.makepyfile(
"""\
Expand Down

0 comments on commit d3adf46

Please sign in to comment.