use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket};
use std::sync::Arc;
use std::time::{Duration, Instant};
use socket2::{Domain, SockAddr, Socket, Type};
use crate::testing::uid_n;
use crate::wire::{op, HEADER_LEN};
use super::schedule::{HELLO_BASE, HELLO_JITTER};
use super::tests::hello_datagram;
use super::*;
#[derive(Clone, Copy)]
struct Scratch {
group: Ipv4Addr,
port: u16,
}
const FANOUT: Scratch = Scratch {
group: Ipv4Addr::new(239, 255, 77, 99),
port: 18812,
};
const TO_THE_WIRE: Scratch = Scratch {
group: Ipv4Addr::new(239, 255, 77, 98),
port: 18813,
};
const RE_ARM: Scratch = Scratch {
group: Ipv4Addr::new(239, 255, 77, 97),
port: 18814,
};
const FROM_THE_WIRE: Scratch = Scratch {
group: Ipv4Addr::new(239, 255, 77, 96),
port: 18815,
};
const UNBLOCKED: Scratch = Scratch {
group: Ipv4Addr::new(239, 255, 77, 95),
port: 18816,
};
const LOOPBACK_PROOF: Scratch = Scratch {
group: Ipv4Addr::new(239, 255, 77, 94),
port: 18817,
};
const DELIVERY: Duration = Duration::from_secs(2);
const PING: &[u8] = b"ping";
fn conn(at: Scratch) -> Option<Conn> {
Conn::open(at.group, at.port, None, None).ok()
}
fn skipped(why: &str) {
eprintln!("skipped: {why}");
}
fn opcode_of(frame: &[u8]) -> Option<u16> {
frame
.get(20..22)
.and_then(|b| <[u8; 2]>::try_from(b).ok())
.map(u16::from_le_bytes)
}
struct Probe {
sender: UdpSocket,
listener: UdpSocket,
group: SocketAddrV4,
}
fn sender(at: Scratch) -> Option<(UdpSocket, SocketAddrV4)> {
let local = crate::wire::default_local_ip().ok()?;
let socket = Socket::new(Domain::IPV4, Type::DGRAM, None).ok()?;
socket
.bind(&SockAddr::from(SocketAddrV4::new(local, 0)))
.ok()?;
socket.set_multicast_if_v4(&local).ok()?;
socket.set_multicast_loop_v4(true).ok()?;
Some((socket.into(), SocketAddrV4::new(at.group, at.port)))
}
impl Probe {
fn open(at: Scratch) -> Option<Self> {
let local = crate::wire::default_local_ip().ok()?;
let (sender, group) = sender(at)?;
let listener = Socket::new(Domain::IPV4, Type::DGRAM, None).ok()?;
listener.set_reuse_address(true).ok()?;
listener
.bind(&SockAddr::from(SocketAddrV4::new(
Ipv4Addr::UNSPECIFIED,
at.port,
)))
.ok()?;
listener.join_multicast_v4(&at.group, &local).ok()?;
listener.set_read_timeout(Some(DELIVERY)).ok()?;
Some(Self {
sender,
listener: listener.into(),
group,
})
}
fn send(&self, data: &[u8]) -> bool {
self.sender.send_to(data, self.group).is_ok()
}
fn recv(&self) -> Option<Vec<u8>> {
let mut buf = vec![0u8; 2048];
let (n, _) = self.listener.recv_from(&mut buf).ok()?;
buf.truncate(n);
Some(buf)
}
fn host_loops_back(&self) -> bool {
self.send(PING) && self.recv().is_some()
}
}
#[test]
fn two_clients_on_one_host_each_get_every_frame() {
const SENT: usize = 20;
let Some(a) = conn(FANOUT) else {
return skipped("no usable multicast interface");
};
let b = conn(FANOUT).expect("a second client could not bind alongside the first");
for (which, c) in [("first", &a), ("second", &b)] {
assert_eq!(
c.reuse_port().ok(),
Some(false),
"the {which} socket is in a reuseport group: receive would be split \
with any other client on this host"
);
}
let Some(probe) = Probe::open(FANOUT) else {
return skipped("the probe cannot be pinned to a multicast interface");
};
if !probe.host_loops_back() {
return skipped("no multicast loopback delivery");
}
for c in [&a, &b] {
while c.recv_within(Duration::from_millis(50)).is_some() {}
}
let readers: Vec<_> = [a, b]
.into_iter()
.map(|c| {
std::thread::spawn(move || {
let mut got = 0;
while c.recv_within(Duration::from_millis(700)).is_some() {
got += 1;
}
got
})
})
.collect();
for _ in 0..SENT {
assert!(
probe.send(PING),
"the probe reached its own listener, so this send must go too"
);
std::thread::sleep(Duration::from_millis(2));
}
let counts: Vec<usize> = readers
.into_iter()
.map(|r| r.join().expect("a reader thread panicked"))
.collect();
assert!(
counts.iter().all(|&n| n == SENT),
"receive is partitioned rather than fanned out: {counts:?} of {SENT} sent"
);
}
#[test]
fn an_announcement_reaches_the_wire() {
let Some(probe) = Probe::open(TO_THE_WIRE) else {
return skipped("the probe cannot be pinned to a multicast interface");
};
if !probe.host_loops_back() {
return skipped("no multicast loopback delivery");
}
let remote = Remote::new(
Config {
uid: Some(uid_n(210)),
target_ip: Some(TO_THE_WIRE.group),
port: Some(TO_THE_WIRE.port),
..Config::default()
},
Arc::new(()),
)
.expect("a client given its own uid reads nothing from disk");
remote
.start()
.expect("the client could not open its socket");
let deadline = Instant::now() + DELIVERY;
let mut seen = Vec::new();
while Instant::now() < deadline && !seen.contains(&op::SYS_HELLO.0) {
let Some(frame) = probe.recv() else { break };
assert!(
frame.len() >= HEADER_LEN,
"received {} bytes, too short for a frame",
frame.len()
);
seen.extend(opcode_of(&frame));
}
remote.close();
assert!(
seen.contains(&op::SYS_HELLO.0),
"the client started but nothing it announced reached the socket; saw {seen:x?}"
);
}
#[test]
fn a_frame_on_the_wire_reaches_the_registry() {
let Some(probe) = Probe::open(LOOPBACK_PROOF) else {
return skipped("the probe cannot be pinned to a multicast interface");
};
if !probe.host_loops_back() {
return skipped("no multicast loopback delivery");
}
let (sender, group) = sender(FROM_THE_WIRE).expect("the probe was pinned, so this pins too");
let remote = Remote::new(
Config {
uid: Some(uid_n(230)),
target_ip: Some(FROM_THE_WIRE.group),
port: Some(FROM_THE_WIRE.port),
..Config::default()
},
Arc::new(()),
)
.expect("a client given its own uid reads nothing from disk");
remote
.start()
.expect("the client could not open its socket");
let peer = uid_n(231);
let hello = hello_datagram(peer, "Peer", "WR0001");
let deadline = Instant::now() + DELIVERY;
let mut seen = None;
while Instant::now() < deadline && seen.is_none() {
sender
.send_to(&hello, group)
.expect("the probe reached its own listener, so this send must go too");
std::thread::sleep(Duration::from_millis(50));
seen = remote.device(peer);
}
remote.close();
let device = seen.expect(
"this host loops multicast back, so the frame was either never delivered to \
the client's socket or never decoded",
);
assert_eq!(device.serial, "WR0001");
}
#[test]
fn a_successful_announcement_re_arms_the_timer() {
let Some(conn) = conn(RE_ARM) else {
return skipped("no usable multicast interface");
};
let remote = Remote::new(
Config {
uid: Some(uid_n(220)),
..Config::default()
},
Arc::new(()),
)
.expect("a client given its own uid reads nothing from disk");
lock(&remote.shared.tx).set_conn(Some(conn));
lock(&remote.shared.schedule).set_hello_timer(None, Duration::ZERO);
remote.shared.announce();
let (last, interval) = lock(&remote.shared.schedule).hello_timer();
assert!(
last.is_some_and(|t| t <= Instant::now()),
"a successful send did not record the announcement"
);
assert!(
(HELLO_BASE..=HELLO_BASE + HELLO_JITTER).contains(&interval),
"interval after a successful send = {interval:?}, want it re-drawn in range"
);
}
#[test]
fn a_send_does_not_wait_for_the_receive_thread() {
const SENDS: u32 = 10;
const BUDGET: Duration = Duration::from_millis(500);
let Some(conn) = conn(UNBLOCKED) else {
return skipped("no usable multicast interface");
};
let remote = Remote::new(
Config {
uid: Some(uid_n(240)),
target_ip: Some(UNBLOCKED.group),
port: Some(UNBLOCKED.port),
..Config::default()
},
Arc::new(()),
)
.expect("a client given its own uid reads nothing from disk");
lock(&remote.shared.tx).set_conn(Some(conn));
remote
.spawn_workers()
.expect("the worker threads could not start");
std::thread::sleep(Duration::from_millis(50));
let started = Instant::now();
for _ in 0..SENDS {
remote.discover().expect("the socket is open");
}
let elapsed = started.elapsed();
remote.close();
assert!(
elapsed < BUDGET,
"{SENDS} sends took {elapsed:?}, so each one waited on the receive thread"
);
}