use std::cell::Cell;
use std::collections::HashMap;
use std::rc::Rc;
use bacnet_transport::bbmd::BdtEntry;
use bacnet_transport::bvll::BvlcResultCode;
use tokio::time::Instant;
use crate::tui::network::matrix::Matrix;
use super::*;
const TIMEOUT: Duration = Duration::from_secs(3);
fn at(n: u16) -> SocketAddrV4 {
SocketAddrV4::new(
Ipv4Addr::new(10, 0, (n / 200) as u8, (n % 200) as u8 + 1),
47808,
)
}
fn table(peers: &[u16]) -> Probe {
Probe::Table(
peers
.iter()
.map(|&n| BdtRow {
address: at(n),
mask: Ipv4Addr::BROADCAST,
})
.collect(),
)
}
struct Fixture(HashMap<SocketAddrV4, (Duration, Probe)>);
impl Fixture {
async fn read(&self, bbmd: SocketAddrV4) -> Probe {
match self.0.get(&bbmd) {
Some((delay, probe)) => {
tokio::time::sleep(*delay).await;
probe.clone()
}
None => {
tokio::time::sleep(TIMEOUT).await;
Probe::Unreachable("no answer".into())
}
}
}
}
async fn one_at_a_time(fixture: &Fixture, seed: SocketAddrV4) -> Vec<(SocketAddrV4, Probe)> {
let mut seen = HashSet::from([seed]);
let mut pending = VecDeque::from([seed]);
let mut probes = Vec::new();
while let Some(bbmd) = pending.pop_front() {
if probes.len() >= MAX_SURVEY {
probes.push((bbmd, Probe::Skipped));
continue;
}
let probe = fixture.read(bbmd).await;
if let Probe::Table(rows) = &probe {
for row in rows {
if seen.insert(row.address) {
pending.push_back(row.address);
}
}
}
probes.push((bbmd, probe));
}
probes
}
async fn at_once(
fixture: &Fixture,
seed: SocketAddrV4,
) -> (Vec<(SocketAddrV4, Probe)>, Duration, usize) {
let (live, most) = (Rc::new(Cell::new(0)), Rc::new(Cell::new(0)));
let begin = Instant::now();
let probes = survey_with(seed, |bbmd| {
let (live, most) = (Rc::clone(&live), Rc::clone(&most));
async move {
live.set(live.get() + 1);
most.set(most.get().max(live.get()));
let probe = fixture.read(bbmd).await;
live.set(live.get() - 1);
probe
}
})
.await;
(probes, begin.elapsed(), most.get())
}
fn three_silent() -> Fixture {
Fixture(HashMap::from([(
at(0),
(Duration::ZERO, table(&[0, 1, 2, 3])),
)]))
}
#[tokio::test(start_paused = true)]
async fn three_silent_peers_cost_one_timeout_not_three() {
let fixture = three_silent();
let begin = Instant::now();
let sequential = one_at_a_time(&fixture, at(0)).await;
let sequential_took = begin.elapsed();
let (parallel, parallel_took, most) = at_once(&fixture, at(0)).await;
assert_eq!(parallel, sequential);
assert_eq!(sequential_took, 3 * TIMEOUT);
assert_eq!(parallel_took, TIMEOUT, "three timeouts should overlap");
assert_eq!(most, 3);
}
#[tokio::test(start_paused = true)]
async fn a_mixed_survey_matches_the_sequential_result() {
let nak = Probe::Nak(BvlcResultCode::READ_BROADCAST_DISTRIBUTION_TABLE_NAK);
let ms = Duration::from_millis;
let fixture = Fixture(HashMap::from([
(at(0), (ms(50), table(&[0, 1, 2, 3, 4, 5, 6]))),
(at(1), (ms(900), table(&[1, 7, 8, 0]))),
(at(2), (ms(10), nak.clone())),
(
at(4),
(ms(20), table(&[4, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18])),
),
(at(5), (ms(400), table(&[5, 4, 19]))),
(
at(6),
(ms(5), Probe::Unreachable("encoding error: lost".into())),
),
(at(7), (ms(30), table(&[7, 20, 21]))),
(at(8), (ms(1), nak)),
(at(9), (ms(250), table(&[9]))),
(at(10), (ms(60), table(&[10, 1]))),
]));
let sequential = one_at_a_time(&fixture, at(0)).await;
let (parallel, _, most) = at_once(&fixture, at(0)).await;
assert_eq!(parallel, sequential);
assert_eq!(most, SURVEY_AT_ONCE);
let read = parallel
.iter()
.filter(|(_, p)| *p != Probe::Skipped)
.count();
assert_eq!(read, MAX_SURVEY);
assert!(parallel.len() > MAX_SURVEY, "some BBMDs go unread");
assert_eq!(Matrix::build(¶llel), Matrix::build(&sequential));
}
#[tokio::test(start_paused = true)]
async fn a_lone_seed_is_read_once() {
let fixture = Fixture(HashMap::from([(at(0), (Duration::ZERO, table(&[0])))]));
let (probes, took, most) = at_once(&fixture, at(0)).await;
assert_eq!(probes, [(at(0), table(&[0]))]);
assert_eq!((took, most), (Duration::ZERO, 1));
}
#[tokio::test(start_paused = true)]
async fn a_real_survey_waits_out_silent_peers_together() {
let mut seed = BipTransport::new(Ipv4Addr::LOCALHOST, 0, Ipv4Addr::LOCALHOST);
seed.enable_bbmd(Vec::new());
let _seed_rx = seed.start().await.unwrap();
let seed_at = address_of(seed.local_mac()).unwrap();
let mut silent = Vec::new();
for _ in 0..3 {
silent.push(
tokio::net::UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))
.await
.unwrap(),
);
}
let silent_at: Vec<SocketAddrV4> = silent
.iter()
.map(|socket| match socket.local_addr().unwrap() {
std::net::SocketAddr::V4(at) => at,
std::net::SocketAddr::V6(_) => unreachable!("bound to IPv4"),
})
.collect();
let entry = |address: &SocketAddrV4| BdtEntry {
ip: address.ip().octets(),
port: address.port(),
broadcast_mask: [255; 4],
};
let rows = std::iter::once(&seed_at)
.chain(&silent_at)
.map(entry)
.collect();
seed.bbmd_state()
.unwrap()
.lock()
.unwrap()
.set_bdt(rows)
.unwrap();
let mut client = BipTransport::new(Ipv4Addr::LOCALHOST, 0, Ipv4Addr::LOCALHOST);
let _client_rx = client.start().await.unwrap();
let begin = Instant::now();
let probes = survey(&client, seed_at).await;
let took = begin.elapsed();
assert_eq!(probes.len(), 4);
assert!(matches!(probes[0], (at, Probe::Table(_)) if at == seed_at));
for ((at, probe), silent) in probes[1..].iter().zip(&silent_at) {
assert_eq!(at, silent);
assert_eq!(*probe, Probe::Unreachable("no answer".into()));
}
assert!(took >= TIMEOUT && took < 2 * TIMEOUT, "took {took:?}");
client.stop().await.unwrap();
seed.stop().await.unwrap();
}