test: protect durable behavior identified in review
This commit is contained in:
7 files changed
+528
-6
No files matched your search
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user