Un lanceur Qt pour Windows, et un assemblage partage
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
"""L'assemblage commun aux deux lanceurs — sans jamais ouvrir de fenêtre ni de micro.
|
||||
|
||||
Le transcripteur réel charge un modèle (opération lourde, parfois absente sur
|
||||
la machine de test) : on injecte donc toujours un `choisir_transcripteur` et
|
||||
un `detecter_moteur` doublés. L'overlay réel (Cocoa ou Qt), lui, est fait pour
|
||||
être construit sans effet visible tant que `show()` n'est pas appelé — les
|
||||
tests le laissent donc réel, et vérifient justement qu'aucune fenêtre ne
|
||||
s'affiche.
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from assemblage import acquerir_verrou, construire
|
||||
from controller import ZonzaController
|
||||
from systeme import choisir_systeme
|
||||
|
||||
|
||||
class _TranscripteurFactice:
|
||||
def __init__(self, modele, langue, amorce):
|
||||
self.modele = modele
|
||||
self.langue = langue
|
||||
self.amorce = amorce
|
||||
|
||||
def warmup(self):
|
||||
pass
|
||||
|
||||
def transcrire(self, audio):
|
||||
return "texte factice"
|
||||
|
||||
|
||||
def _choisir_transcripteur_factice(nom, modele, langue, amorce):
|
||||
return _TranscripteurFactice(modele, langue, amorce)
|
||||
|
||||
|
||||
def _tmp_verrou():
|
||||
return os.path.join(tempfile.mkdtemp(), "instance.lock")
|
||||
|
||||
|
||||
# --- construire() ------------------------------------------------------------
|
||||
|
||||
def test_construire_rend_un_controleur_cable_sur_les_bons_objets():
|
||||
resultat = construire(
|
||||
plateforme="darwin",
|
||||
detecter_moteur=lambda: "mlx",
|
||||
choisir_transcripteur=_choisir_transcripteur_factice,
|
||||
)
|
||||
assert isinstance(resultat["controller"], ZonzaController)
|
||||
# le contrôleur pointe vers CE système, CE transcripteur, CET overlay —
|
||||
# pas une copie recréée à côté.
|
||||
assert resultat["controller"].systeme is resultat["systeme"]
|
||||
assert resultat["controller"].transcripteur is resultat["transcripteur"]
|
||||
assert resultat["controller"].pulse is resultat["overlay"]
|
||||
assert isinstance(resultat["transcripteur"], _TranscripteurFactice)
|
||||
|
||||
|
||||
def test_construire_fonctionne_aussi_pour_win32():
|
||||
resultat = construire(
|
||||
plateforme="win32",
|
||||
detecter_moteur=lambda: "faster-cpu",
|
||||
choisir_transcripteur=_choisir_transcripteur_factice,
|
||||
)
|
||||
from interface.qt import OverlayQt
|
||||
assert isinstance(resultat["overlay"], OverlayQt)
|
||||
|
||||
|
||||
def test_construire_transmet_run_on_main_et_on_state():
|
||||
appels = []
|
||||
resultat = construire(
|
||||
plateforme="darwin",
|
||||
run_on_main=lambda fn: appels.append(fn) or fn(),
|
||||
on_state=lambda s: appels.append(s),
|
||||
detecter_moteur=lambda: "mlx",
|
||||
choisir_transcripteur=_choisir_transcripteur_factice,
|
||||
)
|
||||
resultat["controller"].on_state("recording")
|
||||
assert "recording" in appels
|
||||
|
||||
|
||||
def test_construire_ne_leve_aucune_erreur_de_config():
|
||||
# validate_config(CONFIG) est appelée à l'intérieur : si elle levait, le
|
||||
# test échouerait ici plutôt qu'au premier lancement réel.
|
||||
construire(
|
||||
plateforme="darwin",
|
||||
detecter_moteur=lambda: "mlx",
|
||||
choisir_transcripteur=_choisir_transcripteur_factice,
|
||||
)
|
||||
|
||||
|
||||
# --- acquerir_verrou() --------------------------------------------------------
|
||||
|
||||
def test_acquerir_verrou_rend_un_handle_pour_la_premiere_instance():
|
||||
systeme = choisir_systeme()
|
||||
handle = acquerir_verrou(systeme, _tmp_verrou())
|
||||
assert handle is not None
|
||||
|
||||
|
||||
def test_acquerir_verrou_refuse_une_seconde_instance():
|
||||
systeme = choisir_systeme()
|
||||
chemin = _tmp_verrou()
|
||||
premier = acquerir_verrou(systeme, chemin)
|
||||
assert premier is not None
|
||||
assert acquerir_verrou(systeme, chemin) is None
|
||||
|
||||
|
||||
def test_acquerir_verrou_sans_chemin_utilise_le_dossier_de_donnees_du_systeme():
|
||||
# Pas de deuxième prise ici (on toucherait le verrou réel de l'utilisateur) :
|
||||
# on vérifie seulement que le chemin par défaut est bien dérivé du système.
|
||||
class _SystemeSonde:
|
||||
def __init__(self):
|
||||
self.chemin_demande = None
|
||||
|
||||
def dossier_donnees(self):
|
||||
return "/tmp/zonza-sonde"
|
||||
|
||||
def acquerir_verrou_instance(self, chemin):
|
||||
self.chemin_demande = chemin
|
||||
return "handle-factice"
|
||||
|
||||
sonde = _SystemeSonde()
|
||||
resultat = acquerir_verrou(sonde)
|
||||
assert resultat == "handle-factice"
|
||||
assert sonde.chemin_demande == "/tmp/zonza-sonde/instance.lock"
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Ce qui est testable dans le lanceur Qt SANS boucle d'événements.
|
||||
|
||||
`ZonzaTray.__init__` construit le menu et l'icône de façon synchrone (aucun
|
||||
`.show()`, aucune boucle) : c'est déjà éprouvable. Le marshalage par signal
|
||||
Qt.QueuedConnection, lui, N'EST PAS testé ici — sa livraison dépend d'une
|
||||
boucle d'événements qui tourne, ce qu'aucun test de ce fichier ne fait
|
||||
tourner. On teste donc directement les méthodes qu'un slot appellerait
|
||||
(`_apply_title`, `toggle_sounds`), pas le trajet signal → file → slot.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
PySide6 = pytest.importorskip("PySide6")
|
||||
|
||||
from PySide6.QtWidgets import QApplication # noqa: E402
|
||||
|
||||
from core import CONFIG # noqa: E402
|
||||
from lanceur_qt import _STATE_TITLES, ZonzaTray # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def _app_qt():
|
||||
# QIcon/QPixmap/QSystemTrayIcon exigent une QApplication déjà construite —
|
||||
# sans .exec() : on ne fait jamais tourner la boucle d'événements.
|
||||
app = QApplication.instance() or QApplication([])
|
||||
yield app
|
||||
|
||||
|
||||
class _ControleurFactice:
|
||||
"""Ni toggle() ni transcripteur ne sont appelés tant que .show() ne l'est
|
||||
pas — ce que ces tests évitent justement."""
|
||||
def toggle(self):
|
||||
raise AssertionError("ne doit pas être appelé sans .show()")
|
||||
|
||||
|
||||
class _SystemeFactice:
|
||||
def raccourci_defaut(self):
|
||||
return "<ctrl>+<alt>+d"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tray():
|
||||
t = ZonzaTray(_ControleurFactice(), _SystemeFactice())
|
||||
yield t
|
||||
# évite qu'un menu/tray fantôme traîne d'un test à l'autre (pas de .show()
|
||||
# appelé ici, donc rien n'a jamais été rendu visible).
|
||||
t.menu.deleteLater()
|
||||
|
||||
|
||||
# --- construction du menu -----------------------------------------------
|
||||
|
||||
def test_le_menu_contient_exactement_sons_et_quitter():
|
||||
t = ZonzaTray(_ControleurFactice(), _SystemeFactice())
|
||||
titres = [a.text() for a in t.menu.actions() if not a.isSeparator()]
|
||||
assert titres == ["Sons", "Quitter"]
|
||||
|
||||
|
||||
def test_sons_est_une_case_a_cocher():
|
||||
t = ZonzaTray(_ControleurFactice(), _SystemeFactice())
|
||||
assert t.sounds_action.isCheckable()
|
||||
|
||||
|
||||
def test_l_etat_initial_de_la_case_suit_la_config():
|
||||
CONFIG["sounds"] = True
|
||||
t = ZonzaTray(_ControleurFactice(), _SystemeFactice())
|
||||
assert t.sounds_action.isChecked() is True
|
||||
|
||||
|
||||
def test_le_menu_est_bien_celui_du_tray():
|
||||
t = ZonzaTray(_ControleurFactice(), _SystemeFactice())
|
||||
assert t.tray.contextMenu() is t.menu
|
||||
|
||||
|
||||
# --- bascule des sons -----------------------------------------------------
|
||||
|
||||
def test_toggle_sounds_coupe_le_son(tray):
|
||||
CONFIG["sounds"] = True
|
||||
tray.toggle_sounds(False)
|
||||
assert CONFIG["sounds"] is False
|
||||
assert tray.sounds_action.isChecked() is False
|
||||
|
||||
|
||||
def test_toggle_sounds_reactive_le_son(tray):
|
||||
CONFIG["sounds"] = False
|
||||
tray.toggle_sounds(True)
|
||||
assert CONFIG["sounds"] is True
|
||||
assert tray.sounds_action.isChecked() is True
|
||||
|
||||
|
||||
def test_toggle_sounds_ne_touche_a_rien_d_autre_dans_la_config(tray):
|
||||
avant = dict(CONFIG)
|
||||
tray.toggle_sounds(True)
|
||||
apres = dict(CONFIG)
|
||||
del avant["sounds"]
|
||||
del apres["sounds"]
|
||||
assert avant == apres
|
||||
|
||||
|
||||
# --- correspondance état → icône ------------------------------------------
|
||||
|
||||
def test_trois_etats_sont_couverts():
|
||||
assert set(_STATE_TITLES) == {"idle", "recording", "transcribing"}
|
||||
|
||||
|
||||
def test_etat_idle_par_defaut(tray):
|
||||
assert tray._current_state == "idle"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("etat", ["idle", "recording", "transcribing"])
|
||||
def test_apply_title_memorise_l_etat_recu(tray, etat):
|
||||
tray._apply_title(etat)
|
||||
assert tray._current_state == etat
|
||||
|
||||
|
||||
def test_apply_title_pose_une_icone_non_nulle_pour_chaque_etat(tray):
|
||||
for etat in _STATE_TITLES:
|
||||
tray._apply_title(etat)
|
||||
assert not tray.tray.icon().isNull()
|
||||
|
||||
|
||||
def test_un_etat_inconnu_retombe_sur_l_icone_de_repos(tray):
|
||||
# _apply_title ne doit jamais planter sur un état imprévu — repli sur
|
||||
# "idle" (même garde que app.py : `_STATE_TITLES.get(state, "🎙️")`).
|
||||
tray._apply_title("etat-invente")
|
||||
assert not tray.tray.icon().isNull()
|
||||
Reference in New Issue
Block a user