diff --git a/crates/irtt-client/tests/hmac.rs b/crates/irtt-client/tests/hmac.rs index 6ef315c..d9c13a5 100644 --- a/crates/irtt-client/tests/hmac.rs +++ b/crates/irtt-client/tests/hmac.rs @@ -9,10 +9,9 @@ use irtt_client::{ use irtt_proto::{echo_packet_len, ProtoError, TimestampFields}; use support::{ - config_for_params, default_params, params_for_modes, require_real_backend, - run_one_probe_with_config, standard_timestamps, start_bad_hmac_echo_reply_server, - start_hmac_close_server, start_hmac_required_open_drop_server, RealIrtServer, - ServerObservation, TOKEN, + config_for_params, default_params, params_for_modes, run_one_probe_with_config, + standard_timestamps, start_bad_hmac_echo_reply_server, start_hmac_close_server, + start_hmac_required_open_drop_server, BackendPeer, ServerObservation, TOKEN, }; #[test] @@ -297,14 +296,11 @@ fn assert_no_duplicate_late_or_warning(events: &[ClientEvent]) { } #[test] -fn real_hmac_correct_key_succeeds() { - if !require_real_backend() { - return; - } +fn backend_hmac_correct_key_succeeds() { let key = b"compat-secret".to_vec(); - let server = RealIrtServer::start(Some(&key)).unwrap(); let params = default_params(); - let mut config = config_for_params(server.addr(), ¶ms); + let peer = BackendPeer::start_open_echo(params.clone(), Some(key.clone())); + let mut config = config_for_params(peer.addr(), ¶ms); config.hmac_key = Some(key); let mut client = Client::connect(config).unwrap(); @@ -322,13 +318,10 @@ fn real_hmac_correct_key_succeeds() { } #[test] -fn real_hmac_wrong_key_fails() { - if !require_real_backend() { - return; - } +fn backend_hmac_wrong_key_fails() { let server_key = b"compat-secret".to_vec(); - let server = RealIrtServer::start(Some(&server_key)).unwrap(); - let mut config = config_for_params(server.addr(), &default_params()); + let peer = BackendPeer::start_hmac_required(server_key); + let mut config = config_for_params(peer.addr(), &default_params()); config.open_timeouts = vec![Duration::from_millis(200)]; config.hmac_key = Some(b"wrong-secret".to_vec()); @@ -340,13 +333,10 @@ fn real_hmac_wrong_key_fails() { } #[test] -fn real_hmac_missing_key_fails() { - if !require_real_backend() { - return; - } +fn backend_hmac_missing_key_fails() { let server_key = b"compat-secret".to_vec(); - let server = RealIrtServer::start(Some(&server_key)).unwrap(); - let mut config = config_for_params(server.addr(), &default_params()); + let peer = BackendPeer::start_hmac_required(server_key); + let mut config = config_for_params(peer.addr(), &default_params()); config.open_timeouts = vec![Duration::from_millis(200)]; config.hmac_key = None; diff --git a/crates/irtt-client/tests/protocol_options.rs b/crates/irtt-client/tests/protocol_options.rs index 2a0cbc7..ce635b4 100644 --- a/crates/irtt-client/tests/protocol_options.rs +++ b/crates/irtt-client/tests/protocol_options.rs @@ -10,9 +10,9 @@ use irtt_client::{ use irtt_proto::{echo_packet_len, Clock, Params, ReceivedStats, StampAt, TimestampFields}; use support::{ - config_for_params, default_params, params_for_modes, require_real_backend, run_one_probe, - run_one_probe_with_config, server_fill, standard_timestamps, start_open_server, OneProbeRun, - RealIrtServer, ServerObservation, RECV_COUNT, RECV_WINDOW, TOKEN, + config_for_params, default_params, params_for_modes, run_one_probe, run_one_probe_with_config, + server_fill, standard_timestamps, start_open_server, BackendPeer, OneProbeRun, + ServerObservation, RECV_COUNT, RECV_WINDOW, TOKEN, }; struct ReplyView<'a> { @@ -374,13 +374,10 @@ fn assert_one_way_presence( } #[test] -fn real_basic_open_echo_close() { - if !require_real_backend() { - return; - } - let server = RealIrtServer::start(None).unwrap(); +fn backend_basic_open_echo_close() { let params = default_params(); - let mut client = Client::connect(config_for_params(server.addr(), ¶ms)).unwrap(); + let peer = BackendPeer::start_open_echo(params.clone(), None); + let mut client = Client::connect(config_for_params(peer.addr(), ¶ms)).unwrap(); let outcome = client.open(ClientTimestamp::now()).unwrap(); assert!(matches!(outcome, irtt_client::OpenOutcome::Started { .. })); @@ -396,19 +393,16 @@ fn real_basic_open_echo_close() { } #[test] -fn real_received_stats_smoke() { - if !require_real_backend() { - return; - } +fn backend_received_stats_smoke() { for mode in [ ReceivedStats::None, ReceivedStats::Count, ReceivedStats::Window, ReceivedStats::Both, ] { - let server = RealIrtServer::start(None).unwrap(); let params = params_for_modes(mode, StampAt::None, Clock::Both); - let mut client = Client::connect(config_for_params(server.addr(), ¶ms)).unwrap(); + let peer = BackendPeer::start_open_echo(params.clone(), None); + let mut client = Client::connect(config_for_params(peer.addr(), ¶ms)).unwrap(); client.open(ClientTimestamp::now()).unwrap(); client.send_probe().unwrap(); @@ -421,10 +415,7 @@ fn real_received_stats_smoke() { } #[test] -fn real_timestamp_smoke() { - if !require_real_backend() { - return; - } +fn backend_timestamp_smoke() { for mode in [ StampAt::None, StampAt::Send, @@ -432,9 +423,9 @@ fn real_timestamp_smoke() { StampAt::Both, StampAt::Midpoint, ] { - let server = RealIrtServer::start(None).unwrap(); let params = params_for_modes(ReceivedStats::None, mode, Clock::Both); - let mut client = Client::connect(config_for_params(server.addr(), ¶ms)).unwrap(); + let peer = BackendPeer::start_open_echo(params.clone(), None); + let mut client = Client::connect(config_for_params(peer.addr(), ¶ms)).unwrap(); client.open(ClientTimestamp::now()).unwrap(); client.send_probe().unwrap(); @@ -477,14 +468,11 @@ fn real_timestamp_smoke() { } #[test] -fn real_clock_smoke() { - if !require_real_backend() { - return; - } +fn backend_clock_smoke() { for clock in [Clock::Wall, Clock::Monotonic, Clock::Both] { - let server = RealIrtServer::start(None).unwrap(); let params = params_for_modes(ReceivedStats::None, StampAt::Both, clock); - let mut client = Client::connect(config_for_params(server.addr(), ¶ms)).unwrap(); + let peer = BackendPeer::start_open_echo(params.clone(), None); + let mut client = Client::connect(config_for_params(peer.addr(), ¶ms)).unwrap(); client.open(ClientTimestamp::now()).unwrap(); client.send_probe().unwrap(); diff --git a/crates/irtt-client/tests/support/mod.rs b/crates/irtt-client/tests/support/mod.rs index ee13a81..9e1ce0c 100644 --- a/crates/irtt-client/tests/support/mod.rs +++ b/crates/irtt-client/tests/support/mod.rs @@ -25,20 +25,62 @@ pub const TOKEN: u64 = 0x1234_5678_90ab_cdef; pub const RECV_COUNT: u32 = 42; pub const RECV_WINDOW: u64 = 0x07; -pub fn require_real_backend() -> bool { +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TestBackendKind { + Fake, + Real, +} + +pub fn selected_backend() -> TestBackendKind { match std::env::var("IRTT_TEST_BACKEND").as_deref() { Ok("real") => { let irtt_bin = std::env::var("IRTT_BIN").unwrap_or_else(|_| "irtt".to_string()); match Command::new(&irtt_bin).arg("version").output() { - Ok(output) if output.status.success() => true, + Ok(output) if output.status.success() => TestBackendKind::Real, _ => panic!("IRTT_TEST_BACKEND=real but irtt binary not found at '{irtt_bin}'"), } } - Ok("fake") | Err(_) => false, + Ok("fake") | Err(_) => TestBackendKind::Fake, Ok(other) => panic!("unknown IRTT_TEST_BACKEND value: {other}"), } } +pub enum BackendPeer { + Fake(FakeServer), + Real(RealIrtServer), +} + +impl BackendPeer { + pub fn addr(&self) -> SocketAddr { + match self { + BackendPeer::Fake(s) => s.addr, + BackendPeer::Real(s) => s.addr(), + } + } + + pub fn start_open_echo(params: Params, hmac_key: Option>) -> Self { + match selected_backend() { + TestBackendKind::Fake => { + let timestamps = standard_timestamps(); + BackendPeer::Fake(start_one_probe_server(params, timestamps, hmac_key)) + } + TestBackendKind::Real => { + BackendPeer::Real(RealIrtServer::start(hmac_key.as_deref()).unwrap()) + } + } + } + + pub fn start_hmac_required(key: Vec) -> Self { + match selected_backend() { + TestBackendKind::Fake => BackendPeer::Fake(start_hmac_required_open_drop_server( + key, + Duration::from_millis(250), + )), + TestBackendKind::Real => BackendPeer::Real(RealIrtServer::start(Some(&key)).unwrap()), + } + } +} + pub struct FakeServer { pub addr: SocketAddr, rx: mpsc::Receiver, diff --git a/crates/irtt-client/tests/support/real_irtt.rs b/crates/irtt-client/tests/support/real_irtt.rs index 699118d..0c09313 100644 --- a/crates/irtt-client/tests/support/real_irtt.rs +++ b/crates/irtt-client/tests/support/real_irtt.rs @@ -10,6 +10,7 @@ use std::time::{Duration, Instant}; pub struct RealIrtServer { addr: SocketAddr, child: Child, + keepalive_ms: Option, } impl RealIrtServer { @@ -20,6 +21,12 @@ impl RealIrtServer { let irtt_bin = std::env::var("IRTT_BIN").unwrap_or_else(|_| "irtt".to_string()); + let keepalive_ms = std::env::var("IRTT_TEST_KEEP_SERVER_MS") + .ok() + .and_then(|v| v.parse::().ok()); + + debug_eprintln!("[real_irtt] backend=real irtt_bin={irtt_bin} bind={bind}"); + let mut cmd = Command::new(&irtt_bin); cmd.arg("server") .arg("-b") @@ -33,18 +40,42 @@ impl RealIrtServer { cmd.arg(format!("--hmac=0x{}", hex_key)); } + let full_cmd = format!("{cmd:?}"); + debug_eprintln!("[real_irtt] spawning: {full_cmd}"); + let mut child = cmd .spawn() .map_err(|e| format!("failed to spawn irtt server: {}", e))?; + let pid = child.id(); + debug_eprintln!("[real_irtt] child pid={pid}"); + let stdout = child.stdout.take().ok_or("failed to capture stdout")?; - let (tx, rx) = mpsc::channel(); + let stderr = child.stderr.take().ok_or("failed to capture stderr")?; + + let (out_tx, out_rx) = mpsc::channel(); + let (err_tx, err_rx) = mpsc::channel(); + thread::spawn(move || { let reader = BufReader::new(stdout); for line in reader.lines() { match line { Ok(l) => { - if tx.send(l).is_err() { + if out_tx.send(("stdout", l)).is_err() { + break; + } + } + Err(_) => break, + } + } + }); + + thread::spawn(move || { + let reader = BufReader::new(stderr); + for line in reader.lines() { + match line { + Ok(l) => { + if err_tx.send(("stderr", l)).is_err() { break; } } @@ -55,40 +86,59 @@ impl RealIrtServer { let timeout = Duration::from_secs(5); let start = Instant::now(); - let mut stdout_lines = Vec::new(); - loop { + let mut captured_lines = Vec::new(); + + let ready = loop { let remaining = timeout .checked_sub(start.elapsed()) .unwrap_or(Duration::ZERO); - match rx.recv_timeout(remaining) { - Ok(line) => { - stdout_lines.push(line.clone()); - if line.contains("[ListenerStart]") { - break; + + let line = select_line(&out_rx, &err_rx, remaining); + match line { + Some((stream, text)) => { + debug_eprintln!("[real_irtt] {stream}: {text}"); + captured_lines.push(format!("{stream}: {text}")); + if text.contains("[ListenerStart]") { + break true; + } + if (text.contains("[ListenerStop]") || text.contains("[ServerStop]")) + && (text.contains("error") || text.contains("Error")) + { + break false; } } - Err(mpsc::RecvTimeoutError::Timeout) => { - child.kill().ok(); - child.wait().ok(); - return Err(format!( - "timeout waiting for irtt server to start\nstdout:\n{}", - stdout_lines.join("\n") - )); - } - Err(mpsc::RecvTimeoutError::Disconnected) => { - child.kill().ok(); - child.wait().ok(); - return Err(format!( - "irtt server exited unexpectedly\nstdout:\n{}", - stdout_lines.join("\n") - )); + None => { + break false; } } + }; + + if !ready { + let exit_status = child.try_wait().ok().flatten(); + debug_eprintln!("[real_irtt] startup failed, exit_status={exit_status:?}"); + child.kill().ok(); + child.wait().ok(); + + let output = captured_lines.join("\n"); + let extra = match exit_status { + Some(status) => format!("\nexit status: {status}"), + None => String::new(), + }; + return Err(format!( + "irtt server failed to start\noutput:\n{output}{extra}" + )); } - drop(rx); + debug_eprintln!("[real_irtt] ready on {addr}"); - Ok(Self { addr, child }) + drop(out_rx); + drop(err_rx); + + Ok(Self { + addr, + child, + keepalive_ms, + }) } pub fn addr(&self) -> SocketAddr { @@ -109,7 +159,47 @@ impl RealIrtServer { impl Drop for RealIrtServer { fn drop(&mut self) { + if let Some(ms) = self.keepalive_ms { + debug_eprintln!( + "[real_irtt] keepalive: sleeping {ms}ms before kill (pid={})", + self.child.id() + ); + thread::sleep(Duration::from_millis(ms)); + } + debug_eprintln!("[real_irtt] killing child pid={}", self.child.id()); self.child.kill().ok(); self.child.wait().ok(); } } + +fn select_line( + out_rx: &mpsc::Receiver<(&str, String)>, + err_rx: &mpsc::Receiver<(&str, String)>, + timeout: Duration, +) -> Option<(&'static str, String)> { + if let Ok((_, line)) = out_rx.recv_timeout(timeout) { + return Some(("stdout", line)); + } + if let Ok((_, line)) = err_rx.recv_timeout(Duration::ZERO) { + return Some(("stderr", line)); + } + if timeout > Duration::ZERO { + if let Ok((_, line)) = out_rx.recv_timeout(Duration::from_millis(100)) { + return Some(("stdout", line)); + } + if let Ok((_, line)) = err_rx.recv_timeout(Duration::from_millis(100)) { + return Some(("stderr", line)); + } + } + None +} + +macro_rules! debug_eprintln { + ($($arg:tt)*) => { + if std::env::var("IRTT_TEST_BACKEND_DEBUG").as_deref() == Ok("1") { + eprintln!($($arg)*); + } + }; +} + +use debug_eprintln;