add claude-obsidian
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
# test_concurrent_write.sh — verify multi-writer safety with wiki-lock.sh.
|
||||
#
|
||||
# The critical correctness gate from v1.7 §3.4. Spawns N background workers,
|
||||
# each acquires a lock on the same file, appends a uniquely-tagged line, and
|
||||
# releases. After all workers exit we verify:
|
||||
# - the file received EXACTLY N appended lines (no losses)
|
||||
# - every worker's tagged line is present (no silent dropping)
|
||||
# - no orphaned lockfiles remain
|
||||
# - clear-stale reports 0 leftovers
|
||||
#
|
||||
# Without wiki-lock.sh, concurrent appends to the same file via `echo >> file`
|
||||
# can interleave and corrupt lines on some filesystems. With the lock, only
|
||||
# one worker holds the file at a time, and atomic append-then-release prevents
|
||||
# corruption.
|
||||
#
|
||||
# Hermetic: sandbox vault under mktemp, no network.
|
||||
#
|
||||
# Usage: bash tests/test_concurrent_write.sh
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
LOCK_SH="$ROOT/scripts/wiki-lock.sh"
|
||||
|
||||
WORKERS=10
|
||||
TARGET_FILE_REL="wiki/concepts/Stress.md"
|
||||
|
||||
SANDBOX=$(mktemp -d /tmp/concurrent-write-test-XXXXXX)
|
||||
trap 'rm -rf "$SANDBOX"' EXIT
|
||||
mkdir -p "$SANDBOX/.vault-meta/locks" "$SANDBOX/wiki/concepts"
|
||||
TARGET_ABS="$SANDBOX/$TARGET_FILE_REL"
|
||||
echo "seed" > "$TARGET_ABS"
|
||||
|
||||
export WIKI_LOCK_VAULT="$SANDBOX"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
assert_eq() {
|
||||
if [ "$2" = "$3" ]; then
|
||||
echo "OK $1"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "FAIL $1: expected '$2', got '$3'"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== test_concurrent_write.sh ==="
|
||||
echo "sandbox: $SANDBOX"
|
||||
echo "workers: $WORKERS"
|
||||
echo "target: $TARGET_FILE_REL"
|
||||
echo ""
|
||||
|
||||
# ── Worker function: acquire lock, append, release ──────────────────────────
|
||||
worker() {
|
||||
local id="$1"
|
||||
local attempts=0
|
||||
local max_attempts=50
|
||||
# Random jitter so workers don't all hit at the same instant
|
||||
local jitter=$(awk -v id="$id" 'BEGIN { srand(id); print int(rand()*100) }')
|
||||
# POSIX-portable sub-second sleep via sleep(1) with fractional seconds (GNU/macOS supports it)
|
||||
sleep "0.0${jitter}" 2>/dev/null || sleep 1
|
||||
|
||||
while [ "$attempts" -lt "$max_attempts" ]; do
|
||||
if bash "$LOCK_SH" acquire "$TARGET_FILE_REL" >/dev/null 2>&1; then
|
||||
# Append our line atomically
|
||||
echo "worker-$id-tag" >> "$TARGET_ABS"
|
||||
bash "$LOCK_SH" release "$TARGET_FILE_REL" >/dev/null 2>&1
|
||||
return 0
|
||||
fi
|
||||
attempts=$((attempts + 1))
|
||||
sleep "0.05" 2>/dev/null || sleep 1
|
||||
done
|
||||
echo "worker $id gave up after $attempts attempts" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Spawn workers in parallel ───────────────────────────────────────────────
|
||||
PIDS=()
|
||||
for i in $(seq 1 $WORKERS); do
|
||||
worker "$i" &
|
||||
PIDS+=("$!")
|
||||
done
|
||||
|
||||
# Wait for all workers
|
||||
FAILED_WORKERS=0
|
||||
for pid in "${PIDS[@]}"; do
|
||||
if ! wait "$pid"; then
|
||||
FAILED_WORKERS=$((FAILED_WORKERS + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
assert_eq "all workers completed (no give-ups)" "0" "$FAILED_WORKERS"
|
||||
|
||||
# ── Verify: file has seed + exactly N tagged lines ──────────────────────────
|
||||
TOTAL_LINES=$(wc -l < "$TARGET_ABS")
|
||||
assert_eq "total line count (seed + workers)" "$((WORKERS + 1))" "$TOTAL_LINES"
|
||||
|
||||
# Every worker tag must appear exactly once
|
||||
for i in $(seq 1 $WORKERS); do
|
||||
COUNT=$(grep -c "^worker-$i-tag$" "$TARGET_ABS" || echo 0)
|
||||
if [ "$COUNT" != "1" ]; then
|
||||
echo "FAIL worker-$i tag count: expected 1, got $COUNT"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
done
|
||||
echo "OK every worker tag appears exactly once"
|
||||
PASS=$((PASS + 1))
|
||||
|
||||
# ── Verify: no orphaned lockfiles ───────────────────────────────────────────
|
||||
LIVE_LOCKS=$(bash "$LOCK_SH" list | wc -l)
|
||||
assert_eq "no live lockfiles after workers exited" "0" "$LIVE_LOCKS"
|
||||
|
||||
# ── Verify: clear-stale reports 0 (nothing to reap) ─────────────────────────
|
||||
REAPED=$(bash "$LOCK_SH" clear-stale --max-age 0)
|
||||
assert_eq "clear-stale reaped count" "0" "$REAPED"
|
||||
|
||||
# ── Verify: file content sanity (no truncated/garbled lines) ────────────────
|
||||
GARBLED=$(awk 'length > 100' "$TARGET_ABS" | wc -l)
|
||||
assert_eq "no garbled (overlong) lines" "0" "$GARBLED"
|
||||
|
||||
echo ""
|
||||
echo "Pass: $PASS Fail: $FAIL"
|
||||
if [ $FAIL -gt 0 ]; then
|
||||
echo "File contents:"
|
||||
cat "$TARGET_ABS"
|
||||
exit 1
|
||||
fi
|
||||
echo "All concurrent-write tests passed."
|
||||
Reference in New Issue
Block a user