test: bundled-mode supervisor integration test (faked AppImage layout)
Stages a fake AppImage layout in /tmp without a real .AppImage build:
$ROOT/usr/bin/<app> — copy of the host binary
$ROOT/usr/bin/frankenphp — symlink to system frankenphp
$ROOT/usr/share/<app>/symfony — staged --no-dev composer copy
$ROOT/usr/share/<app>/Caddyfile
The staged Symfony tree is `chmod -R a-w` to actually exercise the
read-only-mount cache/log redirect (Kernel::getCacheDir +
APP_CACHE_DIR override) — without the override, Symfony would fail
to mkdir var/cache/prod and migrations would error out.
Then runs the host with BRIDGE_URL unset (forces bundled mode), polls
/healthz, and asserts:
- status=ok + bundle="PhpQml\Bridge\Publisher" — proves the
HealthController deep-load (predecessor commit) actually
autowired Publisher, i.e. BridgeBundle is reachable.
- User data dir's var/cache exists — APP_CACHE_DIR override fired.
- Staged tree's var/cache/prod is empty — Symfony didn't write into
the read-only mount.
Together this catches every v0.1.0 shakedown bug in CI:
- doubled bin/frankenphp path (resolveFrankenphpBin)
- composer path-repo symlink dangling (staging-symfony's symlink:false sed)
- read-only mount cache failure (Kernel + supervisor env-vars)
- bundle autoload broken (HealthController canary)
Implementation gotcha (caught during dev): the host binary must be
COPIED into the staged layout, not symlinked. Qt's
applicationDirPath() reads /proc/self/exe which dereferences
symlinks, so a symlinked host would resolve to the original build/
dir and the supervisor would hunt for frankenphp + symfony there
instead of the staged tree. Real AppImages copy the binary, mimicking
that here.
Wiring:
- examples/todo/Makefile: extracted the staging-symfony logic out
of the appimage target into its own staging-symfony target. New
integration-bundled target depends on `build` + `staging-symfony`
and runs tests/bundled-supervisor.sh. quality target now invokes
integration-bundled after the existing dev-mode integration test.
- .gitea/workflows/ci.yml: new "Bundled-mode supervisor integration
test" step right after the dev-mode integration step.
Closes the v0.1.1 follow-up "Bundled-mode integration test" tracked
in PLAN.md §13.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -95,3 +95,7 @@ jobs:
|
|||||||
- name: Bridge-integration test (HTTP/SSE round-trip + crash-recover)
|
- name: Bridge-integration test (HTTP/SSE round-trip + crash-recover)
|
||||||
working-directory: examples/todo
|
working-directory: examples/todo
|
||||||
run: ./tests/integration.sh
|
run: ./tests/integration.sh
|
||||||
|
|
||||||
|
- name: Bundled-mode supervisor integration test
|
||||||
|
working-directory: examples/todo
|
||||||
|
run: make integration-bundled
|
||||||
|
|||||||
@@ -40,12 +40,16 @@ clean: ## Remove build artefacts
|
|||||||
integration: ## Run the bridge-integration test (FrankenPHP boot + HTTP/SSE round-trip + crash-recover)
|
integration: ## Run the bridge-integration test (FrankenPHP boot + HTTP/SSE round-trip + crash-recover)
|
||||||
./tests/integration.sh
|
./tests/integration.sh
|
||||||
|
|
||||||
|
.PHONY: integration-bundled
|
||||||
|
integration-bundled: build staging-symfony ## Bundled-mode integration test (faked AppImage layout, no .AppImage build needed)
|
||||||
|
./tests/bundled-supervisor.sh
|
||||||
|
|
||||||
.PHONY: perf
|
.PHONY: perf
|
||||||
perf: ## Run the AppImage perf smoke test (PLAN.md §11 budgets)
|
perf: ## Run the AppImage perf smoke test (PLAN.md §11 budgets)
|
||||||
./tests/perfsmoke.sh build/Todo-x86_64.AppImage
|
./tests/perfsmoke.sh build/Todo-x86_64.AppImage
|
||||||
|
|
||||||
.PHONY: appimage
|
.PHONY: staging-symfony
|
||||||
appimage: build ## Package as a single-file Linux AppImage at build/Todo-x86_64.AppImage
|
staging-symfony: ## Stage a --no-dev composer copy of symfony for AppImage / bundled-mode tests
|
||||||
# Composer install --no-dev in a staging copy of symfony so the
|
# Composer install --no-dev in a staging copy of symfony so the
|
||||||
# dev tree (with maker-bundle etc.) is left untouched.
|
# dev tree (with maker-bundle etc.) is left untouched.
|
||||||
rm -rf build/staging-symfony
|
rm -rf build/staging-symfony
|
||||||
@@ -62,6 +66,9 @@ appimage: build ## Package as a single-file Linux AppImage at build/Todo-x86_64.
|
|||||||
sed -i 's|"symlink": true|"symlink": false|' build/staging-symfony/composer.json
|
sed -i 's|"symlink": true|"symlink": false|' build/staging-symfony/composer.json
|
||||||
rm -f build/staging-symfony/composer.lock
|
rm -f build/staging-symfony/composer.lock
|
||||||
cd build/staging-symfony && composer install --no-dev --no-interaction --classmap-authoritative
|
cd build/staging-symfony && composer install --no-dev --no-interaction --classmap-authoritative
|
||||||
|
|
||||||
|
.PHONY: appimage
|
||||||
|
appimage: build staging-symfony ## Package as a single-file Linux AppImage at build/Todo-x86_64.AppImage
|
||||||
../../packaging/linux/build-appimage.sh \
|
../../packaging/linux/build-appimage.sh \
|
||||||
--app-name todo \
|
--app-name todo \
|
||||||
--host-binary $(QT_BIN) \
|
--host-binary $(QT_BIN) \
|
||||||
@@ -76,7 +83,8 @@ appimage: build ## Package as a single-file Linux AppImage at build/Todo-x86_64.
|
|||||||
@echo "AppImage built. Test with: ./build/Todo-x86_64.AppImage"
|
@echo "AppImage built. Test with: ./build/Todo-x86_64.AppImage"
|
||||||
|
|
||||||
.PHONY: quality
|
.PHONY: quality
|
||||||
quality: build ## Run PHPStan, php-cs-fixer (check), PHPUnit, qmllint, integration
|
quality: build ## Run PHPStan, php-cs-fixer (check), PHPUnit, qmllint, integration (dev + bundled)
|
||||||
cd ../../framework/php && composer quality
|
cd ../../framework/php && composer quality
|
||||||
cmake --build $(BUILD_DIR) --target all_qmllint
|
cmake --build $(BUILD_DIR) --target all_qmllint
|
||||||
./tests/integration.sh
|
./tests/integration.sh
|
||||||
|
$(MAKE) integration-bundled
|
||||||
|
|||||||
141
examples/todo/tests/bundled-supervisor.sh
Executable file
141
examples/todo/tests/bundled-supervisor.sh
Executable file
@@ -0,0 +1,141 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Bundled-mode integration test (v0.1.1).
|
||||||
|
#
|
||||||
|
# Exercises the bundled-mode supervisor codepath end-to-end *without*
|
||||||
|
# requiring a real AppImage build:
|
||||||
|
#
|
||||||
|
# - resolveFrankenphpBin (BackendConnection.cpp) — finds frankenphp
|
||||||
|
# as a sibling of the host binary at usr/bin/frankenphp.
|
||||||
|
# - resolveSymfonyDir / resolveCaddyfilePath — finds the staged
|
||||||
|
# Symfony tree + Caddyfile under usr/share/<app>/.
|
||||||
|
# - runMigrations + spawnChild — supervisor drives the doctrine
|
||||||
|
# migrate, spawns frankenphp, polls /healthz.
|
||||||
|
# - Kernel::getCacheDir / getLogDir override — Symfony writes to
|
||||||
|
# the user data dir, not the (chmod -w) staged tree.
|
||||||
|
# - HealthController deep-load — /healthz response includes a
|
||||||
|
# `bundle` field proving BridgeBundle was autoloaded.
|
||||||
|
#
|
||||||
|
# Catches the v0.1.0 shakedown bugs (doubled bin/frankenphp path,
|
||||||
|
# composer path-repo symlink dangling at runtime, read-only mount
|
||||||
|
# var/cache failure) faster than perfsmoke against a real .AppImage.
|
||||||
|
#
|
||||||
|
# Designed for `make integration-bundled`. Expects the regular
|
||||||
|
# `make build` artefacts to exist; runs `make staging-symfony`
|
||||||
|
# itself if the staged tree isn't present.
|
||||||
|
#
|
||||||
|
# Skip-conditions:
|
||||||
|
# - port 8765 already in use (don't trample a dev instance)
|
||||||
|
# - frankenphp not on PATH
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
BUILD_DIR="$APP_DIR/build/qml"
|
||||||
|
HOST_BIN="$BUILD_DIR/todo"
|
||||||
|
STAGING="$APP_DIR/build/staging-symfony"
|
||||||
|
CADDYFILE="$APP_DIR/Caddyfile"
|
||||||
|
|
||||||
|
APP_NAME=todo
|
||||||
|
PORT=8765
|
||||||
|
|
||||||
|
step() { echo "→ $*"; }
|
||||||
|
fail() { echo "✗ FAIL: $*" >&2; exit 1; }
|
||||||
|
skip() { echo "⊘ SKIP: $*" >&2; exit 0; }
|
||||||
|
|
||||||
|
# ── Pre-flight ─────────────────────────────────────────────────────────
|
||||||
|
[ -x "$HOST_BIN" ] || fail "host binary not built — run 'make build' first ($HOST_BIN)"
|
||||||
|
command -v frankenphp >/dev/null 2>&1 || skip "frankenphp not on PATH"
|
||||||
|
[ -d "$STAGING" ] || { step "no staging-symfony, building it"; (cd "$APP_DIR" && make staging-symfony >/dev/null); }
|
||||||
|
|
||||||
|
if (echo > "/dev/tcp/127.0.0.1/$PORT") 2>/dev/null; then
|
||||||
|
skip "port $PORT already in use (dev instance running?)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Stage a fake AppImage layout in a temp dir ─────────────────────────
|
||||||
|
ROOT="$(mktemp -d)"
|
||||||
|
DATA_DIR="$(mktemp -d)"
|
||||||
|
trap 'cleanup' EXIT INT TERM
|
||||||
|
|
||||||
|
PID=""
|
||||||
|
cleanup() {
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
|
||||||
|
kill -TERM "$PID" 2>/dev/null || true
|
||||||
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
kill -0 "$PID" 2>/dev/null || break
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
kill -KILL "$PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
# Restore writable so rm -rf doesn't choke.
|
||||||
|
[ -d "$ROOT/usr/share/$APP_NAME/symfony" ] && \
|
||||||
|
chmod -R u+w "$ROOT/usr/share/$APP_NAME/symfony" 2>/dev/null || true
|
||||||
|
rm -rf "$ROOT" "$DATA_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
step "stage AppImage layout at $ROOT"
|
||||||
|
mkdir -p "$ROOT/usr/bin" "$ROOT/usr/share/$APP_NAME"
|
||||||
|
# Host binary must be copied, not symlinked: Qt's applicationDirPath()
|
||||||
|
# reads /proc/self/exe which dereferences symlinks, so a symlinked host
|
||||||
|
# would resolve to the build/ dir and the supervisor would look for
|
||||||
|
# frankenphp + symfony there instead of in the staged layout. Real
|
||||||
|
# AppImages copy the binary, mimicking that here.
|
||||||
|
cp "$HOST_BIN" "$ROOT/usr/bin/$APP_NAME"
|
||||||
|
ln -s "$(command -v frankenphp)" "$ROOT/usr/bin/frankenphp"
|
||||||
|
cp -a "$STAGING/." "$ROOT/usr/share/$APP_NAME/symfony/"
|
||||||
|
cp "$CADDYFILE" "$ROOT/usr/share/$APP_NAME/Caddyfile"
|
||||||
|
|
||||||
|
# Make the staged Symfony tree read-only so the cache/log redirect is
|
||||||
|
# actually exercised — without the Kernel::getCacheDir/getLogDir override,
|
||||||
|
# Symfony tries to mkdir var/cache here and fails.
|
||||||
|
chmod -R a-w "$ROOT/usr/share/$APP_NAME/symfony"
|
||||||
|
|
||||||
|
# ── Launch the host ────────────────────────────────────────────────────
|
||||||
|
step "launch host (bundled mode, offscreen, isolated XDG dirs)"
|
||||||
|
LOG="$DATA_DIR/host.log"
|
||||||
|
env -u BRIDGE_URL \
|
||||||
|
XDG_DATA_HOME="$DATA_DIR/share" \
|
||||||
|
XDG_CACHE_HOME="$DATA_DIR/cache" \
|
||||||
|
XDG_CONFIG_HOME="$DATA_DIR/config" \
|
||||||
|
QT_QPA_PLATFORM=offscreen \
|
||||||
|
"$ROOT/usr/bin/$APP_NAME" > "$LOG" 2>&1 &
|
||||||
|
PID=$!
|
||||||
|
|
||||||
|
# ── Poll /healthz ──────────────────────────────────────────────────────
|
||||||
|
step "wait for /healthz"
|
||||||
|
DEADLINE=$(( $(date +%s) + 30 ))
|
||||||
|
HEALTHZ_BODY=""
|
||||||
|
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||||
|
if ! kill -0 "$PID" 2>/dev/null; then
|
||||||
|
sed 's/^/ /' "$LOG" >&2 || true
|
||||||
|
fail "host died during boot"
|
||||||
|
fi
|
||||||
|
if HEALTHZ_BODY="$(curl -fsS -m 1 "http://127.0.0.1:$PORT/healthz" 2>/dev/null)"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
[ -n "$HEALTHZ_BODY" ] || { sed 's/^/ /' "$LOG" >&2 || true; fail "/healthz never responded within 30s"; }
|
||||||
|
|
||||||
|
# ── Verify bundle deep-load ────────────────────────────────────────────
|
||||||
|
step "/healthz body: $HEALTHZ_BODY"
|
||||||
|
echo "$HEALTHZ_BODY" | grep -q '"status":"ok"' \
|
||||||
|
|| fail "/healthz didn't return status:ok"
|
||||||
|
echo "$HEALTHZ_BODY" | grep -q '"bundle":"PhpQml\\\\Bridge\\\\Publisher"' \
|
||||||
|
|| fail "/healthz missing bundle field — HealthController deep-load broken"
|
||||||
|
|
||||||
|
# ── Verify the cache/log redirect actually fired ───────────────────────
|
||||||
|
step "verify Symfony wrote cache to user data dir, not the read-only staging"
|
||||||
|
# Qt's QStandardPaths::AppDataLocation on Linux is $XDG_DATA_HOME/<org>/<app>,
|
||||||
|
# org="php-qml" comes from main.cpp setOrganizationName, app="todo" from setApplicationName.
|
||||||
|
USER_DATA="$DATA_DIR/share/php-qml/$APP_NAME"
|
||||||
|
[ -d "$USER_DATA/var/cache" ] \
|
||||||
|
|| fail "user-data var/cache missing at $USER_DATA — APP_CACHE_DIR override didn't fire"
|
||||||
|
# And not into the staged tree (which is chmod -w anyway):
|
||||||
|
if [ -d "$ROOT/usr/share/$APP_NAME/symfony/var/cache/prod" ] && \
|
||||||
|
[ "$(ls -A "$ROOT/usr/share/$APP_NAME/symfony/var/cache/prod" 2>/dev/null)" ]; then
|
||||||
|
fail "Symfony wrote into the read-only staging tree — Kernel::getCacheDir override broken"
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "All bundled-supervisor assertions passed."
|
||||||
Reference in New Issue
Block a user