#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
use std::sync::Arc;
use std::time::{Duration, Instant};
use zerodds_dcps::runtime::{
DcpsRuntime, RuntimeConfig, UserReaderConfig, UserSample, UserWriterConfig,
};
use zerodds_qos::{
DeadlineQosPolicy, DurabilityKind, LifespanQosPolicy, LivelinessQosPolicy, OwnershipKind,
};
use zerodds_rtps::wire_types::GuidPrefix;
const HZ: u64 = 200;
const SECS: u64 = 2;
const TOTAL: u64 = HZ * SECS;
const MAX_GAP: Duration = Duration::from_millis(150);
fn writer_cfg(topic: &str) -> UserWriterConfig {
UserWriterConfig {
topic_name: topic.into(),
type_name: "RawBytes".into(),
reliable: false, durability: DurabilityKind::Volatile,
deadline: DeadlineQosPolicy::default(),
lifespan: LifespanQosPolicy::default(),
liveliness: LivelinessQosPolicy::default(),
ownership: OwnershipKind::Shared,
ownership_strength: 0,
partition: vec![],
user_data: vec![],
topic_data: vec![],
group_data: vec![],
type_identifier: zerodds_types::TypeIdentifier::None,
data_representation_offer: None,
}
}
fn reader_cfg(topic: &str) -> UserReaderConfig {
UserReaderConfig {
topic_name: topic.into(),
type_name: "RawBytes".into(),
reliable: false,
durability: DurabilityKind::Volatile,
deadline: DeadlineQosPolicy::default(),
liveliness: LivelinessQosPolicy::default(),
ownership: OwnershipKind::Shared,
partition: vec![],
user_data: vec![],
group_data: vec![],
topic_data: vec![],
type_identifier: zerodds_types::TypeIdentifier::None,
type_consistency: zerodds_types::qos::TypeConsistencyEnforcement::default(),
data_representation_offer: None,
}
}
#[cfg_attr(target_os = "macos", ignore)]
#[test]
fn best_effort_clock_stream_has_no_gap_jitter() {
let domain = 93;
let host = [0x4Bu8; 4];
let mk_prefix = |tag: u8| {
let mut p = [tag; 12];
p[..4].copy_from_slice(&host);
GuidPrefix::from_bytes(p)
};
let rt_pub = DcpsRuntime::start(domain, mk_prefix(0xC1), RuntimeConfig::default()).unwrap();
let rt_sub = DcpsRuntime::start(domain, mk_prefix(0xC2), RuntimeConfig::default()).unwrap();
let topic = "A4SimClock";
let w = rt_pub.register_user_writer(writer_cfg(topic)).unwrap();
let (r, rx) = rt_sub.register_user_reader(reader_cfg(topic)).unwrap();
let mdl = Instant::now() + Duration::from_secs(15);
while Instant::now() < mdl
&& (rt_sub.user_reader_matched_count(r) < 1 || rt_pub.user_writer_matched_count(w) < 1)
{
std::thread::sleep(Duration::from_millis(20));
}
assert!(
rt_sub.user_reader_matched_count(r) >= 1 && rt_pub.user_writer_matched_count(w) >= 1,
"reader/writer did not match before the clock burst"
);
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let stop_rx = Arc::clone(&stop);
let sub = std::thread::spawn(move || {
let mut arrivals: Vec<Instant> = Vec::with_capacity(TOTAL as usize);
while !stop_rx.load(std::sync::atomic::Ordering::Relaxed) {
match rx.recv_timeout(Duration::from_millis(100)) {
Ok(UserSample::Alive { .. }) => arrivals.push(Instant::now()),
Ok(_) => {}
Err(_) => {}
}
}
while let Ok(s) = rx.try_recv() {
if matches!(s, UserSample::Alive { .. }) {
arrivals.push(Instant::now());
}
}
arrivals
});
let period = Duration::from_nanos(1_000_000_000 / HZ);
for i in 0..TOTAL {
let t = (i as u32).to_le_bytes();
rt_pub
.write_user_sample(w, t.to_vec())
.expect("clock write");
std::thread::sleep(period);
}
std::thread::sleep(Duration::from_millis(300));
stop.store(true, std::sync::atomic::Ordering::Relaxed);
let arrivals = sub.join().expect("subscriber thread");
let received = arrivals.len() as u64;
assert!(
received >= TOTAL / 2,
"received only {received}/{TOTAL} clock ticks — best-effort loopback should keep most"
);
let mut worst = Duration::ZERO;
for w2 in arrivals.windows(2) {
let gap = w2[1].duration_since(w2[0]);
if gap > worst {
worst = gap;
}
}
assert!(
worst < MAX_GAP,
"worst inter-arrival gap {worst:?} exceeds {MAX_GAP:?} (Fast DDS-style clock stall); \
received {received}/{TOTAL}"
);
}