"""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