Compare commits
No commits in common. "3e269dfb5e1da1a219021c3c0e39797894d457e1" and "99376a8e12d77bcd2255d410c2e5350fc67f747e" have entirely different histories.
3e269dfb5e
...
99376a8e12
13
app.py
13
app.py
@ -12,19 +12,6 @@ native est de toute façon minuscule pour nos besoins : une icône, deux items d
|
|||||||
Zonza vit dans la barre de menu, jamais dans le Dock : c'est le LSUIElement de
|
Zonza vit dans la barre de menu, jamais dans le Dock : c'est le LSUIElement de
|
||||||
l'Info.plist (posé par build_app.sh) qui s'en charge.
|
l'Info.plist (posé par build_app.sh) qui s'en charge.
|
||||||
"""
|
"""
|
||||||
import sys
|
|
||||||
|
|
||||||
# --diagnostic doit s'évaluer AVANT tout import Cocoa/pyobjc : sur une machine
|
|
||||||
# qui ne les a pas (aujourd'hui aucune Windows n'est disponible pour ce
|
|
||||||
# portage, mais c'est la raison d'être de cette commande), c'est le seul
|
|
||||||
# moyen d'éprouver la chaîne. La sortie se fait donc ICI, au chargement du
|
|
||||||
# module — jamais dans main(), qui serait déjà trop tard pour éviter
|
|
||||||
# `from AppKit import ...` ci-dessous.
|
|
||||||
if "--diagnostic" in sys.argv:
|
|
||||||
from diagnostic import diagnostiquer, mettre_en_forme
|
|
||||||
print(mettre_en_forme(diagnostiquer()))
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
|||||||
312
diagnostic.py
312
diagnostic.py
@ -1,312 +0,0 @@
|
|||||||
"""`zonza --diagnostic` : éprouve toute la chaîne sur la machine cible et dit où
|
|
||||||
elle casse.
|
|
||||||
|
|
||||||
Aucune machine Windows n'étant disponible pour ce portage, cette commande est
|
|
||||||
le SEUL moyen pour l'utilisateur de valider `systeme/windows.py` en conditions
|
|
||||||
réelles. D'où deux règles strictes suivies ici :
|
|
||||||
|
|
||||||
1. Chaque épreuve attrape SES PROPRES erreurs : une panne (micro absent,
|
|
||||||
moteur introuvable, `say` manquant...) ne doit jamais faire tomber les
|
|
||||||
épreuves suivantes ni le rapport entier.
|
|
||||||
2. Une épreuve qui n'a pas pu être menée est marquée `reussite=None`
|
|
||||||
(« non concluante ») — JAMAIS `True`. Mentir sur une réussite serait pire
|
|
||||||
que ne rien dire : mieux vaut « je ne sais pas » qu'un faux vert.
|
|
||||||
|
|
||||||
Deux fonctions séparées, pour rester testable : `diagnostiquer()` rend une
|
|
||||||
liste de `ResultatEpreuve` (aucun `print`) ; `mettre_en_forme()` la traduit en
|
|
||||||
texte affichable. Toutes les dépendances matérielles/plateforme sont
|
|
||||||
injectables, ce qui permet d'éprouver l'ENCHAÎNEMENT des épreuves depuis ce
|
|
||||||
Mac sans jamais toucher un vrai micro ni un vrai moteur de transcription.
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
import wave
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
import core
|
|
||||||
import moteur
|
|
||||||
from moteur import _cuda_disponible
|
|
||||||
from systeme import choisir_systeme
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ResultatEpreuve:
|
|
||||||
"""Le compte-rendu d'une seule épreuve.
|
|
||||||
|
|
||||||
`reussite` : True (menée, et tout va bien), False (menée, et quelque chose
|
|
||||||
est cassé), ou None (n'a PAS pu être menée — non concluante, jamais
|
|
||||||
présentée comme une réussite).
|
|
||||||
"""
|
|
||||||
nom: str
|
|
||||||
reussite: bool | None
|
|
||||||
detail: str
|
|
||||||
|
|
||||||
|
|
||||||
# --- épreuve 1 : plateforme -------------------------------------------------
|
|
||||||
|
|
||||||
def epreuve_plateforme(plateforme=None, machine=None, cuda_disponible=None):
|
|
||||||
"""Décrit la machine : plateforme Python, architecture, présence de CUDA."""
|
|
||||||
plateforme = plateforme if plateforme is not None else sys.platform
|
|
||||||
machine = machine if machine is not None else platform.machine()
|
|
||||||
sonder_cuda = cuda_disponible if cuda_disponible is not None else _cuda_disponible
|
|
||||||
try:
|
|
||||||
cuda = sonder_cuda()
|
|
||||||
except Exception as e:
|
|
||||||
return ResultatEpreuve("Plateforme", None, f"sonde CUDA en échec : {e}")
|
|
||||||
cuda_txt = "présent" if cuda else "absent"
|
|
||||||
return ResultatEpreuve("Plateforme", True, f"{plateforme} / {machine} / CUDA {cuda_txt}")
|
|
||||||
|
|
||||||
|
|
||||||
# --- épreuve 2 : moteur retenu ----------------------------------------------
|
|
||||||
|
|
||||||
def epreuve_moteur(detecter_moteur=None, resoudre_modele=None, creer_transcripteur=None):
|
|
||||||
"""Décide du moteur, résout son modèle, instancie le transcripteur.
|
|
||||||
|
|
||||||
Rend `(resultat, transcripteur_ou_None)` : le transcripteur sert à
|
|
||||||
l'épreuve Transcription sans être reconstruit — mais seulement si cette
|
|
||||||
épreuve a réussi ; sinon la Transcription sera honnêtement sautée.
|
|
||||||
"""
|
|
||||||
detecter_moteur = detecter_moteur or moteur.detecter_moteur
|
|
||||||
resoudre_modele = resoudre_modele or core.modele_retenu
|
|
||||||
creer_transcripteur = creer_transcripteur or moteur.choisir_transcripteur
|
|
||||||
try:
|
|
||||||
nom = detecter_moteur()
|
|
||||||
nom_modele = resoudre_modele(nom, core.CONFIG["model"])
|
|
||||||
transcripteur = creer_transcripteur(
|
|
||||||
nom=nom, modele=nom_modele, langue=core.CONFIG["language"],
|
|
||||||
amorce=core.CONFIG["initial_prompt"],
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return ResultatEpreuve("Moteur retenu", None, f"détection impossible : {e}"), None
|
|
||||||
return ResultatEpreuve("Moteur retenu", True, f"{nom} + {nom_modele}"), transcripteur
|
|
||||||
|
|
||||||
|
|
||||||
# --- épreuve 3 : micro -------------------------------------------------------
|
|
||||||
|
|
||||||
def epreuve_micro(ouvrir_flux=None, sample_rate=None, duree_s=1.0, dormir=None):
|
|
||||||
"""Ouvre VRAIMENT le micro pendant `duree_s` et compte les blocs reçus.
|
|
||||||
|
|
||||||
`ouvrir_flux` suit le même contrat que `Recorder._ouvrir_flux` dans
|
|
||||||
`capture.py` : injectable, pour que les tests ne touchent à aucun
|
|
||||||
périphérique réel.
|
|
||||||
|
|
||||||
Le débit vient de CONFIG et n'est pas codé en dur : le diagnostic doit
|
|
||||||
éprouver le micro AU DÉBIT QUE L'APPLICATION UTILISE VRAIMENT, sinon il
|
|
||||||
cesserait silencieusement d'être représentatif si ce réglage changeait.
|
|
||||||
"""
|
|
||||||
from core import CONFIG
|
|
||||||
sample_rate = sample_rate or CONFIG["sample_rate"]
|
|
||||||
dormir = dormir or time.sleep
|
|
||||||
if ouvrir_flux is None:
|
|
||||||
try:
|
|
||||||
import sounddevice as sd
|
|
||||||
except Exception as e:
|
|
||||||
return ResultatEpreuve("Micro", None, f"sounddevice indisponible : {e}")
|
|
||||||
ouvrir_flux = lambda **kw: sd.InputStream(**kw) # noqa: E731
|
|
||||||
|
|
||||||
compte = {"n": 0}
|
|
||||||
|
|
||||||
def on_block(indata, frames, time_info, status):
|
|
||||||
compte["n"] += 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
flux = ouvrir_flux(samplerate=sample_rate, channels=1, dtype="float32",
|
|
||||||
callback=on_block)
|
|
||||||
flux.start()
|
|
||||||
except Exception as e:
|
|
||||||
return ResultatEpreuve("Micro", None, f"ouverture du micro impossible : {e}")
|
|
||||||
|
|
||||||
dormir(duree_s)
|
|
||||||
|
|
||||||
for action in ("stop", "close"):
|
|
||||||
try:
|
|
||||||
getattr(flux, action)()
|
|
||||||
except Exception:
|
|
||||||
pass # meilleur effort : le compte déjà reçu reste valable
|
|
||||||
|
|
||||||
n = compte["n"]
|
|
||||||
return ResultatEpreuve("Micro", n > 0, f"{n} blocs reçus en {duree_s:g} s")
|
|
||||||
|
|
||||||
|
|
||||||
# --- épreuve 4 : transcription -----------------------------------------------
|
|
||||||
|
|
||||||
def _generer_echantillon_parole(texte="Bonjour, ceci est un test de dictée Zonza.",
|
|
||||||
sample_rate=None):
|
|
||||||
"""Synthétise un court échantillon de parole via la commande `say` de macOS.
|
|
||||||
|
|
||||||
LÈVE si `say` est absent, échoue, ou si le WAV produit est illisible —
|
|
||||||
c'est volontaire : l'appelant retombe alors sur du silence plutôt que
|
|
||||||
d'inventer un résultat. Sur une machine sans `say` (Windows, Linux),
|
|
||||||
c'est TOUJOURS ce chemin qui est emprunté.
|
|
||||||
"""
|
|
||||||
from core import CONFIG
|
|
||||||
sample_rate = sample_rate or CONFIG["sample_rate"]
|
|
||||||
if shutil.which("say") is None:
|
|
||||||
raise RuntimeError("commande `say` absente (probablement pas macOS)")
|
|
||||||
chemin = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
["say", "-o", chemin, f"--data-format=LEI16@{sample_rate}", texte],
|
|
||||||
check=True, capture_output=True, timeout=15,
|
|
||||||
)
|
|
||||||
with wave.open(chemin, "rb") as w:
|
|
||||||
brut = w.readframes(w.getnframes())
|
|
||||||
return np.frombuffer(brut, dtype="<i2").astype("float32") / 32768.0
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
os.unlink(chemin)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def epreuve_transcription(transcripteur, generer_parole=None, horloge=None):
|
|
||||||
"""Transcrit VRAIMENT un échantillon — parole synthétisée si possible,
|
|
||||||
sinon une seconde de silence — et rapporte la durée réellement mesurée."""
|
|
||||||
horloge = horloge or time.monotonic
|
|
||||||
generer_parole = generer_parole or _generer_echantillon_parole
|
|
||||||
try:
|
|
||||||
audio = generer_parole()
|
|
||||||
source = "parole synthétisée"
|
|
||||||
except Exception as e:
|
|
||||||
from core import CONFIG
|
|
||||||
audio = np.zeros(CONFIG["sample_rate"], dtype="float32") # une seconde
|
|
||||||
source = f"silence (synthèse vocale indisponible : {e})"
|
|
||||||
debut = horloge()
|
|
||||||
try:
|
|
||||||
texte = transcripteur.transcrire(audio)
|
|
||||||
except Exception as e:
|
|
||||||
return ResultatEpreuve("Transcription", None,
|
|
||||||
f"échec de la transcription ({source}) : {e}")
|
|
||||||
duree = horloge() - debut
|
|
||||||
return ResultatEpreuve("Transcription", True, f"{duree:.2f} s — « {texte} » [{source}]")
|
|
||||||
|
|
||||||
|
|
||||||
# --- épreuve 5 : presse-papier ------------------------------------------------
|
|
||||||
|
|
||||||
def epreuve_presse_papier(copier=None, lire=None):
|
|
||||||
"""Copie un texte marqueur, le relit, vérifie qu'il n'a pas changé en route."""
|
|
||||||
if copier is None or lire is None:
|
|
||||||
try:
|
|
||||||
import pyperclip
|
|
||||||
except Exception as e:
|
|
||||||
return ResultatEpreuve("Presse-papier", None, f"pyperclip indisponible : {e}")
|
|
||||||
copier = copier or pyperclip.copy
|
|
||||||
lire = lire or pyperclip.paste
|
|
||||||
texte = f"zonza-diagnostic-{uuid.uuid4().hex[:8]}"
|
|
||||||
try:
|
|
||||||
copier(texte)
|
|
||||||
relu = lire()
|
|
||||||
except Exception as e:
|
|
||||||
return ResultatEpreuve("Presse-papier", None, f"copie/collage impossible : {e}")
|
|
||||||
if relu == texte:
|
|
||||||
return ResultatEpreuve("Presse-papier", True, "copie puis relecture identiques")
|
|
||||||
return ResultatEpreuve("Presse-papier", False, f"attendu {texte!r}, relu {relu!r}")
|
|
||||||
|
|
||||||
|
|
||||||
# --- épreuve 6 : raccourci ----------------------------------------------------
|
|
||||||
|
|
||||||
def epreuve_raccourci(systeme):
|
|
||||||
"""Rapporte le raccourci par défaut de cette plateforme (aucune action)."""
|
|
||||||
try:
|
|
||||||
raccourci = systeme.raccourci_defaut()
|
|
||||||
except Exception as e:
|
|
||||||
return ResultatEpreuve("Raccourci", None, f"indisponible : {e}")
|
|
||||||
return ResultatEpreuve("Raccourci", True, raccourci)
|
|
||||||
|
|
||||||
|
|
||||||
# --- épreuve 7 : verrou --------------------------------------------------------
|
|
||||||
|
|
||||||
def epreuve_verrou(systeme, chemin=None):
|
|
||||||
"""Vérifie qu'une seconde tentative de verrou sur le même chemin est refusée."""
|
|
||||||
chemin = chemin or os.path.join(
|
|
||||||
tempfile.mkdtemp(prefix="zonza-diagnostic-"), "diagnostic.lock"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
premier = systeme.acquerir_verrou_instance(chemin)
|
|
||||||
second = systeme.acquerir_verrou_instance(chemin)
|
|
||||||
except Exception as e:
|
|
||||||
return ResultatEpreuve("Verrou", None, f"indisponible : {e}")
|
|
||||||
|
|
||||||
if premier is not None:
|
|
||||||
try:
|
|
||||||
premier.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if premier is None:
|
|
||||||
return ResultatEpreuve("Verrou", None, "impossible d'obtenir le premier verrou")
|
|
||||||
if second is None:
|
|
||||||
return ResultatEpreuve("Verrou", True, "seconde instance refusée")
|
|
||||||
return ResultatEpreuve("Verrou", False, "seconde instance NON refusée — verrou inefficace")
|
|
||||||
|
|
||||||
|
|
||||||
# --- orchestration -------------------------------------------------------------
|
|
||||||
|
|
||||||
def diagnostiquer(systeme=None, plateforme=None, machine=None, cuda_disponible=None,
|
|
||||||
detecter_moteur=None, resoudre_modele=None, creer_transcripteur=None,
|
|
||||||
ouvrir_flux=None, duree_micro_s=1.0, dormir=None,
|
|
||||||
generer_parole=None, horloge=None, copier=None, lire=None):
|
|
||||||
"""Éprouve toute la chaîne, dans l'ordre du rapport.
|
|
||||||
|
|
||||||
Ne lève JAMAIS : chaque épreuve attrape ses propres erreurs et se marque
|
|
||||||
non concluante plutôt que de faire tomber les suivantes ou le diagnostic
|
|
||||||
entier. Rend une liste de `ResultatEpreuve` — pas de `print` ici, pour que
|
|
||||||
ce soit testable indépendamment de l'affichage (cf `mettre_en_forme`).
|
|
||||||
"""
|
|
||||||
systeme = systeme or choisir_systeme()
|
|
||||||
resultats = [epreuve_plateforme(plateforme, machine, cuda_disponible)]
|
|
||||||
|
|
||||||
resultat_moteur, transcripteur = epreuve_moteur(
|
|
||||||
detecter_moteur, resoudre_modele, creer_transcripteur
|
|
||||||
)
|
|
||||||
resultats.append(resultat_moteur)
|
|
||||||
|
|
||||||
resultats.append(epreuve_micro(ouvrir_flux, duree_s=duree_micro_s, dormir=dormir))
|
|
||||||
|
|
||||||
if transcripteur is not None:
|
|
||||||
resultats.append(epreuve_transcription(transcripteur, generer_parole, horloge))
|
|
||||||
else:
|
|
||||||
resultats.append(ResultatEpreuve(
|
|
||||||
"Transcription", None, "sautée — aucun moteur n'a pu être instancié"
|
|
||||||
))
|
|
||||||
|
|
||||||
resultats.append(epreuve_presse_papier(copier, lire))
|
|
||||||
resultats.append(epreuve_raccourci(systeme))
|
|
||||||
resultats.append(epreuve_verrou(systeme))
|
|
||||||
return resultats
|
|
||||||
|
|
||||||
|
|
||||||
_SYMBOLES = {True: "✅", False: "❌", None: "➖"}
|
|
||||||
|
|
||||||
|
|
||||||
def formater_resultat(resultat):
|
|
||||||
"""Une ligne du rapport : symbole, nom aligné, détail."""
|
|
||||||
symbole = _SYMBOLES.get(resultat.reussite, "➖")
|
|
||||||
return f"{symbole} {resultat.nom.ljust(16)}: {resultat.detail}"
|
|
||||||
|
|
||||||
|
|
||||||
def mettre_en_forme(resultats):
|
|
||||||
"""Traduit une liste de `ResultatEpreuve` en texte affichable.
|
|
||||||
|
|
||||||
Séparée de `diagnostiquer()` à dessein : cette fonction ne fait aucun
|
|
||||||
travail, uniquement de la mise en forme — donc testable sur des résultats
|
|
||||||
fabriqués à la main, sans jamais exécuter une seule épreuve.
|
|
||||||
"""
|
|
||||||
lignes = [formater_resultat(r) for r in resultats]
|
|
||||||
reussies = sum(1 for r in resultats if r.reussite is True)
|
|
||||||
echouees = sum(1 for r in resultats if r.reussite is False)
|
|
||||||
non_conclu = sum(1 for r in resultats if r.reussite is None)
|
|
||||||
lignes.append("")
|
|
||||||
lignes.append(
|
|
||||||
f"{reussies}/{len(resultats)} réussies, {echouees} en échec, "
|
|
||||||
f"{non_conclu} non concluante(s)."
|
|
||||||
)
|
|
||||||
return "\n".join(lignes)
|
|
||||||
@ -12,7 +12,4 @@ def choisir_systeme(plateforme=None):
|
|||||||
if plateforme.startswith("darwin"):
|
if plateforme.startswith("darwin"):
|
||||||
from systeme.macos import SystemeMacOS
|
from systeme.macos import SystemeMacOS
|
||||||
return SystemeMacOS()
|
return SystemeMacOS()
|
||||||
if plateforme.startswith("win32"):
|
|
||||||
from systeme.windows import SystemeWindows
|
|
||||||
return SystemeWindows()
|
|
||||||
raise NotImplementedError(f"Plateforme non prise en charge : {plateforme}")
|
raise NotImplementedError(f"Plateforme non prise en charge : {plateforme}")
|
||||||
|
|||||||
@ -1,120 +0,0 @@
|
|||||||
"""Appels propres à Windows. Rien ici ne doit être importé directement par le noyau.
|
|
||||||
|
|
||||||
Miroir de `systeme/macos.py`, mais avec une contrainte que macOS n'a pas :
|
|
||||||
`winsound`, `msvcrt` et `ctypes.windll` n'EXISTENT PAS sur macOS. Un import en
|
|
||||||
tête de fichier rendrait ce module impossible à charger — donc impossible à
|
|
||||||
tester — depuis la machine de développement (aucune machine Windows n'est
|
|
||||||
disponible pour ce portage). Les trois dépendances sont donc INJECTABLES :
|
|
||||||
`jouer`, `clavier`, `verrouiller`. Par défaut (None), elles se résolvent
|
|
||||||
paresseusement vers l'API Windows réelle au moment de l'appel, jamais à
|
|
||||||
l'import. Les tests injectent des doublures et n'exécutent donc jamais une
|
|
||||||
ligne de code Windows — ils éprouvent la LOGIQUE de cette classe (mapping des
|
|
||||||
sons, masque des touches, construction des chemins, séquence d'acquisition du
|
|
||||||
verrou), pas le comportement réel de winsound/msvcrt/user32, qu'aucune
|
|
||||||
machine ici ne peut vérifier.
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Alias système Windows joués par winsound.PlaySound(alias, SND_ALIAS | SND_ASYNC).
|
|
||||||
_SONS = {
|
|
||||||
"start": "SystemAsterisk",
|
|
||||||
"done": "SystemExclamation",
|
|
||||||
"error": "SystemHand",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Virtual-key codes Windows des cinq touches modificatrices à surveiller.
|
|
||||||
# ctrl, shift, alt (menu), windows gauche, windows droite.
|
|
||||||
_TOUCHES_MODIFICATRICES = (0x11, 0x10, 0x12, 0x5B, 0x5C)
|
|
||||||
|
|
||||||
# Dans le retour de GetAsyncKeyState, le bit de poids fort indique que la
|
|
||||||
# touche est enfoncée EN CE MOMENT (les bits bas concernent un état passé
|
|
||||||
# qui ne nous intéresse pas ici).
|
|
||||||
_BIT_ENFONCEE = 0x8000
|
|
||||||
|
|
||||||
|
|
||||||
class SystemeWindows:
|
|
||||||
"""Implémentation du contrat Systeme pour Windows.
|
|
||||||
|
|
||||||
jouer(alias) : joue un son. clavier(vk) : rend l'état d'une touche.
|
|
||||||
verrouiller(fichier) : pose un verrou exclusif non bloquant sur `fichier`,
|
|
||||||
lève OSError s'il est déjà pris. Les trois par défaut à None : résolus
|
|
||||||
paresseusement vers l'API Windows réelle au premier usage.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, jouer=None, clavier=None, verrouiller=None):
|
|
||||||
self._jouer = jouer
|
|
||||||
self._clavier = clavier
|
|
||||||
self._verrouiller = verrouiller
|
|
||||||
|
|
||||||
# --- son ------------------------------------------------------------
|
|
||||||
def jouer_son(self, nom):
|
|
||||||
alias = _SONS.get(nom)
|
|
||||||
if not alias:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
(self._jouer or self._jouer_reel)(alias)
|
|
||||||
except Exception:
|
|
||||||
pass # le son ne doit jamais casser la dictée (même raison qu'en macOS)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _jouer_reel(alias):
|
|
||||||
import winsound
|
|
||||||
winsound.PlaySound(alias, winsound.SND_ALIAS | winsound.SND_ASYNC)
|
|
||||||
|
|
||||||
# --- clavier ----------------------------------------------------------
|
|
||||||
def modificateurs_enfonces(self):
|
|
||||||
sonder = self._clavier or self._clavier_reel
|
|
||||||
try:
|
|
||||||
return any(sonder(vk) & _BIT_ENFONCEE for vk in _TOUCHES_MODIFICATRICES)
|
|
||||||
except Exception:
|
|
||||||
return False # dans le doute, ne jamais bloquer le collage (idem macOS)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _clavier_reel(vk):
|
|
||||||
import ctypes
|
|
||||||
return ctypes.windll.user32.GetAsyncKeyState(vk)
|
|
||||||
|
|
||||||
# --- raccourci --------------------------------------------------------
|
|
||||||
def raccourci_defaut(self):
|
|
||||||
# <cmd> n'existe pas sur Windows : pas d'équivalent à la touche Commande.
|
|
||||||
return "<ctrl>+<alt>+d"
|
|
||||||
|
|
||||||
# --- chemins ------------------------------------------------------------
|
|
||||||
def chemin_log(self):
|
|
||||||
return os.path.join(self._dossier_local_appdata(), "Zonza", "Zonza.log")
|
|
||||||
|
|
||||||
def dossier_donnees(self):
|
|
||||||
return os.path.join(self._dossier_local_appdata(), "Zonza")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _dossier_local_appdata():
|
|
||||||
# os.environ est standard : lisible (et falsifiable par les tests via
|
|
||||||
# monkeypatch.setenv) depuis n'importe quelle plateforme, contrairement
|
|
||||||
# à winsound/ctypes.windll/msvcrt.
|
|
||||||
return os.environ.get("LOCALAPPDATA", os.path.expanduser("~"))
|
|
||||||
|
|
||||||
# --- verrou d'instance --------------------------------------------------
|
|
||||||
def acquerir_verrou_instance(self, chemin):
|
|
||||||
parent = os.path.dirname(chemin)
|
|
||||||
if parent:
|
|
||||||
os.makedirs(parent, exist_ok=True)
|
|
||||||
fichier = open(chemin, "a+")
|
|
||||||
fichier.seek(0)
|
|
||||||
# msvcrt.locking verrouille un OCTET à partir de la position courante ;
|
|
||||||
# il exige que cet octet existe déjà dans le fichier, d'où cette garde.
|
|
||||||
if os.fstat(fichier.fileno()).st_size == 0:
|
|
||||||
fichier.write("0")
|
|
||||||
fichier.flush()
|
|
||||||
fichier.seek(0)
|
|
||||||
try:
|
|
||||||
(self._verrouiller or self._verrouiller_reel)(fichier)
|
|
||||||
except OSError:
|
|
||||||
fichier.close()
|
|
||||||
return None
|
|
||||||
return fichier
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _verrouiller_reel(fichier):
|
|
||||||
import msvcrt
|
|
||||||
fichier.seek(0)
|
|
||||||
msvcrt.locking(fichier.fileno(), msvcrt.LK_NBLCK, 1)
|
|
||||||
@ -1,326 +0,0 @@
|
|||||||
"""`zonza --diagnostic` : enchaînement des épreuves et mise en forme.
|
|
||||||
|
|
||||||
Chaque épreuve qui toucherait normalement du matériel réel (micro, moteur de
|
|
||||||
transcription, presse-papier, verrou) est ici exercée avec une doublure
|
|
||||||
injectée — ces tests éprouvent l'ORCHESTRATION (ordre, propagation d'un échec,
|
|
||||||
non-invention d'un résultat), pas le matériel lui-même.
|
|
||||||
"""
|
|
||||||
import numpy as np
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from diagnostic import (
|
|
||||||
ResultatEpreuve,
|
|
||||||
diagnostiquer,
|
|
||||||
epreuve_micro,
|
|
||||||
epreuve_moteur,
|
|
||||||
epreuve_plateforme,
|
|
||||||
epreuve_presse_papier,
|
|
||||||
epreuve_raccourci,
|
|
||||||
epreuve_transcription,
|
|
||||||
epreuve_verrou,
|
|
||||||
formater_resultat,
|
|
||||||
mettre_en_forme,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# --- epreuve_plateforme ------------------------------------------------------
|
|
||||||
|
|
||||||
def test_epreuve_plateforme_rapporte_cuda_present():
|
|
||||||
r = epreuve_plateforme(plateforme="win32", machine="AMD64", cuda_disponible=lambda: True)
|
|
||||||
assert r.reussite is True
|
|
||||||
assert r.detail == "win32 / AMD64 / CUDA présent"
|
|
||||||
|
|
||||||
|
|
||||||
def test_epreuve_plateforme_rapporte_cuda_absent():
|
|
||||||
r = epreuve_plateforme(plateforme="darwin", machine="arm64", cuda_disponible=lambda: False)
|
|
||||||
assert "CUDA absent" in r.detail
|
|
||||||
|
|
||||||
|
|
||||||
def test_epreuve_plateforme_non_concluante_si_la_sonde_cuda_leve():
|
|
||||||
def leve():
|
|
||||||
raise RuntimeError("ctypes en échec")
|
|
||||||
r = epreuve_plateforme(plateforme="win32", machine="AMD64", cuda_disponible=leve)
|
|
||||||
assert r.reussite is None
|
|
||||||
|
|
||||||
|
|
||||||
# --- epreuve_moteur -----------------------------------------------------------
|
|
||||||
|
|
||||||
def test_epreuve_moteur_rapporte_le_nom_et_le_modele():
|
|
||||||
r, transcripteur = epreuve_moteur(
|
|
||||||
detecter_moteur=lambda: "faster-cpu",
|
|
||||||
resoudre_modele=lambda nom, forcage: "Systran/faster-whisper-small",
|
|
||||||
creer_transcripteur=lambda **kw: "un-faux-transcripteur",
|
|
||||||
)
|
|
||||||
assert r.reussite is True
|
|
||||||
assert r.detail == "faster-cpu + Systran/faster-whisper-small"
|
|
||||||
assert transcripteur == "un-faux-transcripteur"
|
|
||||||
|
|
||||||
|
|
||||||
def test_epreuve_moteur_non_concluante_si_la_detection_echoue():
|
|
||||||
def leve():
|
|
||||||
raise RuntimeError("aucun moteur compatible")
|
|
||||||
r, transcripteur = epreuve_moteur(detecter_moteur=leve)
|
|
||||||
assert r.reussite is None
|
|
||||||
assert transcripteur is None
|
|
||||||
|
|
||||||
|
|
||||||
# --- epreuve_micro -------------------------------------------------------------
|
|
||||||
|
|
||||||
class _FauxFlux:
|
|
||||||
def __init__(self, callback, n_blocs):
|
|
||||||
self._callback = callback
|
|
||||||
self._n_blocs = n_blocs
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
for _ in range(self._n_blocs):
|
|
||||||
self._callback(np.zeros(10, dtype="float32"), 10, None, None)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def test_epreuve_micro_compte_les_blocs_recus():
|
|
||||||
def ouvrir_flux(**kw):
|
|
||||||
return _FauxFlux(kw["callback"], n_blocs=7)
|
|
||||||
r = epreuve_micro(ouvrir_flux=ouvrir_flux, dormir=lambda s: None)
|
|
||||||
assert r.reussite is True
|
|
||||||
assert r.detail == "7 blocs reçus en 1 s"
|
|
||||||
|
|
||||||
|
|
||||||
def test_epreuve_micro_echoue_si_aucun_bloc_recu():
|
|
||||||
def ouvrir_flux(**kw):
|
|
||||||
return _FauxFlux(kw["callback"], n_blocs=0)
|
|
||||||
r = epreuve_micro(ouvrir_flux=ouvrir_flux, dormir=lambda s: None)
|
|
||||||
assert r.reussite is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_epreuve_micro_non_concluante_si_louverture_echoue():
|
|
||||||
def ouvrir_flux(**kw):
|
|
||||||
raise OSError("aucun périphérique")
|
|
||||||
r = epreuve_micro(ouvrir_flux=ouvrir_flux, dormir=lambda s: None)
|
|
||||||
assert r.reussite is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_epreuve_micro_utilise_bien_la_duree_demandee():
|
|
||||||
appels = []
|
|
||||||
|
|
||||||
def ouvrir_flux(**kw):
|
|
||||||
return _FauxFlux(kw["callback"], n_blocs=1)
|
|
||||||
|
|
||||||
def dormir(s):
|
|
||||||
appels.append(s)
|
|
||||||
|
|
||||||
epreuve_micro(ouvrir_flux=ouvrir_flux, duree_s=2.5, dormir=dormir)
|
|
||||||
assert appels == [2.5]
|
|
||||||
|
|
||||||
|
|
||||||
# --- epreuve_transcription -----------------------------------------------------
|
|
||||||
|
|
||||||
class _FauxTranscripteur:
|
|
||||||
def __init__(self, texte="bonjour", leve=False):
|
|
||||||
self._texte = texte
|
|
||||||
self._leve = leve
|
|
||||||
|
|
||||||
def transcrire(self, audio):
|
|
||||||
if self._leve:
|
|
||||||
raise RuntimeError("modèle indisponible")
|
|
||||||
return self._texte
|
|
||||||
|
|
||||||
|
|
||||||
def test_epreuve_transcription_rapporte_duree_et_texte():
|
|
||||||
horloges = iter([10.0, 10.8])
|
|
||||||
r = epreuve_transcription(
|
|
||||||
_FauxTranscripteur(texte="bonjour le monde"),
|
|
||||||
generer_parole=lambda: np.zeros(16000, dtype="float32"),
|
|
||||||
horloge=lambda: next(horloges),
|
|
||||||
)
|
|
||||||
assert r.reussite is True
|
|
||||||
assert "0.80 s" in r.detail
|
|
||||||
assert "bonjour le monde" in r.detail
|
|
||||||
|
|
||||||
|
|
||||||
def test_epreuve_transcription_retombe_sur_le_silence_si_la_synthese_echoue():
|
|
||||||
def leve():
|
|
||||||
raise RuntimeError("`say` absent")
|
|
||||||
r = epreuve_transcription(
|
|
||||||
_FauxTranscripteur(texte=""),
|
|
||||||
generer_parole=leve,
|
|
||||||
horloge=lambda: 0.0,
|
|
||||||
)
|
|
||||||
assert r.reussite is True
|
|
||||||
assert "silence" in r.detail
|
|
||||||
|
|
||||||
|
|
||||||
def test_epreuve_transcription_non_concluante_si_la_transcription_echoue():
|
|
||||||
r = epreuve_transcription(
|
|
||||||
_FauxTranscripteur(leve=True),
|
|
||||||
generer_parole=lambda: np.zeros(16000, dtype="float32"),
|
|
||||||
horloge=lambda: 0.0,
|
|
||||||
)
|
|
||||||
assert r.reussite is None
|
|
||||||
|
|
||||||
|
|
||||||
# --- epreuve_presse_papier -------------------------------------------------------
|
|
||||||
|
|
||||||
def test_epreuve_presse_papier_reussit_si_la_relecture_est_identique():
|
|
||||||
stockage = {}
|
|
||||||
r = epreuve_presse_papier(
|
|
||||||
copier=lambda t: stockage.__setitem__("v", t),
|
|
||||||
lire=lambda: stockage["v"],
|
|
||||||
)
|
|
||||||
assert r.reussite is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_epreuve_presse_papier_echoue_si_la_relecture_differe():
|
|
||||||
r = epreuve_presse_papier(copier=lambda t: None, lire=lambda: "autre chose")
|
|
||||||
assert r.reussite is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_epreuve_presse_papier_non_concluante_si_la_copie_leve():
|
|
||||||
def leve(t):
|
|
||||||
raise RuntimeError("presse-papier indisponible")
|
|
||||||
r = epreuve_presse_papier(copier=leve, lire=lambda: "")
|
|
||||||
assert r.reussite is None
|
|
||||||
|
|
||||||
|
|
||||||
# --- epreuve_raccourci -----------------------------------------------------------
|
|
||||||
|
|
||||||
class _FauxSysteme:
|
|
||||||
def __init__(self, raccourci="<ctrl>+<alt>+d", verrou_sequence=None):
|
|
||||||
self._raccourci = raccourci
|
|
||||||
self._verrou_sequence = iter(verrou_sequence or [])
|
|
||||||
|
|
||||||
def raccourci_defaut(self):
|
|
||||||
return self._raccourci
|
|
||||||
|
|
||||||
def acquerir_verrou_instance(self, chemin):
|
|
||||||
return next(self._verrou_sequence)
|
|
||||||
|
|
||||||
|
|
||||||
def test_epreuve_raccourci_rapporte_la_valeur_de_la_plateforme():
|
|
||||||
r = epreuve_raccourci(_FauxSysteme(raccourci="<ctrl>+<alt>+d"))
|
|
||||||
assert r.reussite is True
|
|
||||||
assert r.detail == "<ctrl>+<alt>+d"
|
|
||||||
|
|
||||||
|
|
||||||
# --- epreuve_verrou ---------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_epreuve_verrou_reussit_si_la_seconde_prise_est_refusee():
|
|
||||||
r = epreuve_verrou(_FauxSysteme(verrou_sequence=["handle", None]), chemin="/tmp/x.lock")
|
|
||||||
assert r.reussite is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_epreuve_verrou_echoue_si_la_seconde_prise_nest_pas_refusee():
|
|
||||||
r = epreuve_verrou(_FauxSysteme(verrou_sequence=["h1", "h2"]), chemin="/tmp/x.lock")
|
|
||||||
assert r.reussite is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_epreuve_verrou_non_concluante_si_le_premier_verrou_echoue_deja():
|
|
||||||
r = epreuve_verrou(_FauxSysteme(verrou_sequence=[None, None]), chemin="/tmp/x.lock")
|
|
||||||
assert r.reussite is None
|
|
||||||
|
|
||||||
|
|
||||||
# --- diagnostiquer : enchaînement complet -----------------------------------------
|
|
||||||
|
|
||||||
def test_diagnostiquer_rend_les_sept_epreuves_dans_lordre():
|
|
||||||
resultats = diagnostiquer(
|
|
||||||
systeme=_FauxSysteme(verrou_sequence=["h", None]),
|
|
||||||
plateforme="win32", machine="AMD64", cuda_disponible=lambda: False,
|
|
||||||
detecter_moteur=lambda: "faster-cpu",
|
|
||||||
resoudre_modele=lambda nom, forcage: "modele-test",
|
|
||||||
creer_transcripteur=lambda **kw: _FauxTranscripteur(texte="ok"),
|
|
||||||
ouvrir_flux=lambda **kw: _FauxFlux(kw["callback"], n_blocs=3),
|
|
||||||
dormir=lambda s: None,
|
|
||||||
generer_parole=lambda: np.zeros(16000, dtype="float32"),
|
|
||||||
horloge=lambda: 0.0,
|
|
||||||
copier=lambda t: None,
|
|
||||||
lire=lambda: "peu importe",
|
|
||||||
)
|
|
||||||
assert [r.nom for r in resultats] == [
|
|
||||||
"Plateforme", "Moteur retenu", "Micro", "Transcription",
|
|
||||||
"Presse-papier", "Raccourci", "Verrou",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_diagnostiquer_saute_honnetement_la_transcription_si_le_moteur_echoue():
|
|
||||||
def leve():
|
|
||||||
raise RuntimeError("aucun moteur")
|
|
||||||
resultats = diagnostiquer(
|
|
||||||
systeme=_FauxSysteme(verrou_sequence=["h", None]),
|
|
||||||
detecter_moteur=leve,
|
|
||||||
ouvrir_flux=lambda **kw: _FauxFlux(kw["callback"], n_blocs=1),
|
|
||||||
dormir=lambda s: None,
|
|
||||||
copier=lambda t: None,
|
|
||||||
lire=lambda: "peu importe",
|
|
||||||
)
|
|
||||||
transcription = [r for r in resultats if r.nom == "Transcription"][0]
|
|
||||||
# jamais une réussite inventée quand rien n'a pu être mené.
|
|
||||||
assert transcription.reussite is None
|
|
||||||
assert "sautée" in transcription.detail
|
|
||||||
|
|
||||||
|
|
||||||
class _SystemeTombeToujours:
|
|
||||||
"""Doublure dont chaque méthode lève — pour prouver que rien n'est jamais
|
|
||||||
inventé même quand la couche système est totalement en panne."""
|
|
||||||
|
|
||||||
def raccourci_defaut(self):
|
|
||||||
raise RuntimeError("tout casse")
|
|
||||||
|
|
||||||
def acquerir_verrou_instance(self, chemin):
|
|
||||||
raise RuntimeError("tout casse")
|
|
||||||
|
|
||||||
|
|
||||||
def test_diagnostiquer_ne_leve_jamais_meme_si_toutes_les_epreuves_echouent():
|
|
||||||
def leve(*a, **kw):
|
|
||||||
raise RuntimeError("tout casse")
|
|
||||||
resultats = diagnostiquer(
|
|
||||||
systeme=_SystemeTombeToujours(),
|
|
||||||
plateforme="win32", machine="AMD64", cuda_disponible=leve,
|
|
||||||
detecter_moteur=leve,
|
|
||||||
ouvrir_flux=leve,
|
|
||||||
dormir=lambda s: None,
|
|
||||||
copier=leve,
|
|
||||||
lire=leve,
|
|
||||||
)
|
|
||||||
assert len(resultats) == 7
|
|
||||||
assert all(r.reussite is None for r in resultats)
|
|
||||||
|
|
||||||
|
|
||||||
# --- mise en forme -----------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_formater_resultat_aligne_le_nom_et_prefixe_un_symbole():
|
|
||||||
r = ResultatEpreuve("Plateforme", True, "darwin / arm64 / CUDA absent")
|
|
||||||
ligne = formater_resultat(r)
|
|
||||||
assert ligne == "✅ Plateforme : darwin / arm64 / CUDA absent"
|
|
||||||
|
|
||||||
|
|
||||||
def test_formater_resultat_marque_lechec():
|
|
||||||
r = ResultatEpreuve("Micro", False, "0 blocs reçus en 1 s")
|
|
||||||
assert formater_resultat(r).startswith("❌ ")
|
|
||||||
|
|
||||||
|
|
||||||
def test_formater_resultat_marque_le_non_concluant():
|
|
||||||
r = ResultatEpreuve("Transcription", None, "sautée")
|
|
||||||
assert formater_resultat(r).startswith("➖ ")
|
|
||||||
|
|
||||||
|
|
||||||
def test_mettre_en_forme_ne_prend_aucun_travail_a_sa_charge():
|
|
||||||
# fabriqué à la main : aucune épreuve n'est exécutée ici.
|
|
||||||
resultats = [
|
|
||||||
ResultatEpreuve("Plateforme", True, "darwin / arm64 / CUDA absent"),
|
|
||||||
ResultatEpreuve("Moteur retenu", True, "mlx + mlx-community/whisper-medium"),
|
|
||||||
ResultatEpreuve("Micro", False, "0 blocs reçus en 1 s"),
|
|
||||||
ResultatEpreuve("Transcription", None, "sautée"),
|
|
||||||
]
|
|
||||||
rapport = mettre_en_forme(resultats)
|
|
||||||
for r in resultats:
|
|
||||||
assert formater_resultat(r) in rapport
|
|
||||||
assert "1 en échec" in rapport
|
|
||||||
assert "1 non concluante" in rapport
|
|
||||||
|
|
||||||
|
|
||||||
def test_mettre_en_forme_sur_zero_resultat_ne_leve_pas():
|
|
||||||
assert "0/0" in mettre_en_forme([])
|
|
||||||
@ -1,154 +0,0 @@
|
|||||||
"""Couche Windows du contrat Systeme — éprouvée depuis macOS par injection.
|
|
||||||
|
|
||||||
`winsound`, `msvcrt` et `ctypes.windll` n'existent pas sur macOS : c'est
|
|
||||||
pourquoi `SystemeWindows` les reçoit en paramètres injectables (jouer, clavier,
|
|
||||||
verrouiller) plutôt que de les importer en tête de module. Ces tests éprouvent
|
|
||||||
donc la LOGIQUE française de la classe — quel alias pour quel son, quel bit
|
|
||||||
pour quelle touche, comment les chemins et le verrou sont construits — jamais
|
|
||||||
le comportement réel de winsound.PlaySound, GetAsyncKeyState ou msvcrt.locking :
|
|
||||||
aucune machine ici ne peut le vérifier. Seule une vraie machine Windows le peut.
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from systeme.windows import SystemeWindows
|
|
||||||
|
|
||||||
|
|
||||||
# --- jouer_son ---------------------------------------------------------
|
|
||||||
|
|
||||||
def test_jouer_son_start_produit_lalias_systemasterisk():
|
|
||||||
appels = []
|
|
||||||
s = SystemeWindows(jouer=appels.append)
|
|
||||||
s.jouer_son("start")
|
|
||||||
assert appels == ["SystemAsterisk"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_jouer_son_done_produit_lalias_systemexclamation():
|
|
||||||
appels = []
|
|
||||||
s = SystemeWindows(jouer=appels.append)
|
|
||||||
s.jouer_son("done")
|
|
||||||
assert appels == ["SystemExclamation"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_jouer_son_error_produit_lalias_systemhand():
|
|
||||||
appels = []
|
|
||||||
s = SystemeWindows(jouer=appels.append)
|
|
||||||
s.jouer_son("error")
|
|
||||||
assert appels == ["SystemHand"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_jouer_son_nom_inconnu_ne_produit_aucun_appel():
|
|
||||||
appels = []
|
|
||||||
s = SystemeWindows(jouer=appels.append)
|
|
||||||
s.jouer_son("inconnu")
|
|
||||||
assert appels == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_jouer_son_qui_echoue_ne_leve_pas():
|
|
||||||
def echoue(alias):
|
|
||||||
raise OSError("winsound indisponible")
|
|
||||||
s = SystemeWindows(jouer=echoue)
|
|
||||||
s.jouer_son("start") # ne doit jamais lever : le son ne casse pas la dictée
|
|
||||||
|
|
||||||
|
|
||||||
# --- modificateurs_enfonces --------------------------------------------
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("vk", [0x11, 0x10, 0x12, 0x5B, 0x5C])
|
|
||||||
def test_modificateurs_enfonces_vrai_pour_chacune_des_cinq_touches(vk):
|
|
||||||
# bit de poids fort posé UNIQUEMENT pour la touche ciblée par ce cas.
|
|
||||||
s = SystemeWindows(clavier=lambda sonde, cible=vk: 0x8000 if sonde == cible else 0)
|
|
||||||
assert s.modificateurs_enfonces() is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_modificateurs_enfonces_faux_si_aucun_bit_de_poids_fort_pose():
|
|
||||||
# une valeur non nulle mais sans le bit 0x8000 ne doit pas compter.
|
|
||||||
s = SystemeWindows(clavier=lambda vk: 0x0001)
|
|
||||||
assert s.modificateurs_enfonces() is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_modificateurs_enfonces_faux_si_toutes_les_touches_sont_relachees():
|
|
||||||
s = SystemeWindows(clavier=lambda vk: 0)
|
|
||||||
assert s.modificateurs_enfonces() is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_modificateurs_enfonces_faux_si_la_sonde_leve():
|
|
||||||
def leve(vk):
|
|
||||||
raise OSError("user32 indisponible")
|
|
||||||
s = SystemeWindows(clavier=leve)
|
|
||||||
assert s.modificateurs_enfonces() is False
|
|
||||||
|
|
||||||
|
|
||||||
# --- raccourci_defaut ----------------------------------------------------
|
|
||||||
|
|
||||||
def test_raccourci_defaut_est_ctrl_alt_d():
|
|
||||||
# <cmd> n'existe pas sur Windows, contrairement à macOS.
|
|
||||||
assert SystemeWindows().raccourci_defaut() == "<ctrl>+<alt>+d"
|
|
||||||
|
|
||||||
|
|
||||||
# --- chemins ---------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_chemin_log_derive_de_localappdata(monkeypatch, tmp_path):
|
|
||||||
monkeypatch.setenv("LOCALAPPDATA", str(tmp_path))
|
|
||||||
assert SystemeWindows().chemin_log() == os.path.join(str(tmp_path), "Zonza", "Zonza.log")
|
|
||||||
|
|
||||||
|
|
||||||
def test_dossier_donnees_derive_de_localappdata(monkeypatch, tmp_path):
|
|
||||||
monkeypatch.setenv("LOCALAPPDATA", str(tmp_path))
|
|
||||||
assert SystemeWindows().dossier_donnees() == os.path.join(str(tmp_path), "Zonza")
|
|
||||||
|
|
||||||
|
|
||||||
def test_chemin_log_retombe_sur_le_dossier_utilisateur_si_localappdata_absent(monkeypatch):
|
|
||||||
monkeypatch.delenv("LOCALAPPDATA", raising=False)
|
|
||||||
attendu = os.path.join(os.path.expanduser("~"), "Zonza", "Zonza.log")
|
|
||||||
assert SystemeWindows().chemin_log() == attendu
|
|
||||||
|
|
||||||
|
|
||||||
# --- verrou d'instance -------------------------------------------------------
|
|
||||||
|
|
||||||
def test_le_verrou_rend_un_objet_la_premiere_fois(tmp_path):
|
|
||||||
chemin = str(tmp_path / "sous-dossier" / "z.lock")
|
|
||||||
s = SystemeWindows(verrouiller=lambda f: None)
|
|
||||||
verrou = s.acquerir_verrou_instance(chemin)
|
|
||||||
assert verrou is not None
|
|
||||||
verrou.close()
|
|
||||||
|
|
||||||
|
|
||||||
def test_le_verrou_cree_le_dossier_parent_manquant(tmp_path):
|
|
||||||
chemin = str(tmp_path / "sous-dossier" / "z.lock")
|
|
||||||
s = SystemeWindows(verrouiller=lambda f: None)
|
|
||||||
assert not os.path.isdir(os.path.dirname(chemin))
|
|
||||||
verrou = s.acquerir_verrou_instance(chemin)
|
|
||||||
assert os.path.isdir(os.path.dirname(chemin))
|
|
||||||
verrou.close()
|
|
||||||
|
|
||||||
|
|
||||||
def test_le_verrou_refuse_une_seconde_prise(tmp_path):
|
|
||||||
chemin = str(tmp_path / "z.lock")
|
|
||||||
pris = {"valeur": False}
|
|
||||||
|
|
||||||
def verrouiller(f):
|
|
||||||
if pris["valeur"]:
|
|
||||||
raise OSError("verrou déjà pris")
|
|
||||||
pris["valeur"] = True
|
|
||||||
|
|
||||||
s = SystemeWindows(verrouiller=verrouiller)
|
|
||||||
premier = s.acquerir_verrou_instance(chemin)
|
|
||||||
assert premier is not None
|
|
||||||
second = s.acquerir_verrou_instance(chemin)
|
|
||||||
assert second is None
|
|
||||||
premier.close()
|
|
||||||
|
|
||||||
|
|
||||||
def test_un_verrou_dont_la_prise_echoue_referme_le_fichier(tmp_path):
|
|
||||||
chemin = str(tmp_path / "z.lock")
|
|
||||||
captures = {}
|
|
||||||
|
|
||||||
def echoue(f):
|
|
||||||
captures["fichier"] = f
|
|
||||||
raise OSError("verrou pris")
|
|
||||||
|
|
||||||
s = SystemeWindows(verrouiller=echoue)
|
|
||||||
resultat = s.acquerir_verrou_instance(chemin)
|
|
||||||
assert resultat is None
|
|
||||||
assert captures["fichier"].closed
|
|
||||||
Loading…
Reference in New Issue
Block a user