From 54be9bcc9dd5be09156ce767b3ff01d3c1b037ef Mon Sep 17 00:00:00 2001 From: Andy Miller Date: Mon, 13 Apr 2026 18:57:39 +0100 Subject: [PATCH] Add E2E test server and runner for GPM family-gate testing --- bin/gpm-cache-inject | 2 +- bin/gpm-e2e-test | 162 ++++++++++++++++++++++++++++++++++++++++ bin/gpm-test-server.php | 133 +++++++++++++++++++++++++++++++++ 3 files changed, 296 insertions(+), 1 deletion(-) create mode 100755 bin/gpm-e2e-test create mode 100755 bin/gpm-test-server.php diff --git a/bin/gpm-cache-inject b/bin/gpm-cache-inject index 65f5b0d51..01752ccb8 100755 --- a/bin/gpm-cache-inject +++ b/bin/gpm-cache-inject @@ -63,7 +63,7 @@ $gravVersion = 'unknown'; if (file_exists($changelogFile)) { $first = fopen($changelogFile, 'r'); while (($line = fgets($first)) !== false) { - if (preg_match('/^#\s+([\d]+\.[\d]+\.[\d]+(?:[-\w.]+)?)/', $line, $m)) { + if (preg_match('/^#\s+v?([\d]+\.[\d]+\.[\d]+(?:[-\w.]+)?)/', $line, $m)) { $gravVersion = $m[1]; break; } diff --git a/bin/gpm-e2e-test b/bin/gpm-e2e-test new file mode 100755 index 000000000..7abeb97f2 --- /dev/null +++ b/bin/gpm-e2e-test @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# +# E2E test for family-aware GPM upgrade gate. +# Run from the Grav install root: bash bin/gpm-e2e-test +# +# Requires: php (CLI), bin/gpm-test-server.php, updated Upgrader.php + +set -uo pipefail + +GRAV_DIR="$(cd "$(dirname "$0")/.." && pwd)" +PORT=8043 +SERVER_PID="" +MODE_FILE=/tmp/gpm-test-mode + +# ── Colours ─────────────────────────────────────────────────────────────────── +GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; RESET='\033[0m' +PASS=0; FAIL=0 + +pass() { echo -e " ${GREEN}PASS${RESET} $1"; PASS=$((PASS + 1)); } +fail() { echo -e " ${RED}FAIL${RESET} $1"; FAIL=$((FAIL + 1)); } +section() { echo -e "\n${YELLOW}── $1 ──${RESET}"; } + +# ── Server lifecycle ────────────────────────────────────────────────────────── +start_server() { + stop_server + + # Kill anything still bound to the port + lsof -ti tcp:$PORT | xargs kill -9 2>/dev/null || true + sleep 0.3 + + php -S "localhost:$PORT" \ + -t "$GRAV_DIR" \ + "$GRAV_DIR/bin/gpm-test-server.php" \ + > /tmp/gpm-test-server.log 2>&1 & + SERVER_PID=$! + sleep 0.5 + + # Sanity check + if ! curl -sf "http://localhost:$PORT/grav.json?v=1.8.0-beta.25&stable=1" > /dev/null; then + echo "ERROR: test server failed to start. Log:" >&2 + cat /tmp/gpm-test-server.log >&2 + exit 1 + fi +} + +stop_server() { + if [ -n "${SERVER_PID:-}" ]; then + kill "$SERVER_PID" 2>/dev/null || true + SERVER_PID="" + fi +} + +set_mode() { echo "$1" > "$MODE_FILE"; } + +trap 'stop_server; rm -f "$MODE_FILE"' EXIT + +# ── Helpers ─────────────────────────────────────────────────────────────────── +clear_gpm_cache() { + rm -rf "$GRAV_DIR/cache/gpm" +} + +# Run selfupgrade non-interactively: pipe two empty lines (defaults both +# changelog and upgrade prompts to "No") so the command exits cleanly +# without trying to download anything. +run_selfupgrade() { + printf '\n\n' | php "$GRAV_DIR/bin/gpm" selfupgrade 2>&1 || true +} + +assert_contains() { + local output="$1" pattern="$2" label="$3" + if echo "$output" | grep -qiE "$pattern"; then + pass "$label" + else + fail "$label" + echo " Expected (regex): $pattern" + echo " Actual output:" + echo "$output" | sed 's/^/ /' + fi +} + +assert_not_contains() { + local output="$1" pattern="$2" label="$3" + if echo "$output" | grep -qiE "$pattern"; then + fail "$label" + echo " Should NOT match (regex): $pattern" + echo " Actual output:" + echo "$output" | sed 's/^/ /' + else + pass "$label" + fi +} + +# ───────────────────────────────────────────────────────────────────────────── +GRAV_VERSION=$(grep -m1 '^# v' "$GRAV_DIR/CHANGELOG.md" | sed 's/^# v//') + +echo "" +echo "Install : $GRAV_DIR" +echo "Version : $GRAV_VERSION" +echo "" + +start_server + +# ═════════════════════════════════════════════════════════════════════════════ +# SCENARIO A — old server behaviour (no family filter, returns 2.0.1 globally) +# Client gate in Upgrader.php must block the cross-family upgrade. +# ═════════════════════════════════════════════════════════════════════════════ +section "Scenario A: Old server returns 2.0.1 — client must block" + +set_mode global +clear_gpm_cache +A=$(run_selfupgrade) + +# Server log so we can see what was actually served +echo " Server: $(cat /tmp/gpm-test-server.log | tail -1)" + +assert_contains "$A" "already running the latest" \ + "selfupgrade reports no upgrade available" +assert_not_contains "$A" "Preparing to upgrade|Downloading" \ + "selfupgrade does not attempt to download" + +# ═════════════════════════════════════════════════════════════════════════════ +# SCENARIO B — new server behaviour (family filter, returns 1.8.0-beta.29) +# selfupgrade should recognise the 1.8 update and offer it. +# ═════════════════════════════════════════════════════════════════════════════ +section "Scenario B: Family server returns 1.8.0-beta.29 — upgrade should be offered" + +set_mode family +clear_gpm_cache +B=$(run_selfupgrade) + +echo " Server: $(cat /tmp/gpm-test-server.log | tail -1)" + +assert_contains "$B" "1\.8\.0-beta\.29.*is now available|is now available.*1\.8\.0-beta\.29" \ + "selfupgrade reports 1.8.0-beta.29 is available" +assert_not_contains "$B" "already running the latest" \ + "selfupgrade does not report 'already up to date'" +assert_not_contains "$B" "2\.0\." \ + "selfupgrade does not mention 2.0.x as a target" + +# ═════════════════════════════════════════════════════════════════════════════ +# SCENARIO C — server returns same version (no upgrade needed) +# ═════════════════════════════════════════════════════════════════════════════ +section "Scenario C: Server returns same version — already up to date" + +set_mode same +clear_gpm_cache +C=$(run_selfupgrade) + +echo " Server: $(cat /tmp/gpm-test-server.log | tail -1)" + +assert_contains "$C" "already running the latest" \ + "selfupgrade reports already up to date" + +# ───────────────────────────────────────────────────────────────────────────── +echo "" +if [ "$FAIL" -eq 0 ]; then + echo -e "${GREEN}All $PASS tests passed.${RESET}" + exit 0 +else + echo -e "${RED}$FAIL test(s) failed, $PASS passed.${RESET}" + exit 1 +fi diff --git a/bin/gpm-test-server.php b/bin/gpm-test-server.php new file mode 100755 index 000000000..844151ae5 --- /dev/null +++ b/bin/gpm-test-server.php @@ -0,0 +1,133 @@ + /tmp/gpm-test-mode (default) return latest in client's major.minor family + * echo global > /tmp/gpm-test-mode return globally latest (old server, no family filter) + * echo same > /tmp/gpm-test-mode return same version as client (no upgrade) + */ + +$v = $_GET['v'] ?? ''; +$testing = isset($_GET['testing']); +$mode = trim(@file_get_contents('/tmp/gpm-test-mode') ?: 'family'); + +// Simulated release list — newest first, mirrors a real GitHub releases page +$releases = [ + ['version' => '2.0.1', 'prerelease' => false], + ['version' => '2.0.0', 'prerelease' => false], + ['version' => '1.8.0-beta.29', 'prerelease' => true], + ['version' => '1.8.0-beta.28', 'prerelease' => true], + ['version' => '1.8.0-beta.25', 'prerelease' => true], + ['version' => '1.7.51', 'prerelease' => false], + ['version' => '1.7.50', 'prerelease' => false], +]; + +function extractFamily(string $ver): string +{ + $parts = explode('.', ltrim($ver, 'v')); + return ($parts[0] ?? '0') . '.' . ($parts[1] ?? '0'); +} + +/** + * Find the latest release in $family. + * On stable channel: prefer a stable release, but fall back to latest prerelease if no stable + * exists in that family yet (e.g. 1.8 is still all-beta). + * On testing channel: just return the newest. + */ +function findLatestInFamily(array $releases, string $family, bool $testing): ?string +{ + $stableInFamily = null; + $anyInFamily = null; + + foreach ($releases as $r) { + if (extractFamily($r['version']) !== $family) { + continue; + } + if ($anyInFamily === null) { + $anyInFamily = $r['version']; // newest in family (releases are sorted newest-first) + } + if (!$r['prerelease'] && $stableInFamily === null) { + $stableInFamily = $r['version']; + } + } + + // On stable channel prefer a stable release; fall back to latest prerelease if no stable yet + return ($testing ? $anyInFamily : ($stableInFamily ?? $anyInFamily)); +} + +function findGlobalLatest(array $releases, bool $testing): string +{ + foreach ($releases as $r) { + if ($testing || !$r['prerelease']) { + return $r['version']; + } + } + return $releases[0]['version']; +} + +// ── Resolve version to serve ────────────────────────────────────────────────── +$clientFamily = extractFamily($v); + +switch ($mode) { + case 'global': + // Old server behaviour: always return the globally latest, ignore ?v + $serveVersion = findGlobalLatest($releases, $testing); + break; + case 'same': + // No upgrade scenario: mirror back whatever version the client has + $serveVersion = $v ?: findGlobalLatest($releases, $testing); + break; + case 'family': + default: + // New server behaviour: return latest within the client's major.minor family + $serveVersion = findLatestInFamily($releases, $clientFamily, $testing) + ?? findGlobalLatest($releases, $testing); + break; +} + +// ── Log to stderr (visible in php -S output) ───────────────────────────────── +$log = sprintf( + "[%s] %s v=%s family=%s testing=%s mode=%s → %s\n", + date('H:i:s'), + $_SERVER['REQUEST_URI'], + $v, + $clientFamily, + $testing ? 'yes' : 'no', + $mode, + $serveVersion +); +file_put_contents('php://stderr', $log); + +// ── Respond ─────────────────────────────────────────────────────────────────── +header('Content-Type: application/json'); +echo json_encode([ + 'version' => $serveVersion, + 'date' => '2025-06-01T12:00:00Z', + 'assets' => [ + 'grav-update' => [ + 'name' => "grav-update-v{$serveVersion}.zip", + 'type' => 'application/zip', + 'size' => 3000000, + 'download' => "http://localhost:8043/download/grav-update/{$serveVersion}", + ], + 'grav-admin' => [ + 'name' => "grav-admin-v{$serveVersion}.zip", + 'type' => 'application/zip', + 'size' => 5000000, + 'download' => "http://localhost:8043/download/grav-admin/{$serveVersion}", + ], + 'grav' => [ + 'name' => "grav-v{$serveVersion}.zip", + 'type' => 'application/zip', + 'size' => 4000000, + 'download' => "http://localhost:8043/download/grav/{$serveVersion}", + ], + ], + 'url' => "https://github.com/getgrav/grav/releases/tag/{$serveVersion}", + 'min_php' => '8.1.0', + 'changelog' => new stdClass(), +], JSON_PRETTY_PRINT);