ifcurl/tests/test_checkout.py
Bruno Postle caf303d3ed Add checkout.py: writable working-tree checkouts from bare cache
Implements ifcurl-5oo. get_checkout() ensures the bare cache is current via
fetch_ifc(), then clones it locally to <cache_dir>/checkout/ (no network),
and places it at the resolved commit as a detached HEAD — forcing branch
creation before any commit. Viewer artefact patterns (*.ifcview, *.rdbview,
etc.) are appended to .git/info/exclude so they are locally ignored without
touching the repo's own .gitignore. When a newer commit is requested, the
checkout fetches from origin (the local bare cache) using the default refspec
to avoid git's "refusing to fetch into checked-out branch" error. Unblocks
the JSON-RPC connector (ifcurl-242).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 23:38:13 +01:00

250 lines
9.8 KiB
Python

"""Tests for ifcurl.checkout — working-tree checkout management."""
from __future__ import annotations
import hashlib
from pathlib import Path
import pytest
from ifcurl.checkout import _ensure_checkout, _ensure_viewer_excludes, _VIEWER_EXCLUDES, get_checkout
from ifcurl.url import IfcUrl
# ---------------------------------------------------------------------------
# Fixture helpers
# ---------------------------------------------------------------------------
def _make_bare_cache(tmp_path: Path, remote_url: str, ifc_bytes: bytes = b"IFC4\n") -> dict:
"""Create a source repo + bare cache clone; return metadata dict."""
import git as gitpkg
src = tmp_path / "source"
src.mkdir()
(src / "model.ifc").write_bytes(ifc_bytes)
repo = gitpkg.Repo.init(str(src))
with repo.config_writer() as cw:
cw.set_value("user", "name", "Test")
cw.set_value("user", "email", "t@t.com")
repo.index.add(["model.ifc"])
commit = repo.index.commit("init")
url_hash = hashlib.sha256(remote_url.encode()).hexdigest()[:24]
cache_dir = tmp_path / url_hash
bare_dir = cache_dir / "repo.git"
gitpkg.Repo.clone_from(str(src), str(bare_dir), bare=True)
(cache_dir / "remote_url").write_text(remote_url)
return {
"hexsha": commit.hexsha,
"bytes": ifc_bytes,
"cache_dir": cache_dir,
"bare_dir": bare_dir,
"src": src,
"remote_url": remote_url,
}
def _ifc_url(hexsha: str, remote_url: str, path: str = "model.ifc") -> IfcUrl:
"""Build an IfcUrl for a pre-populated cache (https transport, commit ref)."""
host_path = remote_url.removeprefix("https://")
return IfcUrl.parse(f"ifc://{host_path}@{hexsha}?path={path}")
# ---------------------------------------------------------------------------
# _ensure_viewer_excludes
# ---------------------------------------------------------------------------
class TestEnsureViewerExcludes:
def test_creates_exclude_file(self, tmp_path):
fake_checkout = tmp_path / "checkout"
(fake_checkout / ".git" / "info").mkdir(parents=True)
_ensure_viewer_excludes(fake_checkout)
assert (fake_checkout / ".git" / "info" / "exclude").exists()
def test_patterns_present(self, tmp_path):
fake_checkout = tmp_path / "checkout"
(fake_checkout / ".git" / "info").mkdir(parents=True)
_ensure_viewer_excludes(fake_checkout)
content = (fake_checkout / ".git" / "info" / "exclude").read_text()
assert "*.ifcview" in content
assert "*.rdbview" in content
def test_idempotent(self, tmp_path):
fake_checkout = tmp_path / "checkout"
(fake_checkout / ".git" / "info").mkdir(parents=True)
_ensure_viewer_excludes(fake_checkout)
_ensure_viewer_excludes(fake_checkout)
content = (fake_checkout / ".git" / "info" / "exclude").read_text()
assert content.count("*.ifcview") == 1
def test_appends_to_existing_file(self, tmp_path):
fake_checkout = tmp_path / "checkout"
(fake_checkout / ".git" / "info").mkdir(parents=True)
exclude = fake_checkout / ".git" / "info" / "exclude"
exclude.write_text("# existing\n*.log\n")
_ensure_viewer_excludes(fake_checkout)
content = exclude.read_text()
assert "*.log" in content
assert "*.ifcview" in content
def test_does_not_duplicate_existing_pattern(self, tmp_path):
fake_checkout = tmp_path / "checkout"
(fake_checkout / ".git" / "info").mkdir(parents=True)
exclude = fake_checkout / ".git" / "info" / "exclude"
exclude.write_text("*.ifcview\n")
_ensure_viewer_excludes(fake_checkout)
assert exclude.read_text().count("*.ifcview") == 1
# ---------------------------------------------------------------------------
# _ensure_checkout
# ---------------------------------------------------------------------------
class TestEnsureCheckout:
def test_creates_checkout_dir(self, tmp_path):
meta = _make_bare_cache(tmp_path, "https://example.com/org/repo")
checkout_dir = _ensure_checkout(meta["cache_dir"], meta["hexsha"])
assert checkout_dir.is_dir()
def test_checkout_is_detached_head(self, tmp_path):
import git as gitpkg
meta = _make_bare_cache(tmp_path, "https://example.com/org/repo")
checkout_dir = _ensure_checkout(meta["cache_dir"], meta["hexsha"])
repo = gitpkg.Repo(str(checkout_dir))
assert repo.head.is_detached
def test_checkout_at_correct_commit(self, tmp_path):
import git as gitpkg
meta = _make_bare_cache(tmp_path, "https://example.com/org/repo")
checkout_dir = _ensure_checkout(meta["cache_dir"], meta["hexsha"])
repo = gitpkg.Repo(str(checkout_dir))
assert repo.head.commit.hexsha == meta["hexsha"]
def test_file_present_in_checkout(self, tmp_path):
meta = _make_bare_cache(tmp_path, "https://example.com/org/repo")
checkout_dir = _ensure_checkout(meta["cache_dir"], meta["hexsha"])
assert (checkout_dir / "model.ifc").exists()
def test_idempotent_same_commit(self, tmp_path):
meta = _make_bare_cache(tmp_path, "https://example.com/org/repo")
d1 = _ensure_checkout(meta["cache_dir"], meta["hexsha"])
d2 = _ensure_checkout(meta["cache_dir"], meta["hexsha"])
assert d1 == d2
def test_updates_to_new_commit(self, tmp_path):
import git as gitpkg
meta = _make_bare_cache(tmp_path, "https://example.com/org/repo")
_ensure_checkout(meta["cache_dir"], meta["hexsha"])
# Add a new commit to the bare cache
src_repo = gitpkg.Repo(str(meta["src"]))
(meta["src"] / "model.ifc").write_bytes(b"IFC4\nV2\n")
src_repo.index.add(["model.ifc"])
new_commit = src_repo.index.commit("update")
bare = gitpkg.Repo(str(meta["bare_dir"]))
bare.git.fetch(str(meta["src"]), "+refs/*:refs/*")
checkout_dir = _ensure_checkout(meta["cache_dir"], new_commit.hexsha)
repo = gitpkg.Repo(str(checkout_dir))
assert repo.head.commit.hexsha == new_commit.hexsha
assert (checkout_dir / "model.ifc").read_bytes() == b"IFC4\nV2\n"
# ---------------------------------------------------------------------------
# get_checkout
# ---------------------------------------------------------------------------
class TestGetCheckout:
def _url_and_cache(self, tmp_path, monkeypatch, remote_url="https://example.com/org/repo"):
monkeypatch.setattr("ifcurl.git.user_cache_dir", lambda *a, **kw: str(tmp_path))
meta = _make_bare_cache(tmp_path, remote_url)
url = _ifc_url(meta["hexsha"], remote_url)
return url, meta
def test_returns_path(self, tmp_path, monkeypatch):
url, _ = self._url_and_cache(tmp_path, monkeypatch)
result = get_checkout(url)
assert isinstance(result, Path)
def test_file_exists(self, tmp_path, monkeypatch):
url, _ = self._url_and_cache(tmp_path, monkeypatch)
result = get_checkout(url)
assert result.exists()
def test_file_content_correct(self, tmp_path, monkeypatch):
url, meta = self._url_and_cache(tmp_path, monkeypatch)
result = get_checkout(url)
assert result.read_bytes() == meta["bytes"]
def test_path_ends_with_ifc_filename(self, tmp_path, monkeypatch):
url, _ = self._url_and_cache(tmp_path, monkeypatch)
result = get_checkout(url)
assert result.name == "model.ifc"
def test_checkout_is_detached_head(self, tmp_path, monkeypatch):
import git as gitpkg
url, meta = self._url_and_cache(tmp_path, monkeypatch)
get_checkout(url)
checkout_dir = meta["cache_dir"] / "checkout"
repo = gitpkg.Repo(str(checkout_dir))
assert repo.head.is_detached
def test_viewer_excludes_written(self, tmp_path, monkeypatch):
url, meta = self._url_and_cache(tmp_path, monkeypatch)
get_checkout(url)
exclude = meta["cache_dir"] / "checkout" / ".git" / "info" / "exclude"
content = exclude.read_text()
assert "*.ifcview" in content
assert "*.rdbview" in content
def test_idempotent(self, tmp_path, monkeypatch):
url, _ = self._url_and_cache(tmp_path, monkeypatch)
p1 = get_checkout(url)
p2 = get_checkout(url)
assert p1 == p2
def test_nested_ifc_path(self, tmp_path, monkeypatch):
import git as gitpkg
remote_url = "https://example.com/org/repo2"
monkeypatch.setattr("ifcurl.git.user_cache_dir", lambda *a, **kw: str(tmp_path))
src = tmp_path / "source2"
(src / "models").mkdir(parents=True)
(src / "models" / "structure.ifc").write_bytes(b"IFC4\n")
repo = gitpkg.Repo.init(str(src))
with repo.config_writer() as cw:
cw.set_value("user", "name", "Test")
cw.set_value("user", "email", "t@t.com")
repo.index.add(["models/structure.ifc"])
commit = repo.index.commit("init")
url_hash = hashlib.sha256(remote_url.encode()).hexdigest()[:24]
cache_dir = tmp_path / url_hash
gitpkg.Repo.clone_from(str(src), str(cache_dir / "repo.git"), bare=True)
(cache_dir / "remote_url").write_text(remote_url)
url = IfcUrl.parse(f"ifc://example.com/org/repo2@{commit.hexsha}?path=models/structure.ifc")
result = get_checkout(url)
assert result.name == "structure.ifc"
assert result.exists()
def test_local_transport_raises(self, tmp_path):
url = IfcUrl.parse(f"ifc://{tmp_path}@HEAD?path=model.ifc")
with pytest.raises(ValueError, match="local"):
get_checkout(url)
def test_no_path_raises(self, tmp_path, monkeypatch):
monkeypatch.setattr("ifcurl.git.user_cache_dir", lambda *a, **kw: str(tmp_path))
url = IfcUrl.parse("ifc://example.com/org/repo@abc123def")
with pytest.raises(ValueError, match="'path'"):
get_checkout(url)