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
3 changed files with 144 additions and 13 deletions

View File

@@ -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)

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
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"
)

View File

@@ -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",
[