Skip to content

Commit 89388e5

Browse files
committed
Capture Qt log messages
1 parent a48b50b commit 89388e5

File tree

3 files changed

+131
-2
lines changed

3 files changed

+131
-2
lines changed

pytestqt/_tests/test_logging.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import pytest
2+
3+
pytest_plugins = 'pytester'
4+
5+
6+
@pytest.mark.parametrize('test_succeds', [True, False])
7+
def test_basic_logging(testdir, test_succeds):
8+
"""
9+
:type testdir: _pytest.pytester.TmpTestdir
10+
"""
11+
testdir.makepyfile(
12+
"""
13+
from pytestqt.qt_compat import qDebug, qWarning, qCritical, qFatal
14+
15+
def test_types():
16+
qDebug('this is a DEBUG message')
17+
qWarning('this is a WARNING message')
18+
qCritical('this is a CRITICAL message')
19+
assert {0}
20+
""".format(test_succeds)
21+
)
22+
res = testdir.runpytest()
23+
if test_succeds:
24+
assert 'Captured Qt messages' not in res.stdout.str()
25+
else:
26+
res.stdout.fnmatch_lines([
27+
'*-- Captured Qt messages --*',
28+
'QtDebugMsg: this is a DEBUG message*',
29+
'QtWarningMsg: this is a WARNING message*',
30+
'QtCriticalMsg: this is a CRITICAL message*',
31+
])

pytestqt/plugin.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import collections
12
from contextlib import contextmanager
23
import functools
34
import sys
@@ -6,7 +7,8 @@
67

78
import pytest
89

9-
from pytestqt.qt_compat import QtCore, QtTest, QApplication, QT_API
10+
from pytestqt.qt_compat import QtCore, QtTest, QApplication, QT_API, \
11+
qInstallMsgHandler, QtDebugMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg
1012

1113

1214
def _inject_qtest_methods(cls):
@@ -421,6 +423,78 @@ def pytest_configure(config):
421423
"qt_no_exception_capture: Disables pytest-qt's automatic exception "
422424
'capture for just one test item.')
423425

426+
config.pluginmanager.register(QtLoggingPlugin(config), '_qt_logging')
427+
424428

425429
def pytest_report_header():
426-
return ['qt-api: %s' % QT_API]
430+
return ['qt-api: %s' % QT_API]
431+
432+
433+
class QtLoggingPlugin(object):
434+
"""
435+
Pluging responsible for installing a QtMessageHandler before each
436+
test and augment reporting if the test failed with the messages captured.
437+
"""
438+
439+
def __init__(self, config):
440+
self.config = config
441+
442+
def pytest_runtest_setup(self, item):
443+
item.qt_log_handler = _QtMessageCapture()
444+
previous_handler = qInstallMsgHandler(item.qt_log_handler.handle)
445+
item.qt_previous_handler = previous_handler
446+
447+
@pytest.mark.hookwrapper
448+
def pytest_runtest_makereport(self, item, call):
449+
"""Add captured Qt messages to test item report if the call failed."""
450+
451+
outcome = yield
452+
report = outcome.result
453+
454+
if call.when == 'call':
455+
456+
if not report.passed:
457+
long_repr = getattr(report, 'longrepr', None)
458+
if hasattr(long_repr, 'addsection'):
459+
lines = []
460+
for msg_type, msg in item.qt_log_handler.messages:
461+
msg_name = _QtMessageCapture.get_msg_name(msg_type)
462+
lines.append('{0}: {1}'.format(msg_name, msg))
463+
if lines:
464+
long_repr.addsection('Captured Qt messages',
465+
'\n'.join(lines))
466+
# Release the handler resources.
467+
qInstallMsgHandler(item.qt_previous_handler)
468+
del item.qt_previous_handler
469+
del item.qt_log_handler
470+
471+
472+
class _QtMessageCapture(object):
473+
"""
474+
Captures Qt messages when its `handle` method is installed using
475+
qInstallMsgHandler, and stores them into `messages` attribute.
476+
477+
:attr messages: list of Message named-tuples.
478+
"""
479+
480+
Message = collections.namedtuple('Message', 'msg_type, msg')
481+
482+
def __init__(self):
483+
self.messages = []
484+
485+
def handle(self, msg_type, msg):
486+
"""
487+
Method to be installed using qInstallMsgHandler, stores each message
488+
into the `messages` attribute.
489+
"""
490+
self.messages.append(self.Message(msg_type, msg))
491+
492+
@classmethod
493+
def get_msg_name(cls, msg_type):
494+
"""Returns a string representation of the given QtMsgType enum value."""
495+
return {
496+
QtDebugMsg: 'QtDebugMsg',
497+
QtWarningMsg: 'QtWarningMsg',
498+
QtCriticalMsg: 'QtCriticalMsg',
499+
QtFatalMsg: 'QtFatalMsg',
500+
}[msg_type]

pytestqt/qt_compat.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,22 @@ def _import_module(module_name):
6363
Qt = QtCore.Qt
6464
QEvent = QtCore.QEvent
6565

66+
qDebug = QtCore.qDebug
67+
qWarning = QtCore.qWarning
68+
qCritical = QtCore.qCritical
69+
qFatal = QtCore.qFatal
70+
QtDebugMsg = QtCore.QtDebugMsg
71+
QtWarningMsg = QtCore.QtWarningMsg
72+
QtCriticalMsg = QtCore.QtCriticalMsg
73+
QtFatalMsg = QtCore.QtFatalMsg
74+
6675
if QT_API == 'pyside':
6776
Signal = QtCore.Signal
6877
Slot = QtCore.Slot
6978
Property = QtCore.Property
7079
QApplication = QtGui.QApplication
7180
QWidget = QtGui.QWidget
81+
qInstallMsgHandler = QtCore.qInstallMsgHandler
7282

7383
elif QT_API in ('pyqt4', 'pyqt5'):
7484
Signal = QtCore.pyqtSignal
@@ -79,9 +89,23 @@ def _import_module(module_name):
7989
_QtWidgets = _import_module('QtWidgets')
8090
QApplication = _QtWidgets.QApplication
8191
QWidget = _QtWidgets.QWidget
92+
93+
def qInstallMsgHandler(handler):
94+
"""
95+
Installs the given function as a message handler. This
96+
will adapt Qt5 message handler signature into Qt4
97+
message handler's signature.
98+
"""
99+
def _Qt5MessageHandler(msg_type, context, msg):
100+
handler(msg_type, msg)
101+
if handler is not None:
102+
return QtCore.qInstallMessageHandler(_Qt5MessageHandler)
103+
else:
104+
return QtCore.qInstallMessageHandler(None)
82105
else:
83106
QApplication = QtGui.QApplication
84107
QWidget = QtGui.QWidget
108+
qInstallMsgHandler = QtCore.qInstallMsgHandler
85109

86110
else: # pragma: no cover
87111
USING_PYSIDE = True

0 commit comments

Comments
 (0)