import pytest from app.dmx.backends import FakeOlaClientAdapter, OlaDmxBackend, PythonOlaClientAdapter from app.dmx.frame import DmxFrame @pytest.mark.asyncio async def test_ola_backend_sends_full_frame_and_updates_status() -> None: adapter = FakeOlaClientAdapter() backend = OlaDmxBackend( universe=7, output_port="usb-dmx-1", adapter_factory=lambda: adapter, ) frame = DmxFrame(universe=7) frame.set_channel(1, 255, "scene") frame.set_channel(512, 64, "scene") await backend.send_frame(frame) status = backend.get_status() assert status.connected is True assert status.degraded is False assert status.frames_sent == 1 assert status.send_errors == 0 assert status.last_successful_frame is not None assert status.selected_universe == 7 assert status.selected_output_port == "usb-dmx-1" assert adapter.open_calls == 1 assert len(adapter.sent_frames) == 1 sent_universe, values = adapter.sent_frames[0] assert sent_universe == 7 assert len(values) == 512 assert values[0] == 255 assert values[511] == 64 @pytest.mark.asyncio async def test_ola_backend_reconnects_after_send_failure() -> None: created_adapters: list[FakeOlaClientAdapter] = [] def factory() -> FakeOlaClientAdapter: adapter = FakeOlaClientAdapter( send_results=[RuntimeError("olad unavailable")] if not created_adapters else [], ) created_adapters.append(adapter) return adapter backend = OlaDmxBackend( universe=1, output_port="usb-dmx-1", adapter_factory=factory, ) frame = DmxFrame() frame.set_channel(1, 128, "scene") with pytest.raises(RuntimeError, match="olad unavailable"): await backend.send_frame(frame) failed_status = backend.get_status() assert failed_status.connected is False assert failed_status.degraded is True assert failed_status.send_errors == 1 assert failed_status.frames_sent == 0 await backend.send_frame(frame) recovered_status = backend.get_status() assert recovered_status.connected is True assert recovered_status.degraded is False assert recovered_status.reconnect_count == 1 assert recovered_status.frames_sent == 1 assert recovered_status.send_errors == 1 assert len(created_adapters) == 2 assert created_adapters[0].close_calls == 1 assert created_adapters[1].sent_frames[0][1][0] == 128 @pytest.mark.asyncio async def test_ola_backend_does_not_send_random_black_frame_on_reconnect() -> None: created_adapters: list[FakeOlaClientAdapter] = [] def factory() -> FakeOlaClientAdapter: adapter = FakeOlaClientAdapter( send_results=[RuntimeError("send failed")] if not created_adapters else [], ) created_adapters.append(adapter) return adapter backend = OlaDmxBackend( universe=3, output_port="usb-dmx-2", adapter_factory=factory, ) failed_frame = DmxFrame(universe=3) failed_frame.set_channel(1, 10, "effect") recovered_frame = DmxFrame(universe=3) recovered_frame.set_channel(1, 200, "effect") recovered_frame.set_channel(2, 150, "effect") with pytest.raises(RuntimeError, match="send failed"): await backend.send_frame(failed_frame) await backend.send_frame(recovered_frame) assert created_adapters[0].sent_frames[0][1][0] == 10 assert created_adapters[1].sent_frames[0][0] == 3 assert created_adapters[1].sent_frames[0][1] == recovered_frame.values assert any(value != 0 for value in created_adapters[1].sent_frames[0][1]) def test_python_ola_adapter_uses_buffer_with_tobytes() -> None: captured: dict[str, object] = {} class FakeClient: def SendDmx(self, universe: int, payload: object, callback: object) -> None: captured["universe"] = universe captured["payload_type"] = type(payload).__name__ captured["has_tobytes"] = hasattr(payload, "tobytes") callback(True) class FakeWrapper: def __init__(self) -> None: self._client = FakeClient() def Client(self) -> FakeClient: return self._client def Run(self) -> None: return def Stop(self) -> None: return adapter = PythonOlaClientAdapter(timeout_s=0.1) adapter._wrapper_class = FakeWrapper adapter.send_frame(1, [0, 255] + [0] * 510) assert captured["universe"] == 1 assert captured["payload_type"] == "array" assert captured["has_tobytes"] is True