From 24fc47df6cdc8933ac6acfb9dc133a4079b2200e Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 5 May 2026 11:25:35 +0200 Subject: [PATCH 1/2] Replace fixed-duration sleeps after bus events with gather MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several tests use ``await asyncio.sleep(...)`` to "wait for the listener to run" after firing a bus event. The fixed duration is real wall-clock time and the wait can be indeterministic — if the handler chain happens to need slightly more time on a busy CI runner, the assertion races the handler. ``Bus.fire_event`` returns the listener tasks since #6252; capture and ``await asyncio.gather(*tasks)`` instead of sleeping. Touches test_bus.py (the bus tests were poking scheduling instead of verifying their assertions), test_home_assistant_watchdog.py, test_plugin_base.py, addons/test_manager.py, docker/test_addon.py, and test_store_execute_reload.py. Other cleanups in the same spirit: - ``_fire_test_event`` in addons/test_addon.py becomes ``async def`` and gathers the listener tasks itself, so its 17 call sites collapse to a single ``await _fire_test_event(...)``. - The two test_store_execute_reload.py sites that used the private ``_update_connectivity()`` helper are reworked to set the cached connectivity flag directly and fire the event themselves so they can gather the listener tasks the same way. - The two ``sleep(1)`` post-pull drains in docker/test_interface.py collapse to ``sleep(0)`` (handler tasks are already gathered inside pull_image), saving ~2s. - The ``sleep(0.01)`` waits inside ``container_events()`` task bodies (api/test_addons.py, api/test_store.py, backups/test_manager.py) are just one-yield-to-the-parent and become ``sleep(0)``. Switching to ``gather`` exposes a few latent test mocks that were silently swallowing TypeErrors as background-task failures before: - ``CGroup.add_devices_allowed`` is ``async def`` but was patched as a plain MagicMock in docker/test_addon.py — now patched via ``new_callable=AsyncMock``. - The watchdog does ``await (await self.start())`` / ``await (await self.restart())`` because ``App.start`` / ``App.restart`` return ``asyncio.Task``. The mocks in addons/test_addon.py (test_app_watchdog, test_watchdog_on_stop, test_watchdog_during_attach) needed ``AsyncMock(return_value=)`` to mirror that shape rather than a plain MagicMock. --- tests/addons/test_addon.py | 140 ++++++++++------- tests/addons/test_manager.py | 24 +-- tests/api/test_addons.py | 4 +- tests/api/test_store.py | 2 +- tests/backups/test_manager.py | 2 +- tests/docker/test_addon.py | 22 +-- tests/docker/test_interface.py | 4 +- .../test_home_assistant_watchdog.py | 144 ++++++++++-------- tests/plugins/test_plugin_base.py | 95 ++++++------ .../fixup/test_store_execute_reload.py | 28 ++-- tests/test_bus.py | 19 +-- 11 files changed, 270 insertions(+), 214 deletions(-) diff --git a/tests/addons/test_addon.py b/tests/addons/test_addon.py index 1e1b03e3b83..52ccb319aa4 100644 --- a/tests/addons/test_addon.py +++ b/tests/addons/test_addon.py @@ -6,7 +6,7 @@ from http import HTTPStatus from pathlib import Path, PurePath from typing import Any -from unittest.mock import MagicMock, PropertyMock, call, patch +from unittest.mock import AsyncMock, MagicMock, PropertyMock, call, patch import aiodocker from aiodocker.containers import DockerContainer @@ -51,16 +51,18 @@ from tests.const import TEST_ADDON_SLUG -def _fire_test_event(coresys: CoreSys, name: str, state: ContainerState): - """Fire a test event.""" - coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name=name, - state=state, - id="abc123", - time=1, - ), +async def _fire_test_event(coresys: CoreSys, name: str, state: ContainerState) -> None: + """Fire a test event and await the listener tasks the bus spawned.""" + await asyncio.gather( + *coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name=name, + state=state, + id="abc123", + time=1, + ), + ) ) @@ -125,25 +127,30 @@ async def test_app_state_listener(coresys: CoreSys, install_app_ssh: App) -> Non assert install_app_ssh.state == AppState.UNKNOWN with patch.object(App, "watchdog_container"): - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING) - await asyncio.sleep(0) + await _fire_test_event( + coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING + ) assert install_app_ssh.state == AppState.STARTED - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.STOPPED) - await asyncio.sleep(0) + await _fire_test_event( + coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.STOPPED + ) assert install_app_ssh.state == AppState.STOPPED - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.HEALTHY) - await asyncio.sleep(0) + await _fire_test_event( + coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.HEALTHY + ) assert install_app_ssh.state == AppState.STARTED - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.FAILED) - await asyncio.sleep(0) + await _fire_test_event( + coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.FAILED + ) assert install_app_ssh.state == AppState.ERROR # Test other apps are ignored - _fire_test_event(coresys, "addon_local_non_installed", ContainerState.RUNNING) - await asyncio.sleep(0) + await _fire_test_event( + coresys, "addon_local_non_installed", ContainerState.RUNNING + ) assert install_app_ssh.state == AppState.ERROR @@ -155,15 +162,20 @@ async def test_app_watchdog(coresys: CoreSys, install_app_ssh: App) -> None: install_app_ssh.watchdog = True install_app_ssh._manual_stop = False # pylint: disable=protected-access + # Watchdog does ``await (await self.start())`` because App.start returns + # an asyncio.Task. The mock must mirror that shape. + done_task = asyncio.get_running_loop().create_future() + done_task.set_result(None) with ( - patch.object(App, "restart") as restart, - patch.object(App, "start") as start, + patch.object(App, "restart", AsyncMock(return_value=done_task)) as restart, + patch.object(App, "start", AsyncMock(return_value=done_task)) as start, patch.object(DockerApp, "current_state") as current_state, ): # Restart if it becomes unhealthy current_state.return_value = ContainerState.UNHEALTHY - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.UNHEALTHY) - await asyncio.sleep(0) + await _fire_test_event( + coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.UNHEALTHY + ) restart.assert_called_once() start.assert_not_called() @@ -172,8 +184,9 @@ async def test_app_watchdog(coresys: CoreSys, install_app_ssh: App) -> None: # Rebuild if it failed current_state.return_value = ContainerState.FAILED with patch.object(DockerApp, "stop") as stop: - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.FAILED) - await asyncio.sleep(0) + await _fire_test_event( + coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.FAILED + ) stop.assert_called_once_with(remove_container=True) restart.assert_not_called() start.assert_called_once() @@ -182,15 +195,17 @@ async def test_app_watchdog(coresys: CoreSys, install_app_ssh: App) -> None: # Do not process event if container state has changed since fired current_state.return_value = ContainerState.HEALTHY - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.FAILED) - await asyncio.sleep(0) + await _fire_test_event( + coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.FAILED + ) restart.assert_not_called() start.assert_not_called() # Other apps ignored current_state.return_value = ContainerState.UNHEALTHY - _fire_test_event(coresys, "addon_local_non_installed", ContainerState.UNHEALTHY) - await asyncio.sleep(0) + await _fire_test_event( + coresys, "addon_local_non_installed", ContainerState.UNHEALTHY + ) restart.assert_not_called() start.assert_not_called() @@ -216,8 +231,9 @@ async def test_watchdog_port_conflict_does_not_retry( patch("supervisor.addons.addon.async_capture_exception") as capture_exception, ): caplog.clear() - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.FAILED) - await asyncio.sleep(0) + await _fire_test_event( + coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.FAILED + ) start.assert_called_once() capture_exception.assert_not_called() @@ -231,8 +247,12 @@ async def test_watchdog_on_stop(coresys: CoreSys, install_app_ssh: App) -> None: install_app_ssh.watchdog = True + # Watchdog does ``await (await self.restart())`` because App.restart + # returns an asyncio.Task; the mock must mirror that shape. + done_task = asyncio.get_running_loop().create_future() + done_task.set_result(None) with ( - patch.object(App, "restart") as restart, + patch.object(App, "restart", AsyncMock(return_value=done_task)) as restart, patch.object( DockerApp, "current_state", @@ -241,18 +261,22 @@ async def test_watchdog_on_stop(coresys: CoreSys, install_app_ssh: App) -> None: patch.object(DockerApp, "stop"), ): # Do not restart when app stopped by user - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING) - await asyncio.sleep(0) + await _fire_test_event( + coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING + ) await install_app_ssh.stop() - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.STOPPED) - await asyncio.sleep(0) + await _fire_test_event( + coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.STOPPED + ) restart.assert_not_called() # Do restart app if it stops and user didn't do it - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING) - await asyncio.sleep(0) - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.STOPPED) - await asyncio.sleep(0) + await _fire_test_event( + coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING + ) + await _fire_test_event( + coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.STOPPED + ) restart.assert_called_once() @@ -279,8 +303,7 @@ async def test_listener_attached_on_install(coresys: CoreSys): # Normally this would be defaulted to False on start of the app but test skips that coresys.apps.get_local_only(TEST_ADDON_SLUG).watchdog = False - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING) - await asyncio.sleep(0) + await _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING) assert coresys.apps.get(TEST_ADDON_SLUG).state == AppState.STARTED @@ -297,8 +320,12 @@ async def test_watchdog_during_attach( store = coresys.apps.store[TEST_ADDON_SLUG] await coresys.apps.data.install(store) + # Watchdog does ``await (await self.restart())`` because App.restart + # returns an asyncio.Task; the mock must mirror that shape. + done_task = asyncio.get_running_loop().create_future() + done_task.set_result(None) with ( - patch.object(App, "restart") as restart, + patch.object(App, "restart", AsyncMock(return_value=done_task)) as restart, patch.object(HwHelper, "last_boot", return_value=utcnow()), patch.object(DockerApp, "attach"), patch.object( @@ -315,8 +342,9 @@ async def test_watchdog_during_attach( app.watchdog = True await app.load() - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.STOPPED) - await asyncio.sleep(0) + await _fire_test_event( + coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.STOPPED + ) assert restart.call_count == restart_count @@ -386,7 +414,7 @@ async def test_start(coresys: CoreSys, install_app_ssh: App) -> None: start_task = await install_app_ssh.start() assert start_task - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING) + await _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING) await start_task assert install_app_ssh.state == AppState.STARTED @@ -409,14 +437,12 @@ async def test_start_wait_healthcheck( start_task = await install_app_ssh.start() assert start_task - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING) - await asyncio.sleep(0.01) + await _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING) assert not start_task.done() assert install_app_ssh.state == AppState.STARTUP - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", state) - await asyncio.sleep(0.01) + await _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", state) assert start_task.done() assert install_app_ssh.state == AppState.STARTED @@ -455,7 +481,7 @@ async def test_restart(coresys: CoreSys, install_app_ssh: App) -> None: start_task = await install_app_ssh.restart() assert start_task - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING) + await _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING) await start_task assert install_app_ssh.state == AppState.STARTED @@ -654,7 +680,9 @@ async def test_backup_cold_mode_with_watchdog( async def mock_stop(*args, **kwargs): container.show.return_value["State"]["Status"] = "stopped" container.show.return_value["State"]["Running"] = False - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.STOPPED) + await _fire_test_event( + coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.STOPPED + ) # Patching out the normal end of backup process leaves the container in a stopped state # Watchdog should still not try to restart it though, it should remain this way @@ -738,7 +766,9 @@ async def test_restore_while_running_with_watchdog( async def mock_stop(*args, **kwargs): container.show.return_value["State"]["Status"] = "stopped" container.show.return_value["State"]["Running"] = False - _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.STOPPED) + await _fire_test_event( + coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.STOPPED + ) # We restore a stopped backup so restore will not restart it # Watchdog will see it stop and should not attempt reanimation either diff --git a/tests/addons/test_manager.py b/tests/addons/test_manager.py index 0e3749d979c..f2c5cf7b6ca 100644 --- a/tests/addons/test_manager.py +++ b/tests/addons/test_manager.py @@ -384,16 +384,17 @@ async def test_start_wait_resolved_on_uninstall_in_startup( start_task = await install_app_ssh.start() assert start_task - coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name=f"addon_{TEST_ADDON_SLUG}", - state=ContainerState.RUNNING, - id="abc123", - time=1, - ), + await asyncio.gather( + *coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name=f"addon_{TEST_ADDON_SLUG}", + state=ContainerState.RUNNING, + id="abc123", + time=1, + ), + ) ) - await asyncio.sleep(0.01) assert not start_task.done() assert install_app_ssh.state == AppState.STARTUP @@ -525,6 +526,11 @@ async def mock_update(*args, **kwargs): patch.object(App, "restart") as restart, ): await coresys.apps.update("local_ssh") + # mock_update yielded once (sleep(0)), giving the watchdog task + # spawned by mock_stop time to run to completion within update's + # own awaits — so by the time we get here it's already done. + # A trailing sleep(0) defends against scheduling jitter without + # racing the assertion. await asyncio.sleep(0) start.assert_called_once() restart.assert_not_called() diff --git a/tests/api/test_addons.py b/tests/api/test_addons.py index 10dfaa33a06..1e151ab2639 100644 --- a/tests/api/test_addons.py +++ b/tests/api/test_addons.py @@ -123,7 +123,7 @@ async def test_api_app_start_healthcheck( async def container_events(): nonlocal state_changes - await asyncio.sleep(0.01) + await asyncio.sleep(0) await install_app_ssh.container_state_changed( _create_test_event(f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING) @@ -162,7 +162,7 @@ async def test_api_app_restart_healthcheck( async def container_events(): nonlocal state_changes - await asyncio.sleep(0.01) + await asyncio.sleep(0) await install_app_ssh.container_state_changed( _create_test_event(f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING) diff --git a/tests/api/test_store.py b/tests/api/test_store.py index cf1f8f2dea2..735d5316606 100644 --- a/tests/api/test_store.py +++ b/tests/api/test_store.py @@ -225,7 +225,7 @@ async def test_api_store_update_healthcheck( async def container_events(): nonlocal state_changes - await asyncio.sleep(0.01) + await asyncio.sleep(0) await install_app_ssh.container_state_changed( DockerContainerStateEvent( diff --git a/tests/backups/test_manager.py b/tests/backups/test_manager.py index d1de3e02149..7f2f31710ae 100644 --- a/tests/backups/test_manager.py +++ b/tests/backups/test_manager.py @@ -966,7 +966,7 @@ async def test_backup_with_healthcheck( async def container_events(): nonlocal state_changes - await asyncio.sleep(0.01) + await asyncio.sleep(0) await install_app_ssh.container_state_changed( DockerContainerStateEvent( diff --git a/tests/docker/test_addon.py b/tests/docker/test_addon.py index 45f1e7a65f9..6fd2d123a0b 100644 --- a/tests/docker/test_addon.py +++ b/tests/docker/test_addon.py @@ -6,7 +6,7 @@ from ipaddress import IPv4Address from pathlib import Path from typing import Any -from unittest.mock import MagicMock, Mock, PropertyMock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch import aiodocker import pytest @@ -434,15 +434,15 @@ async def test_app_new_device( with ( patch.object(App, "write_options"), patch.object(OSManager, "available", new=PropertyMock(return_value=is_os)), - patch.object(CGroup, "add_devices_allowed") as add_devices, + patch.object( + CGroup, "add_devices_allowed", new_callable=AsyncMock + ) as add_devices, ): await install_app_ssh.start() - coresys.bus.fire_event( - BusEvent.HARDWARE_NEW_DEVICE, - TEST_HW_DEVICE, + await asyncio.gather( + *coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, TEST_HW_DEVICE) ) - await asyncio.sleep(0.01) add_devices.assert_called_once_with(123, "c 0:0 rwm") @@ -460,15 +460,15 @@ async def test_app_new_device_no_haos( with ( patch.object(App, "write_options"), patch.object(OSManager, "available", new=PropertyMock(return_value=False)), - patch.object(CGroup, "add_devices_allowed") as add_devices, + patch.object( + CGroup, "add_devices_allowed", new_callable=AsyncMock + ) as add_devices, ): await install_app_ssh.start() - coresys.bus.fire_event( - BusEvent.HARDWARE_NEW_DEVICE, - TEST_HW_DEVICE, + await asyncio.gather( + *coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, TEST_HW_DEVICE) ) - await asyncio.sleep(0.01) add_devices.assert_not_called() diff --git a/tests/docker/test_interface.py b/tests/docker/test_interface.py index 971be7b042e..94e2a072183 100644 --- a/tests/docker/test_interface.py +++ b/tests/docker/test_interface.py @@ -431,7 +431,7 @@ async def capture_log_entry(event: PullLogEntry) -> None: ) coresys.docker.images.inspect.assert_called_once_with("test:1.2.3") - await asyncio.sleep(1) + await asyncio.sleep(0) assert events == [ PullLogEntry( job_id=ANY, @@ -894,7 +894,7 @@ async def mock_install(self) -> None: ) coresys.docker.images.inspect.assert_called_once_with("test:1.2.3") - await asyncio.sleep(1) + await asyncio.sleep(0) def job_event(progress: float, done: bool = False): return { diff --git a/tests/homeassistant/test_home_assistant_watchdog.py b/tests/homeassistant/test_home_assistant_watchdog.py index 67d043b9937..c07807d2dc0 100644 --- a/tests/homeassistant/test_home_assistant_watchdog.py +++ b/tests/homeassistant/test_home_assistant_watchdog.py @@ -35,75 +35,80 @@ async def test_home_assistant_watchdog(coresys: CoreSys) -> None: ) as current_state, ): current_state.return_value = ContainerState.UNHEALTHY - coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="homeassistant", - state=ContainerState.UNHEALTHY, - id="abc123", - time=1, - ), + await asyncio.gather( + *coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="homeassistant", + state=ContainerState.UNHEALTHY, + id="abc123", + time=1, + ), + ) ) - await asyncio.sleep(0) restart.assert_called_once() start.assert_not_called() restart.reset_mock() current_state.return_value = ContainerState.FAILED - coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="homeassistant", - state=ContainerState.FAILED, - id="abc123", - time=1, - ), + await asyncio.gather( + *coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="homeassistant", + state=ContainerState.FAILED, + id="abc123", + time=1, + ), + ) ) - await asyncio.sleep(0) restart.assert_not_called() start.assert_called_once() start.reset_mock() # Do not process event if container state has changed since fired current_state.return_value = ContainerState.HEALTHY - coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="homeassistant", - state=ContainerState.FAILED, - id="abc123", - time=1, - ), + await asyncio.gather( + *coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="homeassistant", + state=ContainerState.FAILED, + id="abc123", + time=1, + ), + ) ) - await asyncio.sleep(0) restart.assert_not_called() start.assert_not_called() # Do not restart when home assistant stopped normally - coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="homeassistant", - state=ContainerState.STOPPED, - id="abc123", - time=1, - ), + await asyncio.gather( + *coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="homeassistant", + state=ContainerState.STOPPED, + id="abc123", + time=1, + ), + ) ) - await asyncio.sleep(0) restart.assert_not_called() start.assert_not_called() # Other containers ignored - coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="addon_local_other", - state=ContainerState.UNHEALTHY, - id="abc123", - time=1, - ), + await asyncio.gather( + *coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="addon_local_other", + state=ContainerState.UNHEALTHY, + id="abc123", + time=1, + ), + ) ) - await asyncio.sleep(0) restart.assert_not_called() start.assert_not_called() @@ -133,7 +138,7 @@ async def test_home_assistant_watchdog_rebuild_on_failure(coresys: CoreSys) -> N return_value=ContainerState.FAILED, ), ): - coresys.bus.fire_event( + listener_tasks = coresys.bus.fire_event( BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name="homeassistant", @@ -142,7 +147,7 @@ async def test_home_assistant_watchdog_rebuild_on_failure(coresys: CoreSys) -> N time=1, ), ) - await asyncio.sleep(0.1) + await asyncio.gather(*listener_tasks) start.assert_called_once() rebuild.assert_called_once() @@ -207,16 +212,17 @@ async def test_home_assistant_watchdog_unregisters_on_shutdown( ), ): # Watchdog should respond to events before shutdown - coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="homeassistant", - state=ContainerState.FAILED, - id="abc123", - time=1, - ), + await asyncio.gather( + *coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="homeassistant", + state=ContainerState.FAILED, + id="abc123", + time=1, + ), + ) ) - await asyncio.sleep(0) start.assert_called_once() start.reset_mock() @@ -226,22 +232,26 @@ async def test_home_assistant_watchdog_unregisters_on_shutdown( coresys.homeassistant.core._watchdog_listener = watchdog_listener # Fire shutdown state change - coresys.bus.fire_event(BusEvent.SUPERVISOR_STATE_CHANGE, shutdown_state) - await asyncio.sleep(0) + await asyncio.gather( + *coresys.bus.fire_event( + BusEvent.SUPERVISOR_STATE_CHANGE, shutdown_state + ) + ) # Verify watchdog listener is unregistered assert coresys.homeassistant.core._watchdog_listener is None # Watchdog should not respond to events after shutdown - coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="homeassistant", - state=ContainerState.FAILED, - id="abc123", - time=1, - ), + await asyncio.gather( + *coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="homeassistant", + state=ContainerState.FAILED, + id="abc123", + time=1, + ), + ) ) - await asyncio.sleep(0) start.assert_not_called() restart.assert_not_called() diff --git a/tests/plugins/test_plugin_base.py b/tests/plugins/test_plugin_base.py index 282d736c842..88ac4686a2b 100644 --- a/tests/plugins/test_plugin_base.py +++ b/tests/plugins/test_plugin_base.py @@ -74,76 +74,81 @@ async def test_plugin_watchdog(coresys: CoreSys, plugin: PluginBase) -> None: patch.object(type(plugin.instance), "current_state") as current_state, ): current_state.return_value = ContainerState.UNHEALTHY - coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name=plugin.instance.name, - state=ContainerState.UNHEALTHY, - id="abc123", - time=1, - ), + await asyncio.gather( + *coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name=plugin.instance.name, + state=ContainerState.UNHEALTHY, + id="abc123", + time=1, + ), + ) ) - await asyncio.sleep(0) rebuild.assert_called_once() start.assert_not_called() rebuild.reset_mock() current_state.return_value = ContainerState.FAILED - coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name=plugin.instance.name, - state=ContainerState.FAILED, - id="abc123", - time=1, - ), + await asyncio.gather( + *coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name=plugin.instance.name, + state=ContainerState.FAILED, + id="abc123", + time=1, + ), + ) ) - await asyncio.sleep(0) rebuild.assert_called_once() start.assert_not_called() rebuild.reset_mock() # Stop should be ignored as it means an update or system shutdown, plugins don't stop otherwise current_state.return_value = ContainerState.STOPPED - coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name=plugin.instance.name, - state=ContainerState.STOPPED, - id="abc123", - time=1, - ), + await asyncio.gather( + *coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name=plugin.instance.name, + state=ContainerState.STOPPED, + id="abc123", + time=1, + ), + ) ) - await asyncio.sleep(0) rebuild.assert_not_called() start.assert_not_called() # Do not process event if container state has changed since fired current_state.return_value = ContainerState.HEALTHY - coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name=plugin.instance.name, - state=ContainerState.FAILED, - id="abc123", - time=1, - ), + await asyncio.gather( + *coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name=plugin.instance.name, + state=ContainerState.FAILED, + id="abc123", + time=1, + ), + ) ) - await asyncio.sleep(0) rebuild.assert_not_called() start.assert_not_called() # Other containers ignored - coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="addon_local_other", - state=ContainerState.UNHEALTHY, - id="abc123", - time=1, - ), + await asyncio.gather( + *coresys.bus.fire_event( + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="addon_local_other", + state=ContainerState.UNHEALTHY, + id="abc123", + time=1, + ), + ) ) - await asyncio.sleep(0) rebuild.assert_not_called() start.assert_not_called() diff --git a/tests/resolution/fixup/test_store_execute_reload.py b/tests/resolution/fixup/test_store_execute_reload.py index d0aace1401f..e331ee59ca5 100644 --- a/tests/resolution/fixup/test_store_execute_reload.py +++ b/tests/resolution/fixup/test_store_execute_reload.py @@ -59,8 +59,11 @@ async def test_store_execute_reload_runs_on_connectivity_true(coresys: CoreSys): with patch.object(coresys.store, "reload") as mock_reload: # Fire event with connectivity True - coresys.supervisor._update_connectivity(True) # pylint: disable=protected-access - await asyncio.sleep(0.1) + coresys.supervisor._connectivity = True # pylint: disable=protected-access + listener_tasks = coresys.bus.fire_event( + BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True + ) + await asyncio.gather(*listener_tasks) mock_repository.load.assert_called_once() mock_reload.assert_awaited_once_with(mock_repository) @@ -86,9 +89,12 @@ async def test_store_execute_reload_does_not_run_on_connectivity_false( suggestions=[SuggestionType.EXECUTE_RELOAD], ) - # Fire event with connectivity True - coresys.supervisor._update_connectivity(False) # pylint: disable=protected-access - await asyncio.sleep(0.1) + # Fire event with connectivity False + coresys.supervisor._connectivity = False # pylint: disable=protected-access + listener_tasks = coresys.bus.fire_event( + BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, False + ) + await asyncio.gather(*listener_tasks) mock_repository.load.assert_not_called() @@ -117,14 +123,18 @@ async def test_store_execute_reload_dismiss_suggestion_removes_listener( FixupStoreExecuteReload, "process_fixup", side_effect=ResolutionFixupError ) as mock_fixup: # Fire event with issue there to trigger fixup - coresys.bus.fire_event(BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True) - await asyncio.sleep(0.1) + listener_tasks = coresys.bus.fire_event( + BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True + ) + await asyncio.gather(*listener_tasks) mock_fixup.assert_called_once() # Remove issue and suggestion and re-fire to see listener is gone mock_fixup.reset_mock() coresys.resolution.dismiss_issue(issue) - coresys.bus.fire_event(BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True) - await asyncio.sleep(0.1) + listener_tasks = coresys.bus.fire_event( + BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True + ) + await asyncio.gather(*listener_tasks) mock_fixup.assert_not_called() diff --git a/tests/test_bus.py b/tests/test_bus.py index 4fa888acf73..6d14ed2c564 100644 --- a/tests/test_bus.py +++ b/tests/test_bus.py @@ -16,12 +16,10 @@ async def callback(data) -> None: coresys.bus.register_event(BusEvent.HARDWARE_NEW_DEVICE, callback) - coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, None) - await asyncio.sleep(0) + await asyncio.gather(*coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, None)) assert results[-1] is None - coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, "test") - await asyncio.sleep(0) + await asyncio.gather(*coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, "test")) assert results[-1] == "test" @@ -35,8 +33,7 @@ async def callback(data) -> None: coresys.bus.register_event(BusEvent.HARDWARE_NEW_DEVICE, callback) - coresys.bus.fire_event(BusEvent.HARDWARE_REMOVE_DEVICE, None) - await asyncio.sleep(0) + await asyncio.gather(*coresys.bus.fire_event(BusEvent.HARDWARE_REMOVE_DEVICE, None)) assert len(results) == 0 @@ -50,16 +47,14 @@ async def callback(data) -> None: listener = coresys.bus.register_event(BusEvent.HARDWARE_NEW_DEVICE, callback) - coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, None) - await asyncio.sleep(0) + await asyncio.gather(*coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, None)) assert results[-1] is None - coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, "test") - await asyncio.sleep(0) + await asyncio.gather(*coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, "test")) assert results[-1] == "test" coresys.bus.remove_listener(listener) - coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, None) - await asyncio.sleep(0) + # No listeners remain, so no tasks are returned to gather. + await asyncio.gather(*coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, None)) assert results[-1] == "test" From b551c6d54d96127e2dfec23db972268e32c0d65c Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 6 May 2026 10:32:45 +0200 Subject: [PATCH 2/2] Factor bus.fire_event + gather pattern into a helper Per review feedback, the ``await asyncio.gather(*coresys.bus.fire_event(...))`` incantation was scattered across many call sites. Add ``tests.common.fire_bus_event`` that takes the coresys, event and data, fires the event and awaits the spawned listener tasks. Convert all matching sites to use it, including the ``_fire_test_event`` wrapper in addons/test_addon.py which now just builds the ``DockerContainerStateEvent`` and delegates. --- tests/addons/test_addon.py | 21 ++- tests/addons/test_manager.py | 21 ++- tests/common.py | 13 ++ tests/docker/test_addon.py | 15 +- .../test_home_assistant_watchdog.py | 147 +++++++++--------- tests/plugins/test_plugin_base.py | 98 ++++++------ .../fixup/test_store_execute_reload.py | 22 +-- tests/test_bus.py | 16 +- 8 files changed, 175 insertions(+), 178 deletions(-) diff --git a/tests/addons/test_addon.py b/tests/addons/test_addon.py index 52ccb319aa4..c714db5b259 100644 --- a/tests/addons/test_addon.py +++ b/tests/addons/test_addon.py @@ -47,22 +47,21 @@ from .test_manager import BOOT_FAIL_ISSUE, BOOT_FAIL_SUGGESTIONS -from tests.common import get_fixture_path, is_in_list +from tests.common import fire_bus_event, get_fixture_path, is_in_list from tests.const import TEST_ADDON_SLUG async def _fire_test_event(coresys: CoreSys, name: str, state: ContainerState) -> None: """Fire a test event and await the listener tasks the bus spawned.""" - await asyncio.gather( - *coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name=name, - state=state, - id="abc123", - time=1, - ), - ) + await fire_bus_event( + coresys, + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name=name, + state=state, + id="abc123", + time=1, + ), ) diff --git a/tests/addons/test_manager.py b/tests/addons/test_manager.py index f2c5cf7b6ca..355a718d234 100644 --- a/tests/addons/test_manager.py +++ b/tests/addons/test_manager.py @@ -39,7 +39,7 @@ from supervisor.utils import check_exception_chain from supervisor.utils.common import write_json_file -from tests.common import load_json_fixture +from tests.common import fire_bus_event, load_json_fixture from tests.const import TEST_ADDON_SLUG BOOT_FAIL_ISSUE = Issue( @@ -384,16 +384,15 @@ async def test_start_wait_resolved_on_uninstall_in_startup( start_task = await install_app_ssh.start() assert start_task - await asyncio.gather( - *coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name=f"addon_{TEST_ADDON_SLUG}", - state=ContainerState.RUNNING, - id="abc123", - time=1, - ), - ) + await fire_bus_event( + coresys, + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name=f"addon_{TEST_ADDON_SLUG}", + state=ContainerState.RUNNING, + id="abc123", + time=1, + ), ) assert not start_task.done() diff --git a/tests/common.py b/tests/common.py index 08408979e77..6a3e92bffc4 100644 --- a/tests/common.py +++ b/tests/common.py @@ -12,6 +12,8 @@ from dbus_fast.aio.message_bus import MessageBus +from supervisor.const import BusEvent +from supervisor.coresys import CoreSys from supervisor.jobs.decorator import Job from supervisor.resolution.validate import get_valid_modules from supervisor.utils.yaml import read_yaml_file @@ -19,6 +21,17 @@ from .dbus_service_mocks.base import DBusServiceMock +async def fire_bus_event(coresys: CoreSys, event: BusEvent, data: Any) -> None: + """Fire a bus event and await its listener tasks. + + ``Bus.fire_event`` is sync and returns the listener tasks it spawned. + Tests that drive a system under test by firing a bus event need to + wait for those listener tasks to finish before asserting; this helper + bundles the gather so call sites stay short. + """ + await asyncio.gather(*coresys.bus.fire_event(event, data)) + + def get_fixture_path(filename: str) -> Path: """Get path for fixture.""" return Path(Path(__file__).parent.joinpath("fixtures"), filename) diff --git a/tests/docker/test_addon.py b/tests/docker/test_addon.py index 6fd2d123a0b..3bf9fd6b9f6 100644 --- a/tests/docker/test_addon.py +++ b/tests/docker/test_addon.py @@ -1,6 +1,5 @@ """Test docker app setup.""" -import asyncio from dataclasses import replace from http import HTTPStatus from ipaddress import IPv4Address @@ -36,6 +35,8 @@ from ..common import load_json_fixture from . import DEV_MOUNT +from tests.common import fire_bus_event + @pytest.fixture(name="addonsdata_system") def fixture_addonsdata_system() -> dict[str, Data]: @@ -440,8 +441,10 @@ async def test_app_new_device( ): await install_app_ssh.start() - await asyncio.gather( - *coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, TEST_HW_DEVICE) + await fire_bus_event( + coresys, + BusEvent.HARDWARE_NEW_DEVICE, + TEST_HW_DEVICE, ) add_devices.assert_called_once_with(123, "c 0:0 rwm") @@ -466,8 +469,10 @@ async def test_app_new_device_no_haos( ): await install_app_ssh.start() - await asyncio.gather( - *coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, TEST_HW_DEVICE) + await fire_bus_event( + coresys, + BusEvent.HARDWARE_NEW_DEVICE, + TEST_HW_DEVICE, ) add_devices.assert_not_called() diff --git a/tests/homeassistant/test_home_assistant_watchdog.py b/tests/homeassistant/test_home_assistant_watchdog.py index c07807d2dc0..724d31faffc 100644 --- a/tests/homeassistant/test_home_assistant_watchdog.py +++ b/tests/homeassistant/test_home_assistant_watchdog.py @@ -12,6 +12,8 @@ from supervisor.docker.monitor import DockerContainerStateEvent from supervisor.exceptions import HomeAssistantError +from tests.common import fire_bus_event + async def test_home_assistant_watchdog(coresys: CoreSys) -> None: """Test homeassistant watchdog works correctly.""" @@ -35,32 +37,30 @@ async def test_home_assistant_watchdog(coresys: CoreSys) -> None: ) as current_state, ): current_state.return_value = ContainerState.UNHEALTHY - await asyncio.gather( - *coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="homeassistant", - state=ContainerState.UNHEALTHY, - id="abc123", - time=1, - ), - ) + await fire_bus_event( + coresys, + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="homeassistant", + state=ContainerState.UNHEALTHY, + id="abc123", + time=1, + ), ) restart.assert_called_once() start.assert_not_called() restart.reset_mock() current_state.return_value = ContainerState.FAILED - await asyncio.gather( - *coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="homeassistant", - state=ContainerState.FAILED, - id="abc123", - time=1, - ), - ) + await fire_bus_event( + coresys, + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="homeassistant", + state=ContainerState.FAILED, + id="abc123", + time=1, + ), ) restart.assert_not_called() start.assert_called_once() @@ -68,46 +68,43 @@ async def test_home_assistant_watchdog(coresys: CoreSys) -> None: start.reset_mock() # Do not process event if container state has changed since fired current_state.return_value = ContainerState.HEALTHY - await asyncio.gather( - *coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="homeassistant", - state=ContainerState.FAILED, - id="abc123", - time=1, - ), - ) + await fire_bus_event( + coresys, + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="homeassistant", + state=ContainerState.FAILED, + id="abc123", + time=1, + ), ) restart.assert_not_called() start.assert_not_called() # Do not restart when home assistant stopped normally - await asyncio.gather( - *coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="homeassistant", - state=ContainerState.STOPPED, - id="abc123", - time=1, - ), - ) + await fire_bus_event( + coresys, + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="homeassistant", + state=ContainerState.STOPPED, + id="abc123", + time=1, + ), ) restart.assert_not_called() start.assert_not_called() # Other containers ignored - await asyncio.gather( - *coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="addon_local_other", - state=ContainerState.UNHEALTHY, - id="abc123", - time=1, - ), - ) + await fire_bus_event( + coresys, + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="addon_local_other", + state=ContainerState.UNHEALTHY, + id="abc123", + time=1, + ), ) restart.assert_not_called() start.assert_not_called() @@ -138,7 +135,8 @@ async def test_home_assistant_watchdog_rebuild_on_failure(coresys: CoreSys) -> N return_value=ContainerState.FAILED, ), ): - listener_tasks = coresys.bus.fire_event( + await fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name="homeassistant", @@ -147,7 +145,6 @@ async def test_home_assistant_watchdog_rebuild_on_failure(coresys: CoreSys) -> N time=1, ), ) - await asyncio.gather(*listener_tasks) start.assert_called_once() rebuild.assert_called_once() @@ -212,16 +209,15 @@ async def test_home_assistant_watchdog_unregisters_on_shutdown( ), ): # Watchdog should respond to events before shutdown - await asyncio.gather( - *coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="homeassistant", - state=ContainerState.FAILED, - id="abc123", - time=1, - ), - ) + await fire_bus_event( + coresys, + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="homeassistant", + state=ContainerState.FAILED, + id="abc123", + time=1, + ), ) start.assert_called_once() start.reset_mock() @@ -232,26 +228,25 @@ async def test_home_assistant_watchdog_unregisters_on_shutdown( coresys.homeassistant.core._watchdog_listener = watchdog_listener # Fire shutdown state change - await asyncio.gather( - *coresys.bus.fire_event( - BusEvent.SUPERVISOR_STATE_CHANGE, shutdown_state - ) + await fire_bus_event( + coresys, + BusEvent.SUPERVISOR_STATE_CHANGE, + shutdown_state, ) # Verify watchdog listener is unregistered assert coresys.homeassistant.core._watchdog_listener is None # Watchdog should not respond to events after shutdown - await asyncio.gather( - *coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="homeassistant", - state=ContainerState.FAILED, - id="abc123", - time=1, - ), - ) + await fire_bus_event( + coresys, + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="homeassistant", + state=ContainerState.FAILED, + id="abc123", + time=1, + ), ) start.assert_not_called() restart.assert_not_called() diff --git a/tests/plugins/test_plugin_base.py b/tests/plugins/test_plugin_base.py index 88ac4686a2b..23a62df4ca1 100644 --- a/tests/plugins/test_plugin_base.py +++ b/tests/plugins/test_plugin_base.py @@ -1,6 +1,5 @@ """Test base plugin functionality.""" -import asyncio from unittest.mock import ANY, Mock, PropertyMock, call, patch from aiodocker.containers import DockerContainer @@ -36,6 +35,8 @@ from supervisor.plugins.observer import PluginObserver from supervisor.utils import check_exception_chain +from tests.common import fire_bus_event + @pytest.fixture(name="plugin") async def fixture_plugin( @@ -74,32 +75,30 @@ async def test_plugin_watchdog(coresys: CoreSys, plugin: PluginBase) -> None: patch.object(type(plugin.instance), "current_state") as current_state, ): current_state.return_value = ContainerState.UNHEALTHY - await asyncio.gather( - *coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name=plugin.instance.name, - state=ContainerState.UNHEALTHY, - id="abc123", - time=1, - ), - ) + await fire_bus_event( + coresys, + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name=plugin.instance.name, + state=ContainerState.UNHEALTHY, + id="abc123", + time=1, + ), ) rebuild.assert_called_once() start.assert_not_called() rebuild.reset_mock() current_state.return_value = ContainerState.FAILED - await asyncio.gather( - *coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name=plugin.instance.name, - state=ContainerState.FAILED, - id="abc123", - time=1, - ), - ) + await fire_bus_event( + coresys, + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name=plugin.instance.name, + state=ContainerState.FAILED, + id="abc123", + time=1, + ), ) rebuild.assert_called_once() start.assert_not_called() @@ -107,47 +106,44 @@ async def test_plugin_watchdog(coresys: CoreSys, plugin: PluginBase) -> None: rebuild.reset_mock() # Stop should be ignored as it means an update or system shutdown, plugins don't stop otherwise current_state.return_value = ContainerState.STOPPED - await asyncio.gather( - *coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name=plugin.instance.name, - state=ContainerState.STOPPED, - id="abc123", - time=1, - ), - ) + await fire_bus_event( + coresys, + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name=plugin.instance.name, + state=ContainerState.STOPPED, + id="abc123", + time=1, + ), ) rebuild.assert_not_called() start.assert_not_called() # Do not process event if container state has changed since fired current_state.return_value = ContainerState.HEALTHY - await asyncio.gather( - *coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name=plugin.instance.name, - state=ContainerState.FAILED, - id="abc123", - time=1, - ), - ) + await fire_bus_event( + coresys, + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name=plugin.instance.name, + state=ContainerState.FAILED, + id="abc123", + time=1, + ), ) rebuild.assert_not_called() start.assert_not_called() # Other containers ignored - await asyncio.gather( - *coresys.bus.fire_event( - BusEvent.DOCKER_CONTAINER_STATE_CHANGE, - DockerContainerStateEvent( - name="addon_local_other", - state=ContainerState.UNHEALTHY, - id="abc123", - time=1, - ), - ) + await fire_bus_event( + coresys, + BusEvent.DOCKER_CONTAINER_STATE_CHANGE, + DockerContainerStateEvent( + name="addon_local_other", + state=ContainerState.UNHEALTHY, + id="abc123", + time=1, + ), ) rebuild.assert_not_called() start.assert_not_called() diff --git a/tests/resolution/fixup/test_store_execute_reload.py b/tests/resolution/fixup/test_store_execute_reload.py index e331ee59ca5..42ffe6d279d 100644 --- a/tests/resolution/fixup/test_store_execute_reload.py +++ b/tests/resolution/fixup/test_store_execute_reload.py @@ -13,6 +13,8 @@ from supervisor.resolution.data import Issue, Suggestion from supervisor.resolution.fixups.store_execute_reload import FixupStoreExecuteReload +from tests.common import fire_bus_event + async def test_fixup(coresys: CoreSys, supervisor_internet): """Test fixup.""" @@ -60,10 +62,7 @@ async def test_store_execute_reload_runs_on_connectivity_true(coresys: CoreSys): with patch.object(coresys.store, "reload") as mock_reload: # Fire event with connectivity True coresys.supervisor._connectivity = True # pylint: disable=protected-access - listener_tasks = coresys.bus.fire_event( - BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True - ) - await asyncio.gather(*listener_tasks) + await fire_bus_event(coresys, BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True) mock_repository.load.assert_called_once() mock_reload.assert_awaited_once_with(mock_repository) @@ -91,10 +90,7 @@ async def test_store_execute_reload_does_not_run_on_connectivity_false( # Fire event with connectivity False coresys.supervisor._connectivity = False # pylint: disable=protected-access - listener_tasks = coresys.bus.fire_event( - BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, False - ) - await asyncio.gather(*listener_tasks) + await fire_bus_event(coresys, BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, False) mock_repository.load.assert_not_called() @@ -123,18 +119,12 @@ async def test_store_execute_reload_dismiss_suggestion_removes_listener( FixupStoreExecuteReload, "process_fixup", side_effect=ResolutionFixupError ) as mock_fixup: # Fire event with issue there to trigger fixup - listener_tasks = coresys.bus.fire_event( - BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True - ) - await asyncio.gather(*listener_tasks) + await fire_bus_event(coresys, BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True) mock_fixup.assert_called_once() # Remove issue and suggestion and re-fire to see listener is gone mock_fixup.reset_mock() coresys.resolution.dismiss_issue(issue) - listener_tasks = coresys.bus.fire_event( - BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True - ) - await asyncio.gather(*listener_tasks) + await fire_bus_event(coresys, BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True) mock_fixup.assert_not_called() diff --git a/tests/test_bus.py b/tests/test_bus.py index 6d14ed2c564..743e0e413cf 100644 --- a/tests/test_bus.py +++ b/tests/test_bus.py @@ -1,10 +1,10 @@ """Test bus backend.""" -import asyncio - from supervisor.const import BusEvent from supervisor.coresys import CoreSys +from tests.common import fire_bus_event + async def test_bus_event(coresys: CoreSys) -> None: """Test bus events over the backend.""" @@ -16,10 +16,10 @@ async def callback(data) -> None: coresys.bus.register_event(BusEvent.HARDWARE_NEW_DEVICE, callback) - await asyncio.gather(*coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, None)) + await fire_bus_event(coresys, BusEvent.HARDWARE_NEW_DEVICE, None) assert results[-1] is None - await asyncio.gather(*coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, "test")) + await fire_bus_event(coresys, BusEvent.HARDWARE_NEW_DEVICE, "test") assert results[-1] == "test" @@ -33,7 +33,7 @@ async def callback(data) -> None: coresys.bus.register_event(BusEvent.HARDWARE_NEW_DEVICE, callback) - await asyncio.gather(*coresys.bus.fire_event(BusEvent.HARDWARE_REMOVE_DEVICE, None)) + await fire_bus_event(coresys, BusEvent.HARDWARE_REMOVE_DEVICE, None) assert len(results) == 0 @@ -47,14 +47,14 @@ async def callback(data) -> None: listener = coresys.bus.register_event(BusEvent.HARDWARE_NEW_DEVICE, callback) - await asyncio.gather(*coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, None)) + await fire_bus_event(coresys, BusEvent.HARDWARE_NEW_DEVICE, None) assert results[-1] is None - await asyncio.gather(*coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, "test")) + await fire_bus_event(coresys, BusEvent.HARDWARE_NEW_DEVICE, "test") assert results[-1] == "test" coresys.bus.remove_listener(listener) # No listeners remain, so no tasks are returned to gather. - await asyncio.gather(*coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, None)) + await fire_bus_event(coresys, BusEvent.HARDWARE_NEW_DEVICE, None) assert results[-1] == "test"