Files
local-transcriber/tests/test_utils.py
T
Dmitry DementievandClaude Opus 4.6 617181f2dc feat(openvino): добавлена поддержка Intel GPU через OpenVINO
- Зачем:
  - OpenVINO backend хардкодил "CPU", хотя Intel Arc GPU доступен и даёт ~2x ускорение.
- Что:
  - новые device modes: --device openvino-gpu, openvino-cpu; openvino — авто-детект GPU/CPU.
  - detect_device() проверяет Intel GPU через OpenVINO Core API (с fail-safe).
  - OpenVINOBackend передаёт "GPU"/"CPU" в WhisperPipeline вместо хардкода "CPU".
  - подсказка "Совет: --model large-v3" при наличии GPU и модели не large-v3.
  - .gitattributes для нормализации line endings (eol=lf).
  - 150 тестов, включая GPU detection, routing, fallback, CLI device info.
  - README, docs/gpu.md, docs/PRD.md обновлены для Intel GPU.
- Проверка:
  - uv run pytest (150 passed).
  - uv run transcribe --device openvino-gpu file.mp4 на Intel Arc 140T GPU.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 15:45:27 +03:00

240 lines
8.0 KiB
Python

import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from local_transcriber.utils import (
_is_openvino_gpu_available,
build_output_path,
detect_device,
expand_globs,
get_gpu_name,
get_intel_gpu_name,
has_existing_transcript,
validate_input_file,
)
def test_validate_input_file_not_found(tmp_path):
missing = tmp_path / "no_such_file.mp3"
with pytest.raises(FileNotFoundError):
validate_input_file(missing)
def test_validate_input_file_empty(tmp_path):
empty = tmp_path / "empty.mp3"
empty.touch()
with pytest.raises(ValueError, match="пустой"):
validate_input_file(empty)
def test_validate_input_file_unknown_ext(tmp_path):
f = tmp_path / "notes.txt"
f.write_text("hello")
with pytest.warns(UserWarning, match="не входит в список"):
result = validate_input_file(f)
assert result == f.resolve()
def test_validate_input_file_ok(tmp_path):
f = tmp_path / "audio.mp3"
f.write_bytes(b"\x00" * 16)
result = validate_input_file(f)
assert result == f.resolve()
def test_build_output_path_default():
inp = Path("/some/dir/meeting-2026-03-17.mp4")
result = build_output_path(inp)
assert result == Path("/some/dir/meeting-2026-03-17-transcript.md")
def test_build_output_path_custom():
inp = Path("/some/dir/audio.mp3")
custom = Path("/out/result.md")
result = build_output_path(inp, output=custom)
assert result == custom
def test_detect_device_explicit_passthrough():
"""Явные device strings проходят без изменений."""
assert detect_device("cpu") == "cpu"
assert detect_device("cuda") == "cuda"
assert detect_device("openvino-gpu") == "openvino-gpu"
assert detect_device("openvino-cpu") == "openvino-cpu"
def test_detect_device_auto_openvino_gpu():
"""auto + нет nvidia-smi + есть OpenVINO GPU → openvino-gpu."""
with (
patch("local_transcriber.utils.shutil.which", return_value=None),
patch("local_transcriber.utils._is_openvino_gpu_available", return_value=True),
):
assert detect_device("auto") == "openvino-gpu"
def test_detect_device_auto_openvino_cpu():
"""auto + нет nvidia-smi + есть OpenVINO, нет GPU → openvino-cpu."""
with (
patch("local_transcriber.utils.shutil.which", return_value=None),
patch("local_transcriber.utils._is_openvino_gpu_available", return_value=False),
patch("local_transcriber.utils._is_openvino_available", return_value=True),
):
assert detect_device("auto") == "openvino-cpu"
def test_detect_device_cuda_over_openvino():
"""nvidia-smi доступен и openvino тоже → cuda побеждает."""
with (
patch("local_transcriber.utils.shutil.which", return_value="/usr/bin/nvidia-smi"),
patch("local_transcriber.utils._is_openvino_gpu_available", return_value=True),
):
assert detect_device("auto") == "cuda"
def test_detect_device_auto_cpu_fallback():
"""Ни nvidia-smi, ни openvino → cpu."""
with (
patch("local_transcriber.utils.shutil.which", return_value=None),
patch("local_transcriber.utils._is_openvino_gpu_available", return_value=False),
patch("local_transcriber.utils._is_openvino_available", return_value=False),
):
assert detect_device("auto") == "cpu"
def test_detect_device_openvino_resolves_to_gpu():
"""--device openvino + GPU доступен → openvino-gpu."""
with patch("local_transcriber.utils._is_openvino_gpu_available", return_value=True):
assert detect_device("openvino") == "openvino-gpu"
def test_detect_device_openvino_resolves_to_cpu():
"""--device openvino + GPU недоступен → openvino-cpu."""
with patch("local_transcriber.utils._is_openvino_gpu_available", return_value=False):
assert detect_device("openvino") == "openvino-cpu"
# === _is_openvino_gpu_available ===
def test_is_openvino_gpu_available_true():
mock_core = MagicMock()
mock_core.return_value.available_devices = ["CPU", "GPU"]
with (
patch("local_transcriber.utils._is_openvino_available", return_value=True),
patch("local_transcriber.utils.Core", mock_core, create=True),
):
# Need to patch the import inside the function
with patch.dict("sys.modules", {"openvino": MagicMock(Core=mock_core)}):
assert _is_openvino_gpu_available() is True
def test_is_openvino_gpu_available_no_gpu():
mock_core = MagicMock()
mock_core.return_value.available_devices = ["CPU"]
with (
patch("local_transcriber.utils._is_openvino_available", return_value=True),
patch.dict("sys.modules", {"openvino": MagicMock(Core=mock_core)}),
):
assert _is_openvino_gpu_available() is False
def test_is_openvino_gpu_available_no_openvino():
"""OpenVINO не установлен → False."""
with patch("local_transcriber.utils._is_openvino_available", return_value=False):
assert _is_openvino_gpu_available() is False
def test_is_openvino_gpu_available_runtime_error():
"""Runtime error от OpenVINO → False (fail-safe)."""
with (
patch("local_transcriber.utils._is_openvino_available", return_value=True),
patch.dict("sys.modules", {"openvino": MagicMock(Core=MagicMock(side_effect=RuntimeError("broken")))}),
):
assert _is_openvino_gpu_available() is False
# === get_intel_gpu_name ===
def test_get_intel_gpu_name_success():
mock_core = MagicMock()
mock_core.return_value.get_property.return_value = "Intel(R) Arc(TM) 140T GPU (16GB) (iGPU)"
with patch.dict("sys.modules", {"openvino": MagicMock(Core=mock_core)}):
assert get_intel_gpu_name() == "Intel(R) Arc(TM) 140T GPU (16GB) (iGPU)"
def test_get_intel_gpu_name_error():
"""Ошибка → None (fail-safe)."""
with patch.dict("sys.modules", {"openvino": MagicMock(Core=MagicMock(side_effect=Exception("no gpu")))}):
assert get_intel_gpu_name() is None
def test_get_gpu_name_no_nvidia_smi():
with patch("shutil.which", return_value=None):
with patch("subprocess.run", side_effect=FileNotFoundError):
result = get_gpu_name()
assert result is None
def test_get_gpu_name_empty_stdout():
"""nvidia-smi returns 0 but stdout is empty — should return None, not crash."""
mock_result = subprocess.CompletedProcess(
args=[], returncode=0, stdout="", stderr=""
)
with patch("subprocess.run", return_value=mock_result):
result = get_gpu_name()
assert result is None
def test_get_gpu_name_success():
mock_result = subprocess.CompletedProcess(
args=[], returncode=0, stdout="NVIDIA GeForce RTX 3060\n", stderr=""
)
with patch("subprocess.run", return_value=mock_result):
result = get_gpu_name()
assert result == "NVIDIA GeForce RTX 3060"
def test_expand_globs_no_patterns(tmp_path):
a = tmp_path / "a.mp3"
b = tmp_path / "b.mp3"
result = expand_globs([a, b])
assert result == [a, b]
def test_expand_globs_with_star(tmp_path):
(tmp_path / "x.mp3").write_bytes(b"fake")
(tmp_path / "y.mp3").write_bytes(b"fake")
(tmp_path / "z.txt").write_bytes(b"fake")
result = expand_globs([Path(str(tmp_path / "*.mp3"))])
assert len(result) == 2
assert all(p.suffix == ".mp3" for p in result)
def test_expand_globs_no_match(tmp_path):
result = expand_globs([Path(str(tmp_path / "*.wav"))])
assert result == []
def test_expand_globs_deduplicates(tmp_path):
f = tmp_path / "a.mp3"
f.write_bytes(b"fake")
result = expand_globs([f, Path(str(tmp_path / "*.mp3"))])
assert len(result) == 1
def test_has_existing_transcript_true(tmp_path):
audio = tmp_path / "meeting.mp3"
audio.write_bytes(b"fake")
transcript = tmp_path / "meeting-transcript.md"
transcript.write_text("content")
assert has_existing_transcript(audio) is True
def test_has_existing_transcript_false(tmp_path):
audio = tmp_path / "meeting.mp3"
audio.write_bytes(b"fake")
assert has_existing_transcript(audio) is False