mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-08-27 01:49:21 +00:00
Compare commits
20 Commits
2024.05.2
...
need-updat
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b7c53d9e40 | ||
![]() |
b684c8673e | ||
![]() |
547f42439d | ||
![]() |
c51ceb000f | ||
![]() |
4cbede1bc8 | ||
![]() |
5eac8c7780 | ||
![]() |
ab78d87304 | ||
![]() |
09166e3867 | ||
![]() |
8a5c813cdd | ||
![]() |
4200622f43 | ||
![]() |
c4452a85b4 | ||
![]() |
e57de4a3c1 | ||
![]() |
9fd2c91c55 | ||
![]() |
fbd70013a8 | ||
![]() |
8d18f3e66e | ||
![]() |
5f5754e860 | ||
![]() |
974c882b9a | ||
![]() |
a9ea90096b | ||
![]() |
45c72c426e | ||
![]() |
4e5b75fe19 |
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -149,7 +149,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.publish == 'true'
|
||||||
uses: docker/login-action@v3.1.0
|
uses: docker/login-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
@@ -8,7 +8,7 @@ brotli==1.1.0
|
|||||||
ciso8601==2.3.1
|
ciso8601==2.3.1
|
||||||
colorlog==6.8.2
|
colorlog==6.8.2
|
||||||
cpe==1.2.1
|
cpe==1.2.1
|
||||||
cryptography==42.0.7
|
cryptography==42.0.8
|
||||||
debugpy==1.8.1
|
debugpy==1.8.1
|
||||||
deepmerge==1.1.1
|
deepmerge==1.1.1
|
||||||
dirhash==0.4.0
|
dirhash==0.4.0
|
||||||
@@ -20,11 +20,11 @@ orjson==3.9.15
|
|||||||
pulsectl==24.4.0
|
pulsectl==24.4.0
|
||||||
pyudev==0.24.3
|
pyudev==0.24.3
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
requests==2.32.2
|
requests==2.32.3
|
||||||
securetar==2024.2.1
|
securetar==2024.2.1
|
||||||
sentry-sdk==2.3.1
|
sentry-sdk==2.5.1
|
||||||
setuptools==70.0.0
|
setuptools==70.0.0
|
||||||
voluptuous==0.14.2
|
voluptuous==0.14.2
|
||||||
dbus-fast==2.21.3
|
dbus-fast==2.21.3
|
||||||
typing_extensions==4.12.0
|
typing_extensions==4.12.2
|
||||||
zlib-fast==0.2.0
|
zlib-fast==0.2.0
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
coverage==7.5.1
|
coverage==7.5.3
|
||||||
pre-commit==3.7.1
|
pre-commit==3.7.1
|
||||||
pylint==3.2.2
|
pylint==3.2.3
|
||||||
pytest-aiohttp==1.0.5
|
pytest-aiohttp==1.0.5
|
||||||
pytest-asyncio==0.23.6
|
pytest-asyncio==0.23.6
|
||||||
pytest-cov==5.0.0
|
pytest-cov==5.0.0
|
||||||
pytest-timeout==2.3.1
|
pytest-timeout==2.3.1
|
||||||
pytest==8.2.1
|
pytest==8.2.2
|
||||||
ruff==0.4.5
|
ruff==0.4.8
|
||||||
time-machine==2.14.1
|
time-machine==2.14.1
|
||||||
typing_extensions==4.12.0
|
typing_extensions==4.12.2
|
||||||
urllib3==2.2.1
|
urllib3==2.2.1
|
||||||
|
@@ -285,9 +285,13 @@ class Addon(AddonModel):
|
|||||||
@property
|
@property
|
||||||
def need_update(self) -> bool:
|
def need_update(self) -> bool:
|
||||||
"""Return True if an update is available."""
|
"""Return True if an update is available."""
|
||||||
if self.is_detached:
|
if self.is_detached or self.version == self.latest_version:
|
||||||
return False
|
return False
|
||||||
return self.version != self.latest_version
|
|
||||||
|
with suppress(AddonsNotSupportedError):
|
||||||
|
self._validate_availability(self.data_store)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dns(self) -> list[str]:
|
def dns(self) -> list[str]:
|
||||||
@@ -1343,11 +1347,11 @@ class Addon(AddonModel):
|
|||||||
)
|
)
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
|
|
||||||
|
finally:
|
||||||
# Is add-on loaded
|
# Is add-on loaded
|
||||||
if not self.loaded:
|
if not self.loaded:
|
||||||
await self.load()
|
await self.load()
|
||||||
|
|
||||||
finally:
|
|
||||||
# Run add-on
|
# Run add-on
|
||||||
if data[ATTR_STATE] == AddonState.STARTED:
|
if data[ATTR_STATE] == AddonState.STARTED:
|
||||||
wait_for_start = await self.start()
|
wait_for_start = await self.start()
|
||||||
|
@@ -182,9 +182,13 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def rebuild(self, request: web.Request) -> Awaitable[None]:
|
async def rebuild(self, request: web.Request) -> None:
|
||||||
"""Rebuild Home Assistant."""
|
"""Rebuild Home Assistant."""
|
||||||
return asyncio.shield(self.sys_homeassistant.core.rebuild())
|
body = await api_validate(SCHEMA_RESTART, request)
|
||||||
|
|
||||||
|
await asyncio.shield(
|
||||||
|
self.sys_homeassistant.core.rebuild(safe_mode=body[ATTR_SAFE_MODE])
|
||||||
|
)
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def check(self, request: web.Request) -> None:
|
async def check(self, request: web.Request) -> None:
|
||||||
|
@@ -345,9 +345,6 @@ class Core(CoreSysAttributes):
|
|||||||
if self.state == CoreState.RUNNING:
|
if self.state == CoreState.RUNNING:
|
||||||
self.state = CoreState.SHUTDOWN
|
self.state = CoreState.SHUTDOWN
|
||||||
|
|
||||||
# Stop docker monitoring
|
|
||||||
await self.sys_docker.unload()
|
|
||||||
|
|
||||||
# Shutdown Application Add-ons, using Home Assistant API
|
# Shutdown Application Add-ons, using Home Assistant API
|
||||||
await self.sys_addons.shutdown(AddonStartup.APPLICATION)
|
await self.sys_addons.shutdown(AddonStartup.APPLICATION)
|
||||||
|
|
||||||
|
@@ -103,7 +103,13 @@ class HardwareManager(CoreSysAttributes):
|
|||||||
# Exctract all devices
|
# Exctract all devices
|
||||||
for device in self._udev.list_devices():
|
for device in self._udev.list_devices():
|
||||||
# Skip devices without mapping
|
# Skip devices without mapping
|
||||||
if not device.device_node or self.helper.hide_virtual_device(device):
|
try:
|
||||||
|
if not device.device_node or self.helper.hide_virtual_device(device):
|
||||||
|
continue
|
||||||
|
except UnicodeDecodeError as err:
|
||||||
|
# Some udev properties have an unkown/different encoding. This is a general
|
||||||
|
# problem with pyudev, see https://github.com/pyudev/pyudev/pull/230
|
||||||
|
_LOGGER.warning("Ignoring udev device due to error: %s", err)
|
||||||
continue
|
continue
|
||||||
self._devices[device.sys_name] = Device.import_udev(device)
|
self._devices[device.sys_name] = Device.import_udev(device)
|
||||||
|
|
||||||
|
@@ -367,6 +367,7 @@ class HomeAssistantCore(JobGroup):
|
|||||||
"""Restart Home Assistant Docker."""
|
"""Restart Home Assistant Docker."""
|
||||||
# Create safe mode marker file if necessary
|
# Create safe mode marker file if necessary
|
||||||
if safe_mode:
|
if safe_mode:
|
||||||
|
_LOGGER.debug("Creating safe mode marker file.")
|
||||||
await self.sys_run_in_executor(
|
await self.sys_run_in_executor(
|
||||||
(self.sys_config.path_homeassistant / SAFE_MODE_FILENAME).touch
|
(self.sys_config.path_homeassistant / SAFE_MODE_FILENAME).touch
|
||||||
)
|
)
|
||||||
@@ -383,8 +384,15 @@ class HomeAssistantCore(JobGroup):
|
|||||||
limit=JobExecutionLimit.GROUP_ONCE,
|
limit=JobExecutionLimit.GROUP_ONCE,
|
||||||
on_condition=HomeAssistantJobError,
|
on_condition=HomeAssistantJobError,
|
||||||
)
|
)
|
||||||
async def rebuild(self) -> None:
|
async def rebuild(self, *, safe_mode: bool = False) -> None:
|
||||||
"""Rebuild Home Assistant Docker container."""
|
"""Rebuild Home Assistant Docker container."""
|
||||||
|
# Create safe mode marker file if necessary
|
||||||
|
if safe_mode:
|
||||||
|
_LOGGER.debug("Creating safe mode marker file.")
|
||||||
|
await self.sys_run_in_executor(
|
||||||
|
(self.sys_config.path_homeassistant / SAFE_MODE_FILENAME).touch
|
||||||
|
)
|
||||||
|
|
||||||
with suppress(DockerError):
|
with suppress(DockerError):
|
||||||
await self.instance.stop()
|
await self.instance.stop()
|
||||||
await self.start()
|
await self.start()
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from ...const import CoreState
|
from ...const import CoreState
|
||||||
from ...coresys import CoreSys
|
from ...coresys import CoreSys
|
||||||
from ..const import ContextType, IssueType
|
from ..const import ContextType, IssueType, SuggestionType
|
||||||
from .base import CheckBase
|
from .base import CheckBase
|
||||||
|
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ class CheckDetachedAddonRemoved(CheckBase):
|
|||||||
IssueType.DETACHED_ADDON_REMOVED,
|
IssueType.DETACHED_ADDON_REMOVED,
|
||||||
ContextType.ADDON,
|
ContextType.ADDON,
|
||||||
reference=addon.slug,
|
reference=addon.slug,
|
||||||
|
suggestions=[SuggestionType.EXECUTE_REMOVE],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def approve_check(self, reference: str | None = None) -> bool:
|
async def approve_check(self, reference: str | None = None) -> bool:
|
||||||
|
52
supervisor/resolution/fixups/addon_execute_remove.py
Normal file
52
supervisor/resolution/fixups/addon_execute_remove.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"""Helpers to fix addon issue by removing it."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ...coresys import CoreSys
|
||||||
|
from ...exceptions import AddonsError, ResolutionFixupError
|
||||||
|
from ..const import ContextType, IssueType, SuggestionType
|
||||||
|
from .base import FixupBase
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(coresys: CoreSys) -> FixupBase:
|
||||||
|
"""Check setup function."""
|
||||||
|
return FixupAddonExecuteRemove(coresys)
|
||||||
|
|
||||||
|
|
||||||
|
class FixupAddonExecuteRemove(FixupBase):
|
||||||
|
"""Storage class for fixup."""
|
||||||
|
|
||||||
|
async def process_fixup(self, reference: str | None = None) -> None:
|
||||||
|
"""Initialize the fixup class."""
|
||||||
|
if not (addon := self.sys_addons.get(reference, local_only=True)):
|
||||||
|
_LOGGER.info("Addon %s already removed", reference)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Remove addon
|
||||||
|
_LOGGER.info("Remove addon: %s", reference)
|
||||||
|
try:
|
||||||
|
addon.uninstall()
|
||||||
|
except AddonsError as err:
|
||||||
|
_LOGGER.error("Could not remove %s due to %s", reference, err)
|
||||||
|
raise ResolutionFixupError() from None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def suggestion(self) -> SuggestionType:
|
||||||
|
"""Return a SuggestionType enum."""
|
||||||
|
return SuggestionType.EXECUTE_REMOVE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def context(self) -> ContextType:
|
||||||
|
"""Return a ContextType enum."""
|
||||||
|
return ContextType.ADDON
|
||||||
|
|
||||||
|
@property
|
||||||
|
def issues(self) -> list[IssueType]:
|
||||||
|
"""Return a IssueType enum list."""
|
||||||
|
return [IssueType.DETACHED_ADDON_REMOVED]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auto(self) -> bool:
|
||||||
|
"""Return if a fixup can be apply as auto fix."""
|
||||||
|
return False
|
@@ -4,6 +4,8 @@ import asyncio
|
|||||||
from unittest.mock import MagicMock, PropertyMock, patch
|
from unittest.mock import MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
|
from awesomeversion import AwesomeVersion
|
||||||
|
import pytest
|
||||||
|
|
||||||
from supervisor.addons.addon import Addon
|
from supervisor.addons.addon import Addon
|
||||||
from supervisor.addons.build import AddonBuild
|
from supervisor.addons.build import AddonBuild
|
||||||
@@ -285,3 +287,37 @@ async def test_api_addon_uninstall_remove_config(
|
|||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
assert not coresys.addons.get("local_example", local_only=True)
|
assert not coresys.addons.get("local_example", local_only=True)
|
||||||
assert not test_folder.exists()
|
assert not test_folder.exists()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_api_update_available_validates_version(
|
||||||
|
api_client: TestClient,
|
||||||
|
coresys: CoreSys,
|
||||||
|
install_addon_example: Addon,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
tmp_supervisor_data,
|
||||||
|
path_extern,
|
||||||
|
):
|
||||||
|
"""Test update available field is only true if user can update to latest version."""
|
||||||
|
install_addon_example.data["ingress"] = False
|
||||||
|
install_addon_example.data_store["version"] = "1.3.0"
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
resp = await api_client.get("/addons/local_example/info")
|
||||||
|
assert resp.status == 200
|
||||||
|
result = await resp.json()
|
||||||
|
assert result["data"]["version"] == "1.2.0"
|
||||||
|
assert result["data"]["version_latest"] == "1.3.0"
|
||||||
|
assert result["data"]["update_available"] is True
|
||||||
|
|
||||||
|
# If new version can't be installed due to HA version, then no update is available
|
||||||
|
coresys.homeassistant.version = AwesomeVersion("2024.04.0")
|
||||||
|
install_addon_example.data_store["homeassistant"] = "2024.06.0"
|
||||||
|
|
||||||
|
resp = await api_client.get("/addons/local_example/info")
|
||||||
|
assert resp.status == 200
|
||||||
|
result = await resp.json()
|
||||||
|
assert result["data"]["version"] == "1.2.0"
|
||||||
|
assert result["data"]["version_latest"] == "1.3.0"
|
||||||
|
assert result["data"]["update_available"] is False
|
||||||
|
|
||||||
|
assert "Add-on local_example not supported" not in caplog.text
|
||||||
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
|
from awesomeversion import AwesomeVersion
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from supervisor.coresys import CoreSys
|
from supervisor.coresys import CoreSys
|
||||||
@@ -115,3 +116,29 @@ async def test_api_restart(
|
|||||||
|
|
||||||
assert container.restart.call_count == 2
|
assert container.restart.call_count == 2
|
||||||
assert safe_mode_marker.exists()
|
assert safe_mode_marker.exists()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_api_rebuild(
|
||||||
|
api_client: TestClient,
|
||||||
|
coresys: CoreSys,
|
||||||
|
container: MagicMock,
|
||||||
|
tmp_supervisor_data: Path,
|
||||||
|
path_extern,
|
||||||
|
):
|
||||||
|
"""Test rebuilding homeassistant."""
|
||||||
|
coresys.homeassistant.version = AwesomeVersion("2023.09.0")
|
||||||
|
safe_mode_marker = tmp_supervisor_data / "homeassistant" / "safe-mode"
|
||||||
|
|
||||||
|
with patch.object(HomeAssistantCore, "_block_till_run"):
|
||||||
|
await api_client.post("/homeassistant/rebuild")
|
||||||
|
|
||||||
|
assert container.remove.call_count == 2
|
||||||
|
container.start.assert_called_once()
|
||||||
|
assert not safe_mode_marker.exists()
|
||||||
|
|
||||||
|
with patch.object(HomeAssistantCore, "_block_till_run"):
|
||||||
|
await api_client.post("/homeassistant/rebuild", json={"safe_mode": True})
|
||||||
|
|
||||||
|
assert container.remove.call_count == 4
|
||||||
|
assert container.start.call_count == 2
|
||||||
|
assert safe_mode_marker.exists()
|
||||||
|
@@ -1750,3 +1750,40 @@ async def test_reload_error(
|
|||||||
|
|
||||||
assert "Could not list backups" in caplog.text
|
assert "Could not list backups" in caplog.text
|
||||||
assert coresys.core.healthy is healthy_expected
|
assert coresys.core.healthy is healthy_expected
|
||||||
|
|
||||||
|
|
||||||
|
async def test_monitoring_after_full_restore(
|
||||||
|
coresys: CoreSys, full_backup_mock, install_addon_ssh, container
|
||||||
|
):
|
||||||
|
"""Test monitoring of addon state still works after full restore."""
|
||||||
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
coresys.homeassistant.core.start = AsyncMock(return_value=None)
|
||||||
|
coresys.homeassistant.core.stop = AsyncMock(return_value=None)
|
||||||
|
coresys.homeassistant.core.update = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
manager = BackupManager(coresys)
|
||||||
|
|
||||||
|
backup_instance = full_backup_mock.return_value
|
||||||
|
assert await manager.do_restore_full(backup_instance)
|
||||||
|
|
||||||
|
backup_instance.restore_addons.assert_called_once_with([TEST_ADDON_SLUG])
|
||||||
|
assert coresys.core.state == CoreState.RUNNING
|
||||||
|
coresys.docker.unload.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_monitoring_after_partial_restore(
|
||||||
|
coresys: CoreSys, partial_backup_mock, install_addon_ssh, container
|
||||||
|
):
|
||||||
|
"""Test monitoring of addon state still works after full restore."""
|
||||||
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
|
||||||
|
manager = BackupManager(coresys)
|
||||||
|
|
||||||
|
backup_instance = partial_backup_mock.return_value
|
||||||
|
assert await manager.do_restore_partial(backup_instance, addons=[TEST_ADDON_SLUG])
|
||||||
|
|
||||||
|
backup_instance.restore_addons.assert_called_once_with([TEST_ADDON_SLUG])
|
||||||
|
assert coresys.core.state == CoreState.RUNNING
|
||||||
|
coresys.docker.unload.assert_not_called()
|
||||||
|
@@ -37,6 +37,7 @@ async def test_check(coresys: CoreSys, install_addon_ssh: Addon):
|
|||||||
assert coresys.resolution.issues[0].type is IssueType.DETACHED_ADDON_MISSING
|
assert coresys.resolution.issues[0].type is IssueType.DETACHED_ADDON_MISSING
|
||||||
assert coresys.resolution.issues[0].context is ContextType.ADDON
|
assert coresys.resolution.issues[0].context is ContextType.ADDON
|
||||||
assert coresys.resolution.issues[0].reference == install_addon_ssh.slug
|
assert coresys.resolution.issues[0].reference == install_addon_ssh.slug
|
||||||
|
assert len(coresys.resolution.suggestions) == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_approve(coresys: CoreSys, install_addon_ssh: Addon):
|
async def test_approve(coresys: CoreSys, install_addon_ssh: Addon):
|
||||||
|
@@ -9,7 +9,7 @@ from supervisor.coresys import CoreSys
|
|||||||
from supervisor.resolution.checks.detached_addon_removed import (
|
from supervisor.resolution.checks.detached_addon_removed import (
|
||||||
CheckDetachedAddonRemoved,
|
CheckDetachedAddonRemoved,
|
||||||
)
|
)
|
||||||
from supervisor.resolution.const import ContextType, IssueType
|
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
||||||
|
|
||||||
|
|
||||||
async def test_base(coresys: CoreSys):
|
async def test_base(coresys: CoreSys):
|
||||||
@@ -28,6 +28,7 @@ async def test_check(
|
|||||||
|
|
||||||
await detached_addon_removed()
|
await detached_addon_removed()
|
||||||
assert len(coresys.resolution.issues) == 0
|
assert len(coresys.resolution.issues) == 0
|
||||||
|
assert len(coresys.resolution.suggestions) == 0
|
||||||
|
|
||||||
(addons_dir := tmp_supervisor_data / "addons" / "local").mkdir()
|
(addons_dir := tmp_supervisor_data / "addons" / "local").mkdir()
|
||||||
with patch.object(
|
with patch.object(
|
||||||
@@ -42,6 +43,11 @@ async def test_check(
|
|||||||
assert coresys.resolution.issues[0].context is ContextType.ADDON
|
assert coresys.resolution.issues[0].context is ContextType.ADDON
|
||||||
assert coresys.resolution.issues[0].reference == install_addon_ssh.slug
|
assert coresys.resolution.issues[0].reference == install_addon_ssh.slug
|
||||||
|
|
||||||
|
assert len(coresys.resolution.suggestions) == 1
|
||||||
|
assert coresys.resolution.suggestions[0].type is SuggestionType.EXECUTE_REMOVE
|
||||||
|
assert coresys.resolution.suggestions[0].context is ContextType.ADDON
|
||||||
|
assert coresys.resolution.suggestions[0].reference == install_addon_ssh.slug
|
||||||
|
|
||||||
|
|
||||||
async def test_approve(
|
async def test_approve(
|
||||||
coresys: CoreSys, install_addon_ssh: Addon, tmp_supervisor_data: Path
|
coresys: CoreSys, install_addon_ssh: Addon, tmp_supervisor_data: Path
|
||||||
|
34
tests/resolution/fixup/test_addon_execute_remove.py
Normal file
34
tests/resolution/fixup/test_addon_execute_remove.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
"""Test evaluation base."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from supervisor.addons.addon import Addon
|
||||||
|
from supervisor.coresys import CoreSys
|
||||||
|
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
||||||
|
from supervisor.resolution.data import Issue, Suggestion
|
||||||
|
from supervisor.resolution.fixups.addon_execute_remove import FixupAddonExecuteRemove
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fixup(coresys: CoreSys, install_addon_ssh: Addon):
|
||||||
|
"""Test fixup."""
|
||||||
|
addon_execute_remove = FixupAddonExecuteRemove(coresys)
|
||||||
|
|
||||||
|
assert addon_execute_remove.auto is False
|
||||||
|
|
||||||
|
coresys.resolution.suggestions = Suggestion(
|
||||||
|
SuggestionType.EXECUTE_REMOVE,
|
||||||
|
ContextType.ADDON,
|
||||||
|
reference=install_addon_ssh.slug,
|
||||||
|
)
|
||||||
|
coresys.resolution.issues = Issue(
|
||||||
|
IssueType.DETACHED_ADDON_REMOVED,
|
||||||
|
ContextType.ADDON,
|
||||||
|
reference=install_addon_ssh.slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(Addon, "uninstall") as uninstall:
|
||||||
|
await addon_execute_remove()
|
||||||
|
|
||||||
|
assert uninstall.called
|
||||||
|
|
||||||
|
assert len(coresys.resolution.suggestions) == 0
|
||||||
|
assert len(coresys.resolution.issues) == 0
|
Reference in New Issue
Block a user