Skip to content

Capturing, with and without stdlib logging #2079

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

Closed
SwampFalc opened this issue Nov 23, 2016 · 3 comments
Closed

Capturing, with and without stdlib logging #2079

SwampFalc opened this issue Nov 23, 2016 · 3 comments

Comments

@SwampFalc
Copy link

So I find myself rather confused by output capturing.

Let's start by quoting the manual:

def test_myoutput(capsys): # or use "capfd" for fd-level
    print ("hello")
    sys.stderr.write("world\n")
    out, err = capsys.readouterr()
    assert out == "hello\n"
    assert err == "world\n"
    print ("next")
    out, err = capsys.readouterr()
    assert out == "next\n"

I run this with pytest test_me.py and I get a pass.

Then I run this with pytest -s test_me.py, and I still get a pass. Which according to the manual doesn't make sense. -s is described as disabling capturing. So either I'm not grasping something crucial, or the manual is wrong, or there's a bug in the implementation of -s.

I then become even more confused when I try to test the behaviour of the stdlib logging module. I don't mean testing the module, but testing that my app emits the right log entries in the right circumstances.

So I make this example code:

import sys
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)
handler = logging.StreamHandler(sys.stdout)
logger.addHandler(handler)

def test_myoutput(capsys):
    logger.error('something')
    out, err = capsys.readouterr()
    with capsys.disabled():
        print 'E:' + err
        print 'O:' + out

I run this without -s:

E:
O:

Then I run it with -s:

something
E:
O:

So at first sight this makes some sense. The logging output happens, it just gets captured somewhere, unless I specify -s. So in this case -s does seem to work exactly as described, the only remaining issue is that capsys for some reason doesn't capture what logging emits on sys.stdout...

So we do the obvious next step and replace all occurrences of capsys with capfd!

Run without -s:

E:
O:

Run with -s:

E:
O: something

... What? Without -s the logging output still vanishes into thin air, but now it has actually been captured with -s? So in this case the flag actually turns capturing on, when the description is that it turns capturing off?

Could someone explain what I'm missing here, or what I'm doing wroing?

(I'm aware I could reconfigure my logger so that it goes to a file or something and avoid this, but I'd prefer to stick as closely as possible to my production code)

@nicoddemus
Copy link
Member

Which according to the manual doesn't make sense. -s is described as disabling capturing. So either I'm not grasping something crucial, or the manual is wrong, or there's a bug in the implementation of -s.

-s disables general output capturing of sys.stdout and sys.stderr, but doesn't really have effect on capsys or capfd. Those fixtures will always capture output, otherwise tests which use those fixtures would fail if users pass -s on the command line, which is not desired.

I then become even more confused when I try to test the behaviour of the stdlib logging module.

I will try to explain what's happening.

First, let's remember that -s is a shortcut to --capture=no. Other possible values are fd and sys with fd being the default, so not passing s means --capture=fd. I will use --capture to try to explain what's happening because it is clear than "with/without -s". Also I will only talk about sys.stdout below, but all the points are valid for sys.stderr as well.

The problem is that your example is configuring logging at the module level. What's happening is this:

  1. When pytest starts with --capture=fd, it will replace sys.stdout's file descriptor by a separate file stream, say stream_a.
  2. When your module is collected, you configure your logger at the module level so the logger will write to stream_a.
  3. Your test starts and capsys is created. capsys will then redirect sys.stdout to a temporary file by using the "sys" method (which just changes the sys.stdout reference to another file). Let's call that file stream_b.

That's why you don't see "something" on your test, the contents of logger.error('something') is going to stream_a. When you pass --capture=no, no stream redirection occurs and you see everything that is printed by modules and tests.

Your capfd example has the same problem, because capfd will also redirect the streams, only using the fd method instead which works at the C level and extensions.

All boils down to the fact that you are configuring the module at the module level, which I know is standard practice. Unfortunately I'm not sure how you can use capsys and capfd to capture logging for you.

I suggest that you use the pytest-catchlog instead, which provides a caplog fixture that helps you check what has been loggged.

@SwampFalc
Copy link
Author

Thank you first and foremost for pointing me to pytest-catchlog, I will see if that can solve my issue.

As for the confusion about capturing...

Thank you for explaining that 'capturing' in the context of the command-line flags, and 'capturing' in the context of capsys/capfd, are two wholly distinct and separate things.

Perhaps some terminology changes could be made to prevent such confusion for other users? In the context of the command-line flags, I feel the term 'echoing' could be used: the user chooses whether he wants to have the actual stderr/stdout output of his command echoed in pytest's output or not, and if yes through what mechanism.

@nicoddemus
Copy link
Member

How about adding a note about it in the capture page explaining the distinction? A PR would be most welcome. 😁

Closing this for now then, feel free to reopen if you feel otherwise.

dopplershift added a commit to dopplershift/siphon that referenced this issue May 23, 2017
Pytest 3.1.0 broke our current use of capfd, but that doesn't seem to be
a good way to handle capturing of log messages. (pytest-dev/pytest#2079)
So instead use pytest-catchlog, which was recommended in that issue and
seems to be maintained.
dopplershift added a commit to dopplershift/siphon that referenced this issue Jul 19, 2017
Pytest 3.1.0 broke our current use of capfd, but that doesn't seem to be
a good way to handle capturing of log messages. (pytest-dev/pytest#2079)
So instead use pytest-catchlog, which was recommended in that issue and
seems to be maintained.
dopplershift added a commit to dopplershift/siphon that referenced this issue Jul 19, 2017
Pytest 3.1.0 broke our current use of capfd, but that doesn't seem to be
a good way to handle capturing of log messages. (pytest-dev/pytest#2079)
So instead use pytest-catchlog, which was recommended in that issue and
seems to be maintained.
zmwangx added a commit to zmwangx/caterpillar that referenced this issue Jun 6, 2018
capfd.readouterr().err is hit and miss for the logged messages, for
whatever reason. Maybe [1] explains it, but I didn't bother to read.

[1] pytest-dev/pytest#2079
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants