diff --git a/AUTHORS b/AUTHORS index 3419accfa6b..1f138ffbe03 100644 --- a/AUTHORS +++ b/AUTHORS @@ -31,6 +31,7 @@ Andras Tim Andrea Cimatoribus Andreas Motl Andreas Zeidler +Andrew Pikul Andrew Shapton Andrey Paramonov Andrzej Klajnert diff --git a/changelog/12081.feature.rst b/changelog/12081.feature.rst new file mode 100644 index 00000000000..6538fbf30f8 --- /dev/null +++ b/changelog/12081.feature.rst @@ -0,0 +1 @@ +Added :fixture:`capteesys` to capture AND pass output to next handler set by ``--capture=``. diff --git a/doc/en/how-to/capture-stdout-stderr.rst b/doc/en/how-to/capture-stdout-stderr.rst index 9f7ddce3499..8f2a1a46680 100644 --- a/doc/en/how-to/capture-stdout-stderr.rst +++ b/doc/en/how-to/capture-stdout-stderr.rst @@ -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 --------------------------------------------------------- @@ -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: diff --git a/doc/en/reference/fixtures.rst b/doc/en/reference/fixtures.rst index dff93a035ef..566304d3330 100644 --- a/doc/en/reference/fixtures.rst +++ b/doc/en/reference/fixtures.rst @@ -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``. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 26572174ad4..78ff8384b9d 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -402,6 +402,16 @@ capsys .. autoclass:: pytest.CaptureFixture() :members: +.. fixture:: capteesys + +capteesys +~~~~~~~~~ + +**Tutorial**: :ref:`captures` + +.. autofunction:: _pytest.capture.capteesys() + :no-auto-options: + .. fixture:: capsysbinary capsysbinary diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index de2ee9c8dbd..3daa1fee232 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -904,11 +904,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 @@ -917,8 +919,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() @@ -1004,6 +1006,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] `. + + 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``. diff --git a/testing/test_capture.py b/testing/test_capture.py index 98986af6f1f..4572ac7f454 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -445,6 +445,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( """\