First test kinda working
This commit is contained in:
139
simulator/tests/end_to_end_t1.rs
Normal file
139
simulator/tests/end_to_end_t1.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
//! End-to-end T1 datagram test: spin up substrate's listener in-process with
|
||||
//! channels the test owns, drive a `SimulatorClient` against it, and assert
|
||||
//! the datagram lands in the T1 receiver decoded.
|
||||
//!
|
||||
//! Run with `cargo test -p simulator`.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use simulator::client::SimulatorClient;
|
||||
use substrate::config::QuicConfig;
|
||||
use substrate::transport::server::{accept_loop, bind_endpoint};
|
||||
use substrate::transport::{QuicMessage, SensorType, T1Sender, T2Sender, T3Sender};
|
||||
use tokio::sync::mpsc;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn cert_path(name: &str) -> PathBuf {
|
||||
[env!("CARGO_MANIFEST_DIR"), "..", "certs", name].iter().collect()
|
||||
}
|
||||
|
||||
fn loopback_config(cert: PathBuf, key: PathBuf) -> QuicConfig {
|
||||
QuicConfig {
|
||||
// Port 0 lets the OS pick a free ephemeral port — tests can run in
|
||||
// parallel without colliding on a fixed bind.
|
||||
server_port: 0,
|
||||
server_interface: "127.0.0.1".to_string(),
|
||||
server_cert: cert.to_string_lossy().into_owned(),
|
||||
server_key: key.to_string_lossy().into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn t1_datagram_decoded_into_ecs_channel() -> Result<()> {
|
||||
simulator::install_crypto_provider();
|
||||
|
||||
let cert = cert_path("server.crt");
|
||||
let key = cert_path("server.key");
|
||||
let cfg = loopback_config(cert.clone(), key);
|
||||
|
||||
// Bind the substrate's listener on an ephemeral port.
|
||||
let endpoint = bind_endpoint(&cfg)?;
|
||||
let server_addr: SocketAddr = endpoint.local_addr()?;
|
||||
|
||||
// Channels the test owns — gives us direct visibility into what the T1
|
||||
// demux pushes into the ECS bridge.
|
||||
let (t1_tx, mut t1_rx) = mpsc::channel(64);
|
||||
let (t2_tx, _t2_rx) = mpsc::channel(64);
|
||||
let (t3_tx, _t3_rx) = mpsc::channel(64);
|
||||
|
||||
let server_task = tokio::spawn(accept_loop(
|
||||
endpoint,
|
||||
T1Sender::new(t1_tx),
|
||||
T2Sender::new(t2_tx),
|
||||
T3Sender::new(t3_tx),
|
||||
));
|
||||
|
||||
// Connect a client and send one datagram.
|
||||
let client = SimulatorClient::connect(server_addr, "localhost", &cert).await?;
|
||||
|
||||
let sent = QuicMessage {
|
||||
device_id: Uuid::from_u128(0xdead_beef_cafe_f00d_1234_5678_90ab_cdef),
|
||||
sensor_id: 7,
|
||||
raw_value: 42.0,
|
||||
timestamp_us: 1_700_000_000_000_001,
|
||||
sequence_number: 1,
|
||||
sensor_type: SensorType::Temperature.as_u8(),
|
||||
};
|
||||
client.send_datagram(&sent)?;
|
||||
|
||||
// Wait for the substrate's read_datagrams reader to push it into T1.
|
||||
let received = tokio::time::timeout(Duration::from_secs(2), t1_rx.recv())
|
||||
.await
|
||||
.expect("did not observe T1 datagram within 2s")
|
||||
.expect("T1 channel closed unexpectedly");
|
||||
|
||||
assert_eq!(received, sent);
|
||||
|
||||
client.close().await;
|
||||
server_task.abort();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn t1_burst_preserves_order_and_count() -> Result<()> {
|
||||
simulator::install_crypto_provider();
|
||||
|
||||
let cert = cert_path("server.crt");
|
||||
let key = cert_path("server.key");
|
||||
let cfg = loopback_config(cert.clone(), key);
|
||||
|
||||
let endpoint = bind_endpoint(&cfg)?;
|
||||
let server_addr: SocketAddr = endpoint.local_addr()?;
|
||||
|
||||
// T1 capacity 64 ≥ burst size 32 so nothing is dropped under loopback.
|
||||
let (t1_tx, mut t1_rx) = mpsc::channel(64);
|
||||
let (t2_tx, _t2_rx) = mpsc::channel(8);
|
||||
let (t3_tx, _t3_rx) = mpsc::channel(8);
|
||||
|
||||
let server_task = tokio::spawn(accept_loop(
|
||||
endpoint,
|
||||
T1Sender::new(t1_tx),
|
||||
T2Sender::new(t2_tx),
|
||||
T3Sender::new(t3_tx),
|
||||
));
|
||||
|
||||
let client = SimulatorClient::connect(server_addr, "localhost", &cert).await?;
|
||||
|
||||
let device = Uuid::from_u128(0xa1a2_a3a4_b5b6_b7b8_c9ca_cbcc_cdce_cfd0);
|
||||
const BURST: u32 = 32;
|
||||
for seq in 0..BURST {
|
||||
let msg = QuicMessage {
|
||||
device_id: device,
|
||||
sensor_id: 0,
|
||||
raw_value: f64::from(seq),
|
||||
timestamp_us: 1_700_000_000_000_000 + u64::from(seq),
|
||||
sequence_number: seq,
|
||||
sensor_type: SensorType::Generic.as_u8(),
|
||||
};
|
||||
client.send_datagram(&msg)?;
|
||||
}
|
||||
|
||||
// Drain BURST messages with a per-message timeout. Loopback shouldn't
|
||||
// reorder QUIC datagrams within a single connection.
|
||||
for expected_seq in 0..BURST {
|
||||
let msg = tokio::time::timeout(Duration::from_secs(2), t1_rx.recv())
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("missed datagram seq={expected_seq}"))
|
||||
.expect("T1 channel closed");
|
||||
assert_eq!(msg.sequence_number, expected_seq);
|
||||
assert_eq!(msg.device_id, device);
|
||||
assert_eq!(msg.raw_value, f64::from(expected_seq));
|
||||
}
|
||||
|
||||
client.close().await;
|
||||
server_task.abort();
|
||||
Ok(())
|
||||
}
|
||||
163
simulator/tests/end_to_end_t2.rs
Normal file
163
simulator/tests/end_to_end_t2.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
//! End-to-end T2 (unidirectional stream) tests. Mirrors the T1 harness:
|
||||
//! spin up substrate's listener with channels owned by the test, drive a
|
||||
//! `SimulatorClient` against it, assert what arrives on the T2 receiver.
|
||||
//!
|
||||
//! Run with `cargo test -p simulator`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use simulator::client::SimulatorClient;
|
||||
use substrate::config::QuicConfig;
|
||||
use substrate::transport::server::{accept_loop, bind_endpoint};
|
||||
use substrate::transport::{QuicMessage, SensorType, T1Sender, T2Sender, T3Sender};
|
||||
use tokio::sync::mpsc;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn cert_path(name: &str) -> PathBuf {
|
||||
[env!("CARGO_MANIFEST_DIR"), "..", "certs", name].iter().collect()
|
||||
}
|
||||
|
||||
fn loopback_config(cert: PathBuf, key: PathBuf) -> QuicConfig {
|
||||
QuicConfig {
|
||||
server_port: 0,
|
||||
server_interface: "127.0.0.1".to_string(),
|
||||
server_cert: cert.to_string_lossy().into_owned(),
|
||||
server_key: key.to_string_lossy().into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn t2_single_stream_preserves_order() -> Result<()> {
|
||||
simulator::install_crypto_provider();
|
||||
|
||||
let cert = cert_path("server.crt");
|
||||
let key = cert_path("server.key");
|
||||
let cfg = loopback_config(cert.clone(), key);
|
||||
|
||||
let endpoint = bind_endpoint(&cfg)?;
|
||||
let server_addr: SocketAddr = endpoint.local_addr()?;
|
||||
|
||||
let (t1_tx, _t1_rx) = mpsc::channel(64);
|
||||
let (t2_tx, mut t2_rx) = mpsc::channel(64);
|
||||
let (t3_tx, _t3_rx) = mpsc::channel(64);
|
||||
|
||||
let server_task = tokio::spawn(accept_loop(
|
||||
endpoint,
|
||||
T1Sender::new(t1_tx),
|
||||
T2Sender::new(t2_tx),
|
||||
T3Sender::new(t3_tx),
|
||||
));
|
||||
|
||||
let client = SimulatorClient::connect(server_addr, "localhost", &cert).await?;
|
||||
|
||||
let device = Uuid::from_u128(0x0011_2233_4455_6677_8899_aabb_ccdd_eeff);
|
||||
const N: u32 = 10;
|
||||
let msgs: Vec<QuicMessage> = (0..N)
|
||||
.map(|i| QuicMessage {
|
||||
device_id: device,
|
||||
sensor_id: 1,
|
||||
raw_value: f64::from(i),
|
||||
timestamp_us: 1_700_000_000_000_000 + u64::from(i),
|
||||
sequence_number: i,
|
||||
sensor_type: SensorType::Pressure.as_u8(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
client.send_uni_stream(&msgs).await?;
|
||||
|
||||
for expected in &msgs {
|
||||
let received = tokio::time::timeout(Duration::from_secs(2), t2_rx.recv())
|
||||
.await
|
||||
.expect("missed T2 message")
|
||||
.expect("T2 channel closed unexpectedly");
|
||||
assert_eq!(received, *expected);
|
||||
}
|
||||
|
||||
client.close().await;
|
||||
server_task.abort();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn t2_concurrent_streams_each_internally_ordered() -> Result<()> {
|
||||
simulator::install_crypto_provider();
|
||||
|
||||
let cert = cert_path("server.crt");
|
||||
let key = cert_path("server.key");
|
||||
let cfg = loopback_config(cert.clone(), key);
|
||||
|
||||
let endpoint = bind_endpoint(&cfg)?;
|
||||
let server_addr: SocketAddr = endpoint.local_addr()?;
|
||||
|
||||
let (t1_tx, _t1_rx) = mpsc::channel(64);
|
||||
let (t2_tx, mut t2_rx) = mpsc::channel(256);
|
||||
let (t3_tx, _t3_rx) = mpsc::channel(64);
|
||||
|
||||
let server_task = tokio::spawn(accept_loop(
|
||||
endpoint,
|
||||
T1Sender::new(t1_tx),
|
||||
T2Sender::new(t2_tx),
|
||||
T3Sender::new(t3_tx),
|
||||
));
|
||||
|
||||
let client = SimulatorClient::connect(server_addr, "localhost", &cert).await?;
|
||||
|
||||
// 4 devices × 8 messages each on independent uni streams. Cross-stream
|
||||
// ordering may interleave; per-stream ordering must be strict.
|
||||
const DEVICES: usize = 4;
|
||||
const PER_DEVICE: u32 = 8;
|
||||
let device_ids: Vec<Uuid> = (0..DEVICES).map(|_| Uuid::new_v4()).collect();
|
||||
|
||||
let mut handles = Vec::with_capacity(DEVICES);
|
||||
for &device in &device_ids {
|
||||
let conn = client.conn.clone();
|
||||
handles.push(tokio::spawn(async move {
|
||||
let msgs: Vec<QuicMessage> = (0..PER_DEVICE)
|
||||
.map(|i| QuicMessage {
|
||||
device_id: device,
|
||||
sensor_id: 0,
|
||||
raw_value: f64::from(i),
|
||||
timestamp_us: 1_700_000_000_000_000 + u64::from(i),
|
||||
sequence_number: i,
|
||||
sensor_type: SensorType::Generic.as_u8(),
|
||||
})
|
||||
.collect();
|
||||
// Use the connection directly so each task owns its own stream
|
||||
// — same wire pattern as `SimulatorClient::send_uni_stream`.
|
||||
let mut send = conn.open_uni().await.expect("open_uni");
|
||||
for m in &msgs {
|
||||
send.write_all(&m.to_bytes()).await.expect("write_all");
|
||||
}
|
||||
send.finish().expect("finish");
|
||||
}));
|
||||
}
|
||||
for h in handles {
|
||||
h.await?;
|
||||
}
|
||||
|
||||
// Drain DEVICES × PER_DEVICE messages, group by device, assert per-device
|
||||
// sequence numbers are strictly increasing from 0.
|
||||
let total = DEVICES * PER_DEVICE as usize;
|
||||
let mut by_device: HashMap<Uuid, Vec<u32>> = HashMap::new();
|
||||
for _ in 0..total {
|
||||
let msg = tokio::time::timeout(Duration::from_secs(2), t2_rx.recv())
|
||||
.await
|
||||
.expect("missed T2 message")
|
||||
.expect("T2 channel closed unexpectedly");
|
||||
by_device.entry(msg.device_id).or_default().push(msg.sequence_number);
|
||||
}
|
||||
|
||||
assert_eq!(by_device.len(), DEVICES, "expected one entry per device");
|
||||
for (dev, seqs) in &by_device {
|
||||
let expected: Vec<u32> = (0..PER_DEVICE).collect();
|
||||
assert_eq!(seqs, &expected, "out-of-order or missing sequence for {dev}");
|
||||
}
|
||||
|
||||
client.close().await;
|
||||
server_task.abort();
|
||||
Ok(())
|
||||
}
|
||||
152
simulator/tests/end_to_end_t3.rs
Normal file
152
simulator/tests/end_to_end_t3.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
//! End-to-end T3 (bidirectional stream + oneshot ack) tests. Same shape as
|
||||
//! the T1/T2 harnesses: spin up substrate's listener with channels owned by
|
||||
//! the test, run a "fake ECS" task that drains the T3 receiver and either
|
||||
//! replies or drops the oneshot, and assert the client observes the right
|
||||
//! behaviour.
|
||||
//!
|
||||
//! Run with `cargo test -p simulator`.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use simulator::client::SimulatorClient;
|
||||
use substrate::config::QuicConfig;
|
||||
use substrate::transport::server::{accept_loop, bind_endpoint};
|
||||
use substrate::transport::{QuicMessage, SensorType, T1Sender, T2Sender, T3Sender};
|
||||
use tokio::sync::mpsc;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn cert_path(name: &str) -> PathBuf {
|
||||
[env!("CARGO_MANIFEST_DIR"), "..", "certs", name].iter().collect()
|
||||
}
|
||||
|
||||
fn loopback_config(cert: PathBuf, key: PathBuf) -> QuicConfig {
|
||||
QuicConfig {
|
||||
server_port: 0,
|
||||
server_interface: "127.0.0.1".to_string(),
|
||||
server_cert: cert.to_string_lossy().into_owned(),
|
||||
server_key: key.to_string_lossy().into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker `timestamp_us` the fake ECS stamps onto every ack so the test can
|
||||
/// distinguish a real reply from any echo of the command's own timestamp.
|
||||
const ACK_MARKER_TS: u64 = 999_999_999_999;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn t3_round_trip_with_fake_handler() -> Result<()> {
|
||||
simulator::install_crypto_provider();
|
||||
|
||||
let cert = cert_path("server.crt");
|
||||
let key = cert_path("server.key");
|
||||
let cfg = loopback_config(cert.clone(), key);
|
||||
|
||||
let endpoint = bind_endpoint(&cfg)?;
|
||||
let server_addr: SocketAddr = endpoint.local_addr()?;
|
||||
|
||||
let (t1_tx, _t1_rx) = mpsc::channel(64);
|
||||
let (t2_tx, _t2_rx) = mpsc::channel(64);
|
||||
let (t3_tx, mut t3_rx) = mpsc::channel(64);
|
||||
|
||||
let server_task = tokio::spawn(accept_loop(
|
||||
endpoint,
|
||||
T1Sender::new(t1_tx),
|
||||
T2Sender::new(t2_tx),
|
||||
T3Sender::new(t3_tx),
|
||||
));
|
||||
|
||||
// Fake ECS handler: drain T3 inbounds, mark the timestamp, send back.
|
||||
let handler = tokio::spawn(async move {
|
||||
while let Some(inbound) = t3_rx.recv().await {
|
||||
let mut ack = inbound.command;
|
||||
ack.timestamp_us = ACK_MARKER_TS;
|
||||
// Ignore send error (client may have disconnected before listening).
|
||||
let _ = inbound.reply.send(ack);
|
||||
}
|
||||
});
|
||||
|
||||
let client = SimulatorClient::connect(server_addr, "localhost", &cert).await?;
|
||||
|
||||
let cmd = QuicMessage {
|
||||
device_id: Uuid::from_u128(0xa5a5_a5a5_5a5a_5a5a_a5a5_5a5a_a5a5_5a5a),
|
||||
sensor_id: 3,
|
||||
raw_value: 1.5,
|
||||
timestamp_us: 1_700_000_000_000_000,
|
||||
sequence_number: 7,
|
||||
sensor_type: SensorType::Voltage.as_u8(),
|
||||
};
|
||||
|
||||
let ack = tokio::time::timeout(Duration::from_secs(2), client.request(&cmd))
|
||||
.await
|
||||
.expect("T3 ack timed out")?;
|
||||
|
||||
assert_eq!(ack.device_id, cmd.device_id, "ack should preserve device_id");
|
||||
assert_eq!(ack.sensor_id, cmd.sensor_id, "ack should preserve sensor_id");
|
||||
assert_eq!(
|
||||
ack.sequence_number, cmd.sequence_number,
|
||||
"ack should preserve sequence_number for correlation"
|
||||
);
|
||||
assert_eq!(ack.timestamp_us, ACK_MARKER_TS, "fake ECS should stamp the marker");
|
||||
|
||||
client.close().await;
|
||||
handler.abort();
|
||||
server_task.abort();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn t3_no_handler_resets_stream() -> Result<()> {
|
||||
simulator::install_crypto_provider();
|
||||
|
||||
let cert = cert_path("server.crt");
|
||||
let key = cert_path("server.key");
|
||||
let cfg = loopback_config(cert.clone(), key);
|
||||
|
||||
let endpoint = bind_endpoint(&cfg)?;
|
||||
let server_addr: SocketAddr = endpoint.local_addr()?;
|
||||
|
||||
let (t1_tx, _t1_rx) = mpsc::channel(64);
|
||||
let (t2_tx, _t2_rx) = mpsc::channel(64);
|
||||
let (t3_tx, mut t3_rx) = mpsc::channel(64);
|
||||
|
||||
let server_task = tokio::spawn(accept_loop(
|
||||
endpoint,
|
||||
T1Sender::new(t1_tx),
|
||||
T2Sender::new(t2_tx),
|
||||
T3Sender::new(t3_tx),
|
||||
));
|
||||
|
||||
// Fake ECS that *drops* every oneshot — simulates "no handler installed",
|
||||
// which is the placeholder state in `ingest_system` until M4 lands.
|
||||
let handler = tokio::spawn(async move {
|
||||
while let Some(inbound) = t3_rx.recv().await {
|
||||
drop(inbound);
|
||||
}
|
||||
});
|
||||
|
||||
let client = SimulatorClient::connect(server_addr, "localhost", &cert).await?;
|
||||
|
||||
let cmd = QuicMessage {
|
||||
device_id: Uuid::new_v4(),
|
||||
sensor_id: 0,
|
||||
raw_value: 0.0,
|
||||
timestamp_us: 0,
|
||||
sequence_number: 0,
|
||||
sensor_type: SensorType::Generic.as_u8(),
|
||||
};
|
||||
|
||||
let result = tokio::time::timeout(Duration::from_secs(2), client.request(&cmd)).await;
|
||||
let inner = result.expect("client.request should not hang when stream is reset");
|
||||
assert!(
|
||||
inner.is_err(),
|
||||
"expected request to fail when substrate resets the stream, got Ok({:?})",
|
||||
inner.ok()
|
||||
);
|
||||
|
||||
client.close().await;
|
||||
handler.abort();
|
||||
server_task.abort();
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user