114 lines
3.6 KiB
Python
114 lines
3.6 KiB
Python
|
|
"""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
|