diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index d7c5e7f0ea0..2932ea484c9 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -119,7 +119,10 @@ from .util import ( if TYPE_CHECKING: from . import Recorder -LIVE_MIGRATION_MIN_SCHEMA_VERSION = 0 +# Live schema migration supported starting from schema version 42 or newer +# Schema version 41 was introduced in HA Core 2023.4 +# Schema version 42 was introduced in HA Core 2023.11 +LIVE_MIGRATION_MIN_SCHEMA_VERSION = 42 MIGRATION_NOTE_OFFLINE = ( "Note: this may take several hours on large databases and slow machines. " diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 3eea231a659..dc99ddefa3b 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -87,10 +87,23 @@ async def test_schema_update_calls( call(instance, hass, engine, session_maker, version + 1, 0) for version in range(db_schema.SCHEMA_VERSION) ] - status = migration.SchemaValidationStatus(0, True, set(), 0) assert migrate_schema.mock_calls == [ - call(instance, hass, engine, session_maker, status, 0), - call(instance, hass, engine, session_maker, status, db_schema.SCHEMA_VERSION), + call( + instance, + hass, + engine, + session_maker, + migration.SchemaValidationStatus(0, True, set(), 0), + 42, + ), + call( + instance, + hass, + engine, + session_maker, + migration.SchemaValidationStatus(42, True, set(), 0), + db_schema.SCHEMA_VERSION, + ), ] @@ -117,7 +130,9 @@ async def test_migration_in_progress( new=create_engine_test, ), ): - await async_setup_recorder_instance(hass, wait_recorder=False) + await async_setup_recorder_instance( + hass, wait_recorder=False, wait_recorder_setup=False + ) await hass.async_add_executor_job(instrument_migration.migration_started.wait) assert recorder.util.async_migration_in_progress(hass) is True @@ -129,8 +144,25 @@ async def test_migration_in_progress( assert recorder.get_instance(hass).schema_version == SCHEMA_VERSION +@pytest.mark.parametrize( + ( + "func_to_patch", + "expected_setup_result", + "expected_pn_create", + "expected_pn_dismiss", + ), + [ + ("migrate_schema_non_live", False, 1, 0), + ("migrate_schema_live", True, 2, 1), + ], +) async def test_database_migration_failed( - hass: HomeAssistant, async_setup_recorder_instance: RecorderInstanceGenerator + hass: HomeAssistant, + async_setup_recorder_instance: RecorderInstanceGenerator, + func_to_patch: str, + expected_setup_result: bool, + expected_pn_create: int, + expected_pn_dismiss: int, ) -> None: """Test we notify if the migration fails.""" assert recorder.util.async_migration_in_progress(hass) is False @@ -141,7 +173,7 @@ async def test_database_migration_failed( new=create_engine_test, ), patch( - "homeassistant.components.recorder.migration._apply_update", + f"homeassistant.components.recorder.migration.{func_to_patch}", side_effect=ValueError, ), patch( @@ -153,7 +185,9 @@ async def test_database_migration_failed( side_effect=pn.dismiss, ) as mock_dismiss, ): - await async_setup_recorder_instance(hass, wait_recorder=False) + await async_setup_recorder_instance( + hass, wait_recorder=False, expected_setup_result=expected_setup_result + ) hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await hass.async_block_till_done() @@ -161,8 +195,8 @@ async def test_database_migration_failed( await hass.async_block_till_done() assert recorder.util.async_migration_in_progress(hass) is False - assert len(mock_create.mock_calls) == 2 - assert len(mock_dismiss.mock_calls) == 1 + assert len(mock_create.mock_calls) == expected_pn_create + assert len(mock_dismiss.mock_calls) == expected_pn_dismiss @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) @@ -346,7 +380,7 @@ async def test_events_during_migration_are_queued( ), ): await async_setup_recorder_instance( - hass, {"commit_interval": 0}, wait_recorder=False + hass, {"commit_interval": 0}, wait_recorder=False, wait_recorder_setup=False ) await hass.async_add_executor_job(instrument_migration.migration_started.wait) assert recorder.util.async_migration_in_progress(hass) is True @@ -389,7 +423,7 @@ async def test_events_during_migration_queue_exhausted( ), ): await async_setup_recorder_instance( - hass, {"commit_interval": 0}, wait_recorder=False + hass, {"commit_interval": 0}, wait_recorder=False, wait_recorder_setup=False ) await hass.async_add_executor_job(instrument_migration.migration_started.wait) assert recorder.util.async_migration_in_progress(hass) is True @@ -421,7 +455,15 @@ async def test_events_during_migration_queue_exhausted( @pytest.mark.parametrize( ("start_version", "live"), - [(0, True), (9, True), (16, True), (18, True), (22, True), (25, True), (43, True)], + [ + (0, False), + (9, False), + (16, False), + (18, False), + (22, False), + (25, False), + (43, True), + ], ) async def test_schema_migrate( hass: HomeAssistant, @@ -500,7 +542,9 @@ async def test_schema_migrate( "homeassistant.components.recorder.Recorder._pre_process_startup_events", ), ): - await async_setup_recorder_instance(hass, wait_recorder=False) + await async_setup_recorder_instance( + hass, wait_recorder=False, wait_recorder_setup=live + ) await hass.async_add_executor_job(instrument_migration.migration_started.wait) assert recorder.util.async_migration_in_progress(hass) is True await recorder_helper.async_wait_recorder(hass) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index ed36f4dacbf..8efbf226bc1 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -2555,7 +2555,9 @@ async def test_recorder_info_migration_queue_exhausted( recorder.core, "MIN_AVAILABLE_MEMORY_FOR_QUEUE_BACKLOG", sys.maxsize ), ): - async with async_test_recorder(hass, wait_recorder=False): + async with async_test_recorder( + hass, wait_recorder=False, wait_recorder_setup=False + ): await hass.async_add_executor_job( instrument_migration.migration_started.wait ) diff --git a/tests/conftest.py b/tests/conftest.py index de0dbc2e0d2..0d0fd826b44 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1399,6 +1399,7 @@ async def _async_init_recorder_component( db_url: str | None = None, *, expected_setup_result: bool, + wait_setup: bool, ) -> None: """Initialize the recorder asynchronously.""" # pylint: disable-next=import-outside-toplevel @@ -1416,10 +1417,14 @@ async def _async_init_recorder_component( setup_task = asyncio.ensure_future( async_setup_component(hass, recorder.DOMAIN, {recorder.DOMAIN: config}) ) - # Wait for recorder integration to setup - setup_result = await setup_task - assert setup_result == expected_setup_result - assert (recorder.DOMAIN in hass.config.components) == expected_setup_result + if wait_setup: + # Wait for recorder integration to setup + setup_result = await setup_task + assert setup_result == expected_setup_result + assert (recorder.DOMAIN in hass.config.components) == expected_setup_result + else: + # Wait for recorder to connect to the database + await recorder_helper.async_wait_recorder(hass) _LOGGER.info( "Test recorder successfully started, database location: %s", config[recorder.CONF_DB_URL], @@ -1585,6 +1590,7 @@ async def async_test_recorder( *, expected_setup_result: bool = True, wait_recorder: bool = True, + wait_recorder_setup: bool = True, ) -> AsyncGenerator[recorder.Recorder]: """Setup and return recorder instance.""" # noqa: D401 await _async_init_recorder_component( @@ -1592,6 +1598,7 @@ async def async_test_recorder( config, recorder_db_url, expected_setup_result=expected_setup_result, + wait_setup=wait_recorder_setup, ) await hass.async_block_till_done() instance = hass.data[recorder.DATA_INSTANCE] @@ -1621,6 +1628,7 @@ async def async_setup_recorder_instance( *, expected_setup_result: bool = True, wait_recorder: bool = True, + wait_recorder_setup: bool = True, ) -> AsyncGenerator[recorder.Recorder]: """Set up and return recorder instance.""" @@ -1630,6 +1638,7 @@ async def async_setup_recorder_instance( config, expected_setup_result=expected_setup_result, wait_recorder=wait_recorder, + wait_recorder_setup=wait_recorder_setup, ) )