diff --git a/tests/addons/test_addon.py b/tests/addons/test_addon.py index 1e1b03e3b83..c714db5b259 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 @@ -47,13 +47,14 @@ 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 -def _fire_test_event(coresys: CoreSys, name: str, state: ContainerState): - """Fire a test event.""" - coresys.bus.fire_event( +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 fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name=name, @@ -125,25 +126,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 +161,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 +183,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 +194,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 +230,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 +246,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 +260,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 +302,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 +319,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 +341,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 +413,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 +436,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 +480,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 +679,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 +765,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..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,7 +384,8 @@ async def test_start_wait_resolved_on_uninstall_in_startup( start_task = await install_app_ssh.start() assert start_task - coresys.bus.fire_event( + await fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name=f"addon_{TEST_ADDON_SLUG}", @@ -393,7 +394,6 @@ async def test_start_wait_resolved_on_uninstall_in_startup( time=1, ), ) - await asyncio.sleep(0.01) assert not start_task.done() assert install_app_ssh.state == AppState.STARTUP @@ -525,6 +525,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/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 45f1e7a65f9..3bf9fd6b9f6 100644 --- a/tests/docker/test_addon.py +++ b/tests/docker/test_addon.py @@ -1,12 +1,11 @@ """Test docker app setup.""" -import asyncio from dataclasses import replace from http import HTTPStatus 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 @@ -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]: @@ -434,15 +435,17 @@ 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( + await fire_bus_event( + coresys, 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 +463,17 @@ 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( + await fire_bus_event( + coresys, 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..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,7 +37,8 @@ async def test_home_assistant_watchdog(coresys: CoreSys) -> None: ) as current_state, ): current_state.return_value = ContainerState.UNHEALTHY - coresys.bus.fire_event( + await fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name="homeassistant", @@ -44,13 +47,13 @@ async def test_home_assistant_watchdog(coresys: CoreSys) -> None: 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( + await fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name="homeassistant", @@ -59,14 +62,14 @@ async def test_home_assistant_watchdog(coresys: CoreSys) -> None: 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( + await fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name="homeassistant", @@ -75,12 +78,12 @@ async def test_home_assistant_watchdog(coresys: CoreSys) -> None: 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( + await fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name="homeassistant", @@ -89,12 +92,12 @@ async def test_home_assistant_watchdog(coresys: CoreSys) -> None: time=1, ), ) - await asyncio.sleep(0) restart.assert_not_called() start.assert_not_called() # Other containers ignored - coresys.bus.fire_event( + await fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name="addon_local_other", @@ -103,7 +106,6 @@ async def test_home_assistant_watchdog(coresys: CoreSys) -> None: time=1, ), ) - await asyncio.sleep(0) restart.assert_not_called() start.assert_not_called() @@ -133,7 +135,8 @@ async def test_home_assistant_watchdog_rebuild_on_failure(coresys: CoreSys) -> N return_value=ContainerState.FAILED, ), ): - coresys.bus.fire_event( + await fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name="homeassistant", @@ -142,7 +145,6 @@ async def test_home_assistant_watchdog_rebuild_on_failure(coresys: CoreSys) -> N time=1, ), ) - await asyncio.sleep(0.1) start.assert_called_once() rebuild.assert_called_once() @@ -207,7 +209,8 @@ async def test_home_assistant_watchdog_unregisters_on_shutdown( ), ): # Watchdog should respond to events before shutdown - coresys.bus.fire_event( + await fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name="homeassistant", @@ -216,7 +219,6 @@ async def test_home_assistant_watchdog_unregisters_on_shutdown( time=1, ), ) - await asyncio.sleep(0) start.assert_called_once() start.reset_mock() @@ -226,14 +228,18 @@ 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 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 - coresys.bus.fire_event( + await fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name="homeassistant", @@ -242,6 +248,5 @@ async def test_home_assistant_watchdog_unregisters_on_shutdown( 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..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,7 +75,8 @@ 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( + await fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name=plugin.instance.name, @@ -83,13 +85,13 @@ async def test_plugin_watchdog(coresys: CoreSys, plugin: PluginBase) -> None: 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( + await fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name=plugin.instance.name, @@ -98,14 +100,14 @@ async def test_plugin_watchdog(coresys: CoreSys, plugin: PluginBase) -> None: 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( + await fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name=plugin.instance.name, @@ -114,13 +116,13 @@ async def test_plugin_watchdog(coresys: CoreSys, plugin: PluginBase) -> None: 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( + await fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name=plugin.instance.name, @@ -129,12 +131,12 @@ async def test_plugin_watchdog(coresys: CoreSys, plugin: PluginBase) -> None: time=1, ), ) - await asyncio.sleep(0) rebuild.assert_not_called() start.assert_not_called() # Other containers ignored - coresys.bus.fire_event( + await fire_bus_event( + coresys, BusEvent.DOCKER_CONTAINER_STATE_CHANGE, DockerContainerStateEvent( name="addon_local_other", @@ -143,7 +145,6 @@ async def test_plugin_watchdog(coresys: CoreSys, plugin: PluginBase) -> None: 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..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.""" @@ -59,8 +61,8 @@ 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 + await fire_bus_event(coresys, BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True) mock_repository.load.assert_called_once() mock_reload.assert_awaited_once_with(mock_repository) @@ -86,9 +88,9 @@ 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 + await fire_bus_event(coresys, BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, False) mock_repository.load.assert_not_called() @@ -117,14 +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 - coresys.bus.fire_event(BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True) - await asyncio.sleep(0.1) + 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) - coresys.bus.fire_event(BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True) - await asyncio.sleep(0.1) + 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 4fa888acf73..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,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 fire_bus_event(coresys, BusEvent.HARDWARE_NEW_DEVICE, None) assert results[-1] is None - coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, "test") - await asyncio.sleep(0) + await fire_bus_event(coresys, 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 fire_bus_event(coresys, 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 fire_bus_event(coresys, BusEvent.HARDWARE_NEW_DEVICE, None) assert results[-1] is None - coresys.bus.fire_event(BusEvent.HARDWARE_NEW_DEVICE, "test") - await asyncio.sleep(0) + await fire_bus_event(coresys, 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 fire_bus_event(coresys, BusEvent.HARDWARE_NEW_DEVICE, None) assert results[-1] == "test"