Skip to content

tests: refactor the Qt test harness to support running QObject's within a QCoreApplication#10699

Draft
accumulator wants to merge 5 commits into
spesmilo:masterfrom
accumulator:qobject_test_harness
Draft

tests: refactor the Qt test harness to support running QObject's within a QCoreApplication#10699
accumulator wants to merge 5 commits into
spesmilo:masterfrom
accumulator:qobject_test_harness

Conversation

@accumulator

Copy link
Copy Markdown
Member

tests: refactor the Qt test harness to support running QObject's within a QCoreApplication and its event
loop, so we can fully test behaviour with more complex QObject hierarchies and check for signal emits.

adds some initial tests for QEWallet

@accumulator

Copy link
Copy Markdown
Member Author

Interesting. These new Qt testcases unearthed a pre-existing bug.

FAILED tests/test_daemon.py::TestUnifiedPassword::test_wallet_objects_are_properly_garbage_collected_after_check_pw_for_dir - AssertionError: 2 != 0 : too many lingering objs of type=<class 'electrum.wallet.Abstract_Wallet'>

There are some lifecycle issues with some QObject subclasses (in particular QEWallet), where strong references are kept, even after these objects are supposedly deleted. There's tension between the PyQt wrappers and the underlying Qt C++ objects, either side can initiate destruction, both must be correctly handled.

I'll submit fixes in another PR.

- remove lambda style signal handlers, unless absolutely necessary
- replace signal self-connects with QMetaObject.invokeMethod
- refactor ad-hoc broadcastSucceeded/broadcastFailed signal self-connect in QETxDetails
  to callbacks
Comment thread tests/qml/qt_util.py

@SomberNight SomberNight Jun 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you see the segfault on one of the CI unittest runs? I can reproduce that on python 3.14 with the debug env vars set, with a high reliability (75+%).

