Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
Tests, die das notify-Verhalten gezielt pruefen (``tests/test_features.py``),
legen innerhalb ihres ``with patch(...)``-Blocks einen eigenen, verschachtelten
Patch an und bleiben dadurch unveraendert gueltig.

Zweiter Zweck: deterministischer Abbau geleakter Qt-Objekte zwischen Tests
(siehe ``_collect_leaked_qobjects``).
"""
from __future__ import annotations

import gc
from unittest.mock import MagicMock, patch

import pytest
Expand All @@ -23,3 +27,34 @@ def _block_real_notifications():
"""Unterbindet echte ``notify-send``-Aufrufe in der gesamten Testsuite."""
with patch("app.notify.subprocess.run", MagicMock()):
yield


@pytest.fixture(autouse=True)
def _collect_leaked_qobjects():
"""Sammelt nach JEDEM Test geleakte, parentlose QObjects deterministisch ein.

Root Cause der Python-3.12-Testisolation (``RuntimeError: wrapped C/C++
object of type HotkeyWorker has been deleted``): Mehrere GUI-Tests erzeugen
``BlitztextApp`` (selbst ein ``QObject``) samt ``HotkeyWorker``, ``QThread``
und Fenstern ohne Qt-Parent und lassen die Python-Referenz am Testende
einfach fallen. Weil diese Objekte in Referenzzyklen haengen (Signal-/Slot-
Verbindungen zeigen auf ``self``), raeumt der Refcount sie nicht ab -- erst
Pythons zyklischer Garbage Collector loescht die zugehoerigen C++-Objekte,
und zwar zu einem nichtdeterministischen Zeitpunkt. Auf CPython 3.12 faellt
dieser GC-Lauf reproduzierbar mitten in ein ``HotkeyWorker.run()`` eines
Folgetests (``tests/test_state_machine.py::TestLeftAltEvents``); sip meldet
dann den gerade laufenden Worker als geloescht, sobald ``run()`` das naechste
Mal auf ``self`` (z. B. ``self.workflow_triggered``) zugreift.

Ein explizites ``gc.collect()`` an der Testgrenze macht diesen Abbau
deterministisch: die geleakten Zyklen werden eingesammelt, WAEHREND kein
Worker laeuft. Im Folgetest bleibt fuer den GC nichts mehr einzusammeln, das
mit einem laufenden ``run()`` kollidieren koennte.

Bewusst NUR ``gc.collect()``: kein ``processEvents``/``DeferredDelete``-Pump,
da dieser noch von Tests referenzierte Widgets vorzeitig loeschen und
C++-seitige Doppel-Frees ausloesen kann. ``gc.collect()`` bricht nur echte,
unerreichbare Zyklen auf und ist damit nebenwirkungsfrei.
"""
yield
gc.collect()