mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-08-13 11:09:21 +00:00
Compare commits
2 Commits
restart-ad
...
handle-git
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1f7aafcfd7 | ||
![]() |
ed45651fd9 |
30
.github/workflows/ci.yaml
vendored
30
.github/workflows/ci.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
pip install -r requirements.txt -r requirements_tests.txt
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
lookup-only: true
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: |
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: |
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: |
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -233,7 +233,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: |
|
||||
@@ -265,7 +265,7 @@ jobs:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -307,7 +307,7 @@ jobs:
|
||||
echo "key=mypy-${{ env.MYPY_CACHE_VERSION }}-$mypy_version-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -318,7 +318,7 @@ jobs:
|
||||
echo "Failed to restore Python virtual environment from cache"
|
||||
exit 1
|
||||
- name: Restore mypy cache
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: >-
|
||||
@@ -351,7 +351,7 @@ jobs:
|
||||
cosign-release: "v2.4.3"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -406,7 +406,7 @@ jobs:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.4
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
|
@@ -8,7 +8,7 @@ pytest-asyncio==0.25.2
|
||||
pytest-cov==6.2.1
|
||||
pytest-timeout==2.4.0
|
||||
pytest==8.4.1
|
||||
ruff==0.12.8
|
||||
ruff==0.12.7
|
||||
time-machine==2.17.0
|
||||
types-docker==7.1.0.20250705
|
||||
types-pyyaml==6.0.12.20250516
|
||||
|
@@ -11,13 +11,12 @@ import shutil
|
||||
from awesomeversion import AwesomeVersion
|
||||
import jinja2
|
||||
|
||||
from ..const import AddonState, LogLevel
|
||||
from ..const import LogLevel
|
||||
from ..coresys import CoreSys
|
||||
from ..docker.audio import DockerAudio
|
||||
from ..docker.const import ContainerState
|
||||
from ..docker.stats import DockerStats
|
||||
from ..exceptions import (
|
||||
AddonsError,
|
||||
AudioError,
|
||||
AudioJobError,
|
||||
AudioUpdateError,
|
||||
@@ -132,9 +131,6 @@ class PluginAudio(PluginBase):
|
||||
except (DockerError, PluginError) as err:
|
||||
raise AudioUpdateError("Audio update failed", _LOGGER.error) from err
|
||||
|
||||
# Restart add-ons with audio support after plugin update
|
||||
await self._restart_audio_addons()
|
||||
|
||||
async def restart(self) -> None:
|
||||
"""Restart Audio plugin."""
|
||||
_LOGGER.info("Restarting Audio plugin")
|
||||
@@ -144,9 +140,6 @@ class PluginAudio(PluginBase):
|
||||
except DockerError as err:
|
||||
raise AudioError("Can't start Audio plugin", _LOGGER.error) from err
|
||||
|
||||
# Restart add-ons with audio support after plugin restart
|
||||
await self._restart_audio_addons()
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Run Audio plugin."""
|
||||
_LOGGER.info("Starting Audio plugin")
|
||||
@@ -210,35 +203,6 @@ class PluginAudio(PluginBase):
|
||||
f"Can't update pulse audio config: {err}", _LOGGER.error
|
||||
) from err
|
||||
|
||||
async def _restart_audio_addons(self) -> None:
|
||||
"""Restart all installed add-ons that have audio support."""
|
||||
audio_addons = [
|
||||
addon
|
||||
for addon in self.sys_addons.installed
|
||||
if addon.with_audio and addon.state == AddonState.STARTED
|
||||
]
|
||||
|
||||
if not audio_addons:
|
||||
_LOGGER.debug("No running audio add-ons to restart")
|
||||
return
|
||||
|
||||
_LOGGER.info(
|
||||
"Restarting %d audio add-ons after audio plugin restart: %s",
|
||||
len(audio_addons),
|
||||
[addon.slug for addon in audio_addons],
|
||||
)
|
||||
|
||||
for addon in audio_addons:
|
||||
try:
|
||||
_LOGGER.info("Restarting audio add-on: %s", addon.slug)
|
||||
await addon.restart()
|
||||
except AddonsError as err:
|
||||
_LOGGER.warning(
|
||||
"Failed to restart audio add-on %s after audio plugin restart: %s",
|
||||
addon.slug,
|
||||
err,
|
||||
)
|
||||
|
||||
@Job(
|
||||
name="plugin_audio_restart_after_problem",
|
||||
throttle_period=WATCHDOG_THROTTLE_PERIOD,
|
||||
|
@@ -1,9 +1,11 @@
|
||||
"""Init file for Supervisor add-on Git."""
|
||||
|
||||
import asyncio
|
||||
from enum import StrEnum
|
||||
import errno
|
||||
import functools as ft
|
||||
import logging
|
||||
from os import listdir
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
@@ -20,6 +22,32 @@ from .validate import RE_REPOSITORY
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _RepoDirectoryStatus(StrEnum):
|
||||
"""Basic directory status for repo."""
|
||||
|
||||
GOOD = "good"
|
||||
EMPTY = "empty"
|
||||
MISSING_GIT = "missing_git"
|
||||
|
||||
|
||||
def _check_repo_directory(path: Path) -> _RepoDirectoryStatus:
|
||||
"""Check repository directory.
|
||||
|
||||
Must be run in executor.
|
||||
"""
|
||||
if not path.is_dir():
|
||||
return _RepoDirectoryStatus.EMPTY
|
||||
|
||||
if (path / ".git").is_dir():
|
||||
return _RepoDirectoryStatus.GOOD
|
||||
|
||||
return (
|
||||
_RepoDirectoryStatus.MISSING_GIT
|
||||
if listdir(path)
|
||||
else _RepoDirectoryStatus.EMPTY
|
||||
)
|
||||
|
||||
|
||||
class GitRepo(CoreSysAttributes):
|
||||
"""Manage Add-on Git repository."""
|
||||
|
||||
@@ -50,9 +78,14 @@ class GitRepo(CoreSysAttributes):
|
||||
|
||||
async def load(self) -> None:
|
||||
"""Init Git add-on repository."""
|
||||
if not await self.sys_run_in_executor((self.path / ".git").is_dir):
|
||||
await self.clone()
|
||||
return
|
||||
match await self.sys_run_in_executor(_check_repo_directory, self.path):
|
||||
# Nothing cached. Set up as fresh clone
|
||||
case _RepoDirectoryStatus.EMPTY:
|
||||
await self.clone()
|
||||
return
|
||||
# Repository is corrupt. Try to reset it before loading
|
||||
case _RepoDirectoryStatus.MISSING_GIT:
|
||||
await self.reset()
|
||||
|
||||
# Load repository
|
||||
async with self.lock:
|
||||
@@ -175,6 +208,22 @@ class GitRepo(CoreSysAttributes):
|
||||
async with self.lock:
|
||||
_LOGGER.info("Update add-on %s repository from %s", self.path, self.url)
|
||||
|
||||
# .git is missing, repository is corrupted. Can't continue, raise issue
|
||||
if (
|
||||
await self.sys_run_in_executor(_check_repo_directory, self.path)
|
||||
!= _RepoDirectoryStatus.GOOD
|
||||
):
|
||||
self.sys_resolution.create_issue(
|
||||
IssueType.CORRUPT_REPOSITORY,
|
||||
ContextType.STORE,
|
||||
reference=self.path.stem,
|
||||
suggestions=[SuggestionType.EXECUTE_RESET],
|
||||
)
|
||||
raise StoreGitError(
|
||||
f"Can't update {self.url} repo because git information is missing",
|
||||
_LOGGER.error,
|
||||
)
|
||||
|
||||
try:
|
||||
git_cmd = git.Git()
|
||||
await self.sys_run_in_executor(git_cmd.ls_remote, "--heads", self.url)
|
||||
|
@@ -337,10 +337,32 @@ async def fixture_all_dbus_services(
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def addon_repo_fixtures() -> dict[str, Path]:
|
||||
"""Make addon repo fixtures into valid repositories and return."""
|
||||
addon_repo_fixtures = Path(__file__).parent.joinpath("fixtures") / "addons"
|
||||
core_repo_fixture = addon_repo_fixtures / "core"
|
||||
local_repo_fixture = addon_repo_fixtures / "local"
|
||||
git_repo_fixtures = addon_repo_fixtures / "git"
|
||||
|
||||
# Ensure each repo folder has a dummy .git
|
||||
(core_repo_fixture / ".git").mkdir(exist_ok=True)
|
||||
for f in os.listdir(git_repo_fixtures):
|
||||
if (repo := git_repo_fixtures / f).is_dir():
|
||||
(repo / ".git").mkdir(exist_ok=True)
|
||||
|
||||
return {
|
||||
"core": core_repo_fixture,
|
||||
"local": local_repo_fixture,
|
||||
"git": git_repo_fixtures,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def coresys(
|
||||
docker,
|
||||
dbus_session_bus,
|
||||
addon_repo_fixtures: dict[str, Path],
|
||||
all_dbus_services,
|
||||
aiohttp_client,
|
||||
run_supervisor_state,
|
||||
@@ -393,15 +415,9 @@ async def coresys(
|
||||
coresys_obj.host.network._connectivity = True
|
||||
|
||||
# Fix Paths
|
||||
su_config.ADDONS_CORE = Path(
|
||||
Path(__file__).parent.joinpath("fixtures"), "addons/core"
|
||||
)
|
||||
su_config.ADDONS_LOCAL = Path(
|
||||
Path(__file__).parent.joinpath("fixtures"), "addons/local"
|
||||
)
|
||||
su_config.ADDONS_GIT = Path(
|
||||
Path(__file__).parent.joinpath("fixtures"), "addons/git"
|
||||
)
|
||||
su_config.ADDONS_CORE = addon_repo_fixtures["core"]
|
||||
su_config.ADDONS_LOCAL = addon_repo_fixtures["local"]
|
||||
su_config.ADDONS_GIT = addon_repo_fixtures["git"]
|
||||
su_config.APPARMOR_DATA = Path(
|
||||
Path(__file__).parent.joinpath("fixtures"), "apparmor"
|
||||
)
|
||||
|
@@ -2,15 +2,13 @@
|
||||
|
||||
import errno
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
import pytest
|
||||
|
||||
from supervisor.const import AddonState, LogLevel
|
||||
from supervisor.const import LogLevel
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.docker.audio import DockerAudio
|
||||
from supervisor.exceptions import AddonsError, JobException
|
||||
|
||||
|
||||
@pytest.fixture(name="docker_interface")
|
||||
@@ -87,189 +85,3 @@ async def test_load_error(
|
||||
assert "Can't read pulse-client.tmpl" in caplog.text
|
||||
assert "Can't create default asound" in caplog.text
|
||||
assert coresys.core.healthy is False
|
||||
|
||||
|
||||
async def test_restart_audio_addons_no_audio_addons(
|
||||
coresys: CoreSys,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
"""Test _restart_audio_addons with no audio add-ons installed."""
|
||||
# Mock empty installed add-ons list
|
||||
with patch.object(
|
||||
type(coresys.addons), "installed", new_callable=PropertyMock
|
||||
) as mock_installed:
|
||||
mock_installed.return_value = []
|
||||
# Should complete without errors and not attempt to restart any add-ons
|
||||
await coresys.plugins.audio._restart_audio_addons()
|
||||
|
||||
# Verify the method returned successfully (no exceptions raised)
|
||||
|
||||
|
||||
async def test_restart_audio_addons_with_audio_addons(
|
||||
coresys: CoreSys,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
"""Test _restart_audio_addons with audio add-ons installed."""
|
||||
# Create mock audio add-ons
|
||||
audio_addon_1 = Mock()
|
||||
audio_addon_1.with_audio = True
|
||||
audio_addon_1.state = AddonState.STARTED
|
||||
audio_addon_1.slug = "audio_addon_1"
|
||||
audio_addon_1.restart = AsyncMock()
|
||||
|
||||
audio_addon_2 = Mock()
|
||||
audio_addon_2.with_audio = True
|
||||
audio_addon_2.state = AddonState.STARTED
|
||||
audio_addon_2.slug = "audio_addon_2"
|
||||
audio_addon_2.restart = AsyncMock()
|
||||
|
||||
# Create non-audio add-on that should be ignored
|
||||
non_audio_addon = Mock()
|
||||
non_audio_addon.with_audio = False
|
||||
non_audio_addon.state = AddonState.STARTED
|
||||
non_audio_addon.slug = "non_audio_addon"
|
||||
non_audio_addon.restart = AsyncMock()
|
||||
|
||||
# Create stopped audio add-on that should be ignored
|
||||
stopped_audio_addon = Mock()
|
||||
stopped_audio_addon.with_audio = True
|
||||
stopped_audio_addon.state = AddonState.STOPPED
|
||||
stopped_audio_addon.slug = "stopped_audio_addon"
|
||||
stopped_audio_addon.restart = AsyncMock()
|
||||
|
||||
mock_addons = [audio_addon_1, audio_addon_2, non_audio_addon, stopped_audio_addon]
|
||||
|
||||
with patch.object(
|
||||
type(coresys.addons), "installed", new_callable=PropertyMock
|
||||
) as mock_installed:
|
||||
mock_installed.return_value = mock_addons
|
||||
await coresys.plugins.audio._restart_audio_addons()
|
||||
|
||||
# Verify only audio add-ons in STARTED state were restarted
|
||||
audio_addon_1.restart.assert_called_once()
|
||||
audio_addon_2.restart.assert_called_once()
|
||||
non_audio_addon.restart.assert_not_called()
|
||||
stopped_audio_addon.restart.assert_not_called()
|
||||
|
||||
assert "Restarting 2 audio add-ons after audio plugin restart" in caplog.text
|
||||
assert "audio_addon_1" in caplog.text
|
||||
assert "audio_addon_2" in caplog.text
|
||||
assert "Restarting audio add-on: audio_addon_1" in caplog.text
|
||||
assert "Restarting audio add-on: audio_addon_2" in caplog.text
|
||||
|
||||
|
||||
async def test_restart_audio_addons_with_error_handling(
|
||||
coresys: CoreSys,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
"""Test _restart_audio_addons handles add-on restart errors gracefully."""
|
||||
# Create mock audio add-ons - one succeeds, one fails
|
||||
successful_addon = Mock()
|
||||
successful_addon.with_audio = True
|
||||
successful_addon.state = AddonState.STARTED
|
||||
successful_addon.slug = "successful_addon"
|
||||
successful_addon.restart = AsyncMock()
|
||||
|
||||
failing_addon = Mock()
|
||||
failing_addon.with_audio = True
|
||||
failing_addon.state = AddonState.STARTED
|
||||
failing_addon.slug = "failing_addon"
|
||||
failing_addon.restart = AsyncMock(side_effect=AddonsError("Test error"))
|
||||
|
||||
mock_addons = [successful_addon, failing_addon]
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
type(coresys.addons), "installed", new_callable=PropertyMock
|
||||
) as mock_installed,
|
||||
):
|
||||
mock_installed.return_value = mock_addons
|
||||
await coresys.plugins.audio._restart_audio_addons()
|
||||
|
||||
# Verify both add-ons were attempted to be restarted
|
||||
successful_addon.restart.assert_called_once()
|
||||
failing_addon.restart.assert_called_once()
|
||||
|
||||
# Verify error was captured and logged
|
||||
assert "Failed to restart audio add-on failing_addon" in caplog.text
|
||||
assert "Restarting audio add-on: successful_addon" in caplog.text
|
||||
assert "Restarting audio add-on: failing_addon" in caplog.text
|
||||
|
||||
|
||||
async def test_restart_calls_restart_audio_addons(
|
||||
coresys: CoreSys,
|
||||
docker_interface: tuple[AsyncMock, AsyncMock],
|
||||
write_json: Mock,
|
||||
):
|
||||
"""Test that restart() method calls _restart_audio_addons."""
|
||||
with patch.object(
|
||||
coresys.plugins.audio, "_restart_audio_addons"
|
||||
) as mock_restart_addons:
|
||||
await coresys.plugins.audio.restart()
|
||||
|
||||
# Verify docker restart was called
|
||||
docker_interface[1].assert_called_once()
|
||||
|
||||
# Verify _restart_audio_addons was called after plugin restart
|
||||
mock_restart_addons.assert_called_once()
|
||||
|
||||
|
||||
async def test_update_calls_restart_audio_addons(
|
||||
coresys: CoreSys,
|
||||
):
|
||||
"""Test that update() method calls _restart_audio_addons after successful update."""
|
||||
with (
|
||||
patch.object(
|
||||
type(coresys.plugins.audio), "latest_version", new_callable=PropertyMock
|
||||
) as mock_latest_version,
|
||||
patch.object(
|
||||
type(coresys.plugins.audio), "version", new_callable=PropertyMock
|
||||
) as mock_version,
|
||||
patch("supervisor.plugins.base.PluginBase.update") as mock_super_update,
|
||||
patch.object(
|
||||
coresys.plugins.audio, "_restart_audio_addons"
|
||||
) as mock_restart_addons,
|
||||
patch("supervisor.jobs.decorator.Job.check_conditions", return_value=None),
|
||||
):
|
||||
mock_latest_version.return_value = AwesomeVersion("1.2.3")
|
||||
mock_version.return_value = AwesomeVersion("1.2.2")
|
||||
mock_super_update.return_value = None
|
||||
|
||||
await coresys.plugins.audio.update()
|
||||
|
||||
# Verify super().update() was called
|
||||
mock_super_update.assert_called_once()
|
||||
|
||||
# Verify _restart_audio_addons was called after successful update
|
||||
mock_restart_addons.assert_called_once()
|
||||
|
||||
|
||||
async def test_update_does_not_restart_addons_on_failure(
|
||||
coresys: CoreSys,
|
||||
):
|
||||
"""Test that update() method does not call _restart_audio_addons when update fails."""
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
type(coresys.plugins.audio), "latest_version", new_callable=PropertyMock
|
||||
) as mock_latest_version,
|
||||
patch.object(
|
||||
type(coresys.plugins.audio), "version", new_callable=PropertyMock
|
||||
) as mock_version,
|
||||
patch(
|
||||
"supervisor.plugins.base.PluginBase.update",
|
||||
side_effect=Exception("Update failed"),
|
||||
),
|
||||
patch.object(
|
||||
coresys.plugins.audio, "_restart_audio_addons"
|
||||
) as mock_restart_addons,
|
||||
patch("supervisor.jobs.decorator.Job.check_conditions", return_value=None),
|
||||
pytest.raises(JobException), # Job decorator wraps exceptions in JobException
|
||||
):
|
||||
mock_latest_version.return_value = AwesomeVersion("1.2.3")
|
||||
mock_version.return_value = AwesomeVersion("1.2.2")
|
||||
|
||||
await coresys.plugins.audio.update()
|
||||
|
||||
# Verify _restart_audio_addons was NOT called due to update failure
|
||||
mock_restart_addons.assert_not_called()
|
||||
|
@@ -3,13 +3,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from git import GitCommandError, InvalidGitRepositoryError, NoSuchPathError
|
||||
import pytest
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.exceptions import StoreGitCloneError, StoreGitError
|
||||
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
||||
from supervisor.resolution.data import Issue, Suggestion
|
||||
from supervisor.store.git import GitRepo
|
||||
|
||||
REPO_URL = "https://github.com/awesome-developer/awesome-repo"
|
||||
@@ -93,6 +95,70 @@ async def test_git_load(coresys: CoreSys, tmp_path: Path):
|
||||
assert mock_repo.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("tmp_supervisor_data", "supervisor_internet")
|
||||
async def test_git_load_corrupt(coresys: CoreSys, tmp_path: Path):
|
||||
"""Test git load with corrupt repo."""
|
||||
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||
repo_dir = tmp_path / "repo"
|
||||
repo = GitRepo(coresys, repo_dir, REPO_URL)
|
||||
|
||||
# Pretend we have a something but not .git to force a reset
|
||||
repo_dir.mkdir()
|
||||
marker = repo_dir / "test.txt"
|
||||
marker.touch()
|
||||
|
||||
def mock_clone_from(url, path, *args, **kwargs):
|
||||
"""Mock to just make a .git and return."""
|
||||
Path(path, ".git").mkdir()
|
||||
return MagicMock()
|
||||
|
||||
with patch("git.Repo") as mock_repo:
|
||||
mock_repo.clone_from = mock_clone_from
|
||||
await repo.load()
|
||||
assert mock_repo.call_count == 1
|
||||
assert not marker.exists()
|
||||
assert (repo_dir / ".git").is_dir()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("tmp_supervisor_data", "supervisor_internet")
|
||||
async def test_git_pull_correct(coresys: CoreSys, tmp_path: Path):
|
||||
"""Test git pull with corrupt repo."""
|
||||
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||
repo_dir = tmp_path / "repo"
|
||||
repo = GitRepo(coresys, repo_dir, REPO_URL)
|
||||
|
||||
# Set up a our fake repo
|
||||
repo_dir.mkdir()
|
||||
git_dir = repo_dir / ".git"
|
||||
git_dir.mkdir()
|
||||
(repo_dir / "test.txt").touch()
|
||||
|
||||
with patch("git.Repo"):
|
||||
await repo.load()
|
||||
|
||||
# Make it corrupt
|
||||
git_dir.rmdir()
|
||||
|
||||
# Check that we get an issue on pull
|
||||
with pytest.raises(
|
||||
StoreGitError,
|
||||
match=f"Can't update {REPO_URL} repo because git information is missing",
|
||||
):
|
||||
await repo.pull()
|
||||
assert (
|
||||
Issue(
|
||||
IssueType.CORRUPT_REPOSITORY, ContextType.STORE, reference=repo_dir.stem
|
||||
)
|
||||
in coresys.resolution.issues
|
||||
)
|
||||
assert (
|
||||
Suggestion(
|
||||
SuggestionType.EXECUTE_RESET, ContextType.STORE, reference=repo_dir.stem
|
||||
)
|
||||
in coresys.resolution.suggestions
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"git_errors",
|
||||
[
|
||||
|
Reference in New Issue
Block a user