///|
/// The outcome of the ASGI 3.0 conformance harness: how many round-trip checks
/// ran, how many `passed`, and the names of any that `failures`. A clean run has
/// an empty `failures`. Reusable beyond the test suite — a server or framework
/// built on the seam can call `run_conformance` to self-verify the wiring.
pub(all) struct ConformanceReport {
total : Int
passed : Int
failures : Array[String]
} derive(Eq)
///|
/// Whether every conformance check passed (no failures recorded).
pub fn ConformanceReport::ok(self : ConformanceReport) -> Bool {
self.failures.length() == 0
}
///|
/// One of each `Event` variant, distinct payloads — the table the harness folds
/// over to prove `Event`'s derived equality discriminates every message in the
/// http / websocket / lifespan sets in both directions (no two variants collapse).
fn all_event_variants() -> Array[Event] {
[
HttpRequest(body=b"body", more_body=true),
HttpDisconnect,
HttpResponseStart(status=200, headers=[("a", "b")], trailers=false),
HttpResponseBody(body=b"x", more_body=false),
HttpResponseTrailers(headers=[("t", "v")], more_trailers=false),
HttpResponsePush(path="/p", headers=[]),
HttpResponsePathSend(path="/f"),
HttpResponseZeroCopySend(
fd=7,
offset=Some(0),
count=Some(16),
more_body=false,
),
HttpResponseDebug(info={ "trace": "abc" }.to_json()),
HttpResponseEarlyHint(links=["; rel=preload"]),
WebSocketConnect,
WebSocketReceive(text=Some("hi"), bytes=None),
WebSocketDisconnect(code=1000, reason=Some("bye")),
WebSocketAccept(subprotocol=Some("chat"), headers=[]),
WebSocketSendText("hi"),
WebSocketSendBytes(b"hi"),
WebSocketClose(code=1001, reason="bye"),
WebSocketHttpResponseStart(status=403, headers=[]),
WebSocketHttpResponseBody(body=b"no", more_body=false),
LifespanStartup,
LifespanShutdown,
LifespanStartupComplete,
LifespanStartupFailed(message="boom"),
LifespanShutdownComplete,
LifespanShutdownFailed(message="boom"),
]
}
///|
/// Run the ASGI 3.0 conformance harness: a table-driven battery that drives every
/// `Event` variant and every `Scope` field through `run_http`, `ws_run`, and the
/// `TestClient`, asserting round-trip fidelity — a value put in comes back
/// unchanged, and no two distinct events or scope shapes are confused — and a
/// negative table that asserts `validate_events` rejects every malformed ordering
/// (a body before the response starts, a websocket frame before the handshake, a
/// lifespan shutdown reply before startup, and the rest) with the exact
/// violation. Returns a `ConformanceReport` naming any check that failed, so the
/// suite (and any downstream server) can assert `ok()`.
pub fn run_conformance() -> ConformanceReport {
let results : Array[(String, Bool)] = []
let check = fn(name : String, cond : Bool) -> Unit {
results.push((name, cond))
}
// A. every Event variant is self-equal and pairwise-distinct: derived equality
// discriminates all 25 messages, so none collapses into another.
let evs = all_event_variants()
check("event/count", evs.length() == 25)
for i in 0.. resp.status == 401 && resp.body == b"denied"
None => false
}
check("ws/deny-http", !s_deny.accepted && deny_ok)
let s_close = echo.websocket(
path="/ws",
handler=WebSocketHandler::new(on_receive=fn(_m) {
[SendText("bye"), Close(code=1000, reason="done")]
}),
send=[Text("first"), Text("second-should-be-ignored")],
disconnect=None,
)
check("ws/app-close", s_close.messages == [Text("bye")] && s_close.closed)
// ws_run_app escape hatch sees the full scope (subprotocols) and inbound stream.
let s_app = echo.websocket_app(
path="/ws",
app=fn(scope : WebSocketScope, inbound : Array[Event]) {
[
WebSocketAccept(subprotocol=scope.subprotocols.get(0), headers=[
("x-frames", inbound.length().to_string()),
]),
]
},
subprotocols=["v1"],
send=[Text("a"), Text("b")],
disconnect=None,
)
check(
"ws/run_app",
s_app.accepted &&
s_app.subprotocol == Some("v1") &&
s_app.header("x-frames") == Some("3"),
)
// F. extension capability flags round-trip onto the scope; early-hint events
// are captured by the client.
let ext = Extensions::none()
.enable_push()
.enable_trailers()
.enable_pathsend()
.enable_early_hint()
.enable_websocket_http_response()
check(
"ext/flags",
ext.http_response_push &&
ext.http_response_trailers &&
ext.http_response_pathsend &&
ext.http_response_early_hint &&
ext.websocket_http_response,
)
let eh_client = TestClient::from_app(fn(_req) {
[
HttpResponseEarlyHint(links=["; rel=preload"]),
HttpResponseEarlyHint(links=[
"; rel=preload", "; rel=preload",
]),
HttpResponseStart(status=200, headers=[], trailers=false),
HttpResponseBody(body=b"", more_body=false),
]
})
let eh = eh_client.get("/")
check(
"ext/early-hint",
eh.early_hints.length() == 2 &&
eh.early_hints[0] == ["; rel=preload"] &&
eh.early_hints[1] == ["; rel=preload", "; rel=preload"],
)
// G. streaming response round-trip: early hints, chunked body, and trailers.
let stream = StreamingResponse::new(
status=200,
chunks=[b"one ", b"two"],
trailers=[("digest", "abc")],
early_hints=[["; rel=preload"]],
)
let sclient = TestClient::from_stream(fn(_r) { stream })
let sr = sclient.get("/")
check(
"stream/roundtrip",
sr.body == b"one two" &&
sr.trailer("digest") == Some("abc") &&
sr.early_hints == [["; rel=preload"]],
)
// H. spec_version negotiation compares numerically across the dotted version.
let ver_cases : Array[(String, Int, Int, Bool)] = [
("2.5", 2, 0, true),
("2.5", 2, 5, true),
("2.5", 2, 6, false),
("2.5", 1, 9, true),
("2.5", 3, 0, false),
("2.10", 2, 9, true),
("2.0", 2, 0, true),
("2.0", 2, 1, false),
]
for vc in ver_cases {
let (sv, maj, minr, want) = vc
let v : AsgiVersion = { version: "3.0", spec_version: sv }
check(
"spec_version/" + sv + ">=" + maj.to_string() + "." + minr.to_string(),
v.at_least(major=maj, minor=minr) == want,
)
}
// I. event-ordering validation. A well-formed outbound stream passes
// `validate_events`; a malformed one is rejected with the exact violation.
// This is the negative half of the harness — the ordering rules ASGI pins.
let hscope = Http(HttpScope::new(http_method="GET", path="/"))
let wscope = WebSocket(WebSocketScope::new(path="/ws"))
let lscope = Lifespan(LifespanScope::new())
let ok_start = HttpResponseStart(status=200, headers=[], trailers=false)
let ok_start_tr = HttpResponseStart(status=200, headers=[], trailers=true)
let ok_body = HttpResponseBody(body=b"x", more_body=false)
let ok_trailers = HttpResponseTrailers(
headers=[("t", "v")],
more_trailers=false,
)
let valid_http : Array[(String, Array[Event])] = [
("start+body", [ok_start, ok_body]),
(
"hint+start+body",
[HttpResponseEarlyHint(links=[""]), ok_start, ok_body],
),
("start+body+trailers", [ok_start_tr, ok_body, ok_trailers]),
(
"start+zerocopy",
[
ok_start,
HttpResponseZeroCopySend(fd=3, offset=None, count=None, more_body=false),
],
),
("start+pathsend", [ok_start, HttpResponsePathSend(path="/f")]),
(
"push+start+body",
[HttpResponsePush(path="/p", headers=[]), ok_start, ok_body],
),
(
"start+debug+body",
[ok_start, HttpResponseDebug(info={ "k": "v" }.to_json()), ok_body],
),
]
for vc in valid_http {
check("order/http-valid/" + vc.0, validate_events(hscope, vc.1) == None)
}
let bad_http : Array[(String, Array[Event], EventOrderError)] = [
("body-before-start", [ok_body], BodyBeforeStart),
("dup-start", [ok_start, ok_start], DuplicateResponseStart),
(
"hint-after-start",
[ok_start, HttpResponseEarlyHint(links=[])],
EarlyHintAfterStart,
),
("body-after-complete", [ok_start, ok_body, ok_body], BodyAfterComplete),
(
"unexpected-trailers",
[ok_start, ok_body, ok_trailers],
UnexpectedTrailers,
),
("trailers-before-body", [ok_start_tr, ok_trailers], TrailersBeforeBody),
("missing-start", [], MissingResponseStart),
(
"incomplete-body",
[ok_start, HttpResponseBody(body=b"x", more_body=true)],
IncompleteBody,
),
("missing-trailers", [ok_start_tr, ok_body], MissingTrailers),
(
"event-after-complete",
[ok_start, ok_body, HttpResponsePush(path="/p", headers=[])],
EventAfterComplete,
),
("non-response-event", [WebSocketConnect], NonResponseEvent),
]
for bc in bad_http {
check("order/http-bad/" + bc.0, validate_events(hscope, bc.1) == Some(bc.2))
}
let ws_accept = WebSocketAccept(subprotocol=None, headers=[])
let ws_deny_start = WebSocketHttpResponseStart(status=403, headers=[])
let valid_ws : Array[(String, Array[Event])] = [
("accept", [ws_accept]),
(
"accept+send+close",
[ws_accept, WebSocketSendText("hi"), WebSocketClose(code=1000, reason="")],
),
("close", [WebSocketClose(code=1003, reason="no")]),
(
"deny",
[ws_deny_start, WebSocketHttpResponseBody(body=b"no", more_body=false)],
),
]
for vc in valid_ws {
check("order/ws-valid/" + vc.0, validate_events(wscope, vc.1) == None)
}
let bad_ws : Array[(String, Array[Event], EventOrderError)] = [
("frame-before-accept", [WebSocketSendText("x")], FrameBeforeAccept),
("dup-accept", [ws_accept, ws_accept], DuplicateAccept),
(
"event-after-close",
[WebSocketClose(code=1000, reason=""), WebSocketSendText("x")],
EventAfterClose,
),
(
"denial-body-before-start",
[WebSocketHttpResponseBody(body=b"", more_body=false)],
DenialBodyBeforeStart,
),
("deny-after-accept", [ws_accept, ws_deny_start], DenialAfterAccept),
("accept-after-deny", [ws_deny_start, ws_accept], AcceptAfterDenial),
("incomplete-denial", [ws_deny_start], IncompleteDenial),
("missing-reply", [], MissingHandshakeReply),
]
for bc in bad_ws {
check("order/ws-bad/" + bc.0, validate_events(wscope, bc.1) == Some(bc.2))
}
let valid_ls : Array[(String, Array[Event])] = [
("startup+shutdown", [LifespanStartupComplete, LifespanShutdownComplete]),
("startup-failed", [LifespanStartupFailed(message="boom")]),
(
"shutdown-failed",
[LifespanStartupComplete, LifespanShutdownFailed(message="x")],
),
]
for vc in valid_ls {
check("order/ls-valid/" + vc.0, validate_events(lscope, vc.1) == None)
}
let bad_ls : Array[(String, Array[Event], EventOrderError)] = [
(
"shutdown-before-startup",
[LifespanShutdownComplete],
ShutdownBeforeStartup,
),
(
"dup-startup",
[LifespanStartupComplete, LifespanStartupComplete],
DuplicateLifespanReply,
),
(
"reply-after-cycle",
[
LifespanStartupComplete,
LifespanShutdownComplete,
LifespanShutdownComplete,
],
DuplicateLifespanReply,
),
("non-response-event", [HttpDisconnect], NonResponseEvent),
]
for bc in bad_ls {
check("order/ls-bad/" + bc.0, validate_events(lscope, bc.1) == Some(bc.2))
}
// J. lifespan driver: startup/shutdown replies, in-place state seeding, the
// full cycle's ordering, and the failed-startup short-circuit ASGI pins.
let boot_scope = LifespanScope::new()
let boot = LifespanHandler::new(on_startup=fn(s) {
s.state["ready"] = true.to_json()
Complete
})
let ls_driven = Lifespan(boot_scope)
let up = run_lifespan(boot, ls_driven, [LifespanStartup])
check("lifespan/startup-complete", up == [LifespanStartupComplete])
check("lifespan/state-seeded", boot_scope.state.get("ready") is Some(_))
let cyc_scope = Lifespan(LifespanScope::new())
let cyc = run_lifespan(boot, cyc_scope, [LifespanStartup, LifespanShutdown])
check(
"lifespan/full-cycle",
cyc == [LifespanStartupComplete, LifespanShutdownComplete],
)
check("lifespan/cycle-valid", validate_events(cyc_scope, cyc) == None)
let sfail = LifespanHandler::new(on_shutdown=fn(_s) {
Failed(message="drain")
})
check(
"lifespan/shutdown-failed",
run_lifespan(sfail, Lifespan(LifespanScope::new()), [LifespanShutdown]) ==
[LifespanShutdownFailed(message="drain")],
)
let ufail = LifespanHandler::new(on_startup=fn(_s) { Failed(message="boom") })
check(
"lifespan/startup-failed-short-circuits",
run_lifespan(ufail, Lifespan(LifespanScope::new()), [
LifespanStartup,
LifespanShutdown,
]) ==
[LifespanStartupFailed(message="boom")],
)
check(
"lifespan/non-lifespan-scope",
run_lifespan(boot, hscope, [LifespanStartup]).length() == 0,
)
// K. scope-aware http core: run_http_scoped hands the app the full HttpScope
// (state, root_path) the ergonomic Request drops, and drains the body the
// same way run_http_app does.
let seen_state : Array[Bool] = []
let seen_root : Array[String] = []
let scoped = fn(sh : HttpScope, sbody : Bytes) -> Array[Event] {
seen_state.push(sh.state.get("k") is Some(_))
seen_root.push(sh.root_path)
[
HttpResponseStart(status=200, headers=[], trailers=false),
HttpResponseBody(body=sbody, more_body=false),
]
}
let scoped_scope = Http(
HttpScope::new(
http_method="POST",
path="/x",
root_path="/mnt",
state=Map([("k", Json::string("v"))]),
),
)
let scoped_out = run_http_scoped(scoped, scoped_scope, [
HttpRequest(body=b"ab", more_body=true),
HttpRequest(body=b"c", more_body=false),
])
check("scoped/state-visible", seen_state == [true])
check("scoped/root-visible", seen_root == ["/mnt"])
check("scoped/body-drained", reassemble(scoped_out).body == b"abc")
check(
"scoped/non-http-scope",
run_http_scoped(scoped, lscope, [LifespanStartup]).length() == 0,
)
// L. HTTP/2 pseudo-header lowering: a HEADERS block (pseudo-headers + ordinary
// fields in wire order) maps onto an HttpScope the way a conforming h2/h2c
// server consumes a frame — request line from the pseudo-headers, host
// synthesised from :authority, `:`-prefixed names stripped from the header
// list — and every malformed set is rejected with the exact violation.
let h2 = HttpScope::from_h2_headers([
(":method", "POST"),
(":scheme", "https"),
(":authority", "example.test"),
(":path", "/items?page=2&q=x"),
("content-type", "application/json"),
("accept", "*/*"),
])
check(
"h2/lower",
match h2 {
Ok(s) =>
s.http_method == "POST" &&
s.scheme == "https" &&
s.http_version == "2" &&
s.path == "/items" &&
s.raw_path == b"/items" &&
s.query_string == b"page=2&q=x" &&
s.asgi.spec_version == "2.5" &&
// :authority becomes the leading host header; pseudo-headers are gone.
s.headers ==
[
("host", "example.test"),
("content-type", "application/json"),
("accept", "*/*"),
]
Err(_) => false
},
)
// :authority replaces a host header the peer also sent (RFC 7540 §8.1.2.3).
let h2_host = HttpScope::from_h2_headers([
(":method", "GET"),
(":scheme", "http"),
(":authority", "authority.test"),
(":path", "/"),
("host", "stale.test"),
("x-trace", "1"),
])
check(
"h2/authority-wins",
match h2_host {
Ok(s) => s.headers == [("host", "authority.test"), ("x-trace", "1")]
Err(_) => false
},
)
// No :authority: ordinary headers (including any host) pass through untouched,
// and http_version="3" is honoured for the shared HTTP/3 pseudo-header path.
let h3 = HttpScope::from_h2_headers(
[(":method", "GET"), (":scheme", "https"), (":path", "/p"), ("host", "h")],
http_version="3",
)
check(
"h2/no-authority",
match h3 {
Ok(s) => s.http_version == "3" && s.headers == [("host", "h")]
Err(_) => false
},
)
let bad_h2 : Array[(String, Array[(String, String)], Http2HeaderError)] = [
("missing-method", [(":scheme", "https"), (":path", "/")], MissingMethod),
("missing-scheme", [(":method", "GET"), (":path", "/")], MissingScheme),
("missing-path", [(":method", "GET"), (":scheme", "https")], MissingPath),
(
"empty-path",
[(":method", "GET"), (":scheme", "https"), (":path", "")],
EmptyPath,
),
(
"dup-method",
[
(":method", "GET"),
(":method", "POST"),
(":scheme", "h"),
(":path", "/"),
],
DuplicatePseudoHeader(":method"),
),
(
"unknown-pseudo",
[(":method", "GET"), (":scheme", "h"), (":path", "/"), (":x", "y")],
UnknownPseudoHeader(":x"),
),
(
"pseudo-after-regular",
[(":method", "GET"), ("a", "b"), (":scheme", "h"), (":path", "/")],
PseudoHeaderAfterRegular(":scheme"),
),
]
for bc in bad_h2 {
check(
"h2/bad/" + bc.0,
match HttpScope::from_h2_headers(bc.1) {
Err(e) => e == bc.2
Ok(_) => false
},
)
}
// The SEAM is transport-version agnostic: an http_version="2" request drives
// the exact same synchronous core and yields the same response as "1.1".
let ver_seen : Array[String] = []
let ver_client = TestClient::new(fn(req) {
ver_seen.push(req.path)
Response::text("v2")
})
let v2 = ver_client.request(http_method="GET", path="/h2", http_version="2")
check(
"h2/seam-agnostic",
v2.status == 200 && v2.text() == "v2" && ver_seen == ["/h2"],
)
let failures : Array[String] = []
for r in results {
if !r.1 {
failures.push(r.0)
}
}
{
total: results.length(),
passed: results.length() - failures.length(),
failures,
}
}