Qt Logging Capture

New in version 1.4.

Qt features its own logging mechanism through qInstallMsgHandler (qInstallMessageHandler on Qt5) and qDebug, qWarning, qCritical functions. These are used by Qt to print warning messages when internal errors occur.

pytest-qt automatically captures these messages and displays them when a test fails, similar to what pytest does for stderr and stdout and the pytest-catchlog plugin. For example:

from pytestqt.qt_compat import qWarning

def do_something():
    qWarning('this is a WARNING message')

def test_foo(qtlog):
    do_something()
    assert 0
$ py.test test.py -q
F
================================== FAILURES ===================================
_________________________________ test_types __________________________________

    def test_foo():
        do_something()
>       assert 0
E       assert 0

test.py:8: AssertionError
---------------------------- Captured Qt messages -----------------------------
QtWarningMsg: this is a WARNING message
1 failed in 0.01 seconds

Disabling Logging Capture

Qt logging capture can be disabled altogether by passing the --no-qt-log to the command line, which will fallback to the default Qt bahavior of printing emitted messages directly to stderr:

py.test test.py -q --no-qt-log
F
================================== FAILURES ===================================
_________________________________ test_types __________________________________

    def test_foo():
        do_something()
>       assert 0
E       assert 0

test.py:8: AssertionError
---------------------------- Captured stderr call -----------------------------
this is a WARNING message

pytest-qt also provides a qtlog fixture that can used to check if certain messages were emitted during a test:

def do_something():
    qWarning('this is a WARNING message')

def test_foo(qtlog):
    do_something()
    emitted = [(m.type, m.message.strip()) for m in qtlog.records]
    assert emitted == [(QtWarningMsg, 'this is a WARNING message')]

qtlog.records is a list of Record instances.

Logging can also be disabled on a block of code using the qtlog.disabled() context manager, or with the pytest.mark.no_qt_log mark:

def test_foo(qtlog):
    with qtlog.disabled():
        # logging is disabled within the context manager
        do_something()

@pytest.mark.no_qt_log
def test_bar():
    # logging is disabled for the entire test
    do_something()

Keep in mind that when logging is disabled, qtlog.records will always be an empty list.

Log Formatting

The output format of the messages can also be controlled by using the --qt-log-format command line option, which accepts a string with standard {} formatting which can make use of attribute interpolation of the record objects:

$ py.test test.py --qt-log-format="{rec.when} {rec.type_name}: {rec.message}"

Keep in mind that you can make any of the options above the default for your project by using pytest’s standard addopts option in you pytest.ini file:

[pytest]
qt_log_format = {rec.when} {rec.type_name}: {rec.message}

Automatically failing tests when logging messages are emitted

Printing messages to stderr is not the best solution to notice that something might not be working as expected, specially when running in a continuous integration server where errors in logs are rarely noticed.

You can configure pytest-qt to automatically fail a test if it emits a message of a certain level or above using the qt_log_level_fail ini option:

[pytest]
qt_log_level_fail = CRITICAL

With this configuration, any test which emits a CRITICAL message or above will fail, even if no actual asserts fail within the test:

from pytestqt.qt_compat import qCritical

def do_something():
    qCritical('WM_PAINT failed')

def test_foo(qtlog):
    do_something()
>py.test test.py --color=no -q
F
================================== FAILURES ===================================
__________________________________ test_foo ___________________________________
test.py:5: Failure: Qt messages with level CRITICAL or above emitted
---------------------------- Captured Qt messages -----------------------------
QtCriticalMsg: WM_PAINT failed

The possible values for qt_log_level_fail are:

  • NO: disables test failure by log messages.
  • DEBUG: messages emitted by qDebug function or above.
  • WARNING: messages emitted by qWarning function or above.
  • CRITICAL: messages emitted by qCritical function only.

If some failures are known to happen and considered harmless, they can be ignored by using the qt_log_ignore ini option, which is a list of regular expressions matched using re.search:

[pytest]
qt_log_level_fail = CRITICAL
qt_log_ignore =
    WM_DESTROY.*sent
    WM_PAINT failed
py.test test.py --color=no -q
.
1 passed in 0.01 seconds

Messages which do not match any of the regular expressions defined by qt_log_ignore make tests fail as usual:

def do_something():
    qCritical('WM_PAINT not handled')
    qCritical('QObject: widget destroyed in another thread')

def test_foo(qtlog):
    do_something()
py.test test.py --color=no -q
F
================================== FAILURES ===================================
__________________________________ test_foo ___________________________________
test.py:6: Failure: Qt messages with level CRITICAL or above emitted
---------------------------- Captured Qt messages -----------------------------
QtCriticalMsg: WM_PAINT not handled  (IGNORED)
QtCriticalMsg: QObject: widget destroyed in another thread

You can also override qt_log_level_fail and qt_log_ignore settins from pytest.ini in some tests by using a mark with the same name:

def do_something():
    qCritical('WM_PAINT not handled')
    qCritical('QObject: widget destroyed in another thread')

@pytest.mark.qt_log_level_fail('CRITICAL')
@pytest.mark.qt_log_ignore('WM_DESTROY.*sent', 'WM_PAINT failed')
def test_foo(qtlog):
    do_something()