Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ input validation, auto-rollback safety, and pre-flight checks.
10 shape directories, every one validated against real nginx in Docker, with a
shape checklist (`tests/configs/SHAPES.md`), a provenance manifest and a
near-duplicate assertion (`tests/test-corpus.sh`) so the count cannot be gamed
- [ ] Nginx version matrix testing (1.18, 1.22, 1.25, 1.27)
- [x] Nginx version matrix testing (1.18, 1.22, 1.25, 1.27) — `tests/test-version-matrix.sh`
runs `nginx -t` in each official Docker image: a minimal full config and the
portable http-context templates must pass on all four, while HTTP/3-era
fixtures skip below their minimum version instead of failing. Wired into
`tests/run-tests.sh`; skips cleanly without Docker or a missing Hub tag

### UX
- [ ] `--no-color` flag for CI environments
Expand Down
24 changes: 23 additions & 1 deletion tests/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ bash4_pat="decl""are -A|map""file |read""array "
bash4_hits=$(grep -rEn "$bash4_pat" \
"${SCRIPT_DIR}/../nginx-optimizer-lib/" \
"${SCRIPT_DIR}/run-tests.sh" "${SCRIPT_DIR}/test-corpus.sh" \
"${SCRIPT_DIR}/test-with-nginx.sh" 2>/dev/null \
"${SCRIPT_DIR}/test-with-nginx.sh" "${SCRIPT_DIR}/test-version-matrix.sh" 2>/dev/null \
| grep -v ":[0-9]*:[[:space:]]*#" || true)
if [ -n "$bash4_hits" ]; then
log_fail "Found bash 4+ constructs (macOS ships bash 3.2): $bash4_hits"
Expand Down Expand Up @@ -1248,6 +1248,28 @@ source "${SCRIPT_DIR}/../lib/core/sysinfo.sh"
_SYSINFO_RAM_MB=""
_SYSINFO_CPU_CORES=""

################################################################################
# SECTION 21: Nginx Version Matrix (issue #18)
################################################################################
log_section "Nginx Version Matrix (Docker)"

# tests/test-version-matrix.sh runs `nginx -t` inside the official
# nginx:1.18 / 1.22 / 1.25 / 1.27 images: a minimal full config and the
# portable http-context templates must pass on all four, while HTTP/3-era
# fixtures are gated by the version that introduced their directives
# (skip on 1.18/1.22, run on 1.25/1.27). Docker missing or the daemon down
# is a skip — same contract as test-with-nginx.sh, so CI without Docker
# stays green.
if [ ! -f "${SCRIPT_DIR}/test-version-matrix.sh" ]; then
log_fail "tests/test-version-matrix.sh missing"
elif ! command -v docker &>/dev/null || ! docker info &>/dev/null 2>&1; then
log_skip "Version matrix needs Docker (run ./tests/test-version-matrix.sh where Docker runs)"
elif bash "${SCRIPT_DIR}/test-version-matrix.sh"; then
log_pass "Version matrix passed on nginx 1.18 / 1.22 / 1.25 / 1.27"
else
log_fail "Version matrix failed"
fi

################################################################################
# Summary
################################################################################
Expand Down
242 changes: 242 additions & 0 deletions tests/test-version-matrix.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
#!/bin/bash
# Docker-based nginx VERSION MATRIX validation
#
# test-with-nginx.sh proves the corpus and templates parse on ONE nginx —
# whatever nginx:latest is today. This script proves the portable parts still
# parse on the OLDEST versions we claim to support, and that fixtures using
# directives introduced on newer nginx are skipped — not failed — on versions
# too old to have them.
#
# Matrix: nginx:1.18, 1.22, 1.25, 1.27 (official Docker Hub tags).
# - minimal full config + portable http-context templates: PASS on all four
# - HTTP/3 / quic / `http2 on;` / early_hints fixtures: SKIP below their
# minimum version, RUN at or above it
#
# Same contract as test-with-nginx.sh: Docker missing or daemon down exits 0.
# A missing Docker Hub tag skips THAT version with a reason, never the run.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIGS_DIR="${SCRIPT_DIR}/configs"
TEMPLATES_DIR="${SCRIPT_DIR}/../nginx-optimizer-templates"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

PASS=0
FAIL=0
SKIP=0

