Skip to content

Commit fbbb8ff

Browse files
authored
Merge pull request #1 from melonora/patch-1
Update solve_qt_segfault.md
2 parents b0714f2 + 35dda21 commit fbbb8ff

File tree

1 file changed

+72
-73
lines changed

1 file changed

+72
-73
lines changed

docs/blog/posts/solve_qt_segfault.md

+72-73
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,26 @@ tags:
1717

1818
## Motivation
1919

20-
When providing an GUI application one needs to select GUI backend.
21-
If application is Python and needs to work on all popular OSes[^1]
22-
the good choice is to use Qt. It is a cross-platform GUI toolkit that
20+
When providing a GUI application one needs to select a GUI backend.
21+
If it is a Python application that needs to work on all popular OSes[^1],
22+
Qt is a good choice. It is a cross-platform GUI toolkit that
2323
has good python bindings[^2].
2424

25-
However, the Qt objects require special care during testing.
26-
It this post I will describe my experience of writing such tests based on
25+
However, Qt objects require special care during testing.
26+
In this post I will describe my experience of writing such tests based on
2727
my work on [PartSeg](https://partseg.github.io/)
2828
and [napari](https://napari.org/).
2929

3030
<!-- more -->
3131

3232
## The problem
3333

34-
As the Qt is C++ library it does not know about python memory management.
35-
This mean that if Qt keep reference to some widget it does not increase reference
36-
count of python object. This can lead to situation when python object is deleted,
37-
but there is still event pending in Qt event loop that reference this object.
34+
As Qt is a C++ library it does not know about Python memory management.
35+
This means that if Qt keeps a reference to some widget it does not increase the reference
36+
count of python objects. This can lead to situations when the Python object is deleted,
37+
but there are still events pending in the Qt event loop that reference the object.
3838

39-
When this happens Qt will try to access deleted object, leading to access to unallocated memory (segfault).
39+
When this happens Qt will try to access the deleted object, leading to access of unallocated memory (segfault).
4040
This is very hard to debug because segfault can occur in any subsequent
4141
test, making it unclear what the cause is.
4242

@@ -46,10 +46,10 @@ The error messages vary across different operating systems:
4646
2. Linux `Segmentation fault (core dumped)` or `Fatal Python error: Aborted`
4747
3. macOS `Fatal Python error: Segmentation fault`
4848

49-
Moreover, this behavior is non-deterministic and may be not reproducible locally.
50-
One of observed source of difference is that the CI runs on server version of OS.
51-
I have encountered cases when I cannot reproduce the error on my development machine,
52-
but I could do this on my server.
49+
Moreover, this behavior is non-deterministic and may not be reproducible locally.
50+
One of the observed sources of difference is that the CI runs on a server version of the OS.
51+
I have encountered cases where I cannot reproduce the error on my development machine,
52+
but I could on my server.
5353

5454
### What is segfault
5555

@@ -58,7 +58,7 @@ In such situations, the OS will kill the program to prevent corruption.
5858
For security reasons, the OS does not allow handling this error, as it may be caused by malicious code.
5959

6060
An even worse scenario is when the addressed memory is allocated for a different object than the original pointer[^3] was pointing to.
61-
This can lead to modifying a different object in an unpredictable way, causing the test or program to fail unexpectedly.
61+
This can lead to unpredictably modifying a different object, causing the test or program to fail unexpectedly.
6262

6363
## How to prevent it
6464

@@ -69,51 +69,50 @@ This section is based on my experience and may not be complete.
6969
All Qt objects have a [`deleteLater`](https://doc.qt.io/qt-6/qobject.html#deleteLater) method that schedules the object for deletion.
7070
This allows for the safe deletion of the object, ensuring that all pending events are processed.
7171

72-
If you use some widget in test, that is not child of any other widget,
73-
you should call `deleteLater` on it before the test end.
72+
If you use some widget in your test that is not a child of any other widget,
73+
you should call `deleteLater` on it before the test ends.
7474
It is also good practice to ensure that the widget is hidden before deletion.
7575
So if your test requires showing the widget (e.g. screenshot test) you should hide it before deletion.
7676

7777
When using `pytest` for testing I suggest using the `pytest-qt` plugin.
78-
This plugin provides `qtbot` fixture that can be used to interact with Qt objects.
78+
This plugin provides a `qtbot` fixture that can be used to interact with Qt objects.
7979
It also provides a [`qtbot.add_widget`](https://pytest-qt.readthedocs.io/en/latest/reference.html#pytestqt.qtbot.QtBot.addWidget) method that ensures `deleteLater` is called on the widget when the test ends.
8080

81-
If your widget requires special teardown you can use `before_close_func` argument of `add_widget` method.
81+
If your widget requires special teardown you can use the `before_close_func` argument of the `add_widget` method.
8282

83-
### Ensure all timers, animations or threads are stopped
83+
### Ensure all timers, animations and/or threads are stopped
8484

8585
I have observed that not stopping [`QTimer`](https://doc.qt.io/qt-6/qtimer.html),
86-
[`QPropertyAnimation`](https://doc.qt.io/qt-6/qpropertyanimation.html), [`QThread`](https://doc.qt.io/qt-6/qthread.html) or [`QThreadPool`](https://doc.qt.io/qt-6/qthreadpool.html) can lead to segfault.
87-
It may also lead to some other problems with test.
86+
[`QPropertyAnimation`](https://doc.qt.io/qt-6/qpropertyanimation.html), [`QThread`](https://doc.qt.io/qt-6/qthread.html) or [`QThreadPool`](https://doc.qt.io/qt-6/qthreadpool.html) can lead to a segfault. It may also lead to some other problems with your test.
8887

89-
So if you use any of this objects in test you should ensure that they are stopped before test ends.
88+
So if you use any of these objects in your test you should ensure that they are stopped before the test ends.
9089

9190

9291
### Use the smallest possible widgets for tests
9392

94-
The process of setup and teardown of complex widgets is complex, time-consuming and may contain bugs that are hard to detect.
95-
So if the test purpose is to check behavior of some widget it is better to only create this widget, not the whole window that contains it.
93+
The process of setup and teardown of complex widgets is complex, time-consuming, and may contain bugs that are hard to detect.
94+
So if the test purpose is to check the behavior of some widget it is better to only create this widget, not the whole window that contains it.
9695

9796

9897
## How to debug and prevent
9998

100-
In this section I will describe my tricks used to debug and prevent segfaults.
101-
However, it may not fit to all projects.
99+
In this section, I will describe my tricks used to debug and prevent segfaults.
100+
However, it may not fit all projects.
102101

103102
### Run test under `gdb` or `lldb`
104103

105104
If you could reproduce the segfault locally you can run the test under `gdb` or `lldb`.
106-
Then you could go through the stack trace and see what is the cause of segfault.
105+
Then you could go through the stack trace and see what is the cause of a segfault.
107106

108-
There is also option to increase interpolation between `gdb` and python [https://docs.python.org/3/howto/gdb_helpers.html](https://docs.python.org/3/howto/gdb_helpers.html).
107+
There is also an option to increase interpolation between `gdb` and python [https://docs.python.org/3/howto/gdb_helpers.html](https://docs.python.org/3/howto/gdb_helpers.html).
109108

110-
You may also build qt in debug mode and compile your python wrapper against it. It will provide more information in stack trace, but is complex and time-consuming.
109+
You may also build Qt in debug mode and compile your Python wrapper against it. It will provide more information in the stack trace, but is complex and time-consuming.
111110

112111

113112
### Prevent `QThread` and `QTimer` from running
114113

115-
Commonly, the test do not need to use threads. However, it may happen that integration test may trigger some thread.
116-
It may be a good idea to fail the test if there is call of `QThread.start` method. I use following pytest fixture to do this:
114+
Commonly, tests do not need to use threads. However, an integration test may trigger some threads.
115+
It may be a good idea to fail the test if there is a call of the `QThread.start` method. I use the following pytest fixture to do this:
117116

118117
```python
119118
@pytest.fixture(autouse=True)
@@ -141,10 +140,10 @@ def _block_threads(monkeypatch, request):
141140
monkeypatch.setattr(qt_api.QtCore, "QTimer", OldTimer)
142141
```
143142

144-
As you may see, there is option to allow thread usage by using custom `enablethread` marker.
143+
As you may see, there is an option to allow thread usage by using the custom `enablethread` marker.
145144
The documentation for declaring custom markers is available in [pytest documentation](https://docs.pytest.org/en/stable/example/markers.html#registering-markers).
146145

147-
As documentation do not provide example for `pyproject.toml` I will provide example how to do this:
146+
As the documentation does not provide examples for `pyproject.toml` I will provide examples how to do this:
148147
```toml
149148
[tool.pytest.ini_options]
150149
markers = [
@@ -153,19 +152,19 @@ markers = [
153152
]
154153
```
155154

156-
You may also spot `monkeypatch.setattr(qt_api.QtCore, "QTimer", OldTimer)` line. It is added because `QTimer` is used internally in `pytest-qt` plugin for `qtbot.wait*` methods.
155+
You may also spot the `monkeypatch.setattr(qt_api.QtCore, "QTimer", OldTimer)` line. It is added because `QTimer` is used internally in the `pytest-qt` plugin for `qtbot.wait*` methods.
157156

158-
In similar way you can block usage of `QPropertyAnimation`.
157+
In similar fashion, you can block usage of `QPropertyAnimation`.
159158

160-
This approach raises exception when non-allowed method is called, so it is easy to prevent unwanted usage of threads.
161-
However it may increase hardness of contributing to project, as it is custom behavior, that potential contributor may not expect.
159+
This approach raises an exception when a non-allowed method is called, so it is easy to prevent unwanted usage of threads.
160+
However, it may increase the difficulty of contributing to a project, as it is a custom behavior, which potential contributors may not expect.
162161

163162

164163
### Find active timers after test end
165164

166-
In napari project we have developed a `pytest` fixtures that checks if there are any active `QTimers`, `QThreads`, `QThreadPool` and `QPropertyAnimation` after test end.
165+
In the napari project, we have developed a `pytest` fixture that checks if there are any active `QTimers`, `QThreads`, `QThreadPool` and `QPropertyAnimation` after the test ends.
167166

168-
This method is not perfect as it may not be triggered at every test suite run. So problematic code may be detected after log time.
167+
This method is not perfect as it may not be triggered at every test suite run. So problematic code may be detected after a long time.
169168

170169
```python
171170
@pytest.fixture(auto_use=True)
@@ -223,49 +222,49 @@ def dangling_qthreads(monkeypatch, qtbot, request):
223222
)
224223
```
225224

226-
It is simplified version of napari fixture.
227-
You may see full versions in [napari contest](https://github.com/napari/napari/blob/15c2d7d5ae7c607e3436800328527bd62c421896/napari/conftest.py#L444)
225+
It is a simplified version of the napari fixture.
226+
You can see the full version in [napari contest](https://github.com/napari/napari/blob/15c2d7d5ae7c607e3436800328527bd62c421896/napari/conftest.py#L444)
228227

229-
For other problematic objects you can use similar approach. There are proper fixtures in same [`conftest.py`](https://github.com/napari/napari/blob/15c2d7d5ae7c607e3436800328527bd62c421896/napari/conftest.py) file.
228+
For other problematic objects, you can use a similar approach. There are proper fixtures in the same [`conftest.py`](https://github.com/napari/napari/blob/15c2d7d5ae7c607e3436800328527bd62c421896/napari/conftest.py) file.
230229

231230

232231
### Detect leaked widgets
233232

234233
!!! note
235-
If your test suite is small it may be much simpler to review all tests and check if all top level widgets are scheduled for deletion.
234+
If your test suite is small it may be much simpler to review all tests and check if all top-level widgets are scheduled for deletion.
236235

237-
With big test dataset it may be hard to detect if some widget is not scheduled for delete.
236+
With big test datasets, it may be hard to detect if some widget is not scheduled for deletion.
238237

239-
This whole section is describing set of heuristics that may help to detect such widgets, but may also lead to false positives.
238+
This whole section describes a set of heuristics that may help to detect such widgets, but may also lead to false positives.
240239
If you use some custom, complex procedure for widget deletion you may need to adjust these heuristics or meet strange errors.
241-
This heuristic may report some widget after many test suite runs. It means that in previous test suite runs this widget was deleted by garbage collector, but in this run it was not.
240+
This heuristic may report some widgets after many test suite runs. It means that in the previous test suite runs, this widget was deleted by the garbage collector, but in this run it was not.
242241

243242
!!! note
244-
If you are not expert in Qt and Python I strongly suggest to not write custom teardown procedure for widgets
243+
If you are not an expert in Qt and Python I strongly suggest not to write custom teardown procedures for widgets
245244
and just use `qtbot.add_widget` method everywhere.
246245

247246
#### `QApplication.topLevelWidgets`
248247

249-
The Qt provides method [`QApplication.topLevelWidgets`](https://doc.qt.io/qt-6/qapplication.html#topLevelWidgets) that returns list of all top level widgets.
250-
It is nice place to start searching for leaked widgets. Hoverer it has some limitations:
248+
Qt provides the method [`QApplication.topLevelWidgets`](https://doc.qt.io/qt-6/qapplication.html#topLevelWidgets) that returns a list of all top level widgets.
249+
It is a nice place to start searching for leaked widgets. However, it has some limitations:
251250

252251
1. It may create new python wrappers for widgets, so all methods that are monkeypatched or properties defined outside `__init__` method may not be available.
253-
2. Not all top level widgets are top level widgets that require teardown setup. For example, it returns `QMenu` objects that represents the main window menu bar.
254-
3. It returns all top level widgets, not only those that are created in test.
252+
2. Not all top level widgets are top level widgets that require teardown setup. For example, it returns `QMenu` objects that represent the main window menu bar.
253+
3. It returns all top level widgets, not only those that are created in the test.
255254

256255

257-
Based on above info we cannot use custom attribute to mark widget as handled without defining them in constructor.
258-
However, all Qt Objects have `objectName` property that stored in C++ object and is not recreated in python wrapper.
259-
But it is also could be used by custom code or styling, so it is not perfect.
256+
Based on the above info we cannot use custom attributes to mark widgets as handled without defining them in aconstructor.
257+
However, all Qt Objects have the `objectName` property that is stored as a C++ object and is not recreated in the python wrapper, though
258+
it also could be used by custom code or stylingand therefore it is not perfect.
260259

261-
For code simplicity we will use `objectName` property to mark handled widgets.
262-
We will do this by subleasing `QtBot` class from `pytest-qt` plugin and overriding `addWidget` method.
260+
For code simplicity we will use the `objectName` property to mark handled widgets.
261+
We will do this by subleasing the `QtBot` class from the `pytest-qt` plugin and overriding the `addWidget` method.
263262

264-
We will use fact that `qtbot.addWidget` allow for add custom teardown function that will be called before widget is deleted.
265-
It is done by providing `before_close_func` argument to `addWidget` method. So if object added to `qtbot`
266-
have `objectName` set to some value it could be changed in `before_close_func` function.
263+
We will use the fact that `qtbot.addWidget` allows for adding a custom teardown function that will be called before widget is deleted.
264+
It is done by providing the `before_close_func` argument to the `addWidget` method. So if the object added to the `qtbot`
265+
has the `objectName` set to some value it could be changed in the `before_close_func` function.
267266

268-
We also need to define own `qtbot` fixture that will use our custom `QtBot` class.
267+
We also need to define our own `qtbot` fixture that will use our custom `QtBot` class.
269268

270269
```python
271270
from pytestqt.qtbot import QtBot
@@ -323,12 +322,12 @@ def qtbot(qapp, request): # pragma: no cover
323322

324323
!!! note
325324
As I expect that many readers of this blog post may be maintainers of napari plugins,
326-
the code bellow contains parts specific to the napari project. They are marked with a comment.
325+
the code below contains parts specific to the napari project. These are marked with a comment.
327326
If you are not a napari plugin maintainer, you can remove these parts.
328327

329-
The bellow fixture is implementing our heuristic to detect leaked widgets.
328+
The fixture below is implementing our heuristic to detect leaked widgets.
330329
It looks for all top level widgets that are not children of any other widget and have not been renamed to `handled_widget`.
331-
Then raises an exception with a list of such widgets.
330+
It then raises an exception with a list of such widgets.
332331

333332

334333
```python
@@ -389,13 +388,13 @@ def _find_dangling_widgets(request, qtbot):
389388

390389
## Bonus tip
391390

392-
### Test hanging due to nested event loop
391+
### Tests hanging due to nested event loop
393392

394-
Your tests are hanging, but any above solution did not help. What to do?
393+
Your tests are hanging, but the above solutions did not help. What can you still do?
395394

396-
One of the possible reason is that your code is created some nested event loop by opening [`QDialog`](https://doc.qt.io/qt-6/qdialog.html)
397-
or [`QMessageBox`](https://doc.qt.io/qt-6/qmessagebox.html) using `exec` method.
398-
To get error message instead of hanging test I use following pytest fixture:
395+
One of the possible reasons is that your code created some nested event loop by opening [`QDialog`](https://doc.qt.io/qt-6/qdialog.html)
396+
or [`QMessageBox`](https://doc.qt.io/qt-6/qmessagebox.html) using the `exec` method.
397+
To get an error message instead of hanging test I use the following pytest fixture:
399398

400399
```python
401400
import pytest
@@ -419,9 +418,9 @@ def _block_message_box(monkeypatch, request):
419418

420419
```
421420

422-
As you can see I block multiple methods that can create nested event loop.
423-
In some test I need to allow calling `exec` method of `QDialog`,
424-
so I have defined `enabledialog` marker that I can use to allow this call.
421+
As you can see I block multiple methods that can create a nested event loop.
422+
In some tests I need to allow calling the `exec` method of `QDialog`,
423+
so I have defined the `enabledialog` flag that I can use to allow this call.
425424

426425

427426
```python
@@ -438,4 +437,4 @@ def test_recent(self, tmp_path, qtbot, monkeypatch):
438437

439438
[^1]: This includes Windows, macOS, and various distributions of Linux.
440439
[^2]: PyQt5, PySide2 for Qt5, PyQT6, PySide6 for Qt6.
441-
[^3]: [https://en.wikipedia.org/wiki/Pointer_(computer_programming)](https://en.wikipedia.org/wiki/Pointer_(computer_programming))
440+
[^3]: [https://en.wikipedia.org/wiki/Pointer_(computer_programming)](https://en.wikipedia.org/wiki/Pointer_(computer_programming))

0 commit comments

Comments
 (0)