Skip to content

Option for disabling automatic exception capture #28

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 4 commits into from
Nov 6, 2014
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
62 changes: 62 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,68 @@ ensuring the results are correct::
assert_application_results(app)


Exceptions in virtual methods
=============================

It is common in Qt programming to override virtual C++ methods to customize
behavior, like listening for mouse events, implement drawing routines, etc.

Fortunately, both ``PyQt`` and ``PySide`` support overriding this virtual methods
naturally in your python code::

class MyWidget(QWidget):

# mouseReleaseEvent
def mouseReleaseEvent(self, ev):
print('mouse released at: %s' % ev.pos())

This works fine, but if python code in Qt virtual methods raise an exception
``PyQt`` and ``PySide`` will just print the exception traceback to standard
error, since this method is called deep within Qt's even loop handling and
exceptions are not allowed at that point.

This might be surprising for python users which are used to exceptions
being raised at the calling point: for example, the following code will just
print a stack trace without raising any exception::

class MyWidget(QWidget):

def mouseReleaseEvent(self, ev):
raise RuntimeError('unexpected error')

w = MyWidget()
QTest.mouseClick(w, QtCore.Qt.LeftButton)


To make testing Qt code less surprising, ``pytest-qt`` automatically
installs an exception hook which captures errors and fails tests when exceptions
are raised inside virtual methods, like this::

E Failed: Qt exceptions in virtual methods:
E ________________________________________________________________________________
E File "x:\pytest-qt\pytestqt\_tests\test_exceptions.py", line 14, in event
E raise RuntimeError('unexpected error')
E
E RuntimeError: unexpected error


Disabling the automatic exception hook
--------------------------------------

You can disable the automatic exception hook on individual tests by using a
``qt_no_exception_capture`` marker::

@pytest.mark.qt_no_exception_capture
def test_buttons(qtbot):
...

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

[pytest]
qt_no_exception_capture = 1

This might be desirable if you plan to install a custom exception hook.

QtBot
=====

Expand Down
42 changes: 40 additions & 2 deletions pytestqt/_tests/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import textwrap
import pytest
import sys
from pytestqt.plugin import capture_exceptions, format_captured_exceptions
from pytestqt.qt_compat import QtGui, Qt, QtCore


pytest_plugins = 'pytester'


class Receiver(QtCore.QObject):
"""
Dummy QObject subclass that raises an error on receiving events if
Expand Down Expand Up @@ -45,4 +47,40 @@ def test_format_captured_exceptions():
lines = obtained_text.splitlines()

assert 'Qt exceptions in virtual methods:' in lines
assert 'ValueError: errors were made' in lines
assert 'ValueError: errors were made' in lines


@pytest.mark.parametrize('no_capture_by_marker', [True, False])
def test_no_capture(testdir, no_capture_by_marker):
"""
Make sure options that disable exception capture are working (either marker
or ini configuration value).
:type testdir: TmpTestdir
"""
if no_capture_by_marker:
marker_code = '@pytest.mark.qt_no_exception_capture'
else:
marker_code = ''
testdir.makeini('''
[pytest]
qt_no_exception_capture = 1
''')
testdir.makepyfile('''
import pytest
from pytestqt.qt_compat import QtGui, QtCore

class MyWidget(QtGui.QWidget):

def mouseReleaseEvent(self, ev):
raise RuntimeError

{marker_code}
def test_widget(qtbot):
w = MyWidget()
qtbot.addWidget(w)
qtbot.mouseClick(w, QtCore.Qt.LeftButton)
'''.format(marker_code=marker_code))
result = testdir.runpytest('-s')
# when it fails, it fails with "1 passed, 1 error in", so ensure
# it is passing without errors
result.stdout.fnmatch_lines('*1 passed in*')
30 changes: 23 additions & 7 deletions pytestqt/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class QtBot(object):
**Raw QTest API**

Methods below provide very low level functions, as sending a single mouse click or a key event.
Thos methods are just forwarded directly to the `QTest API`_. Consult the documentation for more
Those methods are just forwarded directly to the `QTest API`_. Consult the documentation for more
information.

---
Expand Down Expand Up @@ -380,18 +380,34 @@ def qapp():


@pytest.yield_fixture
def qtbot(qapp):
def qtbot(qapp, request):
"""
Fixture used to create a QtBot instance for using during testing.

Make sure to call addWidget for each top-level widget you create to ensure
that they are properly closed after the test ends.
"""
result = QtBot(qapp)
with capture_exceptions() as exceptions:
yield result

if exceptions:
pytest.fail(format_captured_exceptions(exceptions))
no_capture = request.node.get_marker('qt_no_exception_capture') or \
request.config.getini('qt_no_exception_capture')
if no_capture:
yield result # pragma: no cover
else:
with capture_exceptions() as exceptions:
yield result
if exceptions:
pytest.fail(format_captured_exceptions(exceptions))

result._close()


def pytest_addoption(parser):
parser.addini('qt_no_exception_capture',
'disable automatic exception capture')


def pytest_configure(config):
config.addinivalue_line(
'markers',
"qt_no_exception_capture: Disables pytest-qt's automatic exception "
'capture for just one test item.')