Merge pull request 'chore(e2e): run the e2e dev server on native Linux' (#172) from feat/e2e-native-linux into master
Reviewed-on: #172
This commit was merged in pull request #172.
This commit is contained in:
commit
42897990d8
4 files changed
+97
-28
No files matched your search
@@ -14,6 +14,11 @@ Log in from the app with **server** `http://10.0.2.2:8090` (Android
|
|||||||
emulator) or `http://127.0.0.1:8090` (Expo web / `adb reverse tcp:8090 tcp:8090`),
|
emulator) or `http://127.0.0.1:8090` (Expo web / `adb reverse tcp:8090 tcp:8090`),
|
||||||
**user** `e2e`, **password** `E2eTest#2026pass`.
|
**user** `e2e`, **password** `E2eTest#2026pass`.
|
||||||
|
|
||||||
|
Runs on native Linux (cargo, curl, ffmpeg with `drawtext`, exiftool) or in
|
||||||
|
WSL driving the Windows toolchain; the script detects which. If port 8090 is
|
||||||
|
taken, pick another with `E2E_PORT=8091` (the app's `drive-app` skill reads
|
||||||
|
the same variable).
|
||||||
|
|
||||||
## Isolation
|
## Isolation
|
||||||
|
|
||||||
It runs next to a real local server without touching it:
|
It runs next to a real local server without touching it:
|
||||||
|
|||||||
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
|
# 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),
|
# 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
|
# its own process name (image-api-e2e, so `stop` can never touch the real
|
||||||
# real server), its own port, database, thumbnails and HLS output. The
|
# server), its own port, database, thumbnails and HLS output. The media
|
||||||
# media library is a *copy* of the pristine fixtures, so tests may upload,
|
# library is a *copy* of the pristine fixtures, so tests may upload, move
|
||||||
# move and delete freely; `reset` restores the known baseline.
|
# and delete freely; `reset` restores the known baseline.
|
||||||
#
|
#
|
||||||
# Written for WSL driving the Windows toolchain (cargo.exe, curl.exe,
|
# Works on native Linux, and in WSL driving the Windows toolchain
|
||||||
# ffmpeg.exe). WSL2's default NAT networking can't reach Windows'
|
# (cargo.exe, curl.exe, ffmpeg.exe). WSL2's default NAT networking can't
|
||||||
# 127.0.0.1, hence curl.exe for every HTTP call.
|
# reach Windows' 127.0.0.1, hence curl.exe for every HTTP call there.
|
||||||
#
|
#
|
||||||
# Usage: dev/e2e/e2e.sh <command> (see `help`)
|
# Usage: dev/e2e/e2e.sh <command> (see `help`)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -21,7 +21,14 @@ WORK="$HERE/.work"
|
|||||||
FIXTURES="$WORK/fixtures"
|
FIXTURES="$WORK/fixtures"
|
||||||
STATE="$WORK/state"
|
STATE="$WORK/state"
|
||||||
BIN_DIR="$WORK/bin"
|
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"
|
LOG="$WORK/server.log"
|
||||||
|
|
||||||
E2E_PORT="${E2E_PORT:-8090}"
|
E2E_PORT="${E2E_PORT:-8090}"
|
||||||
@@ -33,23 +40,30 @@ E2E_USER="${E2E_USER:-e2e}"
|
|||||||
E2E_PASS="${E2E_PASS:-E2eTest#2026pass}"
|
E2E_PASS="${E2E_PASS:-E2eTest#2026pass}"
|
||||||
URL="http://127.0.0.1:$E2E_PORT"
|
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; }
|
log() { printf '\033[1;34m[e2e]\033[0m %s\n' "$*" >&2; }
|
||||||
die() { printf '\033[1;31m[e2e]\033[0m %s\n' "$*" >&2; exit 1; }
|
die() { printf '\033[1;31m[e2e]\033[0m %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
is_running() {
|
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() {
|
cmd_build() {
|
||||||
log "building bins into target/e2e"
|
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"
|
mkdir -p "$BIN_DIR"
|
||||||
# Run from a copy so rebuilding never fights a running (locked) exe.
|
# Run from a copy so rebuilding never fights a running (locked) exe.
|
||||||
local was_running=0
|
local was_running=0
|
||||||
if is_running; then was_running=1; cmd_stop; fi
|
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/image-api$EXT" "$BIN_DIR/$EXE_NAME"
|
||||||
cp "$REPO/target/e2e/debug/create_user.exe" "$BIN_DIR/create_user.exe"
|
cp "$REPO/target/e2e/debug/create_user$EXT" "$BIN_DIR/create_user$EXT"
|
||||||
if [ "$was_running" = 1 ]; then cmd_start; fi
|
if [ "$was_running" = 1 ]; then cmd_start; fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +88,7 @@ cmd_fixtures() {
|
|||||||
write_env() {
|
write_env() {
|
||||||
cat > "$STATE/.env" <<EOF
|
cat > "$STATE/.env" <<EOF
|
||||||
# Generated by dev/e2e/e2e.sh — edit dev/e2e/env.local instead (appended below).
|
# 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")'
|
BASE_PATH='$(win "$STATE/media")'
|
||||||
THUMBNAILS='$(win "$STATE/thumbs")'
|
THUMBNAILS='$(win "$STATE/thumbs")'
|
||||||
VIDEO_PATH='$(win "$STATE/hls")'
|
VIDEO_PATH='$(win "$STATE/hls")'
|
||||||
@@ -96,7 +110,7 @@ WATCH_QUICK_INTERVAL_SECONDS=3
|
|||||||
WATCH_FULL_INTERVAL_SECONDS=60
|
WATCH_FULL_INTERVAL_SECONDS=60
|
||||||
EXCLUDED_DIRS=/private
|
EXCLUDED_DIRS=/private
|
||||||
HLS_CONCURRENCY=2
|
HLS_CONCURRENCY=2
|
||||||
USER_NAME=E2E Tester
|
USER_NAME='E2E Tester'
|
||||||
REEL_PREGEN_ENABLED=0
|
REEL_PREGEN_ENABLED=0
|
||||||
REEL_CACHE_SWEEP_ENABLED=0
|
REEL_CACHE_SWEEP_ENABLED=0
|
||||||
# AI / sibling services are off by default: point Ollama at a closed port so
|
# 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 -a "$FIXTURES/media" "$STATE/media"
|
||||||
cp "$FIXTURES/manifest.json" "$STATE/manifest.json"
|
cp "$FIXTURES/manifest.json" "$STATE/manifest.json"
|
||||||
write_env
|
write_env
|
||||||
[ -x "$BIN_DIR/create_user.exe" ] || cmd_build
|
[ -x "$BIN_DIR/create_user$EXT" ] || cmd_build
|
||||||
(cd "$STATE" && "$BIN_DIR/create_user.exe" "$E2E_USER" "$E2E_PASS")
|
(cd "$STATE" && "$BIN_DIR/create_user$EXT" "$E2E_USER" "$E2E_PASS")
|
||||||
}
|
}
|
||||||
|
|
||||||
login() {
|
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 \
|
-d "{\"username\":\"$E2E_USER\",\"password\":\"$E2E_PASS\"}" 2>/dev/null \
|
||||||
| grep -o '"token":"[^"]*"' | cut -d'"' -f4
|
| 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)
|
api() { # api METHOD PATH [JSON]; reuses $E2E_TOKEN when set (login is a bcrypt verify)
|
||||||
local tok="${E2E_TOKEN:-$(login)}"
|
local tok="${E2E_TOKEN:-$(login)}"
|
||||||
# </dev/null: curl.exe otherwise drains stdin, eating a caller's `while read` input.
|
# </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
|
-H "Content-Type: application/json" ${3:+-d "$3"} </dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +206,11 @@ cmd_start() {
|
|||||||
cmd_stop() {
|
cmd_stop() {
|
||||||
if is_running; then
|
if is_running; then
|
||||||
log "stopping $EXE_NAME"
|
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
|
for _ in $(seq 1 20); do is_running || break; sleep 0.5; done
|
||||||
else
|
else
|
||||||
log "not running"
|
log "not running"
|
||||||
@@ -210,7 +228,7 @@ cmd_status() {
|
|||||||
cmd_info() {
|
cmd_info() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|
||||||
URL (Windows browser / curl.exe): $URL
|
URL (browser / curl): $URL
|
||||||
URL (Android emulator): http://10.0.2.2:$E2E_PORT
|
URL (Android emulator): http://10.0.2.2:$E2E_PORT
|
||||||
URL (USB device, after reverse): adb reverse tcp:$E2E_PORT tcp:$E2E_PORT -> $URL
|
URL (USB device, after reverse): adb reverse tcp:$E2E_PORT tcp:$E2E_PORT -> $URL
|
||||||
Login: $E2E_USER / $E2E_PASS
|
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
|
what was generated and what the server should derive from it, so tests
|
||||||
can assert against it instead of hardcoding counts.
|
can assert against it instead of hardcoding counts.
|
||||||
|
|
||||||
The harness runs in WSL but the tools (ffmpeg.exe / exiftool.exe) are
|
Runs on native Linux with ffmpeg / exiftool, or in WSL driving the Windows
|
||||||
Windows binaries, so every path handed to them goes through `wslpath -w`.
|
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]
|
Usage: gen_fixtures.py <out_dir> [--import DIR]
|
||||||
<out_dir>/media/ the pristine library (never served directly)
|
<out_dir>/media/ the pristine library (never served directly)
|
||||||
@@ -30,11 +31,48 @@ from dataclasses import dataclass, field
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from zoneinfo import ZoneInfo
|
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")
|
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.
|
def _font() -> str:
|
||||||
FONT = "C\\:/Windows/Fonts/consola.ttf"
|
"""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=...).
|
# The owner's display zone and the server zone the e2e env pins (TZ=...).
|
||||||
# Naive fixture datetimes without GPS are walls in this zone.
|
# Naive fixture datetimes without GPS are walls in this zone.
|
||||||
@@ -85,6 +123,10 @@ class Item:
|
|||||||
|
|
||||||
|
|
||||||
def win(p: Path) -> str:
|
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()
|
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/")
|
help="also copy this folder (read-only source) into media/imported/")
|
||||||
a = ap.parse_args()
|
a = ap.parse_args()
|
||||||
if not FFMPEG or not EXIFTOOL:
|
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
|
return 1
|
||||||
|
|
||||||
out: Path = a.out_dir.resolve()
|
out: Path = a.out_dir.resolve()
|
||||||
|
|||||||
Reference in new issue
Block a user