Skip to content

Disable qtlog #58

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

Merged
merged 5 commits into from
Jun 29, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions docs/logging.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ For example:
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``:
Expand All @@ -64,7 +66,7 @@ emitted messages directly to ``stderr``:
this is a WARNING message


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

def do_something():
Expand All @@ -75,10 +77,30 @@ to check if certain messages were emitted during a test::
emitted = [(m.type, m.message.strip()) for m in qtlog.records]
assert emitted == [(QtWarningMsg, 'this is a WARNING message')]

Keep in mind that when ``--no-qt-log`` is passed in the command line,
``qtlog.records`` will always be an empty list. See
:class:`Record <pytestqt.plugin.Record>` for reference documentation on
``Record`` objects.

``qtlog.records`` is a list of :class:`Record <pytestqt.plugin.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:

.. code-block:: python

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
Expand Down
72 changes: 67 additions & 5 deletions pytestqt/_tests/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
pytest_plugins = 'pytester'


@pytest.mark.parametrize('test_succeds, qt_log',
@pytest.mark.parametrize('test_succeeds, qt_log',
[(True, True), (True, False), (False, False),
(False, True)])
def test_basic_logging(testdir, test_succeds, qt_log):
def test_basic_logging(testdir, test_succeeds, qt_log):
"""
Test Qt logging capture output.

Expand All @@ -26,10 +26,10 @@ def test_types():
qWarning('this is a WARNING message')
qCritical('this is a CRITICAL message')
assert {0}
""".format(test_succeds)
""".format(test_succeeds)
)
res = testdir.runpytest(*(['--no-qt-log'] if not qt_log else []))
if test_succeds:
if test_succeeds:
assert 'Captured Qt messages' not in res.stdout.str()
assert 'Captured stderr call' not in res.stdout.str()
else:
Expand Down Expand Up @@ -87,6 +87,67 @@ def test_types(qtlog):
res.stdout.fnmatch_lines('*1 passed*')


@pytest.mark.parametrize('use_context_manager', [True, False])
def test_disable_qtlog_context_manager(testdir, use_context_manager):
"""
Test qtlog.disabled() context manager.

:type testdir: _pytest.pytester.TmpTestdir
"""
testdir.makeini(
"""
[pytest]
qt_log_level_fail = CRITICAL
"""
)

if use_context_manager:
code = 'with qtlog.disabled():'
else:
code = 'if 1:'

testdir.makepyfile(
"""
from pytestqt.qt_compat import qCritical
def test_1(qtlog):
{code}
qCritical('message')
""".format(code=code)
)
res = testdir.inline_run()
passed = 1 if use_context_manager else 0
res.assertoutcome(passed=passed, failed=int(not passed))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that's useful... I checked the output in pytest-mccabe because that's what pytest-flakes did, and I wondered if I really should do things that way.



@pytest.mark.parametrize('use_mark', [True, False])
def test_disable_qtlog_mark(testdir, use_mark):
"""
Test mark which disables logging capture for a test.

:type testdir: _pytest.pytester.TmpTestdir
"""
testdir.makeini(
"""
[pytest]
qt_log_level_fail = CRITICAL
"""
)
mark = '@pytest.mark.no_qt_log' if use_mark else ''

testdir.makepyfile(
"""
from pytestqt.qt_compat import qCritical
import pytest
{mark}
def test_1():
qCritical('message')
""".format(mark=mark)
)
res = testdir.inline_run()
passed = 1 if use_mark else 0
res.assertoutcome(passed=passed, failed=int(not passed))


def test_logging_formatting(testdir):
"""
Test custom formatting for logging messages.
Expand Down Expand Up @@ -345,7 +406,8 @@ def test_context_none(testdir):
def test_foo(request):
log_capture = request.node.qt_log_capture
context = log_capture._Context(None, None, None)
log_capture._handle(QtWarningMsg, "WARNING message", context)
log_capture._handle_with_context(QtWarningMsg,
context, "WARNING message")
assert 0
"""
)
Expand Down
89 changes: 72 additions & 17 deletions pytestqt/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
import re

from pytestqt.qt_compat import QtCore, QtTest, QApplication, QT_API, \
qInstallMsgHandler, QtDebugMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg
qInstallMsgHandler, qInstallMessageHandler, QtDebugMsg, QtWarningMsg, \
QtCriticalMsg, QtFatalMsg


def _inject_qtest_methods(cls):
Expand Down Expand Up @@ -631,30 +632,33 @@ def __init__(self, config):
self.config = config

def pytest_runtest_setup(self, item):
if item.get_marker('no_qt_log'):
return
m = item.get_marker('qt_log_ignore')
if m:
ignore_regexes = m.args
else:
ignore_regexes = self.config.getini('qt_log_ignore')
item.qt_log_capture = _QtMessageCapture(ignore_regexes)
previous_handler = qInstallMsgHandler(item.qt_log_capture._handle)
item.qt_previous_handler = previous_handler
item.qt_log_capture._start()

@pytest.mark.hookwrapper
def pytest_runtest_makereport(self, item, call):
"""Add captured Qt messages to test item report if the call failed."""

outcome = yield
report = outcome.result

m = item.get_marker('qt_log_level_fail')
if m:
log_fail_level = m.args[0]
else:
log_fail_level = self.config.getini('qt_log_level_fail')
assert log_fail_level in QtLoggingPlugin.LOG_FAIL_OPTIONS
if not hasattr(item, 'qt_log_capture'):
return

if call.when == 'call':
report = outcome.result

m = item.get_marker('qt_log_level_fail')
if m:
log_fail_level = m.args[0]
else:
log_fail_level = self.config.getini('qt_log_level_fail')
assert log_fail_level in QtLoggingPlugin.LOG_FAIL_OPTIONS

# make test fail if any records were captured which match
# log_fail_level
Expand Down Expand Up @@ -682,15 +686,14 @@ def pytest_runtest_makereport(self, item, call):
long_repr.addsection('Captured Qt messages',
'\n'.join(lines))

qInstallMsgHandler(item.qt_previous_handler)
del item.qt_previous_handler
item.qt_log_capture._stop()
del item.qt_log_capture


class _QtMessageCapture(object):
"""
Captures Qt messages when its `handle` method is installed using
qInstallMsgHandler, and stores them into `messages` attribute.
qInstallMsgHandler, and stores them into `records` attribute.

:attr _records: list of Record instances.
:attr _ignore_regexes: list of regexes (as strings) that define if a record
Expand All @@ -700,13 +703,51 @@ class _QtMessageCapture(object):
def __init__(self, ignore_regexes):
self._records = []
self._ignore_regexes = ignore_regexes or []
self._previous_handler = None

def _start(self):
"""
Start receiving messages from Qt.
"""
if qInstallMsgHandler:
previous_handler = qInstallMsgHandler(self._handle_no_context)
else:
assert qInstallMessageHandler
previous_handler = qInstallMessageHandler(self._handle_with_context)
self._previous_handler = previous_handler

def _stop(self):
"""
Stop receiving messages from Qt, restoring the previously installed
handler.
"""
if qInstallMsgHandler:
qInstallMsgHandler(self._previous_handler)
else:
assert qInstallMessageHandler
qInstallMessageHandler(self._previous_handler)

@contextmanager
def disabled(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps call this disable instead of disabled? Doing qtlog.disable() seems more natural, disabled sounds more like an attribute to check whether something is currently disabled.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm perhaps... I named it from "disabled within the context".

Either way, I think it is too late to rename it now without breaking semantic versioning... 😶

"""
Context manager that temporarily disables logging capture while
inside it.
"""
self._stop()
try:
yield
finally:
self._start()

_Context = namedtuple('_Context', 'file function line')

def _handle(self, msg_type, message, context=None):
def _append_new_record(self, msg_type, message, context):
"""
Method to be installed using qInstallMsgHandler, stores each message
into the `messages` attribute.
Creates a new Record instance and stores it.

:param msg_type: Qt message typ
:param message: message string, if bytes it will be converted to str.
:param context: QMessageLogContext object or None
"""
def to_unicode(s):
if isinstance(s, bytes):
Expand All @@ -730,6 +771,20 @@ def to_unicode(s):

self._records.append(Record(msg_type, message, ignored, context))

def _handle_no_context(self, msg_type, message):
"""
Method to be installed using qInstallMsgHandler (Qt4),
stores each message into the `_records` attribute.
"""
self._append_new_record(msg_type, message, context=None)

def _handle_with_context(self, msg_type, context, message):
"""
Method to be installed using qInstallMessageHandler (Qt5),
stores each message into the `_records` attribute.
"""
self._append_new_record(msg_type, message, context=context)

@property
def records(self):
"""Access messages captured so far.
Expand Down
20 changes: 7 additions & 13 deletions pytestqt/qt_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ def _import_module(module_name):
QtCriticalMsg = QtCore.QtCriticalMsg
QtFatalMsg = QtCore.QtFatalMsg

# Qt4 and Qt5 have different functions to install a message handler;
# the plugin will try to use the one that is not None
qInstallMsgHandler = None
qInstallMessageHandler = None

if QT_API == 'pyside':
Signal = QtCore.Signal
Slot = QtCore.Slot
Expand All @@ -93,19 +98,7 @@ def _import_module(module_name):
_QtWidgets = _import_module('QtWidgets')
QApplication = _QtWidgets.QApplication
QWidget = _QtWidgets.QWidget

def qInstallMsgHandler(handler):
"""
Installs the given function as a message handler. This
will adapt Qt5 message handler signature into Qt4
message handler's signature.
"""
def _Qt5MessageHandler(msg_type, context, msg):
handler(msg_type, msg, context)
if handler is not None:
return QtCore.qInstallMessageHandler(_Qt5MessageHandler)
else:
return QtCore.qInstallMessageHandler(None)
qInstallMessageHandler = QtCore.qInstallMessageHandler
else:
QApplication = QtGui.QApplication
QWidget = QtGui.QWidget
Expand Down Expand Up @@ -141,6 +134,7 @@ def __getattr__(cls, name):
QApplication = Mock()
QWidget = Mock()
qInstallMsgHandler = Mock()
qInstallMessageHandler = Mock()
qDebug = Mock()
qWarning = Mock()
qCritical = Mock()
Expand Down