Skip to content

Commit 7b1d0cb

Browse files
accumulatorclaude
andcommitted
tests: flush DeferredDelete and run gc in qt_test teardown
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>
1 parent e11f956 commit 7b1d0cb

1 file changed

Lines changed: 23 additions & 1 deletion

File tree

tests/qml/qt_util.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import gc
12
import threading
23
import traceback
34
import unittest
45
from functools import wraps, partial
56
from typing import List, Sequence
67
from unittest import SkipTest
78

8-
from PyQt6.QtCore import QCoreApplication, QMetaObject, Qt, pyqtSlot, QObject, QEventLoop, QTimer
9+
from PyQt6.QtCore import QCoreApplication, QMetaObject, Qt, pyqtSlot, QObject, QEventLoop, QTimer, QEvent
910

1011
from electrum.util import create_and_start_event_loop
1112

@@ -196,5 +197,26 @@ def decorator(self, *args):
196197
except Exception as e:
197198
if self._e is None:
198199
self._e = e
200+
# QObject.deleteLater() merely posts a DeferredDelete event, which Qt only
201+
# delivers once the event loop unwinds to the loop level at which it was
202+
# posted. This test body runs *inside* app.exec() (one level deep, invoked
203+
# via doInvoke), so neither returning here nor a processEvents() call (which
204+
# runs at a deeper level) delivers those events. The C++ objects would then
205+
# linger until some later, racy spin of the QtTestThread's loop, and with
206+
# them everything they reference (e.g. a QEWallet's self.wallet). Flush them
207+
# synchronously now so QObject destruction is deterministic and complete by
208+
# the time the test is considered done.
209+
try:
210+
self.app.sendPostedEvents(None, QEvent.Type.DeferredDelete)
211+
# Force a cyclic GC pass now that the QObjects are gone. The test's
212+
# wallets and helpers sit in reference cycles (e.g. wallet<->txbatcher)
213+
# and would otherwise survive until some later, unrelated gc.collect().
214+
# Collecting here runs their EventListener.__del__, which unregisters
215+
# their callbacks, so we don't pollute global state (electrum.util.
216+
# callback_mgr) for the tests that run after this one.
217+
gc.collect()
218+
except Exception as e:
219+
if self._e is None:
220+
self._e = e
199221
self._testcase_event.set()
200222
return decorator

0 commit comments

Comments
 (0)