use crate::config::DataEndpoint;
use crate::tests::test_payload_len_for;
use crate::{
TelemetryError, TelemetryErrorCode, TelemetryResult,
config::DataType,
packet::Packet,
router::{Clock, EndpointHandler, Router, RouterConfig},
wire_format,
};
use alloc::{string::String, sync::Arc};
use core::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
fn zero_clock() -> Box<dyn Clock + Send + Sync> {
Box::new(|| 0u64)
}
#[test]
fn error_enum_code_roundtrip_and_strings() {
let samples = [
TelemetryError::InvalidType,
TelemetryError::EmptyEndpoints,
TelemetryError::Unpack("oops"),
TelemetryError::Io("disk"),
TelemetryError::HandlerError("fail"),
TelemetryError::MissingPayload,
TelemetryError::TimestampInvalid,
];
for e in samples {
let code = e.to_error_code();
assert!(code.as_str().starts_with('{'));
let back = TelemetryErrorCode::try_from_i32(code as i32);
assert!(back.is_some(), "roundtrip failed for {code:?}");
}
}
#[test]
fn unpack_header_only_short_buffer_fails() {
let tiny = [0x00u8]; let err = wire_format::peek_envelope(&tiny).unwrap_err();
matches_deser_err(err);
let truncated = [0x00u8, 0x80]; let err = wire_format::peek_envelope(&truncated).unwrap_err();
matches_deser_err(err);
}
#[test]
fn header_size_is_prefix_of_wire_image() {
let pkt = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&[1.0, 2.0, 3.0],
&[DataEndpoint::named("SD_CARD"), DataEndpoint::named("RADIO")],
123,
)
.unwrap();
let wire = wire_format::pack_packet(&pkt);
let hdr = wire_format::header_size_bytes(&pkt);
assert!(hdr <= wire.len());
assert!(hdr > 0);
}
fn matches_deser_err(e: TelemetryError) {
match e {
TelemetryError::Unpack(_) => {}
other => panic!("expected Unpack error, got {other:?}"),
}
}
fn rewrite_crc32(buf: &mut [u8]) {
if buf.len() < wire_format::CRC32_BYTES {
return;
}
let data_len = buf.len() - wire_format::CRC32_BYTES;
let mut hasher = crc32fast::Hasher::new();
hasher.update(&buf[..data_len]);
let crc = hasher.finalize();
buf[data_len..].copy_from_slice(&crc.to_le_bytes());
}
#[test]
fn packer_is_canonical_roundtrip() {
use crate::config::{DataEndpoint, DataType};
use crate::{packet::Packet, wire_format};
let msg = "hello world";
let pkt = Packet::from_str_slice(
DataType::TelemetryError,
msg,
&[DataEndpoint::named("SD_CARD"), DataEndpoint::named("RADIO")],
0,
)
.unwrap();
let wire1 = wire_format::pack_packet(&pkt);
let pkt2 = wire_format::unpack_packet(&wire1).unwrap();
let wire2 = wire_format::pack_packet(&pkt2);
assert_eq!(&*wire1, &*wire2, "packer must be canonical");
}
#[test]
fn pack_unpack_roundtrip_matches_packet_identity() {
let pkt = Packet::from_str_slice(
DataType::TelemetryError,
"pack/unpack roundtrip check",
&[DataEndpoint::TelemetryError],
456,
)
.unwrap();
let packed = wire_format::pack_packet(&pkt);
assert_eq!(packed, wire_format::pack_packet(&pkt));
let unpacked = wire_format::unpack_packet(&packed).unwrap();
assert_eq!(unpacked.packet_id(), pkt.packet_id());
assert_eq!(unpacked.payload(), pkt.payload());
}
#[test]
fn packer_varint_scalars_grow_as_expected() {
use crate::config::{DataEndpoint, DataType};
use crate::{packet::Packet, wire_format};
fn non_rle_ascii(len: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(len);
for i in 0..len {
out.push(b'a' + ((i % 26) as u8));
}
out
}
fn pkt_with(len: usize, sender_len: usize, ts: u64) -> Packet {
let sender_bytes = non_rle_ascii(sender_len);
let s: String = sender_bytes.iter().map(|b| char::from(*b)).collect();
let payload = non_rle_ascii(len); Packet::new(
DataType::TelemetryError,
&[DataEndpoint::named("SD_CARD")],
&s,
ts,
Arc::<[u8]>::from(payload),
)
.unwrap()
}
let p1 = pkt_with(10, 5, 0x7F); let w1 = wire_format::pack_packet(&p1);
let h1 = wire_format::header_size_bytes(&p1);
assert!(h1 > 4, "NEP + 4 one-byte varints minimum");
let p2 = pkt_with(200, 200, 0x7F);
let w2 = wire_format::pack_packet(&p2);
let h2 = wire_format::header_size_bytes(&p2);
assert!(w2.len() > w1.len(), "wire should grow with larger varints");
assert!(h2 >= h1, "header should not shrink with larger varints");
let p3 = pkt_with(200, 200, 1u64 << 40); let w3 = wire_format::pack_packet(&p3);
let h3 = wire_format::header_size_bytes(&p3);
assert!(
w3.len() > w2.len(),
"wire should grow with larger timestamp"
);
assert!(h3 > h2, "header should grow with larger timestamp");
assert_eq!(wire_format::packet_wire_size(&p3), w3.len());
}
#[test]
fn endpoints_bitpack_roundtrip_many_and_extremes() {
use crate::{
MAX_VALUE_DATA_ENDPOINT,
config::{DataEndpoint, DataType},
packet::Packet,
wire_format,
};
let mut eps = Vec::<DataEndpoint>::new();
for i in 0..=MAX_VALUE_DATA_ENDPOINT {
if let Some(ep) = DataEndpoint::try_from_u32(i) {
eps.push(ep);
}
}
let mut endpoints = Vec::new();
for _ in 0..4 {
endpoints.extend_from_slice(&eps);
}
let payload = vec![0x55u8; 257]; let pkt = Packet::new(
DataType::TelemetryError,
&endpoints,
"sender",
123456,
Arc::<[u8]>::from(payload),
)
.unwrap();
let wire = wire_format::pack_packet(&pkt);
let back = wire_format::unpack_packet(&wire).unwrap();
let has_all_endpoints = back.endpoints().iter().all(|ep| endpoints.contains(ep));
assert!(has_all_endpoints, "endpoints must roundtrip 1:1");
assert_eq!(back.data_type(), pkt.data_type());
assert_eq!(back.timestamp(), pkt.timestamp());
assert_eq!(back.payload(), pkt.payload());
assert_eq!(wire_format::packet_wire_size(&pkt), wire.len());
}
#[test]
fn peek_envelope_matches_full_parse_on_large_values() {
use crate::config::{DataEndpoint, DataType};
use crate::{packet::Packet, wire_format};
let sender = "S".repeat(10_000); let payload = vec![b'h'; 4096];
let ts = (1u64 << 40) + 123;
let pkt = Packet::new(
DataType::TelemetryError, &[DataEndpoint::named("SD_CARD"), DataEndpoint::named("RADIO")],
&sender,
ts,
Arc::<[u8]>::from(payload),
)
.unwrap();
let wire = wire_format::pack_packet(&pkt);
assert!(
!wire
.windows(sender.len())
.any(|window| window == sender.as_bytes()),
"sender hostname must be discovery/config metadata, not packet-header bytes"
);
let env = wire_format::peek_envelope(&wire).unwrap();
let full = wire_format::unpack_packet(&wire).unwrap();
assert_eq!(env.ty, pkt.data_type());
assert_eq!(env.sender.as_ref(), pkt.sender());
assert_eq!(env.timestamp_ms, pkt.timestamp());
assert_eq!(&*env.endpoints, pkt.endpoints());
assert_eq!(full.data_type(), pkt.data_type());
assert_eq!(full.timestamp(), pkt.timestamp());
assert_eq!(full.endpoints(), pkt.endpoints());
assert_eq!(full.payload(), pkt.payload());
}
#[test]
fn corrupt_endpoint_bits_yields_bad_endpoint_error() {
use crate::{
MAX_VALUE_DATA_ENDPOINT,
config::{DataEndpoint, DataType},
packet::Packet,
wire_format,
};
let bits = 32 - MAX_VALUE_DATA_ENDPOINT.leading_zeros();
let ep_bits: u8 = if bits == 0 { 1 } else { bits as u8 };
let upper_val = (1u64 << ep_bits) - 1;
if upper_val as u32 <= MAX_VALUE_DATA_ENDPOINT {
return;
}
let pkt = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&[1.0, 2.0, 3.0],
&[DataEndpoint::named("SD_CARD")],
123,
)
.unwrap();
let mut wire = wire_format::pack_packet(&pkt).to_vec();
let ep_offset = wire_format::header_size_bytes(&pkt);
assert!(ep_offset < wire.len());
let mut v = upper_val;
for (bitpos, _) in (0..ep_bits).enumerate() {
let byte_idx = ep_offset + (bitpos / 8);
let bit_off = bitpos % 8;
if (v & 1) != 0 {
wire[byte_idx] |= 1 << bit_off;
} else {
wire[byte_idx] &= !(1 << bit_off);
}
v >>= 1;
}
rewrite_crc32(&mut wire);
let err = wire_format::unpack_packet(&wire).unwrap_err();
match err {
TelemetryError::Unpack(msg) if msg.contains("endpoint") => {}
other => panic!("expected bad endpoint unpack error, got {other:?}"),
}
}
#[test]
fn header_size_is_prefix_and_less_than_total() {
use crate::config::{DataEndpoint, DataType};
use crate::{packet::Packet, wire_format};
let pkt = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&[1.0, 2.0, 3.0],
&[DataEndpoint::named("SD_CARD"), DataEndpoint::named("RADIO")],
999,
)
.unwrap();
let wire = wire_format::pack_packet(&pkt);
let hdr = wire_format::header_size_bytes(&pkt);
assert!(hdr > 0 && hdr < wire.len());
assert_eq!(wire_format::packet_wire_size(&pkt), wire.len());
}
#[test]
fn data_as_utf8_ref_trims_trailing_nuls() {
let ty = DataType::TelemetryError;
let mut buf = vec![0u8; test_payload_len_for(ty)];
let s = b"hello\0\0";
buf[..s.len()].copy_from_slice(s);
let pkt = Packet::new(
ty,
&[DataEndpoint::named("SD_CARD")],
"tester",
0,
Arc::<[u8]>::from(buf),
)
.unwrap();
assert_eq!(pkt.data_as_utf8_ref(), Some("hello"));
}
#[test]
fn clear_queues_prevents_further_processing() {
let tx_count = Arc::new(AtomicUsize::new(0));
let tx_count_c = tx_count.clone();
let tx = move |bytes: &[u8]| -> TelemetryResult<()> {
assert!(!bytes.is_empty());
tx_count_c.fetch_add(1, Ordering::SeqCst);
Ok(())
};
let rx_count = Arc::new(AtomicUsize::new(0));
let rx_count_c = rx_count.clone();
let handler = EndpointHandler::new_packet_handler(
DataEndpoint::named("SD_CARD"),
move |_pkt: &Packet| {
rx_count_c.fetch_add(1, Ordering::SeqCst);
Ok(())
},
);
let r = Router::new_with_clock(RouterConfig::new(vec![handler]), zero_clock());
r.add_side_packed("tx", tx);
let pkt_tx = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&[1.0_f32, 2.0, 3.0],
&[DataEndpoint::named("SD_CARD"), DataEndpoint::named("RADIO")],
0,
)
.unwrap();
let pkt_rx = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&[4.0_f32, 5.0, 6.0],
&[DataEndpoint::named("SD_CARD")], 0,
)
.unwrap();
r.tx_queue(pkt_tx).unwrap();
r.rx_queue(pkt_rx).unwrap();
r.clear_queues();
r.drain_queues_with_timeout(0).unwrap();
assert_eq!(
tx_count.load(Ordering::SeqCst),
0,
"should not TX after clear"
);
assert_eq!(
rx_count.load(Ordering::SeqCst),
0,
"should not RX after clear"
);
}
#[test]
fn local_handler_retry_attempts_are_three() {
const EXPECTED_ATTEMPTS: usize = 3;
let counter = Arc::new(AtomicUsize::new(0));
let counter_c = counter.clone();
let failing = EndpointHandler::new_packet_handler(
DataEndpoint::named("SD_CARD"),
move |_pkt: &Packet| {
counter_c.fetch_add(1, Ordering::SeqCst);
Err(TelemetryError::BadArg)
},
);
let r = Router::new_with_clock(RouterConfig::new(vec![failing]), zero_clock());
let pkt = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&[1.0_f32, 2.0, 3.0],
&[DataEndpoint::named("SD_CARD")],
0,
)
.unwrap();
let res = r.tx(pkt);
match res {
Err(TelemetryError::HandlerError(_)) => {}
other => panic!("expected HandlerError after retries, got {other:?}"),
}
assert_eq!(
counter.load(Ordering::SeqCst),
EXPECTED_ATTEMPTS,
"handler should be invoked exactly {EXPECTED_ATTEMPTS} times"
);
}
#[test]
fn from_f32_slice_builds_valid_packet() {
let need = test_payload_len_for(DataType::named("GPS_DATA")) / 4; assert_eq!(need, 3);
let bytes = vec![5.3f32; need];
let pkt = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&bytes,
&[DataEndpoint::named("SD_CARD")],
12345,
)
.unwrap();
assert_eq!(pkt.payload().len(), 12);
assert_eq!(pkt.data_size(), 12);
assert_eq!(pkt.timestamp(), 12345);
}
#[test]
fn from_none_slice_builds_valid_packet() {
let need = 0; assert_eq!(need, 0);
let pkt = Packet::from_no_data(
DataType::named("HEARTBEAT"),
&[DataEndpoint::named("SD_CARD")],
12345,
)
.unwrap();
assert_eq!(pkt.payload().len(), 0);
assert_eq!(pkt.data_size(), 0);
assert_eq!(pkt.timestamp(), 12345);
}
#[test]
fn heartbeat_reaches_each_independently_discovered_network_side() {
crate::tests::ensure_common_test_schema();
use crate::config::register_data_type_with_description;
use crate::discovery::{TopologyBoardNode, build_discovery_topology};
use crate::{MessageClass, MessageDataType, MessageElement, ReliableMode};
let seen_a: Arc<Mutex<Vec<Arc<[u8]>>>> = Arc::new(Mutex::new(Vec::new()));
let seen_b: Arc<Mutex<Vec<Arc<[u8]>>>> = Arc::new(Mutex::new(Vec::new()));
let seen_a_cb = seen_a.clone();
let seen_b_cb = seen_b.clone();
let router = Router::new_with_clock(RouterConfig::default(), zero_clock());
let side_a = router.add_side_packed("A", move |bytes| {
seen_a_cb.lock().unwrap().push(Arc::from(bytes));
Ok(())
});
let side_b = router.add_side_packed("B", move |bytes| {
seen_b_cb.lock().unwrap().push(Arc::from(bytes));
Ok(())
});
let endpoint = DataEndpoint::named("SD_CARD");
let heartbeat = DataType::try_named("HEARTBEAT").unwrap_or_else(|| {
register_data_type_with_description(
"HEARTBEAT",
"test network heartbeat",
MessageElement::Static(0, MessageDataType::NoData, MessageClass::Data),
&[endpoint],
ReliableMode::None,
255,
)
.expect("register HEARTBEAT")
});
router
.rx_from_side(
&build_discovery_topology(
"REMOTE_A",
1,
&[TopologyBoardNode {
sender_id: "REMOTE_A".into(),
reachable_endpoints: vec![endpoint],
reachable_timesync_sources: vec![],
connections: vec![],
}],
)
.unwrap(),
side_a,
)
.unwrap();
router
.rx_from_side(
&build_discovery_topology(
"BRIDGE_B",
1,
&[
TopologyBoardNode {
sender_id: "BRIDGE_B".into(),
reachable_endpoints: vec![],
reachable_timesync_sources: vec![],
connections: vec!["REMOTE_B".into()],
},
TopologyBoardNode {
sender_id: "REMOTE_B".into(),
reachable_endpoints: vec![endpoint],
reachable_timesync_sources: vec![],
connections: vec!["BRIDGE_B".into()],
},
],
)
.unwrap(),
side_b,
)
.unwrap();
router.process_all_queues().unwrap();
seen_a.lock().unwrap().clear();
seen_b.lock().unwrap().clear();
router
.tx(Packet::from_no_data(heartbeat, &[endpoint], 2).unwrap())
.unwrap();
assert_eq!(seen_a.lock().unwrap().len(), 1);
assert_eq!(seen_b.lock().unwrap().len(), 1);
assert_eq!(
router.debug_end_to_end_tracked_count(),
0,
"periodic freshness packets must not create an end-to-end ACK storm",
);
}
#[test]
fn unpack_header_only_then_full_parse_matches() {
let endpoints = &[DataEndpoint::named("SD_CARD"), DataEndpoint::named("RADIO")];
let pkt = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&[5.25_f32, 3.5, 1.0],
endpoints,
42,
)
.unwrap();
let wire = wire_format::pack_packet(&pkt);
let env = wire_format::peek_envelope(&wire).unwrap();
assert_eq!(env.ty, pkt.data_type());
assert_eq!(&*env.endpoints, pkt.endpoints());
assert_eq!(env.sender.as_ref(), pkt.sender());
assert_eq!(env.timestamp_ms, pkt.timestamp());
let round = wire_format::unpack_packet(&wire).unwrap();
round.validate().unwrap();
assert_eq!(round.data_type(), pkt.data_type());
assert_eq!(round.data_size(), pkt.data_size());
assert_eq!(round.timestamp(), pkt.timestamp());
assert_eq!(round.endpoints(), pkt.endpoints());
assert_eq!(round.payload(), pkt.payload());
}
#[test]
fn tx_failure_emits_error_to_local_endpoints() {
let failing_tx = |_bytes: &[u8]| -> TelemetryResult<()> { Err(TelemetryError::Io("boom")) };
let last_payload = Arc::new(Mutex::new(String::new()));
let last_payload_c = last_payload.clone();
let capturing =
EndpointHandler::new_packet_handler(DataEndpoint::named("SD_CARD"), move |pkt: &Packet| {
if pkt.data_type() == DataType::TelemetryError {
*last_payload_c.lock().unwrap() = pkt.as_string();
}
Ok(())
});
let r = Router::new_with_clock(RouterConfig::new(vec![capturing]), zero_clock());
r.add_side_packed("tx", failing_tx);
let pkt = Packet::from_f32_slice(
DataType::named("GPS_DATA"),
&[1.0_f32, 2.0, 3.0],
&[DataEndpoint::named("SD_CARD"), DataEndpoint::named("RADIO")],
7,
)
.unwrap();
let res = r.tx(pkt);
match res {
Err(TelemetryError::HandlerError(_)) => {} other => panic!("expected HandlerError from TX failure, got {other:?}"),
}
let got = last_payload.lock().unwrap().clone();
assert!(
!got.is_empty(),
"expected TelemetryError to be delivered locally after TX failure"
);
}