Compare commits

..

2 Commits

Author SHA1 Message Date
Mike Degatano
1f7aafcfd7 Fix fixture that ensures .git presence 2025-08-07 20:51:02 +00:00
Mike Degatano
ed45651fd9 Handle git dir missing in load and pull 2025-08-07 20:42:22 +00:00
7 changed files with 163 additions and 256 deletions

View File

@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | key: |
@@ -48,7 +48,7 @@ jobs:
pip install -r requirements.txt -r requirements_tests.txt pip install -r requirements.txt -r requirements_tests.txt
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true lookup-only: true
@@ -76,7 +76,7 @@ jobs:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ needs.prepare.outputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | key: |
@@ -88,7 +88,7 @@ jobs:
exit 1 exit 1
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: |
@@ -119,7 +119,7 @@ jobs:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ needs.prepare.outputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | key: |
@@ -131,7 +131,7 @@ jobs:
exit 1 exit 1
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: |
@@ -177,7 +177,7 @@ jobs:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ needs.prepare.outputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | key: |
@@ -189,7 +189,7 @@ jobs:
exit 1 exit 1
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: |
@@ -221,7 +221,7 @@ jobs:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ needs.prepare.outputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | key: |
@@ -233,7 +233,7 @@ jobs:
exit 1 exit 1
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: |
@@ -265,7 +265,7 @@ jobs:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ needs.prepare.outputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | 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 echo "key=mypy-${{ env.MYPY_CACHE_VERSION }}-$mypy_version-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: >- key: >-
@@ -318,7 +318,7 @@ jobs:
echo "Failed to restore Python virtual environment from cache" echo "Failed to restore Python virtual environment from cache"
exit 1 exit 1
- name: Restore mypy cache - name: Restore mypy cache
uses: actions/cache@v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: .mypy_cache path: .mypy_cache
key: >- key: >-
@@ -351,7 +351,7 @@ jobs:
cosign-release: "v2.4.3" cosign-release: "v2.4.3"
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | key: |
@@ -406,7 +406,7 @@ jobs:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ needs.prepare.outputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.2.4 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | key: |

View File

@@ -8,7 +8,7 @@ pytest-asyncio==0.25.2
pytest-cov==6.2.1 pytest-cov==6.2.1
pytest-timeout==2.4.0 pytest-timeout==2.4.0
pytest==8.4.1 pytest==8.4.1
ruff==0.12.8 ruff==0.12.7
time-machine==2.17.0 time-machine==2.17.0
types-docker==7.1.0.20250705 types-docker==7.1.0.20250705
types-pyyaml==6.0.12.20250516 types-pyyaml==6.0.12.20250516

View File

