tests: rework the actually useful tests to live in the integration crate
Rust CI / cargo fmt (push) Successful in 17s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 34s
Rust CI / cargo clippy (push) Successful in 35s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 10s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 1m14s
Rust CI / cargo doc (push) Successful in 23s
Fuzz Smoke / cargo fuzz smoke (push) Failing after 29s
Supply chain / cargo-deny advisories (push) Successful in 9s
Supply chain / cargo-deny bans licenses sources (push) Successful in 5s
Supply chain / actionlint (push) Successful in 3s
Netem Interop / netem interop (push) Successful in 1m11s

This commit is contained in:
Nils Andreas Svee committed 2026-10-03 13:59:38 +07:00
1 parent 74e7532965
commit 48a50d7f3a
101 files changed
+3636 -15159

No files matched your search

@@ -1,379 +0,0 @@
use std::{
net::{SocketAddr, UdpSocket},
sync::{mpsc::sync_channel, Arc, Condvar, Mutex},
thread::{self, JoinHandle},
time::{Duration, Instant},
};
use irtt_proto::{
decode_request, encode_echo_reply, encode_open_reply, flags, DecodedRequestKind, EchoReply,
OpenReply, Params, ReceivedStats, StampAt, TimestampFields,
};
use tokio::{runtime::Builder, time::timeout};
use super::{
blocking::WorkerRuntime, BlockingManagedClient, ManagedClientConfig, ManagedCompletionPolicy,
ManagedEndReason, ManagedEvent, ManagedLifecycle, ManagedPacing, ManagedTargetConfig,
ManagedTargetEndReason,
};
use crate::ClientConfig;
const TOKEN: u64 = 0x1234_5678_90ab_cdef;
struct Signal {
value: Mutex<bool>,
ready: Condvar,
}
impl Signal {
fn set(&self) {
*self.value.lock().unwrap() = true;
self.ready.notify_all();
}
fn wait(&self) {
let value = self.value.lock().unwrap();
let (value, timeout) = self
.ready
.wait_timeout_while(value, Duration::from_secs(2), |value| !*value)
.unwrap();
assert!(*value && !timeout.timed_out());
}
}
struct EchoServer {
addr: SocketAddr,
probe: Arc<Signal>,
close: Arc<Signal>,
worker: JoinHandle<()>,
}
impl EchoServer {
fn start() -> Self {
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
socket
.set_read_timeout(Some(Duration::from_secs(3)))
.unwrap();
let addr = socket.local_addr().unwrap();
let probe = Arc::new(Signal {
value: Mutex::new(false),
ready: Condvar::new(),
});
let close = Arc::new(Signal {
value: Mutex::new(false),
ready: Condvar::new(),
});
let worker_probe = Arc::clone(&probe);
let worker_close = Arc::clone(&close);
let worker = thread::spawn(move || {
let mut buffer = [0_u8; 8192];
let (open_len, peer) = socket.recv_from(&mut buffer).unwrap();
let negotiated = match decode_request(&buffer[..open_len]).unwrap().kind {
DecodedRequestKind::Open { params, .. } => Params::decode(params).unwrap(),
other => panic!("expected an open request, got {other:?}"),
};
let reply = encode_open_reply(
&OpenReply {
flags: flags::FLAG_OPEN | flags::FLAG_REPLY,
token: TOKEN,
params: negotiated.clone(),
},
None,
)
.unwrap();
socket.send_to(&reply, peer).unwrap();
loop {
let (len, packet_peer) = socket.recv_from(&mut buffer).unwrap();
let Ok(request) = decode_request(&buffer[..len]) else {
continue;
};
match request.kind {
DecodedRequestKind::Echo { sequence, .. } => {
worker_probe.set();
let reply = encode_echo_reply(
&EchoReply {
flags: flags::FLAG_REPLY,
token: TOKEN,
sequence,
recv_count: None,
recv_window: None,
timestamps: TimestampFields::default(),
payload: Vec::new(),
},
&negotiated,
None,
)
.unwrap();
socket.send_to(&reply, packet_peer).unwrap();
}
DecodedRequestKind::Close { .. } => {
worker_close.set();
return;
}
DecodedRequestKind::Open { .. } => {}
}
}
});
Self {
addr,
probe,
close,
worker,
}
}
fn wait_probe(&self) {
self.probe.wait();
}
fn wait_close(&self) {
self.close.wait();
}
fn finish(self) {
self.worker.join().unwrap();
}
}
fn test_runtime() -> tokio::runtime::Runtime {
Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.unwrap()
}
fn config(completion: ManagedCompletionPolicy) -> ManagedClientConfig {
ManagedClientConfig {
client: ClientConfig {
duration: Some(Duration::from_millis(120)),
interval: Duration::from_millis(20),
probe_timeout: Duration::from_millis(40),
open_timeouts: vec![Duration::from_millis(200)],
received_stats: ReceivedStats::None,
stamp_at: StampAt::None,
..ClientConfig::default()
},
pacing: ManagedPacing::Burst,
completion,
final_drain: Duration::from_millis(5),
..ManagedClientConfig::default()
}
}
fn target(server: &EchoServer) -> ManagedTargetConfig {
ManagedTargetConfig::new("target", server.addr.to_string())
}
fn assert_stopped(outcome: &super::ManagedOutcome) {
assert_eq!(outcome.end_reason, ManagedEndReason::StopRequested);
}
#[test]
fn starts_without_caller_runtime_and_finishes_naturally() {
let server = EchoServer::start();
let owner = BlockingManagedClient::start(
config(ManagedCompletionPolicy::FinishWhenQuiescent),
vec![target(&server)],
)
.unwrap();
server.wait_probe();
let outcome = owner.join().unwrap();
assert_eq!(outcome.end_reason, ManagedEndReason::TargetsComplete);
assert_eq!(outcome.recent_target_outcomes.len(), 1);
assert_eq!(
outcome.recent_target_outcomes[0].end_reason,
ManagedTargetEndReason::TestComplete
);
server.wait_close();
server.finish();
}
#[test]
fn subscribed_start_captures_events_from_a_short_run() {
let server = EchoServer::start();
let (owner, mut events) = BlockingManagedClient::start_with_subscription(
config(ManagedCompletionPolicy::FinishWhenQuiescent),
vec![target(&server)],
)
.unwrap();
server.wait_probe();
let outcome = owner.join().unwrap();
assert_eq!(outcome.end_reason, ManagedEndReason::TargetsComplete);
let mut saw_started = false;
let mut saw_finished = false;
let mut saw_completed = false;
while let Ok(event) = events.try_recv() {
match event {
ManagedEvent::Started => saw_started = true,
ManagedEvent::TargetFinished { .. } => saw_finished = true,
ManagedEvent::Completed { .. } => saw_completed = true,
_ => {}
}
}
assert!(saw_started && saw_finished && saw_completed);
server.wait_close();
server.finish();
}
#[test]
fn external_handle_stop_ends_with_graceful_close() {
let server = EchoServer::start();
let owner = BlockingManagedClient::start(
config(ManagedCompletionPolicy::ExplicitStop),
vec![target(&server)],
)
.unwrap();
let handle = owner.handle();
server.wait_probe();
test_runtime().block_on(async {
timeout(Duration::from_secs(2), handle.stop())
.await
.unwrap();
});
let outcome = owner.join().unwrap();
assert_stopped(&outcome);
server.wait_close();
server.finish();
}
#[test]
fn dynamic_update_reaches_background_worker() {
let server = EchoServer::start();
let owner = BlockingManagedClient::start(config(ManagedCompletionPolicy::ExplicitStop), vec![])
.unwrap();
let handle = owner.handle();
let acknowledgement = handle
.update_targets(vec![target(&server)])
.unwrap()
.blocking_wait()
.unwrap();
assert_eq!(acknowledgement.sequence, 1);
server.wait_probe();
test_runtime().block_on(async {
timeout(Duration::from_secs(2), owner.handle().stop())
.await
.unwrap();
});
assert_stopped(&owner.join().unwrap());
server.wait_close();
server.finish();
}
#[test]
fn dropping_external_handles_does_not_stop_owner() {
let server = EchoServer::start();
let owner = BlockingManagedClient::start(
config(ManagedCompletionPolicy::FinishWhenQuiescent),
vec![target(&server)],
)
.unwrap();
drop(owner.handle());
drop(owner.handle());
server.wait_probe();
assert_eq!(
owner.join().unwrap().end_reason,
ManagedEndReason::TargetsComplete
);
server.wait_close();
server.finish();
}
#[test]
fn dropping_owner_stops_and_joins_worker() {
let server = EchoServer::start();
let owner = BlockingManagedClient::start(
config(ManagedCompletionPolicy::ExplicitStop),
vec![target(&server)],
)
.unwrap();
let handle = owner.handle();
server.wait_probe();
drop(owner);
let status = handle.status();
assert_eq!(status.lifecycle, ManagedLifecycle::Completed);
assert_eq!(
status.final_outcome.as_ref().unwrap().end_reason,
ManagedEndReason::StopRequested
);
server.wait_close();
server.finish();
}
#[test]
fn dropping_worker_runtime_inside_async_context_does_not_panic() {
test_runtime().block_on(async {
let runtime = Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.unwrap();
drop(WorkerRuntime::new(runtime));
});
}
#[test]
fn worker_runtime_shutdown_is_bounded_with_blocking_work() {
let runtime = Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.unwrap();
let (started_send, started_receive) = sync_channel(0);
let (release_send, release_receive) = sync_channel(0);
runtime.block_on(async move {
tokio::task::spawn_blocking(move || {
started_send.send(()).unwrap();
release_receive.recv().unwrap();
});
});
started_receive
.recv_timeout(Duration::from_secs(2))
.unwrap();
let started = Instant::now();
WorkerRuntime::new(runtime).shutdown();
assert!(started.elapsed() < Duration::from_secs(2));
release_send.send(()).unwrap();
}
#[test]
fn drop_after_natural_completion_preserves_outcome() {
let server = EchoServer::start();
let owner = BlockingManagedClient::start(
config(ManagedCompletionPolicy::FinishWhenQuiescent),
vec![target(&server)],
)
.unwrap();
let handle = owner.handle();
server.wait_probe();
server.wait_close();
let deadline = Instant::now() + Duration::from_secs(2);
let status = loop {
let status = handle.status();
if status.lifecycle == ManagedLifecycle::Completed && status.final_outcome.is_some() {
break status;
}
assert!(Instant::now() < deadline, "managed worker did not complete");
thread::sleep(Duration::from_millis(5));
};
assert_eq!(
status.final_outcome.as_ref().unwrap().end_reason,
ManagedEndReason::TargetsComplete
);
drop(owner);
assert_eq!(
handle.status().final_outcome.as_ref().unwrap().end_reason,
ManagedEndReason::TargetsComplete
);
server.finish();
}
-3
View File
@@ -16,8 +16,5 @@ pub use model::*;
#[cfg(feature = "tokio")]
pub use task::*;
#[cfg(all(feature = "tokio", test))]
mod blocking_tests;
#[cfg(feature = "tokio")]
mod schedule;
@@ -152,73 +152,3 @@ pub(crate) fn instant_abs_diff(left: Instant, right: Instant) -> Duration {
.or_else(|| right.checked_duration_since(left))
.unwrap_or(Duration::ZERO)
}
#[cfg(test)]
mod tests {
use super::*;
use irtt_proto::Params;
fn negotiated(duration_ns: i64) -> NegotiatedParams {
NegotiatedParams {
params: Params {
interval_ns: 10_000_000,
duration_ns,
..Params::default()
},
restrictions: Vec::new(),
}
}
#[test]
fn missed_slots_skip_on_the_absolute_grid_only_after_commit() {
let start = Instant::now();
let mut schedule = ProbeSchedule::new(start, &negotiated(0)).unwrap();
for (now_ms, intended_ms, next_ms) in [(0, 0, 10), (45, 40, 50), (50, 50, 60)] {
let deadline = schedule.next_send_deadline().unwrap();
let commit = schedule
.preflight_managed_commit(deadline, start + Duration::from_millis(now_ms))
.unwrap();
assert_eq!(
commit.scheduled_at,
start + Duration::from_millis(intended_ms)
);
// A failed or pending submission discards this preflight.
assert_eq!(schedule.next_send_deadline(), Some(deadline));
schedule.commit(commit);
assert_eq!(
schedule.next_send_deadline(),
Some(start + Duration::from_millis(next_ms))
);
}
schedule
.skip_missed_slots_at(start + Duration::from_millis(85))
.unwrap();
assert_eq!(
schedule.next_send_deadline(),
Some(start + Duration::from_millis(90))
);
}
#[test]
fn finite_duration_excludes_its_end_even_when_a_slot_is_missed() {
let start = Instant::now();
let mut schedule = ProbeSchedule::new(start, &negotiated(20_000_000)).unwrap();
assert!(schedule.permit_probe_at(start + Duration::from_millis(19)));
let commit = schedule
.preflight_managed_commit(start, start + Duration::from_millis(19))
.unwrap();
schedule.commit(commit);
assert!(schedule.is_finished());
let mut missed = ProbeSchedule::new(start, &negotiated(20_000_000)).unwrap();
assert!(!missed.permit_probe_at(start + Duration::from_millis(20)));
assert!(missed.is_finished());
}
#[test]
fn cadence_rejects_overflow_before_commit() {
assert!(matches!(
advance_cadence(Instant::now(), Duration::MAX, Instant::now()),
Err(ClientError::DurationOverflow)
));
}
}
+46 -150
View File
@@ -177,74 +177,6 @@ pub struct ManagedClientHandle {
max_live_target_generations: usize,
status: watch::Receiver<Arc<ManagedStatus>>,
events: broadcast::WeakSender<ManagedEvent>,
#[cfg(test)]
update_submission_hook: Option<Arc<UpdateSubmissionTestHook>>,
}
// Pauses between admission and enqueue to exercise stop/seal linearization.
#[cfg(test)]
#[derive(Default)]
struct UpdateSubmissionTestHook {
state: std::sync::Mutex<UpdateSubmissionTestState>,
arrived: std::sync::Condvar,
released: std::sync::Condvar,
}
#[cfg(test)]
#[derive(Default)]
struct UpdateSubmissionTestState {
armed: bool,
arrived: bool,
released: bool,
}
#[cfg(test)]
impl UpdateSubmissionTestHook {
fn arm(&self) {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.armed = true;
}
fn pause_after_admission(&self) {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !state.armed {
return;
}
state.arrived = true;
self.arrived.notify_all();
while !state.released {
state = self
.released
.wait(state)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
}
fn wait_until_arrived(&self) {
let state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (state, timeout) = self
.arrived
.wait_timeout_while(state, Duration::from_secs(2), |state| !state.arrived)
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert!(state.arrived && !timeout.timed_out());
}
fn release(&self) {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.released = true;
self.released.notify_all();
}
}
impl fmt::Debug for ManagedClientHandle {
@@ -306,11 +238,6 @@ impl ManagedClientHandle {
});
}
#[cfg(test)]
if let Some(hook) = &self.update_submission_hook {
hook.pause_after_admission();
}
// Observing `Open` only permits attempting submission. A successful
// `try_send` transfers command ownership to the driver. A concurrent
// stop may move admission to `Stopping` before this send, allowing the
@@ -403,9 +330,6 @@ impl Future for ManagedStopReceipt {
type ConnectFuture =
Pin<Box<dyn Future<Output = Result<AsyncClient, ClientError>> + Send + 'static>>;
type WakeFuture = Pin<Box<dyn Future<Output = Option<watch::Receiver<()>>> + Send + 'static>>;
// Snapshots at publication verify status is visible before the corresponding event.
#[cfg(test)]
type EventObservations = Arc<std::sync::Mutex<Vec<(ManagedEvent, Arc<ManagedStatus>)>>>;
fn arm_wake(mut receiver: watch::Receiver<()>) -> WakeFuture {
Box::pin(async move {
@@ -688,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,
@@ -710,20 +636,6 @@ pub struct ManagedClientTask {
final_outcome: Option<Arc<ManagedOutcome>>,
next_generation: u64,
applied_command_sequence: u64,
#[cfg(test)]
event_observations: Option<EventObservations>,
#[cfg(test)]
drain_test_hook: DrainTestHook,
// Counts work that cannot be observed through lossy presentation events.
#[cfg(test)]
timeout_inspections: usize,
}
// Holds queued replies past the drain deadline to exercise the final receive budget.
#[cfg(test)]
#[derive(Default)]
struct DrainTestHook {
defer_work_until_deadline: bool,
}
impl ManagedClientTask {
@@ -734,13 +646,6 @@ impl ManagedClientTask {
}
fn publish_event(&self, event: ManagedEvent) {
#[cfg(test)]
if let Some(observations) = &self.event_observations {
observations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push((event.clone(), Arc::clone(&self.resources().status.borrow())));
}
if let Some(events) = &self.resources().events {
let _ = events.send(event);
}
@@ -1376,22 +1281,6 @@ impl ManagedClientTask {
mut cleanup_failure,
mut post_deadline_receives_remaining,
} => {
#[cfg(test)]
let defer_work = self.drain_test_hook.defer_work_until_deadline && now < deadline;
#[cfg(not(test))]
let defer_work = false;
if defer_work {
self.targets[index].state = TargetState::Draining {
client,
drain_started_at,
deadline,
primary_end,
cleanup_failure,
post_deadline_receives_remaining,
};
return false;
}
if client
.next_probe_timeout_deadline()
.is_some_and(|timeout| timeout <= now)
@@ -1899,10 +1788,6 @@ impl ManagedClientTask {
}
let index = self.timeout_cursor;
self.timeout_cursor = (self.timeout_cursor + 1) % target_len;
#[cfg(test)]
{
self.timeout_inspections += 1;
}
if self.target_has_due_timeout(index, now) {
let generation = self.targets[index].instance.generation;
let step = self.poll_target_timeout(index, now);
@@ -1950,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
@@ -1958,20 +1845,9 @@ impl ManagedClientTask {
.next_probe_timeout_deadline()
.is_some_and(|deadline| deadline <= now)
}
TargetState::Draining {
client,
deadline: _deadline,
..
} => {
#[cfg(test)]
let defer_work = self.drain_test_hook.defer_work_until_deadline && now < *_deadline;
#[cfg(not(test))]
let defer_work = false;
!defer_work
&& client
.next_probe_timeout_deadline()
.is_some_and(|timeout| timeout <= now)
}
TargetState::Draining { client, .. } => client
.next_probe_timeout_deadline()
.is_some_and(|timeout| timeout <= now),
_ => false,
}
}
@@ -2076,17 +1952,9 @@ impl ManagedClientTask {
TargetState::Draining {
client, deadline, ..
} => {
#[cfg(test)]
let defer_work = self.drain_test_hook.defer_work_until_deadline;
#[cfg(not(test))]
let defer_work = false;
non_send_deadline = non_send_deadline
.into_iter()
.chain(
(!defer_work)
.then(|| client.next_probe_timeout_deadline())
.flatten(),
)
.chain(client.next_probe_timeout_deadline())
.chain(Some(*deadline))
.min();
}
@@ -2454,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,
@@ -2476,12 +2346,6 @@ fn build_task(
final_outcome: None,
next_generation,
applied_command_sequence: 0,
#[cfg(test)]
event_observations: None,
#[cfg(test)]
drain_test_hook: DrainTestHook::default(),
#[cfg(test)]
timeout_inspections: 0,
};
let handle = ManagedClientHandle {
stop,
@@ -2489,8 +2353,6 @@ fn build_task(
max_live_target_generations,
status: status_receiver,
events: weak_events,
#[cfg(test)]
update_submission_hook: None,
};
Ok((task, handle))
}
@@ -2533,5 +2395,39 @@ fn close_timeout_failure() -> ManagedTargetFailure {
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;
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"
);
}
}
File diff suppressed because it is too large. Load diff