Skip to content

Commit 6161bcf

Browse files
authored
Merge pull request #2925 from asottile/capfdbinary
Add capfdbinary fixture
2 parents 99a4a93 + 8f90812 commit 6161bcf

File tree

4 files changed

+94
-26
lines changed

4 files changed

+94
-26
lines changed

_pytest/capture.py

+56-20
Original file line numberDiff line numberDiff line change
@@ -180,17 +180,29 @@ def suspend_capture_item(self, item, when, in_=False):
180180
item.add_report_section(when, "stderr", err)
181181

182182

183-
error_capsysfderror = "cannot use capsys and capfd at the same time"
183+
capture_fixtures = {'capfd', 'capfdbinary', 'capsys'}
184+
185+
186+
def _ensure_only_one_capture_fixture(request, name):
187+
fixtures = set(request.fixturenames) & capture_fixtures - set((name,))
188+
if fixtures:
189+
fixtures = sorted(fixtures)
190+
fixtures = fixtures[0] if len(fixtures) == 1 else fixtures
191+
raise request.raiseerror(
192+
"cannot use {0} and {1} at the same time".format(
193+
fixtures, name,
194+
),
195+
)
184196

185197

186198
@pytest.fixture
187199
def capsys(request):
188200
"""Enable capturing of writes to sys.stdout/sys.stderr and make
189201
captured output available via ``capsys.readouterr()`` method calls
190-
which return a ``(out, err)`` tuple.
202+
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text``
203+
objects.
191204
"""
192-
if "capfd" in request.fixturenames:
193-
raise request.raiseerror(error_capsysfderror)
205+
_ensure_only_one_capture_fixture(request, 'capsys')
194206
with _install_capture_fixture_on_item(request, SysCapture) as fixture:
195207
yield fixture
196208

@@ -199,16 +211,30 @@ def capsys(request):
199211
def capfd(request):
200212
"""Enable capturing of writes to file descriptors 1 and 2 and make
201213
captured output available via ``capfd.readouterr()`` method calls
202-
which return a ``(out, err)`` tuple.
214+
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text``
215+
objects.
203216
"""
204-
if "capsys" in request.fixturenames:
205-
request.raiseerror(error_capsysfderror)
217+
_ensure_only_one_capture_fixture(request, 'capfd')
206218
if not hasattr(os, 'dup'):
207219
pytest.skip("capfd fixture needs os.dup function which is not available in this system")
208220
with _install_capture_fixture_on_item(request, FDCapture) as fixture:
209221
yield fixture
210222

211223

224+
@pytest.fixture
225+
def capfdbinary(request):
226+
"""Enable capturing of write to file descriptors 1 and 2 and make
227+
captured output available via ``capfdbinary.readouterr`` method calls
228+
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be
229+
``bytes`` objects.
230+
"""
231+
_ensure_only_one_capture_fixture(request, 'capfdbinary')
232+
if not hasattr(os, 'dup'):
233+
pytest.skip("capfdbinary fixture needs os.dup function which is not available in this system")
234+
with _install_capture_fixture_on_item(request, FDCaptureBinary) as fixture:
235+
yield fixture
236+
237+
212238
@contextlib.contextmanager
213239
def _install_capture_fixture_on_item(request, capture_class):
214240
"""
@@ -378,8 +404,11 @@ class NoCapture:
378404
__init__ = start = done = suspend = resume = lambda *args: None
379405

380406

381-
class FDCapture:
382-
""" Capture IO to/from a given os-level filedescriptor. """
407+
class FDCaptureBinary:
408+
"""Capture IO to/from a given os-level filedescriptor.
409+
410+
snap() produces `bytes`
411+
"""
383412

384413
def __init__(self, targetfd, tmpfile=None):
385414
self.targetfd = targetfd
@@ -418,17 +447,11 @@ def start(self):
418447
self.syscapture.start()
419448

420449
def snap(self):
421-
f = self.tmpfile
422-
f.seek(0)
423-
res = f.read()
424-
if res:
425-
enc = getattr(f, "encoding", None)
426-
if enc and isinstance(res, bytes):
427-
res = six.text_type(res, enc, "replace")
428-
f.truncate(0)
429-
f.seek(0)
430-
return res
431-
return ''
450+
self.tmpfile.seek(0)
451+
res = self.tmpfile.read()
452+
self.tmpfile.seek(0)
453+
self.tmpfile.truncate()
454+
return res
432455

433456
def done(self):
434457
""" stop capturing, restore streams, return original capture file,
@@ -454,6 +477,19 @@ def writeorg(self, data):
454477
os.write(self.targetfd_save, data)
455478

