feat: initial xserial-core with transport, protocol, frame, and pipeline layers

- transport: async Serial/TCP/UDP via Connection enum + Transport trait
- protocol: TextDecoder (UTF-8/Latin-1/ASCII), HexDecoder, PlotDecoder (binary samples)
- frame: LineFramer, FixedLengthFramer, LengthPrefixedFramer, CobsFramer
- pipeline: MultiPipeline for parallel multi-protocol parsing
- 212 tests (182 unit + 18 integration + 1 doctest), clippy clean
This commit is contained in:
2026-06-07 19:20:19 +08:00
commit 60c60a8044
28 changed files with 11162 additions and 0 deletions

View File

@@ -0,0 +1,767 @@
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncRead, AsyncWrite};
use crate::transport::serial::SerialTransport;
use crate::transport::tcp::TcpTransport;
use crate::transport::udp::UdpTransport;
use crate::error::Result;
pub mod serial;
pub mod tcp;
pub mod udp;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransportType {
Serial,
Tcp,
Udp
}
impl std::fmt::Display for TransportType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TransportType::Serial => write!(f, "Serial"),
TransportType::Tcp => write!(f, "Tcp"),
TransportType::Udp => write!(f, "Udp")
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TransportConfig {
Serial {
port: String,
baud_rate: u32
},
Tcp {
addr: String
},
Udp {
bind_addr: String,
remote_addr: Option<String>
}
}
#[async_trait]
pub trait Transport: AsyncRead + AsyncWrite + Send + Sync + Unpin {
fn name(&self) -> &str;
fn transport_type(&self) -> TransportType;
fn is_connected(&self) -> bool;
async fn connect(&mut self) -> Result<()>;
async fn disconnect(&mut self) -> Result<()>;
}
#[derive(Debug)]
pub enum Connection {
Serial(SerialTransport),
Tcp(TcpTransport),
Udp(UdpTransport),
}
impl Connection {
pub fn new(config: TransportConfig) -> Self {
match config {
TransportConfig::Serial { port, baud_rate } => {
Connection::Serial(SerialTransport::new(port, baud_rate))
}
TransportConfig::Tcp { addr } => Connection::Tcp(TcpTransport::new(addr)),
TransportConfig::Udp {
bind_addr,
remote_addr,
} => Connection::Udp(UdpTransport::new(bind_addr, remote_addr)),
}
}
pub fn transport_type(&self) -> TransportType {
match self {
Connection::Serial(_) => TransportType::Serial,
Connection::Tcp(_) => TransportType::Tcp,
Connection::Udp(_) => TransportType::Udp,
}
}
pub fn name(&self) -> &str {
match self {
Connection::Serial(t) => t.name(),
Connection::Tcp(t) => t.name(),
Connection::Udp(t) => t.name(),
}
}
pub fn is_connected(&self) -> bool {
match self {
Connection::Serial(t) => t.is_connected(),
Connection::Tcp(t) => t.is_connected(),
Connection::Udp(t) => t.is_connected(),
}
}
pub async fn connect(&mut self) -> Result<()> {
match self {
Connection::Serial(t) => t.connect().await,
Connection::Tcp(t) => t.connect().await,
Connection::Udp(t) => t.connect().await,
}
}
pub async fn disconnect(&mut self) -> Result<()> {
match self {
Connection::Serial(t) => t.disconnect().await,
Connection::Tcp(t) => t.disconnect().await,
Connection::Udp(t) => t.disconnect().await,
}
}
}
impl AsyncRead for Connection {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match self.get_mut() {
Connection::Serial(t) => std::pin::Pin::new(t).poll_read(cx, buf),
Connection::Tcp(t) => std::pin::Pin::new(t).poll_read(cx, buf),
Connection::Udp(t) => std::pin::Pin::new(t).poll_read(cx, buf),
}
}
}
impl AsyncWrite for Connection {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
match self.get_mut() {
Connection::Serial(t) => std::pin::Pin::new(t).poll_write(cx, buf),
Connection::Tcp(t) => std::pin::Pin::new(t).poll_write(cx, buf),
Connection::Udp(t) => std::pin::Pin::new(t).poll_write(cx, buf),
}
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match self.get_mut() {
Connection::Serial(t) => std::pin::Pin::new(t).poll_flush(cx),
Connection::Tcp(t) => std::pin::Pin::new(t).poll_flush(cx),
Connection::Udp(t) => std::pin::Pin::new(t).poll_flush(cx),
}
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match self.get_mut() {
Connection::Serial(t) => std::pin::Pin::new(t).poll_shutdown(cx),
Connection::Tcp(t) => std::pin::Pin::new(t).poll_shutdown(cx),
Connection::Udp(t) => std::pin::Pin::new(t).poll_shutdown(cx),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transport_type_display() {
assert_eq!(TransportType::Serial.to_string(), "Serial");
assert_eq!(TransportType::Tcp.to_string(), "Tcp");
assert_eq!(TransportType::Udp.to_string(), "Udp");
}
#[test]
fn test_transport_type_debug() {
let _ = format!("{:?}", TransportType::Serial);
let _ = format!("{:?}", TransportType::Tcp);
let _ = format!("{:?}", TransportType::Udp);
}
#[test]
fn test_transport_type_eq() {
assert_eq!(TransportType::Serial, TransportType::Serial);
assert_eq!(TransportType::Tcp, TransportType::Tcp);
assert_eq!(TransportType::Udp, TransportType::Udp);
assert_ne!(TransportType::Serial, TransportType::Tcp);
assert_ne!(TransportType::Serial, TransportType::Udp);
assert_ne!(TransportType::Tcp, TransportType::Udp);
}
#[test]
fn test_transport_type_copy_clone() {
let original = TransportType::Serial;
let cloned = original;
assert_eq!(original, cloned);
let original = TransportType::Tcp;
let cloned = original.clone();
assert_eq!(original, cloned);
let original = TransportType::Udp;
let cloned = original.clone();
assert_eq!(original, cloned);
}
#[test]
fn test_transport_type_serialize() {
assert_eq!(
serde_json::to_string(&TransportType::Serial).unwrap(),
"\"Serial\""
);
assert_eq!(
serde_json::to_string(&TransportType::Tcp).unwrap(),
"\"Tcp\""
);
assert_eq!(
serde_json::to_string(&TransportType::Udp).unwrap(),
"\"Udp\""
);
}
#[test]
fn test_transport_type_deserialize() {
let t: TransportType = serde_json::from_str("\"Serial\"").unwrap();
assert_eq!(t, TransportType::Serial);
let t: TransportType = serde_json::from_str("\"Tcp\"").unwrap();
assert_eq!(t, TransportType::Tcp);
let t: TransportType = serde_json::from_str("\"Udp\"").unwrap();
assert_eq!(t, TransportType::Udp);
}
#[test]
fn test_transport_type_roundtrip() {
for original in [TransportType::Serial, TransportType::Tcp, TransportType::Udp] {
let json = serde_json::to_string(&original).unwrap();
let roundtripped: TransportType = serde_json::from_str(&json).unwrap();
assert_eq!(original, roundtripped);
}
}
#[test]
fn test_transport_config_debug() {
let cfg = TransportConfig::Serial {
port: "COM1".into(),
baud_rate: 115200,
};
let _ = format!("{:?}", cfg);
let cfg = TransportConfig::Tcp {
addr: "127.0.0.1:8080".into(),
};
let _ = format!("{:?}", cfg);
let cfg = TransportConfig::Udp {
bind_addr: "0.0.0.0:9000".into(),
remote_addr: Some("192.168.1.1:9001".into()),
};
let _ = format!("{:?}", cfg);
}
#[test]
fn test_transport_config_clone() {
let original = TransportConfig::Serial {
port: "COM1".into(),
baud_rate: 115200,
};
let cloned = original.clone();
assert_eq!(format!("{:?}", original), format!("{:?}", cloned));
let original = TransportConfig::Tcp {
addr: "127.0.0.1:8080".into(),
};
let cloned = original.clone();
assert_eq!(format!("{:?}", original), format!("{:?}", cloned));
let original = TransportConfig::Udp {
bind_addr: "0.0.0.0:9000".into(),
remote_addr: Some("192.168.1.1:9001".into()),
};
let cloned = original.clone();
assert_eq!(format!("{:?}", original), format!("{:?}", cloned));
}
#[test]
fn test_transport_config_serialize_serial() {
let cfg = TransportConfig::Serial {
port: "COM1".into(),
baud_rate: 115200,
};
let json = serde_json::to_string(&cfg).unwrap();
assert!(json.contains("COM1"));
assert!(json.contains("115200"));
}
#[test]
fn test_transport_config_deserialize_serial() {
let json = r#"{"Serial":{"port":"COM1","baud_rate":9600}}"#;
let cfg: TransportConfig = serde_json::from_str(json).unwrap();
match cfg {
TransportConfig::Serial { port, baud_rate } => {
assert_eq!(port, "COM1");
assert_eq!(baud_rate, 9600);
}
_ => panic!("expected Serial variant"),
}
}
#[test]
fn test_transport_config_serialize_tcp() {
let cfg = TransportConfig::Tcp {
addr: "127.0.0.1:8080".into(),
};
let json = serde_json::to_string(&cfg).unwrap();
assert!(json.contains("127.0.0.1:8080"));
}
#[test]
fn test_transport_config_deserialize_tcp() {
let json = r#"{"Tcp":{"addr":"127.0.0.1:8080"}}"#;
let cfg: TransportConfig = serde_json::from_str(json).unwrap();
match cfg {
TransportConfig::Tcp { addr } => {
assert_eq!(addr, "127.0.0.1:8080");
}
_ => panic!("expected Tcp variant"),
}
}
#[test]
fn test_transport_config_serialize_udp_with_remote() {
let cfg = TransportConfig::Udp {
bind_addr: "0.0.0.0:9000".into(),
remote_addr: Some("192.168.1.1:9001".into()),
};
let json = serde_json::to_string(&cfg).unwrap();
assert!(json.contains("0.0.0.0:9000"));
assert!(json.contains("192.168.1.1:9001"));
}
#[test]
fn test_transport_config_serialize_udp_without_remote() {
let cfg = TransportConfig::Udp {
bind_addr: "0.0.0.0:9000".into(),
remote_addr: None,
};
let json = serde_json::to_string(&cfg).unwrap();
assert!(json.contains("0.0.0.0:9000"));
assert!(json.contains("null"));
}
#[test]
fn test_transport_config_deserialize_udp() {
let json = r#"{"Udp":{"bind_addr":"0.0.0.0:9000","remote_addr":"192.168.1.1:9001"}}"#;
let cfg: TransportConfig = serde_json::from_str(json).unwrap();
match cfg {
TransportConfig::Udp {
bind_addr,
remote_addr,
} => {
assert_eq!(bind_addr, "0.0.0.0:9000");
assert_eq!(remote_addr, Some("192.168.1.1:9001".into()));
}
_ => panic!("expected Udp variant"),
}
let json = r#"{"Udp":{"bind_addr":"0.0.0.0:9000","remote_addr":null}}"#;
let cfg: TransportConfig = serde_json::from_str(json).unwrap();
match cfg {
TransportConfig::Udp {
bind_addr,
remote_addr,
} => {
assert_eq!(bind_addr, "0.0.0.0:9000");
assert_eq!(remote_addr, None);
}
_ => panic!("expected Udp variant"),
}
}
#[test]
fn test_transport_config_roundtrip() {
let configs = vec![
TransportConfig::Serial {
port: "COM3".into(),
baud_rate: 57600,
},
TransportConfig::Tcp {
addr: "10.0.0.1:9999".into(),
},
TransportConfig::Udp {
bind_addr: "0.0.0.0:7000".into(),
remote_addr: Some("10.0.0.2:7001".into()),
},
TransportConfig::Udp {
bind_addr: "127.0.0.1:8000".into(),
remote_addr: None,
},
];
for original in &configs {
let json = serde_json::to_string(original).unwrap();
let roundtripped: TransportConfig = serde_json::from_str(&json).unwrap();
assert_eq!(
serde_json::to_string(&roundtripped).unwrap(),
json,
"roundtrip serialized forms must match"
);
}
}
// ── Connection tests ──────────────────────────────────────────────
#[test]
fn test_connection_new_serial() {
let conn = Connection::new(TransportConfig::Serial {
port: "COM1".into(),
baud_rate: 115200,
});
assert_eq!(conn.transport_type(), TransportType::Serial);
assert_eq!(conn.name(), "COM1");
assert!(!conn.is_connected());
}
#[test]
fn test_connection_new_tcp() {
let conn = Connection::new(TransportConfig::Tcp {
addr: "192.168.1.1:8080".into(),
});
assert_eq!(conn.transport_type(), TransportType::Tcp);
assert_eq!(conn.name(), "192.168.1.1:8080");
assert!(!conn.is_connected());
}
#[test]
fn test_connection_new_udp_with_remote() {
let conn = Connection::new(TransportConfig::Udp {
bind_addr: "0.0.0.0:9000".into(),
remote_addr: Some("192.168.1.1:9001".into()),
});
assert_eq!(conn.transport_type(), TransportType::Udp);
assert_eq!(conn.name(), "0.0.0.0:9000");
assert!(!conn.is_connected());
}
#[test]
fn test_connection_new_udp_without_remote() {
let conn = Connection::new(TransportConfig::Udp {
bind_addr: "127.0.0.1:0".into(),
remote_addr: None,
});
assert_eq!(conn.transport_type(), TransportType::Udp);
assert_eq!(conn.name(), "127.0.0.1:0");
assert!(!conn.is_connected());
}
#[test]
fn test_connection_is_connected_default() {
let serial = Connection::new(TransportConfig::Serial {
port: "COM1".into(),
baud_rate: 9600,
});
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!(!serial.is_connected());
assert!(!tcp.is_connected());
assert!(!udp.is_connected());
}
#[test]
fn test_connection_debug() {
let serial = Connection::new(TransportConfig::Serial {
port: "COM1".into(),
baud_rate: 115200,
});
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,
});
let _ = format!("{:?}", serial);
let _ = format!("{:?}", tcp);
let _ = format!("{:?}", udp);
}
fn noop_waker() -> std::task::Waker {
use std::task::{RawWaker, RawWakerVTable};
unsafe fn clone(_: *const ()) -> RawWaker {
RawWaker::new(std::ptr::null(), &VTABLE)
}
unsafe fn wake(_: *const ()) {}
unsafe fn wake_by_ref(_: *const ()) {}
unsafe fn drop(_: *const ()) {}
static VTABLE: RawWakerVTable =
RawWakerVTable::new(clone, wake, wake_by_ref, drop);
unsafe { std::task::Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) }
}
fn serial_conn() -> Connection {
Connection::new(TransportConfig::Serial {
port: "COM1".into(),
baud_rate: 115200,
})
}
fn tcp_conn() -> Connection {
Connection::new(TransportConfig::Tcp {
addr: "127.0.0.1:8080".into(),
})
}
fn udp_conn() -> Connection {
Connection::new(TransportConfig::Udp {
bind_addr: "127.0.0.1:0".into(),
remote_addr: None,
})
}
#[test]
fn test_connection_poll_read_not_connected() {
use tokio::io::ReadBuf;
use std::pin::Pin;
use std::task::{Context, Poll};
for mut conn in [serial_conn(), tcp_conn(), udp_conn()] {
let pinned = Pin::new(&mut conn);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
let mut buf_data = [0u8; 16];
let mut buf = ReadBuf::new(&mut buf_data);
match pinned.poll_read(&mut cx, &mut buf) {
Poll::Ready(Err(e)) => {
assert_eq!(e.kind(), std::io::ErrorKind::NotConnected);
}
other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other),
}
}
}
#[test]
fn test_connection_poll_write_not_connected() {
use std::pin::Pin;
use std::task::{Context, Poll};
for mut conn in [serial_conn(), tcp_conn(), udp_conn()] {
let pinned = Pin::new(&mut conn);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_write(&mut cx, b"hello") {
Poll::Ready(Err(e)) => {
assert_eq!(e.kind(), std::io::ErrorKind::NotConnected);
}
other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other),
}
}
}
#[test]
fn test_connection_poll_flush_not_connected() {
use std::pin::Pin;
use std::task::{Context, Poll};
for mut conn in [serial_conn(), tcp_conn(), udp_conn()] {
let pinned = Pin::new(&mut conn);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_flush(&mut cx) {
Poll::Ready(Ok(())) => {}
other => panic!("expected Poll::Ready(Ok(())), got {:?}", other),
}
}
}
#[test]
fn test_connection_poll_shutdown_not_connected() {
use std::pin::Pin;
use std::task::{Context, Poll};
for mut conn in [serial_conn(), tcp_conn(), udp_conn()] {
let pinned = Pin::new(&mut conn);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_shutdown(&mut cx) {
Poll::Ready(Ok(())) => {}
other => panic!("expected Poll::Ready(Ok(())), got {:?}", other),
}
}
}
#[tokio::test]
async fn test_connection_tcp_connect_and_disconnect() {
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let mut conn = Connection::new(TransportConfig::Tcp { addr });
assert!(!conn.is_connected());
conn.connect().await.unwrap();
assert!(conn.is_connected());
conn.disconnect().await.unwrap();
assert!(!conn.is_connected());
}
#[tokio::test]
async fn test_connection_udp_connect_and_disconnect_without_remote() {
let mut conn = Connection::new(TransportConfig::Udp {
bind_addr: "127.0.0.1:0".into(),
remote_addr: None,
});
assert!(!conn.is_connected());
conn.connect().await.unwrap();
assert!(conn.is_connected());
conn.disconnect().await.unwrap();
assert!(!conn.is_connected());
}
#[tokio::test]
async fn test_connection_udp_connect_and_disconnect_with_remote() {
use tokio::net::UdpSocket;
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),
});
assert!(!conn.is_connected());
conn.connect().await.unwrap();
assert!(conn.is_connected());
conn.disconnect().await.unwrap();
assert!(!conn.is_connected());
}
#[tokio::test]
async fn test_connection_tcp_double_connect_is_noop() {
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
assert!(conn.is_connected());
let result = conn.connect().await;
assert!(result.is_ok(), "double connect should be a no-op");
assert!(conn.is_connected());
}
#[tokio::test]
async fn test_connection_tcp_disconnect_without_connect() {
let mut conn = Connection::new(TransportConfig::Tcp {
addr: "127.0.0.1:8080".into(),
});
let result = conn.disconnect().await;
assert!(result.is_ok(), "disconnect without connect should be safe");
assert!(!conn.is_connected());
}
#[tokio::test]
async fn test_connection_tcp_connect_to_unreachable() {
let mut conn = Connection::new(TransportConfig::Tcp {
addr: "127.0.0.1:1".into(),
});
let result = conn.connect().await;
assert!(result.is_err());
assert!(!conn.is_connected());
}
#[tokio::test]
async fn test_connection_udp_connect_bind_failure() {
let mut conn = Connection::new(TransportConfig::Udp {
bind_addr: "invalid_addr".into(),
remote_addr: None,
});
let result = conn.connect().await;
assert!(result.is_err());
assert!(!conn.is_connected());
}
#[tokio::test]
async fn test_connection_udp_double_connect_is_noop() {
let mut conn = Connection::new(TransportConfig::Udp {
bind_addr: "127.0.0.1:0".into(),
remote_addr: None,
});
conn.connect().await.unwrap();
assert!(conn.is_connected());
let result = conn.connect().await;
assert!(result.is_ok());
assert!(conn.is_connected());
}
#[tokio::test]
async fn test_connection_tcp_async_read_write() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
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();
AsyncWriteExt::write_all(&mut stream, b"hello").await.unwrap();
let mut buf = [0u8; 5];
AsyncReadExt::read_exact(&mut stream, &mut buf).await.unwrap();
assert_eq!(&buf, b"world");
});
let mut conn = Connection::new(TransportConfig::Tcp { addr });
conn.connect().await.unwrap();
let mut buf = [0u8; 5];
AsyncReadExt::read_exact(&mut conn, &mut buf).await.unwrap();
assert_eq!(&buf, b"hello");
AsyncWriteExt::write_all(&mut conn, b"world").await.unwrap();
conn.disconnect().await.unwrap();
server.await.unwrap();
}
#[tokio::test]
async fn test_connection_udp_async_read_write_with_remote() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UdpSocket;
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),
});
conn.connect().await.unwrap();
AsyncWriteExt::write_all(&mut conn, b"hello").await.unwrap();
let mut buf = [0u8; 1024];
let (n, from) = remote.recv_from(&mut buf).await.unwrap();
assert_eq!(&buf[..n], b"hello");
remote.send_to(b"world", from).await.unwrap();
let mut read_buf = [0u8; 5];
AsyncReadExt::read_exact(&mut conn, &mut read_buf).await.unwrap();
assert_eq!(&read_buf, b"world");
}
}

