Skip to content

Commit e429277

Browse files
committed
Preserve chronological order of stdout and stderr with capsys.
1 parent f214a4b commit e429277

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
@@ -348,7 +348,7 @@ def close(self):
348348
self._capture.stop_capturing()
349349
self._capture = None
350350

351-
def readouterr(self):
351+
def readouterr(self) -> "CaptureResult":
352352
"""Read and return the captured output so far, resetting the internal buffer.
353353
354354
:return: captured content as a namedtuple with ``out`` and ``err`` string attributes
@@ -362,6 +362,17 @@ def readouterr(self):
362362
self._captured_err = self.captureclass.EMPTY_BUFFER
363363
return CaptureResult(captured_out, captured_err)
364364

365+
def read_combined(self) -> str:
366+
"""
367+
Read captured output with both stdout and stderr streams combined, with preserving
368+
the correct order of messages.
369+
"""
370+
if self.captureclass is not OrderedCapture:
371+
raise AttributeError("Only capsys is able to combine streams.")
372+
result = "".join(line[0] for line in OrderedCapture.streams)
373+
OrderedCapture.flush()
374+
return result
375+
365376
def _suspend(self):
366377
"""Suspends this fixture's own capturing temporarily."""
367378
if self._capture is not None:
@@ -604,13 +615,17 @@ def __init__(self, fd, tmpfile=None):
604615
name = patchsysdict[fd]
605616
self._old = getattr(sys, name)
606617
self.name = name
618+
self.fd = fd
607619
if tmpfile is None:
608620
if name == "stdin":
609621
tmpfile = DontReadFromInput()
610622
else:
611-
tmpfile = CaptureIO()
623+
tmpfile = self._get_writer()
612624
self.tmpfile = tmpfile
613625

626+
def _get_writer(self):
627+
return CaptureIO()
628+
614629
def __repr__(self):
615630
return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
616631
self.__class__.__name__,
@@ -677,14 +692,65 @@ def __init__(self, fd, tmpfile=None):
677692
self.tmpfile = tmpfile
678693

679694

695+
class OrderedCapture(SysCapture):
696+
"""Capture class that keeps streams in order."""
697+
698+
streams = collections.deque() # type: collections.deque
699+
700+
def _get_writer(self):
701+
return OrderedWriter(self.fd)
702+
703+
def snap(self):
704+
res = self.tmpfile.getvalue()
705+
if self.name == "stderr":
706+
# both streams are being read one after another, while stderr is last - it will clear the queue
707+
self.flush()
708+
return res
709+
710+
@classmethod
711+
def flush(cls) -> None:
712+
"""Clear streams."""
713+
cls.streams.clear()
714+
715+
@classmethod
716+
def close(cls) -> None:
717+
cls.flush()
718+
719+
680720
map_fixname_class = {
681721
"capfd": FDCapture,
682722
"capfdbinary": FDCaptureBinary,
683-
"capsys": SysCapture,
723+
"capsys": OrderedCapture,
684724
"capsysbinary": SysCaptureBinary,
685725
}
686726

687727

728+
class OrderedWriter:
729+
encoding = sys.getdefaultencoding()
730+
731+
def __init__(self, fd: int) -> None:
732+
super().__init__()
733+
self._fd = fd # type: int
734+
735+
def write(self, text: str, **kwargs) -> int:
736+
OrderedCapture.streams.append((text, self._fd))
737+
return len(text)
738+
739+
def getvalue(self) -> str:
740+
return "".join(
741+
line[0] for line in OrderedCapture.streams if line[1] == self._fd
742+
)
743+
744+
def flush(self) -> None:
745+
pass
746+
747+
def isatty(self) -> bool:
748+
return False
749+
750+
def close(self) -> None:
751+
OrderedCapture.close()
752+
753+
688754
class DontReadFromInput:
689755
encoding = None
690756

testing/test_capture.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,3 +1493,32 @@ def test__get_multicapture() -> None:
14931493
pytest.raises(ValueError, _get_multicapture, "unknown").match(
14941494
r"^unknown capturing method: 'unknown'"
14951495
)
1496+
1497+
1498+
def test_combined_streams(capsys):
1499+
"""Show that capsys is capable of preserving chronological order of streams."""
1500+
print("stdout1")
1501+
print("stdout2")
1502+
print("stderr1", file=sys.stderr)
1503+
print("stdout3")
1504+
print("stderr2", file=sys.stderr)
1505+
print("stderr3", file=sys.stderr)
1506+
print("stdout4")
1507+
print("stdout5")
1508+
1509+
output = capsys.read_combined()
1510+
assert (
1511+
output == "stdout1\n"
1512+
"stdout2\n"
1513+
"stderr1\n"
1514+
"stdout3\n"
1515+
"stderr2\n"
1516+
"stderr3\n"
1517+
"stdout4\n"
1518+
"stdout5\n"
1519+
)
1520+
1521+
1522+
def test_no_capsys_exceptions(capfd):
1523+
with pytest.raises(AttributeError, match="Only capsys is able to combine streams."):
1524+
capfd.read_combined()

0 commit comments

Comments
 (0)