mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-14 12:46:32 +00:00
Fire events on issue changes (#3818)
* Fire events on issue changes * API includes if suggestion has autofix
This commit is contained in:
parent
ffa524d3a4
commit
d5f9fcfdc7
@ -277,6 +277,10 @@ class RestAPI(CoreSysAttributes):
|
|||||||
"/resolution/issue/{issue}",
|
"/resolution/issue/{issue}",
|
||||||
api_resolution.dismiss_issue,
|
api_resolution.dismiss_issue,
|
||||||
),
|
),
|
||||||
|
web.get(
|
||||||
|
"/resolution/issue/{issue}/suggestions",
|
||||||
|
api_resolution.suggestions_for_issue,
|
||||||
|
),
|
||||||
web.post("/resolution/healthcheck", api_resolution.healthcheck),
|
web.post("/resolution/healthcheck", api_resolution.healthcheck),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -7,6 +7,7 @@ import attr
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
|
ATTR_AUTO,
|
||||||
ATTR_CHECKS,
|
ATTR_CHECKS,
|
||||||
ATTR_ENABLED,
|
ATTR_ENABLED,
|
||||||
ATTR_ISSUES,
|
ATTR_ISSUES,
|
||||||
@ -17,6 +18,7 @@ from ..const import (
|
|||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIError, ResolutionNotFound
|
from ..exceptions import APIError, ResolutionNotFound
|
||||||
|
from ..resolution.data import Suggestion
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
SCHEMA_CHECK_OPTIONS = vol.Schema({vol.Optional(ATTR_ENABLED): bool})
|
SCHEMA_CHECK_OPTIONS = vol.Schema({vol.Optional(ATTR_ENABLED): bool})
|
||||||
@ -25,14 +27,26 @@ SCHEMA_CHECK_OPTIONS = vol.Schema({vol.Optional(ATTR_ENABLED): bool})
|
|||||||
class APIResoulution(CoreSysAttributes):
|
class APIResoulution(CoreSysAttributes):
|
||||||
"""Handle REST API for resoulution."""
|
"""Handle REST API for resoulution."""
|
||||||
|
|
||||||
|
def _generate_suggestion_information(self, suggestion: Suggestion):
|
||||||
|
"""Generate suggestion information for response."""
|
||||||
|
resp = attr.asdict(suggestion)
|
||||||
|
resp[ATTR_AUTO] = bool(
|
||||||
|
[
|
||||||
|
fix
|
||||||
|
for fix in self.sys_resolution.fixup.fixes_for_suggestion(suggestion)
|
||||||
|
if fix.auto
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return resp
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def info(self, request: web.Request) -> dict[str, Any]:
|
async def info(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Return network information."""
|
"""Return resolution information."""
|
||||||
return {
|
return {
|
||||||
ATTR_UNSUPPORTED: self.sys_resolution.unsupported,
|
ATTR_UNSUPPORTED: self.sys_resolution.unsupported,
|
||||||
ATTR_UNHEALTHY: self.sys_resolution.unhealthy,
|
ATTR_UNHEALTHY: self.sys_resolution.unhealthy,
|
||||||
ATTR_SUGGESTIONS: [
|
ATTR_SUGGESTIONS: [
|
||||||
attr.asdict(suggestion)
|
self._generate_suggestion_information(suggestion)
|
||||||
for suggestion in self.sys_resolution.suggestions
|
for suggestion in self.sys_resolution.suggestions
|
||||||
],
|
],
|
||||||
ATTR_ISSUES: [attr.asdict(issue) for issue in self.sys_resolution.issues],
|
ATTR_ISSUES: [attr.asdict(issue) for issue in self.sys_resolution.issues],
|
||||||
@ -64,6 +78,20 @@ class APIResoulution(CoreSysAttributes):
|
|||||||
except ResolutionNotFound:
|
except ResolutionNotFound:
|
||||||
raise APIError("The supplied UUID is not a valid suggestion") from None
|
raise APIError("The supplied UUID is not a valid suggestion") from None
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def suggestions_for_issue(self, request: web.Request) -> dict[str, Any]:
|
||||||
|
"""Return suggestions that fix an issue."""
|
||||||
|
try:
|
||||||
|
issue = self.sys_resolution.get_issue(request.match_info.get("issue"))
|
||||||
|
return {
|
||||||
|
ATTR_SUGGESTIONS: [
|
||||||
|
self._generate_suggestion_information(suggestion)
|
||||||
|
for suggestion in self.sys_resolution.suggestions_for_issue(issue)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
except ResolutionNotFound:
|
||||||
|
raise APIError("The supplied UUID is not a valid issue") from None
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def dismiss_issue(self, request: web.Request) -> None:
|
async def dismiss_issue(self, request: web.Request) -> None:
|
||||||
"""Dismiss issue."""
|
"""Dismiss issue."""
|
||||||
|
@ -97,6 +97,7 @@ ATTR_AUDIO_INPUT = "audio_input"
|
|||||||
ATTR_AUDIO_OUTPUT = "audio_output"
|
ATTR_AUDIO_OUTPUT = "audio_output"
|
||||||
ATTR_AUTH = "auth"
|
ATTR_AUTH = "auth"
|
||||||
ATTR_AUTH_API = "auth_api"
|
ATTR_AUTH_API = "auth_api"
|
||||||
|
ATTR_AUTO = "auto"
|
||||||
ATTR_AUTO_UPDATE = "auto_update"
|
ATTR_AUTO_UPDATE = "auto_update"
|
||||||
ATTR_AVAILABLE = "available"
|
ATTR_AVAILABLE = "available"
|
||||||
ATTR_BACKUP = "backup"
|
ATTR_BACKUP = "backup"
|
||||||
@ -441,6 +442,8 @@ class BusEvent(str, Enum):
|
|||||||
|
|
||||||
HARDWARE_NEW_DEVICE = "hardware_new_device"
|
HARDWARE_NEW_DEVICE = "hardware_new_device"
|
||||||
HARDWARE_REMOVE_DEVICE = "hardware_remove_device"
|
HARDWARE_REMOVE_DEVICE = "hardware_remove_device"
|
||||||
|
ISSUE_CHANGED = "issue_changed"
|
||||||
|
ISSUE_REMOVED = "issue_removed"
|
||||||
DOCKER_CONTAINER_STATE_CHANGE = "docker_container_state_change"
|
DOCKER_CONTAINER_STATE_CHANGE = "docker_container_state_change"
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import logging
|
|||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..jobs.const import JobCondition
|
from ..jobs.const import JobCondition
|
||||||
from ..jobs.decorator import Job
|
from ..jobs.decorator import Job
|
||||||
from .data import Suggestion
|
from .data import Issue, Suggestion
|
||||||
from .fixups.base import FixupBase
|
from .fixups.base import FixupBase
|
||||||
from .validate import get_valid_modules
|
from .validate import get_valid_modules
|
||||||
|
|
||||||
@ -51,9 +51,23 @@ class ResolutionFixup(CoreSysAttributes):
|
|||||||
|
|
||||||
_LOGGER.info("System autofix complete")
|
_LOGGER.info("System autofix complete")
|
||||||
|
|
||||||
|
def fixes_for_suggestion(self, suggestion: Suggestion) -> list[FixupBase]:
|
||||||
|
"""Get fixups to run if the suggestion is applied."""
|
||||||
|
return [
|
||||||
|
fix
|
||||||
|
for fix in self.all_fixes
|
||||||
|
if fix.suggestion == suggestion.type and fix.context == suggestion.context
|
||||||
|
]
|
||||||
|
|
||||||
|
def fixes_for_issue(self, issue: Issue) -> list[FixupBase]:
|
||||||
|
"""Get fixups that would fix the issue if run."""
|
||||||
|
return [
|
||||||
|
fix
|
||||||
|
for fix in self.all_fixes
|
||||||
|
if issue.type in fix.issues and issue.context == fix.context
|
||||||
|
]
|
||||||
|
|
||||||
async def apply_fixup(self, suggestion: Suggestion) -> None:
|
async def apply_fixup(self, suggestion: Suggestion) -> None:
|
||||||
"""Apply a fixup for a suggestion."""
|
"""Apply a fixup for a suggestion."""
|
||||||
for fix in self.all_fixes:
|
for fix in self.fixes_for_suggestion(suggestion):
|
||||||
if fix.suggestion != suggestion.type or fix.context != suggestion.context:
|
|
||||||
continue
|
|
||||||
await fix()
|
await fix()
|
||||||
|
@ -20,12 +20,7 @@ class FixupBase(ABC, CoreSysAttributes):
|
|||||||
async def __call__(self) -> None:
|
async def __call__(self) -> None:
|
||||||
"""Execute the evaluation."""
|
"""Execute the evaluation."""
|
||||||
# Get suggestion to fix
|
# Get suggestion to fix
|
||||||
fixing_suggestion: Suggestion | None = None
|
fixing_suggestion: Suggestion | None = next(iter(self.all_suggestions), None)
|
||||||
for suggestion in self.sys_resolution.suggestions:
|
|
||||||
if suggestion.type != self.suggestion or suggestion.context != self.context:
|
|
||||||
continue
|
|
||||||
fixing_suggestion = suggestion
|
|
||||||
break
|
|
||||||
|
|
||||||
# No suggestion
|
# No suggestion
|
||||||
if fixing_suggestion is None:
|
if fixing_suggestion is None:
|
||||||
@ -38,15 +33,12 @@ class FixupBase(ABC, CoreSysAttributes):
|
|||||||
except ResolutionFixupError:
|
except ResolutionFixupError:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.sys_resolution.dismiss_suggestion(fixing_suggestion)
|
|
||||||
|
|
||||||
# Cleanup issue
|
# Cleanup issue
|
||||||
for issue_type in self.issues:
|
for issue in self.sys_resolution.issues_for_suggestion(fixing_suggestion):
|
||||||
issue = Issue(issue_type, self.context, fixing_suggestion.reference)
|
|
||||||
if issue not in self.sys_resolution.issues:
|
|
||||||
continue
|
|
||||||
self.sys_resolution.dismiss_issue(issue)
|
self.sys_resolution.dismiss_issue(issue)
|
||||||
|
|
||||||
|
self.sys_resolution.dismiss_suggestion(fixing_suggestion)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def process_fixup(self, reference: str | None = None) -> None:
|
async def process_fixup(self, reference: str | None = None) -> None:
|
||||||
"""Run processing of fixup."""
|
"""Run processing of fixup."""
|
||||||
@ -71,6 +63,24 @@ class FixupBase(ABC, CoreSysAttributes):
|
|||||||
"""Return if a fixup can be apply as auto fix."""
|
"""Return if a fixup can be apply as auto fix."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_suggestions(self) -> list[Suggestion]:
|
||||||
|
"""List of all suggestions which when applied run this fixup."""
|
||||||
|
return [
|
||||||
|
suggestion
|
||||||
|
for suggestion in self.sys_resolution.suggestions
|
||||||
|
if suggestion.type == self.suggestion and suggestion.context == self.context
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_issues(self) -> list[Issue]:
|
||||||
|
"""List of all issues which could be fixed by this fixup."""
|
||||||
|
return [
|
||||||
|
issue
|
||||||
|
for issue in self.sys_resolution.issues
|
||||||
|
if issue.type in self.issues and issue.context == self.context
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def slug(self) -> str:
|
def slug(self) -> str:
|
||||||
"""Return the check slug."""
|
"""Return the check slug."""
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from ..const import BusEvent
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import ResolutionError, ResolutionNotFound
|
from ..exceptions import ResolutionError, ResolutionNotFound
|
||||||
from ..utils.common import FileConfiguration
|
from ..utils.common import FileConfiguration
|
||||||
@ -82,6 +83,9 @@ class ResolutionManager(FileConfiguration, CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
self._issues.append(issue)
|
self._issues.append(issue)
|
||||||
|
|
||||||
|
# Event on issue creation
|
||||||
|
self.sys_bus.fire_event(BusEvent.ISSUE_CHANGED, issue)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def suggestions(self) -> list[Suggestion]:
|
def suggestions(self) -> list[Suggestion]:
|
||||||
"""Return a list of suggestions that can handled."""
|
"""Return a list of suggestions that can handled."""
|
||||||
@ -92,6 +96,7 @@ class ResolutionManager(FileConfiguration, CoreSysAttributes):
|
|||||||
"""Add suggestion."""
|
"""Add suggestion."""
|
||||||
if suggestion in self._suggestions:
|
if suggestion in self._suggestions:
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Create new suggestion %s - %s / %s",
|
"Create new suggestion %s - %s / %s",
|
||||||
suggestion.type,
|
suggestion.type,
|
||||||
@ -100,6 +105,10 @@ class ResolutionManager(FileConfiguration, CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
self._suggestions.append(suggestion)
|
self._suggestions.append(suggestion)
|
||||||
|
|
||||||
|
# Event on suggestion added to issue
|
||||||
|
for issue in self.issues_for_suggestion(suggestion):
|
||||||
|
self.sys_bus.fire_event(BusEvent.ISSUE_CHANGED, issue)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unsupported(self) -> list[UnsupportedReason]:
|
def unsupported(self) -> list[UnsupportedReason]:
|
||||||
"""Return a list of unsupported reasons."""
|
"""Return a list of unsupported reasons."""
|
||||||
@ -146,13 +155,11 @@ class ResolutionManager(FileConfiguration, CoreSysAttributes):
|
|||||||
suggestions: list[SuggestionType] | None = None,
|
suggestions: list[SuggestionType] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create issues and suggestion."""
|
"""Create issues and suggestion."""
|
||||||
self.issues = Issue(issue, context, reference)
|
if suggestions:
|
||||||
if not suggestions:
|
for suggestion in suggestions:
|
||||||
return
|
self.suggestions = Suggestion(suggestion, context, reference)
|
||||||
|
|
||||||
# Add suggestions
|
self.issues = Issue(issue, context, reference)
|
||||||
for suggestion in suggestions:
|
|
||||||
self.suggestions = Suggestion(suggestion, context, reference)
|
|
||||||
|
|
||||||
async def load(self):
|
async def load(self):
|
||||||
"""Load the resoulution manager."""
|
"""Load the resoulution manager."""
|
||||||
@ -191,6 +198,10 @@ class ResolutionManager(FileConfiguration, CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
self._suggestions.remove(suggestion)
|
self._suggestions.remove(suggestion)
|
||||||
|
|
||||||
|
# Event on suggestion removed from issues
|
||||||
|
for issue in self.issues_for_suggestion(suggestion):
|
||||||
|
self.sys_bus.fire_event(BusEvent.ISSUE_CHANGED, issue)
|
||||||
|
|
||||||
def dismiss_issue(self, issue: Issue) -> None:
|
def dismiss_issue(self, issue: Issue) -> None:
|
||||||
"""Dismiss suggested action."""
|
"""Dismiss suggested action."""
|
||||||
if issue not in self._issues:
|
if issue not in self._issues:
|
||||||
@ -199,8 +210,29 @@ class ResolutionManager(FileConfiguration, CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
self._issues.remove(issue)
|
self._issues.remove(issue)
|
||||||
|
|
||||||
|
# Event on issue removal
|
||||||
|
self.sys_bus.fire_event(BusEvent.ISSUE_REMOVED, issue)
|
||||||
|
|
||||||
def dismiss_unsupported(self, reason: Issue) -> None:
|
def dismiss_unsupported(self, reason: Issue) -> None:
|
||||||
"""Dismiss a reason for unsupported."""
|
"""Dismiss a reason for unsupported."""
|
||||||
if reason not in self._unsupported:
|
if reason not in self._unsupported:
|
||||||
raise ResolutionError(f"The reason {reason} is not active", _LOGGER.warning)
|
raise ResolutionError(f"The reason {reason} is not active", _LOGGER.warning)
|
||||||
self._unsupported.remove(reason)
|
self._unsupported.remove(reason)
|
||||||
|
|
||||||
|
def suggestions_for_issue(self, issue: Issue) -> set[Suggestion]:
|
||||||
|
"""Get suggestions that fix an issue."""
|
||||||
|
return {
|
||||||
|
suggestion
|
||||||
|
for fix in self.fixup.fixes_for_issue(issue)
|
||||||
|
for suggestion in fix.all_suggestions
|
||||||
|
if suggestion.reference == issue.reference
|
||||||
|
}
|
||||||
|
|
||||||
|
def issues_for_suggestion(self, suggestion: Suggestion) -> set[Issue]:
|
||||||
|
"""Get issues fixed by a suggestion."""
|
||||||
|
return {
|
||||||
|
issue
|
||||||
|
for fix in self.fixup.fixes_for_suggestion(suggestion)
|
||||||
|
for issue in fix.all_issues
|
||||||
|
if issue.reference == suggestion.reference
|
||||||
|
}
|
||||||
|
@ -130,3 +130,30 @@ async def test_api_resolution_check_run(coresys: CoreSys, api_client):
|
|||||||
await api_client.post(f"/resolution/check/{free_space.slug}/run")
|
await api_client.post(f"/resolution/check/{free_space.slug}/run")
|
||||||
|
|
||||||
assert free_space.run_check.called
|
assert free_space.run_check.called
|
||||||
|
|
||||||
|
|
||||||
|
async def test_api_resolution_suggestions_for_issue(coresys: CoreSys, api_client):
|
||||||
|
"""Test getting suggestions that fix an issue."""
|
||||||
|
coresys.resolution.issues = corrupt_repo = Issue(
|
||||||
|
IssueType.CORRUPT_REPOSITORY, ContextType.STORE, "repo_1"
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = await api_client.get(f"/resolution/issue/{corrupt_repo.uuid}/suggestions")
|
||||||
|
result = await resp.json()
|
||||||
|
|
||||||
|
assert result["data"]["suggestions"] == []
|
||||||
|
|
||||||
|
coresys.resolution.suggestions = execute_reset = Suggestion(
|
||||||
|
SuggestionType.EXECUTE_RESET, ContextType.STORE, "repo_1"
|
||||||
|
)
|
||||||
|
coresys.resolution.suggestions = execute_remove = Suggestion(
|
||||||
|
SuggestionType.EXECUTE_REMOVE, ContextType.STORE, "repo_1"
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = await api_client.get(f"/resolution/issue/{corrupt_repo.uuid}/suggestions")
|
||||||
|
result = await resp.json()
|
||||||
|
|
||||||
|
assert result["data"]["suggestions"][0]["uuid"] == execute_reset.uuid
|
||||||
|
assert result["data"]["suggestions"][0]["auto"] is True
|
||||||
|
assert result["data"]["suggestions"][1]["uuid"] == execute_remove.uuid
|
||||||
|
assert result["data"]["suggestions"][1]["auto"] is False
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
"""Tests for resolution manager."""
|
"""Tests for resolution manager."""
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from supervisor.const import BusEvent
|
||||||
from supervisor.coresys import CoreSys
|
from supervisor.coresys import CoreSys
|
||||||
from supervisor.exceptions import ResolutionError
|
from supervisor.exceptions import ResolutionError
|
||||||
from supervisor.resolution.const import (
|
from supervisor.resolution.const import (
|
||||||
@ -119,3 +120,103 @@ async def test_resolution_dismiss_unsupported(coresys: CoreSys):
|
|||||||
|
|
||||||
with pytest.raises(ResolutionError):
|
with pytest.raises(ResolutionError):
|
||||||
coresys.resolution.dismiss_unsupported(UnsupportedReason.SOFTWARE)
|
coresys.resolution.dismiss_unsupported(UnsupportedReason.SOFTWARE)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_suggestions_for_issue(coresys: CoreSys):
|
||||||
|
"""Test getting suggestions that fix an issue."""
|
||||||
|
coresys.resolution.issues = corrupt_repo = Issue(
|
||||||
|
IssueType.CORRUPT_REPOSITORY, ContextType.STORE, "test_repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unrelated suggestions don't appear
|
||||||
|
coresys.resolution.suggestions = Suggestion(
|
||||||
|
SuggestionType.EXECUTE_RESET, ContextType.SUPERVISOR
|
||||||
|
)
|
||||||
|
coresys.resolution.suggestions = Suggestion(
|
||||||
|
SuggestionType.EXECUTE_REMOVE, ContextType.STORE, "other_repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert coresys.resolution.suggestions_for_issue(corrupt_repo) == set()
|
||||||
|
|
||||||
|
# Related suggestions do
|
||||||
|
coresys.resolution.suggestions = execute_remove = Suggestion(
|
||||||
|
SuggestionType.EXECUTE_REMOVE, ContextType.STORE, "test_repo"
|
||||||
|
)
|
||||||
|
coresys.resolution.suggestions = execute_reset = Suggestion(
|
||||||
|
SuggestionType.EXECUTE_RESET, ContextType.STORE, "test_repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert coresys.resolution.suggestions_for_issue(corrupt_repo) == {
|
||||||
|
execute_reset,
|
||||||
|
execute_remove,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_issues_for_suggestion(coresys: CoreSys):
|
||||||
|
"""Test getting issues fixed by a suggestion."""
|
||||||
|
coresys.resolution.suggestions = execute_reset = Suggestion(
|
||||||
|
SuggestionType.EXECUTE_RESET, ContextType.STORE, "test_repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unrelated issues don't appear
|
||||||
|
coresys.resolution.issues = Issue(IssueType.FATAL_ERROR, ContextType.CORE)
|
||||||
|
coresys.resolution.issues = Issue(
|
||||||
|
IssueType.CORRUPT_REPOSITORY, ContextType.STORE, "other_repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert coresys.resolution.issues_for_suggestion(execute_reset) == set()
|
||||||
|
|
||||||
|
# Related issues do
|
||||||
|
coresys.resolution.issues = fatal_error = Issue(
|
||||||
|
IssueType.FATAL_ERROR, ContextType.STORE, "test_repo"
|
||||||
|
)
|
||||||
|
coresys.resolution.issues = corrupt_repo = Issue(
|
||||||
|
IssueType.CORRUPT_REPOSITORY, ContextType.STORE, "test_repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert coresys.resolution.issues_for_suggestion(execute_reset) == {
|
||||||
|
fatal_error,
|
||||||
|
corrupt_repo,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_events_on_issue_changes(coresys: CoreSys):
|
||||||
|
"""Test events fired when an issue changes."""
|
||||||
|
coresys.bus.register_event(BusEvent.ISSUE_CHANGED, change_handler := AsyncMock())
|
||||||
|
coresys.bus.register_event(BusEvent.ISSUE_REMOVED, remove_handler := AsyncMock())
|
||||||
|
|
||||||
|
# Creating an issue with a suggestion should fire exactly one event
|
||||||
|
assert coresys.resolution.issues == []
|
||||||
|
assert coresys.resolution.suggestions == []
|
||||||
|
coresys.resolution.create_issue(
|
||||||
|
IssueType.CORRUPT_REPOSITORY,
|
||||||
|
ContextType.STORE,
|
||||||
|
"test_repo",
|
||||||
|
[SuggestionType.EXECUTE_RESET],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(coresys.resolution.issues) == 1
|
||||||
|
assert len(coresys.resolution.suggestions) == 1
|
||||||
|
issue = coresys.resolution.issues[0]
|
||||||
|
suggestion = coresys.resolution.suggestions[0]
|
||||||
|
change_handler.assert_called_once_with(issue)
|
||||||
|
|
||||||
|
# Adding and removing a suggestion that fixes the issue should fire another
|
||||||
|
change_handler.reset_mock()
|
||||||
|
coresys.resolution.suggestions = execute_remove = Suggestion(
|
||||||
|
SuggestionType.EXECUTE_REMOVE, ContextType.STORE, "test_repo"
|
||||||
|
)
|
||||||
|
change_handler.assert_called_once_with(issue)
|
||||||
|
|
||||||
|
change_handler.reset_mock()
|
||||||
|
coresys.resolution.dismiss_suggestion(execute_remove)
|
||||||
|
change_handler.assert_called_once_with(issue)
|
||||||
|
remove_handler.assert_not_called()
|
||||||
|
|
||||||
|
# Applying a suggestion should only fire the issue removed event
|
||||||
|
change_handler.reset_mock()
|
||||||
|
with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0**3))):
|
||||||
|
await coresys.resolution.apply_suggestion(suggestion)
|
||||||
|
|
||||||
|
change_handler.assert_not_called()
|
||||||
|
remove_handler.assert_called_once_with(issue)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user