Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
butla opened this issue Oct 18, 2019 · 27 comments
Open
Labels
plugin: capture related to the capture builtin plugin plugin: logging related to the logging builtin plugin topic: fixtures anything involving fixtures directly or indirectly type: bug problem that needs to be addressed

Comments

@butla
Copy link

butla commented Oct 18, 2019

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.

@asottile
Copy link
Member

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

@butla
Copy link
Author

butla commented Oct 18, 2019

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

@Zac-HD Zac-HD added plugin: capture related to the capture builtin plugin topic: fixtures anything involving fixtures directly or indirectly type: bug problem that needs to be addressed labels Oct 20, 2019
@asottile asottile added the plugin: logging related to the logging builtin plugin label Oct 21, 2019
@blueyed
Copy link
Contributor

blueyed commented Nov 1, 2019

Related (I think): #3448

@MarcoGorelli
Copy link
Contributor

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

@blueyed
Copy link
Contributor

blueyed commented Nov 10, 2019

@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.

@MarcoGorelli
Copy link
Contributor

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 :)

@ahal
Copy link

ahal commented Feb 20, 2020

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.

abravalheri added a commit to pyscaffold/pyscaffoldext-django that referenced this issue Nov 3, 2020
It seems that the bug described in pytest-dev/pytest#5997
is preventing stderr to be captured by capsys.

The workaround consists in using both `capfd` and `--capture=tee-sys`
(or just `-s`).
abravalheri added a commit to pyscaffold/pyscaffoldext-cookiecutter that referenced this issue Nov 3, 2020
It seems that the bug described in pytest-dev/pytest#5997
is preventing stderr to be captured by capsys.

The workaround consists in using both `capfd` and `--capture=tee-sys`
(or just `-s`).
cgalibern added a commit to cvaroqui/opensvc that referenced this issue Dec 1, 2020
Capture logs may fail on OSX, not sure if this also happen on other Os,
to avoid this we replace capsys fixture by capfd, and use pytest '-s'
See also: pytest-dev/pytest#5997

New test rules added
o TestCreateAddDecodeFromWithSymlinks
  o test_everything_succeed_when_no_broken_symlinks
  o test_add_exit_code_0_even_when_broken_symlinks
  o test_add_creates_keys_for_all_non_broken_symlinks
  o test_can_decode_keys_of_non_broken_symlinks
  o test_warn_about_broken_symlinks
cgalibern added a commit to cvaroqui/opensvc that referenced this issue Dec 1, 2020
Workaround for capsys, capfd failure during capture logging
See also: pytest-dev/pytest#5997
abravalheri added a commit to pyscaffold/pyscaffold that referenced this issue Jan 8, 2021
@larsks
Copy link

larsks commented Mar 20, 2021

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

@MarcoGorelli
Copy link
Contributor

Nope, I gave up on this 😳

@taranlu-houzz
Copy link

taranlu-houzz commented Apr 10, 2021

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.

@asottile
Copy link
Member

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

@RonnyPfannschmidt
Copy link
Member

@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

@asottile
Copy link
Member

@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
Copy link
Member

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

@chamini2
Copy link

I am wondering what happend with this @asottile ?

@asottile
Copy link
Member

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)

@chamini2
Copy link

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

@Kache
Copy link
Contributor

Kache commented Feb 3, 2022

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...

@vakorol
Copy link

vakorol commented Mar 13, 2023

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.

@mauvilsa
Copy link

mauvilsa commented May 9, 2023

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()

@PaleNeutron
Copy link

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?

@asottile
Copy link
Member

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

@Feuermurmel
Copy link

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

@RonnyPfannschmidt
Copy link
Member

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

@suhasdotcom
Copy link

suhasdotcom commented Jul 15, 2024

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?

@zhiweigan
Copy link

zhiweigan commented Aug 20, 2024

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.

@RonnyPfannschmidt
Copy link
Member

Never use non lazy/recomputed references to sys.stdout

Studio recapture replaces those and closes old ones dozens of times

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
plugin: capture related to the capture builtin plugin plugin: logging related to the logging builtin plugin topic: fixtures anything involving fixtures directly or indirectly type: bug problem that needs to be addressed
Projects
None yet
Development

No branches or pull requests