#!/usr/bin/env bash # Real end-to-end test for packfreeze. Exits 0 only if every assertion holds. # Run from anywhere: paths are resolved relative to this file. set -u HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WORK="$(mktemp -d "${TMPDIR:-/tmp}/packfreeze-test.XXXXXX")" trap 'rm -rf "$WORK"' EXIT PF="python3 $HERE/packfreeze.py" fails=0 note() { printf ' %s\n' "$1"; } ok() { printf 'ok - %s\n' "$1"; } bad() { printf 'FAIL - %s\n' "$1"; fails=$((fails + 1)); } # ---- fixture ------------------------------------------------------------- mkdir -p "$WORK/src/pkgA" "$WORK/src/pkgB" "$WORK/src/pkgA/__pycache__" "$WORK/src/.git" printf 'alpha\n' > "$WORK/src/pkgA/a.py" printf 'beta-beta\n' > "$WORK/src/pkgA/b.py" printf 'gamma' > "$WORK/src/pkgB/c.py" printf 'root file\n' > "$WORK/src/README.md" printf 'stale\n' > "$WORK/src/pkgA/__pycache__/junk.pyc" printf 'v1\n' > "$WORK/src/.git/config" ln -s "$WORK/src/pkgA/a.py" "$WORK/src/link.py" note "fixture built at $WORK/src" # ---- 1. make ------------------------------------------------------------- $PF make --root "$WORK/src" --out "$WORK/lock1.json" > "$WORK/make1.log" 2>&1 rc=$? [ $rc -eq 0 ] && ok "make exits 0" || bad "make exit=$rc" grep -q 'made ' "$WORK/make1.log" && ok "make prints a summary line" || bad "no summary line" # lock is valid JSON with the fields we rely on python3 - "$WORK/lock1.json" <<'PY' && ok "lock parses as JSON with required keys" || bad "lock JSON/keys" import json, sys d = json.load(open(sys.argv[1])) need = {"format", "generator", "file_count", "total_bytes", "digest_algorithm", "packages", "tree_digest", "files"} assert need <= set(d), need - set(d) assert d["format"] == 1 and d["digest_algorithm"] == "blake2b" assert isinstance(d["files"], dict) and d["files"] PY # ---- 2. determinism: same tree -> byte-identical lock -------------------- $PF make --root "$WORK/src" --out "$WORK/lock2.json" > /dev/null 2>&1 if cmp -s "$WORK/lock1.json" "$WORK/lock2.json"; then ok "two runs produce byte-identical lockfiles" else bad "lockfiles differ between runs" fi # ---- 3. digests are real blake2b of real bytes --------------------------- python3 - "$WORK/src" "$WORK/lock1.json" <<'PY' && ok "every recorded digest equals a fresh blake2b of the file bytes" || bad "digest mismatch vs blake2b" import hashlib, json, os, sys root, lock = sys.argv[1], json.load(open(sys.argv[2])) bad = 0 for rel, dig in lock["files"].items(): p = os.path.join(root, rel) h = hashlib.blake2b() with open(p, "rb") as fh: for b in iter(lambda: fh.read(1 << 16), b""): h.update(b) if h.hexdigest() != dig: bad += 1 assert bad == 0, f"{bad} mismatches" PY # ---- 4. content change is detected -------------------------------------- printf 'alpha-MUTATED\n' > "$WORK/src/pkgA/a.py" $PF verify --root "$WORK/src" --lock "$WORK/lock1.json" > "$WORK/ver.log" 2>&1 rc=$? [ $rc -ne 0 ] && ok "verify fails (nonzero) after a content edit" || bad "verify returned 0 on changed file" grep -q 'CHANGED pkgA/a.py' "$WORK/ver.log" && ok "verify names the changed file" || bad "changed file not reported" printf 'alpha\n' > "$WORK/src/pkgA/a.py" # ---- 5. deletion is detected -------------------------------------------- rm "$WORK/src/pkgB/c.py" $PF verify --root "$WORK/src" --lock "$WORK/lock1.json" > "$WORK/ver2.log" 2>&1 rc=$? [ $rc -ne 0 ] && ok "verify fails after a deletion" || bad "verify returned 0 on deleted file" grep -q 'MISSING pkgB/c.py' "$WORK/ver2.log" && ok "verify names the missing file" || bad "missing file not reported" printf 'gamma' > "$WORK/src/pkgB/c.py" # ---- 6. added file is detected ------------------------------------------ printf 'new\n' > "$WORK/src/pkgB/d.py" $PF verify --root "$WORK/src" --lock "$WORK/lock1.json" > "$WORK/ver3.log" 2>&1 rc=$? [ $rc -ne 0 ] && ok "verify fails after an added file" || bad "verify returned 0 on added file" grep -q 'ADDED pkgB/d.py' "$WORK/ver3.log" && ok "verify names the added file" || bad "added file not reported" rm "$WORK/src/pkgB/d.py" # ---- 7. clean tree verifies OK ------------------------------------------ $PF verify --root "$WORK/src" --lock "$WORK/lock1.json" > "$WORK/ver4.log" 2>&1 rc=$? [ $rc -eq 0 ] && ok "verify passes on an untouched tree" || { bad "verify failed on clean tree"; cat "$WORK/ver4.log"; } grep -q 'OK: ' "$WORK/ver4.log" && ok "verify prints an OK line" || bad "no OK line" # ---- 8. mtime-only change must NOT alter the lock ----------------------- BEFORE=$(sha256sum "$WORK/lock1.json" | cut -d' ' -f1) sleep 1 touch "$WORK/src/pkgA/a.py" "$WORK/src/README.md" $PF make --root "$WORK/src" --out "$WORK/lock3.json" > /dev/null 2>&1 AFTER=$(sha256sum "$WORK/lock3.json" | cut -d' ' -f1) [ "$BEFORE" = "$AFTER" ] && ok "touching files (mtime only) leaves the lock identical" || bad "lock changed on mtime-only touch" # ---- 9. symlinks and VCS/cache dirs are excluded ------------------------ python3 - "$WORK/lock1.json" <<'PY' && ok "symlinks, .git and __pycache__ are excluded from the lock" || bad "unexpected entries in lock" import json, sys files = set(json.load(open(sys.argv[1]))["files"]) assert not any(f.startswith(".git/") for f in files), "git dir leaked in" assert not any("__pycache__" in f for f in files), "pycache leaked in" assert "link.py" not in files, "symlink was followed and locked" assert "pkgA/a.py" in files and "README.md" in files, "real files missing" PY # ---- 10. package digests partition the tree ------------------------------ python3 - "$WORK/lock1.json" <<'PY' && ok "package digests cover every locked file exactly once" || bad "package partition wrong" import json, sys d = json.load(open(sys.argv[1])) seen = {} for pkg, items in []: pass # rebuild partition from files list via the documented rule files = d["files"] pkgs = {} for rel in files: name = rel.split("/", 1)[0] if "/" in rel else "(root)" pkgs.setdefault(name, []).append(rel) assert set(pkgs) == set(d["packages"]), (set(pkgs), set(d["packages"])) assert sum(len(v) for v in pkgs.values()) == len(files), "files double-counted" PY # ---- 11. --include filter ------------------------------------------------ $PF make --root "$WORK/src" --out "$WORK/lockf.json" --include 'pkgA/*' > /dev/null 2>&1 python3 - "$WORK/lockf.json" <<'PY' && ok "--include restricts the lock to matching paths" || bad "--include filter broken" import json, sys files = json.load(open(sys.argv[1]))["files"] assert files and all(f.startswith("pkgA/") for f in files), files assert "pkgA/a.py" in files and "README.md" not in files, files PY # ---- 12. check writes a machine-readable report -------------------------- $PF check --root "$WORK/src" --lock "$WORK/lock1.json" --output "$WORK/report.json" > "$WORK/chk.log" 2>&1 rc=$? [ $rc -eq 0 ] && ok "check exits 0 on a clean tree" || bad "check exit=$rc on clean tree" python3 - "$WORK/report.json" <<'PY' && ok "check report is valid JSON with ok=true and empty diffs" || bad "report shape wrong" import json, sys r = json.load(open(sys.argv[1])) assert r["ok"] is True, r assert r["missing"] == [] and r["changed"] == [] and r["added"] == [] and r["stale"] == [], r assert r["locked_tree_digest"] == r["observed_tree_digest"], r assert r["file_count"] > 0, r PY printf '\n%d failure(s)\n' "$fails" [ "$fails" -eq 0 ]