93 lines
3.6 KiB
Python
93 lines
3.6 KiB
Python
from concurrent.futures import ThreadPoolExecutor
|
|
from pathlib import Path
|
|
from threading import Barrier
|
|
|
|
import pytest
|
|
|
|
from gewu.adapters.git_repository import (
|
|
GitCanonicalRepository,
|
|
GitConflictError,
|
|
GitRepositoryError,
|
|
)
|
|
|
|
|
|
def test_bare_repository_commits_only_explicit_files_and_checks_expected_head(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
repository = GitCanonicalRepository(tmp_path / "private" / "knowledge.git")
|
|
base = repository.head()
|
|
record_path = "projects/project-a/nodes/node-a.yaml"
|
|
commit = repository.publish_files(
|
|
expected_head=base,
|
|
files={record_path: b"title: accepted\n"},
|
|
message="Accept fixture",
|
|
index_directory=tmp_path / "staging" / "publishing",
|
|
)
|
|
assert repository.read_file(commit, record_path) == b"title: accepted\n"
|
|
assert repository.list_files(commit, "projects") == (record_path,)
|
|
assert repository.list_files(commit, "sources") == ()
|
|
assert repository.read_file(commit, "unrelated/user-file.txt") is None
|
|
assert repository.introducing_commit(commit, record_path) == commit
|
|
|
|
with pytest.raises(GitConflictError, match="Canonical branch changed"):
|
|
repository.publish_files(
|
|
expected_head=base,
|
|
files={"projects/project-b/nodes/node-b.yaml": b"title: stale\n"},
|
|
message="Stale writer",
|
|
index_directory=tmp_path / "staging" / "publishing",
|
|
)
|
|
|
|
|
|
def test_record_paths_reject_traversal_and_noncanonical_separators(tmp_path: Path) -> None:
|
|
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
|
|
for unsafe in ("../escape", "/absolute", "projects//node", "projects/./node", "a\\b"):
|
|
with pytest.raises(GitRepositoryError, match="record path is invalid"):
|
|
repository.read_file(repository.head(), unsafe)
|
|
|
|
|
|
def test_independent_repository_adapters_allow_one_compare_and_swap_winner(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
repository_path = tmp_path / "knowledge.git"
|
|
first_adapter = GitCanonicalRepository(repository_path)
|
|
second_adapter = GitCanonicalRepository(repository_path)
|
|
base = first_adapter.head()
|
|
start = Barrier(2)
|
|
index_directory = tmp_path / "staging" / "publishing"
|
|
|
|
def publish(repository: GitCanonicalRepository, name: str) -> tuple[str, str | None]:
|
|
path = f"projects/{name}/nodes/node.yaml"
|
|
start.wait(timeout=5)
|
|
try:
|
|
revision = repository.publish_files(
|
|
expected_head=base,
|
|
files={path: f"title: {name}\n".encode()},
|
|
message=f"Publish {name}",
|
|
index_directory=index_directory,
|
|
)
|
|
except GitConflictError:
|
|
return name, None
|
|
return name, revision
|
|
|
|
with ThreadPoolExecutor(max_workers=2) as workers:
|
|
results = list(
|
|
workers.map(
|
|
lambda args: publish(*args),
|
|
((first_adapter, "project-a"), (second_adapter, "project-b")),
|
|
)
|
|
)
|
|
|
|
winners = [(name, revision) for name, revision in results if revision is not None]
|
|
losers = [name for name, revision in results if revision is None]
|
|
assert len(winners) == len(losers) == 1
|
|
winner, winning_revision = winners[0]
|
|
assert winning_revision is not None
|
|
assert first_adapter.head() == winning_revision
|
|
assert second_adapter.head() == winning_revision
|
|
assert (
|
|
first_adapter.read_file(winning_revision, f"projects/{winner}/nodes/node.yaml")
|
|
== f"title: {winner}\n".encode()
|
|
)
|
|
loser = losers[0]
|
|
assert first_adapter.read_file(winning_revision, f"projects/{loser}/nodes/node.yaml") is None
|