diff --git a/docs/index.rst b/docs/index.rst index 7dbe631d..749aab1b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 ===== diff --git a/pytestqt/_tests/test_exceptions.py b/pytestqt/_tests/test_exceptions.py index 19e7cfa6..bee6fba8 100644 --- a/pytestqt/_tests/test_exceptions.py +++ b/pytestqt/_tests/test_exceptions.py @@ -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 @@ -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 \ No newline at end of file + 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*') \ No newline at end of file diff --git a/pytestqt/plugin.py b/pytestqt/plugin.py index 6a4eb20f..4d6c05e1 100644 --- a/pytestqt/plugin.py +++ b/pytestqt/plugin.py @@ -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. --- @@ -380,7 +380,7 @@ def qapp(): @pytest.yield_fixture -def qtbot(qapp): +def qtbot(qapp, request): """ Fixture used to create a QtBot instance for using during testing. @@ -388,10 +388,26 @@ def qtbot(qapp): 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.') \ No newline at end of file