test: bind hass fixture to the running test loop and remove listener registry private loop
This commit is contained in:
+11
-18
@@ -175,7 +175,6 @@ class ListenerRegistry:
|
|||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._listeners: dict[str, list[object]] = {}
|
self._listeners: dict[str, list[object]] = {}
|
||||||
self.reloaded: list[str] = []
|
self.reloaded: list[str] = []
|
||||||
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():
|
||||||
@@ -184,20 +183,15 @@ class ListenerRegistry:
|
|||||||
self._listeners.setdefault(entry_id, []).append(listener)
|
self._listeners.setdefault(entry_id, []).append(listener)
|
||||||
return _remove
|
return _remove
|
||||||
|
|
||||||
def dispatch_options_update(self, entry) -> None:
|
async def dispatch_options_update(self, entry) -> None:
|
||||||
"""Synchronous dispatch for sync tests (drives a private event loop)."""
|
"""Dispatch on the running loop; sync listeners run inline, async ones awaited."""
|
||||||
for listener in list(self._listeners.get(entry.entry_id, [])):
|
|
||||||
result = listener(self._hass, entry)
|
|
||||||
if inspect.isawaitable(result):
|
|
||||||
self._loop.run_until_complete(result)
|
|
||||||
|
|
||||||
async def async_dispatch_options_update(self, entry) -> None:
|
|
||||||
"""Async dispatch for pytest-asyncio tests (awaits listeners in the running loop)."""
|
|
||||||
for listener in list(self._listeners.get(entry.entry_id, [])):
|
for listener in list(self._listeners.get(entry.entry_id, [])):
|
||||||
result = listener(self._hass, entry)
|
result = listener(self._hass, entry)
|
||||||
if inspect.isawaitable(result):
|
if inspect.isawaitable(result):
|
||||||
await result
|
await result
|
||||||
|
|
||||||
|
async_dispatch_options_update = dispatch_options_update
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self) -> int:
|
def size(self) -> int:
|
||||||
return sum(len(listeners) for listeners in self._listeners.values())
|
return sum(len(listeners) for listeners in self._listeners.values())
|
||||||
@@ -250,20 +244,19 @@ def make_hass() -> tuple:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def hass():
|
async def hass():
|
||||||
"""Provide a standard-shaped hass fixture.
|
"""Provide a standard-shaped hass fixture bound to the running test loop.
|
||||||
|
|
||||||
Mirrors the surface of Home Assistant's ``hass`` fixture: a ``config``
|
Mirrors the surface of Home Assistant's ``hass`` fixture: a ``config``
|
||||||
namespace, a ``config_entries`` manager wired to a listener registry, a
|
namespace, a ``config_entries`` manager wired to a listener registry, a
|
||||||
``data`` dict for integration state, and the running event ``loop``.
|
``data`` dict for integration state, and a ``loop`` bound to the
|
||||||
|
pytest-asyncio loop the test itself runs on (HA's running-loop contract),
|
||||||
|
so code scheduling on ``hass.loop`` runs on the actual test loop.
|
||||||
"""
|
"""
|
||||||
_hass, registry = make_hass()
|
_hass, registry = make_hass()
|
||||||
_hass.registry = registry
|
_hass.registry = registry
|
||||||
_hass.loop = asyncio.new_event_loop()
|
_hass.loop = asyncio.get_running_loop()
|
||||||
try:
|
yield _hass
|
||||||
yield _hass
|
|
||||||
finally:
|
|
||||||
_hass.loop.close()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
@@ -44,3 +46,41 @@ def test_mock_only_harness_contract(monkeypatch):
|
|||||||
monkeypatch.setitem(sys.modules, "homeassistant", MagicMock(name="real-module"))
|
monkeypatch.setitem(sys.modules, "homeassistant", MagicMock(name="real-module"))
|
||||||
with pytest.raises(pytest.UsageError, match="mock-only"):
|
with pytest.raises(pytest.UsageError, match="mock-only"):
|
||||||
_ensure_mock_only()
|
_ensure_mock_only()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_hass_and_registry_run_on_the_running_test_loop(hass, make_entry):
|
||||||
|
"""hass.loop is the test's own loop and the registry spins no private loop."""
|
||||||
|
# hass.loop is the very loop the async test runs on (HA running-loop
|
||||||
|
# contract): code scheduling on hass.loop lands on the test loop itself.
|
||||||
|
assert hass.loop is asyncio.get_running_loop()
|
||||||
|
ran = []
|
||||||
|
|
||||||
|
async def _scheduled():
|
||||||
|
ran.append(True)
|
||||||
|
|
||||||
|
await hass.loop.create_task(_scheduled())
|
||||||
|
assert ran == [True]
|
||||||
|
|
||||||
|
# ListenerRegistry never spins its own event loop (no resource leak) and
|
||||||
|
# dispatch is a plain coroutine: sync listeners run inline, async awaited.
|
||||||
|
registry = hass.registry
|
||||||
|
assert not hasattr(registry, "_loop")
|
||||||
|
assert inspect.iscoroutinefunction(registry.dispatch_options_update)
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def _sync_listener(h, entry):
|
||||||
|
calls.append(("sync", entry.entry_id, h is hass))
|
||||||
|
|
||||||
|
entry = make_entry(entry_id="disp")
|
||||||
|
registry.add("disp", _sync_listener)
|
||||||
|
await registry.dispatch_options_update(entry)
|
||||||
|
assert calls == [("sync", "disp", True)]
|
||||||
|
|
||||||
|
async def _async_listener(h, entry):
|
||||||
|
calls.append(("async", entry.entry_id))
|
||||||
|
|
||||||
|
registry.add("disp", _async_listener)
|
||||||
|
calls.clear()
|
||||||
|
await registry.async_dispatch_options_update(entry)
|
||||||
|
assert calls == [("sync", "disp", True), ("async", "disp")]
|
||||||
@@ -271,7 +271,7 @@ def test_options_flow_create_entry_data_lands_in_entry_options_and_reloads_once(
|
|||||||
assert result["type"] == "create_entry"
|
assert result["type"] == "create_entry"
|
||||||
entry.options = dict(result["data"])
|
entry.options = dict(result["data"])
|
||||||
|
|
||||||
registry.dispatch_options_update(entry)
|
_run(registry.dispatch_options_update(entry))
|
||||||
assert hass.config_entries.async_reload.call_count == 1
|
assert hass.config_entries.async_reload.call_count == 1
|
||||||
assert registry.reloaded == [entry.entry_id]
|
assert registry.reloaded == [entry.entry_id]
|
||||||
assert entry.options == VALID_INPUT
|
assert entry.options == VALID_INPUT
|
||||||
|
|||||||
Reference in New Issue
Block a user