From d571315068f48e5188e6cf713e9d46438f1a8230 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Fri, 2 Oct 2026 23:16:48 +0700 Subject: [PATCH] test(client): avoid scheduler-dependent timing assertions --- crates/irtt-client/tests/linux_transport.rs | 7 +- .../tests/managed_behavior.rs | 88 +++++++++++-------- 2 files changed, 57 insertions(+), 38 deletions(-) diff --git a/crates/irtt-client/tests/linux_transport.rs b/crates/irtt-client/tests/linux_transport.rs index bd89537..68bed05 100644 --- a/crates/irtt-client/tests/linux_transport.rs +++ b/crates/irtt-client/tests/linux_transport.rs @@ -108,8 +108,11 @@ fn assert_kernel_send_endpoint(event: &ClientEvent, before_send: SystemTime) { let selected = i128::from(timing.receive_wall_ns.unwrap()) - one_way.client_to_server.unwrap().as_nanos(); assert!(selected >= ns(before_send)); - assert!( - selected < ns(sent_at.wall), + // TX_SOFTWARE may arrive before or after send returns; only equality + // identifies the userspace fallback. Receipt remains the upper bound. + assert_ne!( + selected, + ns(sent_at.wall), "TX timestamp was not observed; userspace fallback is insufficient" ); assert!(selected <= ns(received_at.wall)); diff --git a/crates/irtt-integration-tests/tests/managed_behavior.rs b/crates/irtt-integration-tests/tests/managed_behavior.rs index 9972f43..8ef85b2 100644 --- a/crates/irtt-integration-tests/tests/managed_behavior.rs +++ b/crates/irtt-integration-tests/tests/managed_behavior.rs @@ -1,6 +1,10 @@ mod support; -use std::{collections::HashMap, time::Duration}; +use std::{ + collections::HashMap, + future::{poll_fn, Future}, + time::Duration, +}; use irtt_client::{managed::*, ClientConfig, ClientEvent}; use irtt_server::ServerConfig; @@ -29,48 +33,60 @@ async fn both_pacing_modes_serve_every_target_on_its_negotiated_grid() { ["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 task = Box::pin(task); let mut sends: HashMap> = HashMap::new(); - let mut actual_sends = Vec::new(); - while let Ok(event) = events.recv().await { - if let ManagedEvent::Client { - target, - event: - ClientEvent::EchoSent { - scheduled_at, - sent_at, - .. - }, - } = event - { - let scheduled = scheduled_at.expect("managed probes expose their cadence"); - assert!(sent_at.mono >= scheduled); - 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); - assert!(!elapsed.is_zero()); - assert_eq!(elapsed.as_nanos() % Duration::from_millis(100).as_nanos(), 0); + 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() { + if let ManagedEvent::Client { + target, + event: + ClientEvent::EchoSent { + scheduled_at, + sent_at, + .. + }, + } = event + { + let scheduled = scheduled_at.expect("managed probes expose their cadence"); + assert!(sent_at.mono >= scheduled); + released += 1; + let previous = sends.entry(target).or_default(); + if let Some(last) = previous.last() { + let elapsed: Duration = scheduled.duration_since(*last); + assert!(!elapsed.is_zero()); + assert_eq!( + elapsed.as_nanos() % Duration::from_millis(100).as_nanos(), + 0 + ); + } + previous.push(scheduled); } - previous.push(scheduled); } - } - let outcome = run.await.unwrap(); + if released > 0 { + release_batches.push(released); + } + result + }) + .await; assert_eq!(outcome.end_reason, ManagedEndReason::TargetsComplete); assert_eq!(outcome.successful_target_outcomes, 2); assert_eq!(sends.len(), 2); - actual_sends.sort_by_key(|(_, at)| *at); - let cross_target_gaps = actual_sends.windows(2) - .filter(|pair| pair[0].0 != pair[1].0) - .map(|pair| pair[1].1.duration_since(pair[0].1)).collect::>(); - assert!(cross_target_gaps.len() >= 2); - // Allow timer wake jitter while separating the nominal 50 ms - // stagger spacing from back-to-back burst sends. + // 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!(cross_target_gaps.iter().filter(|gap| **gap < Duration::from_millis(25)).count() >= 2, - "burst must repeatedly send different targets back-to-back: {cross_target_gaps:?}"), - ManagedPacing::Staggered => assert!(cross_target_gaps.iter().all(|gap| *gap >= Duration::from_millis(35)), - "staggered must space different targets across the interval: {cross_target_gaps:?}"), + 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);