use std::io::Read;
use std::net::{TcpListener, TcpStream};
use liminal::protocol::{CausalContext, Frame, MessageEnvelope, SchemaId, decode};
use super::{DrainOutcome, OutboundError, OutboundWriter};
fn envelope(payload: Vec<u8>) -> MessageEnvelope {
MessageEnvelope::new(
SchemaId::new([7; SchemaId::WIRE_LEN]),
CausalContext::independent(),
payload,
)
}
fn deliver_frame(payload: Vec<u8>) -> Frame {
Frame::Deliver {
flags: 0,
stream_id: 1,
delivery_seq: 1,
envelope: envelope(payload),
}
}
fn socket_pair() -> Result<(TcpStream, TcpStream), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let address = listener.local_addr()?;
let writer = TcpStream::connect(address)?;
let (reader, _) = listener.accept()?;
writer.set_nonblocking(true)?;
Ok((writer, reader))
}
#[test]
fn enqueue_then_drain_delivers_the_frame() -> Result<(), Box<dyn std::error::Error>> {
let (mut writer_socket, mut reader_socket) = socket_pair()?;
let mut outbound = OutboundWriter::new();
let frame = deliver_frame(vec![1, 2, 3, 4]);
outbound.enqueue_frame(&frame)?;
let outcome = outbound.drain(&mut writer_socket, None)?;
assert_eq!(
outcome,
DrainOutcome::Drained,
"a fully-flushed buffer reports Drained"
);
assert_eq!(
outbound.queued_len(),
0,
"a small frame drains fully in one slice"
);
let mut buffer = vec![0_u8; 4096];
let read = reader_socket.read(&mut buffer)?;
let (decoded, _) = decode(&buffer[..read])?;
assert_eq!(decoded, frame);
Ok(())
}
#[test]
fn enqueue_beyond_capacity_reports_overflow() {
let mut outbound = OutboundWriter::with_capacity(64);
let result = outbound.enqueue_frame(&deliver_frame(vec![0_u8; 4096]));
assert!(matches!(result, Err(OutboundError::Overflow { .. })));
assert_eq!(
outbound.queued_len(),
0,
"a rejected frame must not partially enqueue"
);
}
#[test]
fn drain_of_empty_buffer_is_ok() -> Result<(), Box<dyn std::error::Error>> {
let (mut writer_socket, _reader) = socket_pair()?;
let mut outbound = OutboundWriter::new();
let outcome = outbound.drain(&mut writer_socket, None)?;
assert_eq!(
outcome,
DrainOutcome::Drained,
"draining an empty buffer reports Drained"
);
assert_eq!(outbound.queued_len(), 0);
Ok(())
}
#[test]
fn large_frame_survives_partial_writes() -> Result<(), Box<dyn std::error::Error>> {
let (mut writer_socket, mut reader_socket) = socket_pair()?;
reader_socket.set_nonblocking(true)?;
let mut outbound = OutboundWriter::new();
let payload: Vec<u8> = (0..300_000_usize)
.map(|index| u8::try_from(index % 251).unwrap_or(0))
.collect();
let frame = deliver_frame(payload.clone());
outbound.enqueue_frame(&frame)?;
let mut received = Vec::new();
let mut scratch = vec![0_u8; 65536];
for _ in 0..10_000 {
outbound.drain(&mut writer_socket, None)?;
match reader_socket.read(&mut scratch) {
Ok(0) => break,
Ok(read) => received.extend_from_slice(&scratch[..read]),
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
Err(error) => return Err(error.into()),
}
if outbound.queued_len() == 0 && received.len() >= payload.len() {
break;
}
}
let (decoded, _) = decode(&received)?;
assert_eq!(
decoded, frame,
"the large frame is delivered intact across slices"
);
Ok(())
}
#[test]
fn would_block_with_residue_is_reported() -> Result<(), Box<dyn std::error::Error>> {
let (mut writer_socket, _reader) = socket_pair()?;
let mut outbound = OutboundWriter::new();
let payload: Vec<u8> = (0..2_000_000_usize)
.map(|index| u8::try_from(index % 251).unwrap_or(0))
.collect();
outbound.enqueue_frame(&deliver_frame(payload))?;
let mut outcome = DrainOutcome::Drained;
for _ in 0..1_000 {
outcome = outbound.drain(&mut writer_socket, None)?;
if outcome == DrainOutcome::WouldBlockWithResidue {
break;
}
}
assert_eq!(
outcome,
DrainOutcome::WouldBlockWithResidue,
"a full send buffer with residue queued reports WouldBlockWithResidue"
);
assert!(
outbound.queued_len() > 0,
"residue remains queued for a later slice"
);
Ok(())
}
#[test]
fn byte_budget_reports_progress_with_residue() -> Result<(), Box<dyn std::error::Error>> {
let (mut writer_socket, reader_socket) = socket_pair()?;
reader_socket.set_nonblocking(true)?;
let mut outbound = OutboundWriter::new();
outbound.enqueue_frame(&deliver_frame(vec![9_u8; 256]))?;
outbound.enqueue_frame(&deliver_frame(vec![8_u8; 256]))?;
let queued_before = outbound.queued_len();
let outcome = outbound.drain(&mut writer_socket, Some(1))?;
assert_eq!(
outcome,
DrainOutcome::Progress,
"exhausting the byte budget with residue queued reports Progress"
);
assert!(
outbound.queued_len() < queued_before && outbound.queued_len() > 0,
"some bytes flushed, residue remains: partial progress, not Drained or WouldBlock"
);
let outcome = outbound.drain(&mut writer_socket, None)?;
assert_eq!(outcome, DrainOutcome::Drained, "unbudgeted drain finishes");
assert_eq!(outbound.queued_len(), 0);
Ok(())
}