diff --git a/supervisor/core.py b/supervisor/core.py index 0602e831dbb..211cedfc511 100644 --- a/supervisor/core.py +++ b/supervisor/core.py @@ -38,7 +38,8 @@ class Core(CoreSysAttributes): def __init__(self, coresys: CoreSys): """Initialize Supervisor object.""" self.coresys: CoreSys = coresys - self._state: CoreState | None = None + self._state: CoreState = CoreState.INITIALIZE + self._write_run_state(self._state) self.exit_code: int = 0 @property @@ -56,34 +57,36 @@ def healthy(self) -> bool: """Return true if the installation is healthy.""" return len(self.sys_resolution.unhealthy) == 0 - @state.setter - def state(self, new_state: CoreState) -> None: - """Set core into new state.""" - if self._state == new_state: - return + def _write_run_state(self, new_state: CoreState): + """Write run state for s6 service supervisor.""" try: - RUN_SUPERVISOR_STATE.write_text(new_state, encoding="utf-8") + RUN_SUPERVISOR_STATE.write_text(str(new_state), encoding="utf-8") except OSError as err: _LOGGER.warning( "Can't update the Supervisor state to %s: %s", new_state, err ) - finally: - self._state = new_state - # Don't attempt to notify anyone on CLOSE as we're about to stop the event loop - if new_state != CoreState.CLOSE: - self.sys_bus.fire_event(BusEvent.SUPERVISOR_STATE_CHANGE, new_state) + @state.setter + def state(self, new_state: CoreState) -> None: + """Set core into new state.""" + if self._state == new_state: + return + + self._write_run_state(new_state) + self._state = new_state - # These will be received by HA after startup has completed which won't make sense - if new_state not in STARTING_STATES: - self.sys_homeassistant.websocket.supervisor_update_event( - "info", {"state": new_state} - ) + # Don't attempt to notify anyone on CLOSE as we're about to stop the event loop + if new_state != CoreState.CLOSE: + self.sys_bus.fire_event(BusEvent.SUPERVISOR_STATE_CHANGE, new_state) + + # These will be received by HA after startup has completed which won't make sense + if new_state not in STARTING_STATES: + self.sys_homeassistant.websocket.supervisor_update_event( + "info", {"state": new_state} + ) async def connect(self): """Connect Supervisor container.""" - self.state = CoreState.INITIALIZE - # Load information from container await self.sys_supervisor.load() diff --git a/tests/api/middleware/test_security.py b/tests/api/middleware/test_security.py index 1de2a3fd328..032bbf16c46 100644 --- a/tests/api/middleware/test_security.py +++ b/tests/api/middleware/test_security.py @@ -23,7 +23,7 @@ async def mock_handler(request): @pytest.fixture -async def api_system(aiohttp_client, run_dir, coresys: CoreSys) -> TestClient: +async def api_system(aiohttp_client, coresys: CoreSys) -> TestClient: """Fixture for RestAPI client.""" api = RestAPI(coresys) api.webapp = web.Application() @@ -39,7 +39,7 @@ async def api_system(aiohttp_client, run_dir, coresys: CoreSys) -> TestClient: @pytest.fixture -async def api_token_validation(aiohttp_client, run_dir, coresys: CoreSys) -> TestClient: +async def api_token_validation(aiohttp_client, coresys: CoreSys) -> TestClient: """Fixture for RestAPI client with token validation middleware.""" api = RestAPI(coresys) api.webapp = web.Application() diff --git a/tests/conftest.py b/tests/conftest.py index 065158d6393..42a54306a57 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,6 +39,7 @@ ATTR_TYPE, ATTR_VERSION, REQUEST_FROM, + CoreState, ) from supervisor.coresys import CoreSys from supervisor.dbus.network import NetworkManager @@ -307,7 +308,7 @@ async def coresys( dbus_session_bus, all_dbus_services, aiohttp_client, - run_dir, + run_supervisor_state, supervisor_name, ) -> CoreSys: """Create a CoreSys Mock.""" @@ -330,13 +331,17 @@ async def coresys( # Mock test client coresys_obj._supervisor.instance._meta = { - "Config": {"Labels": {"io.hass.arch": "amd64"}} + "Config": {"Labels": {"io.hass.arch": "amd64"}}, + "HostConfig": {"Privileged": True}, } coresys_obj.arch._default_arch = "amd64" coresys_obj.arch._supported_set = {"amd64"} coresys_obj._machine = "qemux86-64" coresys_obj._machine_id = uuid4() + # Load resolution center + await coresys_obj.resolution.load() + # Mock host communication with ( patch("supervisor.dbus.manager.MessageBus") as message_bus, @@ -384,9 +389,14 @@ async def coresys( @pytest.fixture -def ha_ws_client(coresys: CoreSys) -> AsyncMock: +async def ha_ws_client(coresys: CoreSys) -> AsyncMock: """Return HA WS client mock for assertions.""" - return coresys.homeassistant.websocket._client + # Set Supervisor Core state to RUNNING, otherwise WS events won't be delivered + coresys.core.state = CoreState.RUNNING + await asyncio.sleep(0) + client = coresys.homeassistant.websocket._client + client.async_send_command.reset_mock() + return client @pytest.fixture @@ -494,12 +504,10 @@ def store_manager(coresys: CoreSys): @pytest.fixture -def run_dir(tmp_path): - """Fixture to inject hassio env.""" +def run_supervisor_state() -> Generator[MagicMock]: + """Fixture to simulate Supervisor state file in /run/supervisor.""" with patch("supervisor.core.RUN_SUPERVISOR_STATE") as mock_run: - tmp_state = Path(tmp_path, "supervisor") - mock_run.write_text = tmp_state.write_text - yield tmp_state + yield mock_run @pytest.fixture diff --git a/tests/homeassistant/test_websocket.py b/tests/homeassistant/test_websocket.py index e44ba366be0..06fa18c2e32 100644 --- a/tests/homeassistant/test_websocket.py +++ b/tests/homeassistant/test_websocket.py @@ -1,8 +1,9 @@ """Test websocket.""" -# pylint: disable=protected-access, import-error +# pylint: disable=import-error import asyncio import logging +from unittest.mock import AsyncMock from awesomeversion import AwesomeVersion @@ -11,16 +12,15 @@ from supervisor.homeassistant.const import WSEvent, WSType -async def test_send_command(coresys: CoreSys): +async def test_send_command(coresys: CoreSys, ha_ws_client: AsyncMock): """Test websocket error on listen.""" - client = coresys.homeassistant.websocket._client await coresys.homeassistant.websocket.async_send_command({"type": "test"}) - client.async_send_command.assert_called_with({"type": "test"}) + ha_ws_client.async_send_command.assert_called_with({"type": "test"}) await coresys.homeassistant.websocket.async_supervisor_update_event( "test", {"lorem": "ipsum"} ) - client.async_send_command.assert_called_with( + ha_ws_client.async_send_command.assert_called_with( { "type": WSType.SUPERVISOR_EVENT, "data": { @@ -32,11 +32,12 @@ async def test_send_command(coresys: CoreSys): ) -async def test_send_command_old_core_version(coresys: CoreSys, caplog): +async def test_send_command_old_core_version( + coresys: CoreSys, ha_ws_client: AsyncMock, caplog +): """Test websocket error on listen.""" caplog.set_level(logging.INFO) - client = coresys.homeassistant.websocket._client - client.ha_version = AwesomeVersion("1970.1.1") + ha_ws_client.ha_version = AwesomeVersion("1970.1.1") await coresys.homeassistant.websocket.async_send_command( {"type": "supervisor/event"} @@ -50,25 +51,24 @@ async def test_send_command_old_core_version(coresys: CoreSys, caplog): await coresys.homeassistant.websocket.async_supervisor_update_event( "test", {"lorem": "ipsum"} ) - client.async_send_command.assert_not_called() + ha_ws_client.async_send_command.assert_not_called() -async def test_send_message_during_startup(coresys: CoreSys): +async def test_send_message_during_startup(coresys: CoreSys, ha_ws_client: AsyncMock): """Test websocket messages queue during startup.""" - client = coresys.homeassistant.websocket._client await coresys.homeassistant.websocket.load() coresys.core.state = CoreState.SETUP await coresys.homeassistant.websocket.async_supervisor_update_event( "test", {"lorem": "ipsum"} ) - client.async_send_command.assert_not_called() + ha_ws_client.async_send_command.assert_not_called() coresys.core.state = CoreState.RUNNING await asyncio.sleep(0) - assert client.async_send_command.call_count == 2 - assert client.async_send_command.call_args_list[0][0][0] == { + assert ha_ws_client.async_send_command.call_count == 2 + assert ha_ws_client.async_send_command.call_args_list[0][0][0] == { "type": WSType.SUPERVISOR_EVENT, "data": { "event": WSEvent.SUPERVISOR_UPDATE, @@ -76,7 +76,7 @@ async def test_send_message_during_startup(coresys: CoreSys): "data": {"lorem": "ipsum"}, }, } - assert client.async_send_command.call_args_list[1][0][0] == { + assert ha_ws_client.async_send_command.call_args_list[1][0][0] == { "type": WSType.SUPERVISOR_EVENT, "data": { "event": WSEvent.SUPERVISOR_UPDATE, diff --git a/tests/jobs/test_job_decorator.py b/tests/jobs/test_job_decorator.py index 986234bc77d..8e863c95c24 100644 --- a/tests/jobs/test_job_decorator.py +++ b/tests/jobs/test_job_decorator.py @@ -951,7 +951,7 @@ async def execute(self): assert test2.call == 3 -async def test_internal_jobs_no_notify(coresys: CoreSys): +async def test_internal_jobs_no_notify(coresys: CoreSys, ha_ws_client: AsyncMock): """Test internal jobs do not send any notifications.""" class TestClass: @@ -972,18 +972,15 @@ async def execute_default(self) -> bool: return True test1 = TestClass(coresys) - # pylint: disable-next=protected-access - client = coresys.homeassistant.websocket._client - client.async_send_command.reset_mock() await test1.execute_internal() await asyncio.sleep(0) - client.async_send_command.assert_not_called() + ha_ws_client.async_send_command.assert_not_called() await test1.execute_default() await asyncio.sleep(0) - assert client.async_send_command.call_count == 2 - client.async_send_command.assert_called_with( + assert ha_ws_client.async_send_command.call_count == 2 + ha_ws_client.async_send_command.assert_called_with( { "type": "supervisor/event", "data": { diff --git a/tests/jobs/test_job_manager.py b/tests/jobs/test_job_manager.py index 831e1844305..4c584b5ce9b 100644 --- a/tests/jobs/test_job_manager.py +++ b/tests/jobs/test_job_manager.py @@ -1,7 +1,7 @@ """Test the condition decorators.""" import asyncio -from unittest.mock import ANY +from unittest.mock import ANY, AsyncMock import pytest @@ -83,14 +83,14 @@ async def test_update_job(coresys: CoreSys): job.progress = -10 -async def test_notify_on_change(coresys: CoreSys): +async def test_notify_on_change(coresys: CoreSys, ha_ws_client: AsyncMock): """Test jobs notify Home Assistant on changes.""" job = coresys.jobs.new_job(TEST_JOB) job.progress = 50 await asyncio.sleep(0) # pylint: disable=protected-access - coresys.homeassistant.websocket._client.async_send_command.assert_called_with( + ha_ws_client.async_send_command.assert_called_with( { "type": "supervisor/event", "data": { @@ -112,7 +112,7 @@ async def test_notify_on_change(coresys: CoreSys): job.stage = "test" await asyncio.sleep(0) - coresys.homeassistant.websocket._client.async_send_command.assert_called_with( + ha_ws_client.async_send_command.assert_called_with( { "type": "supervisor/event", "data": { @@ -134,7 +134,7 @@ async def test_notify_on_change(coresys: CoreSys): job.reference = "test" await asyncio.sleep(0) - coresys.homeassistant.websocket._client.async_send_command.assert_called_with( + ha_ws_client.async_send_command.assert_called_with( { "type": "supervisor/event", "data": { @@ -156,7 +156,7 @@ async def test_notify_on_change(coresys: CoreSys): with job.start(): await asyncio.sleep(0) - coresys.homeassistant.websocket._client.async_send_command.assert_called_with( + ha_ws_client.async_send_command.assert_called_with( { "type": "supervisor/event", "data": { @@ -178,7 +178,7 @@ async def test_notify_on_change(coresys: CoreSys): job.capture_error() await asyncio.sleep(0) - coresys.homeassistant.websocket._client.async_send_command.assert_called_with( + ha_ws_client.async_send_command.assert_called_with( { "type": "supervisor/event", "data": { @@ -204,7 +204,7 @@ async def test_notify_on_change(coresys: CoreSys): ) await asyncio.sleep(0) - coresys.homeassistant.websocket._client.async_send_command.assert_called_with( + ha_ws_client.async_send_command.assert_called_with( { "type": "supervisor/event", "data": { diff --git a/tests/test_core.py b/tests/test_core.py index 87985225ab2..05100a3bf1a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3,7 +3,7 @@ # pylint: disable=W0212 import datetime import errno -from unittest.mock import AsyncMock, PropertyMock, patch +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from pytest import LogCaptureFixture @@ -16,15 +16,19 @@ from supervisor.utils.whoami import WhoamiData -def test_write_state(run_dir, coresys: CoreSys): +def test_write_state(run_supervisor_state, coresys: CoreSys): """Test write corestate to /run/supervisor.""" coresys.core.state = CoreState.RUNNING - assert run_dir.read_text() == CoreState.RUNNING + run_supervisor_state.write_text.assert_called_with( + str(CoreState.RUNNING), encoding="utf-8" + ) coresys.core.state = CoreState.SHUTDOWN - assert run_dir.read_text() == CoreState.SHUTDOWN + run_supervisor_state.write_text.assert_called_with( + str(CoreState.SHUTDOWN), encoding="utf-8" + ) async def test_adjust_system_datetime(coresys: CoreSys): @@ -83,14 +87,14 @@ async def test_adjust_system_datetime_if_time_behind(coresys: CoreSys): mock_check_connectivity.assert_called_once() -def test_write_state_failure(run_dir, coresys: CoreSys, caplog: LogCaptureFixture): +def test_write_state_failure( + run_supervisor_state: MagicMock, coresys: CoreSys, caplog: LogCaptureFixture +): """Test failure to write corestate to /run/supervisor.""" - with patch( - "supervisor.core.RUN_SUPERVISOR_STATE.write_text", - side_effect=(err := OSError()), - ): - err.errno = errno.EBADMSG - coresys.core.state = CoreState.RUNNING + err = OSError() + err.errno = errno.EBADMSG + run_supervisor_state.write_text.side_effect = err + coresys.core.state = CoreState.RUNNING - assert "Can't update the Supervisor state" in caplog.text - assert coresys.core.healthy is True + assert "Can't update the Supervisor state" in caplog.text + assert coresys.core.state == CoreState.RUNNING diff --git a/tests/test_coresys.py b/tests/test_coresys.py index 47d843f5df2..cf3d1d5c73e 100644 --- a/tests/test_coresys.py +++ b/tests/test_coresys.py @@ -9,7 +9,7 @@ from supervisor.utils.dt import utcnow -async def test_timezone(run_dir, coresys: CoreSys): +async def test_timezone(coresys: CoreSys): """Test write corestate to /run/supervisor.""" # pylint: disable=protected-access coresys.host.sys_dbus._timedate = TimeDate()