AtlatestRepositorysigil-http

sigil-http / tree / test / integrationrun-streaming-tests.sh

1#!/usr/bin/env bash
2# Integration test for the T1 streaming-async fix.
3#
4# Proves that STREAMING responses (http-response/file and SSE) work through a
5# BARE `http-serve` (no with-async), and that the existing with-async consumer
6# pattern still works. See notes/tasks/sigil-http-t1-streaming-async.
7#
8# Why the ceremony: a loose `sigil <file>` run resolves library imports from
9# the global dep cache (the RELEASED package), not local source — so it would
10# silently test the OLD code. To exercise the working-tree sigil-http we build
11# a tiny `from-path` bundle that links THIS repo's sigil-http, then drive it
12# with curl. The bundle is generated in a temp dir (NOT committed) because a
13# nested package.sgl inside the repo confuses `sigil test`/`sigil build`
14# workspace discovery.
16# Requires: curl, a working `sigil` toolchain, and network access on first run
17# (to fetch sigil-run/sigil-stdlib/sigil-json). Run from anywhere:
18# test/integration/run-streaming-tests.sh
20set -u
22SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
24SERVER_SRC="$SCRIPT_DIR/streaming-server-main.sgl"
26APP_DIR="$(mktemp -d /tmp/t1-streaming-app.XXXXXX)"
27BIN="$APP_DIR/build/dev/bin/t1-streaming-server"
28ASSET="$(mktemp /tmp/t1-asset.XXXXXX.bin)"
29DL="$(mktemp /tmp/t1-dl.XXXXXX.bin)"
30SSE="$(mktemp /tmp/t1-sse.XXXXXX.txt)"
31BARE_PORT=18201
32WRAPPED_PORT=18202
33GZIP_PORT=18203
34SRV_PID=""
35FAILED=0
37cleanup() {
38 [ -n "$SRV_PID" ] && kill "$SRV_PID" 2>/dev/null
39 rm -rf "$APP_DIR" "$ASSET" "$DL" "$SSE"
41trap cleanup EXIT
43pass() { echo "PASS: $1"; }
44fail() { echo "FAIL: $1"; FAILED=1; }
46# Scaffold the ephemeral from-path consumer bundle.
47scaffold_app() {
48 mkdir -p "$APP_DIR/src/t1-streaming-server"
49 cp "$SERVER_SRC" "$APP_DIR/src/t1-streaming-server/main.sgl"
50 cat > "$APP_DIR/package.sgl" <<EOF
51(package
52 name: "t1-streaming-server"
53 version: "0.1.0"
54 sigil: "^0.17"
55 description: "Ephemeral integration bundle for the T1 streaming-async fix"
56 entry: '(t1-streaming-server main)
57 bundle-name: "t1-streaming-server"
58 configs: (list
59 (config name: 'dev output-dir: "build/dev" static?: #f debug?: #t optimize: 0 bundle?: #t))
60 dependencies: (list
61 (from-git url: "codeberg:sigil/sigil" package: "sigil-run" version: "^0.17")
62 (from-git url: "codeberg:sigil/sigil" package: "sigil-stdlib" version: "^0.17")
63 (from-git url: "codeberg:sigil/sigil-json" version: "^0.16")
64 (from-path dir: "$REPO_ROOT" package: "sigil-http")))
65EOF
68start_server() {
69 local port="$1" mode="$2"
70 "$BIN" "$port" "$ASSET" "$mode" >"$APP_DIR/server-$mode.log" 2>&1 &
71 SRV_PID=$!
72 local i
73 for i in $(seq 1 60); do
74 if curl -s --max-time 2 "http://127.0.0.1:$port/hello" >/dev/null 2>&1; then
75 return 0
76 fi
77 if ! kill -0 "$SRV_PID" 2>/dev/null; then
78 echo " server ($mode) died on startup; log:"; cat "$APP_DIR/server-$mode.log"
79 return 1
80 fi
81 sleep 0.5
82 done
83 echo " server ($mode) never became ready"
84 return 1
87stop_server() {
88 [ -n "$SRV_PID" ] && kill "$SRV_PID" 2>/dev/null
89 wait "$SRV_PID" 2>/dev/null
90 SRV_PID=""
93run_mode() {
94 local mode="$1" port="$2"
95 echo ""
96 echo "=== mode: $mode (port $port) ==="
97 if ! start_server "$port" "$mode"; then
98 fail "$mode: server startup"
99 stop_server
100 return
101 fi
103 # 1. Streaming file must come back byte-exact.
104 if curl -s --max-time 30 "http://127.0.0.1:$port/file" -o "$DL" \
105 && cmp -s "$ASSET" "$DL"; then
106 pass "$mode: http-response/file is byte-exact ($(stat -c%s "$DL") bytes)"
107 else
108 fail "$mode: http-response/file mismatch (got $(stat -c%s "$DL" 2>/dev/null) bytes)"
109 fi
111 # 2. SSE endpoint must stream all five events.
112 curl -s --max-time 10 "http://127.0.0.1:$port/sse" -o "$SSE"
113 local ticks
114 ticks=$(grep -c '^event: tick' "$SSE" 2>/dev/null || echo 0)
115 if [ "$ticks" = "5" ]; then
116 pass "$mode: SSE streamed 5 events"
117 else
118 fail "$mode: SSE expected 5 events, got $ticks"
119 fi
121 # 3. No async-context crash should appear in the server log.
122 if grep -q "not running in an async context" "$APP_DIR/server-$mode.log"; then
123 fail "$mode: server logged an async-context error (streaming crashed)"
124 else
125 pass "$mode: no async-context error in server log"
126 fi
128 # 4. HEAD returns headers (incl. Content-Length) and ZERO body bytes.
129 # curl -I masks the old HEAD-body bug (it closes after headers), so we
130 # save the body to a file and assert it is empty. See review gotchas.
131 local hb hh hbsize
132 hb="$(mktemp)"; hh="$(mktemp)"
133 curl -s --max-time 5 -X HEAD -D "$hh" -o "$hb" "http://127.0.0.1:$port/big-text"
134 hbsize=$(stat -c%s "$hb" 2>/dev/null || echo -1)
135 if [ "$hbsize" = "0" ] \
136 && head -1 "$hh" | grep -q " 200 " \
137 && grep -qi '^content-length:' "$hh"; then
138 pass "$mode: HEAD /big-text -> headers only (0 body bytes, Content-Length present)"
139 else
140 fail "$mode: HEAD expected 0 body bytes + status 200 + Content-Length (got $hbsize body bytes)"
141 fi
142 rm -f "$hb" "$hh"
144 # 5. gzip is OFF by default: even with Accept-Encoding: gzip, no encoding.
145 local h5
146 h5="$(curl -s --max-time 5 -H 'Accept-Encoding: gzip' -D - -o /dev/null "http://127.0.0.1:$port/big-text")"
147 if echo "$h5" | grep -qi '^content-encoding:'; then
148 fail "$mode: gzip disabled by default but response was content-encoded"
149 else
150 pass "$mode: gzip off by default (no Content-Encoding even when client accepts gzip)"
151 fi
153 stop_server
156# gzip-enabled server (T11): opt-in gzip honoring Accept-Encoding.
157run_gzip() {
158 local port="$1"
159 echo ""
160 echo "=== gzip mode (port $port) ==="
161 if ! start_server "$port" "gzip"; then
162 fail "gzip: server startup"
163 stop_server
164 return
165 fi
167 # Baseline: identity size (no Accept-Encoding).
168 local raw_size gz_size hdr
169 curl -s --max-time 5 -o "$DL" "http://127.0.0.1:$port/big-text"
170 raw_size=$(stat -c%s "$DL" 2>/dev/null || echo 0)
172 # With Accept-Encoding: gzip -> Content-Encoding: gzip and a smaller body.
173 hdr="$(curl -s --max-time 5 -H 'Accept-Encoding: gzip' -D - -o "$DL" "http://127.0.0.1:$port/big-text")"
174 gz_size=$(stat -c%s "$DL" 2>/dev/null || echo 0)
175 if echo "$hdr" | grep -qi '^content-encoding: *gzip' \
176 && echo "$hdr" | grep -qi '^vary: *Accept-Encoding'; then
177 pass "gzip: Accept-Encoding: gzip -> Content-Encoding: gzip + Vary"
178 else
179 fail "gzip: expected Content-Encoding: gzip and Vary: Accept-Encoding"
180 fi
181 if [ "$gz_size" -gt 0 ] && [ "$gz_size" -lt "$raw_size" ]; then
182 pass "gzip: compressed body smaller than identity ($gz_size < $raw_size bytes)"
183 else
184 fail "gzip: compressed body not smaller ($gz_size vs $raw_size bytes)"
185 fi
187 # Without Accept-Encoding -> identity (no Content-Encoding).
188 hdr="$(curl -s --max-time 5 -D - -o /dev/null "http://127.0.0.1:$port/big-text")"
189 if echo "$hdr" | grep -qi '^content-encoding:'; then
190 fail "gzip: response without Accept-Encoding must not be gzipped"
191 else
192 pass "gzip: no Accept-Encoding -> identity"
193 fi
195 # Incompressible/streamed file must never be gzipped, even when enabled.
196 hdr="$(curl -s --max-time 15 -H 'Accept-Encoding: gzip' -D - -o "$DL" "http://127.0.0.1:$port/file")"
197 if echo "$hdr" | grep -qi '^content-encoding:' || ! cmp -s "$ASSET" "$DL"; then
198 fail "gzip: streaming file was altered/encoded (must stay byte-exact identity)"
199 else
200 pass "gzip: streaming file stays byte-exact identity (not gzipped)"
201 fi
203 stop_server
206echo "Scaffolding + building integration bundle (from-path sigil-http)..."
207scaffold_app
208( cd "$APP_DIR" && sigil deps install >/dev/null 2>&1 && sigil build >"$APP_DIR/build.log" 2>&1 )
209if [ ! -x "$BIN" ]; then
210 echo "FAIL: bundle build did not produce $BIN"
211 tail -20 "$APP_DIR/build.log" 2>/dev/null
212 exit 1
213fi
215# 2.1 MB random binary asset (mirrors the production-readiness repro).
216head -c 2100000 /dev/urandom > "$ASSET"
218run_mode "bare" "$BARE_PORT" # T1: streaming works WITHOUT with-async
219run_mode "wrapped" "$WRAPPED_PORT" # regression: existing with-async consumers
220run_gzip "$GZIP_PORT" # T11: opt-in gzip honoring Accept-Encoding
222echo ""
223if [ "$FAILED" -eq 0 ]; then
224 echo "All streaming integration tests passed."
225 exit 0
226else
227 echo "Some streaming integration tests FAILED."
228 exit 1
229fi