e2e.sh and gen_fixtures.py assumed WSL driving the Windows toolchain. They now detect WSL (a microsoft kernel with cargo.exe available) and otherwise use native tools: - e2e.sh: cargo/curl instead of cargo.exe/curl.exe, pgrep/pkill instead of tasklist/taskkill, Linux paths in the generated .env, and binaries without the .exe suffix. - gen_fixtures.py: picks the first ffmpeg that has the drawtext filter (a minimal build earlier on PATH may not; E2E_FFMPEG overrides), finds a monospace font with fc-match, and only translates paths with wslpath for .exe tools. - Quote USER_NAME in the generated .env: the unquoted space made the dotenv parser reject the line. - Mark the scripts executable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
283 lines
10 KiB
Bash
Executable File
283 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Disposable ImageApi instance for end-to-end / agent-driven UI testing.
|
|
#
|
|
# Runs a second, fully isolated server next to any real one: its own build
|
|
# dir (target/e2e — a running target/debug exe is file-locked on Windows),
|
|
# its own process name (image-api-e2e, so `stop` can never touch the real
|
|
# server), its own port, database, thumbnails and HLS output. The media
|
|
# library is a *copy* of the pristine fixtures, so tests may upload, move
|
|
# and delete freely; `reset` restores the known baseline.
|
|
#
|
|
# Works on native Linux, and in WSL driving the Windows toolchain
|
|
# (cargo.exe, curl.exe, ffmpeg.exe). WSL2's default NAT networking can't
|
|
# reach Windows' 127.0.0.1, hence curl.exe for every HTTP call there.
|
|
#
|
|
# Usage: dev/e2e/e2e.sh <command> (see `help`)
|
|
set -euo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO="$(cd "$HERE/../.." && pwd)"
|
|
WORK="$HERE/.work"
|
|
FIXTURES="$WORK/fixtures"
|
|
STATE="$WORK/state"
|
|
BIN_DIR="$WORK/bin"
|
|
|
|
# WSL drives the Windows toolchain; anything else uses native tools.
|
|
if grep -qi microsoft /proc/version 2>/dev/null && command -v cargo.exe >/dev/null; then
|
|
WSL=1; EXT=".exe"; CARGO=cargo.exe; CURL=curl.exe
|
|
else
|
|
WSL=0; EXT=""; CARGO=cargo; CURL=curl
|
|
fi
|
|
EXE_NAME="image-api-e2e$EXT"
|
|
LOG="$WORK/server.log"
|
|
|
|
E2E_PORT="${E2E_PORT:-8090}"
|
|
# 127.0.0.1 is reachable from the Windows browser (Expo web), the Android
|
|
# emulator (as 10.0.2.2) and USB devices via `adb reverse`. Set
|
|
# E2E_BIND=0.0.0.0 for a physical device over Wi-Fi.
|
|
E2E_BIND="${E2E_BIND:-127.0.0.1}"
|
|
E2E_USER="${E2E_USER:-e2e}"
|
|
E2E_PASS="${E2E_PASS:-E2eTest#2026pass}"
|
|
URL="http://127.0.0.1:$E2E_PORT"
|
|
|
|
# A path as the server sees it (Windows path for the WSL-driven exe).
|
|
win() { if [ "$WSL" = 1 ]; then wslpath -w "$1"; else printf '%s' "$1"; fi; }
|
|
# Join a directory from win() with a file name.
|
|
win_join() { if [ "$WSL" = 1 ]; then printf '%s\\%s' "$(win "$1")" "$2"; else printf '%s/%s' "$1" "$2"; fi; }
|
|
log() { printf '\033[1;34m[e2e]\033[0m %s\n' "$*" >&2; }
|
|
die() { printf '\033[1;31m[e2e]\033[0m %s\n' "$*" >&2; exit 1; }
|
|
|
|
is_running() {
|
|
if [ "$WSL" = 1 ]; then
|
|
tasklist.exe /FI "IMAGENAME eq $EXE_NAME" /NH 2>/dev/null | grep -qi "$EXE_NAME"
|
|
else
|
|
pgrep -x "$EXE_NAME" >/dev/null
|
|
fi
|
|
}
|
|
|
|
cmd_build() {
|
|
log "building bins into target/e2e"
|
|
(cd "$REPO" && $CARGO build --target-dir target/e2e --bin image-api --bin create_user)
|
|
mkdir -p "$BIN_DIR"
|
|
# Run from a copy so rebuilding never fights a running (locked) exe.
|
|
local was_running=0
|
|
if is_running; then was_running=1; cmd_stop; fi
|
|
cp "$REPO/target/e2e/debug/image-api$EXT" "$BIN_DIR/$EXE_NAME"
|
|
cp "$REPO/target/e2e/debug/create_user$EXT" "$BIN_DIR/create_user$EXT"
|
|
if [ "$was_running" = 1 ]; then cmd_start; fi
|
|
}
|
|
|
|
cmd_fixtures() {
|
|
local force=0 import=()
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--force) force=1 ;;
|
|
--import) import=(--import "$2"); force=1; shift ;;
|
|
*) die "unknown fixtures option: $1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
if [ -f "$FIXTURES/manifest.json" ] && [ "$force" = 0 ]; then
|
|
log "fixtures exist (generated $(grep -o '"generated_on": "[^"]*"' "$FIXTURES/manifest.json" | cut -d'"' -f4)); --force to regenerate"
|
|
return
|
|
fi
|
|
log "generating fixtures"
|
|
python3 "$HERE/gen_fixtures.py" "$FIXTURES" "${import[@]}"
|
|
}
|
|
|
|
write_env() {
|
|
cat > "$STATE/.env" <<EOF
|
|
# Generated by dev/e2e/e2e.sh — edit dev/e2e/env.local instead (appended below).
|
|
DATABASE_URL='$(win_join "$STATE" e2e.db)'
|
|
BASE_PATH='$(win "$STATE/media")'
|
|
THUMBNAILS='$(win "$STATE/thumbs")'
|
|
VIDEO_PATH='$(win "$STATE/hls")'
|
|
GIFS_DIRECTORY='$(win "$STATE/gifs")'
|
|
PREVIEW_CLIPS_DIRECTORY='$(win "$STATE/previews")'
|
|
BIND_URL=$E2E_BIND:$E2E_PORT
|
|
# Don't claim localhost:8088 — the real server owns it.
|
|
LOCAL_BIND_URL=
|
|
# Expo web (Metro) origins.
|
|
CORS_ALLOWED_ORIGINS=http://localhost:8081,http://localhost:19006
|
|
SECRET_KEY=e2e-dev-only-secret-not-for-production
|
|
RUST_LOG=info
|
|
# Pin the server zone (ImageApi #120 preflight): the Windows exe has no
|
|
# /etc/timezone, so without this server_zone() is None and assumed_server
|
|
# rows fall back to the process-local offset.
|
|
TZ=America/New_York
|
|
# Fast ingest so a fresh reset is browsable within seconds.
|
|
WATCH_QUICK_INTERVAL_SECONDS=3
|
|
WATCH_FULL_INTERVAL_SECONDS=60
|
|
EXCLUDED_DIRS=/private
|
|
HLS_CONCURRENCY=2
|
|
USER_NAME='E2E Tester'
|
|
REEL_PREGEN_ENABLED=0
|
|
REEL_CACHE_SWEEP_ENABLED=0
|
|
# AI / sibling services are off by default: point Ollama at a closed port so
|
|
# insight calls fail fast instead of hitting the real local models. Opt in
|
|
# via env.local (LLM_BACKEND, LLAMA_SWAP_URL, APOLLO_API_BASE_URL, ...).
|
|
OLLAMA_PRIMARY_URL=http://127.0.0.1:9
|
|
EOF
|
|
if [ -f "$HERE/env.local" ]; then
|
|
{ echo; echo "# ── dev/e2e/env.local ──"; cat "$HERE/env.local"; } >> "$STATE/.env"
|
|
fi
|
|
}
|
|
|
|
cmd_reset() {
|
|
[ -f "$FIXTURES/manifest.json" ] || cmd_fixtures
|
|
is_running && cmd_stop
|
|
log "resetting state (db, thumbs, hls, media copy)"
|
|
rm -rf "$STATE"
|
|
mkdir -p "$STATE"/{thumbs,hls,gifs,previews}
|
|
cp -a "$FIXTURES/media" "$STATE/media"
|
|
cp "$FIXTURES/manifest.json" "$STATE/manifest.json"
|
|
write_env
|
|
[ -x "$BIN_DIR/create_user$EXT" ] || cmd_build
|
|
(cd "$STATE" && "$BIN_DIR/create_user$EXT" "$E2E_USER" "$E2E_PASS")
|
|
}
|
|
|
|
login() {
|
|
$CURL -sf -X POST "$URL/login" -H "Content-Type: application/json" \
|
|
-d "{\"username\":\"$E2E_USER\",\"password\":\"$E2E_PASS\"}" 2>/dev/null \
|
|
| grep -o '"token":"[^"]*"' | cut -d'"' -f4
|
|
}
|
|
|
|
api() { # api METHOD PATH [JSON]; reuses $E2E_TOKEN when set (login is a bcrypt verify)
|
|
local tok="${E2E_TOKEN:-$(login)}"
|
|
# </dev/null: curl.exe otherwise drains stdin, eating a caller's `while read` input.
|
|
$CURL -sf -X "$1" "$URL/$2" -H "Authorization: Bearer $tok" \
|
|
-H "Content-Type: application/json" ${3:+-d "$3"} </dev/null
|
|
}
|
|
|
|
indexed_count() {
|
|
api GET "photos?path=&recursive=true" | python3 -c \
|
|
'import json,sys; print(len(json.load(sys.stdin).get("photos", [])))' 2>/dev/null || echo 0
|
|
}
|
|
|
|
seed_tags() {
|
|
local E2E_TOKEN; E2E_TOKEN="$(login)"
|
|
python3 - "$STATE/manifest.json" <<'EOF' | while IFS=$'\t' read -r path tag; do
|
|
import json, sys
|
|
for tag, paths in json.load(open(sys.argv[1]))["tags"].items():
|
|
for p in paths:
|
|
print(f"{p}\t{tag}")
|
|
EOF
|
|
api POST "image/tags" "{\"file_name\":\"$path\",\"tag_name\":\"$tag\"}" >/dev/null \
|
|
|| log "warn: failed to tag $path"
|
|
done
|
|
touch "$STATE/.tags_seeded"
|
|
}
|
|
|
|
cmd_start() {
|
|
is_running && { log "already running at $URL"; return; }
|
|
[ -f "$STATE/.env" ] || cmd_reset
|
|
[ -x "$BIN_DIR/$EXE_NAME" ] || cmd_build
|
|
log "starting $EXE_NAME on $E2E_BIND:$E2E_PORT"
|
|
# cwd = state dir: the server reads .env from its working directory.
|
|
# Redirect the whole subshell, not just the exe: an inherited stdout
|
|
# would keep a caller's pipe (`e2e.sh up | tail`) open until shutdown.
|
|
(cd "$STATE" && exec setsid nohup "$BIN_DIR/$EXE_NAME") </dev/null >>"$LOG" 2>&1 &
|
|
disown
|
|
for _ in $(seq 1 60); do
|
|
[ -n "$(login)" ] && break
|
|
is_running || die "server exited; see $LOG"
|
|
sleep 1
|
|
done
|
|
[ -n "$(login)" ] || die "server did not accept logins within 60s; see $LOG"
|
|
|
|
[ -f "$STATE/.tags_seeded" ] || { log "seeding tags"; seed_tags; }
|
|
|
|
local want have=0
|
|
want="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["counts"]["indexed"])' "$STATE/manifest.json")"
|
|
log "waiting for ingest ($want indexable fixtures)"
|
|
for _ in $(seq 1 90); do
|
|
have="$(indexed_count)"
|
|
[ "$have" -ge "$want" ] && break
|
|
sleep 2
|
|
done
|
|
if [ "$have" -ge "$want" ]; then
|
|
log "ready: $have files indexed"
|
|
else
|
|
log "warn: only $have/$want indexed after 180s (still ingesting?); see $LOG"
|
|
fi
|
|
cmd_info
|
|
}
|
|
|
|
cmd_stop() {
|
|
if is_running; then
|
|
log "stopping $EXE_NAME"
|
|
if [ "$WSL" = 1 ]; then
|
|
taskkill.exe /IM "$EXE_NAME" /F >/dev/null
|
|
else
|
|
pkill -x "$EXE_NAME"
|
|
fi
|
|
for _ in $(seq 1 20); do is_running || break; sleep 0.5; done
|
|
else
|
|
log "not running"
|
|
fi
|
|
}
|
|
|
|
cmd_status() {
|
|
if is_running; then
|
|
echo "running $URL (indexed: $(indexed_count))"
|
|
else
|
|
echo "stopped"
|
|
fi
|
|
}
|
|
|
|
cmd_info() {
|
|
cat <<EOF
|
|
|
|
URL (browser / curl): $URL
|
|
URL (Android emulator): http://10.0.2.2:$E2E_PORT
|
|
URL (USB device, after reverse): adb reverse tcp:$E2E_PORT tcp:$E2E_PORT -> $URL
|
|
Login: $E2E_USER / $E2E_PASS
|
|
Manifest (expected content): $STATE/manifest.json
|
|
Log: $LOG
|
|
|
|
EOF
|
|
}
|
|
|
|
cmd_help() {
|
|
cat <<EOF
|
|
Usage: dev/e2e/e2e.sh <command>
|
|
|
|
up build (if needed) + fixtures (if needed) + reset + start
|
|
build cargo build into target/e2e and install the e2e exe copies
|
|
fixtures [--force] [--import DIR]
|
|
generate the pristine fixture library (--import copies a
|
|
real folder in as media/imported/; the source is read-only)
|
|
reset stop, wipe db/thumbs/hls, re-copy media, recreate user
|
|
start | stop | restart | status
|
|
check verify every fixture's displayed capture time (wall_clock)
|
|
on metadata, memories, gps-summary and photos/exif against
|
|
the manifest, plus photos/exif date-range edges (#152)
|
|
token print a fresh JWT for the e2e user
|
|
api METHOD PATH [JSON]
|
|
authenticated request, e.g. api GET 'photos?path=&recursive=true'
|
|
logs tail -f the server log
|
|
info connection details
|
|
|
|
Env: E2E_PORT (8090), E2E_BIND (127.0.0.1), E2E_USER, E2E_PASS.
|
|
Extra server env: dev/e2e/env.local (appended to the generated .env on reset).
|
|
EOF
|
|
}
|
|
|
|
case "${1:-help}" in
|
|
up) shift; [ -x "$BIN_DIR/$EXE_NAME" ] || cmd_build; cmd_fixtures; cmd_reset; cmd_start ;;
|
|
build) cmd_build ;;
|
|
fixtures) shift; cmd_fixtures "$@" ;;
|
|
reset) cmd_reset ;;
|
|
start) cmd_start ;;
|
|
stop) cmd_stop ;;
|
|
restart) cmd_stop; cmd_start ;;
|
|
status) cmd_status ;;
|
|
token) login ;;
|
|
check) E2E_TOKEN="$(login)" python3 "$HERE/check_wall_clock.py" "$STATE/manifest.json" ;;
|
|
api) shift; api "$@" ;;
|
|
logs) tail -f "$LOG" ;;
|
|
info) cmd_info ;;
|
|
help|-h|--help) cmd_help ;;
|
|
*) die "unknown command: $1 (try help)" ;;
|
|
esac
|