Skip to content

Commit cbeda28

Browse files
committed
Rebase with master
1 parent 478a244 commit cbeda28

File tree

2 files changed

+107
-4
lines changed

2 files changed

+107
-4
lines changed

src/_pytest/capture.py

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def _getcapture(self, method):
9393
if method == "fd":
9494
return MultiCapture(out=True, err=True, Capture=FDCapture)
9595
elif method == "sys":
96-
return MultiCapture(out=True, err=True, Capture=SysCapture)
96+
return MultiCapture(out=True, err=True, Capture=OrderedCapture)
9797
elif method == "no":
9898
return MultiCapture(out=False, err=False, in_=False)
9999
elif method == "tee-sys":
@@ -351,11 +351,13 @@ def close(self):
351351
self._capture.stop_capturing()
352352
self._capture = None
353353

354-
def readouterr(self):
354+
def readouterr(self, combined=False):
355355
"""Read and return the captured output so far, resetting the internal buffer.
356356
357357
:return: captured content as a namedtuple with ``out`` and ``err`` string attributes
358358
"""
359+
if combined:
360+
return CaptureResult(self._get_combined(), None)
359361
captured_out, captured_err = self._captured_out, self._captured_err
360362
if self._capture is not None:
361363
out, err = self._capture.readouterr()
@@ -365,6 +367,13 @@ def readouterr(self):
365367
self._captured_err = self.captureclass.EMPTY_BUFFER
366368
return CaptureResult(captured_out, captured_err)
367369

370+
def _get_combined(self):
371+
if self.captureclass is not OrderedCapture:
372+
raise AttributeError("Only ordered capture is able to combine streams.")
373+
result = "".join(line[0] for line in OrderedCapture.streams)
374+
OrderedCapture.streams.clear()
375+
return result
376+
368377
def _suspend(self):
369378
"""Suspends this fixture's own capturing temporarily."""
370379
if self._capture is not None:
@@ -630,12 +639,16 @@ def __init__(self, fd, tmpfile=None):
630639
name = patchsysdict[fd]
631640
self._old = getattr(sys, name)
632641
self.name = name
642+
self.fd = fd
633643
if tmpfile is None:
634644
if name == "stdin":
635645
tmpfile = DontReadFromInput()
636646
else:
637-
tmpfile = CaptureIO()
647+
tmpfile = self._get_writer()
638648
self.tmpfile = tmpfile
649+
650+
def _get_writer(self):
651+
return CaptureIO()
639652

640653
def __repr__(self):
641654
return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
@@ -698,14 +711,80 @@ def __init__(self, fd, tmpfile=None):
698711
self.tmpfile = tmpfile
699712

700713

714+
715+
class OrderedCapture(SysCapture):
716+
"""This class uses deque to keep streams in order."""
717+
streams = collections.deque()
718+
719+
def _get_writer(self):
720+
return OrderedWriter(self.fd)
721+
722+
def snap(self):
723+
res = self.tmpfile.getvalue()
724+
if self.name == "stderr":
725+
# both streams are being read one after another, while stderr is last - it will clear the queue
726+
self.streams.clear()
727+
return res
728+
701729
map_fixname_class = {
702730
"capfd": FDCapture,
703731
"capfdbinary": FDCaptureBinary,
704-
"capsys": SysCapture,
732+
"capsys": OrderedCapture,
705733
"capsysbinary": SysCaptureBinary,
706734
}
707735

708736

737+
class OrderedWriter:
738+
encoding = sys.getdefaultencoding()
739+
740+
def __init__(self, fd: int) -> None:
741+
super().__init__()
742+
self._fd = fd # type: int
743+
744+
def write(self, text, **kwargs):
745+
OrderedCapture.streams.append((text, self._fd))
746+
return len(text)
747+
748+
def getvalue(self):
749+
return "".join((line[0] for line in OrderedCapture.streams if line[1] == self._fd))
750+
751+
def close(self):
752+
OrderedCapture.streams.clear()
753+
754+
755+
class OrderedCapture(SysCapture):
756+
"""This class uses deque to keep streams in order."""
757+
streams = collections.deque()
758+
759+
def _get_writer(self):
760+
return OrderedWriter(self.fd)
761+
762+
def snap(self):
763+
res = self.tmpfile.getvalue()
764+
if self.name == "stderr":
765+
# both streams are being read one after another, while stderr is last - it will clear the queue
766+
self.streams.clear()
767+
return res
768+
769+
770+
class OrderedWriter:
771+
encoding = sys.getdefaultencoding()
772+
773+
def __init__(self, fd: int) -> None:
774+
super().__init__()
775+
self._fd = fd # type: int
776+
777+
def write(self, text, **kwargs):
778+
OrderedCapture.streams.append((text, self._fd))
779+
return len(text)
780+
781+
def getvalue(self):
782+
return "".join((line[0] for line in OrderedCapture.streams if line[1] == self._fd))
783+
784+
def close(self):
785+
OrderedCapture.streams.clear()
786+
787+
709788
class DontReadFromInput:
710789
encoding = None
711790

testing/test_capture.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1563,3 +1563,27 @@ def test_encodedfile_writelines(tmpfile: BinaryIO) -> None:
15631563
tmpfile.close()
15641564
with pytest.raises(ValueError):
15651565
ef.read()
1566+
1567+
def test_combined_streams(capsys):
1568+
"""Show that capsys is capable of preserving chronological order of streams."""
1569+
print("stdout1")
1570+
print("stdout2")
1571+
print("stderr1", file=sys.stderr)
1572+
print("stdout3")
1573+
print("stderr2", file=sys.stderr)
1574+
print("stderr3", file=sys.stderr)
1575+
print("stdout4")
1576+
print("stdout5")
1577+
1578+
out, err = capsys.readouterr(combined=True)
1579+
assert (
1580+
out == "stdout1\n"
1581+
"stdout2\n"
1582+
"stderr1\n"
1583+
"stdout3\n"
1584+
"stderr2\n"
1585+
"stderr3\n"
1586+
"stdout4\n"
1587+
"stdout5\n"
1588+
)
1589+
assert err is None

0 commit comments

Comments
 (0)