View File

@@ -0,0 +1,286 @@
use async_trait::async_trait;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio_serial::{SerialPortBuilderExt, SerialStream};
use tracing::{debug, info, warn};
use super::{Transport, TransportType};
use crate::error::Result;
#[derive(Debug)]
pub struct SerialTransport {
port: Option<SerialStream>,
port_name: String,
baud_rate: u32,
}
impl SerialTransport {
pub fn new(port_name: String, baud_rate: u32) -> Self {
Self {
port: None,
port_name,
baud_rate,
}
}
pub fn list_ports() -> Vec<serialport::SerialPortInfo> {
match serialport::available_ports() {
Ok(ports) => ports,
Err(e) => {
warn!("Failed to enumerate serial ports: {}", e);
vec![]
}
}
}
pub fn port_name(&self) -> &str {
&self.port_name
}
pub fn baud_rate(&self) -> u32 {
self.baud_rate
}
}
impl AsyncRead for SerialTransport {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut self.get_mut().port {
Some(port) => std::pin::Pin::new(port).poll_read(cx, buf),
None => std::task::Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"Serial port not open",
))),
}
}
}
impl AsyncWrite for SerialTransport {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
match &mut self.get_mut().port {
Some(port) => std::pin::Pin::new(port).poll_write(cx, buf),
None => std::task::Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"Serial port not open",
))),
}
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut self.get_mut().port {
Some(port) => std::pin::Pin::new(port).poll_flush(cx),
None => std::task::Poll::Ready(Ok(())),
}
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut self.get_mut().port {
Some(port) => std::pin::Pin::new(port).poll_shutdown(cx),
None => std::task::Poll::Ready(Ok(())),
}
}
}
#[async_trait]
impl Transport for SerialTransport {
fn name(&self) -> &str {
&self.port_name
}
fn transport_type(&self) -> TransportType {
TransportType::Serial
}
fn is_connected(&self) -> bool {
self.port.is_some()
}
async fn connect(&mut self) -> Result<()> {
if self.is_connected() {
return Ok(())
}
info!("Opening serial port {} at {} baud", self.port_name, self.baud_rate);
let port = tokio_serial::new(&self.port_name, self.baud_rate)
.open_native_async()
.map_err(|e| crate::error::Error::ConnectionFailed(format!(
"Failed to open {}: {}",
self.port_name, e
)))?;
debug!("Serial port {} opened successfully", self.port_name);
self.port = Some(port);
Ok(())
}
async fn disconnect(&mut self) -> Result<()> {
if let Some(port) = self.port.take() {
debug!("Closing serial port {}", self.port_name);
drop(port);
debug!("Serial port {} closed", self.port_name);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::ErrorKind;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use tokio::io::ReadBuf;
fn noop_waker() -> Waker {
unsafe fn raw_clone(_: *const ()) -> RawWaker {
RawWaker::new(std::ptr::null(), &RAW_VTABLE)
}
unsafe fn raw_wake(_: *const ()) {}
unsafe fn raw_wake_by_ref(_: *const ()) {}
unsafe fn raw_drop(_: *const ()) {}
static RAW_VTABLE: RawWakerVTable = RawWakerVTable::new(
raw_clone,
raw_wake,
raw_wake_by_ref,
raw_drop,
);
unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &RAW_VTABLE)) }
}
// ── Constructor and accessor tests ──
#[test]
fn test_new() {
let transport = SerialTransport::new("COM1".into(), 115200);
assert_eq!(transport.port_name(), "COM1");
assert_eq!(transport.baud_rate(), 115200);
}
#[test]
fn test_new_different_params() {
let transport = SerialTransport::new("COM3".into(), 9600);
assert_eq!(transport.port_name(), "COM3");
assert_eq!(transport.baud_rate(), 9600);
}
#[test]
fn test_name() {
let transport = SerialTransport::new("COM1".into(), 115200);
assert_eq!(transport.name(), "COM1");
}
#[test]
fn test_transport_type() {
let transport = SerialTransport::new("COM1".into(), 115200);
assert_eq!(transport.transport_type(), TransportType::Serial);
}
#[test]
fn test_is_connected_false_by_default() {
let transport = SerialTransport::new("COM1".into(), 115200);
assert!(!transport.is_connected());
}
// ── list_ports tests ──
#[test]
fn test_list_ports_does_not_panic() {
let _ = SerialTransport::list_ports();
}
#[test]
fn test_list_ports_returns_vec() {
let ports: Vec<serialport::SerialPortInfo> = SerialTransport::list_ports();
let _ = ports;
}
// ── AsyncRead tests (not connected) ──
#[tokio::test]
async fn test_poll_read_not_connected() {
let mut transport = SerialTransport::new("COM1".into(), 115200);
let pinned = Pin::new(&mut transport);
let mut buf_data = [0u8; 16];
let mut buf = ReadBuf::new(&mut buf_data);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_read(&mut cx, &mut buf) {
Poll::Ready(Err(e)) => assert_eq!(e.kind(), ErrorKind::NotConnected),
other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other),
}
}
// ── AsyncWrite tests (not connected) ──
#[tokio::test]
async fn test_poll_write_not_connected() {
let mut transport = SerialTransport::new("COM1".into(), 115200);
let pinned = Pin::new(&mut transport);
let data = b"hello";
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_write(&mut cx, data) {
Poll::Ready(Err(e)) => assert_eq!(e.kind(), ErrorKind::NotConnected),
other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other),
}
}
#[tokio::test]
async fn test_poll_flush_not_connected() {
let mut transport = SerialTransport::new("COM1".into(), 115200);
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_flush(&mut cx) {
Poll::Ready(Ok(())) => {}
other => panic!("expected Poll::Ready(Ok(())), got {:?}", other),
}
}
#[tokio::test]
async fn test_poll_shutdown_not_connected() {
let mut transport = SerialTransport::new("COM1".into(), 115200);
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_shutdown(&mut cx) {
Poll::Ready(Ok(())) => {}
other => panic!("expected Poll::Ready(Ok(())), got {:?}", other),
}
}
// ── Transport trait method tests ──
#[test]
fn test_transport_name() {
let transport = SerialTransport::new("COM1".into(), 115200);
assert_eq!(transport.name(), "COM1");
}
#[test]
fn test_transport_transport_type() {
let transport = SerialTransport::new("COM1".into(), 115200);
assert_eq!(transport.transport_type(), TransportType::Serial);
}
#[test]
fn test_transport_is_connected() {
let transport = SerialTransport::new("COM1".into(), 115200);
assert!(!transport.is_connected());
}
}

