use std::time::{Duration, Instant};
use bacnet_encoding::npdu::{decode_npdu, NpduAddress};
use super::control_messages::handle_network_message;
use super::envelope_harness::*;
use super::*;
use crate::router_table::ReachabilityStatus;
const BUSY: NetworkMessageType = NetworkMessageType::ROUTER_BUSY_TO_NETWORK;
const AVAILABLE: NetworkMessageType = NetworkMessageType::ROUTER_AVAILABLE_TO_NETWORK;
const PEER_A: &[u8] = &[2];
const PEER_B: &[u8] = &[3];
fn busy_payload(nets: &[u16]) -> Vec<u8> {
let mut out = Vec::with_capacity(nets.len() * 2);
for net in nets {
out.extend_from_slice(&net.to_be_bytes());
}
out
}
fn reachability(h: &Harness, net: u16) -> Option<ReachabilityStatus> {
h.table.try_lock().unwrap().effective_reachability(net)
}
#[test]
fn selector_covers_only_learned_via_peer_routes() {
let mut table = RouterTable::new();
table.add_direct(1000, 0);
table.add_direct(2000, 1);
table.add_learned(3000, 1, MacAddr::from_slice(PEER_A));
table.add_learned(3001, 1, MacAddr::from_slice(PEER_A));
table.add_learned(4000, 1, MacAddr::from_slice(PEER_B));
table.add_learned(5000, 0, MacAddr::from_slice(&[9]));
let peer_a = MacAddr::from_slice(PEER_A);
let mut served = table.routes_served_via_peer(1, &peer_a);
served.sort_unstable();
assert_eq!(served, vec![3000, 3001]);
assert!(table.is_served_via_peer(3000, 1, &peer_a));
assert!(!table.is_served_via_peer(4000, 1, &peer_a));
assert!(!table.is_served_via_peer(5000, 1, &peer_a));
assert!(!table.is_served_via_peer(5000, 0, &peer_a));
assert!(!table.is_served_via_peer(2000, 1, &peer_a));
assert!(!table.is_served_via_peer(1000, 0, &MacAddr::new()));
assert!(!table.is_served_via_peer(9999, 1, &peer_a));
}
#[test]
fn busy_and_available_never_override_direct_routes() {
let mut table = RouterTable::new();
table.add_direct(1000, 0);
table.mark_busy(1000, Instant::now() + Duration::from_secs(30));
assert_eq!(
table.effective_reachability(1000),
Some(ReachabilityStatus::Reachable)
);
assert!(table.lookup(1000).unwrap().busy_until.is_none());
table.mark_unreachable(1000);
table.mark_available(1000);
assert_eq!(
table.effective_reachability(1000),
Some(ReachabilityStatus::Reachable)
);
}
#[test]
fn busy_expiry_boundary_and_sweep() {
let mut table = RouterTable::new();
table.add_learned(3000, 1, MacAddr::from_slice(PEER_A));
table.mark_busy(3000, Instant::now() + Duration::from_secs(30));
assert_eq!(
table.effective_reachability(3000),
Some(ReachabilityStatus::Busy)
);
table.mark_busy(3000, Instant::now());
assert_eq!(
table.effective_reachability(3000),
Some(ReachabilityStatus::Reachable)
);
table.mark_busy(3000, Instant::now() - Duration::from_secs(1));
table.clear_expired_busy();
let entry = table.lookup(3000).unwrap();
assert_eq!(entry.reachability, ReachabilityStatus::Reachable);
assert!(entry.busy_until.is_none());
}
#[test]
fn later_busy_update_extends_deadline() {
let mut table = RouterTable::new();
table.add_learned(3000, 1, MacAddr::from_slice(PEER_A));
let base = Instant::now();
table.mark_busy(3000, base + Duration::from_secs(30));
table.mark_busy(3000, base + Duration::from_secs(31));
assert_eq!(
table.lookup(3000).unwrap().busy_until,
Some(base + Duration::from_secs(31))
);
}
#[tokio::test]
async fn omitted_busy_marks_whole_via_peer_set_only() {
let mut h = Harness::busy_scope();
let mut ctx = h.ctx(1, PEER_A, control_npdu(BUSY, &[]));
ctx.data_attributes = attributes();
h.handle(ctx).await;
assert_eq!(reachability(&h, 3000), Some(ReachabilityStatus::Busy));
assert_eq!(reachability(&h, 3001), Some(ReachabilityStatus::Busy));
for net in [4000, 5000, 1000, 2000] {
assert_eq!(
reachability(&h, net),
Some(ReachabilityStatus::Reachable),
"network {net} must stay reachable"
);
}
assert!(h.drain(1).is_empty());
let mut out = h.drain(0);
assert_eq!(out.len(), 1);
match out.pop().unwrap() {
SendRequest::Broadcast {
npdu,
data_attributes,
} => {
let decoded = decode_npdu(npdu).unwrap();
assert_eq!(decoded.message_type, Some(BUSY.to_raw()));
assert!(decoded.payload.is_empty());
assert_eq!(data_attributes, attributes());
}
SendRequest::Unicast { .. } => panic!("expected broadcast"),
}
}
#[tokio::test]
async fn single_explicit_busy_marks_only_the_covered_route() {
let mut h = Harness::busy_scope();
h.handle(h.ctx(1, PEER_A, control_npdu(BUSY, &busy_payload(&[3000]))))
.await;
assert_eq!(reachability(&h, 3000), Some(ReachabilityStatus::Busy));
for net in [3001, 4000, 5000, 1000, 2000] {
assert_eq!(
reachability(&h, net),
Some(ReachabilityStatus::Reachable),
"network {net} must stay reachable"
);
}
assert_eq!(broadcast_data(h.drain(0)).len(), 1);
}
#[tokio::test]
async fn mixed_list_intersects_with_announcing_path() {
let mut h = Harness::busy_scope();
let payload = busy_payload(&[3001, 4000, 2000, 9999]);
h.handle(h.ctx(1, PEER_A, control_npdu(BUSY, &payload)))
.await;
assert_eq!(reachability(&h, 3001), Some(ReachabilityStatus::Busy));
for net in [3000, 4000, 5000, 1000, 2000] {
assert_eq!(
reachability(&h, net),
Some(ReachabilityStatus::Reachable),
"network {net} must stay reachable"
);
}
let rebroadcasts = broadcast_data(h.drain(0));
assert_eq!(rebroadcasts.len(), 1);
assert_eq!(
decode_npdu(rebroadcasts[0].clone())
.unwrap()
.payload
.as_ref(),
payload.as_slice()
);
}
#[tokio::test]
async fn available_reenables_before_expiry_and_omitted_clears_set() {
let mut h = Harness::busy_scope();
h.handle(h.ctx(1, PEER_A, control_npdu(BUSY, &busy_payload(&[3000]))))
.await;
assert_eq!(reachability(&h, 3000), Some(ReachabilityStatus::Busy));
h.handle(h.ctx(1, PEER_A, control_npdu(AVAILABLE, &busy_payload(&[3000]))))
.await;
assert_eq!(reachability(&h, 3000), Some(ReachabilityStatus::Reachable));
assert!(h
.table
.try_lock()
.unwrap()
.lookup(3000)
.unwrap()
.busy_until
.is_none());
h.handle(h.ctx(1, PEER_A, control_npdu(BUSY, &[]))).await;
assert_eq!(reachability(&h, 3001), Some(ReachabilityStatus::Busy));
h.handle(h.ctx(1, PEER_A, control_npdu(AVAILABLE, &[])))
.await;
for net in [3000, 3001, 4000, 5000, 1000, 2000] {
assert_eq!(
reachability(&h, net),
Some(ReachabilityStatus::Reachable),
"network {net} must be reachable"
);
}
}
#[tokio::test]
async fn unreachable_is_permanent_until_in_scope_available() {
let mut h = Harness::busy_scope();
h.table.try_lock().unwrap().mark_unreachable(3000);
assert_eq!(
reachability(&h, 3000),
Some(ReachabilityStatus::Unreachable)
);
h.table.try_lock().unwrap().clear_expired_busy();
assert_eq!(
reachability(&h, 3000),
Some(ReachabilityStatus::Unreachable)
);
h.handle(h.ctx(1, PEER_B, control_npdu(AVAILABLE, &busy_payload(&[3000]))))
.await;
assert_eq!(
reachability(&h, 3000),
Some(ReachabilityStatus::Unreachable)
);
h.handle(h.ctx(1, PEER_A, control_npdu(AVAILABLE, &busy_payload(&[3000]))))
.await;
assert_eq!(reachability(&h, 3000), Some(ReachabilityStatus::Reachable));
}
#[tokio::test]
async fn busy_never_resurrects_unreachable_route() {
let mut table = RouterTable::new();
table.add_learned(3000, 1, MacAddr::from_slice(PEER_A));
table.mark_unreachable(3000);
table.mark_busy(3000, Instant::now() + Duration::from_secs(30));
let entry = table.lookup(3000).unwrap();
assert_eq!(entry.reachability, ReachabilityStatus::Unreachable);
assert!(entry.busy_until.is_none());
table.mark_busy(3000, Instant::now() - Duration::from_secs(1));
table.clear_expired_busy();
assert_eq!(
table.effective_reachability(3000),
Some(ReachabilityStatus::Unreachable)
);
let mut h = Harness::busy_scope();
h.table.try_lock().unwrap().mark_unreachable(3000);
h.handle(h.ctx(1, PEER_A, control_npdu(BUSY, &busy_payload(&[3000]))))
.await;
assert_eq!(broadcast_data(h.drain(0)).len(), 1);
h.handle(h.ctx(1, PEER_A, control_npdu(BUSY, &[]))).await;
assert_eq!(broadcast_data(h.drain(0)).len(), 1);
assert_eq!(
reachability(&h, 3000),
Some(ReachabilityStatus::Unreachable)
);
assert!(h
.table
.try_lock()
.unwrap()
.lookup(3000)
.unwrap()
.busy_until
.is_none());
h.table.try_lock().unwrap().clear_expired_busy();
assert_eq!(
reachability(&h, 3000),
Some(ReachabilityStatus::Unreachable)
);
assert_eq!(reachability(&h, 3001), Some(ReachabilityStatus::Busy));
}
#[tokio::test]
async fn busy_marks_leave_learning_freshness_untouched() {
let mut h = Harness::busy_scope();
let before: Vec<(u16, _)> = {
let table = h.table.try_lock().unwrap();
[3000, 3001, 4000, 5000]
.into_iter()
.map(|net| (net, table.lookup(net).unwrap().last_seen))
.collect()
};
h.handle(h.ctx(1, PEER_A, control_npdu(BUSY, &[]))).await;
h.handle(h.ctx(1, PEER_A, control_npdu(AVAILABLE, &[])))
.await;
let table = h.table.try_lock().unwrap();
for (net, seen) in before {
assert_eq!(
table.lookup(net).unwrap().last_seen,
seen,
"network {net} freshness must not move on Busy/Available"
);
}
}
#[tokio::test]
async fn queue_full_keeps_marks_bounded_and_local() {
let mut table = RouterTable::new();
table.add_direct(1000, 0);
table.add_direct(2000, 1);
table.add_learned(3000, 1, MacAddr::from_slice(PEER_A));
table.add_learned(3001, 1, MacAddr::from_slice(PEER_A));
table.add_learned(4000, 1, MacAddr::from_slice(PEER_B));
table.add_learned(5000, 0, MacAddr::from_slice(&[9]));
let table = Arc::new(Mutex::new(table));
let (tx0, mut rx0) = mpsc::channel::<SendRequest>(1);
let (tx1, mut rx1) = mpsc::channel::<SendRequest>(16);
tx0.try_send(SendRequest::broadcast(Bytes::from_static(&[0])))
.unwrap();
let ctx = IngressContext::test_local(1, 2000, PEER_A, control_npdu(BUSY, &[]));
handle_network_message(
&table,
&[tx0, tx1],
&ctx,
&control_policy::ControlGate::permissive(),
&LocalControl::default(),
)
.await;
{
let table = table.lock().await;
assert_eq!(
table.effective_reachability(3000),
Some(ReachabilityStatus::Busy)
);
assert_eq!(
table.effective_reachability(3001),
Some(ReachabilityStatus::Busy)
);
for net in [4000, 5000, 1000, 2000] {
assert_eq!(
table.effective_reachability(net),
Some(ReachabilityStatus::Reachable),
"network {net} must stay reachable"
);
}
}
rx0.try_recv().unwrap();
assert!(rx0.try_recv().is_err());
assert!(rx1.try_recv().is_err());
}
#[tokio::test]
async fn directed_busy_is_routed_not_applied() {
let mut h = Harness::busy_scope();
let mut npdu = control_npdu(BUSY, &busy_payload(&[3000]));
npdu.destination = Some(NpduAddress {
network: 5000,
mac_address: MacAddr::from_slice(&[7]),
});
npdu.hop_count = 255;
h.dispatch(h.ctx(1, PEER_A, npdu)).await;
for net in [3000, 3001, 4000, 5000, 1000, 2000] {
assert_eq!(
reachability(&h, net),
Some(ReachabilityStatus::Reachable),
"network {net} must stay reachable"
);
}
assert!(h.drain(1).is_empty());
let mut forwarded = h.drain(0);
assert_eq!(forwarded.len(), 1);
match forwarded.pop().unwrap() {
SendRequest::Unicast { npdu, mac, .. } => {
assert_eq!(mac.as_slice(), &[9]);
let decoded = decode_npdu(npdu).unwrap();
assert_eq!(decoded.destination.unwrap().network, 5000);
assert_eq!(decoded.hop_count, 254);
}
SendRequest::Broadcast { .. } => panic!("expected unicast forward"),
}
}