pytest-dev / pytest-dev/pytest-qt
Leaking top level widgets
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 455
- Forks
- 72
- Avg merge
- 12h 26m
- Merged PRs (30d)
- 1
Description
Hi *,
our pipeline broke lately, running in to segfaults. After a while of searching, it appears that some widgets live on until the the test session concludes. Somehow, during interpreter shutdown, this leads into updating dead QAbstractItemModel instances, which causes the crash.
It is still unclear to me how this unfolds, but the basics are reproducible in pytest-qt at 3e593f25231c1a745320c406d7a025f21a668ec0. Look at this (new) test case (tests/test_widget_leak.py):
import pytest
from pytestqt.qt_compat import qt_api
@pytest.mark.parametrize("n", range(42))
def test_widget_leak(qapp, n):
widget = qt_api.QtWidgets.QWidget()
widget.show()
qapp.processEvents()
assert len(qapp.topLevelWidgets()) == 1
I get a successful run exercising only this new test case:
$ tox -e py38-pyqt5 -- tests/test_widget_leak.py
...
========== 42 passed in 0.12s ==========
py38-pyqt5: OK (5.39=setup[5.00]+cmd[0.40] seconds)
congratulations :) (5.52 seconds)
But running some existing tests cases first, this fails because there are additional top level widgets. Also, the newly created widgets are kept now:
$ tox -e py38-pyqt5 -- tests/test_basics.py::test_stop tests/test_widget_leak.py
...
========== short test summary info ==========
FAILED tests/test_widget_leak.py::test_widget_leak[0] - assert 2 == 1
FAILED tests/test_widget_leak.py::test_widget_leak[1] - assert 3 == 1
FAILED tests/test_widget_leak.py::test_widget_leak[2] - assert 4 == 1
...
========== 42 failed, 1 passed in 0.28s ==========
I failed to directly and explicitly delete/destroy those top level widgets (there is no usable delete/destroy method exposed in Python). The following work around seems to get rid of extra top levels:
diff --git a/src/pytestqt/plugin.py b/src/pytestqt/plugin.py
index fab2c72..fca66b7 100644
--- a/src/pytestqt/plugin.py
+++ b/src/pytestqt/plugin.py
@@ -50,9 +50,15 @@ def qapp_cls():
"""
return qt_api.QtWidgets.QApplication
+@pytest.fixture
+def qapp(qapp_session):
+ try:
+ yield qapp_session
+ finally:
+ _delete_toplevels(qapp_session)
@pytest.fixture(scope="session")
-def qapp(qapp_args, qapp_cls, pytestconfig):
+def qapp_session(qapp_args, qapp_cls, pytestconfig):
"""
Fixture that instantiates the QApplication instance that will be used by
the tests.
@@ -76,6 +82,25 @@ def qapp(qapp_args, qapp_cls, pytestconfig):
return app
+def _delete_toplevels(app):
+ QTimer = qt_api.QtCore.QTimer
+
+ def delete_core():
+ top_levels = app.topLevelWidgets()
+ if top_levels:
+ top = top_levels.pop()
+ if top:
+ top.deleteLater()
+ QTimer.singleShot(0, delete_core)
+ else:
+ loop.exit(0)
+
+ QTimer.singleShot(0, delete_core)
+ loop = qt_api.QtCore.QEventLoop()
+ loop.exec_()
+ assert not app.topLevelWidgets()
+
+
# holds a global QApplication instance created in the qapp fixture; keeping
# this reference alive avoids it being garbage collected too early
_qapp_instance = None
I have no idea though if this has any bad side effects. Also, I am not sure what causes the survival of the widgets. Any insights appreciated.
Greetings, Torsten
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with src/pytestqt/plugin.py and the qapp fixture lifecycle, then run tests/test_widget_leak.py after tests/test_basics.py::test_stop using the tox command shown. Reproduce the retained top-level widgets and inspect the existing fixture and _delete_toplevels workaround. Done means the widget-leak test passes without leaving top-level widgets or causing shutdown failures.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100