pytest icon indicating copy to clipboard operation
pytest copied to clipboard

Output appears in "Captured stderr call" but is unavailable in capsys or capfd

Open butla opened this issue 5 years ago • 27 comments

Environment

Pytest version: 5.2.1 Operating system: Kubuntu 18.04.3 LTS Pip list:

Package            Version
------------------ -------
atomicwrites       1.3.0
attrs              19.3.0
importlib-metadata 0.23
more-itertools     7.2.0
packaging          19.2
pip                19.3.1
pluggy             0.13.0
py                 1.8.0
pyparsing          2.4.2
pytest             5.2.1
setuptools         41.4.0
six                1.12.0
wcwidth            0.1.7
wheel              0.33.6
zipp               0.6.0

Description

It seems that output can appear in "Captured stderr call" but be unavailable to capsys or capfd, which can be a bug? It's definitely weird.

I came accross this issue while trying to test my logging setup. This is a minimal example I came up with (available at https://github.com/butla/experiments/tree/master/pytest_capture_log_error):

# a_file.py
import logging

logging.basicConfig(level=logging.INFO)

LOG_MESSAGE = "the message we'll look for in the test"

_log = logging.getLogger(__name__)


def bla():
    _log.info(LOG_MESSAGE)
    return 5
# test_file.py
import a_file

def test_a(capsys):
    assert a_file.bla() == 5
    assert a_file.LOG_MESSAGE in capsys.readouterr().err

The test fails and it shows

------------------------------------------ Captured stderr call ------------------------------------------
INFO:a_file:the message we'll look for in the test

but both capsys and capfd show out and err to be empty.

I would use caplog, but the messages I get from it don't conform to the log configuration that I have set up in my original app, so I decided to just use stderr.

butla avatar Oct 18 '19 16:10 butla

this is pretty strange, it probably should work with capfd (I could see why it wouldn't with capsys due the way the logging module works)

the test passes with -s -- definitely feels like some sort of bug

asottile avatar Oct 18 '19 21:10 asottile

Oh yeah, passing -s and using capfd fixture makes it pass...

butla avatar Oct 18 '19 21:10 butla

Related (I think): https://github.com/pytest-dev/pytest/issues/3448

blueyed avatar Nov 01 '19 18:11 blueyed

This is just for stderr - if I modify a_file.py to have

def bla():
    print("foo")
    _log.info(LOG_MESSAGE)
    return 5

and test_file.py to have

def test_a(capsys):
    assert a_file.bla() == 5
    assert "foo" in capsys.readouterr().out
    assert a_file.LOG_MESSAGE in capsys.readouterr().err

then only the third assertion fails.

Currently looking into this

MarcoGorelli avatar Nov 10 '19 10:11 MarcoGorelli

@MarcoGorelli This is due to logging using stderr, isn't it? It picks up the mocked away stderr from pytest's capturing likely, i.e. it would also work if you'd do the logging setup in the test.

blueyed avatar Nov 10 '19 13:11 blueyed

Just as an update - I've been studying capture.py, but still haven't figured out where the issue might lie. If this is urgent, anyone more experienced following along is welcome to take over.

Else I'll keep hacking away :)

MarcoGorelli avatar Nov 17 '19 20:11 MarcoGorelli

I'm seeing this issue with stdout logged via a custom logging module we have. Our logger is basically just calling sys.stdout.write(); sys.stdout.flush() at the end of the day.

ahal avatar Feb 20 '20 16:02 ahal

@MarcoGorelli did you ever figure this one out? I just ran into the same problem and found this issue :).

larsks avatar Mar 20 '21 01:03 larsks

Nope, I gave up on this :flushed:

MarcoGorelli avatar Mar 20 '21 10:03 MarcoGorelli

I am also having this issue. Would like to be able to use capsys.readouterr to test format of log messages that change format based on whether or not the message contains a newline.

taranlu-houzz avatar Apr 10 '21 00:04 taranlu-houzz

I have a simpler reproduction that only succeeds with -s -- I'll see what I can do to fix this:

import sys

x = sys.stdout.buffer

def test(capfd):
    x.write(b'wat\n')
    x.flush()
    assert capfd.readouterr().out

asottile avatar Apr 26 '21 15:04 asottile

@asottile as structurally the issue is - user broke the weak wys we have in python, im inclined to condier this one as something to only warn about

@asottile i also do wonder about closing the issue as invalid stdio usage instead of using logcapture

RonnyPfannschmidt avatar Apr 26 '21 21:04 RonnyPfannschmidt

@RonnyPfannschmidt my reproduction above does not use logging -- this is definitely something capfd should be able to handle. pytest almost-mockingly displays "Captured stdout" "wat" when it fails so pytest definitely could capture it properly!

asottile avatar Apr 26 '21 21:04 asottile

ok so I took a bit of a look at this on stream. I can get it to do what I want by disabling the syscapture part of capfd / capfdbinary. This has one downside in that flushing needs to happen whenever the file descriptors are changed when dealing with print because buffering.

I have a bit of a work-in-progress version of this branch, but it requires a lot of changes to the testsuite (not sure whether it's actually a good change or not given how many tests it broke?) -- I also have to change a lot of tests to run in a subprocess so they don't interfere with the tests themselves (because syscapture is no longer being reset)

here's that patch though, or at least the start of it: https://github.com/pytest-dev/pytest/pull/new/wip_fix_capfd

asottile avatar Apr 27 '21 02:04 asottile

I am wondering what happend with this @asottile ?

chamini2 avatar Jan 13 '22 21:01 chamini2

I got busy and never got around to finishing that branch above -- I also wasn't 100% on whether it's ultimately the right decision (since the test adjustments really point out how much it potentially would break consumers)

asottile avatar Jan 13 '22 21:01 asottile

I understand. turning off the syscapture part maybe does not really make sense and this needs a different solution?

chamini2 avatar Jan 13 '22 22:01 chamini2

I think I ran into the same issue, my repro:

# foo.py
print("hello")

Surprisingly:

from importlib import import_module

def test_prints_hello(capfd):
    import_module('foo')
    out, err = capfd.readouterr()

    assert out == ""  # will pass
    # assert out == "hello\n"  # and so will this one (expected), but only if above line is commented out

edit Although this is still happening in the particular project I used, I'm not able to reproduce this in an fresh & independent venv. Very strange...

Kache avatar Feb 03 '22 08:02 Kache

I've bumped into this, too. The STDERR output is shown in pytest's output in the "Captured stderr call", but capsys does not seem to have captured it.

platform linux -- Python 3.10.8, pytest-7.2.1, pluggy-1.0.0
plugins: django-4.5.2, Faker-16.8.1, cov-4.0.0, celery-0.0.0

And as was noted above, the test passes when using capfd and running with the -s command-line argument.

vakorol avatar Mar 13 '23 11:03 vakorol

Any reason why it is not possible to pass a logger instance to pytest's caplog? The only option is to give a logger name. However, that does not work in some cases as seen in this long thread. And capsys or capfd is not what I need either. In unittest assertLogs it is possible to give a logger. Just wonder why it wasn't done similarly in pytest.

For the record, what I ended up doing was the following context manager:

@contextmanager
def capture_logs(logger: logging.Logger):
    with ExitStack() as stack:
        captured = StringIO()
        for handler in logger.handlers:
            if isinstance(handler, logging.StreamHandler):
                stack.enter_context(patch.object(handler, "stream", captured))
        yield captured

And then in a test:

with capture_logs(logger_being_tested) as logs:
    ...
assert ... in logs.getvalue()

mauvilsa avatar May 09 '23 16:05 mauvilsa

Add this snippet to pyproject.toml fix this problem.

[tool.pytest.ini_options]
addopts = "--capture=tee-sys"

But why it is so hard to fix it in passed 3 years?

PaleNeutron avatar Mar 22 '24 02:03 PaleNeutron

if it's so easy why don't you fix it?

asottile avatar Mar 22 '24 02:03 asottile

@asottile You're not helping by lashing out at people. You could have constructively encouraged them to provide input on fixing this issue instead.

Feuermurmel avatar Mar 22 '24 12:03 Feuermurmel

i think we need to split this issue a bit - the initial issue is no longer valid - configuring logging with just the defaults is broken and non-fixable + disables pytests logcapture

logcapture fixtures should be used

logging should onl be configured by an application, not for testrunning

pytest will not do logcapture if logging is configured as the footguns from the past still haunt me at least

RonnyPfannschmidt avatar Mar 22 '24 13:03 RonnyPfannschmidt

I got the same unexpected behavior of capsys in my project . After going through this discussion I tried the following approaches:

  1. Use the -s flag with capfd

    I used the hack given by @asottile

    this is pretty strange, it probably should work with capfd (I could see why it wouldn't with capsys due the way the logging module works)

    the test passes with -s -- definitely feels like some sort of bug

    to get get it working and it worked.

  2. Configure pyproject.toml to --capture=tee-sys

    I used the hack given by @PaleNeutron and added:

    [tool.pytest.ini_options]
    addopts = "--capture=tee-sys"
    

    in pyproject.toml and it worked.

  3. Implement custom marker to mark the test which'll need this

    I have also tried to implement markers to mark the test that'd be needing this so that I wouldn't need to make a global change in the configuration of pytest, here's the marker: conftest.py:

    # conftest.py
    def pytest_runtest_setup(item):
        """
        Effectively sets the -s flag on the test function
        """
        if "needs_output" in item.keywords:
            item.config.option.capture = "no"
    

    pyproject.toml:

    # pyproject.toml
    # ...
    [tool.pytest.ini_options]
    markers = [
      # Creating htis marker because facing the issue detailed in this GitHub discussion:
      #   https://github.com/pytest-dev/pytest/issues/5997
      "needs_output: Tests that require the -s flag"
    ]
    

    test_file.py:

    # test_file.py
    # ...
        @pytest.mark.needs_output
        def test_does_not_accept_a_non_directory(self, capfd, dummy_file):
            # Had to use capfd with -s instead of capsys because of a bug in capsys where it doesn't capture
            # logger's stderr. The bug is being discussed on https://github.com/pytest-dev/pytest/issues/5997
            with pytest.raises(SystemExit):
                implant_process_stage(dummy_file)
            stderr = capfd.readouterr().err
            assert NOT_A_DIRECTORY in stderr
    

    The marker approach didn't work

Since I don't want to make a global change in my project's pytest configuration I am more biased towards an approach which wouldn't need global setting, like the marker approach. Is there a better way to do this? Or better yet, is the issue fixed now?

suhasdotcom avatar Jul 15 '24 08:07 suhasdotcom

I ran into an issue where I was expecting this test to pass:

<some code that logs stuff>
...
out, err = capsys.readouterr()
assert len(out) > 0    

Where the following code failed:

from sys import sysout as sys_stdout
...
console_handler = StreamHandler(sys_stdout)
console_handler.setFormatter(formatter)
console_handler.setLevel(self._log_level)
self._logger.addHandler(console_handler)

But this passed:

import sys
...
console_handler = StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
console_handler.setLevel(self._log_level)
self._logger.addHandler(console_handler)

I haven't had a chance to look into why this might be the case but I figure I'll put this here in case other people run into a similar issue. Before arriving at this solution, I tried the -s and --capture=tee-sys solutions mentioned above and they both worked for me, which was consistent with @suhasdotcom's comment.

zhiweigan avatar Aug 20 '24 22:08 zhiweigan

Never use non lazy/recomputed references to sys.stdout

Studio recapture replaces those and closes old ones dozens of times

RonnyPfannschmidt avatar Aug 21 '24 08:08 RonnyPfannschmidt