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

@@ -1,10 +1,6 @@
mod support;
use std::{
collections::HashMap,
future::{poll_fn, Future},
time::Duration,
};
use std::{collections::HashMap, time::Duration};
use irtt_client::{managed::*, ClientConfig, ClientEvent};
use irtt_server::ServerConfig;
@@ -15,31 +11,30 @@ use tokio::time::timeout;
// on a scheduler, a send cursor, or a private event-publication hook.
#[tokio::test(flavor = "current_thread")]
async fn both_pacing_modes_serve_every_target_on_its_negotiated_grid() {
timeout(Duration::from_secs(5), async {
timeout(Duration::from_secs(10), async {
let server = InTreeServer::start(ServerConfig::default());
for pacing in [ManagedPacing::Burst, ManagedPacing::Staggered] {
let config = ManagedClientConfig {
client: ClientConfig {
duration: Some(Duration::from_millis(600)),
interval: Duration::from_millis(100),
probe_timeout: Duration::from_millis(100),
open_timeouts: vec![Duration::from_millis(200)],
..ClientConfig::default()
},
pacing,
..ManagedClientConfig::default()
};
let targets =
["first", "second"].map(|id| ManagedTargetConfig::new(id, server.addr.to_string()));
let (task, handle) = ManagedClient::task(config, targets.into()).unwrap();
let mut events = handle.subscribe().unwrap();
let mut task = Box::pin(task);
let mut sends: HashMap<TargetInstance, Vec<_>> = HashMap::new();
let mut release_batches = Vec::new();
let outcome = poll_fn(|cx| {
let result = task.as_mut().poll(cx);
let mut released = 0;
while let Ok(event) = events.try_recv() {
let mut observed_pacing = false;
for _ in 0..3 {
let config = ManagedClientConfig {
client: ClientConfig {
duration: Some(Duration::from_millis(600)),
interval: Duration::from_millis(100),
probe_timeout: Duration::from_millis(100),
open_timeouts: vec![Duration::from_millis(200)],
..ClientConfig::default()
},
pacing,
..ManagedClientConfig::default()
};
let targets = ["first", "second"]
.map(|id| ManagedTargetConfig::new(id, server.addr.to_string()));
let (task, handle) = ManagedClient::task(config, targets.into()).unwrap();
let mut events = handle.subscribe().unwrap();
let run = tokio::spawn(task);
let mut sends: HashMap<TargetInstance, Vec<_>> = HashMap::new();
let mut actual_sends = Vec::new();
while let Ok(event) = events.recv().await {
if let ManagedEvent::Client {
target,
event:
@@ -52,7 +47,7 @@ async fn both_pacing_modes_serve_every_target_on_its_negotiated_grid() {
{
let scheduled = scheduled_at.expect("managed probes expose their cadence");
assert!(sent_at.mono >= scheduled);
released += 1;
actual_sends.push((target.clone(), sent_at.mono));
let previous = sends.entry(target).or_default();
if let Some(last) = previous.last() {
let elapsed: Duration = scheduled.duration_since(*last);
@@ -65,33 +60,43 @@ async fn both_pacing_modes_serve_every_target_on_its_negotiated_grid() {
previous.push(scheduled);
}
}
if released > 0 {
release_batches.push(released);
let outcome = run.await.unwrap();
assert_eq!(outcome.end_reason, ManagedEndReason::TargetsComplete);
assert_eq!(outcome.successful_target_outcomes, 2);
assert_eq!(sends.len(), 2);
for target in outcome.recent_target_outcomes.iter() {
assert!(sends[&target.target].len() >= 2);
assert!(target.replies_received >= 2);
}
// Use only the initial pair: later stagger gates can legitimately
// compress after a delayed wake. Startup may instead serve one
// target twice before the other opens, so retry a fresh run then.
if actual_sends.len() < 2 || actual_sends[0].0 == actual_sends[1].0 {
continue;
}
let gap = actual_sends[1].1.duration_since(actual_sends[0].1);
match pacing {
ManagedPacing::Staggered => {
assert!(
gap >= Duration::from_millis(50),
"first stagger gate was bypassed: {gap:?}"
);
observed_pacing = true;
}
ManagedPacing::Burst => {
// A pause between burst sends makes this observation
// inconclusive; it must not fail an otherwise valid run.
observed_pacing = gap < Duration::from_millis(25);
}
}
if observed_pacing {
break;
}
result
})
.await;
assert_eq!(outcome.end_reason, ManagedEndReason::TargetsComplete);
assert_eq!(outcome.successful_target_outcomes, 2);
assert_eq!(sends.len(), 2);
// Drive the public Future and observe the events each poll releases.
// Burst serves ready targets together; staggered releases one before
// yielding to its next gate. Descheduling cannot stretch/compress an
// observed wall-time gap into a different pacing classification.
match pacing {
ManagedPacing::Burst => assert!(
release_batches.iter().filter(|count| **count == 2).count() >= 2,
"burst must repeatedly release both ready targets: {release_batches:?}"
),
ManagedPacing::Staggered => assert!(
release_batches.iter().all(|count| *count == 1),
"staggered must release targets separately: {release_batches:?}"
),
}
for target in outcome.recent_target_outcomes.iter() {
assert!(sends[&target.target].len() >= 2);
assert!(target.replies_received >= 2);
}
assert!(
observed_pacing,
"could not observe {pacing:?} startup pacing in three fresh runs"
);
}
})
.await