View File

@@ -0,0 +1,325 @@
use async_trait::async_trait;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::net::TcpStream;
use tracing::{debug, info};
use super::{Transport, TransportType};
use crate::error::Result;
#[derive(Debug)]
pub struct TcpTransport {
stream: Option<TcpStream>,
addr: String
}
impl TcpTransport {
pub fn new(addr: String) -> Self {
Self {
stream: None,
addr
}
}
pub fn addr(&self) -> &str {
&self.addr
}
}
impl AsyncRead for TcpTransport {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut self.get_mut().stream {
Some(stream) => std::pin::Pin::new(stream).poll_read(cx, buf),
None => std::task::Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"TCP not connected",
))),
}
}
}
impl AsyncWrite for TcpTransport {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
match &mut self.get_mut().stream {
Some(stream) => std::pin::Pin::new(stream).poll_write(cx, buf),
None => std::task::Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"TCP not connected",
))),
}
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut self.get_mut().stream {
Some(stream) => std::pin::Pin::new(stream).poll_flush(cx),
None => std::task::Poll::Ready(Ok(())),
}
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
match &mut self.get_mut().stream {
Some(stream) => std::pin::Pin::new(stream).poll_shutdown(cx),
None => std::task::Poll::Ready(Ok(())),
}
}
}
#[async_trait]
impl Transport for TcpTransport {
fn name(&self) -> &str {
&self.addr
}
fn transport_type(&self) -> TransportType {
TransportType::Tcp
}
fn is_connected(&self) -> bool {
self.stream.is_some()
}
async fn connect(&mut self) -> Result<()> {
if self.is_connected() {
return Ok(());
}
info!("Connecting to TCP {}", self.addr);
let stream = TcpStream::connect(&self.addr).await.map_err(|e| {
crate::error::Error::ConnectionFailed(format!(
"Failed to connect to {}: {}",
self.addr, e
))
})?;
debug!("TCP connection to {} established", self.addr);
self.stream = Some(stream);
Ok(())
}
async fn disconnect(&mut self) -> Result<()> {
if let Some(stream) = self.stream.take() {
debug!("Closing TCP connection {}", self.addr);
drop(stream);
info!("TCP connection {} closed", self.addr);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadBuf};
use tokio::net::TcpListener;
// ── Helper: noop waker for poll tests ─────────────────────────────
fn noop_waker() -> Waker {
unsafe fn noop_raw_clone(data: *const ()) -> RawWaker {
RawWaker::new(data, &NOOP_VTABLE)
}
unsafe fn noop_raw_wake(_data: *const ()) {}
unsafe fn noop_raw_wake_by_ref(_data: *const ()) {}
unsafe fn noop_raw_drop(_data: *const ()) {}
static NOOP_VTABLE: RawWakerVTable = RawWakerVTable::new(
noop_raw_clone,
noop_raw_wake,
noop_raw_wake_by_ref,
noop_raw_drop,
);
unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &NOOP_VTABLE)) }
}
// ── Sync accessor tests ───────────────────────────────────────────
#[test]
fn test_new() {
let transport = TcpTransport::new("127.0.0.1:8080".into());
assert_eq!(transport.addr(), "127.0.0.1:8080");
}
#[test]
fn test_name() {
let transport = TcpTransport::new("127.0.0.1:8080".into());
assert_eq!(transport.name(), transport.addr());
assert_eq!(transport.name(), "127.0.0.1:8080");
}
#[test]
fn test_transport_type() {
let transport = TcpTransport::new("127.0.0.1:8080".into());
assert_eq!(transport.transport_type(), TransportType::Tcp);
}
#[test]
fn test_is_connected_false_by_default() {
let transport = TcpTransport::new("127.0.0.1:8080".into());
assert!(!transport.is_connected());
}
// ── Poll error-state tests (no real connection) ───────────────────
#[test]
fn test_poll_read_not_connected() {
let mut transport = TcpTransport::new("127.0.0.1:8080".into());
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
let mut buf_data = [0u8; 16];
let mut buf = ReadBuf::new(&mut buf_data);
match pinned.poll_read(&mut cx, &mut buf) {
Poll::Ready(Err(e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected),
other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other),
}
}
#[test]
fn test_poll_write_not_connected() {
let mut transport = TcpTransport::new("127.0.0.1:8080".into());
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_write(&mut cx, b"hello") {
Poll::Ready(Err(e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected),
other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other),
}
}
#[test]
fn test_poll_flush_not_connected() {
let mut transport = TcpTransport::new("127.0.0.1:8080".into());
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_flush(&mut cx) {
Poll::Ready(Ok(())) => {} // expected
other => panic!("expected Poll::Ready(Ok(())), got {:?}", other),
}
}
#[test]
fn test_poll_shutdown_not_connected() {
let mut transport = TcpTransport::new("127.0.0.1:8080".into());
let pinned = Pin::new(&mut transport);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
match pinned.poll_shutdown(&mut cx) {
Poll::Ready(Ok(())) => {} // expected
other => panic!("expected Poll::Ready(Ok(())), got {:?}", other),
}
}
// ── Async connect/disconnect tests ────────────────────────────────
#[tokio::test]
async fn test_connect_and_disconnect() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let mut transport = TcpTransport::new(addr);
assert!(!transport.is_connected());
transport.connect().await.unwrap();
assert!(transport.is_connected());
transport.disconnect().await.unwrap();
assert!(!transport.is_connected());
}
#[tokio::test]
async fn test_double_connect_is_noop() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
let mut transport = TcpTransport::new(addr);
transport.connect().await.unwrap();
assert!(transport.is_connected());
// Second connect should be a safe no-op
let result = transport.connect().await;
assert!(result.is_ok(), "double connect should return Ok");
assert!(transport.is_connected());
}
#[tokio::test]
async fn test_connect_to_unreachable_addr() {
let mut transport = TcpTransport::new("127.0.0.1:1".into());
let result = transport.connect().await;
assert!(result.is_err());
match result.unwrap_err() {
Error::ConnectionFailed(_) => {} // expected
other => panic!("expected ConnectionFailed, got {:?}", other),
}
assert!(!transport.is_connected());
}
#[tokio::test]
async fn test_disconnect_without_connect() {
let mut transport = TcpTransport::new("127.0.0.1:8080".into());
let result = transport.disconnect().await;
assert!(result.is_ok(), "disconnect without connect should be a safe no-op");
assert!(!transport.is_connected());
}
// ── Async read/write with real connection ─────────────────────────
#[tokio::test]
async fn test_async_read_write() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap().to_string();
// Server: accept one connection, write "hello", read 5 bytes back, verify "world"
let server_handle = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
let written = tokio::io::AsyncWriteExt::write_all(&mut stream, b"hello").await;
assert!(written.is_ok(), "server write_all hello failed");
let mut buf = [0u8; 5];
let read = tokio::io::AsyncReadExt::read_exact(&mut stream, &mut buf).await;
assert!(read.is_ok(), "server read_exact failed");
assert_eq!(&buf, b"world");
});
// Client: connect, read "hello", write "world", disconnect
let mut transport = TcpTransport::new(addr);
transport.connect().await.unwrap();
let mut read_buf = [0u8; 5];
AsyncReadExt::read_exact(&mut transport, &mut read_buf)
.await
.unwrap();
assert_eq!(&read_buf, b"hello");
AsyncWriteExt::write_all(&mut transport, b"world")
.await
.unwrap();
transport.disconnect().await.unwrap();
// Ensure server task completed without panic
server_handle.await.unwrap();
}
}

