You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardexpand all lines: docs/blog/posts/solve_qt_segfault.md
+72-73
Original file line number
Diff line number
Diff line change
@@ -17,26 +17,26 @@ tags:
17
17
18
18
## Motivation
19
19
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
23
23
has good python bindings[^2].
24
24
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
27
27
my work on [PartSeg](https://partseg.github.io/)
28
28
and [napari](https://napari.org/).
29
29
30
30
<!-- more -->
31
31
32
32
## The problem
33
33
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.
38
38
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).
40
40
This is very hard to debug because segfault can occur in any subsequent
41
41
test, making it unclear what the cause is.
42
42
@@ -46,10 +46,10 @@ The error messages vary across different operating systems:
46
46
2. Linux `Segmentation fault (core dumped)` or `Fatal Python error: Aborted`
47
47
3. macOS `Fatal Python error: Segmentation fault`
48
48
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.
53
53
54
54
### What is segfault
55
55
@@ -58,7 +58,7 @@ In such situations, the OS will kill the program to prevent corruption.
58
58
For security reasons, the OS does not allow handling this error, as it may be caused by malicious code.
59
59
60
60
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.
62
62
63
63
## How to prevent it
64
64
@@ -69,51 +69,50 @@ This section is based on my experience and may not be complete.
69
69
All Qt objects have a [`deleteLater`](https://doc.qt.io/qt-6/qobject.html#deleteLater) method that schedules the object for deletion.
70
70
This allows for the safe deletion of the object, ensuring that all pending events are processed.
71
71
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.
74
74
It is also good practice to ensure that the widget is hidden before deletion.
75
75
So if your test requires showing the widget (e.g. screenshot test) you should hide it before deletion.
76
76
77
77
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.
79
79
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.
80
80
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.
82
82
83
-
### Ensure all timers, animations or threads are stopped
83
+
### Ensure all timers, animations and/or threads are stopped
84
84
85
85
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.
88
87
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.
90
89
91
90
92
91
### Use the smallest possible widgets for tests
93
92
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.
96
95
97
96
98
97
## How to debug and prevent
99
98
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.
102
101
103
102
### Run test under `gdb` or `lldb`
104
103
105
104
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.
107
106
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).
109
108
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.
111
110
112
111
113
112
### Prevent `QThread` and `QTimer` from running
114
113
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:
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.
145
144
The documentation for declaring custom markers is available in [pytest documentation](https://docs.pytest.org/en/stable/example/markers.html#registering-markers).
146
145
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:
148
147
```toml
149
148
[tool.pytest.ini_options]
150
149
markers = [
@@ -153,19 +152,19 @@ markers = [
153
152
]
154
153
```
155
154
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.
157
156
158
-
In similar way you can block usage of `QPropertyAnimation`.
157
+
In similar fashion, you can block usage of `QPropertyAnimation`.
159
158
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.
162
161
163
162
164
163
### Find active timers after test end
165
164
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.
167
166
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.
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)
228
227
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.
230
229
231
230
232
231
### Detect leaked widgets
233
232
234
233
!!! note
235
-
If your test suite is small it may be much simpler to review all tests and check if all toplevel 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.
236
235
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.
238
237
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.
240
239
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.
242
241
243
242
!!! 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
245
244
and just use `qtbot.add_widget` method everywhere.
246
245
247
246
#### `QApplication.topLevelWidgets`
248
247
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:
251
250
252
251
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.
255
254
256
255
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.
260
259
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.
263
262
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.
267
266
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.
0 commit comments