Include new --capture-mode=tee-sys option

Fix #4597
This commit is contained in:
cmachalo 2019-12-03 14:15:13 -08:00 committed by Bruno Oliveira
parent 30f2729684
commit e13ad22364
7 changed files with 111 additions and 8 deletions

View File

@ -52,6 +52,7 @@ Carl Friedrich Bolz
Carlos Jenkins
Ceridwen
Charles Cloud
Charles Machalow
Charnjit SiNGH (CCSJ)
Chris Lamb
Christian Boelsen

View File

@ -0,0 +1 @@
New :ref:`--capture=tee-sys <capture-method>` option to allow both live printing and capturing of test output.

View File

@ -21,27 +21,36 @@ file descriptors. This allows to capture output from simple
print statements as well as output from a subprocess started by
a test.
.. _capture-method:
Setting capturing methods or disabling capturing
-------------------------------------------------
There are two ways in which ``pytest`` can perform capturing:
There are three ways in which ``pytest`` can perform capturing:
* file descriptor (FD) level capturing (default): All writes going to the
* ``fd`` (file descriptor) level capturing (default): All writes going to the
operating system file descriptors 1 and 2 will be captured.
* ``sys`` level capturing: Only writes to Python files ``sys.stdout``
and ``sys.stderr`` will be captured. No capturing of writes to
filedescriptors is performed.
* ``tee-sys`` capturing: Python writes to ``sys.stdout`` and ``sys.stderr``
will be captured, however the writes will also be passed-through to
the actual ``sys.stdout`` and ``sys.stderr``. This allows output to be
'live printed' and captured for plugin use, such as junitxml (new in pytest 5.4).
.. _`disable capturing`:
You can influence output capturing mechanisms from the command line:
.. code-block:: bash
pytest -s # disable all capturing
pytest --capture=sys # replace sys.stdout/stderr with in-mem files
pytest --capture=fd # also point filedescriptors 1 and 2 to temp file
pytest -s # disable all capturing
pytest --capture=sys # replace sys.stdout/stderr with in-mem files
pytest --capture=fd # also point filedescriptors 1 and 2 to temp file
pytest --capture=tee-sys # combines 'sys' and '-s', capturing sys.stdout/stderr
# and passing it along to the actual sys.stdout/stderr
.. _printdebugging:

View File

@ -11,6 +11,7 @@ from io import UnsupportedOperation
from tempfile import TemporaryFile
import pytest
from _pytest.compat import CaptureAndPassthroughIO
from _pytest.compat import CaptureIO
from _pytest.fixtures import FixtureRequest
@ -24,8 +25,8 @@ def pytest_addoption(parser):
action="store",
default="fd" if hasattr(os, "dup") else "sys",
metavar="method",
choices=["fd", "sys", "no"],
help="per-test capturing method: one of fd|sys|no.",
choices=["fd", "sys", "no", "tee-sys"],
help="per-test capturing method: one of fd|sys|no|tee-sys.",
)
group._addoption(
"-s",
@ -90,6 +91,8 @@ class CaptureManager:
return MultiCapture(out=True, err=True, Capture=SysCapture)
elif method == "no":
return MultiCapture(out=False, err=False, in_=False)
elif method == "tee-sys":
return MultiCapture(out=True, err=True, in_=False, Capture=TeeSysCapture)
raise ValueError("unknown capturing method: %r" % method) # pragma: no cover
def is_capturing(self):
@ -681,6 +684,19 @@ class SysCapture:
self._old.flush()
class TeeSysCapture(SysCapture):
def __init__(self, fd, tmpfile=None):
name = patchsysdict[fd]
self._old = getattr(sys, name)
self.name = name
if tmpfile is None:
if name == "stdin":
tmpfile = DontReadFromInput()
else:
tmpfile = CaptureAndPassthroughIO(self._old)
self.tmpfile = tmpfile
class SysCaptureBinary(SysCapture):
# Ignore type because it doesn't match the type in the superclass (str).
EMPTY_BUFFER = b"" # type: ignore

View File

@ -13,6 +13,7 @@ from inspect import signature
from typing import Any
from typing import Callable
from typing import Generic
from typing import IO
from typing import Optional
from typing import overload
from typing import Tuple
@ -371,6 +372,16 @@ class CaptureIO(io.TextIOWrapper):
return self.buffer.getvalue().decode("UTF-8")
class CaptureAndPassthroughIO(CaptureIO):
def __init__(self, other: IO) -> None:
self._other = other
super().__init__()
def write(self, s) -> int:
super().write(s)
return self._other.write(s)
if sys.version_info < (3, 5, 2): # pragma: no cover
def overload(f): # noqa: F811

View File

@ -1285,3 +1285,28 @@ def test_pdb_can_be_rewritten(testdir):
]
)
assert result.ret == 1
def test_tee_stdio_captures_and_live_prints(testdir):
testpath = testdir.makepyfile(
"""
import sys
def test_simple():
print ("@this is stdout@")
print ("@this is stderr@", file=sys.stderr)
"""
)
result = testdir.runpytest_subprocess(
testpath, "--capture=tee-sys", "--junitxml=output.xml"
)
# ensure stdout/stderr were 'live printed'
result.stdout.fnmatch_lines(["*@this is stdout@*"])
result.stderr.fnmatch_lines(["*@this is stderr@*"])
# now ensure the output is in the junitxml
with open(os.path.join(testdir.tmpdir.strpath, "output.xml"), "r") as f:
fullXml = f.read()
assert "<system-out>@this is stdout@\n</system-out>" in fullXml
assert "<system-err>@this is stderr@\n</system-err>" in fullXml

