pivot to Python: replace Kotlin/JVM with stdlib zipapp
Reasons stacked up:
- AV: unsigned JARs that auto-download binaries + upload files trigger
Windows Defender false-positives more often than Python scripts
invoked by code-signed python.exe.
- Qt UI option: PySide6 opens a path to a real Qt UI (matching Prism's
look) if needed later. JVM Qt bindings are abandoned.
- frazclient already needs Python; inlining as 'import cloud_sync' is
zero overhead vs the launcher always shelling out to java.
Implementation:
- cloud_sync package: cli.py (argparse), creds.py, scope.py,
restic.py (binary discovery + auto-download + sha256 verify),
sync.py (pull/push subprocess restic).
- pyproject.toml with hatchling backend; pip-installable.
- Makefile builds cloud-sync.pyz via python -m zipapp (~53 KB).
- 33 pytest tests, stdlib only on runtime.
- CI workflow runs pytest matrix (3.10/3.11/3.12) + builds pyz.
- DESIGN.md + README.md updated to reflect Python.
E2E verified against local restic-rest-server:
pull empty → push initial → rm -rf local → pull restores → modify+push
creates second snapshot → client forget --prune blocked by --append-only.
Throws away ~565 LOC of Kotlin (and 18 jar tests) committed earlier in
this same session. Net result is ~250 LOC Python + 33 tests = smaller
and more aligned with the rest of the stack.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
"""Scope file reader + materializer tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from cloud_sync.scope import (
|
||||
DEFAULT_EXCLUDE,
|
||||
DEFAULT_INCLUDE,
|
||||
Scope,
|
||||
load,
|
||||
materialize_for_restic,
|
||||
)
|
||||
|
||||
|
||||
def test_load_missing_returns_defaults(tmp_path):
|
||||
s = load(tmp_path)
|
||||
assert s.include == DEFAULT_INCLUDE
|
||||
assert s.exclude == DEFAULT_EXCLUDE
|
||||
|
||||
|
||||
def test_load_valid_overrides_defaults(tmp_path):
|
||||
state = tmp_path / ".cloud-sync"
|
||||
state.mkdir()
|
||||
(state / "scope.json").write_text(json.dumps({
|
||||
"include": ["foo/", "bar.txt"],
|
||||
"exclude": ["**/*.log"],
|
||||
}))
|
||||
s = load(tmp_path)
|
||||
assert s.include == ["foo/", "bar.txt"]
|
||||
assert s.exclude == ["**/*.log"]
|
||||
|
||||
|
||||
def test_load_partial_keeps_defaults_for_missing(tmp_path):
|
||||
state = tmp_path / ".cloud-sync"
|
||||
state.mkdir()
|
||||
(state / "scope.json").write_text(json.dumps({"include": ["just-this"]}))
|
||||
s = load(tmp_path)
|
||||
assert s.include == ["just-this"]
|
||||
assert s.exclude == DEFAULT_EXCLUDE
|
||||
|
||||
|
||||
def test_load_invalid_falls_back(tmp_path, capsys):
|
||||
state = tmp_path / ".cloud-sync"
|
||||
state.mkdir()
|
||||
(state / "scope.json").write_text("{not valid json")
|
||||
s = load(tmp_path)
|
||||
assert s.include == DEFAULT_INCLUDE
|
||||
captured = capsys.readouterr()
|
||||
assert "invalid" in captured.err.lower()
|
||||
|
||||
|
||||
def test_materialize_writes_files(tmp_path):
|
||||
scope = Scope(include=["config/", "options.txt"], exclude=["**/*.log"])
|
||||
files_from, exclude_from = materialize_for_restic(tmp_path, scope)
|
||||
assert files_from.exists()
|
||||
assert exclude_from.exists()
|
||||
body_in = files_from.read_text().splitlines()
|
||||
body_ex = exclude_from.read_text().splitlines()
|
||||
# trailing slash stripped on include entries
|
||||
assert "config" in body_in
|
||||
assert "options.txt" in body_in
|
||||
assert "**/*.log" in body_ex
|
||||
|
||||
|
||||
def test_materialize_creates_state_dir(tmp_path):
|
||||
scope = Scope(include=["x"], exclude=["y"])
|
||||
files_from, _ = materialize_for_restic(tmp_path, scope)
|
||||
assert files_from.parent.name == ".cloud-sync"
|
||||
assert files_from.parent.exists()
|
||||
Reference in New Issue
Block a user