Files
xserial/crates/pipeview-core/tests/pipeline.rs

832 lines
31 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, UdpSocket};
use tokio::time::timeout;
use pipeview_core::frame::{
Endian, Framer,
cobs::CobsFramer,
cobs::cobs_encode as raw_cobs_encode,
fixed::FixedLengthFramer,
length::{LengthConfig, LengthPrefixedFramer},
line::{LineConfig, LineFramer},
mixed::{MixedTextPlotConfig as MixedFramerConfig, MixedTextPlotFramer},
};
use pipeview_core::protocol::{
DecodedData, ProtocolDecoder,
hex::{HexConfig, HexDecoder},
mixed::{MIXED_PLOT_ESCAPE, MIXED_PLOT_MARKER, MixedTextPlotConfig, MixedTextPlotDecoder},
plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType},
text::{TextDecoder, TextEncoding},
};
use pipeview_core::transport::serial::{
SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits,
};
use pipeview_core::transport::{Connection, TransportConfig, TransportType};
const TEST_TIMEOUT: Duration = Duration::from_secs(5);
async fn read_to_framer(conn: &mut Connection, framer: &mut dyn Framer) -> Vec<Vec<u8>> {
let mut buf = [0u8; 4096];
let mut all_frames = Vec::new();
loop {
match timeout(TEST_TIMEOUT, AsyncReadExt::read(conn, &mut buf)).await {
Ok(Ok(0)) => break,
Ok(Ok(n)) => {
all_frames.extend(framer.feed(&buf[..n]));
}
Ok(Err(_)) => break,
Err(_) => break,
}
}
if let Some(rest) = framer.flush() {
all_frames.push(rest);
}
all_frames
}
// ══════════════════════════════════════════════════════════════════════
// TCP + LineFramer + TextDecoder
// ══════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn tcp_line_text_utf8() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
stream.write_all(b"hello\nworld\n").await.unwrap();
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut framer = LineFramer::new(LineConfig::default());
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
let decoder = TextDecoder::new(TextEncoding::Utf8);
let results: Vec<String> = frames
.iter()
.filter_map(|f| decoder.decode(f))
.filter_map(|d| match d {
DecodedData::Text(s) => Some(s),
_ => None,
})
.collect();
assert_eq!(results, vec!["hello", "world"]);
server.await.unwrap();
}
#[tokio::test]
async fn tcp_line_text_crlf_stripping() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
stream.write_all(b"line1\r\nline2\r\n").await.unwrap();
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut framer = LineFramer::new(LineConfig::default());
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
let decoder = TextDecoder::new(TextEncoding::Utf8);
let results: Vec<String> = frames
.iter()
.filter_map(|f| decoder.decode(f))
.filter_map(|d| match d {
DecodedData::Text(s) => Some(s),
_ => None,
})
.collect();
assert_eq!(results, vec!["line1", "line2"]);
server.await.unwrap();
}
#[tokio::test]
async fn tcp_line_text_chinese_utf8() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
stream.write_all("你好\n世界\n".as_bytes()).await.unwrap();
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut framer = LineFramer::new(LineConfig::default());
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
let decoder = TextDecoder::new(TextEncoding::Utf8);
let results: Vec<String> = frames
.iter()
.filter_map(|f| decoder.decode(f))
.filter_map(|d| match d {
DecodedData::Text(s) => Some(s),
_ => None,
})
.collect();
assert_eq!(results, vec!["你好", "世界"]);
server.await.unwrap();
}
// ══════════════════════════════════════════════════════════════════════
// TCP + LengthPrefixedFramer + HexDecoder
// ══════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn tcp_length_hex() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
// Frame 1: 3 bytes "foo"
stream.write_all(&3u16.to_be_bytes()).await.unwrap();
stream.write_all(b"foo").await.unwrap();
// Frame 2: 3 bytes "bar"
stream.write_all(&3u16.to_be_bytes()).await.unwrap();
stream.write_all(b"bar").await.unwrap();
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut framer = LengthPrefixedFramer::new(LengthConfig::default());
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
let decoder = HexDecoder::new(HexConfig::default());
let results: Vec<String> = frames
.iter()
.filter_map(|f| decoder.decode(f))
.filter_map(|d| match d {
DecodedData::Hex(s) => Some(s),
_ => None,
})
.collect();
assert_eq!(results, vec!["66 6f 6f", "62 61 72"]); // "foo", "bar" in hex
server.await.unwrap();
}
#[tokio::test]
async fn tcp_length_hex_little_endian() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
stream.write_all(&3u16.to_le_bytes()).await.unwrap();
stream.write_all(b"xyz").await.unwrap();
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut framer = LengthPrefixedFramer::new(LengthConfig {
endian: Endian::Little,
..LengthConfig::default()
});
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
assert_eq!(frames.len(), 1);
let decoder = HexDecoder::new(HexConfig::default());
let result = decoder.decode(&frames[0]).unwrap();
assert!(matches!(result, DecodedData::Hex(ref s) if s == "78 79 7a"));
server.await.unwrap();
}
// ══════════════════════════════════════════════════════════════════════
// TCP + FixedLengthFramer + PlotDecoder
// ══════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn tcp_fixed_plot_f32() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
// 3 samples × 4 bytes each = 12 bytes per frame
let samples: [f32; 3] = [1.0, -2.5, 0.0];
let mut data = Vec::new();
for s in &samples {
data.extend_from_slice(&s.to_le_bytes());
}
stream.write_all(&data).await.unwrap();
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut framer = FixedLengthFramer::new(12); // 3 × f32
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
let decoder = PlotDecoder::new(PlotConfig {
sample_type: SampleType::F32,
endian: Endian::Little,
channels: 1,
format: PlotFormat::Interleaved,
});
assert_eq!(frames.len(), 1);
let result = decoder.decode(&frames[0]).unwrap();
match result {
DecodedData::Plot(frame) => {
assert_eq!(frame.channels.len(), 1);
assert_eq!(frame.channels[0].len(), 3);
assert!((frame.channels[0][0] - 1.0).abs() < 1e-5);
assert!((frame.channels[0][1] - (-2.5)).abs() < 1e-5);
assert!((frame.channels[0][2] - 0.0).abs() < 1e-5);
}
other => panic!("expected Plot, got {:?}", other),
}
server.await.unwrap();
}
#[tokio::test]
async fn tcp_mixed_text_and_plot_single_stream() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
let payload = [
0x00, 0x00, 0x80, 0x3f, // 1.0
0x00, 0x00, 0x00, 0x40, // 2.0
];
let packet = MixedTextPlotDecoder::build_plot_packet(
SampleType::F32,
Endian::Little,
1,
PlotFormat::Interleaved,
2,
&payload,
);
let mut mixed = b"status ok\n".to_vec();
mixed.push(MIXED_PLOT_ESCAPE);
mixed.push(MIXED_PLOT_MARKER);
mixed.extend_from_slice(&raw_cobs_encode(&packet));
mixed.push(0x00);
mixed.extend_from_slice(b"done\n");
stream.write_all(&mixed).await.unwrap();
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut framer = MixedTextPlotFramer::new(MixedFramerConfig::default());
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
let decoder = MixedTextPlotDecoder::new(MixedTextPlotConfig::default());
let results: Vec<DecodedData> = frames.iter().filter_map(|f| decoder.decode(f)).collect();
assert_eq!(results.len(), 3);
assert!(matches!(&results[0], DecodedData::Text(s) if s == "status ok"));
match &results[1] {
DecodedData::Plot(frame) => assert_eq!(frame.channels[0], vec![1.0, 2.0]),
other => panic!("expected Plot, got {other:?}"),
}
assert!(matches!(&results[2], DecodedData::Text(s) if s == "done"));
server.await.unwrap();
}
#[tokio::test]
async fn tcp_fixed_plot_two_channel_u16() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
// 2 channels × 2 samples each × 2 bytes = 8 bytes
let samples: [u16; 4] = [100, 200, 300, 400];
let mut data = Vec::new();
for s in &samples {
data.extend_from_slice(&s.to_le_bytes());
}
stream.write_all(&data).await.unwrap();
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut framer = FixedLengthFramer::new(8);
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
let decoder = PlotDecoder::new(PlotConfig {
sample_type: SampleType::U16,
endian: Endian::Little,
channels: 2,
format: PlotFormat::Interleaved,
});
assert_eq!(frames.len(), 1);
let result = decoder.decode(&frames[0]).unwrap();
match result {
DecodedData::Plot(frame) => {
assert_eq!(frame.channels.len(), 2);
assert_eq!(frame.channels[0], vec![100.0, 300.0]);
assert_eq!(frame.channels[1], vec![200.0, 400.0]);
}
other => panic!("expected Plot, got {:?}", other),
}
server.await.unwrap();
}
// ══════════════════════════════════════════════════════════════════════
// UDP + CobsFramer + TextDecoder
// ══════════════════════════════════════════════════════════════════════
fn cobs_encode(data: &[u8]) -> Vec<u8> {
if data.is_empty() {
return vec![0x01, 0x00];
}
let mut out = Vec::new();
let mut block_start = 0;
for (i, &byte) in data.iter().enumerate() {
if byte == 0x00 || i - block_start == 254 {
let len = i - block_start + 1;
out.push(len as u8);
out.extend_from_slice(&data[block_start..i]);
block_start = i + if byte == 0x00 { 1 } else { 0 };
}
}
if block_start < data.len() {
let len = data.len() - block_start + 1;
out.push(len as u8);
out.extend_from_slice(&data[block_start..]);
}
out.push(0x00);
out
}
#[tokio::test]
async fn udp_cobs_text() {
let remote = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let remote_addr = remote.local_addr().unwrap().to_string();
let mut conn = Connection::new(TransportConfig::Udp {
bind_addr: "127.0.0.1:0".into(),
remote_addr: Some(remote_addr.clone()),
});
conn.connect().await.unwrap();
// Send a probe so the remote learns our actual port
AsyncWriteExt::write_all(&mut conn, b"x").await.unwrap();
let mut probe_buf = [0u8; 1];
let (_, conn_actual_addr) = remote.recv_from(&mut probe_buf).await.unwrap();
// Send COBS-encoded frames to the connection's actual port
let packet1 = cobs_encode(b"hello");
let packet2 = cobs_encode(b"world");
let mut combined = packet1;
combined.extend_from_slice(&packet2);
remote.send_to(&combined, conn_actual_addr).await.unwrap();
let mut framer = CobsFramer::default();
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
let decoder = TextDecoder::new(TextEncoding::Utf8);
let results: Vec<String> = frames
.iter()
.filter_map(|f| decoder.decode(f))
.filter_map(|d| match d {
DecodedData::Text(s) => Some(s),
_ => None,
})
.collect();
assert_eq!(results, vec!["hello", "world"]);
}
// ══════════════════════════════════════════════════════════════════════
// Connection lifecycle: connect → use → disconnect → reconnect → use
// ══════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn connection_reconnect_tcp() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
// First connection
let server1 = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
stream.write_all(b"first\n").await.unwrap();
});
let mut conn = Connection::new(TransportConfig::Tcp { addr: addr.clone() });
conn.connect().await.unwrap();
let mut framer = LineFramer::new(LineConfig::default());
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
server1.await.unwrap();
let decoder = TextDecoder::new(TextEncoding::Utf8);
let results: Vec<String> = frames
.iter()
.filter_map(|f| decoder.decode(f))
.filter_map(|d| match d {
DecodedData::Text(s) => Some(s),
_ => None,
})
.collect();
assert_eq!(results, vec!["first"]);
// Framer state should not carry over (it was fully consumed)
framer.reset();
// Second connection
let listener2 = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr2 = listener2.local_addr().unwrap().to_string();
let server2 = tokio::spawn(async move {
let (mut stream, _) = listener2.accept().await.unwrap();
stream.write_all(b"second\n").await.unwrap();
});
let mut conn2 = Connection::new(TransportConfig::Tcp { addr: addr2 });
conn2.connect().await.unwrap();
let frames = read_to_framer(&mut conn2, &mut framer).await;
conn2.disconnect().await.unwrap();
server2.await.unwrap();
let results: Vec<String> = frames
.iter()
.filter_map(|f| decoder.decode(f))
.filter_map(|d| match d {
DecodedData::Text(s) => Some(s),
_ => None,
})
.collect();
assert_eq!(results, vec!["second"]);
}
// ══════════════════════════════════════════════════════════════════════
// Framer reset mid-stream
// ══════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn framer_reset_mid_stream() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
stream.write_all(b"garbage__data\nvalid\n").await.unwrap();
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut framer = LineFramer::new(LineConfig::default());
// Read exactly "garbage_" (8 bytes) — no newline yet
let mut buf = [0u8; 8];
let n = timeout(TEST_TIMEOUT, AsyncReadExt::read(&mut conn, &mut buf))
.await
.unwrap()
.unwrap();
let frames = framer.feed(&buf[..n]);
assert!(frames.is_empty());
assert!(framer.pending_len() > 0);
// Reset — discard the incomplete "garbage_"
framer.reset();
assert_eq!(framer.pending_len(), 0);
// Read remaining data
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
server.await.unwrap();
let decoder = TextDecoder::new(TextEncoding::Utf8);
let results: Vec<String> = frames
.iter()
.filter_map(|f| decoder.decode(f))
.filter_map(|d| match d {
DecodedData::Text(s) => Some(s),
_ => None,
})
.collect();
assert_eq!(results, vec!["_data", "valid"]);
}
// ══════════════════════════════════════════════════════════════════════
// Multi-frame burst + edge cases
// ══════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn tcp_many_frames_burst() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
// 50 lines in one write
let mut data = String::new();
for i in 0..50 {
data.push_str(&format!("line_{}\n", i));
}
stream.write_all(data.as_bytes()).await.unwrap();
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut framer = LineFramer::new(LineConfig::default());
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
server.await.unwrap();
assert_eq!(frames.len(), 50);
let decoder = TextDecoder::new(TextEncoding::Utf8);
let results: Vec<String> = frames
.iter()
.filter_map(|f| decoder.decode(f))
.filter_map(|d| match d {
DecodedData::Text(s) => Some(s),
_ => None,
})
.collect();
assert_eq!(results.len(), 50);
for (i, line) in results.iter().enumerate() {
assert_eq!(line, &format!("line_{}", i));
}
}
#[tokio::test]
async fn tcp_trailing_data_flushed_on_disconnect() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
stream.write_all(b"no_newline_at_end").await.unwrap();
// Close without sending \n
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut framer = LineFramer::new(LineConfig::default());
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
server.await.unwrap();
// flush() in read_to_framer should capture the trailing data
assert_eq!(frames.len(), 1);
assert_eq!(frames[0], b"no_newline_at_end");
}
#[tokio::test]
async fn empty_data_produces_no_frames() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
drop(stream);
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut framer = LineFramer::new(LineConfig::default());
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
server.await.unwrap();
assert!(frames.is_empty());
}
#[tokio::test]
async fn protocol_switching_on_same_connection() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
// Send text line then length-prefixed binary
stream.write_all(b"text_mode\n").await.unwrap();
stream.write_all(&3u16.to_be_bytes()).await.unwrap();
stream.write_all(b"bin").await.unwrap();
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
// Phase 1: text protocol
let mut line_framer = LineFramer::new(LineConfig::default());
let buf = read_chunk(&mut conn).await;
let frames = line_framer.feed(&buf);
let text_decoder = TextDecoder::new(TextEncoding::Utf8);
let results: Vec<_> = frames
.iter()
.filter_map(|f| text_decoder.decode(f))
.collect();
assert_eq!(results.len(), 1);
assert!(matches!(&results[0], DecodedData::Text(s) if s == "text_mode"));
// Phase 2: feed leftovers + remaining data to length-prefixed framer
let mut len_framer = LengthPrefixedFramer::new(LengthConfig::default());
let mut frames = if let Some(rest) = line_framer.flush() {
len_framer.feed(&rest)
} else {
Vec::new()
};
let rest_buf = read_remaining(&mut conn).await;
frames.extend(len_framer.feed(&rest_buf));
let hex_decoder = HexDecoder::new(HexConfig::default());
let results: Vec<_> = frames
.iter()
.filter_map(|f| hex_decoder.decode(f))
.collect();
assert_eq!(results.len(), 1);
assert!(matches!(&results[0], DecodedData::Hex(s) if s == "62 69 6e"));
server.await.unwrap();
}
// ══════════════════════════════════════════════════════════════════════
// TransportType dispatch via Connection enum
// ══════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn connection_transport_type_dispatch() {
let serial = Connection::new(TransportConfig::Serial {
port: "COM1".into(),
baud_rate: 115200,
data_bits: SerialDataBits::Eight,
parity: SerialParity::None,
stop_bits: SerialStopBits::One,
flow_control: SerialFlowControl::None,
dtr: false,
rts: false,
});
let tcp = Connection::new(TransportConfig::Tcp {
addr: "127.0.0.1:8080".into(),
});
let udp = Connection::new(TransportConfig::Udp {
bind_addr: "0.0.0.0:0".into(),
remote_addr: None,
});
assert_eq!(serial.transport_type(), TransportType::Serial);
assert_eq!(tcp.transport_type(), TransportType::Tcp);
assert_eq!(udp.transport_type(), TransportType::Udp);
}
// ══════════════════════════════════════════════════════════════════════
// Multiple framers, one connection
// ══════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn multiple_framers_one_connection() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
stream.write_all(b"a\nbb\nccc\n").await.unwrap();
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut buf = [0u8; 256];
let n = AsyncReadExt::read(&mut conn, &mut buf).await.unwrap();
conn.disconnect().await.unwrap();
server.await.unwrap();
let data = &buf[..n];
// Framer 1: LineFramer
let mut line = LineFramer::new(LineConfig::default());
let line_frames = line.feed(data);
assert_eq!(line_frames.len(), 3);
// Framer 2: FixedLengthFramer (same bytes, different interpretation)
let mut fixed = FixedLengthFramer::new(3);
let fixed_frames = fixed.feed(data);
assert_eq!(fixed_frames.len(), 3);
assert_eq!(fixed_frames[0], b"a\nb");
assert_eq!(fixed_frames[1], b"b\nc");
assert_eq!(fixed_frames[2], b"cc\n");
}
// ══════════════════════════════════════════════════════════════════════
// Robustness: partial writes, slow consumer
// ══════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn tcp_partial_writes_line_framing() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
// Simulate fragmented writes
stream.write_all(b"hel").await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
stream.write_all(b"lo\nwor").await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
stream.write_all(b"ld\n").await.unwrap();
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut framer = LineFramer::new(LineConfig::default());
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
server.await.unwrap();
let decoder = TextDecoder::new(TextEncoding::Utf8);
let results: Vec<String> = frames
.iter()
.filter_map(|f| decoder.decode(f))
.filter_map(|d| match d {
DecodedData::Text(s) => Some(s),
_ => None,
})
.collect();
assert_eq!(results, vec!["hello", "world"]);
}
#[tokio::test]
async fn decode_summary_on_full_pipeline() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
stream.write_all(b"hello\n").await.unwrap();
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut framer = LineFramer::new(LineConfig::default());
let frames = read_to_framer(&mut conn, &mut framer).await;
conn.disconnect().await.unwrap();
server.await.unwrap();
let decoder = TextDecoder::new(TextEncoding::Utf8);
let decoded = decoder.decode(&frames[0]).unwrap();
assert_eq!(decoded.summary(), "hello");
}
// ══════════════════════════════════════════════════════════════════════
// Helpers
// ══════════════════════════════════════════════════════════════════════
async fn read_chunk(conn: &mut Connection) -> Vec<u8> {
let mut buf = [0u8; 4096];
match timeout(TEST_TIMEOUT, AsyncReadExt::read(conn, &mut buf)).await {
Ok(Ok(n)) => buf[..n].to_vec(),
_ => Vec::new(),
}
}
async fn read_remaining(conn: &mut Connection) -> Vec<u8> {
let mut all = Vec::new();
let mut buf = [0u8; 4096];
loop {
match timeout(TEST_TIMEOUT, AsyncReadExt::read(conn, &mut buf)).await {
Ok(Ok(0)) => break,
Ok(Ok(n)) => all.extend_from_slice(&buf[..n]),
_ => break,
}
}
all
}