From 2d6247aca709fe94762c76675476de3391c9d252 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Mon, 30 Mar 2020 21:14:24 -0400 Subject: [PATCH 01/12] Don't write storage to disk while stopping --- homeassistant/core.py | 6 ++--- homeassistant/helpers/storage.py | 5 +++- tests/helpers/test_storage.py | 42 +++++++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 9265c57bbf349..d9155ece2d3e6 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -152,7 +152,7 @@ class CoreState(enum.Enum): starting = "STARTING" running = "RUNNING" stopping = "STOPPING" - writing_data = "WRITING_DATA" + final_write = "FINAL_WRITE" def __str__(self) -> str: """Return the event.""" @@ -414,7 +414,7 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None: # regardless of the state of the loop. if self.state == CoreState.not_running: # just ignore return - if self.state == CoreState.stopping or self.state == CoreState.writing_data: + if self.state == CoreState.stopping or self.state == CoreState.final_write: _LOGGER.info("async_stop called twice: ignored") return if self.state == CoreState.starting: @@ -428,7 +428,7 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None: await self.async_block_till_done() # stage 2 - self.state = CoreState.writing_data + self.state = CoreState.final_write self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) await self.async_block_till_done() diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 5885aa01e6fbf..4b8757d73fc15 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -6,7 +6,7 @@ from typing import Any, Callable, Dict, List, Optional, Type, Union from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.loader import bind_hass from homeassistant.util import json as json_util @@ -184,6 +184,9 @@ async def _async_callback_stop_write(self, _event): async def _async_handle_write_data(self, *_args): """Handle writing the config.""" + if self.hass.state == CoreState.stopping: + self._async_ensure_stop_listener() + return data = self._data if "data_func" in data: diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index dcadd4d4369f3..162f6fef8366f 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -7,6 +7,7 @@ import pytest from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE +from homeassistant.core import CoreState from homeassistant.helpers import storage from homeassistant.util import dt @@ -79,7 +80,7 @@ async def test_saving_with_delay(hass, store, hass_storage): } -async def test_saving_on_stop(hass, hass_storage): +async def test_saving_on_final_write(hass, hass_storage): """Test delayed saves trigger when we quit Home Assistant.""" store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) store.async_delay_save(lambda: MOCK_DATA, 1) @@ -94,6 +95,45 @@ async def test_saving_on_stop(hass, hass_storage): } +async def test_not_delayed_saving_while_stopping(hass, hass_storage): + """Test delayed saves don't write when stopping Home Assistant.""" + store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) + hass.state = CoreState.stopping + + store.async_delay_save(lambda: MOCK_DATA, 1) + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done() + assert store.key not in hass_storage + + hass.state = CoreState.final_write + hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) + await hass.async_block_till_done() + + assert hass_storage[store.key] == { + "version": MOCK_VERSION, + "key": MOCK_KEY, + "data": MOCK_DATA, + } + + +async def test_not_saving_while_stopping(hass, hass_storage): + """Test saves don't write when stopping Home Assistant.""" + store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) + hass.state = CoreState.stopping + await store.async_save(MOCK_DATA) + assert store.key not in hass_storage + + hass.state = CoreState.final_write + hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) + await hass.async_block_till_done() + + assert hass_storage[store.key] == { + "version": MOCK_VERSION, + "key": MOCK_KEY, + "data": MOCK_DATA, + } + + async def test_loading_while_delay(hass, store, hass_storage): """Test we load new data even if not written yet.""" await store.async_save({"delay": "no"}) From eb20e7b96d6e94f3128858f7f46cb22f72d9076f Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 31 Mar 2020 07:44:37 -0400 Subject: [PATCH 02/12] rework change --- homeassistant/helpers/storage.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 4b8757d73fc15..b9ea997382b47 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -132,7 +132,11 @@ async def async_save(self, data: Union[Dict, List]) -> None: self._data = {"version": self.version, "key": self.key, "data": data} self._async_cleanup_delay_listener() - self._async_cleanup_stop_listener() + if self.hass.state == CoreState.stopping: + self._async_ensure_stop_listener() + return + else: + self._async_cleanup_stop_listener() await self._async_handle_write_data() @callback @@ -142,9 +146,10 @@ def async_delay_save(self, data_func: Callable[[], Dict], delay: float = 0) -> N self._async_cleanup_delay_listener() - self._unsub_delay_listener = async_call_later( - self.hass, delay, self._async_callback_delayed_write - ) + if self.hass.state != CoreState.stopping: + self._unsub_delay_listener = async_call_later( + self.hass, delay, self._async_callback_delayed_write + ) self._async_ensure_stop_listener() @@ -184,9 +189,6 @@ async def _async_callback_stop_write(self, _event): async def _async_handle_write_data(self, *_args): """Handle writing the config.""" - if self.hass.state == CoreState.stopping: - self._async_ensure_stop_listener() - return data = self._data if "data_func" in data: From 15053d0e46b8151b24438342a07a4c539626dc97 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 31 Mar 2020 08:54:20 -0400 Subject: [PATCH 03/12] lint --- homeassistant/helpers/storage.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index b9ea997382b47..7e2c64d5a6f0f 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -135,8 +135,7 @@ async def async_save(self, data: Union[Dict, List]) -> None: if self.hass.state == CoreState.stopping: self._async_ensure_stop_listener() return - else: - self._async_cleanup_stop_listener() + self._async_cleanup_stop_listener() await self._async_handle_write_data() @callback From bbba300d7cee4df1561fa38723b4c29298a01a78 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 31 Mar 2020 09:52:23 -0400 Subject: [PATCH 04/12] remove delay save and schedule final write at stop --- homeassistant/helpers/storage.py | 45 +++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 7e2c64d5a6f0f..1b2ce57fa974b 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -5,7 +5,10 @@ import os from typing import Any, Callable, Dict, List, Optional, Type, Union -from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE +from homeassistant.const import ( + EVENT_HOMEASSISTANT_FINAL_WRITE, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.loader import bind_hass @@ -73,6 +76,7 @@ def __init__( self._data: Optional[Dict[str, Any]] = None self._unsub_delay_listener: Optional[CALLBACK_TYPE] = None self._unsub_stop_listener: Optional[CALLBACK_TYPE] = None + self._unsub_final_write_listener: Optional[CALLBACK_TYPE] = None self._write_lock = asyncio.Lock() self._load_task: Optional[asyncio.Future] = None self._encoder = encoder @@ -125,6 +129,7 @@ async def _async_load(self): stored = await self._async_migrate_func(data["version"], data["data"]) self._load_task = None + self._async_ensure_stop_listener() return stored async def async_save(self, data: Union[Dict, List]) -> None: @@ -133,9 +138,9 @@ async def async_save(self, data: Union[Dict, List]) -> None: self._async_cleanup_delay_listener() if self.hass.state == CoreState.stopping: - self._async_ensure_stop_listener() + self._async_ensure_final_write_listener() return - self._async_cleanup_stop_listener() + self._async_cleanup_final_write_listener() await self._async_handle_write_data() @callback @@ -150,22 +155,30 @@ def async_delay_save(self, data_func: Callable[[], Dict], delay: float = 0) -> N self.hass, delay, self._async_callback_delayed_write ) - self._async_ensure_stop_listener() + self._async_ensure_final_write_listener() + + @callback + def _async_ensure_final_write_listener(self): + """Ensure that we write if we quit before delay has passed.""" + if self._unsub_final_write_listener is None: + self._unsub_final_write_listener = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_callback_final_write + ) @callback def _async_ensure_stop_listener(self): """Ensure that we write if we quit before delay has passed.""" if self._unsub_stop_listener is None: self._unsub_stop_listener = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_callback_stop_write + EVENT_HOMEASSISTANT_STOP, self._async_callback_stop ) @callback - def _async_cleanup_stop_listener(self): + def _async_cleanup_final_write_listener(self): """Clean up a stop listener.""" - if self._unsub_stop_listener is not None: - self._unsub_stop_listener() - self._unsub_stop_listener = None + if self._unsub_final_write_listener is not None: + self._unsub_final_write_listener() + self._unsub_final_write_listener = None @callback def _async_cleanup_delay_listener(self): @@ -177,15 +190,21 @@ def _async_cleanup_delay_listener(self): async def _async_callback_delayed_write(self, _now): """Handle a delayed write callback.""" self._unsub_delay_listener = None - self._async_cleanup_stop_listener() + self._async_cleanup_final_write_listener() await self._async_handle_write_data() - async def _async_callback_stop_write(self, _event): - """Handle a write because Home Assistant is stopping.""" - self._unsub_stop_listener = None + async def _async_callback_final_write(self, _event): + """Handle a write because Home Assistant is in final write state.""" + self._unsub_final_write_listener = None self._async_cleanup_delay_listener() await self._async_handle_write_data() + async def _async_callback_stop(self, _event): + """Handle the stop event and cancel any delay listener that exists.""" + self._unsub_stop_listener = None + self._async_cleanup_delay_listener() + self._async_ensure_final_write_listener() + async def _async_handle_write_data(self, *_args): """Handle writing the config.""" data = self._data From a43bb75082c588c96540a1d00b7cfda5c53bf4e5 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 31 Mar 2020 10:55:47 -0400 Subject: [PATCH 05/12] update tests --- homeassistant/helpers/storage.py | 21 ++++++++------ tests/helpers/test_storage.py | 47 ++++++++++++++++++-------------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 1b2ce57fa974b..a49de1fabe422 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -80,6 +80,7 @@ def __init__( self._write_lock = asyncio.Lock() self._load_task: Optional[asyncio.Future] = None self._encoder = encoder + self._async_ensure_stop_listener() @property def path(self): @@ -129,7 +130,6 @@ async def _async_load(self): stored = await self._async_migrate_func(data["version"], data["data"]) self._load_task = None - self._async_ensure_stop_listener() return stored async def async_save(self, data: Union[Dict, List]) -> None: @@ -137,10 +137,12 @@ async def async_save(self, data: Union[Dict, List]) -> None: self._data = {"version": self.version, "key": self.key, "data": data} self._async_cleanup_delay_listener() + self._async_cleanup_final_write_listener() + if self.hass.state == CoreState.stopping: self._async_ensure_final_write_listener() return - self._async_cleanup_final_write_listener() + await self._async_handle_write_data() @callback @@ -149,13 +151,15 @@ def async_delay_save(self, data_func: Callable[[], Dict], delay: float = 0) -> N self._data = {"version": self.version, "key": self.key, "data_func": data_func} self._async_cleanup_delay_listener() + self._async_cleanup_final_write_listener() - if self.hass.state != CoreState.stopping: - self._unsub_delay_listener = async_call_later( - self.hass, delay, self._async_callback_delayed_write - ) + if self.hass.state == CoreState.stopping: + self._async_ensure_final_write_listener() + return - self._async_ensure_final_write_listener() + self._unsub_delay_listener = async_call_later( + self.hass, delay, self._async_callback_delayed_write + ) @callback def _async_ensure_final_write_listener(self): @@ -202,8 +206,9 @@ async def _async_callback_final_write(self, _event): async def _async_callback_stop(self, _event): """Handle the stop event and cancel any delay listener that exists.""" self._unsub_stop_listener = None + if self._unsub_delay_listener is not None: + self._async_ensure_final_write_listener() self._async_cleanup_delay_listener() - self._async_ensure_final_write_listener() async def _async_handle_write_data(self, *_args): """Handle writing the config.""" diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 162f6fef8366f..d800731a8096d 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -6,7 +6,10 @@ import pytest -from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE +from homeassistant.const import ( + EVENT_HOMEASSISTANT_FINAL_WRITE, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import CoreState from homeassistant.helpers import storage from homeassistant.util import dt @@ -83,7 +86,14 @@ async def test_saving_with_delay(hass, store, hass_storage): async def test_saving_on_final_write(hass, hass_storage): """Test delayed saves trigger when we quit Home Assistant.""" store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) - store.async_delay_save(lambda: MOCK_DATA, 1) + store.async_delay_save(lambda: MOCK_DATA, 5) + assert store.key not in hass_storage + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() assert store.key not in hass_storage hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) @@ -96,8 +106,10 @@ async def test_saving_on_final_write(hass, hass_storage): async def test_not_delayed_saving_while_stopping(hass, hass_storage): - """Test delayed saves don't write when stopping Home Assistant.""" + """Test delayed saves don't write after the stop event has fired.""" store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() hass.state = CoreState.stopping store.async_delay_save(lambda: MOCK_DATA, 1) @@ -105,15 +117,20 @@ async def test_not_delayed_saving_while_stopping(hass, hass_storage): await hass.async_block_till_done() assert store.key not in hass_storage - hass.state = CoreState.final_write - hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) + +async def test_not_delayed_saving_after_stopping(hass, hass_storage): + """Test delayed saves don't write after stop if issued before stopping Home Assistant.""" + store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) + store.async_delay_save(lambda: MOCK_DATA, 10) + assert store.key not in hass_storage + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() + assert store.key not in hass_storage - assert hass_storage[store.key] == { - "version": MOCK_VERSION, - "key": MOCK_KEY, - "data": MOCK_DATA, - } + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=15)) + await hass.async_block_till_done() + assert store.key not in hass_storage async def test_not_saving_while_stopping(hass, hass_storage): @@ -123,16 +140,6 @@ async def test_not_saving_while_stopping(hass, hass_storage): await store.async_save(MOCK_DATA) assert store.key not in hass_storage - hass.state = CoreState.final_write - hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) - await hass.async_block_till_done() - - assert hass_storage[store.key] == { - "version": MOCK_VERSION, - "key": MOCK_KEY, - "data": MOCK_DATA, - } - async def test_loading_while_delay(hass, store, hass_storage): """Test we load new data even if not written yet.""" From cbd541a52331d6a590c5cda6a414a12b69839089 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 31 Mar 2020 12:31:39 -0400 Subject: [PATCH 06/12] fix test component using private methods --- homeassistant/helpers/storage.py | 1 + tests/common.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index a49de1fabe422..d3af87173dd0d 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -130,6 +130,7 @@ async def _async_load(self): stored = await self._async_migrate_func(data["version"], data["data"]) self._load_task = None + self._async_ensure_stop_listener() return stored async def async_save(self, data: Union[Dict, List]) -> None: diff --git a/tests/common.py b/tests/common.py index 8fdcc9b8f860c..d537e3c5c7c53 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1017,8 +1017,11 @@ async def flush_store(store): if store._data is None: return - store._async_cleanup_stop_listener() + store._async_cleanup_final_write_listener() store._async_cleanup_delay_listener() + if store._unsub_stop_listener is not None: + store._unsub_stop_listener() + store._unsub_stop_listener = None await store._async_handle_write_data() @@ -1030,7 +1033,7 @@ async def get_system_health_info(hass, domain): def mock_integration(hass, module): """Mock an integration.""" integration = loader.Integration( - hass, f"homeassistant.components.{module.DOMAIN}", None, module.mock_manifest(), + hass, f"homeassistant.components.{module.DOMAIN}", None, module.mock_manifest() ) _LOGGER.info("Adding mock integration: %s", module.DOMAIN) From acf4b4c51c0b45aa933d9907005a7e1ebb386448 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 31 Mar 2020 13:42:18 -0400 Subject: [PATCH 07/12] cleanup --- homeassistant/helpers/storage.py | 27 +++++---------------------- tests/common.py | 3 --- tests/helpers/test_storage.py | 2 ++ 3 files changed, 7 insertions(+), 25 deletions(-) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index d3af87173dd0d..16a9a8050ef2c 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -5,10 +5,7 @@ import os from typing import Any, Callable, Dict, List, Optional, Type, Union -from homeassistant.const import ( - EVENT_HOMEASSISTANT_FINAL_WRITE, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.loader import bind_hass @@ -75,12 +72,10 @@ def __init__( self._private = private self._data: Optional[Dict[str, Any]] = None self._unsub_delay_listener: Optional[CALLBACK_TYPE] = None - self._unsub_stop_listener: Optional[CALLBACK_TYPE] = None self._unsub_final_write_listener: Optional[CALLBACK_TYPE] = None self._write_lock = asyncio.Lock() self._load_task: Optional[asyncio.Future] = None self._encoder = encoder - self._async_ensure_stop_listener() @property def path(self): @@ -130,7 +125,6 @@ async def _async_load(self): stored = await self._async_migrate_func(data["version"], data["data"]) self._load_task = None - self._async_ensure_stop_listener() return stored async def async_save(self, data: Union[Dict, List]) -> None: @@ -170,14 +164,6 @@ def _async_ensure_final_write_listener(self): EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_callback_final_write ) - @callback - def _async_ensure_stop_listener(self): - """Ensure that we write if we quit before delay has passed.""" - if self._unsub_stop_listener is None: - self._unsub_stop_listener = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._async_callback_stop - ) - @callback def _async_cleanup_final_write_listener(self): """Clean up a stop listener.""" @@ -194,6 +180,10 @@ def _async_cleanup_delay_listener(self): async def _async_callback_delayed_write(self, _now): """Handle a delayed write callback.""" + # catch the case where a call is scheduled and then we stop Home Assistant + if self.hass.state == CoreState.stopping: + self._async_ensure_final_write_listener() + return self._unsub_delay_listener = None self._async_cleanup_final_write_listener() await self._async_handle_write_data() @@ -204,13 +194,6 @@ async def _async_callback_final_write(self, _event): self._async_cleanup_delay_listener() await self._async_handle_write_data() - async def _async_callback_stop(self, _event): - """Handle the stop event and cancel any delay listener that exists.""" - self._unsub_stop_listener = None - if self._unsub_delay_listener is not None: - self._async_ensure_final_write_listener() - self._async_cleanup_delay_listener() - async def _async_handle_write_data(self, *_args): """Handle writing the config.""" data = self._data diff --git a/tests/common.py b/tests/common.py index d537e3c5c7c53..fd61723f4dbf7 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1019,9 +1019,6 @@ async def flush_store(store): store._async_cleanup_final_write_listener() store._async_cleanup_delay_listener() - if store._unsub_stop_listener is not None: - store._unsub_stop_listener() - store._unsub_stop_listener = None await store._async_handle_write_data() diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index d800731a8096d..61648c85ada9e 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -90,6 +90,7 @@ async def test_saving_on_final_write(hass, hass_storage): assert store.key not in hass_storage hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + hass.state = CoreState.stopping await hass.async_block_till_done() async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=10)) @@ -125,6 +126,7 @@ async def test_not_delayed_saving_after_stopping(hass, hass_storage): assert store.key not in hass_storage hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + hass.state = CoreState.stopping await hass.async_block_till_done() assert store.key not in hass_storage From 5a9e35f766b86e2a45a2a7e28060181e7fb8b7f1 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 31 Mar 2020 14:58:48 -0400 Subject: [PATCH 08/12] always listen --- homeassistant/helpers/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 16a9a8050ef2c..16f8655198dea 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -149,12 +149,12 @@ def async_delay_save(self, data_func: Callable[[], Dict], delay: float = 0) -> N self._async_cleanup_final_write_listener() if self.hass.state == CoreState.stopping: - self._async_ensure_final_write_listener() return self._unsub_delay_listener = async_call_later( self.hass, delay, self._async_callback_delayed_write ) + self._async_ensure_final_write_listener() @callback def _async_ensure_final_write_listener(self): From a66dac99c8463af3e8df3ea3a9e5485c7a429d61 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 31 Mar 2020 16:21:14 -0400 Subject: [PATCH 09/12] use stop in restore state again --- homeassistant/helpers/restore_state.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 0757770d2f788..d57d3ad99207a 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -4,10 +4,7 @@ import logging from typing import Any, Awaitable, Dict, List, Optional, Set, cast -from homeassistant.const import ( - EVENT_HOMEASSISTANT_FINAL_WRITE, - EVENT_HOMEASSISTANT_START, -) +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import ( CoreState, HomeAssistant, @@ -187,9 +184,7 @@ def _async_dump_states(*_: Any) -> None: async_track_time_interval(self.hass, _async_dump_states, STATE_DUMP_INTERVAL) # Dump states when stopping hass - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_FINAL_WRITE, _async_dump_states - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_dump_states) @callback def async_restore_entity_added(self, entity_id: str) -> None: From bf3789430b4d718574967194816017c5b73c4704 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 31 Mar 2020 18:23:05 -0400 Subject: [PATCH 10/12] whitelist JSON exceptions for later --- tests/conftest.py | 14 +++++++++-- tests/ignore_uncaught_exceptions.py | 39 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0963d1514904b..f93d519035063 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,7 +19,10 @@ from homeassistant.setup import async_setup_component from homeassistant.util import location -from tests.ignore_uncaught_exceptions import IGNORE_UNCAUGHT_EXCEPTIONS +from tests.ignore_uncaught_exceptions import ( + IGNORE_UNCAUGHT_EXCEPTIONS, + IGNORE_UNCAUGHT_JSON_EXCEPTIONS, +) pytest.register_assert_rewrite("tests.common") @@ -104,6 +107,13 @@ def exc_handle(loop, context): continue if isinstance(ex, ServiceNotFound): continue + if ( + isinstance(ex, TypeError) + and "is not JSON serializable" in str(ex) + and (request.module.__name__, request.function.__name__) + in IGNORE_UNCAUGHT_JSON_EXCEPTIONS + ): + continue raise ex @@ -211,7 +221,7 @@ def hass_client(hass, aiohttp_client, hass_access_token): async def auth_client(): """Return an authenticated client.""" return await aiohttp_client( - hass.http.app, headers={"Authorization": f"Bearer {hass_access_token}"}, + hass.http.app, headers={"Authorization": f"Bearer {hass_access_token}"} ) return auth_client diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 428de1a683c13..df4c0df6e2190 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -93,3 +93,42 @@ ("tests.components.yr.test_sensor", "test_forecast_setup"), ("tests.components.zwave.test_init", "test_power_schemes"), ] + +IGNORE_UNCAUGHT_JSON_EXCEPTIONS = [ + ("tests.components.spotify.test_config_flow", "test_full_flow"), + ("tests.components.smartthings.test_init", "test_config_entry_loads_platforms"), + ( + "tests.components.smartthings.test_init", + "test_scenes_unauthorized_loads_platforms", + ), + ( + "tests.components.smartthings.test_init", + "test_config_entry_loads_unconnected_cloud", + ), + ("tests.components.samsungtv.test_config_flow", "test_ssdp"), + ("tests.components.samsungtv.test_config_flow", "test_user_websocket"), + ("tests.components.samsungtv.test_config_flow", "test_user_already_configured"), + ("tests.components.samsungtv.test_config_flow", "test_autodetect_websocket"), + ("tests.components.samsungtv.test_config_flow", "test_autodetect_websocket_ssl"), + ("tests.components.samsungtv.test_config_flow", "test_ssdp_already_configured"), + ("tests.components.samsungtv.test_config_flow", "test_ssdp_noprefix"), + ("tests.components.samsungtv.test_config_flow", "test_user_legacy"), + ("tests.components.samsungtv.test_config_flow", "test_autodetect_legacy"), + ( + "tests.components.samsungtv.test_media_player", + "test_select_source_invalid_source", + ), + ( + "tests.components.samsungtv.test_media_player", + "test_play_media_channel_as_string", + ), + ( + "tests.components.samsungtv.test_media_player", + "test_play_media_channel_as_non_positive", + ), + ("tests.components.samsungtv.test_media_player", "test_turn_off_websocket"), + ("tests.components.samsungtv.test_media_player", "test_play_media_invalid_type"), + ("tests.components.harmony.test_config_flow", "test_form_import"), + ("tests.components.harmony.test_config_flow", "test_form_ssdp"), + ("tests.components.harmony.test_config_flow", "test_user_form"), +] From 60ec771bc3bc156ca9ca01340db434bbdc8ad0ae Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 31 Mar 2020 19:05:48 -0400 Subject: [PATCH 11/12] review comment --- homeassistant/helpers/storage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 16f8655198dea..00df728fb36d0 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -149,6 +149,7 @@ def async_delay_save(self, data_func: Callable[[], Dict], delay: float = 0) -> N self._async_cleanup_final_write_listener() if self.hass.state == CoreState.stopping: + self._async_ensure_final_write_listener() return self._unsub_delay_listener = async_call_later( From 498d8cb925b3b2362a1230b1610180f7d9363a3c Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Wed, 1 Apr 2020 19:31:43 -0400 Subject: [PATCH 12/12] make zwave tests use mock storage --- tests/components/zwave/test_init.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 4d358bde770c2..6d19022f9592b 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -28,6 +28,7 @@ get_test_home_assistant, mock_coro, mock_registry, + mock_storage, ) from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue @@ -827,6 +828,8 @@ def set_mock_openzwave(self, mock_openzwave): def setUp(self): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() + self.mock_storage = mock_storage() + self.mock_storage.__enter__() self.hass.start() self.registry = mock_registry(self.hass) @@ -862,6 +865,7 @@ def setUp(self): def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" self.hass.stop() + self.mock_storage.__exit__(None, None, None) @patch.object(zwave, "import_module") @patch.object(zwave, "discovery") @@ -1194,6 +1198,8 @@ def set_mock_openzwave(self, mock_openzwave): def setUp(self): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() + self.mock_storage = mock_storage() + self.mock_storage.__enter__() self.hass.start() # Initialize zwave @@ -1209,6 +1215,7 @@ def tearDown(self): # pylint: disable=invalid-name self.hass.services.call("zwave", "stop_network", {}) self.hass.block_till_done() self.hass.stop() + self.mock_storage.__exit__(None, None, None) def test_add_node(self): """Test zwave add_node service."""