1010Tests, die das notify-Verhalten gezielt pruefen (``tests/test_features.py``),
1111legen innerhalb ihres ``with patch(...)``-Blocks einen eigenen, verschachtelten
1212Patch an und bleiben dadurch unveraendert gueltig.
13+
14+ Zweiter Zweck: deterministischer Abbau geleakter Qt-Objekte zwischen Tests
15+ (siehe ``_collect_leaked_qobjects``).
1316"""
1417from __future__ import annotations
1518
19+ import gc
1620from unittest .mock import MagicMock , patch
1721
1822import 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