@@ -11,13 +11,12 @@ import shutil
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
import jinja2 import jinja2
from ..const import AddonState, LogLevel from ..const import LogLevel
from ..coresys import CoreSys from ..coresys import CoreSys
from ..docker.audio import DockerAudio from ..docker.audio import DockerAudio
from ..docker.const import ContainerState from ..docker.const import ContainerState
from ..docker.stats import DockerStats from ..docker.stats import DockerStats
from ..exceptions import ( from ..exceptions import (
AddonsError,
AudioError, AudioError,
AudioJobError, AudioJobError,
AudioUpdateError, AudioUpdateError,
@@ -132,9 +131,6 @@ class PluginAudio(PluginBase):
except (DockerError, PluginError) as err: except (DockerError, PluginError) as err:
raise AudioUpdateError("Audio update failed", _LOGGER.error) from 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: async def restart(self) -> None:
"""Restart Audio plugin.""" """Restart Audio plugin."""
_LOGGER.info("Restarting Audio plugin") _LOGGER.info("Restarting Audio plugin")
@@ -144,9 +140,6 @@ class PluginAudio(PluginBase):
except DockerError as err: except DockerError as err:
raise AudioError("Can't start Audio plugin", _LOGGER.error) from 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: async def start(self) -> None:
"""Run Audio plugin.""" """Run Audio plugin."""
_LOGGER.info("Starting Audio plugin") _LOGGER.info("Starting Audio plugin")
@@ -210,35 +203,6 @@ class PluginAudio(PluginBase):
f"Can't update pulse audio config: {err}", _LOGGER.error f"Can't update pulse audio config: {err}", _LOGGER.error
) from err ) 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( @Job(
name="plugin_audio_restart_after_problem", name="plugin_audio_restart_after_problem",
throttle_period=WATCHDOG_THROTTLE_PERIOD, throttle_period=WATCHDOG_THROTTLE_PERIOD,

View File

@@ -1,9 +1,11 @@
"""Init file for Supervisor add-on Git.""" """Init file for Supervisor add-on Git."""
import asyncio import asyncio
from enum import StrEnum
import errno import errno
import functools as ft import functools as ft
import logging import logging
from os import listdir
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
@@ -20,6 +22,32 @@ from .validate import RE_REPOSITORY
_LOGGER: logging.Logger = logging.getLogger(__name__) _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): class GitRepo(CoreSysAttributes):
"""Manage Add-on Git repository.""" """Manage Add-on Git repository."""
@@ -50,9 +78,14 @@ class GitRepo(CoreSysAttributes):
async def load(self) -> None: async def load(self) -> None:
"""Init Git add-on repository.""" """Init Git add-on repository."""
if not await self.sys_run_in_executor((self.path / ".git").is_dir): match await self.sys_run_in_executor(_check_repo_directory, self.path):
await self.clone() # Nothing cached. Set up as fresh clone
return 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 # Load repository
async with self.lock: async with self.lock:
@@ -175,6 +208,22 @@ class GitRepo(CoreSysAttributes):
async with self.lock: async with self.lock:
_LOGGER.info("Update add-on %s repository from %s", self.path, self.url) _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: try:
git_cmd = git.Git() git_cmd = git.Git()
await self.sys_run_in_executor(git_cmd.ls_remote, "--heads", self.url) await self.sys_run_in_executor(git_cmd.ls_remote, "--heads", self.url)

View File

@@ -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 @pytest.fixture
async def coresys( async def coresys(
docker, docker,
dbus_session_bus, dbus_session_bus,
addon_repo_fixtures: dict[str, Path],
all_dbus_services, all_dbus_services,
aiohttp_client, aiohttp_client,
run_supervisor_state, run_supervisor_state,
@@ -393,15 +415,9 @@ async def coresys(
coresys_obj.host.network._connectivity = True coresys_obj.host.network._connectivity = True
# Fix Paths # Fix Paths
su_config.ADDONS_CORE = Path( su_config.ADDONS_CORE = addon_repo_fixtures["core"]
Path(__file__).parent.joinpath("fixtures"), "addons/core" su_config.ADDONS_LOCAL = addon_repo_fixtures["local"]
) su_config.ADDONS_GIT = addon_repo_fixtures["git"]
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.APPARMOR_DATA = Path( su_config.APPARMOR_DATA = Path(
Path(__file__).parent.joinpath("fixtures"), "apparmor" Path(__file__).parent.joinpath("fixtures"), "apparmor"
) )

View File

@@ -2,15 +2,13 @@
import errno import errno
from pathlib import Path 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 import pytest
from supervisor.const import AddonState, LogLevel from supervisor.const import LogLevel
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.docker.audio import DockerAudio from supervisor.docker.audio import DockerAudio
from supervisor.exceptions import AddonsError, JobException
@pytest.fixture(name="docker_interface") @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 read pulse-client.tmpl" in caplog.text
assert "Can't create default asound" in caplog.text assert "Can't create default asound" in caplog.text
assert coresys.core.healthy is False 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()

View File

@@ -3,13 +3,15 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from git import GitCommandError, InvalidGitRepositoryError, NoSuchPathError from git import GitCommandError, InvalidGitRepositoryError, NoSuchPathError
import pytest import pytest
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.exceptions import StoreGitCloneError, StoreGitError 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 from supervisor.store.git import GitRepo
REPO_URL = "https://github.com/awesome-developer/awesome-repo" 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 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( @pytest.mark.parametrize(
"git_errors", "git_errors",
[ [