- WebDAV uploader: concurrency, request and bandwidth limits, retry with backoff, offline catch-up, encrypted manifests, durability tracking - Automatic unique remote directory claimed with a conditional PUT - AES-256-GCM client-side encryption with keyed blob names - /stats, /progress, /progress/stream and a /dashboard page - Own ctypes inotify binding with a compact watch tree (263 MB -> 76 MB RSS) - Directory-path ignore patterns and a forget command - Indexed range queries instead of LIKE for path prefixes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
3.9 KiB
Python
109 lines
3.9 KiB
Python
import asyncio
|
|
from pathlib import PurePath
|
|
|
|
from versiond.db import Database
|
|
from versiond.diff import hunks, unified
|
|
from versiond.filters import Filters
|
|
from versiond.ingest import Coalescer, Repository
|
|
from versiond.store import BlobStore
|
|
|
|
|
|
def test_filters_paths():
|
|
f = Filters()
|
|
ok = ["app.py", "src/main.go", ".env", ".env.production", "Makefile", "pkg/package.json", ".gitignore"]
|
|
for p in ok:
|
|
assert f.path_reason(PurePath(p)) is None, p
|
|
rejected = {
|
|
"node_modules/x/index.js": "ignored-directory",
|
|
".git/config": "ignored-directory",
|
|
"src/__pycache__/a.cpython-312.pyc": "ignored-directory",
|
|
"a.pyc": "ignored-extension",
|
|
".bashrc_local": "hidden-file",
|
|
"notes.txt~": "temporary-file",
|
|
"main.py.swp": "ignored-extension",
|
|
"venv/lib/x.py": "ignored-directory",
|
|
"target/debug/build.rs": "ignored-directory",
|
|
"4913": "temporary-file",
|
|
"src/pkg.egg-info/PKG-INFO": "ignored-directory",
|
|
}
|
|
for p, reason in rejected.items():
|
|
assert f.path_reason(PurePath(p)) == reason, p
|
|
assert f.size_reason(204_800) is None
|
|
assert f.size_reason(204_801) == "file-too-large"
|
|
assert f.content_reason(b"abc\x00def") == "binary-content"
|
|
|
|
|
|
def test_filter_patterns():
|
|
f = Filters(patterns=("*.log", "secret/*"))
|
|
assert f.path_reason(PurePath("x/debug.log")) == "ignored-pattern"
|
|
assert f.path_reason(PurePath("secret/key.txt")) == "ignored-pattern"
|
|
assert f.path_reason(PurePath("src/key.txt")) is None
|
|
|
|
|
|
def test_directory_patterns():
|
|
f = Filters(patterns=("app/data/uploads",))
|
|
assert f.skip_dir("uploads", "app/data/uploads")
|
|
assert not f.skip_dir("uploads", "other/uploads")
|
|
assert f.path_reason(PurePath("app/data/uploads/a/b.txt")) == "ignored-pattern"
|
|
assert f.path_reason(PurePath("app/data/config.json")) is None
|
|
|
|
|
|
def test_diff_unified():
|
|
a = b"one\ntwo\nthree\n"
|
|
b = b"one\n2\nthree\nfour\n"
|
|
out = unified(hunks(a, b), "a", "b")
|
|
assert "-two\n+2\n" in out and "+four\n" in out
|
|
assert hunks(b"x y\n", b"x y\n", ignore_whitespace=True) == []
|
|
|
|
|
|
def _repo(tmp_path):
|
|
db = Database(tmp_path / "index.sqlite")
|
|
return Repository(db, BlobStore(tmp_path / "spool"), Filters())
|
|
|
|
|
|
def test_coalescer_keeps_first_and_last(tmp_path):
|
|
repo = _repo(tmp_path)
|
|
path = str(tmp_path / "f.py")
|
|
|
|
async def run():
|
|
c = Coalescer(repo, window=0.2, max_hold=1.0, per_hour=100)
|
|
statuses = [c.submit(path, f"v{i}\n".encode(), "test", "edit")["status"] for i in range(6)]
|
|
await asyncio.sleep(0.5)
|
|
return statuses
|
|
|
|
statuses = asyncio.run(run())
|
|
assert statuses[0] == "committed"
|
|
assert set(statuses[1:]) == {"coalesced"}
|
|
file = repo.db.file_by_path(path)
|
|
contents = [repo.blobs.get(v["blob_sha256"]) for v in reversed(repo.db.history(file["id"]))]
|
|
assert contents == [b"v0\n", b"v5\n"]
|
|
|
|
|
|
def test_coalescer_rate_limit_keeps_newest(tmp_path):
|
|
repo = _repo(tmp_path)
|
|
path = str(tmp_path / "f.py")
|
|
|
|
async def run():
|
|
c = Coalescer(repo, window=0.05, max_hold=0.1, per_hour=2)
|
|
c.submit(path, b"a", "t", "e")
|
|
await asyncio.sleep(0.1)
|
|
c.submit(path, b"b", "t", "e")
|
|
await asyncio.sleep(0.2)
|
|
status = c.submit(path, b"c", "t", "e")["status"]
|
|
await asyncio.sleep(0.2)
|
|
flushed = c.flush()
|
|
return status, flushed
|
|
|
|
status, flushed = asyncio.run(run())
|
|
assert status == "coalesced"
|
|
assert flushed == 1 # over the limit it waits, and shutdown still stores it
|
|
file = repo.db.file_by_path(path)
|
|
assert repo.blobs.get(repo.db.latest_version(file["id"])["blob_sha256"]) == b"c"
|
|
|
|
|
|
def test_dedupe_unchanged(tmp_path):
|
|
repo = _repo(tmp_path)
|
|
path = str(tmp_path / "f.py")
|
|
assert repo.commit(path, b"x", "t", "e")["status"] == "committed"
|
|
assert repo.commit(path, b"x", "t", "e")["status"] == "unchanged"
|