Skip to content

Commit 80c1b1c

Browse files
committed
Merge pull request #66 from pytest-dev/teardown-exceptions
Capture exceptions during test teardown
2 parents 1a29606 + 1ae658a commit 80c1b1c

File tree

3 files changed

+67
-10
lines changed

3 files changed

+67
-10
lines changed

docs/virtual_methods.rst

+13-3
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ naturally in your python code::
1616
print('mouse released at: %s' % ev.pos())
1717

1818
This works fine, but if python code in Qt virtual methods raise an exception
19-
``PyQt`` and ``PySide`` will just print the exception traceback to standard
19+
``PyQt4`` and ``PySide`` will just print the exception traceback to standard
2020
error, since this method is called deep within Qt's even loop handling and
21-
exceptions are not allowed at that point.
21+
exceptions are not allowed at that point. In ``PyQt5.5+``, exceptions in
22+
virtual methods will by default call ``abort()``, which will crash the
23+
interpreter.
2224

2325
This might be surprising for python users which are used to exceptions
2426
being raised at the calling point: for example, the following code will just
@@ -61,4 +63,12 @@ Or even disable it for your entire project in your ``pytest.ini`` file:
6163
[pytest]
6264
qt_no_exception_capture = 1
6365
64-
This might be desirable if you plan to install a custom exception hook.
66+
This might be desirable if you plan to install a custom exception hook.
67+
68+
69+
.. note::
70+
71+
Starting with ``PyQt5.5``, exceptions raised during virtual methods will
72+
actually trigger an ``abort()``, crashing the Python interpreter. For this
73+
reason, disabling exception capture in ``PyQt5.5+`` is not recommended
74+
unless you install your own exception hook.

pytestqt/_tests/test_exceptions.py

+38-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22
import sys
3-
from pytestqt.plugin import format_captured_exceptions
3+
from pytestqt.plugin import format_captured_exceptions, QT_API
44

55

66
pytest_plugins = 'pytester'
@@ -75,8 +75,12 @@ def test_no_capture(testdir, no_capture_by_marker):
7575
''')
7676
testdir.makepyfile('''
7777
import pytest
78+
import sys
7879
from pytestqt.qt_compat import QWidget, QtCore
7980
81+
# PyQt 5.5+ will crash if there's no custom exception handler installed
82+
sys.excepthook = lambda *args: None
83+
8084
class MyWidget(QWidget):
8185
8286
def mouseReleaseEvent(self, ev):
@@ -88,5 +92,36 @@ def test_widget(qtbot):
8892
qtbot.addWidget(w)
8993
qtbot.mouseClick(w, QtCore.Qt.LeftButton)
9094
'''.format(marker_code=marker_code))
91-
res = testdir.inline_run()
92-
res.assertoutcome(passed=1)
95+
res = testdir.runpytest()
96+
res.stdout.fnmatch_lines(['*1 passed*'])
97+
98+
99+
def test_exception_capture_on_teardown(testdir):
100+
"""
101+
Exceptions should also be captured during test teardown.
102+
103+
:type testdir: TmpTestdir
104+
"""
105+
testdir.makepyfile('''
106+
import pytest
107+
from pytestqt.qt_compat import QWidget, QtCore, QEvent
108+
109+
class MyWidget(QWidget):
110+
111+
def event(self, ev):
112+
raise RuntimeError('event processed')
113+
114+
def test_widget(qtbot, qapp):
115+
w = MyWidget()
116+
# keep a reference to the widget so it will lives after the test
117+
# ends. This will in turn trigger its event() during test tear down,
118+
# raising the exception during its event processing
119+
test_widget.w = w
120+
qapp.postEvent(w, QEvent(QEvent.User))
121+
''')
122+
res = testdir.runpytest('-s')
123+
res.stdout.fnmatch_lines([
124+
"*RuntimeError('event processed')*",
125+
'*1 error*',
126+
])
127+

pytestqt/plugin.py

+16-4
Original file line numberDiff line numberDiff line change
@@ -544,8 +544,7 @@ def qtbot(qapp, request):
544544
that they are properly closed after the test ends.
545545
"""
546546
result = QtBot()
547-
no_capture = request.node.get_marker('qt_no_exception_capture') or \
548-
request.config.getini('qt_no_exception_capture')
547+
no_capture = _exception_capture_disabled(request.node)
549548
if no_capture:
550549
yield result # pragma: no cover
551550
else:
@@ -557,6 +556,13 @@ def qtbot(qapp, request):
557556
result._close()
558557

559558

559+
def _exception_capture_disabled(item):
560+
"""returns if exception capture is disabled for the given test item.
561+
"""
562+
return item.get_marker('qt_no_exception_capture') or \
563+
item.config.getini('qt_no_exception_capture')
564+
565+
560566
def pytest_addoption(parser):
561567
parser.addini('qt_no_exception_capture',
562568
'disable automatic exception capture')
@@ -581,15 +587,21 @@ def pytest_addoption(parser):
581587

582588

583589
@pytest.mark.hookwrapper
584-
def pytest_runtest_teardown():
590+
def pytest_runtest_teardown(item):
585591
"""
586592
Hook called after each test tear down, to process any pending events and
587593
avoiding leaking events to the next test.
588594
"""
589595
yield
590596
app = QApplication.instance()
591597
if app is not None:
592-
app.processEvents()
598+
if _exception_capture_disabled(item):
599+
app.processEvents()
600+
else:
601+
with capture_exceptions() as exceptions:
602+
app.processEvents()
603+
if exceptions:
604+
pytest.fail('TEARDOWN ERROR: ' + format_captured_exceptions(exceptions))
593605

594606

595607
def pytest_configure(config):

0 commit comments

Comments
 (0)