(the other PR, #10701, does not fix it either - I tried merging f62096f onto this branch)

There is no segfault on master.

log
$ PYTHONASYNCIODEBUG=1 PYTHONDEVMODE=1 pytest tests
=================================================== test session starts ====================================================
platform linux -- Python 3.14.6, pytest-9.1.0, pluggy-1.6.0
rootdir: /home/user/wspace/electrum
collected 980 items                                                                                                        

tests/plugins/test_revealer.py ...                                                                                   [  0%]
tests/plugins/test_timelock_recovery.py .......                                                                      [  1%]
tests/qml/test_qewallet.py Fatal Python error: Aborted

Thread 0x00007f80d7fff6c0 [asyncio_0] (most recent call first):
  File "/opt/cpython-versions/3.14.6/lib/python3.14/concurrent/futures/thread.py", line 116 in _worker
  File "/opt/cpython-versions/3.14.6/lib/python3.14/threading.py", line 1024 in run
  File "/opt/cpython-versions/3.14.6/lib/python3.14/threading.py", line 1082 in _bootstrap_inner
  File "/opt/cpython-versions/3.14.6/lib/python3.14/threading.py", line 1044 in _bootstrap

Thread 0x00007f80dc9fe6c0 [EventLoop] (most recent call first):
  File "/opt/cpython-versions/3.14.6/lib/python3.14/asyncio/futures.py", line 411 in _chain_future
  <invalid frame>

Current thread 0x00007f80dd1ff6c0 [QtTestThread] (most recent call first):
  File "/home/user/wspace/electrum/tests/qml/qt_util.py", line 145 in waitForSignal
  File "/home/user/wspace/electrum/tests/qml/test_qewallet.py", line 175 in test_auth_protected_methods
  File "/home/user/wspace/electrum/tests/qml/qt_util.py", line 189 in decorator
  File "/home/user/wspace/electrum/tests/qml/qt_util.py", line 22 in doInvoke
  File "/home/user/wspace/electrum/tests/qml/qt_util.py", line 91 in start_qt_task
  File "/opt/cpython-versions/3.14.6/lib/python3.14/threading.py", line 1024 in run
  File "/opt/cpython-versions/3.14.6/lib/python3.14/threading.py", line 1082 in _bootstrap_inner
  File "/opt/cpython-versions/3.14.6/lib/python3.14/threading.py", line 1044 in _bootstrap

Thread 0x00007f80e9beebc0 [pytest] (most recent call first):
  File "/opt/cpython-versions/3.14.6/lib/python3.14/threading.py", line 373 in wait
  File "/opt/cpython-versions/3.14.6/lib/python3.14/threading.py", line 670 in wait
  File "/home/user/wspace/electrum/tests/qml/qt_util.py", line 171 in decorator
  File "/opt/cpython-versions/3.14.6/lib/python3.14/unittest/case.py", line 615 in _callTestMethod
  File "/opt/cpython-versions/3.14.6/lib/python3.14/unittest/case.py", line 669 in run
  File "/opt/cpython-versions/3.14.6/lib/python3.14/unittest/case.py", line 725 in __call__
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/_pytest/unittest.py", line 410 in runtest
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/_pytest/runner.py", line 184 in pytest_runtest_call
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/pluggy/_callers.py", line 121 in _multicall
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/pluggy/_manager.py", line 120 in _hookexec
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/pluggy/_hooks.py", line 512 in __call__
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/_pytest/runner.py", line 250 in <lambda>
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/_pytest/runner.py", line 361 in from_call
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/_pytest/runner.py", line 249 in call_and_report
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/_pytest/runner.py", line 139 in runtestprotocol
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/_pytest/runner.py", line 118 in pytest_runtest_protocol
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/pluggy/_callers.py", line 121 in _multicall
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/pluggy/_manager.py", line 120 in _hookexec
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/pluggy/_hooks.py", line 512 in __call__
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/_pytest/main.py", line 408 in pytest_runtestloop
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/pluggy/_callers.py", line 121 in _multicall
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/pluggy/_manager.py", line 120 in _hookexec
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/pluggy/_hooks.py", line 512 in __call__
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/_pytest/main.py", line 384 in _main
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/_pytest/main.py", line 330 in wrap_session
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/_pytest/main.py", line 377 in pytest_cmdline_main
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/pluggy/_callers.py", line 121 in _multicall
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/pluggy/_manager.py", line 120 in _hookexec
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/pluggy/_hooks.py", line 512 in __call__
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/_pytest/config/__init__.py", line 229 in _main
  File "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/_pytest/config/__init__.py", line 253 in _console_main
  File "/home/user/wspace/electrum/.venv314/bin/pytest", line 6 in <module>

Current thread's C stack trace (most recent call first):
  Binary file "/home/user/wspace/electrum/.venv314/bin/python3", at _Py_DumpStack+0x2b [0x560b81bc8c9b]
  Binary file "/home/user/wspace/electrum/.venv314/bin/python3", at +0x318eca [0x560b81bdeeca]
  Binary file "/lib/x86_64-linux-gnu/libc.so.6", at +0x3fdf0 [0x7f80e9c2fdf0]
  Binary file "/lib/x86_64-linux-gnu/libc.so.6", at +0x9495c [0x7f80e9c8495c]
  Binary file "/lib/x86_64-linux-gnu/libc.so.6", at gsignal+0x12 [0x7f80e9c2fcc2]
  Binary file "/lib/x86_64-linux-gnu/libc.so.6", at abort+0x22 [0x7f80e9c184ac]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/Qt6/lib/libQt6Core.so.6", at _Z9qt_assertPKcS0_i+0x0 [0x7f80e26bff56]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/Qt6/lib/libQt6Core.so.6", at +0x100954 [0x7f80e2700954]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/Qt6/lib/libQt6Core.so.6", at _ZNK14QMessageLogger5fatalEPKcz+0x105 [0x7f80e26c17e7]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/QtCore.abi3.so", at +0x226117 [0x7f80dfa26117]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/QtCore.abi3.so", at +0x22e98d [0x7f80dfa2e98d]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/QtCore.abi3.so", at +0x22f82f [0x7f80dfa2f82f]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/Qt6/lib/libQt6Core.so.6", at _ZN7QObject5eventEP6QEvent+0x279 [0x7f80e27de899]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/QtCore.abi3.so", at +0x1e2f3e [0x7f80df9e2f3e]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/Qt6/lib/libQt6Core.so.6", at _ZN16QCoreApplication15notifyInternal2EP7QObjectP6QEvent+0x150 [0x7f80e2785240]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/Qt6/lib/libQt6Core.so.6", at _ZN23QCoreApplicationPrivate16sendPostedEventsEP7QObjectiP11QThreadData+0x275 [0x7f80e2788ec5]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/Qt6/lib/libQt6Core.so.6", at +0x47ef47 [0x7f80e2a7ef47]
  Binary file "/lib/x86_64-linux-gnu/libglib-2.0.so.0", at +0x5c3c5 [0x7f80e39033c5]
  Binary file "/lib/x86_64-linux-gnu/libglib-2.0.so.0", at +0x5e5f7 [0x7f80e39055f7]
  Binary file "/lib/x86_64-linux-gnu/libglib-2.0.so.0", at g_main_context_iteration+0x30 [0x7f80e3905d60]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/Qt6/lib/libQt6Core.so.6", at _ZN20QEventDispatcherGlib13processEventsE6QFlagsIN10QEventLoop17ProcessEventsFlagEE+0x6e [0x7f80e2a7e5de]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/Qt6/lib/libQt6Core.so.6", at _ZN10QEventLoop4execE6QFlagsINS_17ProcessEventsFlagEE+0x24b [0x7f80e27913bb]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/QtCore.abi3.so", at +0xdf33b [0x7f80df8df33b]
  Binary file "/home/user/wspace/electrum/.venv314/bin/python3", at +0x15c1fb [0x560b81a221fb]
  Binary file "/home/user/wspace/electrum/.venv314/bin/python3", at _PyObject_MakeTpCall+0xc0 [0x560b819b22f0]
  Binary file "/home/user/wspace/electrum/.venv314/bin/python3", at _PyEval_EvalFrameDefault+0xa7f2 [0x560b81b2e722]
  Binary file "/home/user/wspace/electrum/.venv314/bin/python3", at +0x26fbac [0x560b81b35bac]
  Binary file "/home/user/wspace/electrum/.venv314/bin/python3", at +0xedabb [0x560b819b3abb]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/QtCore.abi3.so", at +0x22e280 [0x7f80dfa2e280]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/QtCore.abi3.so", at +0x22e6d0 [0x7f80dfa2e6d0]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/QtCore.abi3.so", at +0x22e87c [0x7f80dfa2e87c]
  Binary file "/home/user/wspace/electrum/.venv314/lib/python3.14/site-packages/PyQt6/QtCore.abi3.so", at +0x23480e [0x7f80dfa3480e]
  <truncated rest of calls>

Extension modules: multidict._multidict, yarl._quoting_c, propcache._helpers_c, aiohttp._http_writer, aiohttp._http_parser, aiohttp._websocket.mask, aiohttp._websocket.reader_c, frozenlist._frozenlist, _cffi_backend, PyQt6.QtCore, PyQt6.QtNetwork, PyQt6.QtQml, PyQt6.QtGui, PyQt6.QtQuick, PyQt6.QtMultimedia (total: 15)
Aborted (core dumped)


$ pip freeze --all
aiohappyeyeballs==2.6.2
aiohttp==3.14.1
aiohttp_socks==0.11.0
aiorpcX==0.25.0
aiosignal==1.4.0
attrs==22.2.0
certifi==2026.6.17
cffi==2.0.0
cryptography==49.0.0
dnspython==2.4.2
-e git+ssh://git@github.com/spesmilo/electrum.git@ba358b1f0a66f324a15b7cc4d74c5787fde5c9aa#egg=Electrum
electrum-aionostr==0.1.0
electrum-ecc==0.0.7
frozenlist==1.8.0
idna==3.18
iniconfig==2.3.0
jsonpatch==1.33
jsonpointer==3.1.1
multidict==6.7.1
packaging==26.2
pip==26.1.2
pluggy==1.6.0
propcache==0.5.2
protobuf==7.35.1
pycparser==3.0
Pygments==2.20.0
PyQt6==6.10.2
PyQt6-Qt6==6.10.2
PyQt6_sip==13.11.1
pytest==9.1.0
python-socks==2.8.1
QDarkStyle==3.2.3
qrcode==8.2
QtPy==2.4.3
yarl==1.24.2

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you see the segfault on one of the CI unittest runs? I can reproduce that on python 3.14 with the debug env vars set, with a high reliability (75+%).

Yeah, it was supposed to be a draft PR..

@accumulator accumulator Jun 24, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should have ABRTed 100%, though, did you get any successful runs through?

anyway, the issue is resolved now, but I had to rebase on #10701 as the tests reference refactoring done there. I can split the added tests off in a separate PR, so this PR and #10701 are independent?

- move QEWallet.getInstanceFor to QEDaemon.getQEWalletInstanceFor
- parent QEWallet instances to QEDaemon, parent listmodels to QEWallet
- introduce QEDaemon.unloadWallet method for clean teardown of QEWallet instances
- destroyed signal handler on QEWallet is now plain bound method style
@accumulator accumulator marked this pull request as draft June 24, 2026 15:01
@accumulator accumulator force-pushed the qobject_test_harness branch from ba358b1 to a1bc76c Compare June 24, 2026 15:25
accumulator and others added 3 commits June 25, 2026 00:06
…in a QCoreApplication and its event

loop, so we can fully test behaviour with more complex QObject hierarchies and check for signal emits

add some initial tests for QEWallet
A qt_test body runs inside app.exec() (one loop level deep, via doInvoke),
so a QObject.deleteLater() posted there is never delivered by processEvents()
-- Qt only delivers DeferredDelete once the loop unwinds to the level at which
it was posted. The C++ objects, and everything they reference (e.g. a
QEWallet's self.wallet), lingered until some later, racy spin of the loop.

Flush them synchronously via sendPostedEvents(None, DeferredDelete), then
gc.collect() so reference cycles (e.g. wallet<->txbatcher) are reclaimed and
their EventListener.__del__ unregisters callbacks. This keeps QML tests from
leaking wallet objects and callbacks into later tests (e.g. test_daemon's GC
sanity checks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@accumulator accumulator force-pushed the qobject_test_harness branch from a1bc76c to 7b1d0cb Compare June 24, 2026 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants