From 53e2de4f4d9d817b44759c042ed14c819f009ceb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 1 Nov 2014 13:28:32 -0200 Subject: [PATCH 1/4] Fixed small doc typo --- pytestqt/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytestqt/plugin.py b/pytestqt/plugin.py index 6a4eb20f..2f9ed36e 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. --- From 3d6f80d1217e545f979fb6a9bc705e1f7d633bd2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 5 Nov 2014 21:23:54 -0200 Subject: [PATCH 2/4] Support for not capturing Qt exceptions using marker or config value --- pytestqt/_tests/test_exceptions.py | 42 ++++++++++++++++++++++++++++-- pytestqt/plugin.py | 26 ++++++++++++++---- 2 files changed, 61 insertions(+), 7 deletions(-) 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 2f9ed36e..602e41ab 100644 --- a/pytestqt/plugin.py +++ b/pytestqt/plugin.py @@ -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: + no_capture = request.node.get_marker('qt_no_exception_capture') or \ + request.config.getini('qt_no_exception_capture') + if no_capture: yield result - - if exceptions: - pytest.fail(format_captured_exceptions(exceptions)) + 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 From f9f9bf0303c8db8b69ca4db141fb362377c415d8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 5 Nov 2014 22:43:05 -0200 Subject: [PATCH 3/4] excluding line from code coverage this condition is tested in test_no_capture, but that test runs in another process and that is not captured by coverage --- pytestqt/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytestqt/plugin.py b/pytestqt/plugin.py index 602e41ab..4d6c05e1 100644 --- a/pytestqt/plugin.py +++ b/pytestqt/plugin.py @@ -391,7 +391,7 @@ def qtbot(qapp, request): no_capture = request.node.get_marker('qt_no_exception_capture') or \ request.config.getini('qt_no_exception_capture') if no_capture: - yield result + yield result # pragma: no cover else: with capture_exceptions() as exceptions: yield result From ddcc7e7a72459c1f3fafa3397be65ae802ee69b6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 5 Nov 2014 23:09:37 -0200 Subject: [PATCH 4/4] Added docs about disabling exception capturing --- docs/index.rst | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) 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 =====