test: protect durable behavior identified in review

This commit is contained in:
Nils Andreas Svee committed 2026-10-02 16:12:52 +07:00
1 parent e115b96f21
commit 83b3607228
7 files changed
+528 -6

No files matched your search

+44
View File
@@ -612,6 +612,8 @@ enum OpenSessionFailureCleanup {
/// The sole authoritative zero-or-many target driver.
#[must_use = "ManagedClientTask must be awaited or deliberately dropped"]
pub struct ManagedClientTask {
#[cfg(test)]
timeout_inspections: std::cell::RefCell<Vec<usize>>,
state: DriverState,
lifecycle: ManagedLifecycle,
config: ManagedClientConfig,
@@ -1833,6 +1835,8 @@ impl ManagedClientTask {
}
fn target_has_due_timeout(&self, index: usize, now: Instant) -> bool {
#[cfg(test)]
self.timeout_inspections.borrow_mut().push(index);
match &self.targets[index].state {
TargetState::Active { client } => {
self.state != DriverState::Stopping
@@ -2318,6 +2322,8 @@ fn build_task(
stop: Arc::clone(&stop),
};
let task = ManagedClientTask {
#[cfg(test)]
timeout_inspections: std::cell::RefCell::new(Vec::new()),
state: DriverState::NotStarted,
lifecycle: ManagedLifecycle::NotStarted,
config,
@@ -2387,3 +2393,41 @@ fn close_timeout_failure() -> ManagedTargetFailure {
message: Arc::from("best-effort close did not become writable before its deadline"),
}
}
#[cfg(test)]
mod tests {
use super::*;
// Poll latency through public events cannot distinguish an O(n) discovery
// scan from a bounded scan reliably. This tiny inspection trace measures
// work directly, including empty targets, and checks eventual coverage
// without asserting a cursor value or a particular visitation order.
#[test]
fn timeout_discovery_has_bounded_work_and_eventually_visits_every_target() {
let count = TIMEOUT_WORK_BUDGET * 2 + 1;
let targets = (0..count)
.map(|i| ManagedTargetConfig::new(i.to_string(), "127.0.0.1:2112"))
.collect();
let (mut task, _handle) = ManagedClient::task(
ManagedClientConfig {
max_live_target_generations: count,
..ManagedClientConfig::default()
},
targets,
)
.unwrap();
let mut visited = HashSet::new();
for _ in 0..count.div_ceil(TIMEOUT_WORK_BUDGET) {
task.timeout_inspections.borrow_mut().clear();
task.poll_timeout_pass(Instant::now());
let inspected = task.timeout_inspections.borrow();
assert!(inspected.len() <= TIMEOUT_WORK_BUDGET);
visited.extend(inspected.iter().copied());
}
assert_eq!(
visited.len(),
count,
"timeout discovery must reach all targets"
);
}
}
+170
View File
@@ -1133,3 +1133,173 @@ fn config_duration_to_ns(field: &str, duration: Duration) -> Result<i64, ClientE
reason: format!("{field} is too large to encode as nanoseconds"),
})
}
#[cfg(test)]
mod tests {
use super::*;
use irtt_proto::{FLAG_OPEN, FLAG_REPLY};
fn opened() -> SessionMachine {
let mut machine = SessionMachine::new(
ClientConfig {
max_pending_probes: 2,
clock: Clock::Wall,
stamp_at: irtt_proto::StampAt::Receive,
..ClientConfig::default()
},
"127.0.0.1:2112".parse().unwrap(),
)
.unwrap();
let acceptance = machine
.prepare_open_acceptance(
OpenReply {
flags: FLAG_OPEN | FLAG_REPLY,
token: 1,
params: machine.requested.clone(),
},
ClientTimestamp::now(),
)
.unwrap();
machine.commit_open(acceptance);
machine
}
fn send(machine: &mut SessionMachine) -> ProbeSent {
let probe = machine.prepare_probe().unwrap();
let preflight = machine.preflight_probe_commit(&probe).unwrap();
let commit = machine
.finalize_probe_commit(preflight, ClientTimestamp::now())
.unwrap();
machine.commit_probe_sent(commit, ClientTimestamp::now(), probe.bytes.len())
}
fn reply(machine: &mut SessionMachine, seq: u32) -> ClientEvent {
let events = machine
.process_echo_reply(
EchoReply {
flags: FLAG_REPLY,
token: 1,
sequence: seq,
recv_count: None,
recv_window: None,
timestamps: TimestampFields {
recv_wall: Some(1),
..TimestampFields::default()
},
payload: vec![],
},
32,
ClientTimestamp::now(),
ReceiveMeta::default(),
)
.unwrap();
events.into_iter().next().unwrap()
}
// Advancing a real client through 2^32 submissions is unreasonable. Only
// move the send/receive sequence epoch; construct completion/timeout history through
// ordinary machine transitions and assert emitted behavior on reuse.
#[test]
fn sequence_wrap_reuses_completed_and_timed_out_ids_as_new_probes() {
for timed_out in [false, true] {
let mut machine = opened();
assert_eq!(send(&mut machine).seq, 0);
if timed_out {
let deadline = machine.next_probe_timeout_deadline().unwrap();
assert!(matches!(
machine.poll_timeouts_at(deadline).unwrap()[0],
ClientEvent::EchoLoss { seq: 0, .. }
));
} else {
assert!(matches!(
reply(&mut machine, 0),
ClientEvent::EchoReply { seq: 0, .. }
));
}
let session = machine.open_session_mut().unwrap();
session.next_wire_seq = u32::MAX;
session.highest_received_seq = Some(u32::MAX - 1);
assert_eq!(send(&mut machine).seq, u32::MAX);
assert!(matches!(
reply(&mut machine, u32::MAX),
ClientEvent::EchoReply { seq: u32::MAX, .. }
));
assert_eq!(send(&mut machine).seq, 0);
assert!(matches!(
reply(&mut machine, 0),
ClientEvent::EchoReply { seq: 0, .. }
));
assert!(matches!(
reply(&mut machine, 0),
ClientEvent::DuplicateReply { seq: 0, .. }
));
assert_eq!(send(&mut machine).seq, 1);
assert!(matches!(
reply(&mut machine, 1),
ClientEvent::EchoReply { seq: 1, .. }
));
assert!(
matches!(
reply(&mut machine, 0),
ClientEvent::DuplicateReply { seq: 0, .. }
),
"reused sequence must receive a fresh completion retention lifetime"
);
assert_eq!(send(&mut machine).seq, 2);
assert!(matches!(
reply(&mut machine, 2),
ClientEvent::EchoReply { seq: 2, .. }
));
assert!(
matches!(
reply(&mut machine, 0),
ClientEvent::LateReply {
sent_at: None,
rtt: None,
..
}
),
"evicted reused sequence must not resurrect a previous epoch's timeout"
);
}
}
// A recoverable send failure that consumes an OPT_ID, followed by a
// plausible wrong-ID completion, cannot be induced reliably with public
// sockets. Exercise that failure boundary directly, without a socket hook
// or inspecting the stored correlation flag/metadata.
#[test]
fn failed_submission_disables_later_tx_endpoints_for_the_session() {
let mut machine = opened();
for attempt in 0..3 {
if attempt == 1 {
machine.invalidate_kernel_tx_correlation();
}
let prepared = machine.prepare_probe().unwrap();
let preflight = machine.preflight_probe_commit(&prepared).unwrap();
let pre_send = ClientTimestamp::now();
let commit = machine.finalize_probe_commit(preflight, pre_send).unwrap();
std::thread::sleep(Duration::from_millis(1));
let sent =
machine.commit_probe_sent(commit, ClientTimestamp::now(), prepared.bytes.len());
machine.record_kernel_tx_timestamp(sent.seq, pre_send.wall);
let event = reply(&mut machine, sent.seq);
let ClientEvent::EchoReply {
one_way: Some(one_way),
..
} = event
else {
panic!("expected measured reply")
};
let expected_wall = if attempt == 0 {
pre_send.wall
} else {
sent.sent_at.wall
};
let wall_ns =
i128::try_from(expected_wall.duration_since(UNIX_EPOCH).unwrap().as_nanos())
.unwrap();
assert_eq!(one_way.client_to_server.unwrap().as_nanos(), 1 - wall_ns);
}
}
}