Files
voculinux/tests/test_whispercpp_model_info_ext.py
T
xavierk 180cd4d512
CI / security (push) Successful in 1m14s
CI / lint (push) Successful in 24s
CI / ai-review (push) Skipped
fix(spec16): close the review findings for the Void port (#34)
Model recommendation keyed to the installed build and free memory, fork-sourced Void templates, signed xbps release workflow on Gitea with the pinned repository key, niri injection guard and wtype-first order, tightened naming sweep, web copy fixes, and CI security and duplication fixes.
2026-11-06 14:13:49 +00:00

1204 lines
48 KiB
Python

"""
Comprehensive coverage tests for whispercpp_model_info module.
Tests all functions and constants in the module.
"""
import subprocess
import sys
import unittest
from types import SimpleNamespace
from typing import Any, Optional
from unittest.mock import MagicMock, patch
from vocalinux.utils import whispercpp_model_info as _whispercpp_model_info
_REAL_GGML_DEVICE_PROBE = _whispercpp_model_info.detect_ggml_gpu_devices
def _fake_ggml_library(registry: list[tuple[str, str, int]]) -> MagicMock:
library = MagicMock()
library.ggml_backend_dev_count.return_value = len(registry)
def _get_device(index: int) -> int:
return index + 1
def _get_device_type(pointer: int) -> int:
return registry[pointer - 1][2]
def _get_device_name(pointer: int) -> bytes:
return registry[pointer - 1][0].encode("utf-8")
def _get_device_description(pointer: int) -> bytes:
return registry[pointer - 1][1].encode("utf-8")
library.ggml_backend_dev_get.side_effect = _get_device
library.ggml_backend_dev_type.side_effect = _get_device_type
library.ggml_backend_dev_name.side_effect = _get_device_name
library.ggml_backend_dev_description.side_effect = _get_device_description
return library
def _run_ggml_probe(
registry: list[tuple[str, str, int]],
version_string: str = "1.5.0",
library: Optional[Any] = None,
library_error: Optional[Exception] = None,
missing_extension: bool = False,
) -> Optional[list[dict[str, Any]]]:
_REAL_GGML_DEVICE_PROBE.cache_clear()
native_library = library if library is not None else _fake_ggml_library(registry)
extension = None if missing_extension else SimpleNamespace(__file__="/fake/_pywhispercpp.so")
from vocalinux.utils import pywhispercpp_loader
with (
patch.object(_whispercpp_model_info, "version", return_value=version_string),
patch.object(pywhispercpp_loader, "preload_shared_libraries"),
patch.dict(sys.modules, {"_pywhispercpp": extension}),
):
if library_error is not None:
with patch.object(_whispercpp_model_info.ctypes, "CDLL", side_effect=library_error):
return _REAL_GGML_DEVICE_PROBE()
with patch.object(_whispercpp_model_info.ctypes, "CDLL", return_value=native_library):
return _REAL_GGML_DEVICE_PROBE()
class TestComputeBackendClass(unittest.TestCase):
"""Test cases for ComputeBackend class."""
def test_compute_backend_has_vulkan_constant(self):
"""Test that ComputeBackend has VULKAN constant."""
from vocalinux.utils.whispercpp_model_info import ComputeBackend
self.assertTrue(hasattr(ComputeBackend, "VULKAN"))
self.assertEqual(ComputeBackend.VULKAN, "vulkan")
def test_compute_backend_has_cuda_constant(self):
"""Test that ComputeBackend has CUDA constant."""
from vocalinux.utils.whispercpp_model_info import ComputeBackend
self.assertTrue(hasattr(ComputeBackend, "CUDA"))
self.assertEqual(ComputeBackend.CUDA, "cuda")
def test_compute_backend_has_cpu_constant(self):
"""Test that ComputeBackend has CPU constant."""
from vocalinux.utils.whispercpp_model_info import ComputeBackend
self.assertTrue(hasattr(ComputeBackend, "CPU"))
self.assertEqual(ComputeBackend.CPU, "cpu")
class TestModelInfo(unittest.TestCase):
"""Test cases for model information constants."""
def test_whispercpp_model_info_exists(self):
"""Test that WHISPERCPP_MODEL_INFO is defined."""
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
self.assertIsInstance(WHISPERCPP_MODEL_INFO, dict)
self.assertGreater(len(WHISPERCPP_MODEL_INFO), 0)
def test_whispercpp_model_info_contains_tiny(self):
"""Test that WHISPERCPP_MODEL_INFO contains tiny model."""
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
self.assertIn("tiny", WHISPERCPP_MODEL_INFO)
def test_whispercpp_model_info_contains_base(self):
"""Test that WHISPERCPP_MODEL_INFO contains base model."""
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
self.assertIn("base", WHISPERCPP_MODEL_INFO)
def test_whispercpp_model_info_contains_small(self):
"""Test that WHISPERCPP_MODEL_INFO contains small model."""
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
self.assertIn("small", WHISPERCPP_MODEL_INFO)
def test_whispercpp_model_info_contains_medium(self):
"""Test that WHISPERCPP_MODEL_INFO contains medium model."""
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
self.assertIn("medium", WHISPERCPP_MODEL_INFO)
def test_whispercpp_model_info_contains_large(self):
"""Test that WHISPERCPP_MODEL_INFO contains large model."""
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
self.assertIn("large", WHISPERCPP_MODEL_INFO)
def test_whispercpp_model_info_contains_large_v3_turbo(self):
"""Test that WHISPERCPP_MODEL_INFO contains large-v3-turbo model."""
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
self.assertIn("large-v3-turbo", WHISPERCPP_MODEL_INFO)
def test_model_info_has_size_mb(self):
"""Test that each model has size_mb field."""
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
for model_name, model_info in WHISPERCPP_MODEL_INFO.items():
self.assertIn("size_mb", model_info, f"{model_name} missing size_mb")
self.assertIsInstance(model_info["size_mb"], int)
self.assertGreater(model_info["size_mb"], 0)
def test_model_info_has_params(self):
"""Test that each model has params field."""
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
for model_name, model_info in WHISPERCPP_MODEL_INFO.items():
self.assertIn("params", model_info, f"{model_name} missing params")
self.assertIsInstance(model_info["params"], str)
def test_model_info_has_desc(self):
"""Test that each model has desc field."""
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
for model_name, model_info in WHISPERCPP_MODEL_INFO.items():
self.assertIn("desc", model_info, f"{model_name} missing desc")
self.assertIsInstance(model_info["desc"], str)
def test_model_info_has_url(self):
"""Test that each model has url field."""
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
for model_name, model_info in WHISPERCPP_MODEL_INFO.items():
self.assertIn("url", model_info, f"{model_name} missing url")
self.assertIsInstance(model_info["url"], str)
self.assertTrue(model_info["url"].startswith("https://"))
def test_available_models_list_exists(self):
"""Test that AVAILABLE_MODELS list exists."""
from vocalinux.utils.whispercpp_model_info import AVAILABLE_MODELS
self.assertIsInstance(AVAILABLE_MODELS, list)
self.assertGreater(len(AVAILABLE_MODELS), 0)
def test_available_models_contains_all_models(self):
"""Test that AVAILABLE_MODELS contains all defined models."""
from vocalinux.utils.whispercpp_model_info import AVAILABLE_MODELS, WHISPERCPP_MODEL_INFO
self.assertEqual(set(AVAILABLE_MODELS), set(WHISPERCPP_MODEL_INFO.keys()))
def test_available_models_tracks_upstream_dictation_models(self):
"""Test that AVAILABLE_MODELS covers upstream dictation models."""
from vocalinux.utils.whispercpp_model_info import AVAILABLE_MODELS
expected_models = [
"tiny",
"tiny.en",
"tiny-q5_1",
"tiny.en-q5_1",
"tiny-q8_0",
"base",
"base.en",
"base-q5_1",
"base.en-q5_1",
"base-q8_0",
"small",
"small.en",
"small-q5_1",
"small.en-q5_1",
"small-q8_0",
"medium",
"medium.en",
"medium-q5_0",
"medium.en-q5_0",
"medium-q8_0",
"large-v1",
"large-v2",
"large-v2-q5_0",
"large-v2-q8_0",
"large", # Voculinux alias for upstream large-v3
"large-v3-q5_0",
"large-v3-turbo",
"large-v3-turbo-q5_0",
"large-v3-turbo-q8_0",
"small.en-tdrz",
]
self.assertEqual(AVAILABLE_MODELS, expected_models)
def test_model_sizes_group_whispercpp_variants_for_ui(self):
"""Test that whisper.cpp models are grouped into size buckets."""
from vocalinux.utils.whispercpp_model_info import (
MODEL_SIZES,
MODEL_VARIANTS_BY_SIZE,
NON_DICTATION_MODELS,
WHISPERCPP_MODEL_INFO,
)
self.assertEqual(MODEL_SIZES, ["tiny", "base", "small", "medium", "large"])
self.assertEqual(MODEL_VARIANTS_BY_SIZE["medium"][0], "medium")
self.assertIn("medium.en-q5_0", MODEL_VARIANTS_BY_SIZE["medium"])
self.assertIn("large-v3-turbo-q8_0", MODEL_VARIANTS_BY_SIZE["large"])
self.assertNotIn("small.en-tdrz", MODEL_VARIANTS_BY_SIZE["small"])
grouped_models = {
model_name for variants in MODEL_VARIANTS_BY_SIZE.values() for model_name in variants
}
self.assertEqual(grouped_models, set(WHISPERCPP_MODEL_INFO.keys()) - NON_DICTATION_MODELS)
self.assertFalse(
any(model_name.endswith("-tdrz") for model_name in grouped_models),
"TinyDiarize needs diarization-specific runtime support, not the base dictation picker",
)
def test_tdrz_model_is_catalogued_but_not_a_dictation_model(self):
"""TinyDiarize is fetchable through the catalog, never dictation-selectable."""
from vocalinux.utils.whispercpp_model_info import (
NON_DICTATION_MODELS,
TDRZ_MODEL,
WHISPERCPP_MODEL_INFO,
is_dictation_model,
whispercpp_model_file,
whispercpp_model_source,
)
self.assertEqual(TDRZ_MODEL, "small.en-tdrz")
self.assertIn(TDRZ_MODEL, WHISPERCPP_MODEL_INFO)
self.assertIn(TDRZ_MODEL, NON_DICTATION_MODELS)
self.assertFalse(is_dictation_model(TDRZ_MODEL))
self.assertTrue(is_dictation_model("small.en"))
self.assertFalse(is_dictation_model("not-a-model"))
self.assertEqual(whispercpp_model_file(TDRZ_MODEL), "ggml-small.en-tdrz.bin")
url = WHISPERCPP_MODEL_INFO[TDRZ_MODEL]["url"]
self.assertIn("akashmjn/tinydiarize-whisper.cpp", url)
repo, revision = whispercpp_model_source(TDRZ_MODEL)
self.assertEqual(repo, "akashmjn/tinydiarize-whisper.cpp")
self.assertTrue(revision)
self.assertIn(f"/resolve/{revision}/", url)
main_repo, main_revision = whispercpp_model_source("small.en")
self.assertEqual(main_repo, "ggerganov/whisper.cpp")
self.assertEqual(main_revision, "")
def test_model_variant_helpers_identify_size_and_english_only_variants(self):
"""Test helpers that drive the split whisper.cpp model selectors."""
from vocalinux.utils.whispercpp_model_info import (
get_model_size,
get_model_variants,
is_english_only_model,
)
self.assertEqual(get_model_size("medium.en-q5_0"), "medium")
self.assertEqual(get_model_size("large-v3-turbo-q8_0"), "large")
self.assertTrue(is_english_only_model("tiny.en-q5_1"))
self.assertFalse(is_english_only_model("large-v3-turbo"))
class TestDetectVulkanDevices(unittest.TestCase):
"""Test cases for detect_vulkan_devices function."""
def setUp(self):
"""Clear lru_cache before each test."""
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
detect_vulkan_devices.cache_clear()
def test_detect_vulkan_devices_empty_when_unavailable(self):
"""Test that detect_vulkan_devices returns empty list when vulkaninfo unavailable."""
with patch("subprocess.run") as mock_run:
mock_run.side_effect = FileNotFoundError("vulkaninfo not found")
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
devices = detect_vulkan_devices()
self.assertEqual(devices, [])
def test_detect_vulkan_devices_single_discrete(self):
"""Test detection of a single discrete GPU from real vulkaninfo --summary."""
vulkaninfo_output = (
"Devices:\n"
"========\n"
"GPU0:\n"
"\tapiVersion = 1.3.296\n"
"\tdriverVersion = 550.54.14\n"
"\tvendorID = 0x10de\n"
"\tdeviceID = 0x2206\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
"\tdeviceName = NVIDIA RTX 3080\n"
)
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
devices = detect_vulkan_devices()
self.assertEqual(len(devices), 1)
self.assertEqual(devices[0]["index"], 0)
self.assertEqual(devices[0]["name"], "NVIDIA RTX 3080")
self.assertEqual(devices[0]["device_type"], "discrete")
def test_detect_vulkan_devices_hybrid_system(self):
"""Test detection on hybrid system with real vulkaninfo --summary output."""
vulkaninfo_output = (
"Devices:\n"
"========\n"
"GPU0:\n"
"\tapiVersion = 1.3.296\n"
"\tdriverVersion = 24.2.8\n"
"\tvendorID = 0x8086\n"
"\tdeviceID = 0x3e92\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU\n"
"\tdeviceName = Intel(R) UHD Graphics 630 (CFL GT2)\n"
"GPU1:\n"
"\tapiVersion = 1.3.242\n"
"\tdriverVersion = 550.54.14\n"
"\tvendorID = 0x10de\n"
"\tdeviceID = 0x1f08\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
"\tdeviceName = NVIDIA GeForce RTX 2060\n"
)
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
devices = detect_vulkan_devices()
self.assertEqual(len(devices), 2)
self.assertEqual(devices[0]["index"], 0)
self.assertEqual(devices[0]["name"], "Intel(R) UHD Graphics 630 (CFL GT2)")
self.assertEqual(devices[0]["device_type"], "integrated")
self.assertEqual(devices[1]["index"], 1)
self.assertEqual(devices[1]["name"], "NVIDIA GeForce RTX 2060")
self.assertEqual(devices[1]["device_type"], "discrete")
def test_detect_vulkan_devices_legacy_gpu_id_format(self):
"""Test compatibility with alternate 'GPU id = N' vulkaninfo headers."""
vulkaninfo_output = (
"GPU id = 0\n"
"deviceName = Intel UHD 630\n"
"deviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU\n"
"GPU id = 1\n"
"deviceName = NVIDIA RTX 2060\n"
"deviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
)
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
devices = detect_vulkan_devices()
self.assertEqual(len(devices), 2)
self.assertEqual(devices[0]["index"], 0)
self.assertEqual(devices[1]["index"], 1)
self.assertEqual(devices[1]["device_type"], "discrete")
def test_detect_vulkan_devices_on_timeout(self):
"""Test that timeout returns empty list."""
with patch("subprocess.run") as mock_run:
mock_run.side_effect = subprocess.TimeoutExpired("vulkaninfo", 5)
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
devices = detect_vulkan_devices()
self.assertEqual(devices, [])
def test_detect_vulkan_devices_on_error(self):
"""Test that error returncode returns empty list."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=1, stdout="")
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
devices = detect_vulkan_devices()
self.assertEqual(devices, [])
def test_detect_vulkan_devices_falls_back_without_summary(self):
"""Older vulkaninfo without --summary still enumerates GPUs."""
vulkaninfo_output = (
"GPU0:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
"\tdeviceName = NVIDIA RTX 2060\n"
)
def run_side_effect(cmd, **kwargs):
if cmd == ["vulkaninfo", "--summary"]:
return MagicMock(returncode=1, stdout="")
if cmd == ["vulkaninfo"]:
return MagicMock(returncode=0, stdout=vulkaninfo_output)
return MagicMock(returncode=1, stdout="")
with patch("subprocess.run", side_effect=run_side_effect):
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
devices = detect_vulkan_devices()
self.assertEqual(len(devices), 1)
self.assertEqual(devices[0]["name"], "NVIDIA RTX 2060")
self.assertEqual(devices[0]["device_type"], "discrete")
def test_detect_vulkan_devices_marks_llvmpipe_software(self):
"""Software renderers are classified separately from hardware GPUs."""
vulkaninfo_output = (
"GPU0:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_CPU\n"
"\tdeviceName = llvmpipe (LLVM 19.1.7, 256 bits)\n"
"GPU1:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
"\tdeviceName = NVIDIA GeForce RTX 2060\n"
)
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
devices = detect_vulkan_devices()
self.assertEqual(devices[0]["device_type"], "software")
self.assertEqual(devices[1]["device_type"], "discrete")
class TestPreferDiscreteVulkanDevice(unittest.TestCase):
"""Test cases for _prefer_discrete_vulkan_device function."""
def setUp(self):
"""Clear lru_cache before each test."""
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
detect_vulkan_devices.cache_clear()
def test_prefer_discrete_when_available(self):
"""Test that discrete GPU is preferred when available."""
vulkaninfo_output = (
"GPU0:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU\n"
"\tdeviceName = Intel UHD 630\n"
"GPU1:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
"\tdeviceName = NVIDIA RTX 2060\n"
)
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
from vocalinux.utils.whispercpp_model_info import _prefer_discrete_vulkan_device
preferred_idx = _prefer_discrete_vulkan_device()
self.assertEqual(preferred_idx, 1)
def test_prefer_discrete_falls_back_to_first(self):
"""Test that first device is used when no discrete GPU available."""
vulkaninfo_output = (
"GPU0:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU\n"
"\tdeviceName = Intel UHD 630\n"
)
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
from vocalinux.utils.whispercpp_model_info import _prefer_discrete_vulkan_device
preferred_idx = _prefer_discrete_vulkan_device()
self.assertEqual(preferred_idx, 0)
class TestDetectGgmlGpuDevices(unittest.TestCase):
"""Test ggml's native GPU registry probe."""
def test_probe_assigns_zero_to_first_discrete_gpu(
self: "TestDetectGgmlGpuDevices",
) -> None:
devices = _run_ggml_probe(
[
("Vulkan0", "NVIDIA GeForce GTX 1050", 1),
("CPU", "CPU backend", 0),
]
)
self.assertEqual(
devices,
[
{
"index": 0,
"name": "NVIDIA GeForce GTX 1050",
"backend_name": "Vulkan0",
"device_type": "discrete",
}
],
)
def test_probe_uses_gpu_and_igpu_ordinals(self: "TestDetectGgmlGpuDevices") -> None:
devices = _run_ggml_probe(
[
("Vulkan0", "Intel Arc", 2),
("CPU", "CPU backend", 0),
("Vulkan1", "NVIDIA GeForce GTX 1050", 1),
("ACCEL", "Accelerator", 3),
]
)
self.assertEqual(
devices,
[
{
"index": 0,
"name": "Intel Arc",
"backend_name": "Vulkan0",
"device_type": "integrated",
},
{
"index": 1,
"name": "NVIDIA GeForce GTX 1050",
"backend_name": "Vulkan1",
"device_type": "discrete",
},
],
)
def test_probe_returns_empty_list_for_cpu_only_registry(
self: "TestDetectGgmlGpuDevices",
) -> None:
self.assertEqual(_run_ggml_probe([("CPU", "CPU backend", 0)]), [])
def test_probe_returns_none_when_runtime_is_unavailable(
self: "TestDetectGgmlGpuDevices",
) -> None:
self.assertIsNone(_run_ggml_probe([], version_string="1.4.9"))
self.assertIsNone(_run_ggml_probe([], missing_extension=True))
self.assertIsNone(_run_ggml_probe([], library_error=OSError("unavailable")))
self.assertIsNone(_run_ggml_probe([], library=SimpleNamespace()))
def test_probe_keeps_ordinal_for_device_without_description(
self: "TestDetectGgmlGpuDevices",
) -> None:
library = _fake_ggml_library(
[("Vulkan0", "", 1), ("Vulkan1", "NVIDIA GeForce GTX 1050", 1)]
)
library.ggml_backend_dev_description.side_effect = lambda pointer: (
None if pointer == 1 else b"NVIDIA GeForce GTX 1050"
)
devices = _run_ggml_probe([], library=library)
assert devices is not None
self.assertEqual([device["index"] for device in devices], [0, 1])
self.assertEqual(devices[0]["name"], "Vulkan0")
self.assertEqual(devices[1]["name"], "NVIDIA GeForce GTX 1050")
def test_probe_accepts_prerelease_version(self: "TestDetectGgmlGpuDevices") -> None:
devices = _run_ggml_probe([("Vulkan0", "GPU", 1)], version_string="1.5.1.dev3")
assert devices is not None
self.assertEqual(len(devices), 1)
class TestPreferDiscreteVulkanDeviceMore(unittest.TestCase):
"""Exercise additional Vulkan selection edge cases."""
def setUp(self: "TestPreferDiscreteVulkanDeviceMore") -> None:
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
detect_vulkan_devices.cache_clear()
def test_prefer_discrete_returns_none_when_empty(self):
"""Test that None is returned when no devices found."""
with patch("subprocess.run") as mock_run:
mock_run.side_effect = FileNotFoundError("vulkaninfo not found")
from vocalinux.utils.whispercpp_model_info import _prefer_discrete_vulkan_device
preferred_idx = _prefer_discrete_vulkan_device()
self.assertIsNone(preferred_idx)
def test_prefer_discrete_skips_software_renderer(self):
"""Auto-select must not pick llvmpipe when a real GPU exists."""
vulkaninfo_output = (
"GPU0:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_CPU\n"
"\tdeviceName = llvmpipe (LLVM 19.1.7, 256 bits)\n"
"GPU1:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
"\tdeviceName = NVIDIA RTX 2060\n"
)
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
from vocalinux.utils.whispercpp_model_info import _prefer_discrete_vulkan_device
preferred_idx = _prefer_discrete_vulkan_device()
self.assertEqual(preferred_idx, 1)
def test_prefer_discrete_ignores_software_only(self):
"""Software-only Vulkan should not produce an auto-selected device."""
vulkaninfo_output = (
"GPU0:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_CPU\n"
"\tdeviceName = llvmpipe (LLVM 19.1.7, 256 bits)\n"
)
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
from vocalinux.utils.whispercpp_model_info import (
ComputeBackend,
_prefer_discrete_vulkan_device,
detect_compute_backend,
detect_cuda_support,
detect_vulkan_support,
)
self.assertIsNone(_prefer_discrete_vulkan_device())
detect_vulkan_support.cache_clear()
is_available, device_name = detect_vulkan_support()
self.assertFalse(is_available)
self.assertIsNone(device_name)
detect_cuda_support.cache_clear()
detect_compute_backend.cache_clear()
backend, _info = detect_compute_backend()
self.assertNotEqual(backend, ComputeBackend.VULKAN)
def test_prefer_discrete_with_noncontiguous_gpu_ids(self):
"""Preferred index is the Vulkan GPU id, not the list position."""
vulkaninfo_output = (
"GPU0:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU\n"
"\tdeviceName = Intel UHD 630\n"
"GPU2:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
"\tdeviceName = NVIDIA RTX 2060\n"
)
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
from vocalinux.utils.whispercpp_model_info import (
_prefer_discrete_vulkan_device,
detect_vulkan_support,
)
preferred_idx = _prefer_discrete_vulkan_device()
self.assertEqual(preferred_idx, 2)
detect_vulkan_support.cache_clear()
is_available, device_name = detect_vulkan_support()
self.assertTrue(is_available)
self.assertEqual(device_name, "NVIDIA RTX 2060")
class TestDetectVulkanSupport(unittest.TestCase):
"""Test cases for detect_vulkan_support function."""
def setUp(self):
"""Clear lru_cache before each test."""
from vocalinux.utils.whispercpp_model_info import (
detect_compute_backend,
detect_cuda_support,
detect_vulkan_devices,
detect_vulkan_support,
)
detect_vulkan_support.cache_clear()
detect_vulkan_devices.cache_clear()
detect_cuda_support.cache_clear()
detect_compute_backend.cache_clear()
def test_detect_vulkan_support_returns_tuple(self):
"""Test that detect_vulkan_support returns a tuple."""
from vocalinux.utils.whispercpp_model_info import detect_vulkan_support
result = detect_vulkan_support()
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 2)
def test_detect_vulkan_support_when_available(self):
"""Test Vulkan detection when vulkaninfo is available."""
vulkaninfo_output = (
"GPU0:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
"\tdeviceName = Intel Arc\n"
)
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
from vocalinux.utils.whispercpp_model_info import detect_vulkan_support
is_available, device_name = detect_vulkan_support()
self.assertTrue(is_available)
self.assertEqual(device_name, "Intel Arc")
def test_detect_vulkan_support_prefers_discrete_on_hybrid(self):
"""Hybrid systems should report the discrete GPU name."""
vulkaninfo_output = (
"GPU0:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU\n"
"\tdeviceName = Intel(R) UHD Graphics 630 (CFL GT2)\n"
"GPU1:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
"\tdeviceName = NVIDIA GeForce RTX 2060\n"
)
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
from vocalinux.utils.whispercpp_model_info import detect_vulkan_support
is_available, device_name = detect_vulkan_support()
self.assertTrue(is_available)
self.assertEqual(device_name, "NVIDIA GeForce RTX 2060")
def test_compute_backend_stays_vulkan_with_real_summary_and_nvidia(self):
"""Regression: real GPU0: summary must not fall through to CUDA."""
vulkaninfo_output = (
"Devices:\n"
"========\n"
"GPU0:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU\n"
"\tdeviceName = Intel(R) UHD Graphics 630 (CFL GT2)\n"
"GPU1:\n"
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
"\tdeviceName = NVIDIA GeForce RTX 2060\n"
)
def run_side_effect(cmd, **kwargs):
if cmd and cmd[0] == "vulkaninfo":
return MagicMock(returncode=0, stdout=vulkaninfo_output)
if cmd and cmd[0] == "nvidia-smi":
return MagicMock(returncode=0, stdout="NVIDIA GeForce RTX 2060, 6144 MiB")
return MagicMock(returncode=1, stdout="")
with patch("subprocess.run", side_effect=run_side_effect):
from vocalinux.utils.whispercpp_model_info import ComputeBackend, detect_compute_backend
backend, backend_info = detect_compute_backend()
self.assertEqual(backend, ComputeBackend.VULKAN)
self.assertEqual(backend_info, "NVIDIA GeForce RTX 2060")
def test_detect_vulkan_support_when_unavailable(self):
"""Test Vulkan detection when vulkaninfo is not available."""
with patch("subprocess.run") as mock_run:
mock_run.side_effect = FileNotFoundError("vulkaninfo not found")
from vocalinux.utils.whispercpp_model_info import detect_vulkan_support
is_available, device_name = detect_vulkan_support()
self.assertFalse(is_available)
self.assertIsNone(device_name)
def test_detect_vulkan_support_on_timeout(self):
"""Test Vulkan detection when command times out."""
with patch("subprocess.run") as mock_run:
mock_run.side_effect = subprocess.TimeoutExpired("vulkaninfo", 5)
from vocalinux.utils.whispercpp_model_info import detect_vulkan_support
is_available, device_name = detect_vulkan_support()
self.assertFalse(is_available)
self.assertIsNone(device_name)
def test_detect_vulkan_support_on_error(self):
"""Test Vulkan detection when command returns error."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=1, stdout="")
from vocalinux.utils.whispercpp_model_info import detect_vulkan_support
is_available, device_name = detect_vulkan_support()
self.assertFalse(is_available)
self.assertIsNone(device_name)
class TestDetectCudaSupport(unittest.TestCase):
"""Test cases for detect_cuda_support function."""
def test_detect_cuda_support_returns_tuple(self):
"""Test that detect_cuda_support returns a tuple."""
from vocalinux.utils.whispercpp_model_info import detect_cuda_support
result = detect_cuda_support()
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 2)
def test_detect_cuda_support_when_available(self):
"""Test CUDA detection when nvidia-smi is available."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout="NVIDIA RTX 3080, 10240 MiB\n")
from vocalinux.utils.whispercpp_model_info import detect_cuda_support
is_available, device_info = detect_cuda_support()
self.assertTrue(is_available)
self.assertIsNotNone(device_info)
self.assertIn("RTX 3080", device_info)
def test_detect_cuda_support_when_unavailable(self):
"""Test CUDA detection when nvidia-smi is not available."""
with patch("subprocess.run") as mock_run:
mock_run.side_effect = FileNotFoundError("nvidia-smi not found")
from vocalinux.utils.whispercpp_model_info import detect_cuda_support
is_available, device_info = detect_cuda_support()
self.assertFalse(is_available)
self.assertIsNone(device_info)
def test_detect_cuda_support_on_timeout(self):
"""Test CUDA detection when command times out."""
with patch("subprocess.run") as mock_run:
mock_run.side_effect = subprocess.TimeoutExpired("nvidia-smi", 5)
from vocalinux.utils.whispercpp_model_info import detect_cuda_support
is_available, device_info = detect_cuda_support()
self.assertFalse(is_available)
self.assertIsNone(device_info)
class TestDetectCPUInfo(unittest.TestCase):
"""Test cases for detect_cpu_info function."""
def test_detect_cpu_info_returns_string(self):
"""Test that detect_cpu_info returns a string."""
from vocalinux.utils.whispercpp_model_info import detect_cpu_info
result = detect_cpu_info()
self.assertIsInstance(result, str)
self.assertGreater(len(result), 0)
def test_detect_cpu_info_from_proc_cpuinfo(self):
"""Test CPU detection from /proc/cpuinfo."""
cpu_info_content = "model name : Intel(R) Core(TM) i7-10700K CPU @ 3.80GHz\n"
with patch("builtins.open", create=True) as mock_open:
mock_open.return_value.__enter__.return_value = iter([cpu_info_content])
from vocalinux.utils.whispercpp_model_info import detect_cpu_info
result = detect_cpu_info()
self.assertIsInstance(result, str)
self.assertGreater(len(result), 0)
def test_detect_cpu_info_fallback_to_nproc(self):
"""Test CPU detection falls back to nproc."""
with (
patch("builtins.open", create=True, side_effect=Exception("No /proc/cpuinfo")),
patch("subprocess.run") as mock_run,
):
mock_run.return_value = MagicMock(returncode=0, stdout="8\n")
from vocalinux.utils.whispercpp_model_info import detect_cpu_info
result = detect_cpu_info()
self.assertIsInstance(result, str)
def test_detect_cpu_info_default_fallback(self):
"""Test CPU detection returns default when all methods fail."""
with (
patch("builtins.open", create=True, side_effect=Exception()),
patch("subprocess.run", side_effect=Exception()),
):
from vocalinux.utils.whispercpp_model_info import detect_cpu_info
result = detect_cpu_info()
self.assertEqual(result, "CPU")
class TestDetectComputeBackend(unittest.TestCase):
"""Test cases for detect_compute_backend function."""
def test_detect_compute_backend_returns_tuple(self):
"""Test that detect_compute_backend returns a tuple."""
from vocalinux.utils.whispercpp_model_info import detect_compute_backend
result = detect_compute_backend()
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 2)
def test_detect_compute_backend_prefers_vulkan(self):
"""Test that detect_compute_backend prefers Vulkan."""
with (
patch("vocalinux.utils.whispercpp_model_info.detect_vulkan_support") as mock_vulkan,
patch("vocalinux.utils.whispercpp_model_info.detect_cuda_support") as mock_cuda,
):
mock_vulkan.return_value = (True, "Intel Arc GPU")
mock_cuda.return_value = (True, "NVIDIA RTX 3080 (10GB)")
from vocalinux.utils.whispercpp_model_info import ComputeBackend, detect_compute_backend
backend, info = detect_compute_backend()
self.assertEqual(backend, ComputeBackend.VULKAN)
def test_detect_compute_backend_falls_back_to_cuda(self):
"""Test that detect_compute_backend falls back to CUDA."""
with (
patch("vocalinux.utils.whispercpp_model_info.detect_vulkan_support") as mock_vulkan,
patch("vocalinux.utils.whispercpp_model_info.detect_cuda_support") as mock_cuda,
):
mock_vulkan.return_value = (False, None)
mock_cuda.return_value = (True, "NVIDIA RTX 3080 (10GB)")
from vocalinux.utils.whispercpp_model_info import ComputeBackend, detect_compute_backend
backend, info = detect_compute_backend()
self.assertEqual(backend, ComputeBackend.CUDA)
def test_detect_compute_backend_falls_back_to_cpu(self):
"""Test that detect_compute_backend falls back to CPU."""
with (
patch("vocalinux.utils.whispercpp_model_info.detect_vulkan_support") as mock_vulkan,
patch("vocalinux.utils.whispercpp_model_info.detect_cuda_support") as mock_cuda,
patch("vocalinux.utils.whispercpp_model_info.detect_cpu_info") as mock_cpu,
):
mock_vulkan.return_value = (False, None)
mock_cuda.return_value = (False, None)
mock_cpu.return_value = "Intel Core i7"
from vocalinux.utils.whispercpp_model_info import ComputeBackend, detect_compute_backend
backend, info = detect_compute_backend()
self.assertEqual(backend, ComputeBackend.CPU)
class TestGetRecommendedModel(unittest.TestCase):
"""The recommendation is a ladder rung keyed to the installed build and free memory.
Spec #16 / ticket #20. Every reading is mocked: the installed build's
backend, free VRAM from nvidia-smi and free system RAM. Total memory is
never consulted, so no test here passes a total as a free reading.
"""
LADDER = {"large-v3-turbo-q8_0", "large-v3-turbo-q5_0", "small"}
def _recommend(self, build_backend, free_vram=None, free_ram_mib=10 * 1024.0):
from vocalinux.speech_recognition import model_ladder, recognition_manager
from vocalinux.utils import whispercpp_model_info as info
with (
patch.object(
recognition_manager,
"detect_pywhispercpp_gpu_backend",
return_value=build_backend,
),
patch.object(model_ladder, "free_vram_mib", return_value=free_vram),
patch.object(model_ladder, "free_system_ram_mib", return_value=free_ram_mib),
):
return info.get_recommended_model()
def test_returns_tuple(self):
model, reason = self._recommend("vulkan", free_vram=2600.0)
self.assertIsInstance(model, str)
self.assertIsInstance(reason, str)
def test_ample_free_vram_on_vulkan_build_selects_default_rung(self):
model, reason = self._recommend("vulkan", free_vram=2600.0)
self.assertEqual(model, "large-v3-turbo-q8_0")
self.assertIn("VRAM", reason)
def test_constrained_free_vram_steps_to_q5_rung(self):
model, reason = self._recommend("vulkan", free_vram=1500.0)
self.assertEqual(model, "large-v3-turbo-q5_0")
self.assertIn("1500 MiB free VRAM", reason)
def test_tight_free_vram_selects_floor(self):
model, _reason = self._recommend("vulkan", free_vram=900.0)
self.assertEqual(model, "small")
def test_total_vram_is_not_the_reading(self):
"""A 4 GB card whose VRAM is mostly taken by the desktop gets the floor."""
model, _reason = self._recommend("vulkan", free_vram=512.0)
self.assertEqual(model, "small")
def test_integrated_graphics_read_free_ram(self):
"""No NVIDIA reading means the Vulkan build keeps its model in system RAM."""
model, reason = self._recommend("vulkan", free_vram=None, free_ram_mib=10 * 1024.0)
self.assertEqual(model, "large-v3-turbo-q8_0")
self.assertIn("RAM", reason)
def test_cpu_build_ignores_a_visible_gpu_and_reads_ram(self):
"""A CPU-only build never runs on the GPU, so VRAM is not the constraint."""
model, reason = self._recommend("cpu", free_vram=2600.0, free_ram_mib=1200.0)
self.assertEqual(model, "small")
self.assertIn("RAM", reason)
self.assertNotIn("VRAM", reason)
def test_keys_to_the_installed_build_not_the_hardware_probe(self):
"""The informational probe reporting CPU must not hide an installed GPU build."""
from vocalinux.speech_recognition import model_ladder, recognition_manager
from vocalinux.utils import whispercpp_model_info as info
with (
patch.object(
info, "detect_compute_backend", return_value=(info.ComputeBackend.CPU, "x")
),
patch.object(
recognition_manager,
"detect_pywhispercpp_gpu_backend",
return_value="vulkan",
),
patch.object(model_ladder, "free_vram_mib", return_value=2600.0),
patch.object(model_ladder, "free_system_ram_mib", return_value=512.0),
):
model, reason = info.get_recommended_model()
self.assertEqual(model, "large-v3-turbo-q8_0")
self.assertIn("VRAM", reason)
def test_unreadable_memory_recommends_the_floor(self):
model, reason = self._recommend("vulkan", free_vram=None, free_ram_mib=None)
self.assertEqual(model, "small")
self.assertIn("unreadable", reason)
def test_recommendation_stays_inside_the_ladder_for_any_reading(self):
"""Unquantized turbo, the large family and medium never come back as a default."""
for backend in ("vulkan", "cuda", "cpu"):
for free_vram in (None, 0.0, 900.0, 1500.0, 2600.0, 8000.0):
for free_ram in (512.0, 1200.0, 4096.0, 16384.0):
model, _reason = self._recommend(backend, free_vram, free_ram)
self.assertIn(model, self.LADDER, (backend, free_vram, free_ram))
def test_reason_names_the_backend_the_build_has(self):
_model, reason = self._recommend("cuda", free_vram=2600.0)
self.assertIn("cuda build", reason)
class TestGetModelPath(unittest.TestCase):
"""Test cases for get_model_path function."""
def test_get_model_path_returns_string(self):
"""Test that get_model_path returns a string."""
from vocalinux.utils.whispercpp_model_info import get_model_path
result = get_model_path("tiny")
self.assertIsInstance(result, str)
self.assertGreater(len(result), 0)
def test_get_model_path_tiny(self):
"""Test get_model_path for tiny model."""
from vocalinux.utils.whispercpp_model_info import get_model_path
path = get_model_path("tiny")
self.assertIn("ggml-tiny.bin", path)
def test_get_model_path_base(self):
"""Test get_model_path for base model."""
from vocalinux.utils.whispercpp_model_info import get_model_path
path = get_model_path("base")
self.assertIn("ggml-base.bin", path)
def test_get_model_path_small(self):
"""Test get_model_path for small model."""
from vocalinux.utils.whispercpp_model_info import get_model_path
path = get_model_path("small")
self.assertIn("ggml-small.bin", path)
def test_get_model_path_medium(self):
"""Test get_model_path for medium model."""
from vocalinux.utils.whispercpp_model_info import get_model_path
path = get_model_path("medium")
self.assertIn("ggml-medium.bin", path)
def test_get_model_path_large_uses_v3(self):
"""Test get_model_path for large model uses v3 variant."""
from vocalinux.utils.whispercpp_model_info import get_model_path
path = get_model_path("large")
self.assertIn("ggml-large-v3.bin", path)
def test_large_model_url_uses_v3(self):
"""Test large model metadata points to upstream large-v3."""
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
self.assertIn("ggml-large-v3.bin", WHISPERCPP_MODEL_INFO["large"]["url"])
def test_get_model_path_large_v3_turbo(self):
"""Test get_model_path for large-v3-turbo model."""
from vocalinux.utils.whispercpp_model_info import get_model_path
path = get_model_path("large-v3-turbo")
self.assertIn("ggml-large-v3-turbo.bin", path)
def test_get_model_path_quantized_model(self):
"""Test get_model_path for quantized model."""
from vocalinux.utils.whispercpp_model_info import get_model_path
path = get_model_path("large-v3-turbo-q5_0")
self.assertIn("ggml-large-v3-turbo-q5_0.bin", path)
class TestIsModelDownloaded(unittest.TestCase):
"""Test cases for is_model_downloaded function."""
def test_is_model_downloaded_returns_bool(self):
"""Test that is_model_downloaded returns a boolean."""
from vocalinux.utils.whispercpp_model_info import is_model_downloaded
result = is_model_downloaded("tiny")
self.assertIsInstance(result, bool)
def test_is_model_downloaded_when_exists(self):
"""Test is_model_downloaded when model file exists."""
with patch("os.path.exists", return_value=True):
from vocalinux.utils.whispercpp_model_info import is_model_downloaded
result = is_model_downloaded("base")
self.assertTrue(result)
def test_is_model_downloaded_when_not_exists(self):
"""Test is_model_downloaded when model file does not exist."""
with patch("os.path.exists", return_value=False):
from vocalinux.utils.whispercpp_model_info import is_model_downloaded
result = is_model_downloaded("base")
self.assertFalse(result)
class TestGetBackendDisplayName(unittest.TestCase):
"""Test cases for get_backend_display_name function."""
def test_get_backend_display_name_returns_string(self):
"""Test that get_backend_display_name returns a string."""
from vocalinux.utils.whispercpp_model_info import ComputeBackend, get_backend_display_name
result = get_backend_display_name(ComputeBackend.CPU)
self.assertIsInstance(result, str)
self.assertGreater(len(result), 0)
def test_get_backend_display_name_vulkan(self):
"""Test display name for Vulkan backend."""
from vocalinux.utils.whispercpp_model_info import ComputeBackend, get_backend_display_name
result = get_backend_display_name(ComputeBackend.VULKAN)
self.assertEqual(result, "Vulkan GPU")
def test_get_backend_display_name_cuda(self):
"""Test display name for CUDA backend."""
from vocalinux.utils.whispercpp_model_info import ComputeBackend, get_backend_display_name
result = get_backend_display_name(ComputeBackend.CUDA)
self.assertEqual(result, "NVIDIA CUDA")
def test_get_backend_display_name_cpu(self):
"""Test display name for CPU backend."""
from vocalinux.utils.whispercpp_model_info import ComputeBackend, get_backend_display_name
result = get_backend_display_name(ComputeBackend.CPU)
self.assertEqual(result, "CPU")
def test_get_backend_display_name_unknown(self):
"""Test display name for unknown backend."""
from vocalinux.utils.whispercpp_model_info import get_backend_display_name
result = get_backend_display_name("rocm")
self.assertEqual(result, "ROCM")
if __name__ == "__main__":
unittest.main()