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:
767
crates/xserial-core/src/transport/mod.rs
Normal file
767
crates/xserial-core/src/transport/mod.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
286
crates/xserial-core/src/transport/serial.rs
Normal file
286
crates/xserial-core/src/transport/serial.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
325
crates/xserial-core/src/transport/tcp.rs
Normal file
325
crates/xserial-core/src/transport/tcp.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
426
crates/xserial-core/src/transport/udp.rs
Normal file
426
crates/xserial-core/src/transport/udp.rs
Normal 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][..]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user