Files
jabberwocky238 d7e4fc73b1 phase2: COW append-only backend (segment diff + in-memory VFS)
- store: data.raw append-only / read-by-offset / fsync
- meta: Segment/VersionDiff/FileView/FileMeta + segment-list update (append/overwrite/truncate/extend) + in-memory directory tree
- fuse_ops: all callbacks route through COW backend; per-write Segment logging (op/phys_off/log_off/size)
- tests/phase2.sh: 5 write scenarios + sqlite + dir ops, 24/24 pass

Metadata in-memory (restart-lost; persistence phase 5); data.raw retains superseded bytes pending GC. cur cache / aggregation / multi-block land in phase 3.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 00:52:33 +08:00

119 lines
4.3 KiB
Bash

#!/usr/bin/env bash
# Phase 2 acceptance test: COW append-only backend.
# Exercises the five write scenarios + SQLite + dir ops and checks correctness,
# then confirms the bottom-level log shows Segment-level write observation.
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BIN="$ROOT/etvd_fuse"
MNT="$ROOT/mnt"
BACKEND="${BACKEND:-$ROOT/backend}"
LOG="$ROOT/etvd.log"
cleanup() {
fusermount3 -u "$MNT" 2>/dev/null || true
}
trap cleanup EXIT
cleanup
rm -rf "$MNT" "$BACKEND" "$LOG"
mkdir -p "$MNT" "$BACKEND"
echo "==> starting etvd_fuse (log -> $LOG)"
"$BIN" "$MNT" -o backend="$BACKEND" 2>"$LOG" &
DAEMON=$!
mounted=0
for _ in $(seq 1 50); do
if mountpoint -q "$MNT"; then mounted=1; break; fi
sleep 0.1
done
if [ "$mounted" -ne 1 ]; then
echo "FATAL: mount failed; log:"; cat "$LOG"; exit 1
fi
pass=0; fail=0
check() {
if [[ "$2" == "$3" ]]; then
echo " PASS: $1"; pass=$((pass+1))
else
echo " FAIL: $1"; echo " expected: [$2]"; echo " actual: [$3]"; fail=$((fail+1))
fi
}
byte_hex() { dd if="$1" bs=1 skip="$2" count=1 status=none 2>/dev/null | od -An -tx1 | tr -d ' \n'; }
echo "==> scenario 1: append"
printf 'a\n' > "$MNT/f1"
printf 'b\n' >> "$MNT/f1"
check "append content" "a
b" "$(cat "$MNT/f1")"
check "append size" "4" "$(stat -c%s "$MNT/f1")"
echo "==> scenario 2: random overwrite"
printf 'ABCDEFGH' > "$MNT/f2"
printf 'XXXX' | dd of="$MNT/f2" bs=1 seek=4 count=4 conv=notrunc status=none
check "overwrite content" "ABCDXXXX" "$(cat "$MNT/f2")"
check "overwrite size" "8" "$(stat -c%s "$MNT/f2")"
raw_after_overwrite=$(stat -c%s "$BACKEND/data.raw")
echo " data.raw after overwrite = $raw_after_overwrite bytes (old ABCDEFGH tail retained, > logical 8)"
echo "==> scenario 3: truncate down + up (hole)"
truncate -s 3 "$MNT/f2"
check "truncate-down size" "3" "$(stat -c%s "$MNT/f2")"
check "truncate-down content" "ABC" "$(cat "$MNT/f2")"
truncate -s 10 "$MNT/f2"
check "truncate-up size" "10" "$(stat -c%s "$MNT/f2")"
check "truncate-up hole byte[3]=00" "00" "$(byte_hex "$MNT/f2" 3)"
check "truncate-up hole byte[9]=00" "00" "$(byte_hex "$MNT/f2" 9)"
echo "==> scenario 4: extend write with hole"
printf 'Y' | dd of="$MNT/f3" bs=1 seek=100 count=1 conv=notrunc status=none
check "extend size" "101" "$(stat -c%s "$MNT/f3")"
check "extend hole byte[0]=00" "00" "$(byte_hex "$MNT/f3" 0)"
check "extend byte[100]=59(Y)" "59" "$(byte_hex "$MNT/f3" 100)"
echo "==> scenario 5: fsync + sqlite"
sqlite3 "$MNT/test.db" "CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT); INSERT INTO t VALUES (1,'alice'); INSERT INTO t VALUES (2,'bob');"
check "sqlite select" "1|alice
2|bob" "$(sqlite3 "$MNT/test.db" 'SELECT * FROM t ORDER BY id;')"
sqlite3 "$MNT/test.db" "UPDATE t SET name='ALICE' WHERE id=1;"
check "sqlite update" "1|ALICE
2|bob" "$(sqlite3 "$MNT/test.db" 'SELECT * FROM t ORDER BY id;')"
check "sqlite integrity" "ok" "$(sqlite3 "$MNT/test.db" 'PRAGMA integrity_check;')"
echo "==> scenario 6: dir / rename / unlink / rmdir"
mkdir "$MNT/sub"
echo "x" > "$MNT/sub/a.txt"
check "readdir lists file" "a.txt" "$(ls "$MNT/sub")"
mv "$MNT/sub/a.txt" "$MNT/sub/b.txt"
check "rename" "b.txt" "$(ls "$MNT/sub")"
rm "$MNT/sub/b.txt"
check "unlink empties dir" "" "$(ls "$MNT/sub")"
rmdir "$MNT/sub"
if [ -e "$MNT/sub" ]; then
echo " FAIL: rmdir did not remove sub"; fail=$((fail+1))
else
echo " PASS: rmdir removed sub"; pass=$((pass+1))
fi
echo "==> observation: bottom-level write segments"
nwrites=$(grep -c '\[etvd\] write' "$LOG" || true)
nfsync=$(grep -c '\[etvd\] fsync' "$LOG" || true)
echo " write ops logged: $nwrites"
echo " fsync ops logged: $nfsync"
if [ "$nwrites" -gt 0 ]; then echo " PASS: writes observable"; pass=$((pass+1)); else echo " FAIL: no writes logged"; fail=$((fail+1)); fi
for want in Append Overwrite Extend; do
if grep -q "op=$want" "$LOG"; then echo " PASS: op=$want observed"; pass=$((pass+1));
else echo " FAIL: op=$want not observed"; fail=$((fail+1)); fi
done
if [ "$nfsync" -gt 0 ]; then echo " PASS: fsync observed"; pass=$((pass+1)); else echo " FAIL: no fsync logged"; fail=$((fail+1)); fi
echo "==> old-segment retention (no in-place rewrite, no GC yet)"
raw_final=$(stat -c%s "$BACKEND/data.raw")
echo " data.raw final size = $raw_final bytes"
echo " (raw >> sum of current logical file sizes => superseded bytes retained on disk)"
cleanup
echo ""
echo "RESULT: pass=$pass fail=$fail"
[ "$fail" -eq 0 ]