log_pass() { echo -e "${GREEN}[PASS]${NC} $*"; PASS=$((PASS + 1)); }
log_fail() { echo -e "${RED}[FAIL]${NC} $*"; FAIL=$((FAIL + 1)); }
log_skip() { echo -e "${YELLOW}[SKIP]${NC} $*"; SKIP=$((SKIP + 1)); }

echo "=========================================="
echo " nginx Version Matrix Validation (Docker)"
echo "=========================================="

# Check Docker availability — same skip contract as test-with-nginx.sh
if ! command -v docker &>/dev/null; then
echo "Docker not available - skipping all tests"
exit 0
fi

if ! docker info &>/dev/null 2>&1; then
echo "Docker daemon not running - skipping all tests"
exit 0
fi

MATRIX_VERSIONS="1.18 1.22 1.25 1.27"

# Minimal valid fixture: a complete upstream-default nginx.conf. Mounted as
# /etc/nginx/nginx.conf (not conf.d) — it already carries events{} and http{}.
MINIMAL_FIXTURE="${CONFIGS_DIR}/minimal/nginx-official-default.conf"

# http-context templates that must parse on EVERY version in the matrix:
# gzip and open_file_cache predate 1.18 by a decade. compression.conf's brotli
# lines are commented out — stock images carry no brotli module, so this file
# is intentionally gzip-only here (compiling brotli for the matrix is out of
# scope; see the issue).
PORTABLE_TEMPLATES="compression.conf open-file-cache.conf"

# Version-gated fixtures, one per line: "path|min-version|label".
# Below min-version they log SKIP; at or above it `nginx -t` must pass.
# early_hints needs nginx 1.29 — newer than the whole matrix — so today it
# exercises the skip path on every row; it starts running by itself the day a
# >=1.29 tag joins MATRIX_VERSIONS.
GATED_FIXTURES="
${CONFIGS_DIR}/tls/http3-quic.conf|1.25|tls/http3-quic.conf (listen quic, http2 on, http3 on)
${TEMPLATES_DIR}/early-hints.conf|1.29|early-hints.conf (early_hints on)
"

################################################################################
# Helpers
################################################################################

# Numeric dotted compare, bash 3.2-safe: version_lt 1.9 1.18 -> true.
# (String compare would lie there — '9' > '1' lexically.)
version_lt() {
local a_maj="${1%%.*}" a_min="${1#*.}"
local b_maj="${2%%.*}" b_min="${2#*.}"
a_min="${a_min%%.*}"
b_min="${b_min%%.*}"
if [ "$a_maj" -eq "$b_maj" ]; then
[ "$a_min" -lt "$b_min" ]
else
[ "$a_maj" -lt "$b_maj" ]
fi
}

# Returns 0 when the image is runnable (already cached, or pulled now).
# On failure sets MATRIX_SKIP_REASON for the caller's skip line — "tag gone"
# and "registry unreachable" are different reasons, not one shrug.
MATRIX_SKIP_REASON=""
ensure_image() {
local image="$1"
if docker image inspect "$image" >/dev/null 2>&1; then
return 0
fi
if docker pull -q "$image" >/dev/null 2>&1; then
return 0
fi
if docker manifest inspect "$image" >/dev/null 2>&1; then
MATRIX_SKIP_REASON="tag exists on Docker Hub but pull failed"
else
MATRIX_SKIP_REASON="tag not found on Docker Hub (or registry unreachable)"
fi
return 1
}

# Runs `nginx -t` inside image $1 with the given -v mounts (pass -v args first,
# image is appended). On failure, NGINX_T_ERR carries the first real nginx
# error line for the caller's log line.
NGINX_T_ERR=""
nginx_t() {
local image="$1" out
shift
if out=$(docker run --rm "$@" "$image" nginx -t 2>&1); then
NGINX_T_ERR=""
return 0
fi
NGINX_T_ERR=$(printf '%s\n' "$out" | grep -E "emerg|error" | grep -v "docker-entrypoint" | head -1 || true)
return 1
}

################################################################################
# Setup: self-signed cert for fixtures that reference ssl_certificate
################################################################################

CERT_DIR=$(mktemp -d)
openssl req -x509 -nodes -days 1 -newkey rsa:2048 \
-keyout "$CERT_DIR/privkey.pem" \
-out "$CERT_DIR/fullchain.pem" \
-subj "/CN=test.example.com" 2>/dev/null

################################################################################
# Matrix run
################################################################################

