Add E2E test server and runner for GPM family-gate testing

This commit is contained in:
Andy Miller
2026-04-13 18:57:39 +01:00
parent 98af1cb4e3
commit 54be9bcc9d
3 changed files with 296 additions and 1 deletions

View File

@@ -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;
}

162
bin/gpm-e2e-test Executable file
View File

@@ -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

133
bin/gpm-test-server.php Executable file
View File

@@ -0,0 +1,133 @@
<?php
/**
* Local GPM test server — simulates getgrav.org/downloads/grav.json
*
* Start with: php -S localhost:8043 bin/gpm-test-server.php
*
* Mode is controlled by writing a string to /tmp/gpm-test-mode:
* echo family > /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);