Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / crates / npro-test / transcripts / h1-client-head-chunked.json
Author[]Andy Green <andy@warmcat.com> 2026-10-05 12:39 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-10-05 13:01 UTC
Tree4abf6e16326962a5dcce95a0d31cda579623e4c4   Raw Patch
 
npro-test: npro's h1 client replays C's client transcripts
npro-test: npro's h1 client replays C's client transcripts

The seven h1 client transcripts that need nothing beyond phase 1d (not
the digest retry, which needs the digest and lws' random): npro's client
makes C's request, GET /x or HEAD /x to sansio with C's defaults, and
must write the transcript's request byte for byte; after each rx, give
the app the transcript's app_rx bytes; ask to be released exactly where
C released the connection, for a Content-Length of "10abc", two of
them, or a list of codings; and otherwise have completed the
transaction, so that a HEAD's, or a 304's, framing headers are not
waited on.

And h1-ws-server's first exchange, a GET to the sansio vhost whose http
protocol answers "sansio ok", replays through npro's server: the rest of
it is ws, for phase 1e.

The 304 taken to have a body, or "Connection" spelled as the request is
not, fails it.

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/tests/h1_client_replay.rs b/crates/npro-test/tests/h1_client_replay.rs new file mode 100644 index 0000000..7c183c5 --- /dev/null +++ b/crates/npro-test/tests/h1_client_replay.rs @@ -0,0 +1,129 @@ +//! npro's h1 client replays C's h1 client transcripts. +//! +//! Each is one request C's `api-test-sansio` makes, `GET /x` (or `HEAD /x`) +//! to `sansio`, port 80, with C's defaults: no-cache headers, an `Origin`, +//! and `connection: close`. The request npro writes must be the +//! transcript's first `tx`, byte for byte; after each `rx`, the body npro +//! gives the app must be the transcript's `app_rx` bytes, and npro must ask +//! to release the connection exactly where C released it, its `close`. +//! Where C did not, its transaction was complete at the end, and npro's +//! must be. + +#![expect( + unused_crate_dependencies, + reason = "an integration test sees all of its crate's dependencies; this one uses npro-test and npro-h1" +)] + +// held to clippy's rules for tests +#[cfg(test)] +mod h1_client_replay { + use npro_h1::client::{Client, Event, Request, Scheme}; + use npro_h1::table::DEFAULT_CAPACITY; + use npro_test::{StepKind, Transcript, vendored}; + + /// The h1 client transcripts that need no more of lws than this phase + /// has: not the digest retry, which needs the digest and lws' random. + const CASES: [&str; 7] = [ + "h1-client-get", + "h1-client-cl-junk", + "h1-client-cl-twice", + "h1-client-te-list", + "h1-client-head-chunked", + "h1-client-head-cl", + "h1-client-304-cl", + ]; + + /// Hands `input` to the client until it takes nothing more; returns + /// the body it gave the app, and whether it failed. + fn feed(c: &mut Client<Vec<u8>>, mut input: &[u8]) -> (Vec<u8>, bool) { + let mut body = Vec::new(); + loop { + let Ok(rx) = c.rx(input) else { + return (body, true); + }; + input = &input[rx.consumed..]; + match rx.event { + Some(Event::Body(b)) => body.extend_from_slice(b), + None if rx.consumed == 0 => return (body, false), + Some(Event::Response | Event::BodyEnd) | None => {} + } + } + } + + fn replay(t: &Transcript) { + let mut steps = t.steps.iter().peekable(); + let Some(StepKind::Tx(want)) = steps.next().map(|s| &s.kind) else { + panic!("{}: does not start with the request", t.case); + }; + let method: &[u8] = if want.starts_with(b"HEAD ") { + b"HEAD" + } else { + b"GET" + }; + let mut c = Client::new( + vec![0u8; DEFAULT_CAPACITY], + Request { + method, + path: b"/x", + host: Some(b"sansio"), + origin: Some(b"sansio"), + scheme: Scheme::Http, + no_cache: true, + pipeline: false, + }, + ) + .unwrap(); + let mut out = [0u8; 512]; + let n = c.tx(&mut out); + assert_eq!( + out[..n].escape_ascii().to_string(), + want.escape_ascii().to_string(), + "{}: the request", + t.case + ); + + while let Some(step) = steps.next() { + let StepKind::Rx(rx) = &step.kind else { + panic!("{}: {:?} with no rx before it", t.case, step.kind); + }; + let (mut want_body, mut want_close) = (Vec::new(), false); + while let Some(next) = steps.peek() { + match &next.kind { + StepKind::AppRx(b) => want_body.extend_from_slice(b), + StepKind::Close => want_close = true, + StepKind::Rx(_) => break, + StepKind::Tx(b) => panic!("{}: a second request {b:?}", t.case), + } + steps.next(); + } + let (body, failed) = feed(&mut c, rx); + assert_eq!(body, want_body, "{} at {}us: the body", t.case, step.t_us); + assert_eq!( + failed, + want_close, + "{} at {}us: released, {:?}", + t.case, + step.t_us, + c.failed() + ); + } + // where C did not release it, it had completed the transaction: + // nothing is left waiting for a body that will not come + if c.failed().is_none() { + assert!(c.is_done(), "{}: not done", t.case); + } + } + + #[test] + #[cfg_attr(miri, ignore = "reads the transcripts: native runs keep it")] + fn cs_client_transcripts_replay() { + let all = vendored().unwrap(); + for case in CASES { + let t = all + .iter() + .find(|t| t.case == case) + .unwrap_or_else(|| panic!("no transcript {case}")); + replay(t); + } + } +} diff --git a/crates/npro-test/tests/h1_server_replay.rs b/crates/npro-test/tests/h1_server_replay.rs index 28129a5..abaea6e 100644 --- a/crates/npro-test/tests/h1_server_replay.rs +++ b/crates/npro-test/tests/h1_server_replay.rs @@ -7,6 +7,7 @@ //! with a length of 10 and 3 bytes (none to a HEAD); `/body-done` from the //! body's first piece, with `ok\n`. Each `rx` is handed to npro's server, //! and what it writes before the next must be the transcript's `tx` bytes. +//! So must `h1-ws-server`'s first exchange, a GET to the `sansio` vhost. #![expect( unused_crate_dependencies, @@ -58,6 +59,8 @@ mod h1_server_replay { complete_when_sent: bool, /// `/body-done`: answer from the body. body_done: bool, + /// The `sansio` vhost's `http` instead: this, to any request. + fixed: Option<&'static [u8]>, } impl TxSource for UriApp { @@ -84,6 +87,11 @@ mod h1_server_replay { } fn request(&mut self, s: &mut Server<Vec<u8>>) { + if let Some(f) = self.fixed { + let len = u64::try_from(f.len()).unwrap(); + self.answer(s, len, f); + return; + } let t = s.request(); let path = t .first(Token::GetUri) @@ -146,14 +154,15 @@ mod h1_server_replay { } } - fn replay(t: &Transcript) { - let cfg = head::Config::new() - .with_limit(Token::GetUri, NonZeroU16::new(33).unwrap()) - .with_limit(Token::UserAgent, NonZeroU16::new(16).unwrap()); + /// Replays `t`'s first `rxs` reads, with `app`, in a context limiting + /// what `cfg` limits. + fn replay(t: &Transcript, cfg: head::Config, mut app: UriApp, rxs: usize) { let mut s = Server::new(vec![0u8; DEFAULT_CAPACITY], Config::new(cfg)).unwrap(); - let mut app = UriApp::default(); let mut steps = t.steps.iter().peekable(); - while let Some(step) = steps.next() { + for _ in 0..rxs { + let Some(step) = steps.next() else { + break; + }; let StepKind::Rx(rx) = &step.kind else { panic!("{}: {:?} with no rx before it", t.case, step.kind); }; @@ -188,7 +197,24 @@ mod h1_server_replay { .iter() .find(|t| t.case == case) .unwrap_or_else(|| panic!("no transcript {case}")); - replay(t); + let cfg = head::Config::new() + .with_limit(Token::GetUri, NonZeroU16::new(33).unwrap()) + .with_limit(Token::UserAgent, NonZeroU16::new(16).unwrap()); + replay(t, cfg, UriApp::default(), usize::MAX); } } + + /// `h1-ws-server`'s first exchange, a GET to the `sansio` vhost, whose + /// `http` answers `sansio ok`; the rest of it is ws. + #[test] + #[cfg_attr(miri, ignore = "reads the transcripts: native runs keep it")] + fn h1_ws_servers_first_exchange_replays() { + let all = vendored().unwrap(); + let t = all.iter().find(|t| t.case == "h1-ws-server").unwrap(); + let app = UriApp { + fixed: Some(b"sansio ok\n"), + ..UriApp::default() + }; + replay(t, head::Config::new(), app, 1); + } }
Page fetched 0s ago, creation time: 7ms (vhost etag hits: 0%, cache hits: 0%)