| //! 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);
}
}
}
|