refactor(devices): per-provider typed configs (phases 1-4)
Phase 1 — DeviceConfig hierarchy (device_config.py): - 17 @dataclass(frozen=True) subclasses (WLEDConfig, AdalightConfig, …) sharing BaseDeviceConfig; DeviceConfig = Union[all 17] - Device.to_config() in device_store.py: single flat→typed dispatch point Phase 2+3 — Typed provider signatures + call-site migration: - ProviderDeps(device_store) frozen dataclass in led_client.py - LEDDeviceProvider.create_client(config, *, deps) abstract signature - create_led_client(config, *, deps) factory dispatches via config.device_type - All 17 providers narrowed to their specific config type; drop kwargs.get() - GroupLEDClient.connect() uses device.to_config() + create_led_client() - wled_target_processor: replaced 21-field DeviceInfo unpacking with to_config() + dataclasses.replace(config, use_ddp=…) for DDP override - device_test_mode: build typed config via to_config() + ProviderDeps - Deleted DeviceInfo dataclass, _get_device_info(), _DEVICE_FIELD_DEFAULTS - TargetContext: replaced get_device_info callback with is_test_mode_active Phase 4 — Test migration: - 47-case test suite in tests/core/devices/test_device_config.py (100% coverage) - test_group_device.py TestGroupLEDClient migrated to GroupConfig + ProviderDeps - Removed legacy keyword-arg init path from GroupLEDClient
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
"""Unit tests for the per-provider DeviceConfig hierarchy and Device.to_config()."""
|
||||
|
||||
import pytest
|
||||
|
||||
from ledgrab.core.devices.device_config import (
|
||||
AdalightConfig,
|
||||
AmbiLEDConfig,
|
||||
BaseDeviceConfig,
|
||||
BLEConfig,
|
||||
ChromaConfig,
|
||||
DemoConfig,
|
||||
DMXConfig,
|
||||
ESPNowConfig,
|
||||
GameSenseConfig,
|
||||
GroupConfig,
|
||||
HueConfig,
|
||||
MockConfig,
|
||||
MQTTConfig,
|
||||
OpenRGBConfig,
|
||||
SPIConfig,
|
||||
USBHIDConfig,
|
||||
WLEDConfig,
|
||||
WSConfig,
|
||||
)
|
||||
from ledgrab.storage.device_store import Device
|
||||
|
||||
|
||||
def _make_device(**kwargs) -> Device:
|
||||
defaults = dict(
|
||||
device_id="dev_test01",
|
||||
name="Test Device",
|
||||
url="http://192.168.1.100",
|
||||
led_count=60,
|
||||
software_brightness=200,
|
||||
auto_shutdown=True,
|
||||
rgbw=True,
|
||||
baud_rate=115200,
|
||||
send_latency_ms=5,
|
||||
zone_mode="separate",
|
||||
dmx_protocol="sacn",
|
||||
dmx_start_universe=1,
|
||||
dmx_start_channel=2,
|
||||
espnow_peer_mac="AA:BB:CC:DD:EE:FF",
|
||||
espnow_channel=6,
|
||||
hue_username="hue_user",
|
||||
hue_client_key="hue_key",
|
||||
hue_entertainment_group_id="grp_1",
|
||||
spi_speed_hz=1600000,
|
||||
spi_led_type="APA102",
|
||||
chroma_device_type="keyboard",
|
||||
gamesense_device_type="mouse",
|
||||
ble_family="govee",
|
||||
ble_govee_key="secret_key",
|
||||
group_device_ids=["dev_a", "dev_b"],
|
||||
group_mode="independent",
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return Device(**defaults)
|
||||
|
||||
|
||||
class TestBaseDeviceConfigDefaults:
|
||||
def test_defaults(self):
|
||||
cfg = BaseDeviceConfig(device_id="d1", device_url="http://x", led_count=10)
|
||||
assert cfg.software_brightness == 255
|
||||
assert cfg.test_mode_active is False
|
||||
assert cfg.auto_shutdown is False
|
||||
assert cfg.rgbw is False
|
||||
|
||||
def test_frozen(self):
|
||||
cfg = BaseDeviceConfig(device_id="d1", device_url="http://x", led_count=10)
|
||||
with pytest.raises(Exception):
|
||||
cfg.led_count = 20 # type: ignore[misc]
|
||||
|
||||
|
||||
class TestWLEDConfig:
|
||||
def test_to_config_returns_wled(self):
|
||||
device = _make_device(device_type="wled")
|
||||
cfg = device.to_config()
|
||||
assert isinstance(cfg, WLEDConfig)
|
||||
assert cfg.device_type == "wled"
|
||||
|
||||
def test_base_fields_mapped(self):
|
||||
device = _make_device(device_type="wled")
|
||||
cfg = device.to_config()
|
||||
assert cfg.device_id == "dev_test01"
|
||||
assert cfg.device_url == "http://192.168.1.100"
|
||||
assert cfg.led_count == 60
|
||||
assert cfg.software_brightness == 200
|
||||
assert cfg.auto_shutdown is True
|
||||
assert cfg.rgbw is True
|
||||
|
||||
def test_use_ddp_defaults_false(self):
|
||||
cfg = _make_device(device_type="wled").to_config()
|
||||
assert cfg.use_ddp is False # type: ignore[union-attr]
|
||||
|
||||
def test_test_mode_active_defaults_false(self):
|
||||
# test_mode_active is runtime state, not stored on Device
|
||||
cfg = _make_device(device_type="wled").to_config()
|
||||
assert cfg.test_mode_active is False
|
||||
|
||||
def test_wled_has_no_baud_rate(self):
|
||||
cfg = _make_device(device_type="wled").to_config()
|
||||
assert not hasattr(cfg, "baud_rate")
|
||||
|
||||
def test_wled_has_no_ble_fields(self):
|
||||
cfg = _make_device(device_type="wled").to_config()
|
||||
assert not hasattr(cfg, "ble_family")
|
||||
assert not hasattr(cfg, "ble_govee_key")
|
||||
|
||||
|
||||
class TestAdalightConfig:
|
||||
def test_to_config_returns_adalight(self):
|
||||
cfg = _make_device(device_type="adalight").to_config()
|
||||
assert isinstance(cfg, AdalightConfig)
|
||||
assert cfg.device_type == "adalight"
|
||||
|
||||
def test_baud_rate_mapped(self):
|
||||
cfg = _make_device(device_type="adalight").to_config()
|
||||
assert cfg.baud_rate == 115200 # type: ignore[union-attr]
|
||||
|
||||
def test_adalight_has_no_dmx_fields(self):
|
||||
cfg = _make_device(device_type="adalight").to_config()
|
||||
assert not hasattr(cfg, "dmx_protocol")
|
||||
|
||||
|
||||
class TestAmbiLEDConfig:
|
||||
def test_to_config_returns_ambiled(self):
|
||||
cfg = _make_device(device_type="ambiled").to_config()
|
||||
assert isinstance(cfg, AmbiLEDConfig)
|
||||
assert cfg.device_type == "ambiled"
|
||||
|
||||
def test_baud_rate_mapped(self):
|
||||
cfg = _make_device(device_type="ambiled").to_config()
|
||||
assert cfg.baud_rate == 115200 # type: ignore[union-attr]
|
||||
|
||||
|
||||
class TestDMXConfig:
|
||||
def test_to_config_returns_dmx(self):
|
||||
cfg = _make_device(device_type="dmx").to_config()
|
||||
assert isinstance(cfg, DMXConfig)
|
||||
assert cfg.device_type == "dmx"
|
||||
|
||||
def test_dmx_fields_mapped(self):
|
||||
cfg = _make_device(device_type="dmx").to_config()
|
||||
assert cfg.dmx_protocol == "sacn" # type: ignore[union-attr]
|
||||
assert cfg.dmx_start_universe == 1 # type: ignore[union-attr]
|
||||
assert cfg.dmx_start_channel == 2 # type: ignore[union-attr]
|
||||
|
||||
def test_dmx_has_no_baud_rate(self):
|
||||
cfg = _make_device(device_type="dmx").to_config()
|
||||
assert not hasattr(cfg, "baud_rate")
|
||||
|
||||
|
||||
class TestESPNowConfig:
|
||||
def test_to_config_returns_espnow(self):
|
||||
cfg = _make_device(device_type="espnow").to_config()
|
||||
assert isinstance(cfg, ESPNowConfig)
|
||||
assert cfg.device_type == "espnow"
|
||||
|
||||
def test_espnow_fields_mapped(self):
|
||||
cfg = _make_device(device_type="espnow").to_config()
|
||||
assert cfg.baud_rate == 115200 # type: ignore[union-attr]
|
||||
assert cfg.espnow_peer_mac == "AA:BB:CC:DD:EE:FF" # type: ignore[union-attr]
|
||||
assert cfg.espnow_channel == 6 # type: ignore[union-attr]
|
||||
|
||||
|
||||
class TestHueConfig:
|
||||
def test_to_config_returns_hue(self):
|
||||
cfg = _make_device(device_type="hue").to_config()
|
||||
assert isinstance(cfg, HueConfig)
|
||||
assert cfg.device_type == "hue"
|
||||
|
||||
def test_hue_fields_mapped(self):
|
||||
cfg = _make_device(device_type="hue").to_config()
|
||||
assert cfg.hue_username == "hue_user" # type: ignore[union-attr]
|
||||
assert cfg.hue_client_key == "hue_key" # type: ignore[union-attr]
|
||||
assert cfg.hue_entertainment_group_id == "grp_1" # type: ignore[union-attr]
|
||||
|
||||
|
||||
class TestSPIConfig:
|
||||
def test_to_config_returns_spi(self):
|
||||
cfg = _make_device(device_type="spi").to_config()
|
||||
assert isinstance(cfg, SPIConfig)
|
||||
assert cfg.device_type == "spi"
|
||||
|
||||
def test_spi_fields_mapped(self):
|
||||
cfg = _make_device(device_type="spi").to_config()
|
||||
assert cfg.spi_speed_hz == 1600000 # type: ignore[union-attr]
|
||||
assert cfg.spi_led_type == "APA102" # type: ignore[union-attr]
|
||||
|
||||
|
||||
class TestChromaConfig:
|
||||
def test_to_config_returns_chroma(self):
|
||||
cfg = _make_device(device_type="chroma").to_config()
|
||||
assert isinstance(cfg, ChromaConfig)
|
||||
assert cfg.device_type == "chroma"
|
||||
|
||||
def test_chroma_device_type_mapped(self):
|
||||
cfg = _make_device(device_type="chroma").to_config()
|
||||
assert cfg.chroma_device_type == "keyboard" # type: ignore[union-attr]
|
||||
|
||||
|
||||
class TestGameSenseConfig:
|
||||
def test_to_config_returns_gamesense(self):
|
||||
cfg = _make_device(device_type="gamesense").to_config()
|
||||
assert isinstance(cfg, GameSenseConfig)
|
||||
assert cfg.device_type == "gamesense"
|
||||
|
||||
def test_gamesense_device_type_mapped(self):
|
||||
cfg = _make_device(device_type="gamesense").to_config()
|
||||
assert cfg.gamesense_device_type == "mouse" # type: ignore[union-attr]
|
||||
|
||||
|
||||
class TestBLEConfig:
|
||||
def test_to_config_returns_ble(self):
|
||||
cfg = _make_device(device_type="ble").to_config()
|
||||
assert isinstance(cfg, BLEConfig)
|
||||
assert cfg.device_type == "ble"
|
||||
|
||||
def test_ble_fields_mapped(self):
|
||||
cfg = _make_device(device_type="ble").to_config()
|
||||
assert cfg.ble_family == "govee" # type: ignore[union-attr]
|
||||
assert cfg.ble_govee_key == "secret_key" # type: ignore[union-attr]
|
||||
|
||||
def test_ble_has_no_dmx_fields(self):
|
||||
cfg = _make_device(device_type="ble").to_config()
|
||||
assert not hasattr(cfg, "dmx_protocol")
|
||||
|
||||
|
||||
class TestGroupConfig:
|
||||
def test_to_config_returns_group(self):
|
||||
cfg = _make_device(device_type="group").to_config()
|
||||
assert isinstance(cfg, GroupConfig)
|
||||
assert cfg.device_type == "group"
|
||||
|
||||
def test_group_fields_mapped(self):
|
||||
cfg = _make_device(device_type="group").to_config()
|
||||
assert cfg.group_mode == "independent" # type: ignore[union-attr]
|
||||
assert cfg.group_device_ids == ["dev_a", "dev_b"] # type: ignore[union-attr]
|
||||
|
||||
def test_group_device_ids_is_copy(self):
|
||||
device = _make_device(device_type="group")
|
||||
cfg = device.to_config()
|
||||
# Modifying the device's list should not affect the config snapshot
|
||||
device.group_device_ids.append("dev_c")
|
||||
assert "dev_c" not in cfg.group_device_ids # type: ignore[union-attr]
|
||||
|
||||
def test_group_has_no_ble_fields(self):
|
||||
cfg = _make_device(device_type="group").to_config()
|
||||
assert not hasattr(cfg, "ble_family")
|
||||
|
||||
|
||||
class TestOpenRGBConfig:
|
||||
def test_to_config_returns_openrgb(self):
|
||||
cfg = _make_device(device_type="openrgb").to_config()
|
||||
assert isinstance(cfg, OpenRGBConfig)
|
||||
assert cfg.device_type == "openrgb"
|
||||
|
||||
def test_zone_mode_mapped(self):
|
||||
cfg = _make_device(device_type="openrgb").to_config()
|
||||
assert cfg.zone_mode == "separate" # type: ignore[union-attr]
|
||||
|
||||
|
||||
class TestMockConfig:
|
||||
def test_to_config_returns_mock(self):
|
||||
cfg = _make_device(device_type="mock").to_config()
|
||||
assert isinstance(cfg, MockConfig)
|
||||
assert cfg.device_type == "mock"
|
||||
|
||||
def test_send_latency_ms_mapped(self):
|
||||
cfg = _make_device(device_type="mock").to_config()
|
||||
assert cfg.send_latency_ms == 5 # type: ignore[union-attr]
|
||||
|
||||
|
||||
class TestDemoConfig:
|
||||
def test_to_config_returns_demo(self):
|
||||
cfg = _make_device(device_type="demo").to_config()
|
||||
assert isinstance(cfg, DemoConfig)
|
||||
assert cfg.device_type == "demo"
|
||||
|
||||
def test_send_latency_ms_mapped(self):
|
||||
cfg = _make_device(device_type="demo").to_config()
|
||||
assert cfg.send_latency_ms == 5 # type: ignore[union-attr]
|
||||
|
||||
|
||||
class TestMQTTConfig:
|
||||
def test_to_config_returns_mqtt(self):
|
||||
cfg = _make_device(device_type="mqtt").to_config()
|
||||
assert isinstance(cfg, MQTTConfig)
|
||||
assert cfg.device_type == "mqtt"
|
||||
|
||||
|
||||
class TestWSConfig:
|
||||
def test_to_config_returns_ws(self):
|
||||
cfg = _make_device(device_type="ws").to_config()
|
||||
assert isinstance(cfg, WSConfig)
|
||||
assert cfg.device_type == "ws"
|
||||
|
||||
|
||||
class TestUSBHIDConfig:
|
||||
def test_to_config_returns_usbhid(self):
|
||||
cfg = _make_device(device_type="usbhid").to_config()
|
||||
assert isinstance(cfg, USBHIDConfig)
|
||||
assert cfg.device_type == "usbhid"
|
||||
|
||||
|
||||
class TestUnknownDeviceType:
|
||||
def test_raises_on_unknown_type(self):
|
||||
device = _make_device(device_type="unknown_future_type")
|
||||
with pytest.raises(ValueError, match="Unknown device type"):
|
||||
device.to_config()
|
||||
|
||||
|
||||
class TestFieldIsolation:
|
||||
"""Irrelevant Device fields must not leak into the wrong config type."""
|
||||
|
||||
def test_wled_has_no_espnow_fields(self):
|
||||
cfg = _make_device(device_type="wled").to_config()
|
||||
assert not hasattr(cfg, "espnow_peer_mac")
|
||||
assert not hasattr(cfg, "espnow_channel")
|
||||
|
||||
def test_hue_has_no_spi_fields(self):
|
||||
cfg = _make_device(device_type="hue").to_config()
|
||||
assert not hasattr(cfg, "spi_speed_hz")
|
||||
assert not hasattr(cfg, "spi_led_type")
|
||||
|
||||
def test_adalight_has_no_group_fields(self):
|
||||
cfg = _make_device(device_type="adalight").to_config()
|
||||
assert not hasattr(cfg, "group_mode")
|
||||
assert not hasattr(cfg, "group_device_ids")
|
||||
|
||||
def test_mqtt_has_no_hue_fields(self):
|
||||
cfg = _make_device(device_type="mqtt").to_config()
|
||||
assert not hasattr(cfg, "hue_username")
|
||||
assert not hasattr(cfg, "hue_client_key")
|
||||
Reference in New Issue
Block a user