From b7943be022fbc168824f5f32031f28535249a1c8 Mon Sep 17 00:00:00 2001 From: Cameron Cordes Date: Thu, 15 Oct 2026 21:27:47 -0400 Subject: [PATCH] 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 --- dev/e2e/check_wall_clock.py | 0 dev/e2e/e2e.sh | 60 ++++++++++++++++++++++++------------- dev/e2e/gen_fixtures.py | 60 ++++++++++++++++++++++++++++++++----- 3 files changed, 92 insertions(+), 28 deletions(-) mode change 100644 => 100755 dev/e2e/check_wall_clock.py mode change 100644 => 100755 dev/e2e/e2e.sh mode change 100644 => 100755 dev/e2e/gen_fixtures.py diff --git a/dev/e2e/check_wall_clock.py b/dev/e2e/check_wall_clock.py old mode 100644 new mode 100755 diff --git a/dev/e2e/e2e.sh b/dev/e2e/e2e.sh old mode 100644 new mode 100755 index 74eb330..285982c --- a/dev/e2e/e2e.sh +++ b/dev/e2e/e2e.sh @@ -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 (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" </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 + 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 < $URL Login: $E2E_USER / $E2E_PASS diff --git a/dev/e2e/gen_fixtures.py b/dev/e2e/gen_fixtures.py old mode 100644 new mode 100755 index bb2b624..95dd114 --- a/dev/e2e/gen_fixtures.py +++ b/dev/e2e/gen_fixtures.py @@ -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 [--import 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()