AtlatestRepositorysigil-http
sigil-http / tree / test / integrationrun-keepalive-range-tests.sh
1
#!/usr/bin/env bash2
# Integration tests for T4 (keep-alive + chunked transfer-encoding) and3
# T2 (Range/206/416), driven against a server that links THIS repo's4
# working-tree sigil-http (via a from-path bundle, same technique as5
# run-streaming-tests.sh — a loose `sigil <file>` would resolve the RELEASED6
# package from the dep cache and silently test old code).7
#8
# Wire evidence is captured two ways:9
# - curl for connection reuse, headers, and Range status/sizes10
# - a raw Python socket for byte-exact chunked framing (0\r\n\r\n) and for11
# proving two serial requests are served on ONE connection.12
#13
# Requires: curl, python3, a working `sigil` toolchain, network on first run.14
# test/integration/run-keepalive-range-tests.sh16
set -u18
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"19
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"20
SERVER_SRC="$SCRIPT_DIR/keepalive-range-server-main.sgl"22
APP_DIR="$(mktemp -d /tmp/t4t2-app.XXXXXX)"23
BIN="$APP_DIR/build/dev/bin/t4t2-server"24
ASSET="$(mktemp /tmp/t4t2-asset.XXXXXX.bin)"25
PORT=1825126
SRV_PID=""27
FAILED=029
cleanup() {30
[ -n "$SRV_PID" ] && kill "$SRV_PID" 2>/dev/null31
rm -rf "$APP_DIR" "$ASSET"32
}33
trap cleanup EXIT35
pass() { echo "PASS: $1"; }36
fail() { echo "FAIL: $1"; FAILED=1; }38
scaffold_app() {39
mkdir -p "$APP_DIR/src/t4t2-server"40
cp "$SERVER_SRC" "$APP_DIR/src/t4t2-server/main.sgl"41
cat > "$APP_DIR/package.sgl" <<EOF42
(package43
name: "t4t2-server"44
version: "0.1.0"45
sigil: "^0.17"46
description: "Ephemeral integration bundle for T4 keep-alive + T2 range"47
entry: '(t4t2-server main)48
bundle-name: "t4t2-server"49
configs: (list50
(config name: 'dev output-dir: "build/dev" static?: #f debug?: #t optimize: 0 bundle?: #t))51
dependencies: (list52
(from-git url: "codeberg:sigil/sigil" package: "sigil-run" version: "^0.17")53
(from-git url: "codeberg:sigil/sigil" package: "sigil-stdlib" version: "^0.17")54
(from-git url: "codeberg:sigil/sigil-json" version: "^0.16")55
(from-path dir: "$REPO_ROOT" package: "sigil-http")))56
EOF57
}59
start_server() {60
"$BIN" "$PORT" "$ASSET" >"$APP_DIR/server.log" 2>&1 &61
SRV_PID=$!62
local i63
for i in $(seq 1 60); do64
if curl -s --max-time 2 "http://127.0.0.1:$PORT/hello" >/dev/null 2>&1; then65
return 066
fi67
if ! kill -0 "$SRV_PID" 2>/dev/null; then68
echo " server died on startup; log:"; cat "$APP_DIR/server.log"69
return 170
fi71
sleep 0.572
done73
echo " server never became ready"; return 174
}76
echo "Scaffolding + building integration bundle (from-path sigil-http)..."77
scaffold_app78
( cd "$APP_DIR" && sigil deps install >/dev/null 2>&1 && sigil build >"$APP_DIR/build.log" 2>&1 )79
if [ ! -x "$BIN" ]; then80
echo "FAIL: bundle build did not produce $BIN"81
tail -20 "$APP_DIR/build.log" 2>/dev/null82
exit 183
fi85
# 4 KB asset for range tests.86
head -c 4096 /dev/urandom > "$ASSET"87
ASSET_SIZE=$(stat -c%s "$ASSET")89
if ! start_server; then90
echo "Some integration tests FAILED (startup)."; exit 191
fi93
# ---------------------------------------------------------------------------94
# T4.1 — keep-alive: two requests reuse ONE TCP connection (curl).95
# ---------------------------------------------------------------------------96
REUSE=$(curl -sv --http1.1 "http://127.0.0.1:$PORT/hello" "http://127.0.0.1:$PORT/hello" 2>&1 \97
| grep -c 'Re-using existing connection')98
if [ "$REUSE" -ge 1 ]; then99
pass "T4 keep-alive: curl re-used the connection for a 2nd request"100
else101
fail "T4 keep-alive: curl did NOT re-use the connection (count=$REUSE)"102
fi104
# ---------------------------------------------------------------------------105
# T4.2 — response advertises Connection: keep-alive + Content-Length (HTTP/1.1).106
# ---------------------------------------------------------------------------107
H=$(curl -s --http1.1 -D - -o /dev/null "http://127.0.0.1:$PORT/hello")108
if echo "$H" | grep -qi '^connection: *keep-alive' && echo "$H" | grep -qi '^content-length:'; then109
pass "T4 keep-alive: Connection: keep-alive + Content-Length present"110
else111
fail "T4 keep-alive: missing keep-alive/Content-Length headers"; echo "$H" | sed 's/^/ /'112
fi114
# ---------------------------------------------------------------------------115
# T4.3 — explicit Connection: close is honored.116
# ---------------------------------------------------------------------------117
H=$(curl -s --http1.1 -H 'Connection: close' -D - -o /dev/null "http://127.0.0.1:$PORT/hello")118
if echo "$H" | grep -qi '^connection: *close'; then119
pass "T4 keep-alive: Connection: close honored"120
else121
fail "T4 keep-alive: Connection: close NOT honored"; echo "$H" | sed 's/^/ /'122
fi124
# ---------------------------------------------------------------------------125
# T4.4 — chunked framing over the wire (raw socket: exact bytes + terminator).126
# ---------------------------------------------------------------------------127
python3 - "$PORT" <<'PY'128
import socket, sys129
port = int(sys.argv[1])130
s = socket.create_connection(("127.0.0.1", port), timeout=5)131
s.sendall(b"GET /chunked HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n")132
data = b""133
s.settimeout(5)134
try:135
while True:136
b = s.recv(4096)137
if not b: break138
data += b139
except socket.timeout:140
pass141
s.close()142
head, _, body = data.partition(b"\r\n\r\n")143
ok = True144
if b"transfer-encoding: chunked" not in head.lower():145
print(" raw: missing Transfer-Encoding: chunked header"); ok = False146
if b"content-length" in head.lower():147
print(" raw: unexpected Content-Length on chunked response"); ok = False148
# Expect chunk sizes 7 (=0x7) for each 'chunk-X' and a terminator.149
if b"7\r\nchunk-A\r\n" not in body:150
print(" raw: chunk-A not framed as '7\\r\\nchunk-A\\r\\n'"); ok = False151
if not body.rstrip(b"\r\n").endswith(b"0") and not body.endswith(b"0\r\n\r\n"):152
print(" raw: missing terminating 0-chunk"); ok = False153
if b"0\r\n\r\n" not in body:154
print(" raw: no 0\\r\\n\\r\\n terminator in body"); ok = False155
sys.exit(0 if ok else 1)156
PY157
if [ $? -eq 0 ]; then158
pass "T4 chunked: valid chunk framing + terminating 0\\r\\n\\r\\n (raw socket)"159
else160
fail "T4 chunked: framing/terminator check failed"161
fi163
# ---------------------------------------------------------------------------164
# T4.5 — TWO serial requests served on ONE connection (raw socket, definitive).165
# ---------------------------------------------------------------------------166
python3 - "$PORT" <<'PY'167
import socket, sys168
port = int(sys.argv[1])170
def read_by_content_length(s):171
buf = b""172
while b"\r\n\r\n" not in buf:173
b = s.recv(1)174
if not b: return None, b""175
buf += b176
head, _, rest = buf.partition(b"\r\n\r\n")177
cl = 0178
for line in head.split(b"\r\n"):179
if line.lower().startswith(b"content-length:"):180
cl = int(line.split(b":")[1].strip())181
while len(rest) < cl:182
chunk = s.recv(cl - len(rest))183
if not chunk: break184
rest += chunk185
return head, rest187
s = socket.create_connection(("127.0.0.1", port), timeout=5)188
s.settimeout(5)189
req = b"GET /hello HTTP/1.1\r\nHost: x\r\n\r\n"190
s.sendall(req)191
h1, b1 = read_by_content_length(s)192
s.sendall(req) # SAME socket, second request193
h2, b2 = read_by_content_length(s)194
s.close()195
ok = (h1 and h2 and b1 == b"Hello, World!" and b2 == b"Hello, World!"196
and b" 200 " in h1.split(b"\r\n")[0] and b" 200 " in h2.split(b"\r\n")[0])197
if not ok:198
print(" raw serial: b1=%r b2=%r" % (b1, b2))199
sys.exit(0 if ok else 1)200
PY201
if [ $? -eq 0 ]; then202
pass "T4 keep-alive: two serial requests served on one socket (raw, no reconnect)"203
else204
fail "T4 keep-alive: serial-reuse on one socket failed"205
fi207
# ---------------------------------------------------------------------------208
# T2.1 — Range bytes=0-99 -> 206 + Content-Range + 100 bytes.209
# ---------------------------------------------------------------------------210
DL="$(mktemp)"211
H=$(curl -s -r 0-99 -D - -o "$DL" "http://127.0.0.1:$PORT/file")212
CODE=$(echo "$H" | head -1 | grep -oE '[0-9]{3}')213
SZ=$(stat -c%s "$DL")214
if [ "$CODE" = "206" ] && echo "$H" | grep -qi "^content-range: *bytes 0-99/$ASSET_SIZE" && [ "$SZ" = "100" ]; then215
pass "T2 range: bytes=0-99 -> 206, Content-Range bytes 0-99/$ASSET_SIZE, 100 bytes"216
else217
fail "T2 range: bytes=0-99 (code=$CODE size=$SZ)"; echo "$H" | sed 's/^/ /'218
fi220
# T2.2 — suffix bytes=-500 -> 206 + last 500 bytes.221
H=$(curl -s -r -500 -D - -o "$DL" "http://127.0.0.1:$PORT/file")222
CODE=$(echo "$H" | head -1 | grep -oE '[0-9]{3}')223
SZ=$(stat -c%s "$DL")224
EXP_START=$((ASSET_SIZE - 500))225
if [ "$CODE" = "206" ] && echo "$H" | grep -qi "^content-range: *bytes $EXP_START-$((ASSET_SIZE-1))/$ASSET_SIZE" && [ "$SZ" = "500" ]; then226
pass "T2 range: suffix bytes=-500 -> 206, last 500 bytes"227
else228
fail "T2 range: suffix bytes=-500 (code=$CODE size=$SZ)"; echo "$H" | sed 's/^/ /'229
fi231
# T2.3 — open-ended bytes=500- -> 206 remainder.232
H=$(curl -s -r 500- -D - -o "$DL" "http://127.0.0.1:$PORT/file")233
CODE=$(echo "$H" | head -1 | grep -oE '[0-9]{3}')234
SZ=$(stat -c%s "$DL")235
if [ "$CODE" = "206" ] && [ "$SZ" = "$((ASSET_SIZE-500))" ]; then236
pass "T2 range: open-ended bytes=500- -> 206, $((ASSET_SIZE-500)) bytes"237
else238
fail "T2 range: open-ended bytes=500- (code=$CODE size=$SZ)"239
fi241
# T2.4 — unsatisfiable -> 416 + Content-Range: bytes */total.242
H=$(curl -s -H "Range: bytes=$((ASSET_SIZE+1000))-" -D - -o /dev/null "http://127.0.0.1:$PORT/file")243
CODE=$(echo "$H" | head -1 | grep -oE '[0-9]{3}')244
if [ "$CODE" = "416" ] && echo "$H" | grep -qi "^content-range: *bytes \*/$ASSET_SIZE"; then245
pass "T2 range: unsatisfiable -> 416, Content-Range bytes */$ASSET_SIZE"246
else247
fail "T2 range: unsatisfiable (code=$CODE)"; echo "$H" | sed 's/^/ /'248
fi250
# T2.5 — no Range -> 200, full body byte-exact.251
curl -s -D - -o "$DL" "http://127.0.0.1:$PORT/file" >"$APP_DIR/full-head.txt"252
if head -1 "$APP_DIR/full-head.txt" | grep -q ' 200 ' && cmp -s "$ASSET" "$DL"; then253
pass "T2 range: no Range -> 200, byte-exact full file ($ASSET_SIZE bytes)"254
else255
fail "T2 range: full-file 200 mismatch"256
fi257
rm -f "$DL"259
echo ""260
if [ "$FAILED" -eq 0 ]; then261
echo "All keep-alive + range integration tests passed."262
exit 0263
else264
echo "Some keep-alive + range integration tests FAILED."265
echo "--- server log ---"; cat "$APP_DIR/server.log"266
exit 1267
fi