d7e4fc73b1
- 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>
119 lines
4.3 KiB
Bash
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 ]
|