Skip to content

Commit

Permalink
Separate issues into missing and removed
Browse files Browse the repository at this point in the history
  • Loading branch information
mdegat01 committed May 22, 2024
1 parent 66c95d1 commit ea67c36
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 29 deletions.
49 changes: 49 additions & 0 deletions supervisor/resolution/checks/detached_addon_missing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Helpers to check for detached addons due to repo misisng."""

from ...const import CoreState
from ...coresys import CoreSys
from ..const import ContextType, IssueType
from .base import CheckBase


def setup(coresys: CoreSys) -> CheckBase:
"""Check setup function."""
return CheckDetachedAddonMissing(coresys)


class CheckDetachedAddonMissing(CheckBase):
"""CheckDetachedAddonMissing class for check."""

async def run_check(self) -> None:
"""Run check if not affected by issue."""
for addon in self.sys_addons.installed:
if (
addon.is_detached
and addon.repository not in self.sys_store.repositories
):
self.sys_resolution.create_issue(
IssueType.DETACHED_ADDON_MISSING,
ContextType.ADDON,
reference=addon.slug,
)

async def approve_check(self, reference: str | None = None) -> bool:
"""Approve check if it is affected by issue."""
return (
addon := self.sys_addons.get(reference, local_only=True)
) and addon.is_detached

@property
def issue(self) -> IssueType:
"""Return a IssueType enum."""
return IssueType.DETACHED_ADDON_MISSING

@property
def context(self) -> ContextType:
"""Return a ContextType enum."""
return ContextType.ADDON

@property
def states(self) -> list[CoreState]:
"""Return a list of valid states when this check can run."""
return [CoreState.SETUP]
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Helpers to check for detached addons."""
"""Helpers to check for detached addons due to removal from repo."""

from ...const import CoreState
from ...coresys import CoreSys
Expand All @@ -8,18 +8,18 @@

def setup(coresys: CoreSys) -> CheckBase:
"""Check setup function."""
return CheckDetachedAddon(coresys)
return CheckDetachedAddonRemoved(coresys)


class CheckDetachedAddon(CheckBase):
"""CheckDetachedAddon class for check."""
class CheckDetachedAddonRemoved(CheckBase):
"""CheckDetachedAddonRemoved class for check."""

async def run_check(self) -> None:
"""Run check if not affected by issue."""
for addon in self.sys_addons.installed:
if addon.is_detached:
if addon.is_detached and addon.repository in self.sys_store.repositories:
self.sys_resolution.create_issue(
IssueType.DETACHED_ADDON,
IssueType.DETACHED_ADDON_REMOVED,
ContextType.ADDON,
reference=addon.slug,
)
Expand All @@ -33,7 +33,7 @@ async def approve_check(self, reference: str | None = None) -> bool:
@property
def issue(self) -> IssueType:
"""Return a IssueType enum."""
return IssueType.DETACHED_ADDON
return IssueType.DETACHED_ADDON_REMOVED

