Migrate emulated_roku to use runtime_data to fix flakey tests (#141795)

This commit is contained in:
J. Nick Koston 2025-03-29 23:55:58 -10:00 committed by GitHub
parent beb92a7f9c
commit 65261de7cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 67 additions and 68 deletions

View File

@ -46,6 +46,8 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
type EmulatedRokuConfigEntry = ConfigEntry[EmulatedRoku]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the emulated roku component.""" """Set up the emulated roku component."""
@ -65,22 +67,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_setup_entry(
hass: HomeAssistant, entry: EmulatedRokuConfigEntry
) -> bool:
"""Set up an emulated roku server from a config entry.""" """Set up an emulated roku server from a config entry."""
config = config_entry.data config = entry.data
name: str = config[CONF_NAME]
if DOMAIN not in hass.data: listen_port: int = config[CONF_LISTEN_PORT]
hass.data[DOMAIN] = {} host_ip: str = config.get(CONF_HOST_IP) or await async_get_source_ip(hass)
advertise_ip: str | None = config.get(CONF_ADVERTISE_IP)
name = config[CONF_NAME] advertise_port: int | None = config.get(CONF_ADVERTISE_PORT)
listen_port = config[CONF_LISTEN_PORT] upnp_bind_multicast: bool | None = config.get(CONF_UPNP_BIND_MULTICAST)
host_ip = config.get(CONF_HOST_IP) or await async_get_source_ip(hass)
advertise_ip = config.get(CONF_ADVERTISE_IP)
advertise_port = config.get(CONF_ADVERTISE_PORT)
upnp_bind_multicast = config.get(CONF_UPNP_BIND_MULTICAST)
server = EmulatedRoku( server = EmulatedRoku(
hass, hass,
entry.entry_id,
name, name,
host_ip, host_ip,
listen_port, listen_port,
@ -88,14 +89,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
advertise_port, advertise_port,
upnp_bind_multicast, upnp_bind_multicast,
) )
entry.runtime_data = server
hass.data[DOMAIN][name] = server
return await server.setup() return await server.setup()
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(
hass: HomeAssistant, entry: EmulatedRokuConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
name = entry.data[CONF_NAME] return await entry.runtime_data.unload()
server = hass.data[DOMAIN].pop(name)
return await server.unload()

View File

@ -5,7 +5,13 @@ import logging
from emulated_roku import EmulatedRokuCommandHandler, EmulatedRokuServer from emulated_roku import EmulatedRokuCommandHandler, EmulatedRokuServer
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import CoreState, EventOrigin from homeassistant.core import (
CALLBACK_TYPE,
CoreState,
Event,
EventOrigin,
HomeAssistant,
)
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)
@ -27,16 +33,18 @@ class EmulatedRoku:
def __init__( def __init__(
self, self,
hass, hass: HomeAssistant,
name, entry_id: str,
host_ip, name: str,
listen_port, host_ip: str,
advertise_ip, listen_port: int,
advertise_port, advertise_ip: str | None,
upnp_bind_multicast, advertise_port: int | None,
): upnp_bind_multicast: bool | None,
) -> None:
"""Initialize the properties.""" """Initialize the properties."""
self.hass = hass self.hass = hass
self.entry_id = entry_id
self.roku_usn = name self.roku_usn = name
self.host_ip = host_ip self.host_ip = host_ip
@ -47,21 +55,21 @@ class EmulatedRoku:
self.bind_multicast = upnp_bind_multicast self.bind_multicast = upnp_bind_multicast
self._api_server = None self._api_server: EmulatedRokuServer | None = None
self._unsub_start_listener = None self._unsub_start_listener: CALLBACK_TYPE | None = None
self._unsub_stop_listener = None self._unsub_stop_listener: CALLBACK_TYPE | None = None
async def setup(self): async def setup(self) -> bool:
"""Start the emulated_roku server.""" """Start the emulated_roku server."""
class EventCommandHandler(EmulatedRokuCommandHandler): class EventCommandHandler(EmulatedRokuCommandHandler):
"""emulated_roku command handler to turn commands into events.""" """emulated_roku command handler to turn commands into events."""
def __init__(self, hass): def __init__(self, hass: HomeAssistant) -> None:
self.hass = hass self.hass = hass
def on_keydown(self, roku_usn, key): def on_keydown(self, roku_usn: str, key: str) -> None:
"""Handle keydown event.""" """Handle keydown event."""
self.hass.bus.async_fire( self.hass.bus.async_fire(
EVENT_ROKU_COMMAND, EVENT_ROKU_COMMAND,
@ -73,7 +81,7 @@ class EmulatedRoku:
EventOrigin.local, EventOrigin.local,
) )
def on_keyup(self, roku_usn, key): def on_keyup(self, roku_usn: str, key: str) -> None:
"""Handle keyup event.""" """Handle keyup event."""
self.hass.bus.async_fire( self.hass.bus.async_fire(
EVENT_ROKU_COMMAND, EVENT_ROKU_COMMAND,
@ -85,7 +93,7 @@ class EmulatedRoku:
EventOrigin.local, EventOrigin.local,
) )
def on_keypress(self, roku_usn, key): def on_keypress(self, roku_usn: str, key: str) -> None:
"""Handle keypress event.""" """Handle keypress event."""
self.hass.bus.async_fire( self.hass.bus.async_fire(
EVENT_ROKU_COMMAND, EVENT_ROKU_COMMAND,
@ -97,7 +105,7 @@ class EmulatedRoku:
EventOrigin.local, EventOrigin.local,
) )
def launch(self, roku_usn, app_id): def launch(self, roku_usn: str, app_id: str) -> None:
"""Handle launch event.""" """Handle launch event."""
self.hass.bus.async_fire( self.hass.bus.async_fire(
EVENT_ROKU_COMMAND, EVENT_ROKU_COMMAND,
@ -129,17 +137,19 @@ class EmulatedRoku:
bind_multicast=self.bind_multicast, bind_multicast=self.bind_multicast,
) )
async def emulated_roku_stop(event): async def emulated_roku_stop(event: Event | None) -> None:
"""Wrap the call to emulated_roku.close.""" """Wrap the call to emulated_roku.close."""
LOGGER.debug("Stopping emulated_roku %s", self.roku_usn) LOGGER.debug("Stopping emulated_roku %s", self.roku_usn)
self._unsub_stop_listener = None self._unsub_stop_listener = None
assert self._api_server is not None
await self._api_server.close() await self._api_server.close()
async def emulated_roku_start(event): async def emulated_roku_start(event: Event | None) -> None:
"""Wrap the call to emulated_roku.start.""" """Wrap the call to emulated_roku.start."""
try: try:
LOGGER.debug("Starting emulated_roku %s", self.roku_usn) LOGGER.debug("Starting emulated_roku %s", self.roku_usn)
self._unsub_start_listener = None self._unsub_start_listener = None
assert self._api_server is not None
await self._api_server.start() await self._api_server.start()
except OSError: except OSError:
LOGGER.exception( LOGGER.exception(
@ -165,7 +175,7 @@ class EmulatedRoku:
return True return True
async def unload(self): async def unload(self) -> bool:
"""Unload the emulated_roku server.""" """Unload the emulated_roku server."""
LOGGER.debug("Unloading emulated_roku %s", self.roku_usn) LOGGER.debug("Unloading emulated_roku %s", self.roku_usn)
@ -177,6 +187,7 @@ class EmulatedRoku:
self._unsub_stop_listener() self._unsub_stop_listener()
self._unsub_stop_listener = None self._unsub_stop_listener = None
assert self._api_server is not None
await self._api_server.close() await self._api_server.close()
return True return True

View File

@ -1,6 +1,7 @@
"""Tests for emulated_roku library bindings.""" """Tests for emulated_roku library bindings."""
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
from uuid import uuid4
from homeassistant.components.emulated_roku.binding import ( from homeassistant.components.emulated_roku.binding import (
ATTR_APP_ID, ATTR_APP_ID,
@ -14,14 +15,15 @@ from homeassistant.components.emulated_roku.binding import (
ROKU_COMMAND_LAUNCH, ROKU_COMMAND_LAUNCH,
EmulatedRoku, EmulatedRoku,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import Event, HomeAssistant
async def test_events_fired_properly(hass: HomeAssistant) -> None: async def test_events_fired_properly(hass: HomeAssistant) -> None:
"""Test that events are fired correctly.""" """Test that events are fired correctly."""
binding = EmulatedRoku( random_name = uuid4().hex
hass, "Test Emulated Roku", "1.2.3.4", 8060, None, None, None # Note that this test is accessing the internal EmulatedRoku class
) # and should be refactored in the future not to do so.
binding = EmulatedRoku(hass, "x", random_name, "1.2.3.4", 8060, None, None, None)
events = [] events = []
roku_event_handler = None roku_event_handler = None
@ -41,7 +43,8 @@ async def test_events_fired_properly(hass: HomeAssistant) -> None:
return Mock(start=AsyncMock(), close=AsyncMock()) return Mock(start=AsyncMock(), close=AsyncMock())
def listener(event): def listener(event: Event) -> None:
if event.data[ATTR_SOURCE_NAME] == random_name:
events.append(event) events.append(event)
with patch( with patch(
@ -53,10 +56,10 @@ async def test_events_fired_properly(hass: HomeAssistant) -> None:
assert roku_event_handler is not None assert roku_event_handler is not None
roku_event_handler.on_keydown("Test Emulated Roku", "A") roku_event_handler.on_keydown(random_name, "A")
roku_event_handler.on_keyup("Test Emulated Roku", "A") roku_event_handler.on_keyup(random_name, "A")
roku_event_handler.on_keypress("Test Emulated Roku", "C") roku_event_handler.on_keypress(random_name, "C")
roku_event_handler.launch("Test Emulated Roku", "1") roku_event_handler.launch(random_name, "1")
await hass.async_block_till_done() await hass.async_block_till_done()
@ -64,20 +67,20 @@ async def test_events_fired_properly(hass: HomeAssistant) -> None:
assert events[0].event_type == EVENT_ROKU_COMMAND assert events[0].event_type == EVENT_ROKU_COMMAND
assert events[0].data[ATTR_COMMAND_TYPE] == ROKU_COMMAND_KEYDOWN assert events[0].data[ATTR_COMMAND_TYPE] == ROKU_COMMAND_KEYDOWN
assert events[0].data[ATTR_SOURCE_NAME] == "Test Emulated Roku" assert events[0].data[ATTR_SOURCE_NAME] == random_name
assert events[0].data[ATTR_KEY] == "A" assert events[0].data[ATTR_KEY] == "A"
assert events[1].event_type == EVENT_ROKU_COMMAND assert events[1].event_type == EVENT_ROKU_COMMAND
assert events[1].data[ATTR_COMMAND_TYPE] == ROKU_COMMAND_KEYUP assert events[1].data[ATTR_COMMAND_TYPE] == ROKU_COMMAND_KEYUP
assert events[1].data[ATTR_SOURCE_NAME] == "Test Emulated Roku" assert events[1].data[ATTR_SOURCE_NAME] == random_name
assert events[1].data[ATTR_KEY] == "A" assert events[1].data[ATTR_KEY] == "A"
assert events[2].event_type == EVENT_ROKU_COMMAND assert events[2].event_type == EVENT_ROKU_COMMAND
assert events[2].data[ATTR_COMMAND_TYPE] == ROKU_COMMAND_KEYPRESS assert events[2].data[ATTR_COMMAND_TYPE] == ROKU_COMMAND_KEYPRESS
assert events[2].data[ATTR_SOURCE_NAME] == "Test Emulated Roku" assert events[2].data[ATTR_SOURCE_NAME] == random_name
assert events[2].data[ATTR_KEY] == "C" assert events[2].data[ATTR_KEY] == "C"
assert events[3].event_type == EVENT_ROKU_COMMAND assert events[3].event_type == EVENT_ROKU_COMMAND
assert events[3].data[ATTR_COMMAND_TYPE] == ROKU_COMMAND_LAUNCH assert events[3].data[ATTR_COMMAND_TYPE] == ROKU_COMMAND_LAUNCH
assert events[3].data[ATTR_SOURCE_NAME] == "Test Emulated Roku" assert events[3].data[ATTR_SOURCE_NAME] == random_name
assert events[3].data[ATTR_APP_ID] == "1" assert events[3].data[ATTR_APP_ID] == "1"

View File

@ -86,16 +86,6 @@ async def test_setup_entry_successful(hass: HomeAssistant) -> None:
assert await emulated_roku.async_setup_entry(hass, entry) is True assert await emulated_roku.async_setup_entry(hass, entry) is True
assert len(instantiate.mock_calls) == 1 assert len(instantiate.mock_calls) == 1
assert hass.data[emulated_roku.DOMAIN]
roku_instance = hass.data[emulated_roku.DOMAIN]["Emulated Roku Test"]
assert roku_instance.roku_usn == "Emulated Roku Test"
assert roku_instance.host_ip == "1.2.3.5"
assert roku_instance.listen_port == 8060
assert roku_instance.advertise_ip == "1.2.3.4"
assert roku_instance.advertise_port == 8071
assert roku_instance.bind_multicast is False
async def test_unload_entry(hass: HomeAssistant) -> None: async def test_unload_entry(hass: HomeAssistant) -> None:
@ -113,10 +103,6 @@ async def test_unload_entry(hass: HomeAssistant) -> None:
): ):
assert await emulated_roku.async_setup_entry(hass, entry) is True assert await emulated_roku.async_setup_entry(hass, entry) is True
assert emulated_roku.DOMAIN in hass.data
await hass.async_block_till_done() await hass.async_block_till_done()
assert await emulated_roku.async_unload_entry(hass, entry) assert await emulated_roku.async_unload_entry(hass, entry)
assert len(hass.data[emulated_roku.DOMAIN]) == 0