chore(e2e): run the e2e dev server on native Linux
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>
This commit is contained in:
1 parent
50dd9aa016
commit
b7943be022
3 files changed
+92
-28
No files matched your search
Regular → Executable
File mode changed.
Regular → Executable
+39
-21
@@ -3,14 +3,14 @@
|
||||
#
|
||||
# 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.exe, 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.
|
||||
# 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.
|
||||
#
|
||||
# Written for 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.
|
||||
# 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
|
||||
@@ -21,7 +21,14 @@ WORK="$HERE/.work"
|
||||
FIXTURES="$WORK/fixtures"
|
||||
STATE="$WORK/state"
|
||||
BIN_DIR="$WORK/bin"
|
||||
EXE_NAME="image-api-e2e.exe"
|
||||
|
||||
# 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}"
|
||||
@@ -33,23 +40,30 @@ E2E_USER="${E2E_USER:-e2e}"
|
||||
E2E_PASS="${E2E_PASS:-E2eTest#2026pass}"
|
||||
URL="http://127.0.0.1:$E2E_PORT"
|
||||
|
||||
win() { wslpath -w "$1"; }
|
||||
# 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() {
|
||||
tasklist.exe /FI "IMAGENAME eq $EXE_NAME" /NH 2>/dev/null | grep -qi "$EXE_NAME"
|
||||
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.exe build --target-dir target/e2e --bin image-api --bin create_user)
|
||||
(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.exe" "$BIN_DIR/$EXE_NAME"
|
||||
cp "$REPO/target/e2e/debug/create_user.exe" "$BIN_DIR/create_user.exe"
|
||||
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
|
||||
}
|
||||
|
||||
@@ -74,7 +88,7 @@ cmd_fixtures() {
|
||||
write_env() {
|
||||
cat > "$STATE/.env" <<EOF
|
||||
# Generated by dev/e2e/e2e.sh — edit dev/e2e/env.local instead (appended below).
|
||||
DATABASE_URL='$(win "$STATE")\\e2e.db'
|
||||
DATABASE_URL='$(win_join "$STATE" e2e.db)'
|
||||
BASE_PATH='$(win "$STATE/media")'
|
||||
THUMBNAILS='$(win "$STATE/thumbs")'
|
||||
VIDEO_PATH='$(win "$STATE/hls")'
|
||||
@@ -96,7 +110,7 @@ WATCH_QUICK_INTERVAL_SECONDS=3
|
||||
WATCH_FULL_INTERVAL_SECONDS=60
|
||||
EXCLUDED_DIRS=/private
|
||||
HLS_CONCURRENCY=2
|
||||
USER_NAME=E2E Tester
|
||||
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
|
||||
@@ -118,12 +132,12 @@ cmd_reset() {
|
||||
cp -a "$FIXTURES/media" "$STATE/media"
|
||||
cp "$FIXTURES/manifest.json" "$STATE/manifest.json"
|
||||
write_env
|
||||
[ -x "$BIN_DIR/create_user.exe" ] || cmd_build
|
||||
(cd "$STATE" && "$BIN_DIR/create_user.exe" "$E2E_USER" "$E2E_PASS")
|
||||
[ -x "$BIN_DIR/create_user$EXT" ] || cmd_build
|
||||
(cd "$STATE" && "$BIN_DIR/create_user$EXT" "$E2E_USER" "$E2E_PASS")
|
||||
}
|
||||
|
||||
login() {
|
||||
curl.exe -sf -X POST "$URL/login" -H "Content-Type: application/json" \
|
||||
$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
|
||||
}
|
||||
@@ -131,7 +145,7 @@ login() {
|
||||
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.exe -sf -X "$1" "$URL/$2" -H "Authorization: Bearer $tok" \
|
||||
$CURL -sf -X "$1" "$URL/$2" -H "Authorization: Bearer $tok" \
|
||||
-H "Content-Type: application/json" ${3:+-d "$3"} </dev/null
|
||||
}
|
||||
|
||||
@@ -192,7 +206,11 @@ cmd_start() {
|
||||
cmd_stop() {
|
||||
if is_running; then
|
||||
log "stopping $EXE_NAME"
|
||||
taskkill.exe /IM "$EXE_NAME" /F >/dev/null
|
||||
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"
|
||||
@@ -210,7 +228,7 @@ cmd_status() {
|
||||
cmd_info() {
|
||||
cat <<EOF
|
||||
|
||||
URL (Windows browser / curl.exe): $URL
|
||||
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
|
||||
|
||||
Regular → Executable
+53
-7
@@ -8,8 +8,9 @@ in a single exiftool batch. A `manifest.json` next to the media records
|
||||
what was generated and what the server should derive from it, so tests
|
||||
can assert against it instead of hardcoding counts.
|
||||
|
||||
The harness runs in WSL but the tools (ffmpeg.exe / exiftool.exe) are
|
||||
Windows binaries, so every path handed to them goes through `wslpath -w`.
|
||||
Runs on native Linux with ffmpeg / exiftool, or in WSL driving the Windows
|
||||
binaries (ffmpeg.exe / exiftool.exe), in which case every path handed to
|
||||
them goes through `wslpath -w`.
|
||||
|
||||
Usage: gen_fixtures.py <out_dir> [--import DIR]
|
||||
<out_dir>/media/ the pristine library (never served directly)
|
||||
@@ -30,11 +31,48 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
FFMPEG = shutil.which("ffmpeg.exe") or shutil.which("ffmpeg")
|
||||
|
||||
|
||||
def _find_ffmpeg() -> str | None:
|
||||
"""The first ffmpeg that can burn labels in (has `drawtext`, i.e. was
|
||||
built with freetype). E2E_FFMPEG overrides. Minimal builds earlier on
|
||||
PATH (e.g. a custom one in ~/bin) often lack it."""
|
||||
if os.environ.get("E2E_FFMPEG"):
|
||||
return os.environ["E2E_FFMPEG"]
|
||||
candidates = [shutil.which("ffmpeg.exe"), shutil.which("ffmpeg"), "/usr/bin/ffmpeg"]
|
||||
for cand in dict.fromkeys(c for c in candidates if c and os.path.exists(c)):
|
||||
try:
|
||||
filters = subprocess.run(
|
||||
[cand, "-hide_banner", "-filters"], capture_output=True, text=True
|
||||
).stdout
|
||||
except OSError:
|
||||
continue
|
||||
if " drawtext " in filters:
|
||||
return cand
|
||||
return None
|
||||
|
||||
|
||||
FFMPEG = _find_ffmpeg()
|
||||
EXIFTOOL = shutil.which("exiftool.exe") or shutil.which("exiftool")
|
||||
# drawtext needs an explicit font on Windows ffmpeg builds. The colon after
|
||||
# the drive letter must be escaped inside a filter argument.
|
||||
FONT = "C\\:/Windows/Fonts/consola.ttf"
|
||||
|
||||
def _font() -> str:
|
||||
"""A monospace font for ffmpeg's drawtext, as a filter-escaped path."""
|
||||
if FFMPEG and FFMPEG.endswith(".exe"):
|
||||
# Windows ffmpeg needs an explicit font; the colon after the drive
|
||||
# letter must be escaped inside a filter argument.
|
||||
return "C\\:/Windows/Fonts/consola.ttf"
|
||||
try:
|
||||
found = subprocess.check_output(
|
||||
["fc-match", "-f", "%{file}", "monospace"], text=True
|
||||
).strip()
|
||||
if found:
|
||||
return found
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
pass
|
||||
return "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
|
||||
|
||||
|
||||
FONT = _font()
|
||||
|
||||
# The owner's display zone and the server zone the e2e env pins (TZ=...).
|
||||
# Naive fixture datetimes without GPS are walls in this zone.
|
||||
@@ -85,6 +123,10 @@ class Item:
|
||||
|
||||
|
||||
def win(p: Path) -> str:
|
||||
"""`p` as the external tools see it: a Windows path for WSL-driven
|
||||
.exe tools, unchanged for native ones."""
|
||||
if not (FFMPEG or "").endswith(".exe"):
|
||||
return str(p)
|
||||
return subprocess.check_output(["wslpath", "-w", str(p)], text=True).strip()
|
||||
|
||||
|
||||
@@ -288,7 +330,11 @@ def main() -> int:
|
||||
help="also copy this folder (read-only source) into media/imported/")
|
||||
a = ap.parse_args()
|
||||
if not FFMPEG or not EXIFTOOL:
|
||||
print("ffmpeg(.exe) and exiftool(.exe) must be on PATH", file=sys.stderr)
|
||||
print(
|
||||
"ffmpeg(.exe) with the drawtext filter (built with freetype) and "
|
||||
"exiftool(.exe) must be on PATH; set E2E_FFMPEG to pick an ffmpeg",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
out: Path = a.out_dir.resolve()
|
||||
|
||||
Reference in new issue
Block a user