Skip to content

Commit a764161

Browse files
committed
fix(tests): collect leaked QObjects between tests to end Py3.12 isolation flake
Ursache: Mehrere GUI-Tests (test_smoke_launch, test_tray_preset_menu, test_settings_dialog, gui_app in test_state_machine) erzeugen BlitztextApp (selbst ein QObject) samt HotkeyWorker, QThread und Fenstern ohne Qt-Parent und lassen die Python-Referenz am Testende fallen. Diese Objekte haengen in Referenzzyklen (Signal/Slot-Verbindungen zeigen auf self), sodass der Refcount sie nicht abraeumt -- erst Pythons zyklischer GC loescht die C++-Objekte, zu nichtdeterministischem Zeitpunkt. Auf CPython 3.12 (CI-Matrix) faellt dieser GC-Lauf reproduzierbar mitten in HotkeyWorker.run() eines Folgetests; sip meldet dann den laufenden Worker als geloescht: RuntimeError: wrapped C/C++ object of type HotkeyWorker has been deleted (app/hotkey_service.py:307, self.workflow_triggered.emit) Betroffen: tests/test_state_machine.py::TestLeftAltEvents:: test_leftalt_keydown_triggers_toggle. Fix: Autouse-Fixture in tests/conftest.py ruft nach JEDEM Test gc.collect(). Der Abbau der geleakten Zyklen wird damit deterministisch an der Testgrenze erzwungen -- waehrend kein Worker laeuft. Im Folgetest bleibt fuer den GC nichts mehr einzusammeln, das mit einem laufenden run() kollidieren koennte. Reiner Test-Fix, kein Produktivcode. Bewusst NUR gc.collect(): eine erste, aggressivere Variante mit processEvents()+sendPostedEvents(DeferredDelete) loeschte noch von Tests referenzierte Widgets vorzeitig und provozierte C++-Doppel-Frees (Fatal Python error: Aborted). gc.collect() bricht nur echte, unerreichbare Zyklen auf und ist nebenwirkungsfrei. Verifikation (Py3.12-venv wie CI, QT_QPA_PLATFORM=offscreen, WHISPER_GUI_TESTS=1): - Einzeltest, test_state_machine.py, volle Suite: gruen (494 passed) - Zusaetzlich Py3.14: gruen; Non-GUI-Default: 424 passed / 70 skipped - Leak-Messung: geleakte BlitztextApp zwischen Tests 7 -> 2 (Peak) - compileall app + secret-scan (CI-Muster): sauber Risiken: gc.collect() nach jedem Test erhoeht die Suite-Laufzeit der Offscreen-GUI- Suite (~10s -> ~20s bei 494 Tests). Bewusst zugunsten deterministischer Isolation akzeptiert. Keine Testdeaktivierung, kein skip/xfail/retry. Rollback: git revert <diese Commit-SHA> bzw. Entfernen der Fixture _collect_leaked_qobjects aus tests/conftest.py stellt den alten Stand her.
1 parent 646f471 commit a764161

1 file changed

Lines changed: 35 additions & 0 deletions

File tree

tests/conftest.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010
Tests, die das notify-Verhalten gezielt pruefen (``tests/test_features.py``),
1111
legen innerhalb ihres ``with patch(...)``-Blocks einen eigenen, verschachtelten
1212
Patch an und bleiben dadurch unveraendert gueltig.
13+
14+
Zweiter Zweck: deterministischer Abbau geleakter Qt-Objekte zwischen Tests
15+
(siehe ``_collect_leaked_qobjects``).
1316
"""
1417
from __future__ import annotations
1518

19+
import gc
1620
from unittest.mock import MagicMock, patch
1721

1822
import pytest
@@ -23,3 +27,34 @@ def _block_real_notifications():
2327
"""Unterbindet echte ``notify-send``-Aufrufe in der gesamten Testsuite."""
2428
with patch("app.notify.subprocess.run", MagicMock()):
2529
yield
30+
31+
32+
@pytest.fixture(autouse=True)
33+
def _collect_leaked_qobjects():
34+
"""Sammelt nach JEDEM Test geleakte, parentlose QObjects deterministisch ein.
35+
36+
Root Cause der Python-3.12-Testisolation (``RuntimeError: wrapped C/C++
37+
object of type HotkeyWorker has been deleted``): Mehrere GUI-Tests erzeugen
38+
``BlitztextApp`` (selbst ein ``QObject``) samt ``HotkeyWorker``, ``QThread``
39+
und Fenstern ohne Qt-Parent und lassen die Python-Referenz am Testende
40+
einfach fallen. Weil diese Objekte in Referenzzyklen haengen (Signal-/Slot-
41+
Verbindungen zeigen auf ``self``), raeumt der Refcount sie nicht ab -- erst
42+
Pythons zyklischer Garbage Collector loescht die zugehoerigen C++-Objekte,
43+
und zwar zu einem nichtdeterministischen Zeitpunkt. Auf CPython 3.12 faellt
44+
dieser GC-Lauf reproduzierbar mitten in ein ``HotkeyWorker.run()`` eines
45+
Folgetests (``tests/test_state_machine.py::TestLeftAltEvents``); sip meldet
46+
dann den gerade laufenden Worker als geloescht, sobald ``run()`` das naechste
47+
Mal auf ``self`` (z. B. ``self.workflow_triggered``) zugreift.
48+
49+
Ein explizites ``gc.collect()`` an der Testgrenze macht diesen Abbau
50+
deterministisch: die geleakten Zyklen werden eingesammelt, WAEHREND kein
51+
Worker laeuft. Im Folgetest bleibt fuer den GC nichts mehr einzusammeln, das
52+
mit einem laufenden ``run()`` kollidieren koennte.
53+
54+
Bewusst NUR ``gc.collect()``: kein ``processEvents``/``DeferredDelete``-Pump,
55+
da dieser noch von Tests referenzierte Widgets vorzeitig loeschen und
56+
C++-seitige Doppel-Frees ausloesen kann. ``gc.collect()`` bricht nur echte,
57+
unerreichbare Zyklen auf und ist damit nebenwirkungsfrei.
58+
"""
59+
yield
60+
gc.collect()

0 commit comments

Comments
 (0)