| Author | Andy Green <andy@warmcat.com> 2026-10-05 20:22 UTC | | Committer | Andy Green <andy@warmcat.com> 2026-10-06 03:36 UTC | | Tree | f2ff55abe716b0a3e6122ca3c75271adde424b2c Raw Patch | | | npro-test: the permessage-deflate transcripts replay | npro-test: the permessage-deflate transcripts replay
The client replay offers permessage-deflate in the ws-client-pmd-* cases,
quiet as C's are, since what it would send is deflated, and takes the
server's answer: ws-client-pmd-rsv2, -rsv1-continuation and -rsv1-ping
replay byte for byte, masked close and all. The server replay takes it
on C's sansio-pmd vhost: ws-server-pmd-rsv1-continuation replays, the 101
saying it, the plain first frame given the app, the RSV1 continuation
refused. npro-test builds npro-ws with pmd.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kg5Eemy68ZaqDBcUJQG6J
|
diff --git a/crates/npro-test/Cargo.toml b/crates/npro-test/Cargo.toml
index 100fc95..d5a7ad5 100644
--- a/crates/npro-test/Cargo.toml
+++ b/crates/npro-test/Cargo.toml
@@ -12,7 +12,7 @@ repository.workspace = true
[dev-dependencies]
npro-core = { path = "../npro-core", features = ["replay"] }
npro-h1 = { path = "../npro-h1" }
-npro-ws = { path = "../npro-ws" }
+npro-ws = { path = "../npro-ws", features = ["pmd"] }
[lints]
workspace = true
diff --git a/crates/npro-test/tests/ws_client_replay.rs b/crates/npro-test/tests/ws_client_replay.rs
index e00d207..6122ed6 100644
--- a/crates/npro-test/tests/ws_client_replay.rs
+++ b/crates/npro-test/tests/ws_client_replay.rs
@@ -7,7 +7,10 @@
//! the response is checked as C checks it, and the connection is then
//! npro-ws', masking from the same stream. The app is C's
//! `callback_client`: once established it sends `Hello`, unless the case
-//! has it quiet.
+//! has it quiet. The `ws-client-pmd-*` cases also offer permessage-deflate,
+//! which the server takes; they are quiet, as C's are, since what the
+//! client would send is deflated, and those bytes are the deflater's
+//! business.
//!
//! What npro writes after each `rx` must be the transcript's `tx` bytes,
//! the messages it hands the app its `app_rx` bytes, and it must ask to be
@@ -24,17 +27,36 @@ mod ws_client_replay {
use npro_test::{StepKind, Transcript, vendored};
use npro_ws::conn::{AsClient, Event, Kind, Ws};
use npro_ws::handshake::{ClientKey, MAX_REQUEST_LINES};
+ use npro_ws::pmd;
- /// The ws client transcripts this phase replays, and whether the app
- /// is quiet: not the permessage-deflate ones, the next phase's, nor the
- /// digest retry, which needs digest auth.
- const CASES: [(&str, bool); 6] = [
- ("ws-client", false),
- ("ws-client-interim", false),
- ("ws-client-ping-close", true),
- ("ws-client-huge-frame", false),
- ("ws-client-rsv1-no-ext", false),
- ("ws-client-rsv2", false),
+ /// A case: its transcript, whether the app is quiet, and the
+ /// extensions offered.
+ struct Case {
+ name: &'static str,
+ quiet: bool,
+ extensions: Option<&'static [u8]>,
+ }
+
+ const fn case(name: &'static str, quiet: bool, extensions: Option<&'static [u8]>) -> Case {
+ Case {
+ name,
+ quiet,
+ extensions,
+ }
+ }
+
+ /// The ws client transcripts: all but the digest retry, which needs
+ /// digest auth.
+ const CASES: [Case; 9] = [
+ case("ws-client", false, None),
+ case("ws-client-interim", false, None),
+ case("ws-client-ping-close", true, None),
+ case("ws-client-huge-frame", false, None),
+ case("ws-client-rsv1-no-ext", false, None),
+ case("ws-client-rsv2", false, None),
+ case("ws-client-pmd-rsv2", true, Some(pmd::OFFER)),
+ case("ws-client-pmd-rsv1-continuation", true, Some(pmd::OFFER)),
+ case("ws-client-pmd-rsv1-ping", true, Some(pmd::OFFER)),
];
/// The subprotocols offered.
@@ -82,6 +104,7 @@ mod ws_client_replay {
client: Box<Client<Vec<u8>>>,
key: ClientKey,
random: Option<SeededRandom>,
+ extensions: Option<&'static [u8]>,
},
Ws(Box<Ws<AsClient<SeededRandom>>>),
}
@@ -99,6 +122,7 @@ mod ws_client_replay {
client,
key,
random,
+ extensions,
} => {
let rx = client.rx(&input[at..]).unwrap();
at = at.checked_add(rx.consumed).unwrap();
@@ -106,10 +130,18 @@ mod ws_client_replay {
if rx.event == Some(H1Event::Response) {
assert!(client.is_upgraded());
let checked = key
- .check(client.status(), client.response(), Some(OFFERED), None)
+ .check(
+ client.status(),
+ client.response(),
+ Some(OFFERED),
+ *extensions,
+ )
.unwrap();
assert_eq!(checked.protocol, Some(OFFERED));
let mut ws = Ws::client(random.take().unwrap());
+ if let Some(said) = checked.extensions {
+ ws = ws.with_pmd(pmd::client_accept(said).unwrap());
+ }
app.established(&mut ws);
*conn = Conn::Ws(Box::new(ws));
continue;
@@ -139,13 +171,15 @@ mod ws_client_replay {
}
}
- fn replay(t: &Transcript, quiet: bool) {
+ fn replay(t: &Transcript, c: &Case) {
// C's run was seeded: its random is in the transcript
let seed = t.seed.unwrap_or_else(|| panic!("{}: not seeded", t.case));
let mut random = SeededRandom::new(seed.get());
let key = ClientKey::new(&mut random).unwrap();
let mut lines = [0u8; MAX_REQUEST_LINES + 32];
- let lines_len = key.request_lines(Some(OFFERED), None, &mut lines).unwrap();
+ let lines_len = key
+ .request_lines(Some(OFFERED), c.extensions, &mut lines)
+ .unwrap();
let mut client = Client::new(
vec![0u8; DEFAULT_CAPACITY],
Request {
@@ -177,9 +211,10 @@ mod ws_client_replay {
client: Box::new(client),
key,
random: Some(random),
+ extensions: c.extensions,
};
let mut app = ClientApp {
- quiet,
+ quiet: c.quiet,
..ClientApp::default()
};
while let Some(step) = steps.next() {
@@ -224,12 +259,12 @@ mod ws_client_replay {
#[cfg_attr(miri, ignore = "reads the transcripts: native runs keep it")]
fn cs_ws_client_transcripts_replay_byte_for_byte() {
let all = vendored().unwrap();
- for (case, quiet) in CASES {
+ for c in &CASES {
let t = all
.iter()
- .find(|t| t.case == case)
- .unwrap_or_else(|| panic!("no transcript {case}"));
- replay(t, quiet);
+ .find(|t| t.case == c.name)
+ .unwrap_or_else(|| panic!("no transcript {}", c.name));
+ replay(t, c);
}
}
}
diff --git a/crates/npro-test/tests/ws_server_replay.rs b/crates/npro-test/tests/ws_server_replay.rs
index 3ea0d95..a037e44 100644
--- a/crates/npro-test/tests/ws_server_replay.rs
+++ b/crates/npro-test/tests/ws_server_replay.rs
@@ -1,7 +1,8 @@
//! npro's ws server replays C's ws server transcripts byte for byte.
//!
//! Each transcript is one connection to C's `sansio` vhost, whose ws
-//! subprotocols are `http`, its default, and `echo`. npro's h1 server takes
+//! subprotocols are `http`, its default, and `echo`, or to its `sansio-pmd`
+//! vhost, the same with permessage-deflate. npro's h1 server takes
//! the request; one asking for `Upgrade: websocket` goes to npro-ws's
//! handshake, which answers a refusal with C's status page and an accepted
//! upgrade with C's 101, after which the connection is npro-ws'. The app is
@@ -28,10 +29,10 @@ mod ws_server_replay {
use npro_test::{StepKind, Transcript, vendored};
use npro_ws::conn::{Event, Kind, Ws};
use npro_ws::handshake::{self, MAX_101};
+ use npro_ws::pmd;
- /// The ws server transcripts this half of the phase replays: not the
- /// permessage-deflate ones, which are the next phase's.
- const CASES: [&str; 10] = [
+ /// The ws server transcripts.
+ const CASES: [&str; 11] = [
"h1-ws-server",
"ws-server-version-8",
"ws-server-no-version",
@@ -42,6 +43,7 @@ mod ws_server_replay {
"ws-server-close-partial",
"ws-server-close-when-flushed",
"ws-server-huge-frame",
+ "ws-server-pmd-rsv1-continuation",
];
/// The `sansio` vhost's ws subprotocols, the first its default.
@@ -121,12 +123,23 @@ mod ws_server_replay {
app.at = 0;
return Answer::H1;
}
+ // the vhost with permessage-deflate takes it if it is offered
+ let pmd = (t.first(Token::Host) == Some(b"sansio-pmd".as_slice()))
+ .then(|| pmd::server_accept(t).unwrap())
+ .flatten();
match handshake::server(t, &PROTOCOLS, Some(0)) {
Ok(a) => {
+ let lines = pmd
+ .as_ref()
+ .map_or(&[][..], pmd::ServerAccepted::header_lines);
let mut first = [0u8; MAX_101];
let n =
- handshake::response_101(&a, PROTOCOLS[a.protocol], b"", &mut first).unwrap();
- Answer::Upgraded(Box::new(Ws::server(&first[..n])))
+ handshake::response_101(&a, PROTOCOLS[a.protocol], lines, &mut first).unwrap();
+ let mut ws = Ws::server(&first[..n]);
+ if let Some(p) = pmd {
+ ws = ws.with_pmd(p.params());
+ }
+ Answer::Upgraded(Box::new(ws))
}
Err(r) => {
s.refuse_upgrade(r.status(), r.header()).unwrap();
|