mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-08-14 19:49:21 +00:00
Compare commits
8 Commits
2024.10.1
...
trigger-sy
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e415923553 | ||
![]() |
95c638991d | ||
![]() |
e2ada42001 | ||
![]() |
22e50b4ace | ||
![]() |
334484de7f | ||
![]() |
180a7c3990 | ||
![]() |
d5f33de808 | ||
![]() |
6539f0df6f |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -335,7 +335,7 @@ jobs:
|
||||
-o console_output_style=count \
|
||||
tests
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v4.4.2
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}
|
||||
path: .coverage
|
||||
|
@@ -1,5 +1,5 @@
|
||||
aiodns==3.2.0
|
||||
aiohttp==3.10.9
|
||||
aiohttp==3.10.10
|
||||
atomicwrites-homeassistant==1.4.1
|
||||
attrs==24.2.0
|
||||
awesomeversion==24.6.0
|
||||
@@ -8,7 +8,7 @@ ciso8601==2.3.1
|
||||
colorlog==6.8.2
|
||||
cpe==1.3.1
|
||||
cryptography==43.0.1
|
||||
debugpy==1.8.6
|
||||
debugpy==1.8.7
|
||||
deepmerge==2.0
|
||||
dirhash==0.5.0
|
||||
docker==7.1.0
|
||||
|
@@ -1,4 +1,4 @@
|
||||
coverage==7.6.1
|
||||
coverage==7.6.3
|
||||
pre-commit==4.0.1
|
||||
pylint==3.3.1
|
||||
pytest-aiohttp==1.0.5
|
||||
|
@@ -239,11 +239,13 @@ class APIHost(CoreSysAttributes):
|
||||
# return 2 lines at minimum.
|
||||
lines = max(2, lines)
|
||||
# entries=cursor[[:num_skip]:num_entries]
|
||||
range_header = f"entries=:-{lines-1}:{lines}"
|
||||
range_header = f"entries=:-{lines-1}:{'' if follow else lines}"
|
||||
elif RANGE in request.headers:
|
||||
range_header = request.headers.get(RANGE)
|
||||
else:
|
||||
range_header = f"entries=:-{DEFAULT_LINES-1}:{DEFAULT_LINES}"
|
||||
range_header = (
|
||||
f"entries=:-{DEFAULT_LINES-1}:{'' if follow else DEFAULT_LINES}"
|
||||
)
|
||||
|
||||
async with self.sys_host.logs.journald_logs(
|
||||
params=params, range_header=range_header, accept=LogFormat.JOURNAL
|
||||
|
@@ -10,7 +10,10 @@ from pathlib import Path
|
||||
|
||||
from ..addons.addon import Addon
|
||||
from ..const import (
|
||||
ATTR_DATA,
|
||||
ATTR_DAYS_UNTIL_STALE,
|
||||
ATTR_SLUG,
|
||||
ATTR_TYPE,
|
||||
FILE_HASSIO_BACKUPS,
|
||||
FOLDER_HOMEASSISTANT,
|
||||
CoreState,
|
||||
@@ -21,7 +24,9 @@ from ..exceptions import (
|
||||
BackupInvalidError,
|
||||
BackupJobError,
|
||||
BackupMountDownError,
|
||||
HomeAssistantWSError,
|
||||
)
|
||||
from ..homeassistant.const import WSType
|
||||
from ..jobs.const import JOB_GROUP_BACKUP_MANAGER, JobCondition, JobExecutionLimit
|
||||
from ..jobs.decorator import Job
|
||||
from ..jobs.job_group import JobGroup
|
||||
@@ -299,6 +304,18 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||
# Ignore exceptions from waiting for addon startup, addon errors handled elsewhere
|
||||
await asyncio.gather(*addon_start_tasks, return_exceptions=True)
|
||||
|
||||
try:
|
||||
await self.sys_homeassistant.websocket.async_send_command(
|
||||
{
|
||||
ATTR_TYPE: WSType.BACKUP_SYNC,
|
||||
ATTR_DATA: {
|
||||
ATTR_SLUG: backup.slug,
|
||||
},
|
||||
},
|
||||
)
|
||||
except HomeAssistantWSError as err:
|
||||
_LOGGER.error("Can't send backup sync to Home Assistant: %s", err)
|
||||
|
||||
return backup
|
||||
finally:
|
||||
self.sys_core.state = CoreState.RUNNING
|
||||
|
@@ -32,6 +32,7 @@ class WSType(StrEnum):
|
||||
SUPERVISOR_EVENT = "supervisor/event"
|
||||
BACKUP_START = "backup/start"
|
||||
BACKUP_END = "backup/end"
|
||||
BACKUP_SYNC = "backup/sync"
|
||||
|
||||
|
||||
class WSEvent(StrEnum):
|
||||
|
@@ -34,6 +34,7 @@ MIN_VERSION = {
|
||||
WSType.SUPERVISOR_EVENT: "2021.2.4",
|
||||
WSType.BACKUP_START: "2022.1.0",
|
||||
WSType.BACKUP_END: "2022.1.0",
|
||||
WSType.BACKUP_SYNC: "2024.10.99",
|
||||
}
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
@@ -41,7 +41,7 @@ def _check_connectivity_throttle_period(coresys: CoreSys, *_) -> timedelta:
|
||||
if coresys.supervisor.connectivity:
|
||||
return timedelta(minutes=10)
|
||||
|
||||
return timedelta()
|
||||
return timedelta(seconds=30)
|
||||
|
||||
|
||||
class Supervisor(CoreSysAttributes):
|
||||
|
@@ -7,6 +7,7 @@ from aiohttp.test_utils import TestClient
|
||||
from supervisor.host.const import LogFormat
|
||||
|
||||
DEFAULT_LOG_RANGE = "entries=:-99:100"
|
||||
DEFAULT_LOG_RANGE_FOLLOW = "entries=:-99:"
|
||||
|
||||
|
||||
async def common_test_api_advanced_logs(
|
||||
@@ -34,7 +35,7 @@ async def common_test_api_advanced_logs(
|
||||
|
||||
journald_logs.assert_called_once_with(
|
||||
params={"SYSLOG_IDENTIFIER": syslog_identifier, "follow": ""},
|
||||
range_header=DEFAULT_LOG_RANGE,
|
||||
range_header=DEFAULT_LOG_RANGE_FOLLOW,
|
||||
accept=LogFormat.JOURNAL,
|
||||
)
|
||||
|
||||
@@ -62,6 +63,6 @@ async def common_test_api_advanced_logs(
|
||||
"_BOOT_ID": "ccc",
|
||||
"follow": "",
|
||||
},
|
||||
range_header=DEFAULT_LOG_RANGE,
|
||||
range_header=DEFAULT_LOG_RANGE_FOLLOW,
|
||||
accept=LogFormat.JOURNAL,
|
||||
)
|
||||
|
@@ -15,6 +15,7 @@ from tests.dbus_service_mocks.base import DBusServiceMock
|
||||
from tests.dbus_service_mocks.systemd import Systemd as SystemdService
|
||||
|
||||
DEFAULT_RANGE = "entries=:-99:100"
|
||||
DEFAULT_RANGE_FOLLOW = "entries=:-99:"
|
||||
# pylint: disable=protected-access
|
||||
|
||||
|
||||
@@ -233,7 +234,7 @@ async def test_advanced_logs(
|
||||
"SYSLOG_IDENTIFIER": coresys.host.logs.default_identifiers,
|
||||
"follow": "",
|
||||
},
|
||||
range_header=DEFAULT_RANGE,
|
||||
range_header=DEFAULT_RANGE_FOLLOW,
|
||||
accept=LogFormat.JOURNAL,
|
||||
)
|
||||
|
||||
|
@@ -1169,6 +1169,40 @@ async def test_backup_progress(
|
||||
]
|
||||
|
||||
|
||||
async def test_backup_sync_notification(
|
||||
coresys: CoreSys,
|
||||
install_addon_ssh: Addon,
|
||||
container: MagicMock,
|
||||
ha_ws_client: AsyncMock,
|
||||
tmp_supervisor_data,
|
||||
path_extern,
|
||||
):
|
||||
"""Test backup sync notification."""
|
||||
container.status = "running"
|
||||
install_addon_ssh.path_data.mkdir()
|
||||
coresys.core.state = CoreState.RUNNING
|
||||
ha_ws_client.ha_version = AwesomeVersion("9999.9.9")
|
||||
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||
|
||||
ha_ws_client.async_send_command.reset_mock()
|
||||
partial_backup: Backup = await coresys.backups.do_backup_partial()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
messages = [
|
||||
call.args[0]
|
||||
for call in ha_ws_client.async_send_command.call_args_list
|
||||
if call.args[0]["type"] == WSType.BACKUP_SYNC
|
||||
]
|
||||
assert messages == [
|
||||
{
|
||||
"data": {
|
||||
"slug": partial_backup.slug,
|
||||
},
|
||||
"type": WSType.BACKUP_SYNC,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def test_restore_progress(
|
||||
request: pytest.FixtureRequest,
|
||||
coresys: CoreSys,
|
||||
|
@@ -1,12 +1,15 @@
|
||||
"""Common test functions."""
|
||||
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
from inspect import getclosurevars
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from dbus_fast.aio.message_bus import MessageBus
|
||||
|
||||
from supervisor.jobs.decorator import Job
|
||||
from supervisor.resolution.validate import get_valid_modules
|
||||
from supervisor.utils.yaml import read_yaml_file
|
||||
|
||||
@@ -82,3 +85,17 @@ async def mock_dbus_services(
|
||||
services[module] = service_module.setup(to_mock[module]).export(bus)
|
||||
|
||||
return services
|
||||
|
||||
|
||||
def get_job_decorator(func) -> Job:
|
||||
"""Get Job object of decorated function."""
|
||||
# Access the closure of the wrapper function
|
||||
job = getclosurevars(func).nonlocals["self"]
|
||||
if not isinstance(job, Job):
|
||||
raise TypeError(f"{func.__qualname__} is not a Job")
|
||||
return job
|
||||
|
||||
|
||||
def reset_last_call(func, group: str | None = None) -> None:
|
||||
"""Reset last call for a function using the Job decorator."""
|
||||
get_job_decorator(func).set_last_call(datetime.min, group)
|
||||
|
@@ -26,8 +26,11 @@ from supervisor.jobs.job_group import JobGroup
|
||||
from supervisor.os.manager import OSManager
|
||||
from supervisor.plugins.audio import PluginAudio
|
||||
from supervisor.resolution.const import UnhealthyReason
|
||||
from supervisor.supervisor import Supervisor
|
||||
from supervisor.utils.dt import utcnow
|
||||
|
||||
from tests.common import reset_last_call
|
||||
|
||||
|
||||
async def test_healthy(coresys: CoreSys, caplog: pytest.LogCaptureFixture):
|
||||
"""Test the healty decorator."""
|
||||
@@ -73,6 +76,7 @@ async def test_internet(
|
||||
):
|
||||
"""Test the internet decorator."""
|
||||
coresys.core.state = CoreState.RUNNING
|
||||
reset_last_call(Supervisor.check_connectivity)
|
||||
|
||||
class TestClass:
|
||||
"""Test class."""
|
||||
|
@@ -1,6 +1,6 @@
|
||||
"""Test supervisor object."""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import errno
|
||||
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
|
||||
|
||||
@@ -8,6 +8,7 @@ from aiohttp import ClientTimeout
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
from awesomeversion import AwesomeVersion
|
||||
import pytest
|
||||
from time_machine import travel
|
||||
|
||||
from supervisor.const import UpdateChannel
|
||||
from supervisor.coresys import CoreSys
|
||||
@@ -22,6 +23,8 @@ from supervisor.resolution.const import ContextType, IssueType
|
||||
from supervisor.resolution.data import Issue
|
||||
from supervisor.supervisor import Supervisor
|
||||
|
||||
from tests.common import reset_last_call
|
||||
|
||||
|
||||
@pytest.fixture(name="websession", scope="function")
|
||||
async def fixture_webession(coresys: CoreSys) -> AsyncMock:
|
||||
@@ -58,21 +61,33 @@ async def test_connectivity_check(
|
||||
assert supervisor_unthrottled.connectivity is connectivity
|
||||
|
||||
|
||||
@pytest.mark.parametrize("side_effect,call_count", [(ClientError(), 3), (None, 1)])
|
||||
@pytest.mark.parametrize(
|
||||
"side_effect,call_interval,throttled",
|
||||
[
|
||||
(None, timedelta(minutes=5), True),
|
||||
(None, timedelta(minutes=15), False),
|
||||
(ClientError(), timedelta(seconds=20), True),
|
||||
(ClientError(), timedelta(seconds=40), False),
|
||||
],
|
||||
)
|
||||
async def test_connectivity_check_throttling(
|
||||
coresys: CoreSys,
|
||||
websession: AsyncMock,
|
||||
side_effect: Exception | None,
|
||||
call_count: int,
|
||||
call_interval: timedelta,
|
||||
throttled: bool,
|
||||
):
|
||||
"""Test connectivity check throttled when checks succeed."""
|
||||
coresys.supervisor.connectivity = None
|
||||
websession.head.side_effect = side_effect
|
||||
|
||||
for _ in range(3):
|
||||
reset_last_call(Supervisor.check_connectivity)
|
||||
with travel(datetime.now(), tick=False) as traveller:
|
||||
await coresys.supervisor.check_connectivity()
|
||||
traveller.shift(call_interval)
|
||||
await coresys.supervisor.check_connectivity()
|
||||
|
||||
assert websession.head.call_count == call_count
|
||||
assert websession.head.call_count == (1 if throttled else 2)
|
||||
|
||||
|
||||
async def test_update_failed(coresys: CoreSys, capture_exception: Mock):
|
||||
|
Reference in New Issue
Block a user