for ver in $MATRIX_VERSIONS; do
image="nginx:${ver}"
echo ""
echo "--- nginx:${ver} ---"

if ! ensure_image "$image"; then
log_skip "nginx:${ver} — ${MATRIX_SKIP_REASON}"
continue
fi

# 1. Minimal full config — must parse on every supported version.
if nginx_t "$image" -v "${MINIMAL_FIXTURE}:/etc/nginx/nginx.conf:ro"; then
log_pass "nginx:${ver} minimal fixture (full nginx.conf)"
else
log_fail "nginx:${ver} minimal fixture rejected: ${NGINX_T_ERR}"
fi

# 2. Portable http-context templates — wrapped in a generated full config,
# same shape as test-with-nginx.sh's generic wrapper.
for tmpl in $PORTABLE_TEMPLATES; do
tmpdir=$(mktemp -d)
cat > "$tmpdir/nginx.conf" <<WRAPPER
events { worker_connections 1024; }
http {
include /etc/nginx/templates/${tmpl};
server {
listen 80;
server_name localhost;
location / { return 200 'ok'; }
}
}
WRAPPER
if nginx_t "$image" \
-v "$tmpdir/nginx.conf:/etc/nginx/nginx.conf:ro" \
-v "${TEMPLATES_DIR}/${tmpl}:/etc/nginx/templates/${tmpl}:ro"; then
log_pass "nginx:${ver} template ${tmpl} (http context)"
else
log_fail "nginx:${ver} template ${tmpl} failed on a version that must support it: ${NGINX_T_ERR}"
fi
rm -rf "$tmpdir"
done

# 3. Version-gated fixtures — server-context snippets mounted into conf.d/
# (the image's stock nginx.conf already includes conf.d/*.conf inside
# http{}). Below their min-version they must SKIP, never FAIL.
while IFS='|' read -r fpath minver label; do
[ -n "$fpath" ] || continue
if version_lt "$ver" "$minver"; then
log_skip "nginx:${ver} ${label} — needs nginx >= ${minver}"
continue
fi

if grep -q "ssl_certificate" "$fpath" 2>/dev/null; then
# Rewrite cert paths to the generated self-signed pair — same sed
# pipeline as test-with-nginx.sh (key/trusted/client anchored
# before the bare ssl_certificate pattern so it can't swallow them).
tmpdir=$(mktemp -d)
sed \
-e 's|ssl_certificate_key .*|ssl_certificate_key /etc/nginx/ssl/privkey.pem;|g' \
-e 's|ssl_trusted_certificate .*|ssl_trusted_certificate /etc/nginx/ssl/fullchain.pem;|g' \
-e 's|ssl_client_certificate .*|ssl_client_certificate /etc/nginx/ssl/fullchain.pem;|g' \
-e 's|ssl_certificate .*|ssl_certificate /etc/nginx/ssl/fullchain.pem;|g' \
"$fpath" > "$tmpdir/test.conf"
if nginx_t "$image" \
-v "$tmpdir/test.conf:/etc/nginx/conf.d/test.conf:ro" \
-v "$CERT_DIR/fullchain.pem:/etc/nginx/ssl/fullchain.pem:ro" \
-v "$CERT_DIR/privkey.pem:/etc/nginx/ssl/privkey.pem:ro"; then
log_pass "nginx:${ver} ${label}"
else
log_fail "nginx:${ver} ${label} rejected: ${NGINX_T_ERR}"
fi
rm -rf "$tmpdir"
else
if nginx_t "$image" -v "$fpath:/etc/nginx/conf.d/test.conf:ro"; then
log_pass "nginx:${ver} ${label}"
else
log_fail "nginx:${ver} ${label} rejected: ${NGINX_T_ERR}"
fi
fi
done <<< "$GATED_FIXTURES"
done

################################################################################
# Cleanup & Summary
################################################################################

rm -rf "$CERT_DIR"

echo ""
echo "=========================================="
echo -e " Results: ${GREEN}${PASS} passed${NC}, ${RED}${FAIL} failed${NC}, ${YELLOW}${SKIP} skipped${NC}"
echo "=========================================="

if [ "$FAIL" -gt 0 ]; then
echo -e "${RED}TESTS FAILED${NC}"
exit 1
else
echo -e "${GREEN}ALL TESTS PASSED${NC}"
exit 0
fi
Loading