From 56fe86f543f608167557183ec24cc604e17302ae Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Sat, 3 Oct 2026 13:22:33 +0700 Subject: [PATCH] test: retain durable negotiation and lifecycle review coverage --- crates/irtt-app/tests/cli.rs | 57 +++++++++++++++++++ crates/irtt-client/tests/client/open.rs | 21 +++++++ .../tests/managed_behavior.rs | 31 ++++++++++ crates/irtt-server/tests/session_behavior.rs | 3 + 4 files changed, 112 insertions(+) diff --git a/crates/irtt-app/tests/cli.rs b/crates/irtt-app/tests/cli.rs index 1c46d5a..cdebaa6 100644 --- a/crates/irtt-app/tests/cli.rs +++ b/crates/irtt-app/tests/cli.rs @@ -87,6 +87,63 @@ fn dispatcher_reports_and_runs_the_enabled_applets() { } } +// The default presentation must reach a meaningful packet summary for a finite run. +#[cfg(feature = "client")] +#[test] +fn default_client_output_emits_a_packet_summary() { + use std::{io::Read, process::Stdio, thread}; + let server = in_tree_server::InTreeServer::start(ServerConfig::default()); + let mut client = ClientProcess( + Command::new(env!("CARGO_BIN_EXE_irtt-client")) + .env_clear() + .args([ + "--duration", + "100ms", + "--interval", + "20ms", + &server.addr.to_string(), + ]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(), + ); + let mut stdout = client.0.stdout.take().unwrap(); + let stdout = thread::spawn(move || { + let mut text = String::new(); + stdout.read_to_string(&mut text).unwrap(); + text + }); + let mut stderr = client.0.stderr.take().unwrap(); + let stderr = thread::spawn(move || { + let mut text = String::new(); + stderr.read_to_string(&mut text).unwrap(); + text + }); + let status = client.wait(); + let stdout = stdout.join().unwrap(); + let stderr = stderr.join().unwrap(); + assert!(status.success(), "{stderr}"); + assert!(stdout.contains("irtt-rs summary"), "{stdout}"); + let packets = stdout + .lines() + .find(|line| line.starts_with("packets:")) + .expect("default output must include the packet summary"); + for field in ["sent=", "received="] { + let count = packets + .split_whitespace() + .find_map(|word| word.strip_prefix(field)) + .unwrap() + .parse::() + .unwrap(); + assert!( + count > 0, + "summary must report {field} a positive count: {packets}" + ); + } +} + // A real authenticated server detects broken client argument-to-config mapping. #[cfg(feature = "client")] #[test] diff --git a/crates/irtt-client/tests/client/open.rs b/crates/irtt-client/tests/client/open.rs index dbd97b2..d6f47cd 100644 --- a/crates/irtt-client/tests/client/open.rs +++ b/crates/irtt-client/tests/client/open.rs @@ -1,5 +1,26 @@ use super::*; +// Untrusted returned intervals must be rejected before managed scheduling, +// even when loose negotiation permits legitimate server restrictions. +#[test] +fn loose_open_rejects_nonpositive_returned_intervals() { + for interval_ns in [0, -1] { + let mut returned = default_params(); + returned.interval_ns = interval_ns; + let server = open_success_server(returned); + let config = ClientConfig { + negotiation_policy: NegotiationPolicy::Loose, + ..default_test_config(server.addr) + }; + let mut client = Client::connect(config).unwrap(); + assert!( + matches!(client.open(), Err(ClientError::NegotiationRejected { .. })), + "returned interval {interval_ns} must be rejected" + ); + server.join(); + } +} + #[test] fn open_fails_when_already_open() { let params = default_params(); diff --git a/crates/irtt-integration-tests/tests/managed_behavior.rs b/crates/irtt-integration-tests/tests/managed_behavior.rs index 1144aa6..10bd4fa 100644 --- a/crates/irtt-integration-tests/tests/managed_behavior.rs +++ b/crates/irtt-integration-tests/tests/managed_behavior.rs @@ -162,9 +162,18 @@ async fn target_updates_preserve_identical_sessions_and_retire_removed_generatio handle.update_targets(vec![]).unwrap().await.unwrap(); loop { if let ManagedEvent::TargetFinished { outcome } = events.recv().await.unwrap() { + let status = handle.status(); assert_eq!(outcome.target, first); assert_eq!(outcome.end_reason, ManagedTargetEndReason::Removed); assert_eq!(outcome.cleanup_failure, None); + assert_eq!(status.total_target_outcomes, 1); + assert_eq!(status.successful_target_outcomes, 1); + assert_eq!( + status.recent_target_outcomes.as_ref(), + &[(*outcome).clone()] + ); + assert!(status.targets.iter().all(|target| target.target != first + || target.lifecycle == ManagedTargetLifecycle::Terminal)); break; } } @@ -191,6 +200,28 @@ async fn target_updates_preserve_identical_sessions_and_retire_removed_generatio } } let stop = handle.stop(); + loop { + match events.recv().await.unwrap() { + ManagedEvent::TargetFinished { outcome } => { + let status = handle.status(); + assert_eq!(outcome.target, second); + assert_eq!(status.total_target_outcomes, 2); + assert_eq!( + status.recent_target_outcomes.as_ref(), + &[(*outcome).clone()] + ); + assert!(status.targets.iter().all(|target| target.target != second + || target.lifecycle == ManagedTargetLifecycle::Terminal)); + } + ManagedEvent::Completed { outcome } => { + let status = handle.status(); + assert_eq!(status.lifecycle, ManagedLifecycle::Completed); + assert_eq!(status.final_outcome.as_ref(), Some(&outcome)); + break; + } + _ => {} + } + } let outcome = run.await.unwrap(); stop.await; assert_eq!(outcome.end_reason, ManagedEndReason::StopRequested); diff --git a/crates/irtt-server/tests/session_behavior.rs b/crates/irtt-server/tests/session_behavior.rs index db63b22..c647b94 100644 --- a/crates/irtt-server/tests/session_behavior.rs +++ b/crates/irtt-server/tests/session_behavior.rs @@ -31,6 +31,9 @@ fn authenticated_sessions_are_bounded_and_owned_by_the_opening_endpoint() { Some(key), ) .unwrap(); + let mut unkeyed = ServerCore::new(ServerConfig::default()); + assert!(unkeyed.handle_datagram(peer, &open).unwrap().is_none()); + assert_eq!(unkeyed.session_count(), 0); let unauthenticated = encode_request( RequestToEncode::Open { params: ¶ms,