use anyhow::Result; use tokio::net::{TcpStream, UdpSocket}; use tokio_util::codec::Framed; use futures::{StreamExt, SinkExt}; use rmon_common::protocol::{ClientMessage, AgentMessage, BincodeCodec, PROTOCOL_VERSION}; use std::time::Duration; use tokio::time::sleep; use bytes::BytesMut; use tokio_util::codec::{Decoder, Encoder}; use std::io; use rmon_agent::config::Config; use rmon_agent::run_agent; use std::path::PathBuf; use std::fs; /// Client side codec pub struct ClientCodec; impl Decoder for ClientCodec { type Item = AgentMessage; type Error = io::Error; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { let mut codec = BincodeCodec::::default(); codec.decode(src) } } impl Encoder for ClientCodec { type Error = io::Error; fn encode(&mut self, item: ClientMessage, dst: &mut BytesMut) -> Result<(), Self::Error> { let mut codec = BincodeCodec::::default(); codec.encode(item, dst) } } async fn setup_agent(port: u16, udp_port: u16, config_path: PathBuf, data_dir: &str) -> Result<()> { let config_str = format!(r#" [storage] data_dir = "{}" default_mode = "circular" default_window = "1h" [[sources]] type = "udp" name = "test_source" bind = "127.0.0.1:{}" [sources.timestamp] format = "none" [[sources.signals]] id = "sig1" label = "Sig 1" unit = "V" scale = 1.0 offset = 0.0 byte_offset = 0 byte_type = "f32_le" path = ["test"] "#, data_dir, udp_port); if let Some(parent) = config_path.parent() { fs::create_dir_all(parent)?; } fs::write(&config_path, config_str.clone())?; let config: Config = toml::from_str(&config_str)?; tokio::spawn(async move { let _ = run_agent(config, &format!("127.0.0.1:{}", port), config_path).await; }); sleep(Duration::from_millis(300)).await; Ok(()) } #[tokio::test] async fn test_agent_full_flow() -> Result<()> { tokio::time::timeout(Duration::from_secs(20), async { let port = 7892; let udp_port = 9001; let data_dir = "/tmp/rmon-test-data-full"; let config_path = PathBuf::from("/tmp/rmon-test/config_full.toml"); let _ = fs::remove_dir_all(data_dir); setup_agent(port, udp_port, config_path, data_dir).await?; let stream = TcpStream::connect(format!("127.0.0.1:{}", port)).await?; let mut framed = Framed::new(stream, ClientCodec); framed.send(ClientMessage::Handshake { protocol_version: PROTOCOL_VERSION, client_version: "0.1.0".to_string(), }).await?; let ack = framed.next().await.ok_or_else(|| anyhow::anyhow!("Disconnected"))??; if let AgentMessage::HandshakeAck { accepted, .. } = ack { assert!(accepted); } else { panic!("Expected HandshakeAck, got {:?}", ack); } framed.send(ClientMessage::Subscribe { ids: vec!["udp://test_source/sig1".to_string()], from_ns: None, }).await?; let udp_socket = UdpSocket::bind("127.0.0.1:0").await?; let val: f32 = 42.0; udp_socket.send_to(&val.to_le_bytes(), format!("127.0.0.1:{}", udp_port)).await?; let msg = framed.next().await.ok_or_else(|| anyhow::anyhow!("Disconnected"))??; if let AgentMessage::DataBatch { batches } = msg { assert_eq!(batches[0].id, "udp://test_source/sig1"); assert_eq!(batches[0].samples[0].value, 42.0); } else { panic!("Expected DataBatch, got {:?}", msg); } // Check continuous storage sleep(Duration::from_millis(200)).await; let signal_dir = format!("{}/test_source/udp___test_source_sig1", data_dir); let data_files = fs::read_dir(&signal_dir).map_err(|e| anyhow::anyhow!("Failed to read {}: {}", signal_dir, e))?; assert!(data_files.count() > 0); Ok::<(), anyhow::Error>(()) }).await.map_err(|_| anyhow::anyhow!("Test timed out"))? } #[tokio::test] async fn test_agent_config_remote() -> Result<()> { tokio::time::timeout(Duration::from_secs(20), async { let port = 7894; let udp_port = 9002; let data_dir = "/tmp/rmon-test-data-remote"; let config_path = PathBuf::from("/tmp/rmon-test/config_remote.toml"); let _ = fs::remove_dir_all(data_dir); setup_agent(port, udp_port, config_path.clone(), data_dir).await?; let stream = TcpStream::connect(format!("127.0.0.1:{}", port)).await?; let mut framed = Framed::new(stream, ClientCodec); framed.send(ClientMessage::Handshake { protocol_version: PROTOCOL_VERSION, client_version: "0.1.0".to_string(), }).await?; let _ = framed.next().await.unwrap()?; // 1. Get Config framed.send(ClientMessage::GetConfig).await?; let msg = framed.next().await.ok_or_else(|| anyhow::anyhow!("Disconnected"))??; if let AgentMessage::ConfigReport { config_toml } = msg { assert!(config_toml.contains("test_source")); } else { panic!("Expected ConfigReport, got {:?}", msg); } // 2. Update Config let new_config = format!(r#" [storage] data_dir = "{}" default_mode = "circular" default_window = "1h" [[sources]] type = "shell" name = "new_source" command = "echo \"val: 100\"" poll_interval = "100ms" [sources.timestamp] format = "none" [[sources.signals]] id = "sig_new" label = "New Sig" unit = "X" scale = 1.0 offset = 0.0 regex = "val: ([0-9]+)" path = ["new"] "#, data_dir); framed.send(ClientMessage::UpdateConfig { config_toml: new_config.to_string() }).await?; // Wait for hot-reload sleep(Duration::from_millis(2000)).await; // 3. Check if signals changed framed.send(ClientMessage::ListSignals).await?; let msg = tokio::time::timeout(Duration::from_secs(5), framed.next()).await .map_err(|_| anyhow::anyhow!("Timeout waiting for SignalList"))? .ok_or_else(|| anyhow::anyhow!("Disconnected"))??; if let AgentMessage::SignalList { signals } = msg { assert!(signals.iter().any(|s| s.id == "shell://new_source/sig_new"), "New signal not found in {:?}", signals); } else { panic!("Expected SignalList, got {:?}", msg); } Ok::<(), anyhow::Error>(()) }).await.map_err(|_| anyhow::anyhow!("Test timed out"))? }