diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index fdc0591e70f..890cc2e1a8f 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -7,6 +7,7 @@ from collections.abc import Callable, Iterable from concurrent.futures import CancelledError import contextlib from datetime import datetime, timedelta +from functools import cached_property import logging import queue import sqlite3 @@ -258,7 +259,7 @@ class Recorder(threading.Thread): """Return the number of items in the recorder backlog.""" return self._queue.qsize() - @property + @cached_property def dialect_name(self) -> SupportedDialect | None: """Return the dialect the recorder uses.""" return self._dialect_name @@ -1446,6 +1447,7 @@ class Recorder(threading.Thread): self.engine = create_engine(self.db_url, **kwargs, future=True) self._dialect_name = try_parse_enum(SupportedDialect, self.engine.dialect.name) + self.__dict__.pop("dialect_name", None) sqlalchemy_event.listen(self.engine, "connect", self._setup_recorder_connection) Base.metadata.create_all(self.engine) diff --git a/tests/components/recorder/auto_repairs/events/test_schema.py b/tests/components/recorder/auto_repairs/events/test_schema.py index 5713e287222..e3b2638eded 100644 --- a/tests/components/recorder/auto_repairs/events/test_schema.py +++ b/tests/components/recorder/auto_repairs/events/test_schema.py @@ -17,16 +17,14 @@ async def test_validate_db_schema_fix_float_issue( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - db_engine, + db_engine: str, + recorder_dialect_name: None, ) -> None: """Test validating DB schema with postgresql and mysql. Note: The test uses SQLite, the purpose is only to exercise the code. """ with ( - patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", db_engine - ), patch( "homeassistant.components.recorder.auto_repairs.schema._validate_db_schema_precision", return_value={"events.double precision"}, @@ -50,17 +48,19 @@ async def test_validate_db_schema_fix_float_issue( @pytest.mark.parametrize("enable_schema_validation", [True]) +@pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_utf8_issue_event_data( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + db_engine: str, + recorder_dialect_name: None, ) -> None: """Test validating DB schema with MySQL. Note: The test uses SQLite, the purpose is only to exercise the code. """ with ( - patch("homeassistant.components.recorder.core.Recorder.dialect_name", "mysql"), patch( "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_supports_utf8", return_value={"event_data.4-byte UTF-8"}, @@ -81,17 +81,19 @@ async def test_validate_db_schema_fix_utf8_issue_event_data( @pytest.mark.parametrize("enable_schema_validation", [True]) +@pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_collation_issue( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + db_engine: str, + recorder_dialect_name: None, ) -> None: """Test validating DB schema with MySQL. Note: The test uses SQLite, the purpose is only to exercise the code. """ with ( - patch("homeassistant.components.recorder.core.Recorder.dialect_name", "mysql"), patch( "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_has_correct_collation", return_value={"events.utf8mb4_unicode_ci"}, diff --git a/tests/components/recorder/auto_repairs/states/test_schema.py b/tests/components/recorder/auto_repairs/states/test_schema.py index 7d14a873bfe..58910a4441a 100644 --- a/tests/components/recorder/auto_repairs/states/test_schema.py +++ b/tests/components/recorder/auto_repairs/states/test_schema.py @@ -17,16 +17,14 @@ async def test_validate_db_schema_fix_float_issue( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - db_engine, + db_engine: str, + recorder_dialect_name: None, ) -> None: """Test validating DB schema with postgresql and mysql. Note: The test uses SQLite, the purpose is only to exercise the code. """ with ( - patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", db_engine - ), patch( "homeassistant.components.recorder.auto_repairs.schema._validate_db_schema_precision", return_value={"states.double precision"}, @@ -52,17 +50,19 @@ async def test_validate_db_schema_fix_float_issue( @pytest.mark.parametrize("enable_schema_validation", [True]) +@pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_utf8_issue_states( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + db_engine: str, + recorder_dialect_name: None, ) -> None: """Test validating DB schema with MySQL. Note: The test uses SQLite, the purpose is only to exercise the code. """ with ( - patch("homeassistant.components.recorder.core.Recorder.dialect_name", "mysql"), patch( "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_supports_utf8", return_value={"states.4-byte UTF-8"}, @@ -82,17 +82,19 @@ async def test_validate_db_schema_fix_utf8_issue_states( @pytest.mark.parametrize("enable_schema_validation", [True]) +@pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_utf8_issue_state_attributes( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + db_engine: str, + recorder_dialect_name: None, ) -> None: """Test validating DB schema with MySQL. Note: The test uses SQLite, the purpose is only to exercise the code. """ with ( - patch("homeassistant.components.recorder.core.Recorder.dialect_name", "mysql"), patch( "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_supports_utf8", return_value={"state_attributes.4-byte UTF-8"}, @@ -113,17 +115,19 @@ async def test_validate_db_schema_fix_utf8_issue_state_attributes( @pytest.mark.parametrize("enable_schema_validation", [True]) +@pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_collation_issue( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + db_engine: str, + recorder_dialect_name: None, ) -> None: """Test validating DB schema with MySQL. Note: The test uses SQLite, the purpose is only to exercise the code. """ with ( - patch("homeassistant.components.recorder.core.Recorder.dialect_name", "mysql"), patch( "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_has_correct_collation", return_value={"states.utf8mb4_unicode_ci"}, diff --git a/tests/components/recorder/auto_repairs/statistics/test_schema.py b/tests/components/recorder/auto_repairs/statistics/test_schema.py index 0badceee0d2..f4e1d74aadf 100644 --- a/tests/components/recorder/auto_repairs/statistics/test_schema.py +++ b/tests/components/recorder/auto_repairs/statistics/test_schema.py @@ -11,18 +11,20 @@ from ...common import async_wait_recording_done from tests.typing import RecorderInstanceGenerator +@pytest.mark.parametrize("db_engine", ["mysql"]) @pytest.mark.parametrize("enable_schema_validation", [True]) async def test_validate_db_schema_fix_utf8_issue( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + db_engine: str, + recorder_dialect_name: None, ) -> None: """Test validating DB schema with MySQL. Note: The test uses SQLite, the purpose is only to exercise the code. """ with ( - patch("homeassistant.components.recorder.core.Recorder.dialect_name", "mysql"), patch( "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_supports_utf8", return_value={"statistics_meta.4-byte UTF-8"}, @@ -51,15 +53,13 @@ async def test_validate_db_schema_fix_float_issue( caplog: pytest.LogCaptureFixture, table: str, db_engine: str, + recorder_dialect_name: None, ) -> None: """Test validating DB schema with postgresql and mysql. Note: The test uses SQLite, the purpose is only to exercise the code. """ with ( - patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", db_engine - ), patch( "homeassistant.components.recorder.auto_repairs.schema._validate_db_schema_precision", return_value={f"{table}.double precision"}, @@ -90,17 +90,19 @@ async def test_validate_db_schema_fix_float_issue( @pytest.mark.parametrize("enable_schema_validation", [True]) +@pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_collation_issue( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + recorder_dialect_name: None, + db_engine: str, ) -> None: """Test validating DB schema with MySQL. Note: The test uses SQLite, the purpose is only to exercise the code. """ with ( - patch("homeassistant.components.recorder.core.Recorder.dialect_name", "mysql"), patch( "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_has_correct_collation", return_value={"statistics.utf8mb4_unicode_ci"}, diff --git a/tests/components/recorder/auto_repairs/test_schema.py b/tests/components/recorder/auto_repairs/test_schema.py index 14c74e2614e..d921c0cdbf8 100644 --- a/tests/components/recorder/auto_repairs/test_schema.py +++ b/tests/components/recorder/auto_repairs/test_schema.py @@ -1,7 +1,5 @@ """The test validating and repairing schema.""" -from unittest.mock import patch - import pytest from sqlalchemy import text @@ -28,17 +26,15 @@ async def test_validate_db_schema( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - db_engine, + db_engine: str, + recorder_dialect_name: None, ) -> None: """Test validating DB schema with MySQL and PostgreSQL. Note: The test uses SQLite, the purpose is only to exercise the code. """ - with patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", db_engine - ): - await async_setup_recorder_instance(hass) - await async_wait_recording_done(hass) + await async_setup_recorder_instance(hass) + await async_wait_recording_done(hass) assert "Schema validation failed" not in caplog.text assert "Detected statistics schema errors" not in caplog.text assert "Database is about to correct DB schema errors" not in caplog.text diff --git a/tests/components/recorder/conftest.py b/tests/components/recorder/conftest.py new file mode 100644 index 00000000000..834a8c0a16b --- /dev/null +++ b/tests/components/recorder/conftest.py @@ -0,0 +1,26 @@ +"""Fixtures for the recorder component tests.""" + +from collections.abc import Generator +from unittest.mock import patch + +import pytest + +from homeassistant.components import recorder +from homeassistant.core import HomeAssistant + + +@pytest.fixture +def recorder_dialect_name( + hass: HomeAssistant, db_engine: str +) -> Generator[None, None, None]: + """Patch the recorder dialect.""" + if instance := hass.data.get(recorder.DATA_INSTANCE): + instance.__dict__.pop("dialect_name", None) + with patch.object(instance, "_dialect_name", db_engine): + yield + instance.__dict__.pop("dialect_name", None) + else: + with patch( + "homeassistant.components.recorder.Recorder.dialect_name", db_engine + ): + yield diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 006e6311109..207f74bc01c 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -8,7 +8,7 @@ from datetime import datetime, timedelta from pathlib import Path import sqlite3 import threading -from typing import cast +from typing import Any, cast from unittest.mock import MagicMock, Mock, patch from freezegun.api import FrozenDateTimeFactory @@ -293,7 +293,7 @@ async def test_saving_state(hass: HomeAssistant, setup_recorder: None) -> None: @pytest.mark.parametrize( - ("dialect_name", "expected_attributes"), + ("db_engine", "expected_attributes"), [ (SupportedDialect.MYSQL, {"test_attr": 5, "test_attr_10": "silly\0stuff"}), (SupportedDialect.POSTGRESQL, {"test_attr": 5, "test_attr_10": "silly"}), @@ -301,18 +301,19 @@ async def test_saving_state(hass: HomeAssistant, setup_recorder: None) -> None: ], ) async def test_saving_state_with_nul( - hass: HomeAssistant, setup_recorder: None, dialect_name, expected_attributes + hass: HomeAssistant, + db_engine: str, + recorder_dialect_name: None, + setup_recorder: None, + expected_attributes: dict[str, Any], ) -> None: """Test saving and restoring a state with nul in attributes.""" entity_id = "test.recorder" state = "restoring_from_db" attributes = {"test_attr": 5, "test_attr_10": "silly\0stuff"} - with patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", dialect_name - ): - hass.states.async_set(entity_id, state, attributes) - await async_wait_recording_done(hass) + hass.states.async_set(entity_id, state, attributes) + await async_wait_recording_done(hass) with session_scope(hass=hass, read_only=True) as session: db_states = [] @@ -2071,18 +2072,19 @@ async def test_in_memory_database( assert "In-memory SQLite database is not supported" in caplog.text +@pytest.mark.parametrize("db_engine", ["mysql"]) async def test_database_connection_keep_alive( hass: HomeAssistant, + recorder_dialect_name: None, async_setup_recorder_instance: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture, ) -> None: """Test we keep alive socket based dialects.""" - with patch("homeassistant.components.recorder.Recorder.dialect_name"): - instance = await async_setup_recorder_instance(hass) - # We have to mock this since we don't have a mock - # MySQL server available in tests. - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await instance.async_recorder_ready.wait() + instance = await async_setup_recorder_instance(hass) + # We have to mock this since we don't have a mock + # MySQL server available in tests. + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await instance.async_recorder_ready.wait() async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=recorder.core.KEEPALIVE_TIME) diff --git a/tests/components/recorder/test_system_health.py b/tests/components/recorder/test_system_health.py index ee4217dab69..fbcefa0b13e 100644 --- a/tests/components/recorder/test_system_health.py +++ b/tests/components/recorder/test_system_health.py @@ -37,18 +37,18 @@ async def test_recorder_system_health( @pytest.mark.parametrize( - "dialect_name", [SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL] + "db_engine", [SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL] ) async def test_recorder_system_health_alternate_dbms( - recorder_mock: Recorder, hass: HomeAssistant, dialect_name + recorder_mock: Recorder, + hass: HomeAssistant, + db_engine: SupportedDialect, + recorder_dialect_name: None, ) -> None: """Test recorder system health.""" assert await async_setup_component(hass, "system_health", {}) await async_wait_recording_done(hass) with ( - patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", dialect_name - ), patch( "sqlalchemy.orm.session.Session.execute", return_value=Mock(scalar=Mock(return_value=("1048576"))), @@ -60,16 +60,19 @@ async def test_recorder_system_health_alternate_dbms( "current_recorder_run": instance.recorder_runs_manager.current.start, "oldest_recorder_run": instance.recorder_runs_manager.first.start, "estimated_db_size": "1.00 MiB", - "database_engine": dialect_name.value, + "database_engine": db_engine.value, "database_version": ANY, } @pytest.mark.parametrize( - "dialect_name", [SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL] + "db_engine", [SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL] ) async def test_recorder_system_health_db_url_missing_host( - recorder_mock: Recorder, hass: HomeAssistant, dialect_name + recorder_mock: Recorder, + hass: HomeAssistant, + db_engine: SupportedDialect, + recorder_dialect_name: None, ) -> None: """Test recorder system health with a db_url without a hostname.""" assert await async_setup_component(hass, "system_health", {}) @@ -77,9 +80,6 @@ async def test_recorder_system_health_db_url_missing_host( instance = get_instance(hass) with ( - patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", dialect_name - ), patch.object( instance, "db_url", @@ -95,7 +95,7 @@ async def test_recorder_system_health_db_url_missing_host( "current_recorder_run": instance.recorder_runs_manager.current.start, "oldest_recorder_run": instance.recorder_runs_manager.first.start, "estimated_db_size": "1.00 MiB", - "database_engine": dialect_name.value, + "database_engine": db_engine.value, "database_version": ANY, }