Skip to content

Commit 7ca57d7

Browse files
committed
Update documentation for Qt logging
1 parent fe92893 commit 7ca57d7

File tree

2 files changed

+231
-13
lines changed

2 files changed

+231
-13
lines changed

docs/index.rst

Lines changed: 220 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ activate a new fresh environment and execute::
6666

6767
.. _virtualenv: http://virtualenv.readthedocs.org/
6868

69-
Quick Tutorial
70-
==============
69+
Tutorial
70+
========
7171

7272
``pytest-qt`` registers a new fixture_ named ``qtbot``, which acts as *bot* in the sense
7373
that it can send keyboard and mouse events to any widgets being tested. This way, the programmer
@@ -261,13 +261,225 @@ You can disable the automatic exception hook on individual tests by using a
261261

262262
Or even disable it for your entire project in your ``pytest.ini`` file::
263263

264+
.. code-block:: ini
265+
264266
[pytest]
265267
qt_no_exception_capture = 1
266268
267269
This might be desirable if you plan to install a custom exception hook.
268270

271+
Qt Logging Capture
272+
==================
273+
274+
.. versionadded:: 1.4
275+
276+
Qt features its own logging mechanism through ``qInstallMsgHandler``
277+
(``qInstallMessageHandler`` on Qt5) and ``qDebug``, ``qWarning``, ``qCritical``
278+
functions. These are used by Qt to print warning messages when internal errors
279+
occur.
280+
281+
``pytest-qt`` automatically captures these messages and displays them when a
282+
test fails, similar to what ``pytest`` does for ``stderr`` and ``stdout`` and
283+
the `pytest-catchlog <https://github.com/eisensheng/pytest-catchlog>`_ plugin.
284+
For example:
285+
286+
.. code-block:: python
287+
288+
from pytestqt.qt_compat import qWarning
289+
290+
def do_something():
291+
qWarning('this is a WARNING message')
292+
293+
def test_foo(qtlog):
294+
do_something()
295+
assert 0
296+
297+
298+
.. code-block:: bash
299+
300+
$ py.test test.py -q
301+
F
302+
================================== FAILURES ===================================
303+
_________________________________ test_types __________________________________
304+
305+
def test_foo():
306+
do_something()
307+
> assert 0
308+
E assert 0
309+
310+
test.py:8: AssertionError
311+
---------------------------- Captured Qt messages -----------------------------
312+
QtWarningMsg: this is a WARNING message
313+
1 failed in 0.01 seconds
314+
315+
316+
Qt logging capture can be disabled altogether by passing the ``--no-qt-log``
317+
to the command line, which will fallback to the default Qt bahavior of printing
318+
emitted messages directly to ``stderr``:
319+
320+
.. code-block:: bash
321+
322+
py.test test.py -q --no-qt-log
323+
F
324+
================================== FAILURES ===================================
325+
_________________________________ test_types __________________________________
326+
327+
def test_foo():
328+
do_something()
329+
> assert 0
330+
E assert 0
331+
332+
test.py:8: AssertionError
333+
---------------------------- Captured stderr call -----------------------------
334+
this is a WARNING message
335+
336+
337+
``pytest-qt`` also provides a ``qtlog`` fixture, which tests can use
338+
to check if certain messages were emitted during a test::
339+
340+
def do_something():
341+
qWarning('this is a WARNING message')
342+
343+
def test_foo(qtlog):
344+
do_something()
345+
emitted = [(m.type, m.message.strip()) for m in qtlog.records]
346+
assert emitted == [(QtWarningMsg, 'this is a WARNING message')]
347+
348+
Keep in mind that when ``--no-qt-log`` is passed in the command line,
349+
``qtlog.records`` will always be an empty list. See
350+
:class:`Record <pytestqt.plugin.Record>` for reference documentation on
351+
``Record`` objects.
352+
353+
The output format of the messages can also be controlled by using the
354+
``--qt-log-format`` command line option, which accepts a string with standard
355+
``{}`` formatting which can make use of attribute interpolation of the record
356+
objects:
357+
358+
.. code-block:: bash
359+
360+
$ py.test test.py --qt-log-format="{rec.when} {rec.type_name}: {rec.message}"
361+
362+
Keep in mind that you can make any of the options above the default
363+
for your project by using pytest's standard ``addopts`` option in you
364+
``pytest.ini`` file:
365+
366+
367+
.. code-block:: ini
368+
369+
[pytest]
370+
qt_log_format = {rec.when} {rec.type_name}: {rec.message}
371+
372+
373+
Automatically failing tests when logging messages are emitted
374+
-------------------------------------------------------------
375+
376+
Printing messages to ``stderr`` is not the best solution to notice that
377+
something might not be working as expected, specially when running in a
378+
continuous integration server where errors in logs are rarely noticed.
379+
380+
You can configure ``pytest-qt`` to automatically fail a test if it emits
381+
a message of a certain level or above using the ``qt_log_level_fail`` ini
382+
option:
383+
384+
385+
.. code-block:: ini
386+
387+
[pytest]
388+
qt_log_level_fail = CRITICAL
389+
390+
With this configuration, any test which emits a CRITICAL message or above
391+
will fail, even if no actual asserts fail within the test:
392+
393+
.. code-block:: python
394+
395+
from pytestqt.qt_compat import qCritical
396+
397+
def do_something():
398+
qCritical('WM_PAINT failed')
399+
400+
def test_foo(qtlog):
401+
do_something()
402+
403+
404+
.. code-block:: bash
405+
406+
>py.test test.py --color=no -q
407+
F
408+
================================== FAILURES ===================================
409+
__________________________________ test_foo ___________________________________
410+
test.py:5: Failure: Qt messages with level CRITICAL or above emitted
411+
---------------------------- Captured Qt messages -----------------------------
412+
QtCriticalMsg: WM_PAINT failed
413+
414+
The possible values for ``qt_log_level_fail`` are:
415+
416+
* ``NO``: disables test failure by log messages.
417+
* ``DEBUG``: messages emitted by ``qDebug`` function or above.
418+
* ``WARNING``: messages emitted by ``qWarning`` function or above.
419+
* ``CRITICAL``: messages emitted by ``qCritical`` function only.
420+
421+
If some failures are known to happen and considered harmless, they can
422+
be ignored by using the ``qt_log_ignore`` ini option, which
423+
is a list of regular expressions matched using ``re.search``:
424+
425+
.. code-block:: ini
426+
427+
[pytest]
428+
qt_log_level_fail = CRITICAL
429+
qt_log_ignore =
430+
WM_DESTROY.*sent
431+
WM_PAINT failed
432+
433+
.. code-block:: bash
434+
435+
py.test test.py --color=no -q
436+
.
437+
1 passed in 0.01 seconds
438+
439+
440+
Messages which do not match any of the regular expressions
441+
defined by ``qt_log_ignore`` make tests fail as usual:
442+
443+
.. code-block:: python
444+
445+
def do_something():
446+
qCritical('WM_PAINT not handled')
447+
qCritical('QObject: widget destroyed in another thread')
448+
449+
def test_foo(qtlog):
450+
do_something()
451+
452+
.. code-block:: bash
453+
454+
py.test test.py --color=no -q
455+
F
456+
================================== FAILURES ===================================
457+
__________________________________ test_foo ___________________________________
458+
test.py:6: Failure: Qt messages with level CRITICAL or above emitted
459+
---------------------------- Captured Qt messages -----------------------------
460+
QtCriticalMsg: WM_PAINT not handled (IGNORED)
461+
QtCriticalMsg: QObject: widget destroyed in another thread
462+
463+
464+
You can also override ``qt_log_level_fail`` and ``qt_log_ignore`` settins
465+
from ``pytest.ini`` in some tests by using a mark with the same name:
466+
467+
.. code-block:: python
468+
469+
def do_something():
470+
qCritical('WM_PAINT not handled')
471+
qCritical('QObject: widget destroyed in another thread')
472+
473+
@pytest.mark.qt_log_level_fail('CRITICAL')
474+
@pytest.mark.qt_log_ignore('WM_DESTROY.*sent', 'WM_PAINT failed')
475+
def test_foo(qtlog):
476+
do_something()
477+
478+
Reference
479+
=========
480+
269481
QtBot
270-
=====
482+
-----
271483

