Files
led-controller/tests/test_driver_delivery_wifi.py
Jimmy ace5770b3a refactor(api): complete fastapi migration and related features
Finish native FastAPI controllers, drop vendored microdot, and add
Wi-Fi driver runtime, beat SSE, simulated BPM, sequence playback
improvements, bridge ESP-NOW sources, UI updates, and tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 22:55:28 +12:00

462 lines
13 KiB
Python

"""Tests for dual-transport delivery (ESP-NOW bridge + Wi-Fi WebSocket) and Wi-Fi runtime."""
from __future__ import annotations
import asyncio
import json
import socket
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
from unittest.mock import AsyncMock
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SRC_PATH = PROJECT_ROOT / "src"
for p in (str(PROJECT_ROOT), str(SRC_PATH)):
if p in sys.path:
sys.path.remove(p)
sys.path.insert(0, p)
_models = sys.modules.get("models")
if _models is not None:
_mf = (getattr(_models, "__file__", "") or "").replace("\\", "/")
if "/tests/models" in _mf:
for key in list(sys.modules):
if key == "models" or key.startswith("models."):
del sys.modules[key]
from util.bridge_envelope import BROADCAST_MAC # noqa: E402
class FakeDevices:
def __init__(self, docs: Dict[str, Dict[str, Any]]):
self._docs = docs
def read(self, mac: str) -> Optional[Dict[str, Any]]:
return self._docs.get(mac)
def items(self):
return self._docs.items()
class RecordingBridge:
def __init__(self) -> None:
self.envelopes: List[Dict[str, Any]] = []
async def send(self, data, addr=None):
del addr
if isinstance(data, dict):
self.envelopes.append(data)
elif isinstance(data, str):
self.envelopes.append(json.loads(data))
return True
def mac_keys(self) -> List[str]:
keys: List[str] = []
for env in self.envelopes:
devs = env.get("dv") or env.get("devices") or {}
keys.extend(devs.keys())
return keys
@pytest.fixture
def bridge():
return RecordingBridge()
@pytest.fixture
def espnow_devices():
return FakeDevices(
{
"188b0e1560a8": {
"id": "188b0e1560a8",
"name": "esp-a",
"transport": "espnow",
"address": "188b0e1560a8",
},
"e8f60a16ea10": {
"id": "e8f60a16ea10",
"name": "esp-b",
"transport": "espnow",
"address": "e8f60a16ea10",
},
}
)
@pytest.fixture
def mixed_devices():
return FakeDevices(
{
"188b0e1560a8": {
"id": "188b0e1560a8",
"name": "esp-a",
"transport": "espnow",
"address": "188b0e1560a8",
},
"102030405060": {
"id": "102030405060",
"name": "wifi-a",
"transport": "wifi",
"address": "192.168.50.10",
},
}
)
def test_wifi_message_for_device_narrows_select():
from util.driver_delivery import _wifi_message_for_device
msg = json.dumps(
{"v": "1", "select": {"wifi-a": 0, "esp-a": 1}},
separators=(",", ":"),
)
narrowed = _wifi_message_for_device(msg, "wifi-a")
body = json.loads(narrowed)
assert body["select"] == {"wifi-a": 0}
def test_combine_preset_chunks_for_wifi():
from util.driver_delivery import _combine_preset_chunks_for_wifi
chunks = [
json.dumps({"v": "1", "presets": {"a": {"p": "on"}}}, separators=(",", ":")),
json.dumps(
{"v": "1", "presets": {"b": {"p": "blink"}}, "save": True, "default": "b"},
separators=(",", ":"),
),
]
combined = json.loads(_combine_preset_chunks_for_wifi(chunks))
assert combined["presets"]["a"]["p"] == "on"
assert combined["presets"]["b"]["p"] == "blink"
assert combined["save"] is True
assert combined["default"] == "b"
def test_deliver_json_broadcast_espnow_only(bridge, espnow_devices, monkeypatch):
from util import driver_delivery
wifi_sends: list[tuple[str, str]] = []
async def fake_wifi(ip, msg):
wifi_sends.append((ip, msg))
return True
monkeypatch.setattr(driver_delivery, "send_json_line_to_ip", fake_wifi)
async def _run():
return await driver_delivery.deliver_json_messages(
bridge,
[json.dumps({"v": "1", "select": ["off"]})],
None,
espnow_devices,
delay_s=0,
)
deliveries, n = asyncio.run(_run())
assert n == 1
assert deliveries >= 1
assert bridge.mac_keys() == [BROADCAST_MAC]
assert wifi_sends == []
def test_deliver_json_broadcast_includes_wifi(bridge, mixed_devices, monkeypatch):
from util import driver_delivery
wifi_sends: list[tuple[str, str]] = []
async def fake_wifi(ip, msg):
wifi_sends.append((ip, msg))
return True
monkeypatch.setattr(driver_delivery, "send_json_line_to_ip", fake_wifi)
async def _run():
return await driver_delivery.deliver_json_messages(
bridge,
[json.dumps({"v": "1", "select": ["off"]})],
None,
mixed_devices,
delay_s=0,
)
deliveries, _n = asyncio.run(_run())
assert deliveries >= 2
assert bridge.mac_keys() == [BROADCAST_MAC]
assert len(wifi_sends) == 1
assert wifi_sends[0][0] == "192.168.50.10"
def test_deliver_json_targeted_espnow_unicasts(bridge, espnow_devices, monkeypatch):
from util import driver_delivery
monkeypatch.setattr(
driver_delivery,
"send_json_line_to_ip",
AsyncMock(return_value=True),
)
async def _run():
return await driver_delivery.deliver_json_messages(
bridge,
[json.dumps({"v": "1", "select": ["2"]})],
["188b0e1560a8", "e8f60a16ea10"],
espnow_devices,
delay_s=0,
)
asyncio.run(_run())
keys = bridge.mac_keys()
assert "18:8b:0e:15:60:a8" in keys
assert "e8:f6:0a:16:ea:10" in keys
assert BROADCAST_MAC not in keys
def test_deliver_json_targeted_wifi_uses_websocket(bridge, mixed_devices, monkeypatch):
from util import driver_delivery
wifi_sends: list[tuple[str, str]] = []
async def fake_wifi(ip, msg):
wifi_sends.append((ip, msg))
return True
monkeypatch.setattr(driver_delivery, "send_json_line_to_ip", fake_wifi)
async def _run():
await driver_delivery.deliver_json_messages(
bridge,
[json.dumps({"v": "1", "select": {"wifi-a": 0}})],
["102030405060"],
mixed_devices,
delay_s=0,
)
asyncio.run(_run())
assert bridge.mac_keys() == []
assert len(wifi_sends) == 1
assert wifi_sends[0][0] == "192.168.50.10"
body = json.loads(wifi_sends[0][1])
assert body["select"] == {"wifi-a": 0}
def test_deliver_json_unicast_flag_wifi(bridge, mixed_devices, monkeypatch):
from util import driver_delivery
wifi_sends: list[str] = []
async def fake_wifi(ip, msg):
wifi_sends.append(msg)
return True
monkeypatch.setattr(driver_delivery, "send_json_line_to_ip", fake_wifi)
async def _run():
await driver_delivery.deliver_json_messages(
bridge,
[json.dumps({"v": "1", "b": 128})],
["102030405060"],
mixed_devices,
delay_s=0,
unicast=True,
)
asyncio.run(_run())
assert len(wifi_sends) == 1
assert bridge.mac_keys() == []
def test_deliver_preset_broadcast_then_per_device_wifi(
bridge, mixed_devices, monkeypatch
):
from util import driver_delivery
wifi_sends: list[str] = []
async def fake_wifi(ip, msg):
wifi_sends.append(msg)
return True
monkeypatch.setattr(driver_delivery, "send_json_line_to_ip", fake_wifi)
chunks = [
json.dumps(
{"v": "1", "presets": {"p1": {"p": "on"}}, "save": True},
separators=(",", ":"),
)
]
async def _run():
return await driver_delivery.deliver_preset_broadcast_then_per_device(
bridge,
chunks,
None,
mixed_devices,
default_id=None,
delay_s=0,
)
count = asyncio.run(_run())
assert count >= 2
assert bridge.mac_keys() == [BROADCAST_MAC]
assert len(wifi_sends) == 1
combined = json.loads(wifi_sends[0])
assert "p1" in combined["presets"]
def test_deliver_json_requires_bridge(monkeypatch):
from util import driver_delivery
import models.transport as transport_mod
monkeypatch.setattr(transport_mod, "get_current_bridge", lambda: None)
async def _run():
with pytest.raises(RuntimeError, match="Transport not configured"):
await driver_delivery.deliver_json_messages(
None, ["{}"], None, FakeDevices({}), delay_s=0
)
asyncio.run(_run())
def test_device_status_broadcaster_send_text():
from util.device_status_broadcaster import (
_ws_send_text,
broadcast_device_tcp_snapshot_to,
broadcast_device_tcp_status,
register_device_status_ws,
unregister_device_status_ws,
)
class StarletteLikeWS:
def __init__(self):
self.out: list[str] = []
async def send_text(self, msg: str):
self.out.append(msg)
class SendTextOnlyWS:
def __init__(self):
self.out: list[str] = []
async def send(self, msg: str):
self.out.append(msg)
async def _run():
starlette = StarletteLikeWS()
legacy_ws = SendTextOnlyWS()
await _ws_send_text(starlette, '{"ok":true}')
await _ws_send_text(legacy_ws, '{"ok":true}')
assert starlette.out == ['{"ok":true}']
assert legacy_ws.out == ['{"ok":true}']
await register_device_status_ws(starlette)
await broadcast_device_tcp_status("192.168.1.5", True)
assert len(starlette.out) == 2
status = json.loads(starlette.out[1])
assert status["type"] == "device_tcp"
assert status["ip"] == "192.168.1.5"
assert status["connected"] is True
await broadcast_device_tcp_snapshot_to(starlette)
snapshot = json.loads(starlette.out[2])
assert snapshot["type"] == "device_tcp_snapshot"
assert "connected_ips" in snapshot
await unregister_device_status_ws(starlette)
asyncio.run(_run())
def test_process_udp_datagram_registers_and_connects(monkeypatch):
from util import wifi_driver_runtime
registered: list[tuple[str, str, str]] = []
connected: list[str] = []
def fake_register(device_name, peer_ip, mac, device_type=None):
del device_type
registered.append((device_name, peer_ip, str(mac)))
monkeypatch.setattr(
wifi_driver_runtime,
"_register_udp_device_sync",
fake_register,
)
monkeypatch.setattr(
wifi_driver_runtime.tcp_client_registry,
"ensure_driver_connection",
lambda ip: connected.append(ip),
)
line = json.dumps(
{"v": "1", "device_name": "strip-a", "mac": "aabbccddeeff", "type": "led"}
).encode()
wifi_driver_runtime._process_udp_datagram(line, "192.168.1.42")
assert registered == [("strip-a", "192.168.1.42", "aabbccddeeff")]
assert connected == ["192.168.1.42"]
def test_process_udp_datagram_ignores_invalid():
from util.wifi_driver_runtime import _process_udp_datagram
_process_udp_datagram(b"not-json\n", "10.0.0.1")
_process_udp_datagram(b'{"v":"1"}\n', "10.0.0.1")
def test_discovery_protocol_uses_datagram_endpoint(monkeypatch):
pytest.importorskip("uvloop")
import uvloop
from util.wifi_driver_runtime import _DiscoveryProtocol
async def _run():
echoed: list[bytes] = []
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
holder: dict = {"closing": False}
loop = asyncio.get_running_loop()
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
transport, _protocol = await loop.create_datagram_endpoint(
lambda: _DiscoveryProtocol(holder),
sock=sock,
)
class _EchoClient(asyncio.DatagramProtocol):
def connection_made(self, t):
self._transport = t
def datagram_received(self, data, addr):
del addr
echoed.append(data)
client_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client_sock.bind(("127.0.0.1", 0))
client_transport, _ = await loop.create_datagram_endpoint(
_EchoClient,
sock=client_sock,
)
payload = b'{"v":"1","device_name":"x","mac":"112233445566"}\n'
client_transport.sendto(payload, ("127.0.0.1", port))
await asyncio.sleep(0.05)
holder["closing"] = True
client_transport.close()
transport.close()
return echoed, payload
monkeypatch.setattr(
"util.wifi_driver_runtime._register_udp_device_sync",
lambda *a, **k: None,
)
monkeypatch.setattr(
"util.wifi_driver_runtime.tcp_client_registry.ensure_driver_connection",
lambda _ip: None,
)
echoed, payload = asyncio.run(_run())
assert echoed == [payload]