@property
def context(self) -> ContextType:
Expand All @@ -43,4 +43,4 @@ def context(self) -> ContextType:
@property
def states(self) -> list[CoreState]:
"""Return a list of valid states when this check can run."""
return [CoreState.RUNNING, CoreState.SETUP]
return [CoreState.SETUP]
3 changes: 2 additions & 1 deletion supervisor/resolution/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ class IssueType(StrEnum):
CORRUPT_DOCKER = "corrupt_docker"
CORRUPT_REPOSITORY = "corrupt_repository"
CORRUPT_FILESYSTEM = "corrupt_filesystem"
DETACHED_ADDON = "detached_addon"
DETACHED_ADDON_MISSING = "detached_addon_missing"
DETACHED_ADDON_REMOVED = "detached_addon_removed"
DISABLED_DATA_DISK = "disabled_data_disk"
DNS_LOOP = "dns_loop"
DNS_SERVER_FAILED = "dns_server_failed"
Expand Down
85 changes: 85 additions & 0 deletions tests/resolution/check/test_check_detached_addon_missing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Test check for detached addons due to repo missing."""
from unittest.mock import patch

from supervisor.addons.addon import Addon
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.resolution.checks.detached_addon_missing import (
CheckDetachedAddonMissing,
)
from supervisor.resolution.const import ContextType, IssueType


async def test_base(coresys: CoreSys):
"""Test check basics."""
detached_addon_missing = CheckDetachedAddonMissing(coresys)
assert detached_addon_missing.slug == "detached_addon_missing"
assert detached_addon_missing.enabled


async def test_check(coresys: CoreSys, install_addon_ssh: Addon):
"""Test check for detached addons."""
detached_addon_missing = CheckDetachedAddonMissing(coresys)
coresys.core.state = CoreState.SETUP

await detached_addon_missing()
assert len(coresys.resolution.issues) == 0

# Mock test addon was been installed from a now non-existent store
install_addon_ssh.slug = "abc123_ssh"
coresys.addons.data.system["abc123_ssh"] = coresys.addons.data.system["local_ssh"]
coresys.addons.local["abc123_ssh"] = coresys.addons.local["local_ssh"]
install_addon_ssh.data["repository"] = "abc123"

await detached_addon_missing()

assert len(coresys.resolution.issues) == 1
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].reference == install_addon_ssh.slug


async def test_approve(coresys: CoreSys, install_addon_ssh: Addon):
"""Test approve existing detached addon issues."""
detached_addon_missing = CheckDetachedAddonMissing(coresys)
coresys.core.state = CoreState.SETUP

assert (
await detached_addon_missing.approve_check(reference=install_addon_ssh.slug)
is False
)

# Mock test addon was been installed from a now non-existent store
install_addon_ssh.slug = "abc123_ssh"
coresys.addons.data.system["abc123_ssh"] = coresys.addons.data.system["local_ssh"]
coresys.addons.local["abc123_ssh"] = coresys.addons.local["local_ssh"]
install_addon_ssh.data["repository"] = "abc123"

assert (
await detached_addon_missing.approve_check(reference=install_addon_ssh.slug)
is True
)


async def test_did_run(coresys: CoreSys):
"""Test that the check ran as expected."""
detached_addon_missing = CheckDetachedAddonMissing(coresys)
should_run = detached_addon_missing.states
should_not_run = [state for state in CoreState if state not in should_run]
assert should_run == [CoreState.SETUP]
assert len(should_not_run) != 0

with patch.object(
CheckDetachedAddonMissing, "run_check", return_value=None
) as check:
for state in should_run:
coresys.core.state = state
await detached_addon_missing()
check.assert_called_once()
check.reset_mock()

for state in should_not_run:
coresys.core.state = state
await detached_addon_missing()
check.assert_not_called()
check.reset_mock()
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
"""Test check for detached addons."""
"""Test check for detached addons due to removal from repo."""
from pathlib import Path
from unittest.mock import PropertyMock, patch

from supervisor.addons.addon import Addon
from supervisor.config import CoreConfig
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.resolution.checks.detached_addon import CheckDetachedAddon
from supervisor.resolution.checks.detached_addon_removed import (
CheckDetachedAddonRemoved,
)
from supervisor.resolution.const import ContextType, IssueType


async def test_base(coresys: CoreSys):
"""Test check basics."""
detached_addon = CheckDetachedAddon(coresys)
assert detached_addon.slug == "detached_addon"
assert detached_addon.enabled
detached_addon_removed = CheckDetachedAddonRemoved(coresys)
assert detached_addon_removed.slug == "detached_addon_removed"
assert detached_addon_removed.enabled


async def test_check(
coresys: CoreSys, install_addon_ssh: Addon, tmp_supervisor_data: Path
):
"""Test check for detached addons."""
detached_addon = CheckDetachedAddon(coresys)
coresys.core.state = CoreState.RUNNING
detached_addon_removed = CheckDetachedAddonRemoved(coresys)
coresys.core.state = CoreState.SETUP

await detached_addon()
await detached_addon_removed()
assert len(coresys.resolution.issues) == 0

(addons_dir := tmp_supervisor_data / "addons" / "local").mkdir()
Expand All @@ -33,10 +35,10 @@ async def test_check(
):
await coresys.store.load()

await detached_addon()
await detached_addon_removed()

assert len(coresys.resolution.issues) == 1
assert coresys.resolution.issues[0].type is IssueType.DETACHED_ADDON
assert coresys.resolution.issues[0].type is IssueType.DETACHED_ADDON_REMOVED
assert coresys.resolution.issues[0].context is ContextType.ADDON
assert coresys.resolution.issues[0].reference == install_addon_ssh.slug

Expand All @@ -45,37 +47,45 @@ async def test_approve(
coresys: CoreSys, install_addon_ssh: Addon, tmp_supervisor_data: Path
):
"""Test approve existing detached addon issues."""
detached_addon = CheckDetachedAddon(coresys)
coresys.core.state = CoreState.RUNNING
detached_addon_removed = CheckDetachedAddonRemoved(coresys)
coresys.core.state = CoreState.SETUP

assert await detached_addon.approve_check(reference=install_addon_ssh.slug) is False
assert (
await detached_addon_removed.approve_check(reference=install_addon_ssh.slug)
is False
)

(addons_dir := tmp_supervisor_data / "addons" / "local").mkdir()
with patch.object(
CoreConfig, "path_addons_local", new=PropertyMock(return_value=addons_dir)
):
await coresys.store.load()

assert await detached_addon.approve_check(reference=install_addon_ssh.slug) is True
assert (
await detached_addon_removed.approve_check(reference=install_addon_ssh.slug)
is True
)


async def test_did_run(coresys: CoreSys):
"""Test that the check ran as expected."""
detached_addon = CheckDetachedAddon(coresys)
should_run = detached_addon.states
detached_addon_removed = CheckDetachedAddonRemoved(coresys)
should_run = detached_addon_removed.states
should_not_run = [state for state in CoreState if state not in should_run]
assert should_run == [CoreState.RUNNING, CoreState.SETUP]
assert should_run == [CoreState.SETUP]
assert len(should_not_run) != 0

with patch.object(CheckDetachedAddon, "run_check", return_value=None) as check:
with patch.object(
CheckDetachedAddonRemoved, "run_check", return_value=None
) as check:
for state in should_run:
coresys.core.state = state
await detached_addon()
await detached_addon_removed()
check.assert_called_once()
check.reset_mock()

for state in should_not_run:
coresys.core.state = state
await detached_addon()
await detached_addon_removed()
check.assert_not_called()
check.reset_mock()

0 comments on commit ea67c36

Please sign in to comment.