core/tests/helpers/test_storage.py

461 lines
14 KiB
Python

"""Tests for the storage helper."""
import asyncio
from datetime import timedelta
import json
from unittest.mock import Mock, patch
import pytest
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
from tests.common import async_fire_time_changed
MOCK_VERSION = 1
MOCK_VERSION_2 = 2
MOCK_MINOR_VERSION_1 = 1
MOCK_MINOR_VERSION_2 = 2
MOCK_KEY = "storage-test"
MOCK_DATA = {"hello": "world"}
MOCK_DATA2 = {"goodbye": "cruel world"}
@pytest.fixture
def store(hass):
"""Fixture of a store that prevents writing on Home Assistant stop."""
return storage.Store(hass, MOCK_VERSION, MOCK_KEY)
@pytest.fixture
def store_v_1_1(hass):
"""Fixture of a store that prevents writing on Home Assistant stop."""
return storage.Store(
hass, MOCK_VERSION, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1
)
@pytest.fixture
def store_v_1_2(hass):
"""Fixture of a store that prevents writing on Home Assistant stop."""
return storage.Store(
hass, MOCK_VERSION, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_2
)
@pytest.fixture
def store_v_2_1(hass):
"""Fixture of a store that prevents writing on Home Assistant stop."""
return storage.Store(
hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1
)
async def test_loading(hass, store):
"""Test we can save and load data."""
await store.async_save(MOCK_DATA)
data = await store.async_load()
assert data == MOCK_DATA
async def test_custom_encoder(hass):
"""Test we can save and load data."""
class JSONEncoder(json.JSONEncoder):
"""Mock JSON encoder."""
def default(self, o):
"""Mock JSON encode method."""
return "9"
store = storage.Store(hass, MOCK_VERSION, MOCK_KEY, encoder=JSONEncoder)
await store.async_save(Mock())
data = await store.async_load()
assert data == "9"
async def test_loading_non_existing(hass, store):
"""Test we can save and load data."""
with patch("homeassistant.util.json.open", side_effect=FileNotFoundError):
data = await store.async_load()
assert data is None
async def test_loading_parallel(hass, store, hass_storage, caplog):
"""Test we can save and load data."""
hass_storage[store.key] = {"version": MOCK_VERSION, "data": MOCK_DATA}
results = await asyncio.gather(store.async_load(), store.async_load())
assert results[0] == MOCK_DATA
assert results[0] is results[1]
assert caplog.text.count(f"Loading data for {store.key}")
async def test_saving_with_delay(hass, store, hass_storage):
"""Test saving data after a delay."""
store.async_delay_save(lambda: MOCK_DATA, 1)
assert store.key not in hass_storage
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": MOCK_DATA,
}
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, 5)
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))
await hass.async_block_till_done()
assert store.key not in hass_storage
hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE)
await hass.async_block_till_done()
assert hass_storage[store.key] == {
"version": MOCK_VERSION,
"minor_version": 1,
"key": MOCK_KEY,
"data": MOCK_DATA,
}
async def test_not_delayed_saving_while_stopping(hass, hass_storage):
"""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)
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=2))
await hass.async_block_till_done()
assert store.key not in hass_storage
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)
hass.state = CoreState.stopping
await hass.async_block_till_done()
assert store.key not in hass_storage
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):
"""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
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"})
assert hass_storage[store.key] == {
"version": MOCK_VERSION,
"minor_version": 1,
"key": MOCK_KEY,
"data": {"delay": "no"},
}
store.async_delay_save(lambda: {"delay": "yes"}, 1)
assert hass_storage[store.key] == {
"version": MOCK_VERSION,
"minor_version": 1,
"key": MOCK_KEY,
"data": {"delay": "no"},
}
data = await store.async_load()
assert data == {"delay": "yes"}
async def test_writing_while_writing_delay(hass, store, hass_storage):
"""Test a write while a write with delay is active."""
store.async_delay_save(lambda: {"delay": "yes"}, 1)
assert store.key not in hass_storage
await store.async_save({"delay": "no"})
assert hass_storage[store.key] == {
"version": MOCK_VERSION,
"minor_version": 1,
"key": MOCK_KEY,
"data": {"delay": "no"},
}
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": {"delay": "no"},
}
data = await store.async_load()
assert data == {"delay": "no"}
async def test_multiple_delay_save_calls(hass, store, hass_storage):
"""Test a write while a write with changing delays."""
store.async_delay_save(lambda: {"delay": "yes"}, 1)
store.async_delay_save(lambda: {"delay": "yes"}, 2)
store.async_delay_save(lambda: {"delay": "yes"}, 3)
assert store.key not in hass_storage
await store.async_save({"delay": "no"})
assert hass_storage[store.key] == {
"version": MOCK_VERSION,
"minor_version": 1,
"key": MOCK_KEY,
"data": {"delay": "no"},
}
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": {"delay": "no"},
}
data = await store.async_load()
assert data == {"delay": "no"}
async def test_multiple_save_calls(hass, store, hass_storage):
"""Test multiple write tasks."""
assert store.key not in hass_storage
tasks = [store.async_save({"savecount": savecount}) for savecount in range(6)]
await asyncio.gather(*tasks)
assert hass_storage[store.key] == {
"version": MOCK_VERSION,
"minor_version": 1,
"key": MOCK_KEY,
"data": {"savecount": 5},
}
data = await store.async_load()
assert data == {"savecount": 5}
async def test_migrator_no_existing_config(hass, store, hass_storage):
"""Test migrator with no existing config."""
with patch("os.path.isfile", return_value=False), patch.object(
store, "async_load", return_value={"cur": "config"}
):
data = await storage.async_migrator(hass, "old-path", store)
assert data == {"cur": "config"}
assert store.key not in hass_storage
async def test_migrator_existing_config(hass, store, hass_storage):
"""Test migrating existing config."""
with patch("os.path.isfile", return_value=True), patch("os.remove") as mock_remove:
data = await storage.async_migrator(
hass, "old-path", store, old_conf_load_func=lambda _: {"old": "config"}
)
assert len(mock_remove.mock_calls) == 1
assert data == {"old": "config"}
assert hass_storage[store.key] == {
"key": MOCK_KEY,
"version": MOCK_VERSION,
"minor_version": 1,
"data": data,
}
async def test_migrator_transforming_config(hass, store, hass_storage):
"""Test migrating config to new format."""
async def old_conf_migrate_func(old_config):
"""Migrate old config to new format."""
return {"new": old_config["old"]}
with patch("os.path.isfile", return_value=True), patch("os.remove") as mock_remove:
data = await storage.async_migrator(
hass,
"old-path",
store,
old_conf_migrate_func=old_conf_migrate_func,
old_conf_load_func=lambda _: {"old": "config"},
)
assert len(mock_remove.mock_calls) == 1
assert data == {"new": "config"}
assert hass_storage[store.key] == {
"key": MOCK_KEY,
"version": MOCK_VERSION,
"minor_version": 1,
"data": data,
}
async def test_minor_version_default(hass, store, hass_storage):
"""Test minor version default."""
await store.async_save(MOCK_DATA)
assert hass_storage[store.key]["minor_version"] == 1
async def test_minor_version(hass, store_v_1_2, hass_storage):
"""Test minor version."""
await store_v_1_2.async_save(MOCK_DATA)
assert hass_storage[store_v_1_2.key]["minor_version"] == MOCK_MINOR_VERSION_2
async def test_migrate_major_not_implemented_raises(hass, store, store_v_2_1):
"""Test migrating between major versions fails if not implemented."""
await store_v_2_1.async_save(MOCK_DATA)
with pytest.raises(NotImplementedError):
await store.async_load()
async def test_migrate_minor_not_implemented(
hass, hass_storage, store_v_1_1, store_v_1_2
):
"""Test migrating between minor versions does not fail if not implemented."""
assert store_v_1_1.key == store_v_1_2.key
await store_v_1_1.async_save(MOCK_DATA)
assert hass_storage[store_v_1_1.key] == {
"key": MOCK_KEY,
"version": MOCK_VERSION,
"minor_version": MOCK_MINOR_VERSION_1,
"data": MOCK_DATA,
}
data = await store_v_1_2.async_load()
assert hass_storage[store_v_1_1.key]["data"] == data
await store_v_1_2.async_save(MOCK_DATA)
assert hass_storage[store_v_1_2.key] == {
"key": MOCK_KEY,
"version": MOCK_VERSION,
"minor_version": MOCK_MINOR_VERSION_2,
"data": MOCK_DATA,
}
async def test_migration(hass, hass_storage, store_v_1_2):
"""Test migration."""
calls = 0
class CustomStore(storage.Store):
async def _async_migrate_func(
self, old_major_version, old_minor_version, old_data: dict
):
nonlocal calls
calls += 1
assert old_major_version == store_v_1_2.version
assert old_minor_version == store_v_1_2.minor_version
return old_data
await store_v_1_2.async_save(MOCK_DATA)
assert hass_storage[store_v_1_2.key] == {
"key": MOCK_KEY,
"version": MOCK_VERSION,
"minor_version": MOCK_MINOR_VERSION_2,
"data": MOCK_DATA,
}
assert calls == 0
legacy_store = CustomStore(hass, 2, store_v_1_2.key, minor_version=1)
data = await legacy_store.async_load()
assert calls == 1
assert hass_storage[store_v_1_2.key]["data"] == data
await legacy_store.async_save(MOCK_DATA)
assert hass_storage[legacy_store.key] == {
"key": MOCK_KEY,
"version": 2,
"minor_version": 1,
"data": MOCK_DATA,
}
async def test_legacy_migration(hass, hass_storage, store_v_1_2):
"""Test legacy migration method signature."""
calls = 0
class LegacyStore(storage.Store):
async def _async_migrate_func(self, old_version, old_data: dict):
nonlocal calls
calls += 1
assert old_version == store_v_1_2.version
return old_data
await store_v_1_2.async_save(MOCK_DATA)
assert hass_storage[store_v_1_2.key] == {
"key": MOCK_KEY,
"version": MOCK_VERSION,
"minor_version": MOCK_MINOR_VERSION_2,
"data": MOCK_DATA,
}
assert calls == 0
legacy_store = LegacyStore(hass, 2, store_v_1_2.key, minor_version=1)
data = await legacy_store.async_load()
assert calls == 1
assert hass_storage[store_v_1_2.key]["data"] == data
await legacy_store.async_save(MOCK_DATA)
assert hass_storage[legacy_store.key] == {
"key": MOCK_KEY,
"version": 2,
"minor_version": 1,
"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"},
}