parent
30f2729684
commit
e13ad22364
1
AUTHORS
1
AUTHORS
|
@ -52,6 +52,7 @@ Carl Friedrich Bolz
|
||||||
Carlos Jenkins
|
Carlos Jenkins
|
||||||
Ceridwen
|
Ceridwen
|
||||||
Charles Cloud
|
Charles Cloud
|
||||||
|
Charles Machalow
|
||||||
Charnjit SiNGH (CCSJ)
|
Charnjit SiNGH (CCSJ)
|
||||||
Chris Lamb
|
Chris Lamb
|
||||||
Christian Boelsen
|
Christian Boelsen
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
New :ref:`--capture=tee-sys <capture-method>` option to allow both live printing and capturing of test output.
|
|
@ -21,27 +21,36 @@ file descriptors. This allows to capture output from simple
|
||||||
print statements as well as output from a subprocess started by
|
print statements as well as output from a subprocess started by
|
||||||
a test.
|
a test.
|
||||||
|
|
||||||
|
.. _capture-method:
|
||||||
|
|
||||||
Setting capturing methods or disabling capturing
|
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.
|
operating system file descriptors 1 and 2 will be captured.
|
||||||
|
|
||||||
* ``sys`` level capturing: Only writes to Python files ``sys.stdout``
|
* ``sys`` level capturing: Only writes to Python files ``sys.stdout``
|
||||||
and ``sys.stderr`` will be captured. No capturing of writes to
|
and ``sys.stderr`` will be captured. No capturing of writes to
|
||||||
filedescriptors is performed.
|
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`:
|
.. _`disable capturing`:
|
||||||
|
|
||||||
You can influence output capturing mechanisms from the command line:
|
You can influence output capturing mechanisms from the command line:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pytest -s # disable all capturing
|
pytest -s # disable all capturing
|
||||||
pytest --capture=sys # replace sys.stdout/stderr with in-mem files
|
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=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:
|
.. _printdebugging:
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ from io import UnsupportedOperation
|
||||||
from tempfile import TemporaryFile
|
from tempfile import TemporaryFile
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest.compat import CaptureAndPassthroughIO
|
||||||
from _pytest.compat import CaptureIO
|
from _pytest.compat import CaptureIO
|
||||||
from _pytest.fixtures import FixtureRequest
|
from _pytest.fixtures import FixtureRequest
|
||||||
|
|
||||||
|
@ -24,8 +25,8 @@ def pytest_addoption(parser):
|
||||||
action="store",
|
action="store",
|
||||||
default="fd" if hasattr(os, "dup") else "sys",
|
default="fd" if hasattr(os, "dup") else "sys",
|
||||||
metavar="method",
|
metavar="method",
|
||||||
choices=["fd", "sys", "no"],
|
choices=["fd", "sys", "no", "tee-sys"],
|
||||||
help="per-test capturing method: one of fd|sys|no.",
|
help="per-test capturing method: one of fd|sys|no|tee-sys.",
|
||||||
)
|
)
|
||||||
group._addoption(
|
group._addoption(
|
||||||
"-s",
|
"-s",
|
||||||
|
@ -90,6 +91,8 @@ class CaptureManager:
|
||||||
return MultiCapture(out=True, err=True, Capture=SysCapture)
|
return MultiCapture(out=True, err=True, Capture=SysCapture)
|
||||||
elif method == "no":
|
elif method == "no":
|
||||||
return MultiCapture(out=False, err=False, in_=False)
|
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
|
raise ValueError("unknown capturing method: %r" % method) # pragma: no cover
|
||||||
|
|
||||||
def is_capturing(self):
|
def is_capturing(self):
|
||||||
|
@ -681,6 +684,19 @@ class SysCapture:
|
||||||
self._old.flush()
|
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):
|
class SysCaptureBinary(SysCapture):
|
||||||
# Ignore type because it doesn't match the type in the superclass (str).
|
# Ignore type because it doesn't match the type in the superclass (str).
|
||||||
EMPTY_BUFFER = b"" # type: ignore
|
EMPTY_BUFFER = b"" # type: ignore
|
||||||
|
|
|
@ -13,6 +13,7 @@ from inspect import signature
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
|
from typing import IO
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import overload
|
from typing import overload
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
@ -371,6 +372,16 @@ class CaptureIO(io.TextIOWrapper):
|
||||||
return self.buffer.getvalue().decode("UTF-8")
|
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
|
if sys.version_info < (3, 5, 2): # pragma: no cover
|
||||||
|
|
||||||
def overload(f): # noqa: F811
|
def overload(f): # noqa: F811
|
||||||
|
|
|
@ -1285,3 +1285,28 @@ def test_pdb_can_be_rewritten(testdir):
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert result.ret == 1
|
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)
|
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:
|
class TestCaptureManager:
|
||||||
def test_getmethod_default_no_fd(self, monkeypatch):
|
def test_getmethod_default_no_fd(self, monkeypatch):
|
||||||
from _pytest.capture import pytest_addoption
|
from _pytest.capture import pytest_addoption
|
||||||
|
@ -816,6 +820,25 @@ class TestCaptureIO:
|
||||||
assert f.getvalue() == "foo\r\n"
|
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():
|
def test_dontreadfrominput():
|
||||||
from _pytest.capture import DontReadFromInput
|
from _pytest.capture import DontReadFromInput
|
||||||
|
|
||||||
|
@ -1112,6 +1135,23 @@ class TestStdCapture:
|
||||||
pytest.raises(IOError, sys.stdin.read)
|
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):
|
class TestStdCaptureFD(TestStdCapture):
|
||||||
pytestmark = needsosdup
|
pytestmark = needsosdup
|
||||||
captureclass = staticmethod(StdCaptureFD)
|
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):
|
def test_capturing_and_logging_fundamentals(testdir, method):
|
||||||
if method == "StdCaptureFD" and not hasattr(os, "dup"):
|
if method == "StdCaptureFD" and not hasattr(os, "dup"):
|
||||||
pytest.skip("need os.dup")
|
pytest.skip("need os.dup")
|
||||||
|
|
Loading…
Reference in New Issue