mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-05-22 15:48:51 +01:00
c772a9bbb0
* Replace fixed-duration sleeps after bus events with gather 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=<settled future>)`` to mirror that shape rather than a plain MagicMock. * 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.
131 lines
4.6 KiB
Python
131 lines
4.6 KiB
Python
"""Test evaluation base."""
|
|
|
|
# pylint: disable=import-error,protected-access
|
|
import asyncio
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
from supervisor.const import BusEvent
|
|
from supervisor.coresys import CoreSys
|
|
from supervisor.exceptions import ResolutionFixupError
|
|
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
|
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."""
|
|
store_execute_reload = FixupStoreExecuteReload(coresys)
|
|
|
|
assert store_execute_reload.auto
|
|
|
|
coresys.resolution.add_suggestion(
|
|
Suggestion(SuggestionType.EXECUTE_RELOAD, ContextType.STORE, reference="test")
|
|
)
|
|
coresys.resolution.add_issue(
|
|
Issue(IssueType.FATAL_ERROR, ContextType.STORE, reference="test")
|
|
)
|
|
|
|
mock_repositorie = AsyncMock()
|
|
coresys.store.repositories["test"] = mock_repositorie
|
|
|
|
with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0**3))):
|
|
await store_execute_reload()
|
|
|
|
assert mock_repositorie.load.called
|
|
assert mock_repositorie.update.called
|
|
assert len(coresys.resolution.suggestions) == 0
|
|
assert len(coresys.resolution.issues) == 0
|
|
|
|
|
|
@pytest.mark.usefixtures("supervisor_internet")
|
|
async def test_store_execute_reload_runs_on_connectivity_true(coresys: CoreSys):
|
|
"""Test fixup runs when connectivity goes from false to true."""
|
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
|
coresys.supervisor._update_connectivity(False) # pylint: disable=protected-access
|
|
await asyncio.sleep(0)
|
|
|
|
mock_repository = AsyncMock()
|
|
coresys.store.repositories["test_store"] = mock_repository
|
|
coresys.resolution.add_issue(
|
|
Issue(
|
|
IssueType.FATAL_ERROR,
|
|
ContextType.STORE,
|
|
reference="test_store",
|
|
),
|
|
suggestions=[SuggestionType.EXECUTE_RELOAD],
|
|
)
|
|
|
|
with patch.object(coresys.store, "reload") as mock_reload:
|
|
# Fire event with connectivity True
|
|
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)
|
|
|
|
|
|
@pytest.mark.usefixtures("supervisor_internet")
|
|
async def test_store_execute_reload_does_not_run_on_connectivity_false(
|
|
coresys: CoreSys,
|
|
):
|
|
"""Test fixup does not run when connectivity goes from true to false."""
|
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
|
coresys.supervisor._update_connectivity(True) # pylint: disable=protected-access
|
|
await asyncio.sleep(0)
|
|
|
|
mock_repository = AsyncMock()
|
|
coresys.store.repositories["test_store"] = mock_repository
|
|
coresys.resolution.add_issue(
|
|
Issue(
|
|
IssueType.FATAL_ERROR,
|
|
ContextType.STORE,
|
|
reference="test_store",
|
|
),
|
|
suggestions=[SuggestionType.EXECUTE_RELOAD],
|
|
)
|
|
|
|
# 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()
|
|
|
|
|
|
@pytest.mark.usefixtures("supervisor_internet")
|
|
async def test_store_execute_reload_dismiss_suggestion_removes_listener(
|
|
coresys: CoreSys,
|
|
):
|
|
"""Test fixup does not run on event if suggestion has been dismissed."""
|
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
|
coresys.supervisor._update_connectivity(True) # pylint: disable=protected-access
|
|
await asyncio.sleep(0)
|
|
|
|
mock_repository = AsyncMock()
|
|
coresys.store.repositories["test_store"] = mock_repository
|
|
coresys.resolution.add_issue(
|
|
issue := Issue(
|
|
IssueType.FATAL_ERROR,
|
|
ContextType.STORE,
|
|
reference="test_store",
|
|
),
|
|
suggestions=[SuggestionType.EXECUTE_RELOAD],
|
|
)
|
|
|
|
with patch.object(
|
|
FixupStoreExecuteReload, "process_fixup", side_effect=ResolutionFixupError
|
|
) as mock_fixup:
|
|
# Fire event with issue there to trigger fixup
|
|
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)
|
|
|
|
await fire_bus_event(coresys, BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True)
|
|
mock_fixup.assert_not_called()
|