456479

480+
class FDCapture(FDCaptureBinary):
481+
"""Capture IO to/from a given os-level filedescriptor.
482+
483+
snap() produces text
484+
"""
485+
def snap(self):
486+
res = FDCaptureBinary.snap(self)
487+
enc = getattr(self.tmpfile, "encoding", None)
488+
if enc and isinstance(res, bytes):
489+
res = six.text_type(res, enc, "replace")
490+
return res
491+
492+
457493
class SysCapture:
458494
def __init__(self, fd, tmpfile=None):
459495
name = patchsysdict[fd]

changelog/2923.feature

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add ``capfdbinary`` a version of ``capfd`` which returns bytes from
2+
``readouterr()``.

doc/en/capture.rst

+10-4
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ of the failing function and hide the other one::
8585
Accessing captured output from a test function
8686
---------------------------------------------------
8787

88-
The ``capsys`` and ``capfd`` fixtures allow to access stdout/stderr
89-
output created during test execution. Here is an example test function
90-
that performs some output related checks:
88+
The ``capsys``, ``capfd``, and ``capfdbinary`` fixtures allow access to
89+
stdout/stderr output created during test execution. Here is an example test
90+
function that performs some output related checks:
9191

9292
.. code-block:: python
9393
@@ -110,11 +110,17 @@ output streams and also interacts well with pytest's
110110
own per-test capturing.
111111

112112
If you want to capture on filedescriptor level you can use
113-
the ``capfd`` function argument which offers the exact
113+
the ``capfd`` fixture which offers the exact
114114
same interface but allows to also capture output from
115115
libraries or subprocesses that directly write to operating
116116
system level output streams (FD1 and FD2).
117117

118+
.. versionadded:: 3.3
119+
120+
If the code under test writes non-textual data, you can capture this using
121+
the ``capfdbinary`` fixture which instead returns ``bytes`` from
122+
the ``readouterr`` method.
123+
118124

119125
.. versionadded:: 3.0
120126

testing/test_capture.py

+26-2
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ def test_two(capfd, capsys):
398398
result = testdir.runpytest(p)
399399
result.stdout.fnmatch_lines([
400400
"*ERROR*setup*test_one*",
401-
"E*capsys*capfd*same*time*",
401+
"E*capfd*capsys*same*time*",
402402
"*ERROR*setup*test_two*",
403403
"E*capsys*capfd*same*time*",
404404
"*2 error*"])
@@ -418,10 +418,21 @@ def test_two(capfd, request):
418418
"*test_one*",
419419
"*capsys*capfd*same*time*",
420420
"*test_two*",
421-
"*capsys*capfd*same*time*",
421+
"*capfd*capsys*same*time*",
422422
"*2 failed in*",
423423
])
424424

425+
def test_capsyscapfdbinary(self, testdir):
426+
p = testdir.makepyfile("""
427+
def test_one(capsys, capfdbinary):
428+
pass
429+
""")
430+
result = testdir.runpytest(p)
431+
result.stdout.fnmatch_lines([
432+
"*ERROR*setup*test_one*",
433+
"E*capfdbinary*capsys*same*time*",
434+
"*1 error*"])
435+
425436
@pytest.mark.parametrize("method", ["sys", "fd"])
426437
def test_capture_is_represented_on_failure_issue128(self, testdir, method):
427438
p = testdir.makepyfile("""
@@ -446,6 +457,19 @@ def test_hello(capfd):
446457
""")
447458
reprec.assertoutcome(passed=1)
448459

460+
@needsosdup
461+
def test_capfdbinary(self, testdir):
462+
reprec = testdir.inline_runsource("""
463+
def test_hello(capfdbinary):
464+
import os
465+
# some likely un-decodable bytes
466+
os.write(1, b'\\xfe\\x98\\x20')
467+
out, err = capfdbinary.readouterr()
468+
assert out == b'\\xfe\\x98\\x20'
469+
assert err == b''
470+
""")
471+
reprec.assertoutcome(passed=1)
472+
449473
def test_partial_setup_failure(self, testdir):
450474
p = testdir.makepyfile("""
451475
def test_hello(capsys, missingarg):

0 commit comments

Comments
 (0)