272484
.. module:: pytestqt.plugin
273485
.. autoclass:: QtBot
@@ -280,6 +492,11 @@ Signals
280492

281493
.. autoclass:: SignalTimeoutError
282494

495+
Log Capture
496+
-----------
497+
498+
.. autoclass:: Record
499+
283500
Versioning
284501
==========
285502

pytestqt/plugin.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -294,8 +294,8 @@ def waitSignals(self, signals=None, timeout=1000, raising=False):
294294
Stops current test until all given signals are triggered.
295295
296296
Used to stop the control flow of a test until all (and only
297-
all) signals are emitted, or a number of milliseconds, specified by
298-
``timeout``, has elapsed.
297+
all) signals are emitted or the number of milliseconds specified by
298+
``timeout`` has elapsed.
299299
300300
Best used as a context manager::
301301
@@ -723,14 +723,15 @@ def records(self):
723723
class Record(object):
724724
"""Hold information about a message sent by one of Qt log functions.
725725
726-
:attr str message: message contents.
727-
:attr Qt.QtMsgType type: enum that identifies message type
728-
:attr str type_name: `type` as a string
729-
:attr str log_type_name:
730-
type name similar to the logging package, for example ``DEBUG``,
731-
``WARNING``, etc.
732-
:attr datetime.datetime when: when the message was sent
733-
:attr ignored: If this record matches a regex from the "qt_log_ignore"
726+
:ivar str message: message contents.
727+
:ivar Qt.QtMsgType type: enum that identifies message type
728+
:ivar str type_name: ``type`` as string: ``"QtDebugMsg"``,
729+
``"QtWarningMsg"`` or ``"QtCriticalMsg"``.
730+
:ivar str log_type_name:
731+
type name similar to the logging package: ``DEBUG``,
732+
``WARNING`` and ``CRITICAL``.
733+
:ivar datetime.datetime when: when the message was captured
734+
:ivar bool ignored: If this record matches a regex from the "qt_log_ignore"
734735
option.
735736
"""
736737

0 commit comments

Comments
 (0)