fix: raise minimum HA version for runtime_data and model listener accumulation faithfully in tests
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "NestQuest",
|
"name": "NestQuest",
|
||||||
"homeassistant": "2024.1.0",
|
"homeassistant": "2024.6.0",
|
||||||
"render_readme": true
|
"render_readme": true
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -25,10 +25,16 @@ def test_hacs_json_exists_and_is_valid(hacs_config: dict) -> None:
|
|||||||
"""Verify hacs.json exists and is valid JSON."""
|
"""Verify hacs.json exists and is valid JSON."""
|
||||||
assert isinstance(hacs_config, dict)
|
assert isinstance(hacs_config, dict)
|
||||||
assert hacs_config.get("name") == "NestQuest"
|
assert hacs_config.get("name") == "NestQuest"
|
||||||
assert hacs_config.get("homeassistant") == "2024.1.0"
|
assert hacs_config.get("homeassistant") == "2024.6.0"
|
||||||
assert hacs_config.get("render_readme") is True
|
assert hacs_config.get("render_readme") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_hacs_json_minimum_version_supports_runtime_data(hacs_config: dict) -> None:
|
||||||
|
"""Advertised HA floor must support ConfigEntry.runtime_data (added in 2024.6)."""
|
||||||
|
advertised = tuple(int(part) for part in hacs_config.get("homeassistant", "").split("."))
|
||||||
|
assert advertised >= (2024, 6, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_license_exists_and_non_empty() -> None:
|
def test_license_exists_and_non_empty() -> None:
|
||||||
"""Verify LICENSE file exists and is non-empty."""
|
"""Verify LICENSE file exists and is non-empty."""
|
||||||
assert LICENSE_PATH.exists(), f"LICENSE file does not exist at {LICENSE_PATH}"
|
assert LICENSE_PATH.exists(), f"LICENSE file does not exist at {LICENSE_PATH}"
|
||||||
|
|||||||
+31
-7
@@ -30,31 +30,30 @@ def _run(coro):
|
|||||||
|
|
||||||
|
|
||||||
class _ListenerRegistry:
|
class _ListenerRegistry:
|
||||||
"""Models HA's update-listener bookkeeping: re-adding replaces, not accumulates."""
|
"""Models HA's update-listener bookkeeping: appends to a list per entry, with per-listener removers."""
|
||||||
|
|
||||||
def __init__(self, hass=None):
|
def __init__(self, hass=None):
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._listeners: dict[str, object] = {}
|
self._listeners: dict[str, list[object]] = {}
|
||||||
self.reloaded: list[str] = []
|
self.reloaded: list[str] = []
|
||||||
self._loop = asyncio.new_event_loop()
|
self._loop = asyncio.new_event_loop()
|
||||||
|
|
||||||
def add(self, entry_id: str, listener) -> object:
|
def add(self, entry_id: str, listener) -> object:
|
||||||
def _remove():
|
def _remove():
|
||||||
self._listeners.pop(entry_id, None)
|
self._listeners.get(entry_id, []).remove(listener)
|
||||||
|
|
||||||
self._listeners[entry_id] = listener
|
self._listeners.setdefault(entry_id, []).append(listener)
|
||||||
return _remove
|
return _remove
|
||||||
|
|
||||||
def dispatch_options_update(self, entry) -> None:
|
def dispatch_options_update(self, entry) -> None:
|
||||||
listener = self._listeners.get(entry.entry_id)
|
for listener in list(self._listeners.get(entry.entry_id, [])):
|
||||||
if listener is not None:
|
|
||||||
result = listener(self._hass, entry)
|
result = listener(self._hass, entry)
|
||||||
if inspect.isawaitable(result):
|
if inspect.isawaitable(result):
|
||||||
self._loop.run_until_complete(result)
|
self._loop.run_until_complete(result)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self) -> int:
|
def size(self) -> int:
|
||||||
return len(self._listeners)
|
return sum(len(listeners) for listeners in self._listeners.values())
|
||||||
|
|
||||||
|
|
||||||
def _make_hass():
|
def _make_hass():
|
||||||
@@ -176,6 +175,31 @@ def test_three_reload_cycles_keep_single_listener() -> None:
|
|||||||
assert hass.config_entries.async_reload.call_count == 0
|
assert hass.config_entries.async_reload.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_three_setup_unload_cycles_leave_no_listeners() -> None:
|
||||||
|
"""Listeners accumulate per entry (HA semantics); unload must remove them all."""
|
||||||
|
hass, registry = _make_hass()
|
||||||
|
entry = _wire_entry(_make_entry(), registry)
|
||||||
|
for _ in range(3):
|
||||||
|
_run(async_setup_entry(hass, entry))
|
||||||
|
_run(async_unload_entry(hass, entry))
|
||||||
|
assert registry._listeners.get(entry.entry_id, []) == []
|
||||||
|
assert registry.size == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_options_update_after_each_setup_invokes_exactly_one_listener() -> None:
|
||||||
|
"""After each setup, an options update dispatches to exactly one listener."""
|
||||||
|
hass, registry = _make_hass()
|
||||||
|
entry = _wire_entry(_make_entry(), registry)
|
||||||
|
for _ in range(3):
|
||||||
|
_run(async_setup_entry(hass, entry))
|
||||||
|
registry.dispatch_options_update(entry)
|
||||||
|
assert hass.config_entries.async_reload.call_count == 1
|
||||||
|
assert registry.reloaded == [entry.entry_id]
|
||||||
|
_run(async_unload_entry(hass, entry))
|
||||||
|
hass.config_entries.async_reload.reset_mock()
|
||||||
|
registry.reloaded.clear()
|
||||||
|
|
||||||
|
|
||||||
def test_reload_setup_cycles_dispatch_reload_each_time() -> None:
|
def test_reload_setup_cycles_dispatch_reload_each_time() -> None:
|
||||||
hass, registry = _make_hass()
|
hass, registry = _make_hass()
|
||||||
entry = _wire_entry(_make_entry(), registry)
|
entry = _wire_entry(_make_entry(), registry)
|
||||||
|
|||||||
Reference in New Issue
Block a user