View File

@@ -0,0 +1,426 @@
use std::pin::Pin;
use std::task::{Context, Poll};
use async_trait::async_trait;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::net::UdpSocket;
use tracing::{debug, info};
use super::{Transport, TransportType};
use crate::error::Result;
#[derive(Debug)]
pub struct UdpTransport {
socket: Option<UdpSocket>,
bind_addr: String,
remote_addr: Option<String>,
read_buf: Vec<u8>,
read_pos: usize,
temp_recv_buf: Vec<u8>,
}
impl UdpTransport {
pub fn new(bind_addr: String, remote_addr: Option<String>) -> Self {
Self {
socket: None,
bind_addr,
remote_addr,
read_buf: Vec::new(),
read_pos: 0,
temp_recv_buf: vec![0u8; 65536], // 初始化一次,重复使用
}
}
pub fn bind_addr(&self) -> &str {
&self.bind_addr
}
}
impl AsyncRead for UdpTransport {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
let this = self.get_mut();
// 优先处理遗留的内部缓冲数据
if this.read_pos < this.read_buf.len() {
let remaining = &this.read_buf[this.read_pos..];
let to_copy = std::cmp::min(remaining.len(), buf.remaining());
buf.put_slice(&remaining[..to_copy]);
this.read_pos += to_copy;
// 如果缓冲区数据已被全部读完,清空它以便后续复用,避免无止境增长
if this.read_pos == this.read_buf.len() {
this.read_buf.clear();
this.read_pos = 0;
}
return Poll::Ready(Ok(()));
}
// 如果内部缓冲为空,尝试从 Socket 读取新的 UDP 包
let socket = match this.socket.as_ref() {
Some(s) => s,
None => {
return Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"UDP socket is not connected",
)));
}
};
// 使用复用的 temp_recv_buf包裹为 tokio 需要的 ReadBuf
let mut temp_buf = ReadBuf::new(&mut this.temp_recv_buf);
// 如果配置了 remote_addr说明 socket 已经 connect 过,可以用 poll_recv
// 否则只能用 poll_recv_from丢弃掉远端地址信息
let poll_result = if this.remote_addr.is_some() {
socket.poll_recv(cx, &mut temp_buf)
} else {
// poll_recv_from 会返回 (usize, SocketAddr),我们将其映射回统一的 () 类型
socket.poll_recv_from(cx, &mut temp_buf).map(|res| res.map(|_| ()))
};
match poll_result {
Poll::Ready(Ok(())) => {
let filled = temp_buf.filled();
let to_copy = std::cmp::min(filled.len(), buf.remaining());
buf.put_slice(&filled[..to_copy]);
// 核心逻辑:如果本次读取到的 UDP 包大于外面提供的 buf 的剩余空间
// 将没装下的剩余部分放进内部的 read_buf 留作下次 poll_read 使用
if to_copy < filled.len() {
this.read_buf.clear(); // 确保安全清空
this.read_buf.extend_from_slice(&filled[to_copy..]);
this.read_pos = 0;
}
Poll::Ready(Ok(()))
}
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
Poll::Pending => Poll::Pending,
}
}
}
impl AsyncWrite for UdpTransport {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
let this = self.get_mut();
let socket = match this.socket.as_ref() {
Some(s) => s,
None => {
return Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"UDP socket is not connected",
)));
}
};
// AsyncWrite 是不带目标地址的流式写入接口。
// 因此必须绑定了远端地址 (remote_addr) 才能确切知道把 UDP 包发送给谁。
if this.remote_addr.is_some() {
socket.poll_send(cx, buf)
} else {
Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Remote address must be set to use AsyncWrite for UDP",
)))
}
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
// UDP 是无连接的数据报协议,没有缓冲区需要手动 flush
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
// UDP 没有 TCP 的四次挥手过程,直接 Ready
Poll::Ready(Ok(()))
}
}
#[async_trait]
impl Transport for UdpTransport {
fn name(&self) -> &str {
&self.bind_addr
}
fn transport_type(&self) -> TransportType {
TransportType::Udp
}
fn is_connected(&self) -> bool {
self.socket.is_some()
}
async fn connect(&mut self) -> Result<()> {
if self.is_connected() {
return Ok(());
}
info!("Binding UDP socket to {}", self.bind_addr);
let socket = UdpSocket::bind(&self.bind_addr).await.map_err(|e| {
crate::error::Error::ConnectionFailed(format!(
"Failed to bind UDP {}: {}",
self.bind_addr, e
))
})?;
if let Some(ref remote) = self.remote_addr {
socket.connect(remote).await.map_err(|e| {
crate::error::Error::ConnectionFailed(format!(
"Failed to connect UDP to {}: {}",
remote, e
))
})?;
debug!("UDP connected to {}", remote);
}
debug!("UDP socket bound to {}", self.bind_addr);
self.socket = Some(socket);
Ok(())
}
async fn disconnect(&mut self) -> Result<()> {
if let Some(socket) = self.socket.take() {
debug!("Closing UDP socket {}", self.bind_addr);
drop(socket);
info!("UDP socket {} closed", self.bind_addr);
}
self.read_buf.clear();
self.read_pos = 0;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use std::sync::LazyLock;
use std::task::{RawWaker, RawWakerVTable, Waker};
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadBuf};
use tokio::net::UdpSocket;
static NOOP_VTABLE: RawWakerVTable = RawWakerVTable::new(
|_| RawWaker::new(std::ptr::null(), &NOOP_VTABLE),
|_| {},
|_| {},
|_| {},
);
fn noop_waker() -> Waker {
unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &NOOP_VTABLE)) }
}
static NOOP_WAKER: LazyLock<Waker> = LazyLock::new(noop_waker);
fn noop_context() -> Context<'static> {
Context::from_waker(&NOOP_WAKER)
}
#[test]
fn test_new_with_remote() {
let t = UdpTransport::new("127.0.0.1:9000".into(), Some("127.0.0.1:9001".into()));
assert_eq!(t.bind_addr(), "127.0.0.1:9000");
}
#[test]
fn test_new_without_remote() {
let t = UdpTransport::new("0.0.0.0:0".into(), None);
assert_eq!(t.bind_addr(), "0.0.0.0:0");
}
#[test]
fn test_name() {
let t = UdpTransport::new("192.168.1.1:8888".into(), None);
assert_eq!(t.name(), "192.168.1.1:8888");
}
#[test]
fn test_transport_type() {
let t = UdpTransport::new("127.0.0.1:0".into(), None);
assert_eq!(t.transport_type(), TransportType::Udp);
}
#[test]
fn test_is_connected_false_by_default() {
let t = UdpTransport::new("127.0.0.1:0".into(), None);
assert!(!t.is_connected());
}
#[test]
fn test_poll_read_not_connected() {
let mut t = UdpTransport::new("127.0.0.1:0".into(), None);
let mut buf_data = [0u8; 64];
let mut buf = ReadBuf::new(&mut buf_data);
let mut cx = noop_context();
let result = Pin::new(&mut t).poll_read(&mut cx, &mut buf);
match result {
Poll::Ready(Err(ref e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected),
other => panic!(
"expected Poll::Ready(Err(NotConnected)), got {other:?}"
),
}
}
#[test]
fn test_poll_write_not_connected() {
let mut t = UdpTransport::new("127.0.0.1:0".into(), None);
let mut cx = noop_context();
let data = b"test";
let result = Pin::new(&mut t).poll_write(&mut cx, data);
match result {
Poll::Ready(Err(ref e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected),
other => panic!(
"expected Poll::Ready(Err(NotConnected)), got {other:?}"
),
}
}
#[test]
fn test_poll_flush_not_connected() {
let mut t = UdpTransport::new("127.0.0.1:0".into(), None);
let mut cx = noop_context();
let result = Pin::new(&mut t).poll_flush(&mut cx);
assert!(matches!(result, Poll::Ready(Ok(()))));
}
#[test]
fn test_poll_shutdown_not_connected() {
let mut t = UdpTransport::new("127.0.0.1:0".into(), None);
let mut cx = noop_context();
let result = Pin::new(&mut t).poll_shutdown(&mut cx);
assert!(matches!(result, Poll::Ready(Ok(()))));
}
#[tokio::test]
async fn test_connect_and_disconnect_without_remote() {
let mut t = UdpTransport::new("127.0.0.1:0".into(), None);
assert!(!t.is_connected());
t.connect().await.expect("connect should succeed");
assert!(t.is_connected());
t.disconnect().await.expect("disconnect should succeed");
assert!(!t.is_connected());
}
#[tokio::test]
async fn test_connect_and_disconnect_with_remote() {
let remote = UdpSocket::bind("127.0.0.1:0").await.expect("remote bind");
let remote_addr = remote.local_addr().unwrap().to_string();
let mut t = UdpTransport::new("127.0.0.1:0".into(), Some(remote_addr));
t.connect().await.expect("connect with remote should succeed");
assert!(t.is_connected());
t.disconnect().await.expect("disconnect should succeed");
assert!(!t.is_connected());
}
#[tokio::test]
async fn test_double_connect_is_noop() {
let mut t = UdpTransport::new("127.0.0.1:0".into(), None);
t.connect().await.expect("first connect");
assert!(t.is_connected());
t.connect().await.expect("second connect (idempotent)");
assert!(t.is_connected());
}
#[tokio::test]
async fn test_connect_bind_failure() {
let mut t = UdpTransport::new("invalid_addr".into(), None);
let result = t.connect().await;
match result {
Err(Error::ConnectionFailed(_)) => {}
other => panic!("expected ConnectionFailed error, got {other:?}"),
}
}
#[tokio::test]
async fn test_async_read_write_with_remote() {
let remote = UdpSocket::bind("127.0.0.1:0").await.expect("remote bind");
let remote_addr = remote.local_addr().unwrap().to_string();
let mut t = UdpTransport::new("127.0.0.1:0".into(), Some(remote_addr));
t.connect().await.expect("connect");
AsyncWriteExt::write_all(&mut t, b"hello")
.await
.expect("write_all hello");
let mut buf = [0u8; 1024];
let (n, transport_addr) = remote.recv_from(&mut buf).await.expect("remote recv_from");
assert_eq!(&buf[..n], b"hello");
remote
.send_to(b"world", transport_addr)
.await
.expect("remote send_to");
let mut read_buf = [0u8; 5];
AsyncReadExt::read_exact(&mut t, &mut read_buf)
.await
.expect("read_exact world");
assert_eq!(&read_buf, b"world");
}
#[tokio::test]
async fn test_write_fails_without_remote() {
let mut t = UdpTransport::new("127.0.0.1:0".into(), None);
t.connect().await.expect("connect (binds but no remote)");
let result = AsyncWriteExt::write_all(&mut t, b"test").await;
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[tokio::test]
async fn test_oversized_datagram_buffering() {
let remote = UdpSocket::bind("127.0.0.1:0").await.expect("remote bind");
let remote_addr = remote.local_addr().unwrap().to_string();
let mut t = UdpTransport::new("127.0.0.1:0".into(), Some(remote_addr));
t.connect().await.expect("connect");
AsyncWriteExt::write_all(&mut t, b"x")
.await
.expect("write ping");
let mut ping_buf = [0u8; 1];
let (_, transport_addr) = remote
.recv_from(&mut ping_buf)
.await
.expect("remote recv ping");
let large_data: Vec<u8> = vec![b'A'; 100];
remote
.send_to(&large_data, transport_addr)
.await
.expect("send large datagram");
let mut small_buf = [0u8; 10];
AsyncReadExt::read_exact(&mut t, &mut small_buf)
.await
.expect("read 10 bytes");
assert_eq!(&small_buf, b"AAAAAAAAAA");
let mut rest_buf = [0u8; 90];
AsyncReadExt::read_exact(&mut t, &mut rest_buf)
.await
.expect("read 90 bytes");
assert_eq!(&rest_buf[..], &vec![b'A'; 90][..]);
}
}