parent
30f2729684
commit
e13ad22364
1
AUTHORS
1
AUTHORS
|
@ -52,6 +52,7 @@ Carl Friedrich Bolz
|
|||
Carlos Jenkins
|
||||
Ceridwen
|
||||
Charles Cloud
|
||||
Charles Machalow
|
||||
Charnjit SiNGH (CCSJ)
|
||||
Chris Lamb
|
||||
Christian Boelsen
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
New :ref:`--capture=tee-sys <capture-method>` option to allow both live printing and capturing of test output.
|
|
@ -21,18 +21,25 @@ 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:
|
||||
|
@ -42,6 +49,8 @@ You can influence output capturing mechanisms from the command line:
|
|||
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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue