Files
10Backward/tests/test_blog.py
T

215 lines
5.9 KiB
Python

from __future__ import annotations
import logging
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from git import GitCommandError, InvalidGitRepositoryError, Repo
from tenbackward.blog import (
BACKOFF_BASE_SECONDS,
BlogRepoError,
BlogTransientError,
DEFAULT_BLOG_SUBDIR,
DEFAULT_REPO_URL,
blog_dir,
blog_repo_url,
ensure_repo,
)
def _git_err(stderr: str = "boom") -> GitCommandError:
return GitCommandError(["git"], stderr=stderr, status=1)
def test_blog_dir_default_subdir(tmp_path: Path) -> None:
assert blog_dir(tmp_path) == tmp_path / DEFAULT_BLOG_SUBDIR
def test_blog_repo_url_default(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("BLOG_REPO_URL", raising=False)
assert blog_repo_url() == DEFAULT_REPO_URL
def test_blog_repo_url_env_override(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("BLOG_REPO_URL", "https://example.com/repo.git")
assert blog_repo_url() == "https://example.com/repo.git"
def test_ensure_repo_clones_when_missing(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
target = tmp_path / "blog"
fake_repo = MagicMock(spec=Repo)
calls: list[tuple[str, str]] = []
def fake_clone(url: str, path: str) -> Repo:
calls.append((url, path))
target.mkdir(parents=True)
(target / ".git").mkdir()
return fake_repo
sleeps: list[float] = []
result = ensure_repo(
tmp_path,
repo_url="https://example.com/repo.git",
blog_path=target,
max_retries=2,
sleep=sleeps.append,
clone_impl=fake_clone,
)
assert result is fake_repo
assert calls == [("https://example.com/repo.git", str(target))]
assert sleeps == []
def test_ensure_repo_pulls_when_clone_exists(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
target = tmp_path / "blog"
target.mkdir()
(target / ".git").mkdir()
fake_repo = MagicMock(spec=Repo)
monkeypatch.setattr("tenbackward.blog.Repo", MagicMock(return_value=fake_repo))
fetch_impl = MagicMock()
pull_impl = MagicMock()
sleeps: list[float] = []
result = ensure_repo(
tmp_path,
repo_url="https://example.com/repo.git",
blog_path=target,
max_retries=3,
sleep=sleeps.append,
fetch_impl=lambda r: fetch_impl,
pull_impl=lambda r: pull_impl,
)
assert result is fake_repo
fetch_impl.assert_called_once_with()
pull_impl.assert_called_once_with()
assert sleeps == []
def test_ensure_repo_aborts_on_local_modifications(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
target = tmp_path / "blog"
target.mkdir()
(target / ".git").mkdir()
fake_repo = MagicMock(spec=Repo)
monkeypatch.setattr("tenbackward.blog.Repo", MagicMock(return_value=fake_repo))
fetch_impl = MagicMock()
pull_impl = MagicMock(
side_effect=_git_err("Your local changes to 'foo' would be overwritten by merge")
)
sleeps: list[float] = []
with caplog.at_level(logging.ERROR):
with pytest.raises(BlogRepoError, match="local modifications"):
ensure_repo(
tmp_path,
repo_url="https://example.com/repo.git",
blog_path=target,
max_retries=5,
sleep=sleeps.append,
fetch_impl=lambda r: fetch_impl,
pull_impl=lambda r: pull_impl,
)
assert fetch_impl.call_count == 1
assert pull_impl.call_count == 1
assert sleeps == []
def test_ensure_repo_retries_transient_error_then_succeeds(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
target = tmp_path / "blog"
target.mkdir()
(target / ".git").mkdir()
fake_repo = MagicMock(spec=Repo)
monkeypatch.setattr("tenbackward.blog.Repo", MagicMock(return_value=fake_repo))
fetch_impl = MagicMock(side_effect=[_git_err("Could not resolve host"), None])
pull_impl = MagicMock()
sleeps: list[float] = []
ensure_repo(
tmp_path,
repo_url="https://example.com/repo.git",
blog_path=target,
max_retries=5,
sleep=sleeps.append,
fetch_impl=lambda r: fetch_impl,
pull_impl=lambda r: pull_impl,
)
assert fetch_impl.call_count == 2
pull_impl.assert_called_once_with()
assert sleeps == [BACKOFF_BASE_SECONDS ** 0]
def test_ensure_repo_raises_after_exhausted_retries(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
target = tmp_path / "blog"
target.mkdir()
(target / ".git").mkdir()
fake_repo = MagicMock(spec=Repo)
monkeypatch.setattr("tenbackward.blog.Repo", MagicMock(return_value=fake_repo))
fetch_impl = MagicMock(side_effect=_git_err("Could not resolve host"))
pull_impl = MagicMock()
sleeps: list[float] = []
with pytest.raises(BlogTransientError, match="git pull failed"):
ensure_repo(
tmp_path,
repo_url="https://example.com/repo.git",
blog_path=target,
max_retries=2,
sleep=sleeps.append,
fetch_impl=lambda r: fetch_impl,
pull_impl=lambda r: pull_impl,
)
assert fetch_impl.call_count == 2
pull_impl.assert_not_called()
assert sleeps == [BACKOFF_BASE_SECONDS ** 0]
def test_ensure_repo_invalid_clone_dir_raises(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
target = tmp_path / "blog"
target.mkdir()
(target / "not-a-repo.txt").write_text("hi")
def fake_repo(_path: str) -> Repo:
raise InvalidGitRepositoryError(f"{_path} is not a repo")
monkeypatch.setattr("tenbackward.blog.Repo", fake_repo)
with pytest.raises(BlogRepoError, match="not a valid git repository"):
ensure_repo(
tmp_path,
repo_url="https://example.com/repo.git",
blog_path=target,
max_retries=0,
)