Files
audio-scribe/tests/test_doctor_failures.py
JMR-devandClaude Sonnet 5 b23995f218 refactor: rename the package from ccn-transcribe to audio-scribe
The distribution, console script and import package are now audio-scribe /
audio_scribe (src/audio_scribe). Everything named for the old project follows:

- CcnError -> AudioScribeError, and its code "ccn_error" -> "audio_scribe_error"
  (the code is never persisted, so existing job state still loads)
- CCN_LIVE -> AUDIO_SCRIBE_LIVE for the live-GPU tests
- OpenVINO kernel cache moves to <cache>/audio-scribe/ov_cache; the first run
  after upgrading recompiles kernels, and the old directory is left in place
- README, build-binary.sh, hatch/coverage config and uv.lock updated to match

Breaking: the command is now `audio-scribe`; reinstall any tool install of the
old name with `uv tool uninstall ccn-transcribe && uv tool install .`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 15:52:42 -05:00

231 lines
8.6 KiB
Python

"""Doctor's failure paths -- the rows that only appear on a broken machine."""
from __future__ import annotations
import builtins
import shutil
import subprocess
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple
import openvino as ov
from audio_scribe import doctor
from audio_scribe.backends import probe
from audio_scribe.media import ffmpeg, ytdlp_opts
if TYPE_CHECKING:
from collections.abc import Callable
import pytest
def const[T](value: T) -> Callable[..., T]:
"""A typed stand-in for a bare lambda in monkeypatch.setattr."""
def _inner(*_a: object, **_k: object) -> T:
return value
return _inner
def _no_binary(monkeypatch: pytest.MonkeyPatch, *names: str) -> None:
real = shutil.which
def which(name: str, _mode: int = 0, _path: str | None = None) -> str | None:
return None if name in names else real(name)
monkeypatch.setattr(shutil, "which", which)
def _no_deno(monkeypatch: pytest.MonkeyPatch) -> None:
"""Hide deno from PATH *and* from the interpreter's own bin directory."""
_no_binary(monkeypatch, "deno")
monkeypatch.setattr(ytdlp_opts, "bundled", const(None))
class TestBinaries:
def test_a_missing_binary_fails_with_a_hint(self, monkeypatch: pytest.MonkeyPatch) -> None:
_no_binary(monkeypatch, "ffmpeg")
check = doctor.binary_check("ffmpeg")
assert check.status is doctor.Status.FAIL
assert check.hint
class TestOpenvino:
def test_an_unimportable_openvino_fails(self, monkeypatch: pytest.MonkeyPatch) -> None:
real = builtins.__import__
def blocked(name: str, *a: object, **k: object) -> Any:
if name == "openvino":
raise ImportError("not installed")
return real(name, *a, **k) # type: ignore[arg-type]
monkeypatch.setattr(builtins, "__import__", blocked)
assert doctor.check_openvino().status is doctor.Status.FAIL
assert doctor.check_devices()[0].status is doctor.Status.FAIL
def test_an_intel_gpu_without_a_plugin_is_a_plugin_problem(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# README section 7: clinfo working while OpenVINO shows only CPU is a
# plugin problem, not a driver problem.
class Core:
available_devices: ClassVar[list[str]] = ["CPU"]
monkeypatch.setattr(ov, "Core", Core)
monkeypatch.setattr(probe, "has_intel_gpu", const(True))
checks = doctor.check_devices()
assert any(c.name == "gpu plugin" and c.status is doctor.Status.FAIL for c in checks)
assert "plugin problem" in (checks[-1].hint or "")
class TestFlacEncoder:
def test_absent_encoder_fails(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ffmpeg, "has_flac_encoder", const(False))
assert doctor.check_flac_encoder().status is doctor.Status.FAIL
def test_a_broken_ffmpeg_fails(self, monkeypatch: pytest.MonkeyPatch) -> None:
def boom() -> bool:
raise OSError("ffmpeg is gone")
monkeypatch.setattr(ffmpeg, "has_flac_encoder", boom)
assert doctor.check_flac_encoder().status is doctor.Status.FAIL
class TestJsRuntime:
def test_the_bundled_deno_is_found_when_it_is_not_on_path(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
# Installed as a tool, the environment's bin/ is not on PATH. Before this
# was resolved directly, doctor reported the machine had no deno at all.
_no_binary(monkeypatch, "deno")
deno = tmp_path / "bin/deno"
deno.parent.mkdir(parents=True)
deno.write_text("#!/bin/sh\n")
deno.chmod(0o755)
monkeypatch.setattr(sys, "prefix", str(tmp_path))
check = doctor.check_js_runtime()
assert check.status is doctor.Status.PASS
assert str(deno) in check.detail
def test_no_runtime_at_all_fails(self, monkeypatch: pytest.MonkeyPatch) -> None:
_no_deno(monkeypatch)
monkeypatch.setattr(ytdlp_opts, "find_node", const(None))
check = doctor.check_js_runtime()
assert check.status is doctor.Status.FAIL
assert "YouTube" in (check.hint or "")
def test_nvm_only_node_warns_about_cron(self, monkeypatch: pytest.MonkeyPatch) -> None:
_no_deno(monkeypatch)
monkeypatch.setattr(
ytdlp_opts, "find_node", const("/home/u/.nvm/versions/node/v24/bin/node")
)
check = doctor.check_js_runtime()
assert check.status is doctor.Status.WARN
assert "cron" in (check.hint or "")
def test_system_node_passes(self, monkeypatch: pytest.MonkeyPatch) -> None:
_no_deno(monkeypatch)
monkeypatch.setattr(ytdlp_opts, "find_node", const("/usr/bin/node"))
assert doctor.check_js_runtime().status is doctor.Status.PASS
class TestRenderNodes:
def test_no_nodes_warns(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(probe, "render_nodes", const([]))
assert doctor.check_render_nodes()[0].status is doctor.Status.WARN
def test_an_unwritable_node_fails_with_the_group_fix(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
node = probe.RenderNode(
path=Path("/dev/dri/renderD128"),
vendor="intel",
driver="i915",
mode=0o660,
writable=False,
)
monkeypatch.setattr(probe, "render_nodes", const([node]))
checks = doctor.check_render_nodes()
failing = [c for c in checks if c.status is doctor.Status.FAIL]
assert failing
assert "usermod -aG render" in (failing[0].hint or "")
def test_a_properly_permissioned_node_is_silent(self, monkeypatch: pytest.MonkeyPatch) -> None:
node = probe.RenderNode(
path=Path("/dev/dri/renderD128"),
vendor="intel",
driver="i915",
mode=0o660,
writable=True,
)
monkeypatch.setattr(probe, "render_nodes", const([node]))
assert all(c.status is doctor.Status.PASS for c in doctor.check_render_nodes())
class TestClinfo:
def test_missing_clinfo_warns(self, monkeypatch: pytest.MonkeyPatch) -> None:
_no_binary(monkeypatch, "clinfo")
assert doctor.check_clinfo().status is doctor.Status.WARN
def test_no_opencl_device_points_at_the_compute_runtime(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
def empty(*_a: object, **_k: object) -> Any:
return subprocess.CompletedProcess(args=[], returncode=0, stdout=b"", stderr=b"")
monkeypatch.setattr(subprocess, "run", empty)
check = doctor.check_clinfo()
assert check.status is doctor.Status.FAIL
assert "intel-compute-runtime" in (check.hint or "")
def test_a_crashing_clinfo_warns(self, monkeypatch: pytest.MonkeyPatch) -> None:
def boom(*_a: object, **_k: object) -> None:
raise OSError("cannot execute")
monkeypatch.setattr(subprocess, "run", boom)
assert doctor.check_clinfo().status is doctor.Status.WARN
class TestModelAndCache:
def test_an_uncached_model_warns_with_the_download_command(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
def boom(*_a: object, **_k: object) -> None:
raise OSError("not cached")
monkeypatch.setattr(doctor, "snapshot", boom)
check = doctor.check_model()
assert check.status is doctor.Status.WARN
assert "hf download" in (check.hint or "")
def test_an_unwritable_cache_dir_fails(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(doctor, "cache_root", const(Path("/proc/nope/cache")))
assert doctor.check_cache_dir().status is doctor.Status.FAIL
def test_low_disk_warns(self, monkeypatch: pytest.MonkeyPatch) -> None:
class Usage(NamedTuple):
total: int
used: int
free: int
def tiny(_p: object) -> Usage:
return Usage(total=10**9, used=10**9, free=10**8)
monkeypatch.setattr(shutil, "disk_usage", tiny)
assert doctor.check_disk().status is doctor.Status.WARN
class TestVendorRows:
def test_a_present_nvidia_is_reported(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(probe, "has_nvidia_gpu", const(True))
rows = {c.name: c for c in doctor.check_vendors()}
assert rows["nvidia"].status is doctor.Status.PASS
def test_a_present_amd_is_reported(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(probe, "has_amd_gpu", const(True))
rows = {c.name: c for c in doctor.check_vendors()}
assert rows["amd"].status is doctor.Status.PASS