1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2026-05-22 15:48:51 +01:00
Files
supervisor/tests/resolution/fixup/test_store_execute_reload.py
T
Stefan Agner c772a9bbb0 Replace fixed-duration sleeps after bus events with gather (#6803)
* 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.
2026-05-06 12:02:28 +02:00

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()