"""Tests for the UVC controller abstraction layer.""" from __future__ import annotations from app.camera.uvc.base import UvcParam, UvcParamInfo from app.camera.uvc.stub import NullUvcController class TestNullUvcController: """NullUvcController must implement the full interface, all as no-ops.""" def setup_method(self) -> None: self.ctrl = NullUvcController() def test_is_not_open(self) -> None: assert not self.ctrl.is_open() def test_open_returns_false(self) -> None: assert self.ctrl.open("any device") is False def test_close_does_not_raise(self) -> None: self.ctrl.close() # must not raise def test_get_param_info_returns_unsupported(self) -> None: info = self.ctrl.get_param_info(UvcParam.BRIGHTNESS) assert isinstance(info, UvcParamInfo) assert info.supported is False assert info.param is UvcParam.BRIGHTNESS def test_get_all_params_covers_all_uvc_params(self) -> None: infos = self.ctrl.get_all_params() returned_params = {i.param for i in infos} all_params = set(UvcParam) assert returned_params == all_params def test_get_all_params_all_unsupported(self) -> None: for info in self.ctrl.get_all_params(): assert info.supported is False def test_set_value_returns_false(self) -> None: assert self.ctrl.set_value(UvcParam.CONTRAST, 50) is False def test_set_auto_returns_false(self) -> None: assert self.ctrl.set_auto(UvcParam.WHITE_BALANCE, True) is False def test_set_value_does_not_raise(self) -> None: for param in UvcParam: self.ctrl.set_value(param, 0) # must not raise def test_set_auto_does_not_raise(self) -> None: for param in UvcParam: self.ctrl.set_auto(param, True) # must not raise self.ctrl.set_auto(param, False) class TestUvcParamInfo: """UvcParamInfo dataclass sanity checks.""" def test_defaults(self) -> None: info = UvcParamInfo(param=UvcParam.BRIGHTNESS, supported=True) assert info.minimum == 0 assert info.maximum == 100 assert info.default == 50 assert info.current == 50 assert info.step == 1 assert info.auto_supported is False assert info.auto_enabled is False def test_unsupported_flag(self) -> None: info = UvcParamInfo(param=UvcParam.GAMMA, supported=False) assert info.supported is False def test_custom_range(self) -> None: info = UvcParamInfo( param=UvcParam.HUE, supported=True, minimum=-180, maximum=180, default=0, current=-45, ) assert info.minimum == -180 assert info.maximum == 180 assert info.current == -45 class TestMakeUvcControllerFallback: """make_uvc_controller falls back to NullUvcController when no lib available.""" def test_returns_controller_instance(self) -> None: from app.camera.uvc import make_uvc_controller from app.camera.uvc.base import UvcControllerBase ctrl = make_uvc_controller("Test Camera") assert isinstance(ctrl, UvcControllerBase) def test_stub_used_when_native_lib_absent(self, monkeypatch) -> None: """If the native import fails, should return NullUvcController.""" import builtins real_import = builtins.__import__ def patched_import(name, *args, **kwargs): if name in ("duvc_ctl", "uvc"): raise ImportError(f"Mocked missing: {name}") return real_import(name, *args, **kwargs) monkeypatch.setattr(builtins, "__import__", patched_import) # Re-import to exercise factory with patched import import importlib import app.camera.uvc as uvc_pkg importlib.reload(uvc_pkg) ctrl = uvc_pkg.make_uvc_controller("Test Camera") # Should be functional (not raise), may be Null or platform controller from app.camera.uvc.base import UvcControllerBase assert isinstance(ctrl, UvcControllerBase) # Reload back to normal importlib.reload(uvc_pkg)