Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initialize Supervisor Core state in constructor #5686

Merged
merged 6 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 22 additions & 19 deletions supervisor/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()

Expand Down
4 changes: 2 additions & 2 deletions tests/api/middleware/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down
26 changes: 17 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
ATTR_TYPE,
ATTR_VERSION,
REQUEST_FROM,
CoreState,
)
from supervisor.coresys import CoreSys
from supervisor.dbus.network import NetworkManager
Expand Down Expand Up @@ -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."""
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
30 changes: 15 additions & 15 deletions tests/homeassistant/test_websocket.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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": {
Expand All @@ -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"}
Expand All @@ -50,33 +51,32 @@ 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,
"update_key": "test",
"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,
Expand Down
11 changes: 4 additions & 7 deletions tests/jobs/test_job_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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": {
Expand Down
16 changes: 8 additions & 8 deletions tests/jobs/test_job_manager.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Test the condition decorators."""

import asyncio
from unittest.mock import ANY
from unittest.mock import ANY, AsyncMock

import pytest

Expand Down Expand Up @@ -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": {
Expand All @@ -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": {
Expand All @@ -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": {
Expand All @@ -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": {
Expand All @@ -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": {
Expand All @@ -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": {
Expand Down
30 changes: 17 additions & 13 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Expand Down Expand Up @@ -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
Loading