Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
832 lines
31 KiB
Rust
832 lines
31 KiB
Rust
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
|
||
}
|