use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use bacnet_encoding::npdu::{decode_npdu, Npdu, NpduAddress};
use bacnet_types::enums::NetworkMessageType;
use bacnet_types::MacAddr;
use bytes::Bytes;
use super::envelope_harness::*;
use super::SendRequest;
use crate::router_table::{ClaimCounters, Convergence, PortId, Reachability};
const I_AM: NetworkMessageType = NetworkMessageType::I_AM_ROUTER_TO_NETWORK;
const ACK: NetworkMessageType = NetworkMessageType::INITIALIZE_ROUTING_TABLE_ACK;
struct DebugCounter(Arc<AtomicUsize>);
impl DebugCounter {
fn new(events: Arc<AtomicUsize>) -> Self {
static REGISTRATION_PEER: std::sync::OnceLock<tracing::Dispatch> =
std::sync::OnceLock::new();
REGISTRATION_PEER
.get_or_init(|| tracing::Dispatch::new(tracing::subscriber::NoSubscriber::default()));
Self(events)
}
}
impl tracing::Subscriber for DebugCounter {
fn enabled(&self, metadata: &tracing::Metadata<'_>) -> bool {
*metadata.level() == tracing::Level::DEBUG
&& metadata.target() == "bacnet_network::router::hosted_host"
}
fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
tracing::span::Id::from_u64(1)
}
fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
fn event(&self, event: &tracing::Event<'_>) {
if self.enabled(event.metadata()) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
fn enter(&self, _: &tracing::span::Id) {}
fn exit(&self, _: &tracing::span::Id) {}
}
struct Fixture {
h: Harness,
}
impl Fixture {
fn new() -> Self {
Self {
h: Harness::with_networks(&[1000, 2000, 4000]),
}
}
fn learned() -> Self {
let fixture = Self::new();
fixture.h.table().set_convergence(Convergence::Hardened);
fixture.h.learned(3000, 0, &[1]);
fixture
}
async fn deliver(&mut self, port: usize, mac: &[u8], kind: NetworkMessageType, payload: &[u8]) {
let npdu = control_npdu(kind, payload);
self.h.handle(self.h.ctx(port, mac, npdu.clone())).await;
for index in 0..3 {
let rx = &mut self.h.rxs[index];
if kind == I_AM && index != port {
let SendRequest::Broadcast { npdu: data, .. } = rx.try_recv().unwrap() else {
panic!("expected unchanged I-Am rebroadcast");
};
let relayed = decode_npdu(data).unwrap();
assert_eq!(relayed.payload, npdu.payload);
assert_eq!(relayed.message_type, npdu.message_type);
} else if kind == NetworkMessageType::INITIALIZE_ROUTING_TABLE && index == port {
let SendRequest::Unicast {
npdu: data,
mac: dest,
..
} = rx.try_recv().unwrap()
else {
panic!("expected Init ACK");
};
assert_eq!(dest.as_slice(), mac);
let ack = decode_npdu(data).unwrap();
assert_eq!(ack.message_type, Some(ACK.to_raw()));
assert!(ack.payload.is_empty());
}
assert!(rx.try_recv().is_err());
}
}
async fn claim(&mut self, port: usize, mac: &[u8], kind: NetworkMessageType) {
let payload: &[u8] = if kind == I_AM {
&[0x0b, 0xb8]
} else {
&[1, 0x0b, 0xb8, 0, 0]
};
self.deliver(port, mac, kind, payload).await;
}
fn claims(&self) -> ClaimCounters {
self.h.table().claim_counters()
}
async fn assert_forwarded(&mut self, expected_port: usize, expected_mac: &[u8]) {
assert_eq!(
self.h.table().effective_reachability(3000, self.h.now()),
Some(Reachability::Reachable)
);
let npdu = Npdu {
destination: Some(NpduAddress {
network: 3000,
mac_address: MacAddr::from_slice(&[9]),
}),
hop_count: 255,
payload: Bytes::from_static(&[0x10, 0x20]),
..Default::default()
};
self.h.dispatch(self.h.ctx(2, &[7], npdu.clone())).await;
for index in 0..3 {
let rx = &mut self.h.rxs[index];
if index == expected_port {
let SendRequest::Unicast {
npdu: data, mac, ..
} = rx.try_recv().unwrap()
else {
panic!("expected learned-route forwarding");
};
assert_eq!(mac.as_slice(), expected_mac);
let forwarded = decode_npdu(data).unwrap();
assert_eq!(forwarded.destination, npdu.destination);
assert_eq!(forwarded.payload, npdu.payload);
assert_eq!(forwarded.hop_count, 254);
}
assert!(rx.try_recv().is_err());
}
}
}
#[tokio::test]
async fn both_claim_paths_keep_forwarding_old_route_until_repeat() {
for first in [I_AM, ACK] {
for second in [I_AM, ACK] {
for repeat_mac in [&[2][..], &[3][..]] {
let mut fixture = Fixture::learned();
fixture.claim(1, &[2], first).await;
fixture.assert_forwarded(0, &[1]).await;
assert_eq!(fixture.claims(), claim_counts(|c| c.pending_started = 1));
fixture.claim(1, repeat_mac, second).await;
fixture.assert_forwarded(1, repeat_mac).await;
assert_eq!(
fixture.claims(),
claim_counts(|c| {
c.learned_ok = 1;
c.pending_started = 1;
c.corroborated_applied = 1;
})
);
fixture.claim(1, &[4], second).await;
fixture.assert_forwarded(1, &[4]).await;
assert_eq!(fixture.claims().learned_ok, 2);
}
}
}
}
#[tokio::test]
async fn duplicate_entries_in_one_message_cannot_corroborate_a_replacement() {
for kind in [I_AM, ACK] {
let payload: &[u8] = if kind == I_AM {
&[0x0b, 0xb8, 0x0b, 0xb8]
} else {
&[2, 0x0b, 0xb8, 0, 0, 0x0b, 0xb8, 0, 0]
};
let mut fixture = Fixture::learned();
fixture.deliver(1, &[2], kind, payload).await;
fixture.assert_forwarded(0, &[1]).await;
assert_eq!(fixture.claims(), claim_counts(|c| c.pending_started = 1));
fixture.claim(1, &[2], kind).await;
fixture.assert_forwarded(1, &[2]).await;
assert_eq!(fixture.claims().corroborated_applied, 1);
let mut fixture = Fixture::new();
fixture.deliver(1, &[2], kind, payload).await;
fixture.assert_forwarded(1, &[2]).await;
fixture.deliver(1, &[3], kind, payload).await;
fixture.assert_forwarded(1, &[3]).await;
assert_eq!(fixture.claims(), claim_counts(|c| c.learned_ok = 4));
}
}
#[tokio::test]
async fn alternating_ports_in_both_handlers_never_replace_forwarding_route() {
for kind in [I_AM, ACK] {
let mut fixture = Fixture::learned();
for index in 0..20 {
fixture.claim(1 + index % 2, &[2], kind).await;
fixture.assert_forwarded(0, &[1]).await;
}
assert_eq!(fixture.claims(), claim_counts(|c| c.pending_started = 20));
}
}
#[tokio::test]
async fn unrelated_management_and_absent_only_messages_preserve_pending() {
for kind in [I_AM, ACK] {
for other in [
NetworkMessageType::INITIALIZE_ROUTING_TABLE,
NetworkMessageType::I_COULD_BE_ROUTER_TO_NETWORK,
] {
let mut fixture = Fixture::learned();
fixture.claim(1, &[2], kind).await;
let payload: &[u8] = if other == NetworkMessageType::INITIALIZE_ROUTING_TABLE {
&[2, 0x13, 0x89, 2, 0, 0x13, 0x88, 0, 0]
} else {
&[0x0b, 0xb8, 0]
};
for port in [1, 0] {
fixture.deliver(port, &[3], other, payload).await;
fixture.assert_forwarded(0, &[1]).await;
}
if other == NetworkMessageType::INITIALIZE_ROUTING_TABLE {
assert!(fixture.h.table().lookup(5001).is_some());
} else {
assert!(fixture.h.table().lookup(5001).is_none());
}
fixture.claim(1, &[2], kind).await;
fixture.assert_forwarded(1, &[2]).await;
assert_eq!(
fixture.claims(),
claim_counts(|c| {
c.learned_ok = 1;
c.pending_started = 1;
c.corroborated_applied = 1;
})
);
}
}
}
#[tokio::test]
async fn all_absent_learning_paths_apply_on_first_message() {
for kind in [
I_AM,
ACK,
NetworkMessageType::INITIALIZE_ROUTING_TABLE,
NetworkMessageType::I_COULD_BE_ROUTER_TO_NETWORK,
] {
let mut fixture = Fixture::new();
let payload: &[u8] = if kind == I_AM {
&[0x0b, 0xb8]
} else if kind == NetworkMessageType::I_COULD_BE_ROUTER_TO_NETWORK {
&[0x0b, 0xb8, 0]
} else if kind == NetworkMessageType::INITIALIZE_ROUTING_TABLE {
&[1, 0x0b, 0xb8, 2, 0]
} else {
&[1, 0x0b, 0xb8, 0, 0]
};
fixture.deliver(1, &[2], kind, payload).await;
fixture.assert_forwarded(1, &[2]).await;
assert_eq!(
fixture.claims(),
claim_counts(|c| c.learned_ok = u64::from(kind == I_AM || kind == ACK))
);
}
}
#[tokio::test]
async fn disconnect_retains_route_state_and_pending_and_counts_only_complete_requests() {
let mut fixture = Fixture::learned();
fixture.claim(1, &[2], I_AM).await;
let debug_events = Arc::new(AtomicUsize::new(0));
let _subscriber = tracing::subscriber::set_default(DebugCounter::new(debug_events.clone()));
let before = fixture.h.table().lookup(3000).unwrap();
let kind = NetworkMessageType::DISCONNECT_CONNECTION_TO_NETWORK;
for net in [3000u16, 4000, 5000, 0, 0xffff, 3000] {
fixture.deliver(1, &[2], kind, &net.to_be_bytes()).await;
fixture.assert_forwarded(0, &[1]).await;
}
for payload in [&[][..], &[0x0b][..]] {
fixture.deliver(1, &[2], kind, payload).await;
}
{
let table = fixture.h.table();
let route = table.lookup(3000).unwrap();
assert_eq!(route.last_seen(), before.last_seen());
assert_eq!(route.busy_until(), before.busy_until());
assert_eq!(route.flap_count(), before.flap_count());
assert_eq!(route.last_port_change(), before.last_port_change());
assert!(table.lookup(4000).unwrap().is_direct());
assert_eq!(table.lookup(4000).unwrap().port(), PortId(2));
assert_eq!(table.len(), 4);
assert_eq!(
table.claim_counters(),
claim_counts(|c| {
c.pending_started = 1;
c.disconnect_removal_ignored = 6;
})
);
}
assert_eq!(debug_events.load(Ordering::Relaxed), 6);
fixture.claim(1, &[2], ACK).await; fixture.assert_forwarded(1, &[2]).await;
assert_eq!(fixture.claims().corroborated_applied, 1);
}
#[tokio::test]
async fn debug_counter_counts_this_thread_whoever_registers_the_callsite() {
let kind = NetworkMessageType::DISCONNECT_CONNECTION_TO_NETWORK;
let payload = 3000u16.to_be_bytes();
let debug_events = Arc::new(AtomicUsize::new(0));
let _subscriber = tracing::subscriber::set_default(DebugCounter::new(debug_events.clone()));
std::thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async { Fixture::learned().deliver(1, &[2], kind, &payload).await });
})
.join()
.unwrap();
Fixture::learned().deliver(1, &[2], kind, &payload).await;
assert_eq!(debug_events.load(Ordering::Relaxed), 1);
}