use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
use spvirit_codec::epics_decode::{PvaHeader, PvaPacket, PvaPacketCommand};
use spvirit_codec::spvirit_encode::{
encode_client_connection_validation, encode_create_channel_request, encode_put_request,
};
use spvirit_server::PvaServer;
use spvirit_server::handler::{PvListMode, ServerState, handle_connection, rand_guid};
use spvirit_server::monitor::MonitorRegistry;
use spvirit_server::pvstore::SourceRegistry;
use spvirit_server::simple_store::SimplePvStore;
use spvirit_types::ScalarValue;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
const PV: &str = "SEG:TARGET";
const VERSION: u8 = 2;
const IS_BE: bool = false;
const IO_TIMEOUT: Duration = Duration::from_secs(5);
fn split_into_segments(msg: &[u8], at: usize) -> (Vec<u8>, Vec<u8>) {
let (header, payload) = msg.split_at(8);
let (a, b) = payload.split_at(at);
let mut first = header.to_vec();
first[2] = (first[2] & !0x30) | 0x10; first[4..8].copy_from_slice(&(a.len() as u32).to_le_bytes());
first.extend_from_slice(a);
let mut last = header.to_vec();
last[2] = (last[2] & !0x30) | 0x20; last[4..8].copy_from_slice(&(b.len() as u32).to_le_bytes());
last.extend_from_slice(b);
(first, last)
}
async fn spawn_server(initial: f64) -> (Arc<SimplePvStore>, SocketAddr) {
let server = PvaServer::builder().ao(PV, initial).build();
let store = server.store().clone();
let sources = Arc::new(SourceRegistry::new());
sources.add("builtin", 0, store.clone()).await;
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind test listener");
let addr = listener.local_addr().expect("listener addr");
let state = Arc::new(ServerState::new(
sources,
Arc::new(MonitorRegistry::new()),
false,
PvListMode::Off,
0,
None,
rand_guid(),
addr.port(),
None,
IpAddr::V4(Ipv4Addr::LOCALHOST),
));
tokio::spawn(async move {
let mut conn_id = 1u64;
while let Ok((stream, _peer)) = listener.accept().await {
let state = state.clone();
let id = conn_id;
conn_id += 1;
tokio::spawn(async move {
let _ = handle_connection(state, stream, id, IO_TIMEOUT).await;
});
}
});
(store, addr)
}
async fn read_frame(stream: &mut TcpStream) -> Vec<u8> {
let mut header = [0u8; 8];
tokio::time::timeout(IO_TIMEOUT, stream.read_exact(&mut header))
.await
.expect("timeout reading header")
.expect("read header");
let parsed = PvaHeader::new(&header);
let payload_len = if parsed.flags.is_control {
0
} else {
parsed.payload_length as usize
};
let mut payload = vec![0u8; payload_len];
if payload_len > 0 {
tokio::time::timeout(IO_TIMEOUT, stream.read_exact(&mut payload))
.await
.expect("timeout reading payload")
.expect("read payload");
}
let mut full = header.to_vec();
full.extend_from_slice(&payload);
full
}
async fn read_until<F>(stream: &mut TcpStream, mut accept: F) -> PvaPacketCommand
where
F: FnMut(&PvaPacketCommand) -> bool,
{
for _ in 0..32 {
let raw = read_frame(stream).await;
let mut pkt = PvaPacket::new(&raw);
if let Some(cmd) = pkt.decode_payload()
&& accept(&cmd)
{
return cmd;
}
}
panic!("expected command never arrived");
}
async fn open_put_channel(addr: SocketAddr) -> (TcpStream, u32, u32) {
let mut stream = TcpStream::connect(addr).await.expect("connect");
read_until(&mut stream, |cmd| {
matches!(cmd, PvaPacketCommand::ConnectionValidation(_))
})
.await;
let validation = encode_client_connection_validation(
16_384,
512,
0,
"anonymous",
"tester",
"localhost",
VERSION,
IS_BE,
);
stream
.write_all(&validation)
.await
.expect("write validation");
read_until(&mut stream, |cmd| {
matches!(cmd, PvaPacketCommand::ConnectionValidated(_))
})
.await;
let cid = 1u32;
stream
.write_all(&encode_create_channel_request(cid, PV, VERSION, IS_BE))
.await
.expect("write create channel");
let sid = match read_until(
&mut stream,
|cmd| matches!(cmd, PvaPacketCommand::CreateChannel(p) if p.is_server && p.cid == cid),
)
.await
{
PvaPacketCommand::CreateChannel(p) => {
assert!(p.status.is_none(), "create channel failed: {:?}", p.status);
p.sid
}
other => panic!("unexpected command: {other:?}"),
};
let ioid = 1u32;
let pv_request = vec![0xfd, 0x02, 0x00, 0x80, 0x00, 0x00];
stream
.write_all(&encode_put_request(
sid,
ioid,
0x08,
&pv_request,
VERSION,
IS_BE,
))
.await
.expect("write put init");
read_until(&mut stream, |cmd| {
matches!(cmd, PvaPacketCommand::Op(op) if op.command == 11 && (op.subcmd & 0x08) != 0)
})
.await;
(stream, sid, ioid)
}
async fn put_value(value: f64, segmented: bool) -> Option<ScalarValue> {
let (store, addr) = spawn_server(1.0).await;
let (mut stream, sid, ioid) = open_put_channel(addr).await;
let mut body = vec![0x01, 0x02];
body.extend_from_slice(&value.to_le_bytes());
let msg = encode_put_request(sid, ioid, 0x00, &body, VERSION, IS_BE);
if segmented {
let payload_len = msg.len() - 8;
let split_at = payload_len - 6;
assert!(split_at > 0 && split_at < payload_len, "interior split");
let (first, last) = split_into_segments(&msg, split_at);
stream.write_all(&first).await.expect("write first segment");
stream.write_all(&last).await.expect("write last segment");
} else {
stream.write_all(&msg).await.expect("write put data");
}
read_until(&mut stream, |cmd| {
matches!(cmd, PvaPacketCommand::Op(op) if op.command == 11 && (op.subcmd & 0x08) == 0)
})
.await;
store.get_value(PV).await
}
#[tokio::test]
async fn segmented_put_matches_unsegmented_put() {
let segmented = put_value(2.5, true).await;
let unsegmented = put_value(2.5, false).await;
assert_eq!(
segmented,
Some(ScalarValue::F64(2.5)),
"segmented PUT did not land the written value"
);
assert_eq!(
segmented, unsegmented,
"segmented and unsegmented PUT produced different record state"
);
}