Auto-detects NVIDIA (CUDA), AMD (ROCm), or CPU-only GPU and installs the correct PyTorch variant + easyocr + all visual extraction dependencies. Removes easyocr from video-full pip extras to avoid pulling ~2GB of wrong CUDA packages on non-NVIDIA systems. New files: - video_setup.py (835 lines): GPU detection, PyTorch install, ROCm config, venv checks, system dep validation, module selection, verification - test_video_setup.py (60 tests): Full coverage of detection, install, verify Updated docs: CHANGELOG, AGENTS.md, CLAUDE.md, README.md, CLI_REFERENCE, FAQ, TROUBLESHOOTING, installation guide, video dependency plan All 2523 tests passing (15 skipped). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
680 lines
26 KiB
Python
680 lines
26 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests for Video Setup (cli/video_setup.py) and video_visual.py resilience.
|
|
|
|
Tests cover:
|
|
- GPU detection (NVIDIA, AMD ROCm, AMD without ROCm, CPU fallback)
|
|
- CUDA / ROCm version → index URL mapping
|
|
- PyTorch installation (mocked subprocess)
|
|
- Visual deps installation (mocked subprocess)
|
|
- Installation verification
|
|
- run_setup orchestrator
|
|
- Venv detection and creation
|
|
- System dep checks (tesseract binary)
|
|
- ROCm env var configuration
|
|
- Module selection (SetupModules)
|
|
- Tesseract circuit breaker (video_visual.py)
|
|
- --setup flag in VIDEO_ARGUMENTS and early-exit in video_scraper
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from skill_seekers.cli.video_setup import (
|
|
_BASE_VIDEO_DEPS,
|
|
GPUInfo,
|
|
GPUVendor,
|
|
SetupModules,
|
|
_build_visual_deps,
|
|
_cuda_version_to_index_url,
|
|
_detect_distro,
|
|
_PYTORCH_BASE,
|
|
_rocm_version_to_index_url,
|
|
check_tesseract,
|
|
configure_rocm_env,
|
|
create_venv,
|
|
detect_gpu,
|
|
get_venv_activate_cmd,
|
|
get_venv_python,
|
|
install_torch,
|
|
install_visual_deps,
|
|
is_in_venv,
|
|
run_setup,
|
|
verify_installation,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# GPU Detection Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestGPUDetection(unittest.TestCase):
|
|
"""Tests for detect_gpu() and its helpers."""
|
|
|
|
@patch("skill_seekers.cli.video_setup.shutil.which")
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
def test_nvidia_detected(self, mock_run, mock_which):
|
|
"""nvidia-smi present → GPUVendor.NVIDIA."""
|
|
mock_which.side_effect = lambda cmd: "/usr/bin/nvidia-smi" if cmd == "nvidia-smi" else None
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout=(
|
|
"+-------------------------+\n"
|
|
"| NVIDIA GeForce RTX 4090 On |\n"
|
|
"| CUDA Version: 12.4 |\n"
|
|
"+-------------------------+\n"
|
|
),
|
|
)
|
|
gpu = detect_gpu()
|
|
assert gpu.vendor == GPUVendor.NVIDIA
|
|
assert "12.4" in gpu.compute_version
|
|
assert "cu124" in gpu.index_url
|
|
|
|
@patch("skill_seekers.cli.video_setup.shutil.which")
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
@patch("skill_seekers.cli.video_setup._read_rocm_version", return_value="6.3.1")
|
|
def test_amd_rocm_detected(self, mock_rocm_ver, mock_run, mock_which):
|
|
"""rocminfo present → GPUVendor.AMD."""
|
|
|
|
def which_side(cmd):
|
|
if cmd == "nvidia-smi":
|
|
return None
|
|
if cmd == "rocminfo":
|
|
return "/usr/bin/rocminfo"
|
|
return None
|
|
|
|
mock_which.side_effect = which_side
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="Marketing Name: AMD Radeon RX 7900 XTX\n",
|
|
)
|
|
gpu = detect_gpu()
|
|
assert gpu.vendor == GPUVendor.AMD
|
|
assert "rocm6.3" in gpu.index_url
|
|
|
|
@patch("skill_seekers.cli.video_setup.shutil.which")
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
def test_amd_no_rocm_fallback(self, mock_run, mock_which):
|
|
"""AMD GPU in lspci but no ROCm → AMD vendor, CPU index URL."""
|
|
|
|
def which_side(cmd):
|
|
if cmd == "lspci":
|
|
return "/usr/bin/lspci"
|
|
return None
|
|
|
|
mock_which.side_effect = which_side
|
|
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="06:00.0 VGA compatible controller: AMD/ATI Navi 31 [Radeon RX 7900 XTX]\n",
|
|
)
|
|
gpu = detect_gpu()
|
|
assert gpu.vendor == GPUVendor.AMD
|
|
assert "cpu" in gpu.index_url
|
|
assert any("ROCm is not installed" in d for d in gpu.details)
|
|
|
|
@patch("skill_seekers.cli.video_setup.shutil.which", return_value=None)
|
|
def test_cpu_fallback(self, mock_which):
|
|
"""No GPU tools found → GPUVendor.NONE."""
|
|
gpu = detect_gpu()
|
|
assert gpu.vendor == GPUVendor.NONE
|
|
assert "cpu" in gpu.index_url
|
|
|
|
@patch("skill_seekers.cli.video_setup.shutil.which")
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
def test_nvidia_smi_error(self, mock_run, mock_which):
|
|
"""nvidia-smi returns non-zero → skip to next check."""
|
|
mock_which.side_effect = lambda cmd: (
|
|
"/usr/bin/nvidia-smi" if cmd == "nvidia-smi" else None
|
|
)
|
|
mock_run.return_value = MagicMock(returncode=1, stdout="")
|
|
gpu = detect_gpu()
|
|
assert gpu.vendor == GPUVendor.NONE
|
|
|
|
@patch("skill_seekers.cli.video_setup.shutil.which")
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
def test_nvidia_smi_timeout(self, mock_run, mock_which):
|
|
"""nvidia-smi times out → skip to next check."""
|
|
mock_which.side_effect = lambda cmd: (
|
|
"/usr/bin/nvidia-smi" if cmd == "nvidia-smi" else None
|
|
)
|
|
mock_run.side_effect = subprocess.TimeoutExpired(cmd="nvidia-smi", timeout=10)
|
|
gpu = detect_gpu()
|
|
assert gpu.vendor == GPUVendor.NONE
|
|
|
|
@patch("skill_seekers.cli.video_setup.shutil.which")
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
def test_rocminfo_error(self, mock_run, mock_which):
|
|
"""rocminfo returns non-zero → skip to next check."""
|
|
|
|
def which_side(cmd):
|
|
if cmd == "nvidia-smi":
|
|
return None
|
|
if cmd == "rocminfo":
|
|
return "/usr/bin/rocminfo"
|
|
return None
|
|
|
|
mock_which.side_effect = which_side
|
|
mock_run.return_value = MagicMock(returncode=1, stdout="")
|
|
gpu = detect_gpu()
|
|
assert gpu.vendor == GPUVendor.NONE
|
|
|
|
|
|
# =============================================================================
|
|
# Version Mapping Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestVersionMapping(unittest.TestCase):
|
|
"""Tests for CUDA/ROCm version → index URL mapping."""
|
|
|
|
def test_cuda_124(self):
|
|
assert _cuda_version_to_index_url("12.4") == f"{_PYTORCH_BASE}/cu124"
|
|
|
|
def test_cuda_126(self):
|
|
assert _cuda_version_to_index_url("12.6") == f"{_PYTORCH_BASE}/cu124"
|
|
|
|
def test_cuda_121(self):
|
|
assert _cuda_version_to_index_url("12.1") == f"{_PYTORCH_BASE}/cu121"
|
|
|
|
def test_cuda_118(self):
|
|
assert _cuda_version_to_index_url("11.8") == f"{_PYTORCH_BASE}/cu118"
|
|
|
|
def test_cuda_old_falls_to_cpu(self):
|
|
assert _cuda_version_to_index_url("10.2") == f"{_PYTORCH_BASE}/cpu"
|
|
|
|
def test_cuda_invalid_string(self):
|
|
assert _cuda_version_to_index_url("garbage") == f"{_PYTORCH_BASE}/cpu"
|
|
|
|
def test_rocm_63(self):
|
|
assert _rocm_version_to_index_url("6.3.1") == f"{_PYTORCH_BASE}/rocm6.3"
|
|
|
|
def test_rocm_60(self):
|
|
assert _rocm_version_to_index_url("6.0") == f"{_PYTORCH_BASE}/rocm6.2.4"
|
|
|
|
def test_rocm_old_falls_to_cpu(self):
|
|
assert _rocm_version_to_index_url("5.4") == f"{_PYTORCH_BASE}/cpu"
|
|
|
|
def test_rocm_invalid(self):
|
|
assert _rocm_version_to_index_url("bad") == f"{_PYTORCH_BASE}/cpu"
|
|
|
|
|
|
# =============================================================================
|
|
# Venv Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestVenv(unittest.TestCase):
|
|
"""Tests for venv detection and creation."""
|
|
|
|
def test_is_in_venv_returns_bool(self):
|
|
result = is_in_venv()
|
|
assert isinstance(result, bool)
|
|
|
|
def test_is_in_venv_detects_prefix_mismatch(self):
|
|
# If sys.prefix != sys.base_prefix, we're in a venv
|
|
with patch.object(sys, "prefix", "/some/venv"), \
|
|
patch.object(sys, "base_prefix", "/usr"):
|
|
assert is_in_venv() is True
|
|
|
|
def test_is_in_venv_detects_no_venv(self):
|
|
with patch.object(sys, "prefix", "/usr"), \
|
|
patch.object(sys, "base_prefix", "/usr"):
|
|
assert is_in_venv() is False
|
|
|
|
def test_create_venv_in_tempdir(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
venv_path = os.path.join(tmpdir, "test_venv")
|
|
result = create_venv(venv_path)
|
|
assert result is True
|
|
assert os.path.isdir(venv_path)
|
|
|
|
def test_create_venv_already_exists(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
# Create it once
|
|
create_venv(tmpdir)
|
|
# Creating again should succeed (already exists)
|
|
assert create_venv(tmpdir) is True
|
|
|
|
def test_get_venv_python_linux(self):
|
|
with patch("skill_seekers.cli.video_setup.platform.system", return_value="Linux"):
|
|
path = get_venv_python("/path/.venv")
|
|
assert path.endswith("bin/python")
|
|
|
|
def test_get_venv_activate_cmd_linux(self):
|
|
with patch("skill_seekers.cli.video_setup.platform.system", return_value="Linux"):
|
|
cmd = get_venv_activate_cmd("/path/.venv")
|
|
assert "source" in cmd
|
|
assert "bin/activate" in cmd
|
|
|
|
|
|
# =============================================================================
|
|
# System Dep Check Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestSystemDeps(unittest.TestCase):
|
|
"""Tests for system dependency checks."""
|
|
|
|
@patch("skill_seekers.cli.video_setup.shutil.which", return_value=None)
|
|
def test_tesseract_not_installed(self, mock_which):
|
|
result = check_tesseract()
|
|
assert result["installed"] is False
|
|
assert result["has_eng"] is False
|
|
assert isinstance(result["install_cmd"], str)
|
|
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
@patch("skill_seekers.cli.video_setup.shutil.which", return_value="/usr/bin/tesseract")
|
|
def test_tesseract_installed_with_eng(self, mock_which, mock_run):
|
|
mock_run.side_effect = [
|
|
# --version call
|
|
MagicMock(returncode=0, stdout="tesseract 5.3.0\n", stderr=""),
|
|
# --list-langs call
|
|
MagicMock(returncode=0, stdout="List of available languages:\neng\nosd\n", stderr=""),
|
|
]
|
|
result = check_tesseract()
|
|
assert result["installed"] is True
|
|
assert result["has_eng"] is True
|
|
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
@patch("skill_seekers.cli.video_setup.shutil.which", return_value="/usr/bin/tesseract")
|
|
def test_tesseract_installed_no_eng(self, mock_which, mock_run):
|
|
mock_run.side_effect = [
|
|
MagicMock(returncode=0, stdout="tesseract 5.3.0\n", stderr=""),
|
|
MagicMock(returncode=0, stdout="List of available languages:\nosd\n", stderr=""),
|
|
]
|
|
result = check_tesseract()
|
|
assert result["installed"] is True
|
|
assert result["has_eng"] is False
|
|
|
|
def test_detect_distro_returns_string(self):
|
|
result = _detect_distro()
|
|
assert isinstance(result, str)
|
|
|
|
@patch("builtins.open", side_effect=OSError)
|
|
def test_detect_distro_no_os_release(self, mock_open):
|
|
assert _detect_distro() == "unknown"
|
|
|
|
|
|
# =============================================================================
|
|
# ROCm Configuration Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestROCmConfig(unittest.TestCase):
|
|
"""Tests for configure_rocm_env()."""
|
|
|
|
def test_sets_miopen_find_mode(self):
|
|
env_backup = os.environ.get("MIOPEN_FIND_MODE")
|
|
try:
|
|
os.environ.pop("MIOPEN_FIND_MODE", None)
|
|
changes = configure_rocm_env()
|
|
assert "MIOPEN_FIND_MODE=FAST" in changes
|
|
assert os.environ["MIOPEN_FIND_MODE"] == "FAST"
|
|
finally:
|
|
if env_backup is not None:
|
|
os.environ["MIOPEN_FIND_MODE"] = env_backup
|
|
|
|
def test_does_not_override_existing(self):
|
|
env_backup = os.environ.get("MIOPEN_FIND_MODE")
|
|
try:
|
|
os.environ["MIOPEN_FIND_MODE"] = "NORMAL"
|
|
changes = configure_rocm_env()
|
|
miopen_changes = [c for c in changes if "MIOPEN_FIND_MODE" in c]
|
|
assert len(miopen_changes) == 0
|
|
assert os.environ["MIOPEN_FIND_MODE"] == "NORMAL"
|
|
finally:
|
|
if env_backup is not None:
|
|
os.environ["MIOPEN_FIND_MODE"] = env_backup
|
|
else:
|
|
os.environ.pop("MIOPEN_FIND_MODE", None)
|
|
|
|
def test_sets_miopen_user_db_path(self):
|
|
env_backup = os.environ.get("MIOPEN_USER_DB_PATH")
|
|
try:
|
|
os.environ.pop("MIOPEN_USER_DB_PATH", None)
|
|
changes = configure_rocm_env()
|
|
db_changes = [c for c in changes if "MIOPEN_USER_DB_PATH" in c]
|
|
assert len(db_changes) == 1
|
|
finally:
|
|
if env_backup is not None:
|
|
os.environ["MIOPEN_USER_DB_PATH"] = env_backup
|
|
|
|
|
|
# =============================================================================
|
|
# Module Selection Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestModuleSelection(unittest.TestCase):
|
|
"""Tests for SetupModules and _build_visual_deps."""
|
|
|
|
def test_default_modules_all_true(self):
|
|
m = SetupModules()
|
|
assert m.torch is True
|
|
assert m.easyocr is True
|
|
assert m.opencv is True
|
|
assert m.tesseract is True
|
|
assert m.scenedetect is True
|
|
assert m.whisper is True
|
|
|
|
def test_build_all_deps(self):
|
|
deps = _build_visual_deps(SetupModules())
|
|
assert "yt-dlp" in deps
|
|
assert "youtube-transcript-api" in deps
|
|
assert "easyocr" in deps
|
|
assert "opencv-python-headless" in deps
|
|
assert "pytesseract" in deps
|
|
assert "scenedetect[opencv]" in deps
|
|
assert "faster-whisper" in deps
|
|
|
|
def test_build_no_optional_deps(self):
|
|
"""Even with all optional modules off, base video deps are included."""
|
|
m = SetupModules(
|
|
torch=False, easyocr=False, opencv=False,
|
|
tesseract=False, scenedetect=False, whisper=False,
|
|
)
|
|
deps = _build_visual_deps(m)
|
|
assert deps == list(_BASE_VIDEO_DEPS)
|
|
|
|
def test_build_partial_deps(self):
|
|
m = SetupModules(easyocr=True, opencv=True, tesseract=False, scenedetect=False, whisper=False)
|
|
deps = _build_visual_deps(m)
|
|
assert "yt-dlp" in deps
|
|
assert "youtube-transcript-api" in deps
|
|
assert "easyocr" in deps
|
|
assert "opencv-python-headless" in deps
|
|
assert "pytesseract" not in deps
|
|
assert "faster-whisper" not in deps
|
|
|
|
|
|
# =============================================================================
|
|
# Installation Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestInstallation(unittest.TestCase):
|
|
"""Tests for install_torch() and install_visual_deps()."""
|
|
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
def test_install_torch_success(self, mock_run):
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
gpu = GPUInfo(vendor=GPUVendor.NVIDIA, index_url=f"{_PYTORCH_BASE}/cu124")
|
|
assert install_torch(gpu) is True
|
|
call_args = mock_run.call_args[0][0]
|
|
assert "torch" in call_args
|
|
assert "--index-url" in call_args
|
|
assert f"{_PYTORCH_BASE}/cu124" in call_args
|
|
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
def test_install_torch_cpu(self, mock_run):
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
gpu = GPUInfo(vendor=GPUVendor.NONE, index_url=f"{_PYTORCH_BASE}/cpu")
|
|
assert install_torch(gpu) is True
|
|
call_args = mock_run.call_args[0][0]
|
|
assert f"{_PYTORCH_BASE}/cpu" in call_args
|
|
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
def test_install_torch_failure(self, mock_run):
|
|
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="error msg")
|
|
gpu = GPUInfo(vendor=GPUVendor.NVIDIA, index_url=f"{_PYTORCH_BASE}/cu124")
|
|
assert install_torch(gpu) is False
|
|
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
def test_install_torch_timeout(self, mock_run):
|
|
mock_run.side_effect = subprocess.TimeoutExpired(cmd="pip", timeout=600)
|
|
gpu = GPUInfo(vendor=GPUVendor.NVIDIA, index_url=f"{_PYTORCH_BASE}/cu124")
|
|
assert install_torch(gpu) is False
|
|
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
def test_install_torch_custom_python(self, mock_run):
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
gpu = GPUInfo(vendor=GPUVendor.NONE, index_url=f"{_PYTORCH_BASE}/cpu")
|
|
install_torch(gpu, python_exe="/custom/python")
|
|
call_args = mock_run.call_args[0][0]
|
|
assert call_args[0] == "/custom/python"
|
|
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
def test_install_visual_deps_success(self, mock_run):
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
assert install_visual_deps() is True
|
|
call_args = mock_run.call_args[0][0]
|
|
assert "easyocr" in call_args
|
|
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
def test_install_visual_deps_failure(self, mock_run):
|
|
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="error")
|
|
assert install_visual_deps() is False
|
|
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
def test_install_visual_deps_partial_modules(self, mock_run):
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
modules = SetupModules(easyocr=True, opencv=False, tesseract=False, scenedetect=False, whisper=False)
|
|
install_visual_deps(modules)
|
|
call_args = mock_run.call_args[0][0]
|
|
assert "easyocr" in call_args
|
|
assert "opencv-python-headless" not in call_args
|
|
|
|
@patch("skill_seekers.cli.video_setup.subprocess.run")
|
|
def test_install_visual_deps_base_only(self, mock_run):
|
|
"""Even with all optional modules off, base video deps get installed."""
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
modules = SetupModules(easyocr=False, opencv=False, tesseract=False, scenedetect=False, whisper=False)
|
|
result = install_visual_deps(modules)
|
|
assert result is True
|
|
call_args = mock_run.call_args[0][0]
|
|
assert "yt-dlp" in call_args
|
|
assert "youtube-transcript-api" in call_args
|
|
assert "easyocr" not in call_args
|
|
|
|
|
|
# =============================================================================
|
|
# Verification Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestVerification(unittest.TestCase):
|
|
"""Tests for verify_installation()."""
|
|
|
|
@patch.dict("sys.modules", {"torch": None, "easyocr": None, "cv2": None})
|
|
def test_returns_dict(self):
|
|
results = verify_installation()
|
|
assert isinstance(results, dict)
|
|
|
|
def test_expected_keys(self):
|
|
results = verify_installation()
|
|
for key in ("yt-dlp", "youtube-transcript-api", "torch", "torch.cuda", "torch.rocm", "easyocr", "opencv"):
|
|
assert key in results, f"Missing key: {key}"
|
|
|
|
|
|
# =============================================================================
|
|
# Orchestrator Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestRunSetup(unittest.TestCase):
|
|
"""Tests for run_setup() orchestrator."""
|
|
|
|
@patch("skill_seekers.cli.video_setup.verify_installation")
|
|
@patch("skill_seekers.cli.video_setup.install_visual_deps", return_value=True)
|
|
@patch("skill_seekers.cli.video_setup.install_torch", return_value=True)
|
|
@patch("skill_seekers.cli.video_setup.check_tesseract")
|
|
@patch("skill_seekers.cli.video_setup.detect_gpu")
|
|
def test_non_interactive_success(self, mock_detect, mock_tess, mock_torch, mock_deps, mock_verify):
|
|
mock_detect.return_value = GPUInfo(
|
|
vendor=GPUVendor.NONE, name="CPU-only", index_url=f"{_PYTORCH_BASE}/cpu",
|
|
)
|
|
mock_tess.return_value = {"installed": True, "has_eng": True, "install_cmd": "", "version": "5.3.0"}
|
|
mock_verify.return_value = {
|
|
"torch": True, "torch.cuda": False, "torch.rocm": False,
|
|
"easyocr": True, "opencv": True, "pytesseract": True,
|
|
"scenedetect": True, "faster-whisper": True,
|
|
}
|
|
rc = run_setup(interactive=False)
|
|
assert rc == 0
|
|
mock_torch.assert_called_once()
|
|
mock_deps.assert_called_once()
|
|
|
|
@patch("skill_seekers.cli.video_setup.install_torch", return_value=False)
|
|
@patch("skill_seekers.cli.video_setup.check_tesseract")
|
|
@patch("skill_seekers.cli.video_setup.detect_gpu")
|
|
def test_failure_returns_nonzero(self, mock_detect, mock_tess, mock_torch):
|
|
mock_detect.return_value = GPUInfo(
|
|
vendor=GPUVendor.NONE, name="CPU-only", index_url=f"{_PYTORCH_BASE}/cpu",
|
|
)
|
|
mock_tess.return_value = {"installed": True, "has_eng": True, "install_cmd": "", "version": "5.3.0"}
|
|
rc = run_setup(interactive=False)
|
|
assert rc == 1
|
|
|
|
@patch("skill_seekers.cli.video_setup.install_torch", return_value=True)
|
|
@patch("skill_seekers.cli.video_setup.install_visual_deps", return_value=False)
|
|
@patch("skill_seekers.cli.video_setup.check_tesseract")
|
|
@patch("skill_seekers.cli.video_setup.detect_gpu")
|
|
def test_visual_deps_failure(self, mock_detect, mock_tess, mock_deps, mock_torch):
|
|
mock_detect.return_value = GPUInfo(
|
|
vendor=GPUVendor.NONE, name="CPU-only", index_url=f"{_PYTORCH_BASE}/cpu",
|
|
)
|
|
mock_tess.return_value = {"installed": True, "has_eng": True, "install_cmd": "", "version": "5.3.0"}
|
|
rc = run_setup(interactive=False)
|
|
assert rc == 1
|
|
|
|
@patch("skill_seekers.cli.video_setup.verify_installation")
|
|
@patch("skill_seekers.cli.video_setup.install_visual_deps", return_value=True)
|
|
@patch("skill_seekers.cli.video_setup.install_torch", return_value=True)
|
|
@patch("skill_seekers.cli.video_setup.check_tesseract")
|
|
@patch("skill_seekers.cli.video_setup.detect_gpu")
|
|
def test_rocm_configures_env(self, mock_detect, mock_tess, mock_torch, mock_deps, mock_verify):
|
|
"""AMD GPU → configure_rocm_env called and env vars set."""
|
|
mock_detect.return_value = GPUInfo(
|
|
vendor=GPUVendor.AMD, name="RX 7900", index_url=f"{_PYTORCH_BASE}/rocm6.3",
|
|
)
|
|
mock_tess.return_value = {"installed": True, "has_eng": True, "install_cmd": "", "version": "5.3.0"}
|
|
mock_verify.return_value = {
|
|
"torch": True, "torch.cuda": False, "torch.rocm": True,
|
|
"easyocr": True, "opencv": True, "pytesseract": True,
|
|
"scenedetect": True, "faster-whisper": True,
|
|
}
|
|
rc = run_setup(interactive=False)
|
|
assert rc == 0
|
|
assert os.environ.get("MIOPEN_FIND_MODE") is not None
|
|
|
|
|
|
# =============================================================================
|
|
# Tesseract Circuit Breaker Tests (video_visual.py)
|
|
# =============================================================================
|
|
|
|
|
|
class TestTesseractCircuitBreaker(unittest.TestCase):
|
|
"""Tests for _tesseract_broken flag in video_visual.py."""
|
|
|
|
def test_circuit_breaker_flag_exists(self):
|
|
import skill_seekers.cli.video_visual as vv
|
|
assert hasattr(vv, "_tesseract_broken")
|
|
|
|
def test_circuit_breaker_skips_after_failure(self):
|
|
import skill_seekers.cli.video_visual as vv
|
|
from skill_seekers.cli.video_models import FrameType
|
|
|
|
# Save and set broken state
|
|
original = vv._tesseract_broken
|
|
try:
|
|
vv._tesseract_broken = True
|
|
result = vv._run_tesseract_ocr("/nonexistent/path.png", FrameType.CODE_EDITOR)
|
|
assert result == []
|
|
finally:
|
|
vv._tesseract_broken = original
|
|
|
|
def test_circuit_breaker_allows_when_not_broken(self):
|
|
import skill_seekers.cli.video_visual as vv
|
|
from skill_seekers.cli.video_models import FrameType
|
|
|
|
original = vv._tesseract_broken
|
|
try:
|
|
vv._tesseract_broken = False
|
|
if not vv.HAS_PYTESSERACT:
|
|
# pytesseract not installed → returns [] immediately
|
|
result = vv._run_tesseract_ocr("/nonexistent/path.png", FrameType.CODE_EDITOR)
|
|
assert result == []
|
|
# If pytesseract IS installed, it would try to run and potentially fail
|
|
# on our fake path — that's fine, the circuit breaker would trigger
|
|
finally:
|
|
vv._tesseract_broken = original
|
|
|
|
|
|
# =============================================================================
|
|
# MIOPEN Env Var Tests (video_visual.py)
|
|
# =============================================================================
|
|
|
|
|
|
class TestMIOPENEnvVars(unittest.TestCase):
|
|
"""Tests that video_visual.py sets MIOPEN env vars at import time."""
|
|
|
|
def test_miopen_find_mode_set(self):
|
|
# video_visual.py sets this at module level before torch import
|
|
assert "MIOPEN_FIND_MODE" in os.environ
|
|
|
|
def test_miopen_user_db_path_set(self):
|
|
assert "MIOPEN_USER_DB_PATH" in os.environ
|
|
|
|
|
|
# =============================================================================
|
|
# Argument & Early-Exit Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestVideoArgumentSetup(unittest.TestCase):
|
|
"""Tests for --setup flag in VIDEO_ARGUMENTS."""
|
|
|
|
def test_setup_in_video_arguments(self):
|
|
from skill_seekers.cli.arguments.video import VIDEO_ARGUMENTS
|
|
|
|
assert "setup" in VIDEO_ARGUMENTS
|
|
assert VIDEO_ARGUMENTS["setup"]["kwargs"]["action"] == "store_true"
|
|
|
|
def test_parser_accepts_setup(self):
|
|
import argparse
|
|
|
|
from skill_seekers.cli.arguments.video import add_video_arguments
|
|
|
|
parser = argparse.ArgumentParser()
|
|
add_video_arguments(parser)
|
|
args = parser.parse_args(["--setup"])
|
|
assert args.setup is True
|
|
|
|
def test_parser_default_false(self):
|
|
import argparse
|
|
|
|
from skill_seekers.cli.arguments.video import add_video_arguments
|
|
|
|
parser = argparse.ArgumentParser()
|
|
add_video_arguments(parser)
|
|
args = parser.parse_args(["--url", "https://example.com"])
|
|
assert args.setup is False
|
|
|
|
|
|
class TestVideoScraperSetupEarlyExit(unittest.TestCase):
|
|
"""Test that --setup exits before source validation."""
|
|
|
|
@patch("skill_seekers.cli.video_setup.run_setup", return_value=0)
|
|
def test_setup_skips_source_validation(self, mock_setup):
|
|
"""--setup without --url should NOT error about missing source."""
|
|
from skill_seekers.cli.video_scraper import main
|
|
|
|
old_argv = sys.argv
|
|
try:
|
|
sys.argv = ["video_scraper", "--setup"]
|
|
rc = main()
|
|
assert rc == 0
|
|
mock_setup.assert_called_once_with(interactive=True)
|
|
finally:
|
|
sys.argv = old_argv
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|