diff --git a/_pytest/capture.py b/_pytest/capture.py index fee0116a5f0..f2ebe38c8c0 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -180,7 +180,7 @@ def suspend_capture_item(self, item, when, in_=False): item.add_report_section(when, "stderr", err) -capture_fixtures = {'capfd', 'capfdbinary', 'capsys'} +capture_fixtures = {'capfd', 'capfdbinary', 'capsys', 'capsysbinary'} def _ensure_only_one_capture_fixture(request, name): @@ -207,6 +207,22 @@ def capsys(request): yield fixture +@pytest.fixture +def capsysbinary(request): + """Enable capturing of writes to sys.stdout/sys.stderr and make + captured output available via ``capsys.readouterr()`` method calls + which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``bytes`` + objects. + """ + _ensure_only_one_capture_fixture(request, 'capsysbinary') + # Currently, the implementation uses the python3 specific `.buffer` + # property of CaptureIO. + if sys.version_info < (3,): + raise request.raiseerror('capsysbinary is only supported on python 3') + with _install_capture_fixture_on_item(request, SysCaptureBinary) as fixture: + yield fixture + + @pytest.fixture def capfd(request): """Enable capturing of writes to file descriptors 1 and 2 and make @@ -506,10 +522,9 @@ def start(self): setattr(sys, self.name, self.tmpfile) def snap(self): - f = self.tmpfile - res = f.getvalue() - f.truncate(0) - f.seek(0) + res = self.tmpfile.getvalue() + self.tmpfile.seek(0) + self.tmpfile.truncate() return res def done(self): @@ -528,6 +543,14 @@ def writeorg(self, data): self._old.flush() +class SysCaptureBinary(SysCapture): + def snap(self): + res = self.tmpfile.buffer.getvalue() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res + + class DontReadFromInput: """Temporary stub class. Ideally when stdin is accessed, the capturing should be turned off, with possibly all data captured diff --git a/changelog/2934.feature b/changelog/2934.feature new file mode 100644 index 00000000000..38dcb5f73e3 --- /dev/null +++ b/changelog/2934.feature @@ -0,0 +1,2 @@ +Add ``capsysbinary`` a version of ``capsys`` which returns bytes from +``readouterr()``. diff --git a/doc/en/capture.rst b/doc/en/capture.rst index 7ec9871dcee..2557199613b 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -85,9 +85,9 @@ of the failing function and hide the other one:: Accessing captured output from a test function --------------------------------------------------- -The ``capsys``, ``capfd``, and ``capfdbinary`` fixtures allow access to -stdout/stderr output created during test execution. Here is an example test -function that performs some output related checks: +The ``capsys``, ``capsysbinary``, ``capfd``, and ``capfdbinary`` fixtures +allow access to stdout/stderr output created during test execution. Here is +an example test function that performs some output related checks: .. code-block:: python @@ -115,11 +115,20 @@ same interface but allows to also capture output from libraries or subprocesses that directly write to operating system level output streams (FD1 and FD2). +.. versionadded:: 3.3 + +If the code under test writes non-textual data, you can capture this using +the ``capsysbinary`` fixture which instead returns ``bytes`` from +the ``readouterr`` method. The ``capfsysbinary`` fixture is currently only +available in python 3. + + .. versionadded:: 3.3 If the code under test writes non-textual data, you can capture this using the ``capfdbinary`` fixture which instead returns ``bytes`` from -the ``readouterr`` method. +the ``readouterr`` method. The ``capfdbinary`` fixture operates on the +filedescriptor level. .. versionadded:: 3.0 diff --git a/testing/test_capture.py b/testing/test_capture.py index 4aad385b340..3e8162f7154 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -470,6 +470,38 @@ def test_hello(capfdbinary): """) reprec.assertoutcome(passed=1) + @pytest.mark.skipif( + sys.version_info < (3,), + reason='only have capsysbinary in python 3', + ) + def test_capsysbinary(self, testdir): + reprec = testdir.inline_runsource(""" + def test_hello(capsysbinary): + import sys + # some likely un-decodable bytes + sys.stdout.buffer.write(b'\\xfe\\x98\\x20') + out, err = capsysbinary.readouterr() + assert out == b'\\xfe\\x98\\x20' + assert err == b'' + """) + reprec.assertoutcome(passed=1) + + @pytest.mark.skipif( + sys.version_info >= (3,), + reason='only have capsysbinary in python 3', + ) + def test_capsysbinary_forbidden_in_python2(self, testdir): + testdir.makepyfile(""" + def test_hello(capsysbinary): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*test_hello*", + "*capsysbinary is only supported on python 3*", + "*1 error in*", + ]) + def test_partial_setup_failure(self, testdir): p = testdir.makepyfile(""" def test_hello(capsys, missingarg): @@ -1233,7 +1265,7 @@ def test_capattr(): reprec.assertoutcome(passed=1) -def test_pickling_and_unpickling_enocded_file(): +def test_pickling_and_unpickling_encoded_file(): # See https://bitbucket.org/pytest-dev/pytest/pull-request/194 # pickle.loads() raises infinite recursion if # EncodedFile.__getattr__ is not implemented properly