birdcage/tui/tests/test_camera_backend.py

114 lines
3.6 KiB
Python
Raw Normal View History

"""Test camera backend abstraction — DemoCamera, SubprocessCamera, detection.
DemoCamera is always available and produces valid output files.
SubprocessCamera reports unavailable when the tool doesn't exist.
"""
from pathlib import Path
import pytest
from birdcage_tui.camera import (
CameraConfig,
DemoCamera,
SubprocessCamera,
auto_select_backend,
detect_cameras,
)
def test_demo_camera_captures_file(tmp_path: Path):
"""DemoCamera should produce a file at the requested path."""
camera = DemoCamera()
output = tmp_path / "test.jpg"
result = camera.capture(output)
assert result.success
assert output.exists()
assert output.stat().st_size > 0
assert result.timestamp # ISO-8601 UTC string
assert result.duration_ms >= 0
def test_demo_camera_always_available():
"""DemoCamera.is_available() should always return True."""
camera = DemoCamera()
assert camera.is_available()
def test_demo_camera_name():
"""DemoCamera.name should be 'demo'."""
camera = DemoCamera()
assert camera.name == "demo"
def test_demo_camera_valid_jpeg(tmp_path: Path):
"""DemoCamera should produce a file starting with JPEG SOI marker."""
camera = DemoCamera()
output = tmp_path / "test.jpg"
result = camera.capture(output)
assert result.success
data = output.read_bytes()
# JPEG files start with SOI marker: FF D8
assert data[:2] == b"\xff\xd8"
def test_demo_camera_custom_resolution():
"""DemoCamera should accept custom resolution."""
camera = DemoCamera(resolution=(640, 480))
assert camera._resolution == (640, 480)
def test_subprocess_camera_not_available():
"""SubprocessCamera with nonexistent tool should be unavailable."""
config = CameraConfig(backend_cmd="nonexistent_camera_tool_xyz")
camera = SubprocessCamera(config)
assert not camera.is_available()
def test_subprocess_camera_capture_missing_tool(tmp_path: Path):
"""Capture with missing tool should return failure."""
config = CameraConfig(backend_cmd="nonexistent_camera_tool_xyz")
camera = SubprocessCamera(config)
output = tmp_path / "test.jpg"
result = camera.capture(output)
assert not result.success
assert result.error # Has a meaningful error message
def test_subprocess_camera_name():
"""SubprocessCamera.name should include tool and device."""
config = CameraConfig(backend_cmd="fswebcam", device="/dev/video0")
camera = SubprocessCamera(config)
assert "fswebcam" in camera.name
assert "/dev/video0" in camera.name
def test_detect_cameras_returns_list():
"""detect_cameras() should return a list (may be empty in CI)."""
devices = detect_cameras()
assert isinstance(devices, list)
def test_auto_select_returns_available_backend():
"""auto_select should return an available backend (demo or real)."""
config = CameraConfig(backend_cmd="nonexistent_tool_xyz")
backend = auto_select_backend(config)
# Should always return something available
assert backend.is_available()
# If no real backends exist, should be demo; otherwise a real one
assert backend.name # Has a name
@pytest.mark.parametrize("backend_cmd", ["fswebcam", "ffmpeg", "libcamera-still"])
def test_subprocess_unknown_backend_fails(tmp_path: Path, backend_cmd: str):
"""Unknown backend command pattern should be in the lookup table."""
config = CameraConfig(backend_cmd=backend_cmd)
SubprocessCamera(config)
# The command template should exist even if the tool isn't installed
assert backend_cmd in SubprocessCamera._COMMANDS