371 lines
13 KiB
Bash
Executable File
371 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# run.sh — E2E test suite for musicfs.
|
|
#
|
|
# Full distributed path: server in Incus VM serving a generated music library
|
|
# over gRPC, local client mounting it as FUSE, tests run against the mountpoint.
|
|
#
|
|
# Requires: devenv shell (for ffmpeg, metaflac, id3v2, incus, grpcurl, Postgres)
|
|
#
|
|
# Usage:
|
|
# scripts/e2e/run.sh full suite (creates VM, runs tests, destroys)
|
|
# KEEP=1 scripts/e2e/run.sh keep VM + DB alive after run for debugging
|
|
#
|
|
set -euo pipefail
|
|
|
|
# ─────────────────────────── config ────────────────────────────────────
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
SCRIPTS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
|
MNT="$PROJECT_DIR/target/e2e/mnt"
|
|
MUSIC_DIR="$PROJECT_DIR/target/e2e/music"
|
|
LOG_DIR="$PROJECT_DIR/logs/e2e"
|
|
DB_NAME="musicfs_e2e"
|
|
VM_NAME="musicfs-e2e"
|
|
PORT=50061
|
|
KEEP="${KEEP:-0}"
|
|
RUST_LOG_VAL="${RUST_LOG:-musicfs=debug}"
|
|
|
|
CLIENT_PID=""
|
|
STARTED_PG=""
|
|
. "$SCRIPT_DIR/lib.sh"
|
|
|
|
log() { printf '\033[1;34m[e2e]\033[0m %s\n' "$*" >&2; }
|
|
die() { printf '\033[1;31m[e2e error]\033[0m %s\n' "$*" >&2; exit 1; }
|
|
|
|
# ──────────────────────── postgres ─────────────────────────────────────
|
|
ensure_postgres() {
|
|
if pg_isready -h "$PGHOST" >/dev/null 2>&1; then return 0; fi
|
|
log "Postgres not running — starting via devenv…"
|
|
devenv up postgres &
|
|
STARTED_PG=$!
|
|
local i
|
|
for ((i = 0; i < 45; i++)); do
|
|
if pg_isready -h "$PGHOST" >/dev/null 2>&1; then
|
|
log "Postgres ready"
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
die "Postgres did not become ready in 45s"
|
|
}
|
|
|
|
stop_postgres() {
|
|
[[ -n "$STARTED_PG" ]] || return 0
|
|
log "stopping Postgres (started by this script)…"
|
|
kill "$STARTED_PG" 2>/dev/null || true
|
|
wait "$STARTED_PG" 2>/dev/null || true
|
|
STARTED_PG=""
|
|
}
|
|
|
|
# ──────────────────────── VM management ────────────────────────────────
|
|
vm_ip() { incus list "$VM_NAME" -f csv -c 4 2>/dev/null | head -1 | awk '{print $1}'; }
|
|
|
|
detect_e2e_ip() {
|
|
local bridge_cidr
|
|
bridge_cidr="$(incus network get incusbr0 ipv4.address 2>/dev/null || true)"
|
|
if [[ -z "$bridge_cidr" ]]; then echo ""; return; fi
|
|
local gateway="${bridge_cidr%%/*}"
|
|
echo "${gateway%.*}.146"
|
|
}
|
|
|
|
start_vm() {
|
|
log "starting VM '$VM_NAME' (port $PORT)…"
|
|
# e2e VM uses .146 to avoid colliding with the dev VM (.145)
|
|
local e2e_ip
|
|
e2e_ip="$(detect_e2e_ip)"
|
|
VM_NAME="$VM_NAME" \
|
|
VM_IP="$e2e_ip" \
|
|
MUSIC_SOURCE="$MUSIC_DIR" \
|
|
LISTEN_PORT="$PORT" \
|
|
RUST_LOG=debug \
|
|
bash "$SCRIPTS_DIR/vm.sh" up
|
|
|
|
local ip; ip="$(vm_ip)"
|
|
[[ -n "$ip" ]] || die "could not determine VM IP"
|
|
log "VM IP: $ip"
|
|
|
|
log "waiting for health check…"
|
|
local i
|
|
for ((i = 0; i < 45; i++)); do
|
|
if grpcurl -plaintext "$ip:$PORT" grpc.health.v1.Health/Check 2>/dev/null | grep -q SERVING; then
|
|
log "server healthy"
|
|
VM_IP="$ip"
|
|
return
|
|
fi
|
|
sleep 1
|
|
done
|
|
die "server health check timed out"
|
|
}
|
|
|
|
destroy_vm() {
|
|
VM_NAME="$VM_NAME" bash "$SCRIPTS_DIR/vm.sh" destroy 2>/dev/null || true
|
|
}
|
|
|
|
# ──────────────────────── DB management ────────────────────────────────
|
|
reset_db() {
|
|
log "resetting database '$DB_NAME'…"
|
|
dropdb --if-exists "$DB_NAME" 2>/dev/null || true
|
|
createdb "$DB_NAME"
|
|
psql -q -d "$DB_NAME" -f "$PROJECT_DIR/db/schema.sql"
|
|
}
|
|
|
|
drop_db() {
|
|
dropdb --if-exists "$DB_NAME" 2>/dev/null || true
|
|
}
|
|
|
|
# ────────────────────── client lifecycle ───────────────────────────────
|
|
build_client() {
|
|
log "building client…"
|
|
(cd "$PROJECT_DIR" && cargo build --bin musicfs)
|
|
}
|
|
|
|
start_client() {
|
|
mkdir -p "$MNT" "$LOG_DIR"
|
|
local dsn="postgresql://fujin@localhost/$DB_NAME?host=$PGHOST"
|
|
|
|
log "starting client (mountpoint $MNT)…"
|
|
RUST_LOG="$RUST_LOG_VAL" \
|
|
"$PROJECT_DIR/target/debug/musicfs" \
|
|
--source "http://$VM_IP:$PORT" \
|
|
--mountpoint "$MNT" \
|
|
--database "$dsn" \
|
|
--log-dir "$LOG_DIR" &
|
|
CLIENT_PID=$!
|
|
wait_for_mount
|
|
}
|
|
|
|
wait_for_mount() {
|
|
log "waiting for FUSE mount…"
|
|
local i
|
|
for ((i = 0; i < 60; i++)); do
|
|
if [[ -d "$MNT/E2E Artist A" ]]; then
|
|
log "mount ready"
|
|
return
|
|
fi
|
|
if ! kill -0 "$CLIENT_PID" 2>/dev/null; then
|
|
die "client process exited prematurely — check $LOG_DIR"
|
|
fi
|
|
sleep 1
|
|
done
|
|
die "FUSE mount not ready after 60s"
|
|
}
|
|
|
|
stop_client() {
|
|
[[ -n "$CLIENT_PID" ]] || return 0
|
|
kill "$CLIENT_PID" 2>/dev/null || true
|
|
local i
|
|
for ((i = 0; i < 10; i++)); do
|
|
kill -0 "$CLIENT_PID" 2>/dev/null || break
|
|
sleep 1
|
|
done
|
|
kill -9 "$CLIENT_PID" 2>/dev/null || true
|
|
wait "$CLIENT_PID" 2>/dev/null || true
|
|
CLIENT_PID=""
|
|
fusermount -u "$MNT" 2>/dev/null || true
|
|
sleep 1
|
|
}
|
|
|
|
# ─────────────────────────── cleanup ───────────────────────────────────
|
|
cleanup() {
|
|
local rv=$?
|
|
log "cleaning up…"
|
|
stop_client || true
|
|
if [[ "$KEEP" != "1" ]]; then
|
|
drop_db
|
|
destroy_vm
|
|
stop_postgres || true
|
|
rm -rf "$MNT"
|
|
else
|
|
log "KEEP=1 — VM, DB, and mount left intact for debugging"
|
|
log " mountpoint: $MNT"
|
|
log " VM IP: ${VM_IP:-<unknown>}"
|
|
log " logs: $LOG_DIR"
|
|
fi
|
|
exit $rv
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# ─────────────────────────── paths ─────────────────────────────────────
|
|
ALPHA="$MNT/E2E Artist A/E2E Album X/01 - Alpha.flac"
|
|
BETA="$MNT/E2E Artist A/E2E Album X/02 - Beta.flac"
|
|
GAMMA="$MNT/E2E Artist B/E2E Album Y/01 - Gamma.mp3"
|
|
DELTA="$MNT/E2E Artist B/E2E Album Y/02 - Delta.mp3"
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
# TESTS
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
test_structural() {
|
|
printf '\n\033[1;36m── structural ──\033[0m\n'
|
|
|
|
local root; root="$(ls "$MNT" 2>/dev/null || true)"
|
|
assert_contains "root has Artist A dir" "$root" "E2E Artist A"
|
|
assert_contains "root has Artist B dir" "$root" "E2E Artist B"
|
|
|
|
local album_a; album_a="$(ls "$MNT/E2E Artist A/E2E Album X" 2>/dev/null || true)"
|
|
assert_contains "Album X has Alpha.flac" "$album_a" "01 - Alpha.flac"
|
|
assert_contains "Album X has Beta.flac" "$album_a" "02 - Beta.flac"
|
|
|
|
local album_b; album_b="$(ls "$MNT/E2E Artist B/E2E Album Y" 2>/dev/null || true)"
|
|
assert_contains "Album Y has Gamma.mp3" "$album_b" "01 - Gamma.mp3"
|
|
assert_contains "Album Y has Delta.mp3" "$album_b" "02 - Delta.mp3"
|
|
|
|
local ftype; ftype="$(stat -c %F "$ALPHA" 2>/dev/null || echo "missing")"
|
|
assert_eq "Alpha is regular file" "regular file" "$ftype"
|
|
|
|
local fsize; fsize="$(stat -c %s "$ALPHA" 2>/dev/null || echo 0)"
|
|
assert_gt "Alpha virtual size > 0" "$fsize" 0
|
|
|
|
local dtype; dtype="$(stat -c %F "$MNT/E2E Artist A" 2>/dev/null || echo "missing")"
|
|
assert_eq "Artist A is directory" "directory" "$dtype"
|
|
}
|
|
|
|
test_cache() {
|
|
printf '\n\033[1;36m── cache behavior ──\033[0m\n'
|
|
|
|
ffprobe -hide_banner -v quiet -show_format "$BETA" >/dev/null 2>&1 || true
|
|
ffprobe -hide_banner -v quiet -show_format "$BETA" >/dev/null 2>&1 || true
|
|
|
|
sleep 1
|
|
local log_file=""
|
|
log_file="$(ls -t "$LOG_DIR"/musicfs.*.log 2>/dev/null | head -1)" || true
|
|
local logs=""
|
|
if [[ -n "$log_file" ]]; then
|
|
logs="$(cat "$log_file" 2>/dev/null || true)"
|
|
fi
|
|
|
|
assert_contains "cache miss on first read" "$logs" "cache miss"
|
|
assert_contains "cache hit on second read" "$logs" "cache hit"
|
|
}
|
|
|
|
test_flac_read() {
|
|
printf '\n\033[1;36m── FLAC read ──\033[0m\n'
|
|
|
|
local title; title="$(metaflac --show-tag=TITLE "$ALPHA" 2>/dev/null || true)"
|
|
local artist; artist="$(metaflac --show-tag=ARTIST "$ALPHA" 2>/dev/null || true)"
|
|
local album; album="$(metaflac --show-tag=ALBUM "$ALPHA" 2>/dev/null || true)"
|
|
|
|
assert_contains "FLAC TITLE tag" "$title" "TITLE=Alpha"
|
|
assert_contains "FLAC ARTIST tag" "$artist" "ARTIST=E2E Artist A"
|
|
assert_contains "FLAC ALBUM tag" "$album" "ALBUM=E2E Album X"
|
|
|
|
local pics; pics="$(metaflac --list --block-type=PICTURE "$ALPHA" 2>/dev/null || true)"
|
|
assert_contains "FLAC has cover art" "$pics" "PICTURE"
|
|
}
|
|
|
|
test_flac_write() {
|
|
printf '\n\033[1;36m── FLAC write ──\033[0m\n'
|
|
|
|
metaflac --remove-tag=ARTIST --set-tag="ARTIST=Modified FLAC" "$BETA" 2>/dev/null
|
|
|
|
local artist; artist="$(metaflac --show-tag=ARTIST "$BETA" 2>/dev/null || true)"
|
|
assert_contains "FLAC write — ARTIST updated" "$artist" "ARTIST=Modified FLAC"
|
|
|
|
local title; title="$(metaflac --show-tag=TITLE "$BETA" 2>/dev/null || true)"
|
|
assert_contains "FLAC write — TITLE preserved" "$title" "TITLE=Beta"
|
|
}
|
|
|
|
test_mp3_read() {
|
|
printf '\n\033[1;36m── MP3 read ──\033[0m\n'
|
|
|
|
local info; info="$(id3v2 --list "$GAMMA" 2>/dev/null || true)"
|
|
|
|
assert_contains "MP3 TITLE tag" "$info" "Gamma"
|
|
assert_contains "MP3 ARTIST tag" "$info" "E2E Artist B"
|
|
assert_contains "MP3 ALBUM tag" "$info" "E2E Album Y"
|
|
}
|
|
|
|
test_mp3_write() {
|
|
printf '\n\033[1;36m── MP3 write ──\033[0m\n'
|
|
|
|
id3v2 --song "Modified Delta" --artist "Modified MP3" "$DELTA" 2>/dev/null
|
|
|
|
local info; info="$(id3v2 --list "$DELTA" 2>/dev/null || true)"
|
|
assert_contains "MP3 write — TIT2 updated" "$info" "Modified Delta"
|
|
assert_contains "MP3 write — TPE1 updated" "$info" "Modified MP3"
|
|
}
|
|
|
|
test_chunked_write() {
|
|
printf '\n\033[1;36m── chunked write resilience ──\033[0m\n'
|
|
|
|
# Write at offset 8192 — well past any metadata blocks for both formats.
|
|
# The write handler should ignore it (no tag pattern match) and the
|
|
# metadata structure remains parseable.
|
|
|
|
local title_before; title_before="$(metaflac --show-tag=TITLE "$ALPHA" 2>/dev/null || true)"
|
|
printf 'XXXX' | dd of="$ALPHA" bs=1 seek=8192 count=4 conv=notrunc 2>/dev/null || true
|
|
sleep 0.5
|
|
local title_after; title_after="$(metaflac --show-tag=TITLE "$ALPHA" 2>/dev/null || true)"
|
|
assert_eq "FLAC tags unchanged after non-metadata write" "$title_before" "$title_after"
|
|
|
|
local mp3_before; mp3_before="$(id3v2 --list "$GAMMA" 2>/dev/null | grep -i 'title' || true)"
|
|
printf 'YYYY' | dd of="$GAMMA" bs=1 seek=8192 count=4 conv=notrunc 2>/dev/null || true
|
|
sleep 0.5
|
|
local mp3_after; mp3_after="$(id3v2 --list "$GAMMA" 2>/dev/null | grep -i 'title' || true)"
|
|
assert_eq "MP3 tags unchanged after non-metadata write" "$mp3_before" "$mp3_after"
|
|
}
|
|
|
|
test_persistence() {
|
|
printf '\n\033[1;36m── persistence (client restart) ──\033[0m\n'
|
|
|
|
metaflac --remove-tag=ARTIST --set-tag="ARTIST=Persisted Artist" "$ALPHA" 2>/dev/null
|
|
|
|
local before; before="$(metaflac --show-tag=ARTIST "$ALPHA" 2>/dev/null || true)"
|
|
assert_contains "persistence — write before restart" "$before" "ARTIST=Persisted Artist"
|
|
|
|
log "restarting client…"
|
|
stop_client
|
|
start_client
|
|
|
|
local after; after="$(metaflac --show-tag=ARTIST "$ALPHA" 2>/dev/null || true)"
|
|
assert_contains "persistence — read after restart" "$after" "ARTIST=Persisted Artist"
|
|
}
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
# MAIN
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
main() {
|
|
[[ -n "${PGHOST:-}" ]] || die "PGHOST not set — run inside devenv shell"
|
|
ensure_postgres
|
|
command -v incus >/dev/null || die "incus not found"
|
|
command -v grpcurl >/dev/null || die "grpcurl not found"
|
|
command -v metaflac >/dev/null || die "metaflac not found"
|
|
command -v id3v2 >/dev/null || die "id3v2 not found"
|
|
command -v ffprobe >/dev/null || die "ffprobe not found"
|
|
|
|
log "=== E2E suite start ==="
|
|
|
|
# Phase 0: fixtures
|
|
bash "$SCRIPT_DIR/make-fixtures.sh" "$MUSIC_DIR"
|
|
|
|
# Phase 1: server
|
|
start_vm
|
|
|
|
# Phase 2: client
|
|
build_client
|
|
reset_db
|
|
start_client
|
|
|
|
# Phase 3-8: tests (read-only first, mutating last)
|
|
test_structural
|
|
test_cache
|
|
test_flac_read
|
|
test_flac_write
|
|
test_mp3_read
|
|
test_mp3_write
|
|
test_chunked_write
|
|
test_persistence
|
|
|
|
# Summary
|
|
printf '\n'
|
|
summary
|
|
local rv=$?
|
|
log "=== E2E suite done ==="
|
|
exit $rv
|
|
}
|
|
|
|
main "$@"
|