View File

@ -32,6 +32,10 @@ def StdCapture(out=True, err=True, in_=True):
return capture.MultiCapture(out, err, in_, Capture=capture.SysCapture)
def TeeStdCapture(out=True, err=True, in_=True):
return capture.MultiCapture(out, err, in_, Capture=capture.TeeSysCapture)
class TestCaptureManager:
def test_getmethod_default_no_fd(self, monkeypatch):
from _pytest.capture import pytest_addoption
@ -816,6 +820,25 @@ class TestCaptureIO:
assert f.getvalue() == "foo\r\n"
class TestCaptureAndPassthroughIO(TestCaptureIO):
def test_text(self):
sio = io.StringIO()
f = capture.CaptureAndPassthroughIO(sio)
f.write("hello")
s1 = f.getvalue()
assert s1 == "hello"
s2 = sio.getvalue()
assert s2 == s1
f.close()
sio.close()
def test_unicode_and_str_mixture(self):
sio = io.StringIO()
f = capture.CaptureAndPassthroughIO(sio)
f.write("\u00f6")
pytest.raises(TypeError, f.write, b"hello")
def test_dontreadfrominput():
from _pytest.capture import DontReadFromInput
@ -1112,6 +1135,23 @@ class TestStdCapture:
pytest.raises(IOError, sys.stdin.read)
class TestTeeStdCapture(TestStdCapture):
captureclass = staticmethod(TeeStdCapture)
def test_capturing_error_recursive(self):
""" for TeeStdCapture since we passthrough stderr/stdout, cap1
should get all output, while cap2 should only get "cap2\n" """
with self.getcapture() as cap1:
print("cap1")
with self.getcapture() as cap2:
print("cap2")
out2, err2 = cap2.readouterr()
out1, err1 = cap1.readouterr()
assert out1 == "cap1\ncap2\n"
assert out2 == "cap2\n"
class TestStdCaptureFD(TestStdCapture):
pytestmark = needsosdup
captureclass = staticmethod(StdCaptureFD)
@ -1252,7 +1292,7 @@ def test_close_and_capture_again(testdir):
)
@pytest.mark.parametrize("method", ["SysCapture", "FDCapture"])
@pytest.mark.parametrize("method", ["SysCapture", "FDCapture", "TeeSysCapture"])
def test_capturing_and_logging_fundamentals(testdir, method):
if method == "StdCaptureFD" and not hasattr(os, "dup"):
pytest.skip("need os.dup")