Skip to content

Commit 123e4f6

Browse files
committed
Add capfdbinary fixture
`capfdbinary` works like `capfd` but produces bytes for `readouterr()`.
1 parent 685387a commit 123e4f6

File tree

4 files changed

+90
-25
lines changed

4 files changed

+90
-25
lines changed

_pytest/capture.py

+55-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,29 @@ 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+
if not hasattr(os, 'dup'):
232+
pytest.skip("capfdbinary funcarg needs os.dup")
233+
with _install_capture_fixture_on_item(request, FDCaptureBinary) as fixture:
234+
yield fixture
235+
236+
212237
@contextlib.contextmanager
213238
def _install_capture_fixture_on_item(request, capture_class):
214239
"""
@@ -378,8 +403,11 @@ class NoCapture:
378403
__init__ = start = done = suspend = resume = lambda *args: None
379404

380405

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

384412
def __init__(self, targetfd, tmpfile=None):
385413
self.targetfd = targetfd
@@ -418,17 +446,11 @@ def start(self):
418446
self.syscapture.start()
419447

420448
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 ''
449+
self.tmpfile.seek(0)
450+
res = self.tmpfile.read()
451+
self.tmpfile.seek(0)
452+
self.tmpfile.truncate()
453+
return res
432454

433455
def done(self):
434456
""" stop capturing, restore streams, return original capture file,
@@ -454,6 +476,19 @@ def writeorg(self, data):
454476
os.write(self.targetfd_save, data)
455477

456478

479+
class FDCapture(FDCaptureBinary):
480+
"""Capture IO to/from a given os-level filedescriptor.
481+
482+
snap() produces text
483+
"""
484+
def snap(self):
485+
res = FDCaptureBinary.snap(self)
486+
enc = getattr(self.tmpfile, "encoding", None)
487+
if enc and isinstance(res, bytes):
488+
res = six.text_type(res, enc, "replace")
489+
return res
490+
491+
457492
class SysCapture:
458493
def __init__(self, fd, tmpfile=None):
459494
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

+7-3
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
@@ -115,6 +115,10 @@ 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+
If the code under test writes non-textual data, you can capture this using
119+
the ``capfdbinary`` function argument which instead returns ``bytes`` from
120+
the ``readouterr`` method.
121+
118122

119123
.. versionadded:: 3.0
120124

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)