Store: copy pending data (#59934)

This commit is contained in:
Paulus Schoutsen 2021-11-18 15:56:22 -08:00 committed by GitHub
parent 0fb21af07f
commit 442597928e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 42 additions and 11 deletions

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable from collections.abc import Callable
from contextlib import suppress from contextlib import suppress
from copy import deepcopy
import inspect import inspect
from json import JSONEncoder from json import JSONEncoder
import logging import logging
@ -133,6 +134,10 @@ class Store:
# If we didn't generate data yet, do it now. # If we didn't generate data yet, do it now.
if "data_func" in data: if "data_func" in data:
data["data"] = data.pop("data_func")() data["data"] = data.pop("data_func")()
# We make a copy because code might assume it's safe to mutate loaded data
# and we don't want that to mess with what we're trying to store.
data = deepcopy(data)
else: else:
data = await self.hass.async_add_executor_job( data = await self.hass.async_add_executor_job(
json_util.load_json, self.path json_util.load_json, self.path

View file

@ -1,5 +1,6 @@
{ {
"version": 1, "version": 1,
"minor_version": 1,
"key": "trace.saved_traces", "key": "trace.saved_traces",
"data": { "data": {
"automation.sun": [ "automation.sun": [

View file

@ -1,5 +1,6 @@
{ {
"version": 1, "version": 1,
"minor_version": 1,
"key": "trace.saved_traces", "key": "trace.saved_traces",
"data": { "data": {
"script.sun": [ "script.sun": [

View file

@ -49,7 +49,7 @@ import homeassistant.util.dt as dt_util
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
DEFAULT_CONFIG_ENTRY_ID = 1 DEFAULT_CONFIG_ENTRY_ID = "1"
DEFAULT_HOST = "1.2.3.4" DEFAULT_HOST = "1.2.3.4"
DEFAULT_SITE = "site_id" DEFAULT_SITE = "site_id"

View file

@ -1089,7 +1089,7 @@ async def test_restoring_client(hass, aioclient_mock):
data=ENTRY_CONFIG, data=ENTRY_CONFIG,
source="test", source="test",
options={}, options={},
entry_id=1, entry_id="1",
) )
registry = er.async_get(hass) registry = er.async_get(hass)

View file

@ -14,7 +14,7 @@ from .test_controller import (
setup_unifi_integration, setup_unifi_integration,
) )
from tests.common import MockConfigEntry from tests.common import MockConfigEntry, flush_store
async def test_setup_with_no_config(hass): async def test_setup_with_no_config(hass):
@ -110,6 +110,7 @@ async def test_wireless_clients(hass, hass_storage, aioclient_mock):
config_entry = await setup_unifi_integration( config_entry = await setup_unifi_integration(
hass, aioclient_mock, clients_response=[client_1, client_2] hass, aioclient_mock, clients_response=[client_1, client_2]
) )
await flush_store(hass.data[unifi.UNIFI_WIRELESS_CLIENTS]._store)
for mac in [ for mac in [
"00:00:00:00:00:00", "00:00:00:00:00:00",

View file

@ -22,6 +22,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from .test_controller import ( from .test_controller import (
CONTROLLER_HOST, CONTROLLER_HOST,
DEFAULT_CONFIG_ENTRY_ID,
DESCRIPTION, DESCRIPTION,
ENTRY_CONFIG, ENTRY_CONFIG,
setup_unifi_integration, setup_unifi_integration,
@ -857,7 +858,7 @@ async def test_restore_client_succeed(hass, aioclient_mock):
data=ENTRY_CONFIG, data=ENTRY_CONFIG,
source="test", source="test",
options={}, options={},
entry_id=1, entry_id=DEFAULT_CONFIG_ENTRY_ID,
) )
registry = er.async_get(hass) registry = er.async_get(hass)
@ -947,7 +948,7 @@ async def test_restore_client_no_old_state(hass, aioclient_mock):
data=ENTRY_CONFIG, data=ENTRY_CONFIG,
source="test", source="test",
options={}, options={},
entry_id=1, entry_id=DEFAULT_CONFIG_ENTRY_ID,
) )
registry = er.async_get(hass) registry = er.async_get(hass)

View file

@ -28,13 +28,13 @@ MOCK_DATA2 = {"goodbye": "cruel world"}
@pytest.fixture @pytest.fixture
def store(hass): def store(hass):
"""Fixture of a store that prevents writing on Home Assistant stop.""" """Fixture of a store that prevents writing on Home Assistant stop."""
yield storage.Store(hass, MOCK_VERSION, MOCK_KEY) return storage.Store(hass, MOCK_VERSION, MOCK_KEY)
@pytest.fixture @pytest.fixture
def store_v_1_1(hass): def store_v_1_1(hass):
"""Fixture of a store that prevents writing on Home Assistant stop.""" """Fixture of a store that prevents writing on Home Assistant stop."""
yield storage.Store( return storage.Store(
hass, MOCK_VERSION, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 hass, MOCK_VERSION, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1
) )
@ -42,7 +42,7 @@ def store_v_1_1(hass):
@pytest.fixture @pytest.fixture
def store_v_1_2(hass): def store_v_1_2(hass):
"""Fixture of a store that prevents writing on Home Assistant stop.""" """Fixture of a store that prevents writing on Home Assistant stop."""
yield storage.Store( return storage.Store(
hass, MOCK_VERSION, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_2 hass, MOCK_VERSION, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_2
) )
@ -50,7 +50,7 @@ def store_v_1_2(hass):
@pytest.fixture @pytest.fixture
def store_v_2_1(hass): def store_v_2_1(hass):
"""Fixture of a store that prevents writing on Home Assistant stop.""" """Fixture of a store that prevents writing on Home Assistant stop."""
yield storage.Store( return storage.Store(
hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1
) )
@ -91,8 +91,8 @@ async def test_loading_parallel(hass, store, hass_storage, caplog):
results = await asyncio.gather(store.async_load(), store.async_load()) results = await asyncio.gather(store.async_load(), store.async_load())
assert results[0] is MOCK_DATA assert results[0] == MOCK_DATA
assert results[1] is MOCK_DATA assert results[0] is results[1]
assert caplog.text.count(f"Loading data for {store.key}") assert caplog.text.count(f"Loading data for {store.key}")
@ -436,3 +436,25 @@ async def test_legacy_migration(hass, hass_storage, store_v_1_2):
"minor_version": 1, "minor_version": 1,
"data": MOCK_DATA, "data": MOCK_DATA,
} }
async def test_changing_delayed_written_data(hass, store, hass_storage):
"""Test changing data that is written with delay."""
data_to_store = {"hello": "world"}
store.async_delay_save(lambda: data_to_store, 1)
assert store.key not in hass_storage
loaded_data = await store.async_load()
assert loaded_data == data_to_store
assert loaded_data is not data_to_store
loaded_data["hello"] = "earth"
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1))
await hass.async_block_till_done()
assert hass_storage[store.key] == {
"version": MOCK_VERSION,
"minor_version": 1,
"key": MOCK_KEY,
"data": {"hello": "world"},
}