diff --git a/moteur/__init__.py b/moteur/__init__.py index 888854b..5323fac 100644 --- a/moteur/__init__.py +++ b/moteur/__init__.py @@ -1,4 +1,5 @@ """Choisit le moteur de transcription d'après le matériel.""" +import ctypes import platform import sys @@ -16,13 +17,41 @@ def detecter_moteur(): return nom_moteur_pour(sys.platform, platform.machine(), _cuda_disponible()) -def _cuda_disponible(): - try: - import ctypes - ctypes.CDLL("libcudart.so") - return True - except Exception: - return False +# Le nom de la bibliothèque partagée CUDA diffère radicalement par plateforme ET +# par version : `libcudart.so` (parfois suffixé `.12`, `.11.0`...) sous Linux/macOS, +# mais `cudart64_12.dll`, `cudart64_110.dll`... sous Windows, où le numéro de version +# est ENCODÉ DANS LE NOM du fichier. `ctypes.util.find_library` ne résout pas ces DLL +# versionnées de façon fiable — il cherche un nom canonique qui n'existe pas pour +# CUDA sous Windows. On essaie donc une liste explicite de noms candidats, du plus +# récent au plus ancien, et on s'arrête au premier qui charge. +_NOMS_CUDART_WINDOWS = [ + "cudart64_12.dll", + "cudart64_110.dll", + "cudart64_101.dll", + "cudart64_100.dll", +] +_NOMS_CUDART_UNIX = [ + "libcudart.so", + "libcudart.so.12", + "libcudart.so.11.0", +] + + +def _cuda_disponible(plateforme=None, charger=None): + """Sonde la présence du runtime CUDA en tentant de charger sa bibliothèque + partagée. `plateforme` et `charger` sont injectables : ça rend la sonde + testable avec une doublure, sans dépendre d'une vraie machine CUDA ni d'un + vrai Windows. Ne lève jamais — dans le doute, on rend False.""" + plateforme = plateforme if plateforme is not None else sys.platform + charger = charger if charger is not None else ctypes.CDLL + noms = _NOMS_CUDART_WINDOWS if plateforme.startswith("win") else _NOMS_CUDART_UNIX + for nom in noms: + try: + charger(nom) + return True + except Exception: + continue + return False def choisir_transcripteur(nom, modele, langue, amorce): diff --git a/tests/test_moteur.py b/tests/test_moteur.py index a6a4b9f..5cda145 100644 --- a/tests/test_moteur.py +++ b/tests/test_moteur.py @@ -29,3 +29,38 @@ def test_un_moteur_inconnu_est_refuse_clairement(): with pytest.raises(NotImplementedError) as e: choisir_transcripteur(nom="vosk", modele="x", langue="fr", amorce="") assert "vosk" in str(e.value) + + +# --- Sonde CUDA : le nom de la bibliothèque partagée diffère par OS et par version. +# Ces tests injectent une doublure de chargeur pour rester exécutables depuis +# n'importe quelle machine, sans jamais dépendre d'une vraie installation CUDA. +from moteur import _cuda_disponible + + +def test_cuda_reconnue_sur_windows_via_dll_versionnee(): + def charger(nom): + if nom == "cudart64_12.dll": + return object() + raise OSError(f"introuvable : {nom}") + assert _cuda_disponible(plateforme="win32", charger=charger) is True + + +def test_cuda_absente_sur_windows_si_aucune_dll_ne_charge(): + def charger(nom): + raise OSError(f"introuvable : {nom}") + assert _cuda_disponible(plateforme="win32", charger=charger) is False + + +def test_cuda_reconnue_sur_linux_darwin_via_so(): + def charger(nom): + if nom == "libcudart.so": + return object() + raise OSError(f"introuvable : {nom}") + assert _cuda_disponible(plateforme="linux", charger=charger) is True + assert _cuda_disponible(plateforme="darwin", charger=charger) is True + + +def test_cuda_sonde_ne_leve_jamais_meme_si_le_chargeur_explose(): + def charger(nom): + raise RuntimeError("chargeur casse, pas seulement OSError") + assert _cuda_disponible(plateforme="win32", charger=charger) is False