test(client): cover TTL and pending expiry without poll coupling

This commit is contained in:
Nils Andreas Svee committed 2026-10-03 00:20:03 +07:00
1 parent d571315068
commit 143d730aa2
3 files changed
+128 -55

No files matched your search

+52
View File
@@ -375,6 +375,58 @@ mod tests {
}
}
#[test]
fn pending_removal_and_sequence_reuse_preserve_expiry_and_capacity() {
let start = Instant::now();
// Remove the head, middle or tail, then reuse that sequence at a later
// deadline. Observe expiry and capacity, not private list links.
for removed in 0..3 {
let mut pending_map = PendingMap::new(3);
for seq in 0..3 {
pending_map.preflight_insert(seq).unwrap();
pending_map
.commit_insert(pending(seq, start + Duration::from_secs(u64::from(seq))));
}
assert!(matches!(
pending_map.preflight_insert(3),
Err(ClientError::PendingLimitExceeded { .. })
));
assert_eq!(pending_map.remove(removed).unwrap().wire_seq, removed);
pending_map.preflight_insert(removed).unwrap();
let reused_deadline = start + Duration::from_secs(4);
pending_map.commit_insert(pending(removed, reused_deadline));
let remaining = (0..3).filter(|seq| *seq != removed).collect::<Vec<_>>();
for (index, seq) in remaining.iter().enumerate() {
assert_eq!(
pending_map.next_timeout_deadline(),
Some(start + Duration::from_secs(u64::from(*seq)))
);
let expired = pending_map.drain_expired_bounded(start + Duration::from_secs(3), 1);
assert_eq!(
expired
.probes
.iter()
.map(|probe| probe.wire_seq)
.collect::<Vec<_>>(),
vec![*seq]
);
assert_eq!(expired.more_due, index == 0);
}
assert_eq!(pending_map.next_timeout_deadline(), Some(reused_deadline));
let expired = pending_map.drain_expired_bounded(reused_deadline, 3);
assert_eq!(expired.probes.len(), 1);
assert_eq!(expired.probes[0].wire_seq, removed);
assert!(!expired.more_due);
assert!(pending_map.is_empty());
assert_eq!(pending_map.next_timeout_deadline(), None);
pending_map.preflight_insert(removed).unwrap();
pending_map.commit_insert(pending(removed, reused_deadline));
assert_eq!(pending_map.remove(removed).unwrap().wire_seq, removed);
assert!(pending_map.is_empty());
}
}
// Public loss/reply events cannot reveal leaked retention bookkeeping.
#[test]
fn timed_out_retention_stays_bounded_after_repeated_removals() {
+17 -1
View File
@@ -14,6 +14,8 @@ use irtt_proto::{decode_request, Clock, DecodedRequestKind, ReceivedStats, Stamp
use irtt_server::{ServerConfig, ServerCore};
use nix::sys::socket::{recvmsg, ControlMessageOwned, MsgFlags};
const TEST_TTL: u32 = 37;
// A compliant packet peer uses the real server core. Its receive-side IP_TOS
// observation is independent of the client/server parameter agreement.
fn peer(dscp_allowed: bool, probes: usize) -> (SocketAddr, thread::JoinHandle<Vec<u8>>) {
@@ -24,6 +26,7 @@ fn peer(dscp_allowed: bool, probes: usize) -> (SocketAddr, thread::JoinHandle<Ve
socket2::SockRef::from(&socket)
.set_recv_tos_v4(true)
.unwrap();
nix::sys::socket::setsockopt(&socket, nix::sys::socket::sockopt::Ipv4RecvTtl, &true).unwrap();
let addr = socket.local_addr().unwrap();
let task = thread::spawn(move || {
let mut core = ServerCore::new(
@@ -35,7 +38,7 @@ fn peer(dscp_allowed: bool, probes: usize) -> (SocketAddr, thread::JoinHandle<Ve
// OPEN, every ECHO, then CLOSE.
for _ in 0..probes + 2 {
let mut bytes = [0; 2048];
let mut control = nix::cmsg_space!(u8);
let mut control = nix::cmsg_space!(u8, i32);
let mut iov = [IoSliceMut::new(&mut bytes)];
let msg = recvmsg::<nix::sys::socket::SockaddrIn>(
std::os::fd::AsRawFd::as_raw_fd(&socket),
@@ -54,6 +57,18 @@ fn peer(dscp_allowed: bool, probes: usize) -> (SocketAddr, thread::JoinHandle<Ve
_ => None,
})
.expect("Linux must report IP_TOS");
let ttl = msg
.cmsgs()
.unwrap()
.find_map(|message| match message {
ControlMessageOwned::Ipv4Ttl(ttl) => Some(ttl),
_ => None,
})
.expect("Linux must report IP_TTL");
assert_eq!(
ttl, TEST_TTL as i32,
"configured TTL must survive OPEN, ECHO and CLOSE"
);
markings.push(marking);
let source = SocketAddr::from((source.ip(), source.port()));
if let Some(reply) = core.handle_datagram(source, &bytes[..size]).unwrap() {
@@ -79,6 +94,7 @@ fn config(addr: SocketAddr) -> ClientConfig {
length: 128,
open_timeouts: vec![Duration::from_secs(1)],
socket_config: SocketConfig {
ttl: Some(TEST_TTL),
recv_timeout: Some(Duration::from_secs(1)),
..SocketConfig::default()
},