- parsers/registry.py: PARSER_REGISTRY mit shelly_pm-Eintrag; get_parser(), get_schema(), list_parser_types() als Public API - parsers/__init__.py: Registry-Funktionen exportiert - core/device_manager.py: globalen ShellyParser entfernt; DeviceManager verwaltet jetzt ein eigenes parser-Dict pro Device (per get_parser()) - api/server.py: GET /parsers Endpoint hinzugefügt (gibt get_schema() zurück) - tests/unit/test_parser_registry.py: 17 neue Tests (Registry-API, PARSER_REGISTRY-Integrität, DeviceManager-Integration) - tests/unit/test_device_manager.py: Test auf neues API angepasst (patch statt parser=-Argument) Tests: 63/63 passed
214 lines
8.2 KiB
Python
214 lines
8.2 KiB
Python
"""Unit tests for the Parser Registry."""
|
|
|
|
import pytest
|
|
|
|
from parsers.registry import PARSER_REGISTRY, get_parser, get_schema, list_parser_types
|
|
from parsers.shelly_parser import ShellyParser
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# get_parser
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_get_parser_shelly_pm_returns_shelly_parser_instance() -> None:
|
|
parser = get_parser("shelly_pm")
|
|
assert isinstance(parser, ShellyParser)
|
|
|
|
|
|
def test_get_parser_returns_new_instance_each_call() -> None:
|
|
p1 = get_parser("shelly_pm")
|
|
p2 = get_parser("shelly_pm")
|
|
assert p1 is not p2
|
|
|
|
|
|
def test_get_parser_unknown_type_raises_key_error() -> None:
|
|
with pytest.raises(KeyError, match="Unknown parser type"):
|
|
get_parser("does_not_exist")
|
|
|
|
|
|
def test_get_parser_error_message_contains_available_types() -> None:
|
|
with pytest.raises(KeyError) as exc_info:
|
|
get_parser("unknown_xyz")
|
|
assert "shelly_pm" in str(exc_info.value)
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# get_schema
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_get_schema_returns_dict() -> None:
|
|
schema = get_schema()
|
|
assert isinstance(schema, dict)
|
|
|
|
|
|
def test_get_schema_contains_shelly_pm() -> None:
|
|
schema = get_schema()
|
|
assert "shelly_pm" in schema
|
|
|
|
|
|
def test_get_schema_shelly_pm_has_required_fields() -> None:
|
|
entry = get_schema()["shelly_pm"]
|
|
assert "label" in entry
|
|
assert "description" in entry
|
|
assert "topic_hint" in entry
|
|
assert "data_unit" in entry
|
|
assert "parameters" in entry
|
|
|
|
|
|
def test_get_schema_does_not_expose_class() -> None:
|
|
"""The 'class' key must not leak into the public schema."""
|
|
schema = get_schema()
|
|
for entry in schema.values():
|
|
assert "class" not in entry
|
|
|
|
|
|
def test_get_schema_parameters_are_list() -> None:
|
|
schema = get_schema()
|
|
for entry in schema.values():
|
|
assert isinstance(entry["parameters"], list)
|
|
|
|
|
|
def test_get_schema_shelly_pm_parameters_have_required_keys() -> None:
|
|
params = get_schema()["shelly_pm"]["parameters"]
|
|
assert len(params) > 0
|
|
for param in params:
|
|
assert "name" in param
|
|
assert "label" in param
|
|
assert "type" in param
|
|
assert "default" in param
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# list_parser_types
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_list_parser_types_returns_sorted_list() -> None:
|
|
types = list_parser_types()
|
|
assert isinstance(types, list)
|
|
assert types == sorted(types)
|
|
|
|
|
|
def test_list_parser_types_contains_shelly_pm() -> None:
|
|
assert "shelly_pm" in list_parser_types()
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# PARSER_REGISTRY integrity
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_registry_all_entries_have_class() -> None:
|
|
for key, entry in PARSER_REGISTRY.items():
|
|
assert "class" in entry, f"Entry {key!r} missing 'class'"
|
|
|
|
|
|
def test_registry_all_classes_have_parse_message() -> None:
|
|
for key, entry in PARSER_REGISTRY.items():
|
|
cls = entry["class"]
|
|
assert hasattr(cls, "parse_message"), (
|
|
f"Parser class for {key!r} missing parse_message()"
|
|
)
|
|
|
|
|
|
def test_registry_all_classes_are_instantiable() -> None:
|
|
for key, entry in PARSER_REGISTRY.items():
|
|
cls = entry["class"]
|
|
instance = cls()
|
|
assert instance is not None
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Integration: DeviceManager uses registry per device
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_device_manager_creates_per_device_parser() -> None:
|
|
"""DeviceManager must store an independent parser per device_id."""
|
|
from unittest.mock import Mock
|
|
|
|
from api.models import BridgeConfig, DeviceConfig, SessionConfig
|
|
from core.device_manager import DeviceManager
|
|
|
|
mqtt_client = Mock()
|
|
event_queue = Mock()
|
|
manager = DeviceManager(mqtt_client=mqtt_client, event_queue=event_queue)
|
|
|
|
config = BridgeConfig(
|
|
mqtt=None,
|
|
devices=[
|
|
DeviceConfig(
|
|
device_id="dev-a",
|
|
machine_name="Machine A",
|
|
mqtt_topic="dev-a/status/pm1:0",
|
|
parser_type="shelly_pm",
|
|
device_status_timeout_s=120,
|
|
session_config=SessionConfig(
|
|
standby_threshold_w=20,
|
|
working_threshold_w=100,
|
|
start_debounce_s=3,
|
|
stop_debounce_s=15,
|
|
message_timeout_s=20,
|
|
heartbeat_interval_s=60,
|
|
),
|
|
),
|
|
DeviceConfig(
|
|
device_id="dev-b",
|
|
machine_name="Machine B",
|
|
mqtt_topic="dev-b/status/pm1:0",
|
|
parser_type="shelly_pm",
|
|
device_status_timeout_s=120,
|
|
session_config=SessionConfig(
|
|
standby_threshold_w=20,
|
|
working_threshold_w=100,
|
|
start_debounce_s=3,
|
|
stop_debounce_s=15,
|
|
message_timeout_s=20,
|
|
heartbeat_interval_s=60,
|
|
),
|
|
),
|
|
],
|
|
version="1.0",
|
|
)
|
|
|
|
manager.apply_config(config)
|
|
|
|
assert "dev-a" in manager.parsers
|
|
assert "dev-b" in manager.parsers
|
|
# Each device gets its own parser instance
|
|
assert manager.parsers["dev-a"] is not manager.parsers["dev-b"]
|
|
|
|
|
|
def test_device_manager_removes_parser_on_device_removal() -> None:
|
|
from unittest.mock import Mock
|
|
|
|
from api.models import BridgeConfig, DeviceConfig, SessionConfig
|
|
from core.device_manager import DeviceManager
|
|
|
|
mqtt_client = Mock()
|
|
event_queue = Mock()
|
|
manager = DeviceManager(mqtt_client=mqtt_client, event_queue=event_queue)
|
|
|
|
device = DeviceConfig(
|
|
device_id="dev-x",
|
|
machine_name="Machine X",
|
|
mqtt_topic="dev-x/status/pm1:0",
|
|
parser_type="shelly_pm",
|
|
device_status_timeout_s=120,
|
|
session_config=SessionConfig(
|
|
standby_threshold_w=20,
|
|
working_threshold_w=100,
|
|
start_debounce_s=3,
|
|
stop_debounce_s=15,
|
|
message_timeout_s=20,
|
|
heartbeat_interval_s=60,
|
|
),
|
|
)
|
|
manager.apply_config(BridgeConfig(mqtt=None, devices=[device], version="1.0"))
|
|
assert "dev-x" in manager.parsers
|
|
|
|
manager.apply_config(BridgeConfig(mqtt=None, devices=[], version="1.0"))
|
|
assert "dev-x" not in manager.parsers
|