tests(client): cleanup unnecessary tests and hooks
Rust CI / cargo fmt (push) Successful in 14s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 32s
Rust CI / cargo clippy (push) Successful in 34s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Failing after 34s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 7s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 10s
Rust CI / cargo doc (push) Successful in 26s
Fuzz Smoke / cargo fuzz smoke (push) Failing after 32s
Supply chain / cargo-deny advisories (push) Successful in 6s
Supply chain / cargo-deny bans licenses sources (push) Successful in 5s
Supply chain / actionlint (push) Successful in 2s
Netem Interop / netem interop (push) Successful in 1m3s

This commit is contained in:
Nils Andreas Svee committed 2026-10-02 08:33:49 +07:00
1 parent 50fe83a4c8
commit dc5b19059e
27 files changed
+118 -2625

No files matched your search

+5 -13
View File
@@ -157,22 +157,14 @@ impl BlockingManagedClient {
}
fn join_worker(&mut self) -> Result<ManagedOutcome, BlockingManagedJoinError> {
join_worker(
self.worker
.take()
.expect("blocking managed worker is joined at most once"),
)
self.worker
.take()
.expect("blocking managed worker is joined at most once")
.join()
.map_err(|_| BlockingManagedJoinError::WorkerPanicked)
}
}
pub(super) fn join_worker(
worker: JoinHandle<ManagedOutcome>,
) -> Result<ManagedOutcome, BlockingManagedJoinError> {
worker
.join()
.map_err(|_| BlockingManagedJoinError::WorkerPanicked)
}
impl Drop for BlockingManagedClient {
fn drop(&mut self) {
if self.worker.is_some() {
@@ -12,8 +12,7 @@ use irtt_proto::{
use tokio::{runtime::Builder, time::timeout};
use super::{
blocking::{join_worker, WorkerRuntime},
BlockingManagedClient, BlockingManagedJoinError, ManagedClientConfig, ManagedCompletionPolicy,
blocking::WorkerRuntime, BlockingManagedClient, ManagedClientConfig, ManagedCompletionPolicy,
ManagedEndReason, ManagedEvent, ManagedLifecycle, ManagedPacing, ManagedTargetConfig,
ManagedTargetEndReason,
};
@@ -309,15 +308,6 @@ fn dropping_owner_stops_and_joins_worker() {
server.finish();
}
#[test]
fn worker_panic_is_a_join_error() {
let worker = thread::spawn(|| -> super::ManagedOutcome { panic!("test worker panic") });
assert_eq!(
join_worker(worker),
Err(BlockingManagedJoinError::WorkerPanicked)
);
}
#[test]
fn dropping_worker_runtime_inside_async_context_does_not_panic() {
test_runtime().block_on(async {
-196
View File
@@ -449,199 +449,3 @@ pub(crate) fn classify_client_error(
message: Arc::from(error.to_string()),
}
}
#[cfg(test)]
mod tests {
use std::{collections::TryReserveError, io};
use super::*;
fn io_error() -> io::Error {
io::Error::other("boom")
}
fn try_reserve_error() -> TryReserveError {
Vec::<u8>::new().try_reserve(usize::MAX).unwrap_err()
}
fn classification_cases() -> Vec<(ClientError, ManagedTargetFailureKind)> {
vec![
(
ClientError::Resolve {
addr: "example.invalid:2112".to_owned(),
},
ManagedTargetFailureKind::Resolve,
),
(
ClientError::Socket(io_error()),
ManagedTargetFailureKind::Socket,
),
(
ClientError::DatagramLengthMismatch {
expected: 64,
actual: 32,
},
ManagedTargetFailureKind::Socket,
),
(
ClientError::SocketOption {
operation: "set read timeout",
remote: "127.0.0.1:2112".parse().unwrap(),
source: io_error(),
},
ManagedTargetFailureKind::SocketOption,
),
(
ClientError::ReadTimeoutRestore { source: io_error() },
ManagedTargetFailureKind::SocketOption,
),
(
ClientError::Protocol(irtt_proto::ProtoError::BadMagic),
ManagedTargetFailureKind::Protocol,
),
(
ClientError::ProtocolVersionMismatch {
requested: 1,
received: 2,
},
ManagedTargetFailureKind::Protocol,
),
(ClientError::ZeroToken, ManagedTargetFailureKind::Protocol),
(
ClientError::UnexpectedNoTestReply,
ManagedTargetFailureKind::Protocol,
),
(
ClientError::NonZeroNoTestToken { token: 7 },
ManagedTargetFailureKind::Protocol,
),
(
ClientError::ServerRejected,
ManagedTargetFailureKind::Protocol,
),
(
ClientError::NegotiationRejected {
reason: "duration".to_owned(),
},
ManagedTargetFailureKind::Protocol,
),
(ClientError::OpenTimeout, ManagedTargetFailureKind::Timeout),
(
ClientError::InvalidConfig {
reason: "interval".to_owned(),
},
ManagedTargetFailureKind::Configuration,
),
(
ClientError::OpenTimeoutTooSmall {
timeout: Duration::from_millis(1),
minimum: Duration::from_millis(10),
},
ManagedTargetFailureKind::Configuration,
),
(
ClientError::NoOpenTimeouts,
ManagedTargetFailureKind::Configuration,
),
(
ClientError::AllocationFailed {
operation: "pending probes",
source: try_reserve_error(),
},
ManagedTargetFailureKind::ResourceExhausted,
),
(
ClientError::CounterOverflow {
counter: "packets_sent",
},
ManagedTargetFailureKind::ResourceExhausted,
),
(
ClientError::DurationOverflow,
ManagedTargetFailureKind::ResourceExhausted,
),
(
ClientError::PendingLimitExceeded { limit: 8 },
ManagedTargetFailureKind::ResourceExhausted,
),
(
ClientError::NoTokioRuntime,
ManagedTargetFailureKind::InvalidState,
),
(ClientError::NotOpen, ManagedTargetFailureKind::InvalidState),
(
ClientError::AlreadyOpen,
ManagedTargetFailureKind::InvalidState,
),
(
ClientError::AlreadyCompleted,
ManagedTargetFailureKind::InvalidState,
),
(
ClientError::AlreadyClosed,
ManagedTargetFailureKind::InvalidState,
),
(
ClientError::StalePreparedProbe {
prepared_seq: 3,
next_wire_seq: 4,
},
ManagedTargetFailureKind::InvalidState,
),
(
ClientError::PendingSequenceCollision { seq: 9 },
ManagedTargetFailureKind::InvalidState,
),
]
}
#[test]
fn every_client_error_classifies_to_its_documented_kind() {
for (error, expected) in classification_cases() {
let failure = classify_client_error(ManagedTargetFailurePhase::Opening, &error);
assert_eq!(
failure.kind, expected,
"unexpected classification for {error:?}"
);
assert_eq!(failure.phase, ManagedTargetFailurePhase::Opening);
assert_eq!(&*failure.message, error.to_string());
}
}
#[test]
fn classification_preserves_the_reported_phase() {
for phase in [
ManagedTargetFailurePhase::Connecting,
ManagedTargetFailurePhase::Opening,
ManagedTargetFailurePhase::Sending,
ManagedTargetFailurePhase::Receiving,
ManagedTargetFailurePhase::Timing,
ManagedTargetFailurePhase::Closing,
] {
let failure = classify_client_error(phase, &ClientError::OpenTimeout);
assert_eq!(failure.phase, phase);
}
}
#[test]
fn phase_and_kind_display_without_debug_spelling() {
assert_eq!(
ManagedTargetFailurePhase::Connecting.to_string(),
"connecting"
);
assert_eq!(ManagedTargetFailurePhase::Closing.to_string(), "closing");
assert_eq!(
ManagedTargetFailureKind::SocketOption.to_string(),
"socket option"
);
assert_eq!(
ManagedTargetFailureKind::ResourceExhausted.to_string(),
"resource exhausted"
);
assert_eq!(
ManagedTargetFailureKind::InvalidState.to_string(),
"invalid state"
);
assert_eq!(ManagedTargetFailureKind::Other.to_string(), "other");
}
}
+8 -12
View File
@@ -70,7 +70,14 @@ impl ProbeSchedule {
) -> Result<ScheduleCommit, ClientError> {
let (scheduled_at, next_send_at) =
advance_cadence(scheduled_at, self.interval, permission_at)?;
Ok(self.finish_commit(scheduled_at, next_send_at))
Ok(ScheduleCommit {
scheduled_at,
next_send_at: if self.end_at.is_some_and(|end| next_send_at >= end) {
None
} else {
Some(next_send_at)
},
})
}
pub(crate) fn commit(&mut self, commit: ScheduleCommit) {
@@ -90,17 +97,6 @@ impl ProbeSchedule {
pub(crate) fn is_finished(&self) -> bool {
self.next_send_at.is_none()
}
fn finish_commit(&self, scheduled_at: Instant, next_send_at: Instant) -> ScheduleCommit {
ScheduleCommit {
scheduled_at,
next_send_at: if self.end_at.is_some_and(|end| next_send_at >= end) {
None
} else {
Some(next_send_at)
},
}
}
}
pub(crate) fn advance_cadence(
+16 -82
View File
@@ -181,6 +181,7 @@ pub struct ManagedClientHandle {
update_submission_hook: Option<Arc<UpdateSubmissionTestHook>>,
}
// Pauses between admission and enqueue to exercise stop/seal linearization.
#[cfg(test)]
#[derive(Default)]
struct UpdateSubmissionTestHook {
@@ -402,10 +403,9 @@ 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>)>>>;
#[cfg(test)]
type StaggerObservations = Arc<std::sync::Mutex<Vec<(usize, Duration)>>>;
fn arm_wake(mut receiver: watch::Receiver<()>) -> WakeFuture {
Box::pin(async move {
@@ -525,17 +525,7 @@ impl TargetRuntime {
/// The client owns `packets_sent`; this runtime only mirrors it so a
/// terminal outcome can report it after the client is gone.
fn sync_packets_sent(&mut self, client: &AsyncClient) {
self.record_packets_sent(client.packets_sent());
}
/// Adopt an already-read authoritative sent count.
fn record_packets_sent(&mut self, packets_sent: u64) {
self.counters.packets_sent = packets_sent;
}
/// Account for one client event against this target's durable counters.
fn observe(&mut self, event: &ClientEvent) {
self.counters.observe(event);
self.counters.packets_sent = client.packets_sent();
}
/// Build this target's durable outcome.
@@ -723,21 +713,17 @@ pub struct ManagedClientTask {
#[cfg(test)]
event_observations: Option<EventObservations>,
#[cfg(test)]
stagger_observations: Option<StaggerObservations>,
#[cfg(test)]
drain_test_hook: DrainTestHook,
// Counts work that cannot be observed through lossy presentation events.
#[cfg(test)]
timeout_inspections: usize,
#[cfg(test)]
deadline_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,
fail_receive: bool,
fail_close: bool,
}
impl ManagedClientTask {
@@ -837,7 +823,7 @@ impl ManagedClientTask {
fn publish_client_events(&mut self, index: usize, events: Vec<ClientEvent>) {
for event in events {
self.targets[index].observe(&event);
self.targets[index].counters.observe(&event);
self.publish_event(ManagedEvent::Client {
target: self.targets[index].instance.clone(),
event,
@@ -1421,10 +1407,6 @@ impl ManagedClientTask {
return true;
}
#[cfg(test)]
let injected_receive_failure = self.take_drain_receive_failure();
#[cfg(not(test))]
let injected_receive_failure = None;
let mut retained_state_changed = false;
let received = match client.poll_recv(cx) {
Poll::Pending => false,
@@ -1441,13 +1423,7 @@ impl ManagedClientTask {
return self.begin_close(index, client, primary_end, cleanup_failure, now);
}
};
if let Some(error) = injected_receive_failure {
self.targets[index].sync_packets_sent(&client);
cleanup_failure.get_or_insert_with(|| {
classify_client_error(ManagedTargetFailurePhase::Receiving, &error)
});
return self.begin_close(index, client, primary_end, cleanup_failure, now);
}
if client.is_peer_closed() {
self.targets[index].sync_packets_sent(&client);
self.finish_target(index, ManagedTargetEndReason::PeerClosed, cleanup_failure);
@@ -1525,17 +1501,7 @@ impl ManagedClientTask {
Poll::Ready(Ok(events)) => {
self.targets[index].sync_packets_sent(&client);
self.publish_client_events(index, events);
#[cfg(test)]
let injected_close_failure = self.take_drain_close_failure().map(|error| {
classify_client_error(ManagedTargetFailurePhase::Closing, &error)
});
#[cfg(not(test))]
let injected_close_failure = None;
self.finish_target(
index,
primary_end,
cleanup_failure.or(injected_close_failure),
);
self.finish_target(index, primary_end, cleanup_failure);
false
}
Poll::Ready(Err(error)) => {
@@ -1616,26 +1582,6 @@ impl ManagedClientTask {
true
}
#[cfg(test)]
fn take_drain_receive_failure(&mut self) -> Option<ClientError> {
if !mem::take(&mut self.drain_test_hook.fail_receive) {
return None;
}
Some(ClientError::Socket(std::io::Error::other(
"injected drain receive failure",
)))
}
#[cfg(test)]
fn take_drain_close_failure(&mut self) -> Option<ClientError> {
if !mem::take(&mut self.drain_test_hook.fail_close) {
return None;
}
Some(ClientError::Socket(std::io::Error::other(
"injected drain close failure",
)))
}
fn active_count(&self) -> usize {
self.targets
.iter()
@@ -1643,7 +1589,7 @@ impl ManagedClientTask {
.count()
}
fn active_stagger_spacing(&self) -> Option<(usize, Duration)> {
fn active_stagger_spacing(&self) -> Option<Duration> {
let mut active = 0;
let mut minimum: Option<Duration> = None;
for target in &self.targets {
@@ -1661,7 +1607,7 @@ impl ManagedClientTask {
.interval();
minimum = minimum.into_iter().chain(Some(interval)).min();
}
minimum.map(|interval| (active, stagger_spacing(interval, active)))
minimum.map(|interval| stagger_spacing(interval, active))
}
fn stagger_target_added(&mut self, now: Instant) {
@@ -1672,7 +1618,7 @@ impl ManagedClientTask {
let candidate = self
.last_stagger_send
.zip(self.active_stagger_spacing())
.and_then(|(last, (_, spacing))| last.checked_add(spacing))
.and_then(|(last, spacing)| last.checked_add(spacing))
.filter(|gate| *gate > now);
self.send_gate = candidate.map(|candidate| existing.min(candidate));
}
@@ -1708,21 +1654,17 @@ impl ManagedClientTask {
fn record_stagger_acceptance(
&mut self,
result: SendResult,
stagger_spacing: Option<(usize, Duration)>,
stagger_spacing: Option<Duration>,
accepted_at: Instant,
) {
if !result.accepted() {
return;
}
let Some((_active, spacing)) = stagger_spacing else {
let Some(spacing) = stagger_spacing else {
return;
};
self.last_stagger_send = Some(accepted_at);
self.send_gate = self.next_stagger_gate(spacing, accepted_at);
#[cfg(test)]
if let Some(observations) = &self.stagger_observations {
observations.lock().unwrap().push((_active, spacing));
}
}
fn poll_one_send(
@@ -1730,7 +1672,7 @@ impl ManagedClientTask {
index: usize,
cx: &mut Context<'_>,
now: Instant,
stagger_spacing: Option<(usize, Duration)>,
stagger_spacing: Option<Duration>,
) -> SendResult {
if !self.targets[index].desired || self.targets[index].retirement.is_some() {
self.targets[index].send_waiting = false;
@@ -1832,7 +1774,7 @@ impl ManagedClientTask {
} else {
None
};
self.targets[index].record_packets_sent(after);
self.targets[index].counters.packets_sent = after;
match result {
Poll::Pending => {
self.targets[index].send_waiting = true;
@@ -2109,11 +2051,7 @@ impl ManagedClientTask {
.all(|target| matches!(target.state, TargetState::Terminal))
}
fn next_deadline(&mut self) -> Option<Instant> {
#[cfg(test)]
{
self.deadline_inspections += self.targets.len();
}
fn next_deadline(&self) -> Option<Instant> {
let mut non_send_deadline = None;
let mut send_deadline = None;
for target in &self.targets {
@@ -2541,13 +2479,9 @@ fn build_task(
#[cfg(test)]
event_observations: None,
#[cfg(test)]
stagger_observations: None,
#[cfg(test)]
drain_test_hook: DrainTestHook::default(),
#[cfg(test)]
timeout_inspections: 0,
#[cfg(test)]
deadline_inspections: 0,
};
let handle = ManagedClientHandle {
stop,
+5 -153
View File
@@ -16,7 +16,7 @@ use irtt_proto::{
use tokio::runtime::{Builder, Runtime};
use super::*;
use crate::{socket::resolution_call_counts, ClientAuthConfig, NegotiationPolicy, RunMode};
use crate::{ClientAuthConfig, NegotiationPolicy, RunMode};
const TOKEN: u64 = 0x1234_5678_90ab_cdef;
@@ -388,44 +388,6 @@ fn assert_rotated_without_catchup(sends: &[(TargetInstance, u32, Instant, Instan
}
}
fn run_negotiated_stagger_case(
requested: Duration,
negotiated: &[Duration],
) -> (Vec<(usize, Duration)>, Vec<u64>) {
let servers = negotiated
.iter()
.map(|interval| start_server_negotiating(ServerBehavior::Echo, None, Some(*interval)))
.collect::<Vec<_>>();
let mut managed = config(ManagedPacing::Staggered);
managed.client.duration = Some(Duration::from_millis(190));
managed.client.interval = requested;
managed.client.negotiation_policy = NegotiationPolicy::Loose;
let targets = servers
.iter()
.enumerate()
.map(|(index, server)| target(&format!("target-{index}"), server.addr))
.collect();
let (mut task, _) = ManagedClient::task(managed, targets).unwrap();
let observations = Arc::new(Mutex::new(Vec::new()));
task.stagger_observations = Some(Arc::clone(&observations));
let outcome = runtime().block_on(task);
let packets_sent = (0..servers.len())
.map(|index| {
outcome
.recent_target_outcomes
.iter()
.find(|target| target.target.id.as_ref() == format!("target-{index}"))
.unwrap()
.packets_sent
})
.collect();
for server in servers {
server.finish();
}
let spacings = observations.lock().unwrap().clone();
(spacings, packets_sent)
}
fn run_deferred_drain_burst(packet_count: usize) -> ManagedOutcome {
let server = start_server(ServerBehavior::DeferredBurst(packet_count), None);
let mut managed = config(ManagedPacing::Staggered);
@@ -459,13 +421,11 @@ fn run_deferred_drain_burst(packet_count: usize) -> ManagedOutcome {
#[test]
fn construction_is_runtime_and_io_free() {
let before = resolution_call_counts();
let (task, handle) = ManagedClient::task(
config(ManagedPacing::Staggered),
vec![ManagedTargetConfig::new("dns", "no-such-host.invalid")],
)
.unwrap();
assert_eq!(resolution_call_counts(), before);
assert_eq!(handle.status().lifecycle, ManagedLifecycle::NotStarted);
drop(task);
}
@@ -1400,7 +1360,6 @@ fn first_poll_without_runtime_fails_durably() {
#[test]
fn pre_poll_stop() {
let before = resolution_call_counts();
let (task, handle) = ManagedClient::task(
config(ManagedPacing::Staggered),
vec![ManagedTargetConfig::new("one", "no-such-host.invalid")],
@@ -1413,7 +1372,6 @@ fn pre_poll_stop() {
};
assert!(poll_once(receipt.as_mut()).is_ready());
assert_eq!(outcome.end_reason, ManagedEndReason::StopRequested);
assert_eq!(resolution_call_counts(), before);
}
#[test]
@@ -1604,45 +1562,6 @@ fn staggered_pacing_fairness() {
second.finish();
}
#[test]
fn staggered_pacing_uses_all_active_negotiated_intervals() {
let requested = Duration::from_millis(120);
let (single, single_packets) =
run_negotiated_stagger_case(requested, &[Duration::from_millis(40)]);
assert!(single
.iter()
.all(|entry| *entry == (1, Duration::from_millis(40))));
assert!(single_packets[0] >= 4);
let (different, different_packets) = run_negotiated_stagger_case(
requested,
&[Duration::from_millis(30), Duration::from_millis(90)],
);
let while_both_active = different
.iter()
.filter(|(active, _)| *active == 2)
.collect::<Vec<_>>();
assert!(!while_both_active.is_empty());
assert!(while_both_active
.iter()
.all(|(_, spacing)| *spacing == Duration::from_millis(15)));
assert!(different_packets[0] > different_packets[1]);
let (equal, _) = run_negotiated_stagger_case(
requested,
&[Duration::from_millis(60), Duration::from_millis(60)],
);
let while_both_active = equal
.iter()
.filter(|(active, _)| *active == 2)
.collect::<Vec<_>>();
assert!(!while_both_active.is_empty());
assert!(while_both_active
.iter()
.all(|(_, spacing)| *spacing == Duration::from_millis(30)));
}
#[test]
fn stagger_gate_tracks_active_membership() {
let first =
@@ -1690,7 +1609,7 @@ fn stagger_gate_tracks_active_membership() {
let accepted_at = Instant::now();
task.record_stagger_acceptance(
SendResult::Failed { accepted: true },
Some((2, Duration::from_millis(50))),
Some(Duration::from_millis(50)),
accepted_at,
);
assert_eq!(
@@ -1752,11 +1671,7 @@ fn the_stagger_gate_advances_by_one_spacing_per_send_without_accumulating_latene
let start = Instant::now();
let lateness = Duration::from_millis(3);
task.record_stagger_acceptance(
SendResult::Ready { accepted: true },
Some((1, spacing)),
start,
);
task.record_stagger_acceptance(SendResult::Ready { accepted: true }, Some(spacing), start);
assert_eq!(task.send_gate, Some(start + spacing), "first send anchors");
// Every later send is observed `lateness` after the gate that released it.
@@ -1766,7 +1681,7 @@ fn the_stagger_gate_advances_by_one_spacing_per_send_without_accumulating_latene
let released_at = start + spacing * slot;
task.record_stagger_acceptance(
SendResult::Ready { accepted: true },
Some((1, spacing)),
Some(spacing),
released_at + lateness,
);
assert_eq!(
@@ -1782,7 +1697,7 @@ fn the_stagger_gate_advances_by_one_spacing_per_send_without_accumulating_latene
let resumed_at = stalled_gate + spacing;
task.record_stagger_acceptance(
SendResult::Ready { accepted: true },
Some((1, spacing)),
Some(spacing),
resumed_at,
);
assert_eq!(task.send_gate, Some(resumed_at + spacing));
@@ -1926,69 +1841,6 @@ fn pending_limit_failure_drains_reply_and_closes_session() {
assert!(has_close(&records));
}
#[test]
fn drain_failures_preserve_primary_outcome_and_first_cleanup_failure() {
let completed_server = start_server(ServerBehavior::Echo, None);
let (mut completed_task, _) = ManagedClient::task(
config(ManagedPacing::Staggered),
vec![target("completed", completed_server.addr)],
)
.unwrap();
completed_task.drain_test_hook.fail_receive = true;
completed_task.drain_test_hook.fail_close = true;
let completed = runtime().block_on(completed_task);
let completed_target = &completed.recent_target_outcomes[0];
assert!(matches!(
completed_target.end_reason,
ManagedTargetEndReason::TestComplete
));
assert_eq!(completed.failed_target_outcomes, 0);
assert!(matches!(
completed_target.cleanup_failure,
Some(ManagedTargetFailure {
phase: ManagedTargetFailurePhase::Receiving,
kind: ManagedTargetFailureKind::Socket,
..
})
));
completed_server.finish();
let stopped_server = start_server(ServerBehavior::Echo, None);
let mut managed = config(ManagedPacing::Staggered);
managed.completion = ManagedCompletionPolicy::ExplicitStop;
managed.client.duration = None;
let (mut stopped_task, handle) =
ManagedClient::task(managed, vec![target("stopped", stopped_server.addr)]).unwrap();
stopped_task.drain_test_hook.fail_receive = true;
let seen = Arc::clone(&stopped_server.probe_seen);
let stopper = thread::spawn(move || {
let (flag, ready) = &*seen;
let guard = flag.lock().unwrap();
let (guard, timeout) = ready
.wait_timeout_while(guard, Duration::from_secs(2), |seen| !*seen)
.unwrap();
assert!(*guard && !timeout.timed_out());
drop(handle.stop());
});
let stopped = runtime().block_on(stopped_task);
stopper.join().unwrap();
let stopped_target = &stopped.recent_target_outcomes[0];
assert!(matches!(
stopped_target.end_reason,
ManagedTargetEndReason::Stopped
));
assert_eq!(stopped.failed_target_outcomes, 0);
assert!(matches!(
stopped_target.cleanup_failure,
Some(ManagedTargetFailure {
phase: ManagedTargetFailurePhase::Receiving,
kind: ManagedTargetFailureKind::Socket,
..
})
));
stopped_server.finish();
}
#[test]
fn final_drain_overflow_is_rejected_and_preserves_success() {
let mut invalid = config(ManagedPacing::Staggered);