From 1b24e5bc162fbcaa8ca65aef5ee9383fbaa2bb99 Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Thu, 25 Apr 2024 05:05:12 +0000 Subject: [PATCH] Bump version --- README.md | 2 +- ha_version | 2 +- requirements_test.txt | 13 ++- .../common.py | 94 ++++++++++++++----- .../components/recorder/common.py | 26 ++--- .../components/recorder/db_schema_0.py | 6 +- .../const.py | 6 +- .../plugins.py | 31 +++++- version | 2 +- 9 files changed, 128 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 7ce6b9e..a73dd71 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pytest-homeassistant-custom-component -![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2024.4.4&labelColor=blue) +![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2024.5.0b0&labelColor=blue) [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/MatthewFlamm/pytest-homeassistant-custom-component) diff --git a/ha_version b/ha_version index d5b5964..b9dad6f 100644 --- a/ha_version +++ b/ha_version @@ -1 +1 @@ -2024.4.4 \ No newline at end of file +2024.5.0b0 \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt index d25af33..1ffd3c2 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,32 +8,31 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt -coverage==7.4.4 +coverage==7.5.0 freezegun==1.4.0 mock-open==1.4.0 mypy-dev==1.10.0a3 pydantic==1.10.12 pylint-per-file-ignores==1.3.2 -pipdeptree==2.16.1 +pipdeptree==2.17.0 pytest-asyncio==0.23.6 pytest-aiohttp==1.0.5 pytest-cov==5.0.0 pytest-freezer==0.4.8 pytest-github-actions-annotate-failures==0.2.0 pytest-socket==0.7.0 -pytest-test-groups==1.0.3 pytest-sugar==1.0.0 pytest-timeout==2.3.1 pytest-unordered==0.6.0 pytest-picked==0.5.0 -pytest-xdist==3.3.1 +pytest-xdist==3.5.0 pytest==8.1.1 -requests-mock==1.11.0 +requests-mock==1.12.1 respx==0.21.0 syrupy==4.6.1 tqdm==4.66.2 -uv==0.1.24 -homeassistant==2024.4.4 +uv==0.1.35 +homeassistant==2024.5.0b0 SQLAlchemy==2.0.29 paho-mqtt==1.6.1 diff --git a/src/pytest_homeassistant_custom_component/common.py b/src/pytest_homeassistant_custom_component/common.py index 866a152..b2b5574 100644 --- a/src/pytest_homeassistant_custom_component/common.py +++ b/src/pytest_homeassistant_custom_component/common.py @@ -7,7 +7,6 @@ from __future__ import annotations import asyncio -from collections import OrderedDict from collections.abc import AsyncGenerator, Generator, Mapping, Sequence from contextlib import asynccontextmanager, contextmanager from datetime import UTC, datetime, timedelta @@ -28,6 +27,7 @@ from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401 import pytest +from syrupy import SnapshotAssertion import voluptuous as vol from homeassistant import auth, bootstrap, config_entries, loader @@ -81,7 +81,6 @@ translation, ) from homeassistant.helpers.dispatcher import ( - SignalType, async_dispatcher_connect, async_dispatcher_send, ) @@ -100,6 +99,7 @@ json_loads_array, json_loads_object, ) +from homeassistant.util.signal_type import SignalType from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.uuid as uuid_util import homeassistant.util.yaml.loader as yaml_loader @@ -268,7 +268,7 @@ def async_add_executor_job(target, *args): return orig_async_add_executor_job(target, *args) - def async_create_task(coroutine, name=None, eager_start=False): + def async_create_task(coroutine, name=None, eager_start=True): """Create task.""" if isinstance(coroutine, Mock) and not isinstance(coroutine, AsyncMock): fut = asyncio.Future() @@ -305,7 +305,6 @@ def async_create_task(coroutine, name=None, eager_start=False): hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, hass.config_entries._async_shutdown, - run_immediately=True, ) # Load the registries @@ -359,9 +358,9 @@ def async_create_task(coroutine, name=None, eager_start=False): hass.set_state(CoreState.running) - @callback - def clear_instance(event): + async def clear_instance(event): """Clear global instance.""" + await asyncio.sleep(0) # Give aiohttp one loop iteration to close INSTANCES.remove(hass) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, clear_instance) @@ -458,7 +457,7 @@ def async_fire_mqtt_message( mqtt_data: MqttData = hass.data["mqtt"] assert mqtt_data.client - mqtt_data.client._mqtt_handle_message(msg) + mqtt_data.client._async_mqtt_on_message(Mock(), None, msg) fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message) @@ -594,12 +593,12 @@ def json_round_trip(obj: Any) -> Any: def mock_state_change_event( hass: HomeAssistant, new_state: State, old_state: State | None = None ) -> None: - """Mock state change envent.""" - event_data = {"entity_id": new_state.entity_id, "new_state": new_state} - - if old_state: - event_data["old_state"] = old_state - + """Mock state change event.""" + event_data = { + "entity_id": new_state.entity_id, + "new_state": new_state, + "old_state": old_state, + } hass.bus.fire(EVENT_STATE_CHANGED, event_data, context=new_state.context) @@ -655,7 +654,9 @@ def mock_area_registry( fixture instead. """ registry = ar.AreaRegistry(hass) - registry.areas = mock_entries or OrderedDict() + registry.areas = ar.AreaRegistryItems() + for key, entry in mock_entries.items(): + registry.areas[key] = entry hass.data[ar.DATA_REGISTRY] = registry return registry @@ -677,7 +678,7 @@ def mock_device_registry( fixture instead. """ registry = dr.DeviceRegistry(hass) - registry.devices = dr.DeviceRegistryItems() + registry.devices = dr.ActiveDeviceRegistryItems() registry._device_data = registry.devices.data if mock_entries is None: mock_entries = {} @@ -921,9 +922,7 @@ def __init__( def _async_on_stop(_: Event) -> None: self.async_shutdown() - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_on_stop, run_immediately=True - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_on_stop) class MockToggleEntity(entity.ToggleEntity): @@ -1496,7 +1495,7 @@ def async_capture_events(hass: HomeAssistant, event_name: str) -> list[Event]: def capture_events(event: Event) -> None: events.append(event) - hass.bus.async_listen(event_name, capture_events, run_immediately=True) + hass.bus.async_listen(event_name, capture_events) return events @@ -1530,12 +1529,12 @@ class _HA_ANY: _other = _SENTINEL - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Test equal.""" self._other = other return True - def __ne__(self, other: Any) -> bool: + def __ne__(self, other: object) -> bool: """Test not equal.""" self._other = other return False @@ -1645,6 +1644,40 @@ def import_and_test_deprecated_constant( assert constant_name in module.__all__ +def import_and_test_deprecated_alias( + caplog: pytest.LogCaptureFixture, + module: ModuleType, + alias_name: str, + replacement: Any, + breaks_in_ha_version: str, +) -> None: + """Import and test deprecated alias replaced by a value. + + - Import deprecated alias + - Assert value is the same as the replacement + - Assert a warning is logged + - Assert the deprecated alias is included in the modules.__dir__() + - Assert the deprecated alias is included in the modules.__all__() + """ + replacement_name = f"{replacement.__module__}.{replacement.__name__}" + value = import_deprecated_constant(module, alias_name) + assert value == replacement + assert ( + module.__name__, + logging.WARNING, + ( + f"{alias_name} was used from test_constant_deprecation," + f" this is a deprecated alias which will be removed in HA Core {breaks_in_ha_version}. " + f"Use {replacement_name} instead, please report " + "it to the author of the 'test_constant_deprecation' custom integration" + ), + ) in caplog.record_tuples + + # verify deprecated alias is included in dir() + assert alias_name in dir(module) + assert alias_name in module.__all__ + + def help_test_all(module: ModuleType) -> None: """Test module.__all__ is correctly set.""" assert set(module.__all__) == { @@ -1707,3 +1740,22 @@ async def _async_setup_entry( mock_platform(hass, f"test.{domain}", platform, built_in=built_in) return platform + + +async def snapshot_platform( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + config_entry_id: str, +) -> None: + """Snapshot a platform.""" + entity_entries = er.async_entries_for_config_entry(entity_registry, config_entry_id) + assert entity_entries + assert ( + len({entity_entry.domain for entity_entry in entity_entries}) == 1 + ), "Please limit the loaded platforms to 1 platform." + for entity_entry in entity_entries: + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + assert entity_entry.disabled_by is None, "Please enable all entities." + assert (state := hass.states.get(entity_entry.entity_id)) + assert state == snapshot(name=f"{entity_entry.entity_id}-state") diff --git a/src/pytest_homeassistant_custom_component/components/recorder/common.py b/src/pytest_homeassistant_custom_component/components/recorder/common.py index 55c7a29..dd0db85 100644 --- a/src/pytest_homeassistant_custom_component/components/recorder/common.py +++ b/src/pytest_homeassistant_custom_component/components/recorder/common.py @@ -113,7 +113,9 @@ async def async_wait_recording_done(hass: HomeAssistant) -> None: await hass.async_block_till_done() -async def async_wait_purge_done(hass: HomeAssistant, max: int = None) -> None: +async def async_wait_purge_done( + hass: HomeAssistant, max_number: int | None = None +) -> None: """Wait for max number of purge events. Because a purge may insert another PurgeTask into @@ -121,9 +123,9 @@ async def async_wait_purge_done(hass: HomeAssistant, max: int = None) -> None: a maximum number of WaitTasks that we will put into the queue. """ - if not max: - max = DEFAULT_PURGE_TASKS - for _ in range(max + 1): + if not max_number: + max_number = DEFAULT_PURGE_TASKS + for _ in range(max_number + 1): await async_wait_recording_done(hass) @@ -329,10 +331,10 @@ def convert_pending_states_to_meta(instance: Recorder, session: Session) -> None entity_ids: set[str] = set() states: set[States] = set() states_meta_objects: dict[str, StatesMeta] = {} - for object in session: - if isinstance(object, States): - entity_ids.add(object.entity_id) - states.add(object) + for session_object in session: + if isinstance(session_object, States): + entity_ids.add(session_object.entity_id) + states.add(session_object) entity_id_to_metadata_ids = instance.states_meta_manager.get_many( entity_ids, session, True @@ -356,10 +358,10 @@ def convert_pending_events_to_event_types(instance: Recorder, session: Session) event_types: set[str] = set() events: set[Events] = set() event_types_objects: dict[str, EventTypes] = {} - for object in session: - if isinstance(object, Events): - event_types.add(object.event_type) - events.add(object) + for session_object in session: + if isinstance(session_object, Events): + event_types.add(session_object.event_type) + events.add(session_object) event_type_to_event_type_ids = instance.event_type_manager.get_many( event_types, session, True diff --git a/src/pytest_homeassistant_custom_component/components/recorder/db_schema_0.py b/src/pytest_homeassistant_custom_component/components/recorder/db_schema_0.py index 286d762..008e2df 100644 --- a/src/pytest_homeassistant_custom_component/components/recorder/db_schema_0.py +++ b/src/pytest_homeassistant_custom_component/components/recorder/db_schema_0.py @@ -34,7 +34,7 @@ _LOGGER = logging.getLogger(__name__) -class Events(Base): # type: ignore +class Events(Base): # type: ignore[valid-type,misc] """Event history data.""" __tablename__ = "events" @@ -70,7 +70,7 @@ def to_native(self): return None -class States(Base): # type: ignore +class States(Base): # type: ignore[valid-type,misc] """State change history.""" __tablename__ = "states" @@ -129,7 +129,7 @@ def to_native(self): return None -class RecorderRuns(Base): # type: ignore +class RecorderRuns(Base): # type: ignore[valid-type,misc] """Representation of recorder run.""" __tablename__ = "recorder_runs" diff --git a/src/pytest_homeassistant_custom_component/const.py b/src/pytest_homeassistant_custom_component/const.py index 89915b7..cb8f11f 100644 --- a/src/pytest_homeassistant_custom_component/const.py +++ b/src/pytest_homeassistant_custom_component/const.py @@ -3,9 +3,9 @@ This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component. """ -from typing import Final +from typing import TYPE_CHECKING, Final MAJOR_VERSION: Final = 2024 -MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "4" +MINOR_VERSION: Final = 5 +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/src/pytest_homeassistant_custom_component/plugins.py b/src/pytest_homeassistant_custom_component/plugins.py index 4d5177c..caadda7 100644 --- a/src/pytest_homeassistant_custom_component/plugins.py +++ b/src/pytest_homeassistant_custom_component/plugins.py @@ -66,6 +66,7 @@ label_registry as lr, recorder as recorder_helper, ) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType from homeassistant.setup import BASE_PLATFORMS, async_setup_component from homeassistant.util import location @@ -907,26 +908,45 @@ def __init__(self, mid: int) -> None: self.rc = 0 with patch("paho.mqtt.client.Client") as mock_client: + # The below use a call_soon for the on_publish/on_subscribe/on_unsubscribe + # callbacks to simulate the behavior of the real MQTT client which will + # not be synchronous. @ha.callback def _async_fire_mqtt_message(topic, payload, qos, retain): async_fire_mqtt_message(hass, topic, payload, qos, retain) mid = get_mid() - mock_client.on_publish(0, 0, mid) + hass.loop.call_soon(mock_client.on_publish, 0, 0, mid) return FakeInfo(mid) def _subscribe(topic, qos=0): mid = get_mid() - mock_client.on_subscribe(0, 0, mid) + hass.loop.call_soon(mock_client.on_subscribe, 0, 0, mid) return (0, mid) def _unsubscribe(topic): mid = get_mid() - mock_client.on_unsubscribe(0, 0, mid) + hass.loop.call_soon(mock_client.on_unsubscribe, 0, 0, mid) return (0, mid) + def _connect(*args, **kwargs): + # Connect always calls reconnect once, but we + # mock it out so we call reconnect to simulate + # the behavior. + mock_client.reconnect() + hass.loop.call_soon_threadsafe( + mock_client.on_connect, mock_client, None, 0, 0, 0 + ) + mock_client.on_socket_open( + mock_client, None, Mock(fileno=Mock(return_value=-1)) + ) + mock_client.on_socket_register_write( + mock_client, None, Mock(fileno=Mock(return_value=-1)) + ) + return 0 + mock_client = mock_client.return_value - mock_client.connect.return_value = 0 + mock_client.connect.side_effect = _connect mock_client.subscribe.side_effect = _subscribe mock_client.unsubscribe.side_effect = _unsubscribe mock_client.publish.side_effect = _async_fire_mqtt_message @@ -988,8 +1008,9 @@ async def _setup_mqtt_entry( # connected set to True to get a more realistic behavior when subscribing mock_mqtt_instance.connected = True + mqtt_client_mock.on_connect(mqtt_client_mock, None, 0, 0, 0) - hass.helpers.dispatcher.async_dispatcher_send(mqtt.MQTT_CONNECTED) + async_dispatcher_send(hass, mqtt.MQTT_CONNECTED) await hass.async_block_till_done() return mock_mqtt_instance diff --git a/version b/version index 2ded95b..1c3202f 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.13.115 +0.13.116