Skip to content

Commit 0199962

Browse files
committed
Preserve chronological order of stdout and stderr with capsys.
1 parent 68acd10 commit 0199962

File tree

4 files changed

+117
-3
lines changed

4 files changed

+117
-3
lines changed

changelog/5449.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow ``capsys`` to retrieve combined stdout + stderr streams with original order of messages.

doc/en/capture.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,21 @@ as a context manager, disabling capture inside the ``with`` block:
170170
with capsys.disabled():
171171
print("output not captured, going directly to sys.stdout")
172172
print("this output is also captured")
173+
174+
175+
Preserving streams order
176+
------------------------
177+
178+
The ``capsys`` fixture has an additional ``read_combined()`` method. This method returns single value
179+
with both ``stdout`` and ``stderr`` streams combined with preserved chronological order.
180+
181+
.. code-block:: python
182+
183+
def test_combine(capsys):
184+
print("I'm in stdout")
185+
print("I'm in stderr", file=sys.stderr)
186+
print("Hey, stdout again!")
187+
188+
output = capsys.read_combined()
189+
190+
assert output == "I'm in stdout\nI'm in stderr\nHey, stdout again!\n"

src/_pytest/capture.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ def close(self):
366366
self._capture.stop_capturing()
367367
self._capture = None
368368

369-
def readouterr(self):
369+
def readouterr(self) -> "CaptureResult":
370370
"""Read and return the captured output so far, resetting the internal buffer.
371371
372372
:return: captured content as a namedtuple with ``out`` and ``err`` string attributes
@@ -380,6 +380,17 @@ def readouterr(self):
380380
self._captured_err = self.captureclass.EMPTY_BUFFER
381381
return CaptureResult(captured_out, captured_err)
382382

383+
def read_combined(self) -> str:
384+
"""
385+
Read captured output with both stdout and stderr streams combined, with preserving
386+
the correct order of messages.
387+
"""
388+
if self.captureclass is not OrderedCapture:
389+
raise AttributeError("Only capsys is able to combine streams.")
390+
result = "".join(line[0] for line in OrderedCapture.streams)
391+
OrderedCapture.flush()
392+
return result
393+
383394
def _suspend(self):
384395
"""Suspends this fixture's own capturing temporarily."""
385396
if self._capture is not None:
@@ -622,13 +633,17 @@ def __init__(self, fd, tmpfile=None):
622633
name = patchsysdict[fd]
623634
self._old = getattr(sys, name)
624635
self.name = name
636+
self.fd = fd
625637
if tmpfile is None:
626638
if name == "stdin":
627639
tmpfile = DontReadFromInput()
628640
else:
629-
tmpfile = CaptureIO()
641+
tmpfile = self._get_writer()
630642
self.tmpfile = tmpfile
631643

644+
def _get_writer(self):
645+
return CaptureIO()
646+
632647
def __repr__(self):
633648
return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
634649
self.__class__.__name__,
@@ -695,14 +710,65 @@ def __init__(self, fd, tmpfile=None):
695710
self.tmpfile = tmpfile
696711

697712

713+
class OrderedCapture(SysCapture):
714+
"""Capture class that keeps streams in order."""
715+
716+
streams = collections.deque() # type: collections.deque
717+
718+
def _get_writer(self):
719+
return OrderedWriter(self.fd)
720+
721+
def snap(self):
722+
res = self.tmpfile.getvalue()
723+
if self.name == "stderr":
724+
# both streams are being read one after another, while stderr is last - it will clear the queue
725+
self.flush()
726+
return res
727+
728+
@classmethod
729+
def flush(cls) -> None:
730+
"""Clear streams."""
731+
cls.streams.clear()
732+
733+
@classmethod
734+
def close(cls) -> None:
735+
cls.flush()
736+
737+
698738
map_fixname_class = {
699739
"capfd": FDCapture,
700740
"capfdbinary": FDCaptureBinary,
701-
"capsys": SysCapture,
741+
"capsys": OrderedCapture,
702742
"capsysbinary": SysCaptureBinary,
703743
}
704744

705745

746+
class OrderedWriter:
747+
encoding = sys.getdefaultencoding()
748+
749+
def __init__(self, fd: int) -> None:
750+
super().__init__()
751+
self._fd = fd # type: int
752+
753+
def write(self, text: str, **kwargs) -> int:
754+
OrderedCapture.streams.append((text, self._fd))
755+
return len(text)
756+
757+
def getvalue(self) -> str:
758+
return "".join(
759+
line[0] for line in OrderedCapture.streams if line[1] == self._fd
760+
)
761+
762+
def flush(self) -> None:
763+
pass
764+
765+
def isatty(self) -> bool:
766+
return False
767+
768+
def close(self) -> None:
769+
OrderedCapture.close()
770+
771+
706772
class DontReadFromInput:
707773
encoding = None
708774

testing/test_capture.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1522,3 +1522,32 @@ def test_logging():
15221522
)
15231523
result.stdout.no_fnmatch_line("*Captured stderr call*")
15241524
result.stdout.no_fnmatch_line("*during collection*")
1525+
1526+
1527+
def test_combined_streams(capsys):
1528+
"""Show that capsys is capable of preserving chronological order of streams."""
1529+
print("stdout1")
1530+
print("stdout2")
1531+
print("stderr1", file=sys.stderr)
1532+
print("stdout3")
1533+
print("stderr2", file=sys.stderr)
1534+
print("stderr3", file=sys.stderr)
1535+
print("stdout4")
1536+
print("stdout5")
1537+
1538+
output = capsys.read_combined()
1539+
assert (
1540+
output == "stdout1\n"
1541+
"stdout2\n"
1542+
"stderr1\n"
1543+
"stdout3\n"
1544+
"stderr2\n"
1545+
"stderr3\n"
1546+
"stdout4\n"
1547+
"stdout5\n"
1548+
)
1549+
1550+
1551+
def test_no_capsys_exceptions(capfd):
1552+
with pytest.raises(AttributeError, match="Only capsys is able to combine streams."):
1553+
capfd.read_combined()

0 commit comments

Comments
 (0)