#![cfg(unix)]
use futures_util::StreamExt;
use super::fake_podman::{self, FakeReply};
async fn read_stream(reply: FakeReply) -> (Vec<serde_json::Value>, Option<String>) {
let fake = fake_podman::start_replying(move |_method, _target| match &reply {
FakeReply::Body(s, b) => FakeReply::Body(*s, b.clone()),
FakeReply::ChunkedEnd(c) => FakeReply::ChunkedEnd(c.clone()),
FakeReply::ChunkedTruncated(c) => FakeReply::ChunkedTruncated(c.clone()),
FakeReply::ChunkedCutMidPayload(c) => FakeReply::ChunkedCutMidPayload(c.clone()),
});
let client = fake.client();
let resp = client
.get_stream(&format!("{}/events", crate::libpod::API_PREFIX))
.await
.expect("the fake answers 200");
let mut stream = crate::libpod::parse_json_lines::<serde_json::Value>(resp.into_body());
let mut frames = Vec::new();
let mut ended_as = None;
while let Some(item) = stream.next().await {
match item {
Ok(value) => frames.push(value),
Err(e) => {
ended_as = Some(e.stream_end_kind().to_string());
break;
}
}
}
(frames, ended_as)
}
fn frame(action: &str) -> String {
format!("{{\"Type\":\"container\",\"Action\":\"{action}\"}}\n")
}
#[tokio::test]
async fn a_properly_terminated_stream_ends_clean() {
let (frames, ended_as) =
read_stream(FakeReply::ChunkedEnd(vec![frame("start"), frame("die")])).await;
assert_eq!(frames.len(), 2, "both frames arrive: {frames:?}");
assert_eq!(
ended_as, None,
"a terminated chunked body must not surface as an error, it surfaced as {ended_as:?}"
);
}
#[tokio::test]
async fn a_cut_between_chunks_is_a_body_eof() {
let (frames, ended_as) = read_stream(FakeReply::ChunkedTruncated(vec![frame("start")])).await;
assert_eq!(
frames.len(),
1,
"the frame written before the cut still arrives: {frames:?}"
);
assert_eq!(
ended_as.as_deref(),
Some("body-unexpected-eof"),
"hyper reports this as a Body error wrapping UnexpectedEof \
(\"unexpected EOF during chunk size line\"), not IncompleteMessage"
);
}
#[tokio::test]
async fn a_cut_mid_payload_is_also_a_body_eof() {
let (_frames, ended_as) = read_stream(FakeReply::ChunkedCutMidPayload(format!(
"{{\"Type\":\"container\",\"Action\":\"start\",\"padding\":\"{}\"}}\n",
"a".repeat(64)
)))
.await;
assert_eq!(ended_as.as_deref(), Some("body-unexpected-eof"));
}
#[tokio::test]
async fn neither_cut_is_an_incomplete_message() {
for reply in [
FakeReply::ChunkedTruncated(vec![frame("start")]),
FakeReply::ChunkedCutMidPayload(format!("{{\"padding\":\"{}\"}}\n", "a".repeat(64))),
] {
let (_frames, ended_as) = read_stream(reply).await;
assert_ne!(
ended_as.as_deref(),
Some("incomplete-message"),
"a severed body is not the head-level IncompleteMessage"
);
}
}
#[tokio::test]
async fn the_two_shapes_carry_the_same_payload() {
let (clean_frames, clean_end) = read_stream(FakeReply::ChunkedEnd(vec![frame("start")])).await;
let (severed_frames, severed_end) =
read_stream(FakeReply::ChunkedTruncated(vec![frame("start")])).await;
assert_eq!(clean_frames, severed_frames, "same frames delivered");
assert!(clean_end.is_none(), "clean end: {clean_end:?}");
assert!(severed_end.is_some(), "severed end must be an error");
}