mirror of
https://github.com/home-assistant/core.git
synced 2025-12-24 21:06:19 +00:00
Live db migrations and recovery (#49036)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
@@ -1,19 +1,41 @@
|
||||
"""The tests for the Recorder component."""
|
||||
# pylint: disable=protected-access
|
||||
import datetime
|
||||
import sqlite3
|
||||
from unittest.mock import Mock, PropertyMock, call, patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.exc import InternalError, OperationalError, ProgrammingError
|
||||
from sqlalchemy.exc import (
|
||||
DatabaseError,
|
||||
InternalError,
|
||||
OperationalError,
|
||||
ProgrammingError,
|
||||
)
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from homeassistant.bootstrap import async_setup_component
|
||||
from homeassistant.components.recorder import RecorderRuns, const, migration, models
|
||||
from homeassistant.components import recorder
|
||||
from homeassistant.components.recorder import RecorderRuns, migration, models
|
||||
from homeassistant.components.recorder.const import DATA_INSTANCE
|
||||
from homeassistant.components.recorder.models import States
|
||||
from homeassistant.components.recorder.util import session_scope
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .common import async_wait_recording_done_without_instance
|
||||
|
||||
from tests.common import async_fire_time_changed, async_mock_service
|
||||
from tests.components.recorder import models_original
|
||||
|
||||
|
||||
def _get_native_states(hass, entity_id):
|
||||
with session_scope(hass=hass) as session:
|
||||
return [
|
||||
state.to_native()
|
||||
for state in session.query(States).filter(States.entity_id == entity_id)
|
||||
]
|
||||
|
||||
|
||||
def create_engine_test(*args, **kwargs):
|
||||
"""Test version of create_engine that initializes with old schema.
|
||||
|
||||
@@ -26,6 +48,7 @@ def create_engine_test(*args, **kwargs):
|
||||
|
||||
async def test_schema_update_calls(hass):
|
||||
"""Test that schema migrations occur in correct order."""
|
||||
await async_setup_component(hass, "persistent_notification", {})
|
||||
with patch(
|
||||
"homeassistant.components.recorder.create_engine", new=create_engine_test
|
||||
), patch(
|
||||
@@ -35,16 +58,147 @@ async def test_schema_update_calls(hass):
|
||||
await async_setup_component(
|
||||
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await async_wait_recording_done_without_instance(hass)
|
||||
|
||||
update.assert_has_calls(
|
||||
[
|
||||
call(hass.data[const.DATA_INSTANCE].engine, version + 1, 0)
|
||||
call(hass.data[DATA_INSTANCE].engine, version + 1, 0)
|
||||
for version in range(0, models.SCHEMA_VERSION)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def test_database_migration_failed(hass):
|
||||
"""Test we notify if the migration fails."""
|
||||
await async_setup_component(hass, "persistent_notification", {})
|
||||
create_calls = async_mock_service(hass, "persistent_notification", "create")
|
||||
dismiss_calls = async_mock_service(hass, "persistent_notification", "dismiss")
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.recorder.create_engine", new=create_engine_test
|
||||
), patch(
|
||||
"homeassistant.components.recorder.migration._apply_update",
|
||||
side_effect=ValueError,
|
||||
):
|
||||
await async_setup_component(
|
||||
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
|
||||
)
|
||||
hass.states.async_set("my.entity", "on", {})
|
||||
hass.states.async_set("my.entity", "off", {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(create_calls) == 2
|
||||
assert len(dismiss_calls) == 1
|
||||
|
||||
|
||||
async def test_database_migration_encounters_corruption(hass):
|
||||
"""Test we move away the database if its corrupt."""
|
||||
await async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
sqlite3_exception = DatabaseError("statement", {}, [])
|
||||
sqlite3_exception.__cause__ = sqlite3.DatabaseError()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.recorder.migration.schema_is_current",
|
||||
side_effect=[False, True],
|
||||
), patch(
|
||||
"homeassistant.components.recorder.migration.migrate_schema",
|
||||
side_effect=sqlite3_exception,
|
||||
), patch(
|
||||
"homeassistant.components.recorder.move_away_broken_database"
|
||||
) as move_away:
|
||||
await async_setup_component(
|
||||
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
|
||||
)
|
||||
hass.states.async_set("my.entity", "on", {})
|
||||
hass.states.async_set("my.entity", "off", {})
|
||||
await async_wait_recording_done_without_instance(hass)
|
||||
|
||||
assert move_away.called
|
||||
|
||||
|
||||
async def test_database_migration_encounters_corruption_not_sqlite(hass):
|
||||
"""Test we fail on database error when we cannot recover."""
|
||||
await async_setup_component(hass, "persistent_notification", {})
|
||||
create_calls = async_mock_service(hass, "persistent_notification", "create")
|
||||
dismiss_calls = async_mock_service(hass, "persistent_notification", "dismiss")
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.recorder.migration.schema_is_current",
|
||||
side_effect=[False, True],
|
||||
), patch(
|
||||
"homeassistant.components.recorder.migration.migrate_schema",
|
||||
side_effect=DatabaseError("statement", {}, []),
|
||||
), patch(
|
||||
"homeassistant.components.recorder.move_away_broken_database"
|
||||
) as move_away:
|
||||
await async_setup_component(
|
||||
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
|
||||
)
|
||||
hass.states.async_set("my.entity", "on", {})
|
||||
hass.states.async_set("my.entity", "off", {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not move_away.called
|
||||
assert len(create_calls) == 2
|
||||
assert len(dismiss_calls) == 1
|
||||
|
||||
|
||||
async def test_events_during_migration_are_queued(hass):
|
||||
"""Test that events during migration are queued."""
|
||||
|
||||
await async_setup_component(hass, "persistent_notification", {})
|
||||
with patch(
|
||||
"homeassistant.components.recorder.create_engine", new=create_engine_test
|
||||
):
|
||||
await async_setup_component(
|
||||
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
|
||||
)
|
||||
hass.states.async_set("my.entity", "on", {})
|
||||
hass.states.async_set("my.entity", "off", {})
|
||||
await hass.async_block_till_done()
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2))
|
||||
await hass.async_block_till_done()
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4))
|
||||
await hass.data[DATA_INSTANCE].async_recorder_ready.wait()
|
||||
await async_wait_recording_done_without_instance(hass)
|
||||
|
||||
db_states = await hass.async_add_executor_job(_get_native_states, hass, "my.entity")
|
||||
assert len(db_states) == 2
|
||||
|
||||
|
||||
async def test_events_during_migration_queue_exhausted(hass):
|
||||
"""Test that events during migration takes so long the queue is exhausted."""
|
||||
await async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.recorder.create_engine", new=create_engine_test
|
||||
), patch.object(recorder, "MAX_QUEUE_BACKLOG", 1):
|
||||
await async_setup_component(
|
||||
hass, "recorder", {"recorder": {"db_url": "sqlite://"}}
|
||||
)
|
||||
hass.states.async_set("my.entity", "on", {})
|
||||
await hass.async_block_till_done()
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2))
|
||||
await hass.async_block_till_done()
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4))
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("my.entity", "off", {})
|
||||
await hass.data[DATA_INSTANCE].async_recorder_ready.wait()
|
||||
await async_wait_recording_done_without_instance(hass)
|
||||
|
||||
db_states = await hass.async_add_executor_job(_get_native_states, hass, "my.entity")
|
||||
assert len(db_states) == 1
|
||||
hass.states.async_set("my.entity", "on", {})
|
||||
await async_wait_recording_done_without_instance(hass)
|
||||
db_states = await hass.async_add_executor_job(_get_native_states, hass, "my.entity")
|
||||
assert len(db_states) == 2
|
||||
|
||||
|
||||
async def test_schema_migrate(hass):
|
||||
"""Test the full schema migration logic.
|
||||
|
||||
@@ -53,6 +207,8 @@ async def test_schema_migrate(hass):
|
||||
inspection could quickly become quite cumbersome.
|
||||
"""
|
||||
|
||||
await async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
def _mock_setup_run(self):
|
||||
self.run_info = RecorderRuns(
|
||||
start=self.recording_start, created=dt_util.utcnow()
|
||||
|
||||
Reference in New Issue
Block a user