From ce9505c9f598f34b4969718dbccce2a4b7785c25 Mon Sep 17 00:00:00 2001 From: Jason Lawrence Date: Tue, 7 Apr 2020 16:04:00 -0500 Subject: [PATCH 1/6] Improve Plex debounce/throttle logic --- homeassistant/components/plex/__init__.py | 13 +++++- homeassistant/components/plex/const.py | 2 + homeassistant/components/plex/server.py | 32 +++++++++++---- tests/components/plex/test_server.py | 50 +++++++++++++++++------ 4 files changed, 75 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index ff36f4f5c320ac..7c27220a09f25b 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -33,6 +33,8 @@ CONF_SERVER_IDENTIFIER, CONF_SHOW_ALL_CONTROLS, CONF_USE_EPISODE_ART, + DEBOUNCE_LAST_FIRED, + DEBOUNCE_UNSUB, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, @@ -84,7 +86,14 @@ async def async_setup(hass, config): """Set up the Plex component.""" hass.data.setdefault( PLEX_DOMAIN, - {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}, PLATFORMS_COMPLETED: {}}, + { + SERVERS: {}, + DISPATCHERS: {}, + WEBSOCKETS: {}, + PLATFORMS_COMPLETED: {}, + DEBOUNCE_LAST_FIRED: {}, + DEBOUNCE_UNSUB: {}, + }, ) plex_config = config.get(PLEX_DOMAIN, {}) @@ -179,6 +188,8 @@ async def async_setup_entry(hass, entry): ) hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, []) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + hass.data[PLEX_DOMAIN][DEBOUNCE_LAST_FIRED][server_id] = None + hass.data[PLEX_DOMAIN][DEBOUNCE_UNSUB][server_id] = None def update_plex(): async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 126c6eb313a454..3f79e39d341a55 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -9,7 +9,9 @@ DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True +DEBOUNCE_LAST_FIRED = "debounce_last_fired" DEBOUNCE_TIMEOUT = 1 +DEBOUNCE_UNSUB = "debounce_unsub" DISPATCHERS = "dispatchers" PLATFORMS = frozenset(["media_player", "sensor"]) PLATFORMS_COMPLETED = "platforms_completed" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 4134ad4e32b273..3840b898600d7b 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -15,6 +15,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later +import homeassistant.util.dt as dt_util from .const import ( CONF_CLIENT_IDENTIFIER, @@ -22,8 +23,11 @@ CONF_MONITORED_USERS, CONF_SERVER, CONF_USE_EPISODE_ART, + DEBOUNCE_LAST_FIRED, DEBOUNCE_TIMEOUT, + DEBOUNCE_UNSUB, DEFAULT_VERIFY_SSL, + DOMAIN as PLEX_DOMAIN, PLEX_NEW_MP_SIGNAL, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, PLEX_UPDATE_SENSOR_SIGNAL, @@ -46,24 +50,34 @@ def debounce(func): """Decorate function to debounce callbacks from Plex websocket.""" - unsub = None - async def call_later_listener(self, _): """Handle call_later callback.""" - nonlocal unsub - unsub = None await func(self) + self.hass.data[PLEX_DOMAIN][DEBOUNCE_LAST_FIRED][ + self.machine_identifier + ] = dt_util.utcnow() + self.hass.data[PLEX_DOMAIN][DEBOUNCE_UNSUB][self.machine_identifier] = None @wraps(func) async def wrapper(self): """Schedule async callback.""" - nonlocal unsub + now = dt_util.utcnow() + + last_fired = self.hass.data[PLEX_DOMAIN][DEBOUNCE_LAST_FIRED][ + self.machine_identifier + ] + unsub = self.hass.data[PLEX_DOMAIN][DEBOUNCE_UNSUB][self.machine_identifier] if unsub: - _LOGGER.debug("Throttling update of %s", self.friendly_name) unsub() # pylint: disable=not-callable - unsub = async_call_later( - self.hass, DEBOUNCE_TIMEOUT, partial(call_later_listener, self), - ) + + if last_fired and (now - last_fired).total_seconds() < DEBOUNCE_TIMEOUT: + _LOGGER.debug("Throttling update of %s", self.friendly_name) + unsub = async_call_later( + self.hass, DEBOUNCE_TIMEOUT, partial(call_later_listener, self), + ) + self.hass.data[PLEX_DOMAIN][DEBOUNCE_UNSUB][self.machine_identifier] = unsub + else: + await call_later_listener(self, None) return wrapper diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 3b70f30189af98..982cbf4830712a 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -154,18 +154,44 @@ async def test_debouncer(hass, caplog): server_id = mock_plex_server.machineIdentifier - # First two updates are skipped - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - - next_update = dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + print(hass.data[DOMAIN][SERVERS][server_id]) + + with patch( + "homeassistant.components.plex.server.PlexServer._fetch_platform_data", + return_value=([], []), + ) as mock_update: + # Called immediately + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 1 + + # Throttled + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 1 + + # Throttled + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 1 + + # Called from scheduler + next_update = dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + assert mock_update.call_count == 2 + + # Throttled + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 2 + + # Called from scheduler + next_update = dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + assert mock_update.call_count == 3 assert ( - caplog.text.count(f"Throttling update of {mock_plex_server.friendlyName}") == 2 + caplog.text.count(f"Throttling update of {mock_plex_server.friendlyName}") == 3 ) From dfde21d7f90a31f34c78b283fef599bc77398fb4 Mon Sep 17 00:00:00 2001 From: Jason Lawrence Date: Wed, 8 Apr 2020 09:30:41 -0500 Subject: [PATCH 2/6] Use Debouncer helper, rewrite affected tests --- homeassistant/components/plex/__init__.py | 13 +- homeassistant/components/plex/const.py | 2 - homeassistant/components/plex/server.py | 54 ++----- tests/components/plex/common.py | 20 --- tests/components/plex/test_config_flow.py | 6 +- tests/components/plex/test_init.py | 130 ++++++++++------ tests/components/plex/test_server.py | 180 +++++++++++----------- 7 files changed, 194 insertions(+), 211 deletions(-) delete mode 100644 tests/components/plex/common.py diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 7c27220a09f25b..ff36f4f5c320ac 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -33,8 +33,6 @@ CONF_SERVER_IDENTIFIER, CONF_SHOW_ALL_CONTROLS, CONF_USE_EPISODE_ART, - DEBOUNCE_LAST_FIRED, - DEBOUNCE_UNSUB, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, @@ -86,14 +84,7 @@ async def async_setup(hass, config): """Set up the Plex component.""" hass.data.setdefault( PLEX_DOMAIN, - { - SERVERS: {}, - DISPATCHERS: {}, - WEBSOCKETS: {}, - PLATFORMS_COMPLETED: {}, - DEBOUNCE_LAST_FIRED: {}, - DEBOUNCE_UNSUB: {}, - }, + {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}, PLATFORMS_COMPLETED: {}}, ) plex_config = config.get(PLEX_DOMAIN, {}) @@ -188,8 +179,6 @@ async def async_setup_entry(hass, entry): ) hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, []) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) - hass.data[PLEX_DOMAIN][DEBOUNCE_LAST_FIRED][server_id] = None - hass.data[PLEX_DOMAIN][DEBOUNCE_UNSUB][server_id] = None def update_plex(): async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 3f79e39d341a55..126c6eb313a454 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -9,9 +9,7 @@ DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True -DEBOUNCE_LAST_FIRED = "debounce_last_fired" DEBOUNCE_TIMEOUT = 1 -DEBOUNCE_UNSUB = "debounce_unsub" DISPATCHERS = "dispatchers" PLATFORMS = frozenset(["media_player", "sensor"]) PLATFORMS_COMPLETED = "platforms_completed" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 3840b898600d7b..e4ca2783f4e51c 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,5 +1,4 @@ """Shared class to maintain Plex server instances.""" -from functools import partial, wraps import logging import ssl from urllib.parse import urlparse @@ -13,9 +12,8 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import callback +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_call_later -import homeassistant.util.dt as dt_util from .const import ( CONF_CLIENT_IDENTIFIER, @@ -23,11 +21,8 @@ CONF_MONITORED_USERS, CONF_SERVER, CONF_USE_EPISODE_ART, - DEBOUNCE_LAST_FIRED, DEBOUNCE_TIMEOUT, - DEBOUNCE_UNSUB, DEFAULT_VERIFY_SSL, - DOMAIN as PLEX_DOMAIN, PLEX_NEW_MP_SIGNAL, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, PLEX_UPDATE_SENSOR_SIGNAL, @@ -47,41 +42,6 @@ plexapi.X_PLEX_VERSION = X_PLEX_VERSION -def debounce(func): - """Decorate function to debounce callbacks from Plex websocket.""" - - async def call_later_listener(self, _): - """Handle call_later callback.""" - await func(self) - self.hass.data[PLEX_DOMAIN][DEBOUNCE_LAST_FIRED][ - self.machine_identifier - ] = dt_util.utcnow() - self.hass.data[PLEX_DOMAIN][DEBOUNCE_UNSUB][self.machine_identifier] = None - - @wraps(func) - async def wrapper(self): - """Schedule async callback.""" - now = dt_util.utcnow() - - last_fired = self.hass.data[PLEX_DOMAIN][DEBOUNCE_LAST_FIRED][ - self.machine_identifier - ] - unsub = self.hass.data[PLEX_DOMAIN][DEBOUNCE_UNSUB][self.machine_identifier] - if unsub: - unsub() # pylint: disable=not-callable - - if last_fired and (now - last_fired).total_seconds() < DEBOUNCE_TIMEOUT: - _LOGGER.debug("Throttling update of %s", self.friendly_name) - unsub = async_call_later( - self.hass, DEBOUNCE_TIMEOUT, partial(call_later_listener, self), - ) - self.hass.data[PLEX_DOMAIN][DEBOUNCE_UNSUB][self.machine_identifier] = unsub - else: - await call_later_listener(self, None) - - return wrapper - - class PlexServer: """Manages a single Plex server connection.""" @@ -101,6 +61,13 @@ def __init__(self, hass, server_config, known_server_id=None, options=None): self._accounts = [] self._owner_username = None self._version = None + self._update_platforms_debouncer = Debouncer( + hass, + _LOGGER, + cooldown=DEBOUNCE_TIMEOUT, + immediate=True, + function=self._async_update_platforms, + ) # Header conditionally added as it is not available in config entry v1 if CONF_CLIENT_IDENTIFIER in server_config: @@ -206,8 +173,11 @@ def _fetch_platform_data(self): """Fetch all data from the Plex server in a single method.""" return (self._plex_server.clients(), self._plex_server.sessions()) - @debounce async def async_update_platforms(self): + """Wrap the Debouncer call.""" + await self._update_platforms_debouncer.async_call() + + async def _async_update_platforms(self): """Update the platform entities.""" _LOGGER.debug("Updating devices") diff --git a/tests/components/plex/common.py b/tests/components/plex/common.py deleted file mode 100644 index adc6f4e0299d49..00000000000000 --- a/tests/components/plex/common.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Common fixtures and functions for Plex tests.""" -from datetime import timedelta - -from homeassistant.components.plex.const import ( - DEBOUNCE_TIMEOUT, - PLEX_UPDATE_PLATFORMS_SIGNAL, -) -from homeassistant.helpers.dispatcher import async_dispatcher_send -import homeassistant.util.dt as dt_util - -from tests.common import async_fire_time_changed - - -async def trigger_plex_update(hass, server_id): - """Update Plex by sending signal and jumping ahead by debounce timeout.""" - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - next_update = dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index bd5d45c0246b71..d839ccc674b839 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -15,13 +15,14 @@ CONF_USE_EPISODE_ART, DOMAIN, PLEX_SERVER_CONFIG, + PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, ) from homeassistant.config_entries import ENTRY_STATE_LOADED from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component -from .common import trigger_plex_update from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN from .mock_classes import MockPlexAccount, MockPlexServer @@ -415,7 +416,8 @@ async def test_option_flow_new_users_available(hass, caplog): server_id = mock_plex_server.machineIdentifier - await trigger_plex_update(hass, server_id) + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index cd1ea8725bd4a3..241926d9c07847 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -3,8 +3,9 @@ from datetime import timedelta import ssl -from asynctest import patch +from asynctest import ClockedTestCase, patch import plexapi +import pytest import requests from homeassistant.components.media_player import DOMAIN as MP_DOMAIN @@ -23,14 +24,18 @@ CONF_URL, CONF_VERIFY_SSL, ) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .common import trigger_plex_update from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN from .mock_classes import MockPlexAccount, MockPlexServer -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + async_test_home_assistant, +) async def test_setup_with_config(hass): @@ -74,63 +79,93 @@ async def test_setup_with_config(hass): ) -async def test_setup_with_config_entry(hass, caplog): - """Test setup component with config.""" +class TestClockedPlex(ClockedTestCase): + """Create clock-controlled asynctest class.""" - mock_plex_server = MockPlexServer() + @pytest.fixture(autouse=True) + def inject_fixture(self, caplog): + """Inject pytest fixtures as instance attributes.""" + self.caplog = caplog - entry = MockConfigEntry( - domain=const.DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) + async def setUp(self): + """Initialize this test class.""" + self.hass = await async_test_home_assistant(self.loop) - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert mock_listen.called + async def tearDown(self): + """Clean up the HomeAssistant instance.""" + await self.hass.async_stop() - assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + async def test_setup_with_config_entry(self): + """Test setup component with config.""" + hass = self.hass - server_id = mock_plex_server.machineIdentifier - loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] + mock_plex_server = MockPlexServer() - assert loaded_server.plex_server == mock_plex_server + entry = MockConfigEntry( + domain=const.DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) - assert server_id in hass.data[const.DOMAIN][const.DISPATCHERS] - assert server_id in hass.data[const.DOMAIN][const.WEBSOCKETS] - assert ( - hass.data[const.DOMAIN][const.PLATFORMS_COMPLETED][server_id] == const.PLATFORMS - ) + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ) as mock_listen: + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() - await trigger_plex_update(hass, server_id) + assert mock_listen.called - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) + assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_LOADED - await trigger_plex_update(hass, server_id) + server_id = mock_plex_server.machineIdentifier + loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] - for test_exception in ( - plexapi.exceptions.BadRequest, - requests.exceptions.RequestException, - ): - with patch.object( - mock_plex_server, "clients", side_effect=test_exception - ) as patched_clients_bad_request: - await trigger_plex_update(hass, server_id) + assert loaded_server.plex_server == mock_plex_server - assert patched_clients_bad_request.called + assert server_id in hass.data[const.DOMAIN][const.DISPATCHERS] + assert server_id in hass.data[const.DOMAIN][const.WEBSOCKETS] assert ( - f"Could not connect to Plex server: {mock_plex_server.friendlyName}" - in caplog.text + hass.data[const.DOMAIN][const.PLATFORMS_COMPLETED][server_id] + == const.PLATFORMS + ) + + async_dispatcher_send( + hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) + ) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.plex_plex_server_1") + assert sensor.state == str(len(mock_plex_server.accounts)) + + # Ensure existing entities refresh + await self.advance(const.DEBOUNCE_TIMEOUT) + async_dispatcher_send( + hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) ) - caplog.clear() + await hass.async_block_till_done() + + for test_exception in ( + plexapi.exceptions.BadRequest, + requests.exceptions.RequestException, + ): + with patch.object( + mock_plex_server, "clients", side_effect=test_exception + ) as patched_clients_bad_request: + await self.advance(const.DEBOUNCE_TIMEOUT) + async_dispatcher_send( + hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) + ) + await hass.async_block_till_done() + + assert patched_clients_bad_request.called + assert ( + f"Could not connect to Plex server: {mock_plex_server.friendlyName}" + in self.caplog.text + ) + self.caplog.clear() async def test_set_config_entry_unique_id(hass): @@ -292,7 +327,8 @@ async def test_setup_with_photo_session(hass): server_id = mock_plex_server.machineIdentifier - await trigger_plex_update(hass, server_id) + async_dispatcher_send(hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() media_player = hass.states.get("media_player.plex_product_title") assert media_player.state == "idle" diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 982cbf4830712a..f0a22ec4d0d5c3 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -1,8 +1,7 @@ """Tests for Plex server.""" import copy -from datetime import timedelta -from asynctest import patch +from asynctest import ClockedTestCase, patch from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.plex.const import ( @@ -14,13 +13,11 @@ SERVERS, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -import homeassistant.util.dt as dt_util -from .common import trigger_plex_update from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .mock_classes import MockPlexServer -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry, async_test_home_assistant async def test_new_users_available(hass): @@ -48,7 +45,8 @@ async def test_new_users_available(hass): server_id = mock_plex_server.machineIdentifier - await trigger_plex_update(hass, server_id) + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -86,7 +84,8 @@ async def test_new_ignored_users_available(hass, caplog): server_id = mock_plex_server.machineIdentifier - await trigger_plex_update(hass, server_id) + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -100,98 +99,107 @@ async def test_new_ignored_users_available(hass, caplog): assert sensor.state == str(len(mock_plex_server.accounts)) -async def test_mark_sessions_idle(hass): - """Test marking media_players as idle when sessions end.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) +class TestClockedPlex(ClockedTestCase): + """Create clock-controlled asynctest class.""" - mock_plex_server = MockPlexServer(config_entry=entry) + async def setUp(self): + """Initialize this test class.""" + self.hass = await async_test_home_assistant(self.loop) - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + async def tearDown(self): + """Clean up the HomeAssistant instance.""" + await self.hass.async_stop() - server_id = mock_plex_server.machineIdentifier - - await trigger_plex_update(hass, server_id) - - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) + async def test_mark_sessions_idle(self): + """Test marking media_players as idle when sessions end.""" + hass = self.hass - mock_plex_server.clear_clients() - mock_plex_server.clear_sessions() + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) - await trigger_plex_update(hass, server_id) + mock_plex_server = MockPlexServer(config_entry=entry) - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == "0" + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + server_id = mock_plex_server.machineIdentifier -async def test_debouncer(hass, caplog): - """Test debouncer decorator logic.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - - mock_plex_server = MockPlexServer(config_entry=entry) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - server_id = mock_plex_server.machineIdentifier - - print(hass.data[DOMAIN][SERVERS][server_id]) - - with patch( - "homeassistant.components.plex.server.PlexServer._fetch_platform_data", - return_value=([], []), - ) as mock_update: - # Called immediately async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) await hass.async_block_till_done() - assert mock_update.call_count == 1 - # Throttled - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - assert mock_update.call_count == 1 + sensor = hass.states.get("sensor.plex_plex_server_1") + assert sensor.state == str(len(mock_plex_server.accounts)) - # Throttled - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - assert mock_update.call_count == 1 + mock_plex_server.clear_clients() + mock_plex_server.clear_sessions() - # Called from scheduler - next_update = dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert mock_update.call_count == 2 - - # Throttled + await self.advance(DEBOUNCE_TIMEOUT) async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) await hass.async_block_till_done() - assert mock_update.call_count == 2 - # Called from scheduler - next_update = dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert mock_update.call_count == 3 - - assert ( - caplog.text.count(f"Throttling update of {mock_plex_server.friendlyName}") == 3 - ) + sensor = hass.states.get("sensor.plex_plex_server_1") + assert sensor.state == "0" + + async def test_debouncer(self): + """Test debouncer behavior.""" + hass = self.hass + + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + mock_plex_server = MockPlexServer(config_entry=entry) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + server_id = mock_plex_server.machineIdentifier + + with patch( + "homeassistant.components.plex.server.PlexServer._fetch_platform_data", + return_value=([], []), + ) as mock_update: + # Called immediately + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 1 + + # Throttled + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 1 + + # Throttled + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 1 + + # Called from scheduler + await self.advance(DEBOUNCE_TIMEOUT) + await hass.async_block_till_done() + assert mock_update.call_count == 2 + + # Throttled + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + assert mock_update.call_count == 2 + + # Called from scheduler + await self.advance(DEBOUNCE_TIMEOUT) + await hass.async_block_till_done() + assert mock_update.call_count == 3 From 9afb6f6bbf64a294af77cd1382da24b6add0a8a5 Mon Sep 17 00:00:00 2001 From: Jason Lawrence Date: Wed, 8 Apr 2020 10:28:59 -0500 Subject: [PATCH 3/6] Mock storage so files aren't left behind --- tests/components/plex/test_init.py | 4 ++++ tests/components/plex/test_server.py | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 241926d9c07847..4befdcc3735f42 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -35,6 +35,7 @@ MockConfigEntry, async_fire_time_changed, async_test_home_assistant, + mock_storage, ) @@ -90,10 +91,13 @@ def inject_fixture(self, caplog): async def setUp(self): """Initialize this test class.""" self.hass = await async_test_home_assistant(self.loop) + self.mock_storage = mock_storage() + self.mock_storage.__enter__() async def tearDown(self): """Clean up the HomeAssistant instance.""" await self.hass.async_stop() + self.mock_storage.__exit__(None, None, None) async def test_setup_with_config_entry(self): """Test setup component with config.""" diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index f0a22ec4d0d5c3..d5a4c5f12b8d1b 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -17,7 +17,7 @@ from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .mock_classes import MockPlexServer -from tests.common import MockConfigEntry, async_test_home_assistant +from tests.common import MockConfigEntry, async_test_home_assistant, mock_storage async def test_new_users_available(hass): @@ -105,10 +105,13 @@ class TestClockedPlex(ClockedTestCase): async def setUp(self): """Initialize this test class.""" self.hass = await async_test_home_assistant(self.loop) + self.mock_storage = mock_storage() + self.mock_storage.__enter__() async def tearDown(self): """Clean up the HomeAssistant instance.""" await self.hass.async_stop() + self.mock_storage.__exit__(None, None, None) async def test_mark_sessions_idle(self): """Test marking media_players as idle when sessions end.""" From 9776c78851f81065d8b347dc15a79f6ebe7c29b9 Mon Sep 17 00:00:00 2001 From: Jason Lawrence Date: Wed, 8 Apr 2020 12:32:36 -0500 Subject: [PATCH 4/6] Don't bother with wrapper method, store debouncer call during init --- homeassistant/components/plex/server.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index e4ca2783f4e51c..d9e2d2bd9cca49 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -61,13 +61,13 @@ def __init__(self, hass, server_config, known_server_id=None, options=None): self._accounts = [] self._owner_username = None self._version = None - self._update_platforms_debouncer = Debouncer( + self.async_update_platforms = Debouncer( hass, _LOGGER, cooldown=DEBOUNCE_TIMEOUT, immediate=True, function=self._async_update_platforms, - ) + ).async_call # Header conditionally added as it is not available in config entry v1 if CONF_CLIENT_IDENTIFIER in server_config: @@ -173,10 +173,6 @@ def _fetch_platform_data(self): """Fetch all data from the Plex server in a single method.""" return (self._plex_server.clients(), self._plex_server.sessions()) - async def async_update_platforms(self): - """Wrap the Debouncer call.""" - await self._update_platforms_debouncer.async_call() - async def _async_update_platforms(self): """Update the platform entities.""" _LOGGER.debug("Updating devices") From ca5c242a2a67d9a05626ec59487155a3ddf32ec0 Mon Sep 17 00:00:00 2001 From: Jason Lawrence Date: Wed, 8 Apr 2020 13:26:00 -0500 Subject: [PATCH 5/6] Test cleanup from review --- tests/components/plex/test_init.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 4befdcc3735f42..ef2199b11c5394 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -73,12 +73,6 @@ async def test_setup_with_config(hass): assert loaded_server.plex_server == mock_plex_server - assert server_id in hass.data[const.DOMAIN][const.DISPATCHERS] - assert server_id in hass.data[const.DOMAIN][const.WEBSOCKETS] - assert ( - hass.data[const.DOMAIN][const.PLATFORMS_COMPLETED][server_id] == const.PLATFORMS - ) - class TestClockedPlex(ClockedTestCase): """Create clock-controlled asynctest class.""" @@ -129,13 +123,6 @@ async def test_setup_with_config_entry(self): assert loaded_server.plex_server == mock_plex_server - assert server_id in hass.data[const.DOMAIN][const.DISPATCHERS] - assert server_id in hass.data[const.DOMAIN][const.WEBSOCKETS] - assert ( - hass.data[const.DOMAIN][const.PLATFORMS_COMPLETED][server_id] - == const.PLATFORMS - ) - async_dispatcher_send( hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) ) @@ -290,22 +277,12 @@ async def test_unload_config_entry(hass): assert loaded_server.plex_server == mock_plex_server - assert server_id in hass.data[const.DOMAIN][const.DISPATCHERS] - assert server_id in hass.data[const.DOMAIN][const.WEBSOCKETS] - assert ( - hass.data[const.DOMAIN][const.PLATFORMS_COMPLETED][server_id] == const.PLATFORMS - ) - with patch("homeassistant.components.plex.PlexWebsocket.close") as mock_close: await hass.config_entries.async_unload(entry.entry_id) assert mock_close.called assert entry.state == ENTRY_STATE_NOT_LOADED - assert server_id not in hass.data[const.DOMAIN][const.SERVERS] - assert server_id not in hass.data[const.DOMAIN][const.DISPATCHERS] - assert server_id not in hass.data[const.DOMAIN][const.WEBSOCKETS] - async def test_setup_with_photo_session(hass): """Test setup component with config.""" From 70f67192ac849803fb7d1debef46f2b31ad7bcef Mon Sep 17 00:00:00 2001 From: Jason Lawrence Date: Thu, 9 Apr 2020 16:35:16 -0500 Subject: [PATCH 6/6] Don't patch own code in tests --- tests/components/plex/test_server.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index d5a4c5f12b8d1b..6eff97ae7dc5bc 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -173,9 +173,8 @@ async def test_debouncer(self): server_id = mock_plex_server.machineIdentifier - with patch( - "homeassistant.components.plex.server.PlexServer._fetch_platform_data", - return_value=([], []), + with patch.object(mock_plex_server, "clients", return_value=[]), patch.object( + mock_plex_server, "sessions", return_value=[] ) as mock_update: # Called immediately async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))