Le test exerce enfin l'annulation de la garde, dans les deux interfaces

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Ralph Mayola 2026-08-23 19:16:41 +02:00
parent 82e0e16393
commit 99376a8e12
2 changed files with 123 additions and 51 deletions

View File

@ -23,8 +23,8 @@ repère basculé, pour ne pas sortir à l'envers.
import threading
from PySide6.QtCore import QPointF, QRectF, Qt, QTimer
from PySide6.QtGui import QColor, QFontMetrics, QPainter, QPainterPath, QPen
from PySide6.QtWidgets import QWidget
from PySide6.QtGui import QColor, QFontMetrics, QGuiApplication, QPainter, QPainterPath, QPen
from PySide6.QtWidgets import QApplication, QWidget
from audio_level import bar_heights, push_level
from core import CONFIG, bar_metrics
@ -184,7 +184,10 @@ class VueOverlay(QWidget):
painter.drawPath(tail)
def paintEvent(self, event):
painter = QPainter(self)
# `with` ferme le QPainter (.end()) meme si une exception surgit en
# cours de dessin : sans ca, la fermeture ne tenait qu'au comptage de
# references de PySide6 — suffisant aujourd'hui, fragile demain.
with QPainter(self) as painter:
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
w = CONFIG["bar_width"]
@ -253,13 +256,11 @@ class OverlayQt:
def _ensure_view(self):
if self._view is not None:
return
# Import local : PySide6 ne doit être exigé QUE quand cette classe est
# réellement instanciée (cf interface/__init__.py::classe_overlay_pour),
# jamais au chargement du module — sinon les tests réclameraient Qt sur
# toute machine, y compris ce Mac où Cocoa suffit.
from PySide6.QtGui import QGuiApplication
from PySide6.QtWidgets import QApplication
# PySide6 est déjà exigé dès l'IMPORT de ce module (QWidget, QTimer...
# ci-dessus) : le report réel n'est pas ici, mais dans la fabrique
# (interface/__init__.py::classe_overlay_pour), qui n'importe
# `interface.qt` que pour la branche win32 — jamais sur ce Mac, où
# Cocoa suffit.
if QApplication.instance() is None:
QApplication([])

View File

@ -26,3 +26,74 @@ def test_l_overlay_qt_declare_tout_le_contrat():
classe = classe_overlay_pour("win32")
for methode in ("show", "hide", "set_level", "set_status", "rearmer_garde"):
assert callable(getattr(classe, methode, None)), f"{methode} manque"
# --- garde de duree de vie : annulation reelle a hide() ---------------------
#
# Exception assumee au principe "aucune fenetre" du haut de ce fichier : la
# seule facon honnete de prouver qu'une garde est REELLEMENT annulee (et pas
# seulement oubliee dans le champ `_garde`) est d'interroger l'etat vrai de la
# minuterie native — NSTimer.isValid() cote Cocoa, QTimer.isActive() cote Qt.
# Une doublure (fake) ne fournirait aucune garantie sur ce point : elle ne
# ferait que rejouer ce que le code de production affirme deja faire.
#
# Piege du 21/08 (halo fige) : une garde dereferencee (`_garde = None`) mais
# jamais invalidee continue de courir en coulisse, et peut refermer un overlay
# rouvert entre-temps, en pleine dictee. D'ou : on capture l'objet minuterie
# AVANT hide()/rearmer_garde(), pour verifier SON etat a lui, pas seulement le
# champ qui le reference.
from interface.cocoa import PulseWindow
from interface.qt import OverlayQt
def _garde_active_cocoa(garde):
return bool(garde.isValid())
def _garde_active_qt(garde):
return garde.isActive()
_IMPLEMENTATIONS_REELLES = [
(lambda: PulseWindow.alloc().init(), _garde_active_cocoa),
(OverlayQt, _garde_active_qt),
]
_IDS_IMPLEMENTATIONS = ["cocoa", "qt"]
@pytest.fixture(params=_IMPLEMENTATIONS_REELLES, ids=_IDS_IMPLEMENTATIONS)
def overlay_et_garde_active(request):
"""Une vraie instance d'overlay, plus la fonction qui lit l'etat reel de sa
minuterie de garde. Ferme toujours la fenetre a la fin, meme si le test echoue."""
construire, garde_active = request.param
ov = construire()
try:
yield ov, garde_active
finally:
ov.hide()
def test_show_arme_une_garde_valide(overlay_et_garde_active):
ov, garde_active = overlay_et_garde_active
ov.show()
assert ov._garde is not None
assert garde_active(ov._garde)
def test_hide_annule_reellement_la_garde(overlay_et_garde_active):
ov, garde_active = overlay_et_garde_active
ov.show()
garde = ov._garde # reference gardee AVANT hide()
ov.hide()
assert ov._garde is None
assert not garde_active(garde), "la minuterie doit etre invalidee, pas seulement oubliee"
def test_rearmer_garde_ne_laisse_qu_une_seule_garde_active(overlay_et_garde_active):
ov, garde_active = overlay_et_garde_active
ov.show()
ancienne_garde = ov._garde
ov.rearmer_garde(5.0)
assert ov._garde is not ancienne_garde
assert not garde_active(ancienne_garde), "l'ancienne garde doit etre invalidee au remplacement"
assert garde_active(ov._garde)