test(client): unify fake and real backend testing via BackendPeer

Introduce a BackendPeer abstraction to transparently switch between fake and real IRTT backends. This simplifies test logic by removing manual skip checks and providing a consistent interface for server lifecycle management.
This commit is contained in:
Nils Andreas Svee committed 2026-05-10 20:56:43 +07:00
1 parent 3d49f725ae
commit a4e2f3aef4
4 files changed
+186 -76

No files matched your search

+12 -22
View File
@@ -9,10 +9,9 @@ use irtt_client::{
use irtt_proto::{echo_packet_len, ProtoError, TimestampFields}; use irtt_proto::{echo_packet_len, ProtoError, TimestampFields};
use support::{ use support::{
config_for_params, default_params, params_for_modes, require_real_backend, config_for_params, default_params, params_for_modes, run_one_probe_with_config,
run_one_probe_with_config, standard_timestamps, start_bad_hmac_echo_reply_server, standard_timestamps, start_bad_hmac_echo_reply_server, start_hmac_close_server,
start_hmac_close_server, start_hmac_required_open_drop_server, RealIrtServer, start_hmac_required_open_drop_server, BackendPeer, ServerObservation, TOKEN,
ServerObservation, TOKEN,
}; };
#[test] #[test]
@@ -297,14 +296,11 @@ fn assert_no_duplicate_late_or_warning(events: &[ClientEvent]) {
} }
#[test] #[test]
fn real_hmac_correct_key_succeeds() { fn backend_hmac_correct_key_succeeds() {
if !require_real_backend() {
return;
}
let key = b"compat-secret".to_vec(); let key = b"compat-secret".to_vec();
let server = RealIrtServer::start(Some(&key)).unwrap();
let params = default_params(); let params = default_params();
let mut config = config_for_params(server.addr(), &params); let peer = BackendPeer::start_open_echo(params.clone(), Some(key.clone()));
let mut config = config_for_params(peer.addr(), &params);
config.hmac_key = Some(key); config.hmac_key = Some(key);
let mut client = Client::connect(config).unwrap(); let mut client = Client::connect(config).unwrap();
@@ -322,13 +318,10 @@ fn real_hmac_correct_key_succeeds() {
} }
#[test] #[test]
fn real_hmac_wrong_key_fails() { fn backend_hmac_wrong_key_fails() {
if !require_real_backend() {
return;
}
let server_key = b"compat-secret".to_vec(); let server_key = b"compat-secret".to_vec();
let server = RealIrtServer::start(Some(&server_key)).unwrap(); let peer = BackendPeer::start_hmac_required(server_key);
let mut config = config_for_params(server.addr(), &default_params()); let mut config = config_for_params(peer.addr(), &default_params());
config.open_timeouts = vec![Duration::from_millis(200)]; config.open_timeouts = vec![Duration::from_millis(200)];
config.hmac_key = Some(b"wrong-secret".to_vec()); config.hmac_key = Some(b"wrong-secret".to_vec());
@@ -340,13 +333,10 @@ fn real_hmac_wrong_key_fails() {
} }
#[test] #[test]
fn real_hmac_missing_key_fails() { fn backend_hmac_missing_key_fails() {
if !require_real_backend() {
return;
}
let server_key = b"compat-secret".to_vec(); let server_key = b"compat-secret".to_vec();
let server = RealIrtServer::start(Some(&server_key)).unwrap(); let peer = BackendPeer::start_hmac_required(server_key);
let mut config = config_for_params(server.addr(), &default_params()); let mut config = config_for_params(peer.addr(), &default_params());
config.open_timeouts = vec![Duration::from_millis(200)]; config.open_timeouts = vec![Duration::from_millis(200)];
config.hmac_key = None; config.hmac_key = None;
+15 -27
View File
@@ -10,9 +10,9 @@ use irtt_client::{
use irtt_proto::{echo_packet_len, Clock, Params, ReceivedStats, StampAt, TimestampFields}; use irtt_proto::{echo_packet_len, Clock, Params, ReceivedStats, StampAt, TimestampFields};
use support::{ use support::{
config_for_params, default_params, params_for_modes, require_real_backend, run_one_probe, config_for_params, default_params, params_for_modes, run_one_probe, run_one_probe_with_config,
run_one_probe_with_config, server_fill, standard_timestamps, start_open_server, OneProbeRun, server_fill, standard_timestamps, start_open_server, BackendPeer, OneProbeRun,
RealIrtServer, ServerObservation, RECV_COUNT, RECV_WINDOW, TOKEN, ServerObservation, RECV_COUNT, RECV_WINDOW, TOKEN,
}; };
struct ReplyView<'a> { struct ReplyView<'a> {
@@ -374,13 +374,10 @@ fn assert_one_way_presence(
} }
#[test] #[test]
fn real_basic_open_echo_close() { fn backend_basic_open_echo_close() {
if !require_real_backend() {
return;
}
let server = RealIrtServer::start(None).unwrap();
let params = default_params(); let params = default_params();
let mut client = Client::connect(config_for_params(server.addr(), &params)).unwrap(); let peer = BackendPeer::start_open_echo(params.clone(), None);
let mut client = Client::connect(config_for_params(peer.addr(), &params)).unwrap();
let outcome = client.open(ClientTimestamp::now()).unwrap(); let outcome = client.open(ClientTimestamp::now()).unwrap();
assert!(matches!(outcome, irtt_client::OpenOutcome::Started { .. })); assert!(matches!(outcome, irtt_client::OpenOutcome::Started { .. }));
@@ -396,19 +393,16 @@ fn real_basic_open_echo_close() {
} }
#[test] #[test]
fn real_received_stats_smoke() { fn backend_received_stats_smoke() {
if !require_real_backend() {
return;
}
for mode in [ for mode in [
ReceivedStats::None, ReceivedStats::None,
ReceivedStats::Count, ReceivedStats::Count,
ReceivedStats::Window, ReceivedStats::Window,
ReceivedStats::Both, ReceivedStats::Both,
] { ] {
let server = RealIrtServer::start(None).unwrap();
let params = params_for_modes(mode, StampAt::None, Clock::Both); let params = params_for_modes(mode, StampAt::None, Clock::Both);
let mut client = Client::connect(config_for_params(server.addr(), &params)).unwrap(); let peer = BackendPeer::start_open_echo(params.clone(), None);
let mut client = Client::connect(config_for_params(peer.addr(), &params)).unwrap();
client.open(ClientTimestamp::now()).unwrap(); client.open(ClientTimestamp::now()).unwrap();
client.send_probe().unwrap(); client.send_probe().unwrap();
@@ -421,10 +415,7 @@ fn real_received_stats_smoke() {
} }
#[test] #[test]
fn real_timestamp_smoke() { fn backend_timestamp_smoke() {
if !require_real_backend() {
return;
}
for mode in [ for mode in [
StampAt::None, StampAt::None,
StampAt::Send, StampAt::Send,
@@ -432,9 +423,9 @@ fn real_timestamp_smoke() {
StampAt::Both, StampAt::Both,
StampAt::Midpoint, StampAt::Midpoint,
] { ] {
let server = RealIrtServer::start(None).unwrap();
let params = params_for_modes(ReceivedStats::None, mode, Clock::Both); let params = params_for_modes(ReceivedStats::None, mode, Clock::Both);
let mut client = Client::connect(config_for_params(server.addr(), &params)).unwrap(); let peer = BackendPeer::start_open_echo(params.clone(), None);
let mut client = Client::connect(config_for_params(peer.addr(), &params)).unwrap();
client.open(ClientTimestamp::now()).unwrap(); client.open(ClientTimestamp::now()).unwrap();
client.send_probe().unwrap(); client.send_probe().unwrap();
@@ -477,14 +468,11 @@ fn real_timestamp_smoke() {
} }
#[test] #[test]
fn real_clock_smoke() { fn backend_clock_smoke() {
if !require_real_backend() {
return;
}
for clock in [Clock::Wall, Clock::Monotonic, Clock::Both] { 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 params = params_for_modes(ReceivedStats::None, StampAt::Both, clock);
let mut client = Client::connect(config_for_params(server.addr(), &params)).unwrap(); let peer = BackendPeer::start_open_echo(params.clone(), None);
let mut client = Client::connect(config_for_params(peer.addr(), &params)).unwrap();
client.open(ClientTimestamp::now()).unwrap(); client.open(ClientTimestamp::now()).unwrap();
client.send_probe().unwrap(); client.send_probe().unwrap();
+45 -3
View File
@@ -25,20 +25,62 @@ pub const TOKEN: u64 = 0x1234_5678_90ab_cdef;
pub const RECV_COUNT: u32 = 42; pub const RECV_COUNT: u32 = 42;
pub const RECV_WINDOW: u64 = 0x07; 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() { match std::env::var("IRTT_TEST_BACKEND").as_deref() {
Ok("real") => { Ok("real") => {
let irtt_bin = std::env::var("IRTT_BIN").unwrap_or_else(|_| "irtt".to_string()); let irtt_bin = std::env::var("IRTT_BIN").unwrap_or_else(|_| "irtt".to_string());
match Command::new(&irtt_bin).arg("version").output() { 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}'"), _ => 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}"), 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<Vec<u8>>) -> 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<u8>) -> 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 struct FakeServer {
pub addr: SocketAddr, pub addr: SocketAddr,
rx: mpsc::Receiver<ServerObservation>, rx: mpsc::Receiver<ServerObservation>,
+114 -24
View File
@@ -10,6 +10,7 @@ use std::time::{Duration, Instant};
pub struct RealIrtServer { pub struct RealIrtServer {
addr: SocketAddr, addr: SocketAddr,
child: Child, child: Child,
keepalive_ms: Option<u64>,
} }
impl RealIrtServer { impl RealIrtServer {
@@ -20,6 +21,12 @@ impl RealIrtServer {
let irtt_bin = std::env::var("IRTT_BIN").unwrap_or_else(|_| "irtt".to_string()); 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::<u64>().ok());
debug_eprintln!("[real_irtt] backend=real irtt_bin={irtt_bin} bind={bind}");
let mut cmd = Command::new(&irtt_bin); let mut cmd = Command::new(&irtt_bin);
cmd.arg("server") cmd.arg("server")
.arg("-b") .arg("-b")
@@ -33,18 +40,42 @@ impl RealIrtServer {
cmd.arg(format!("--hmac=0x{}", hex_key)); cmd.arg(format!("--hmac=0x{}", hex_key));
} }
let full_cmd = format!("{cmd:?}");
debug_eprintln!("[real_irtt] spawning: {full_cmd}");
let mut child = cmd let mut child = cmd
.spawn() .spawn()
.map_err(|e| format!("failed to spawn irtt server: {}", e))?; .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 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 || { thread::spawn(move || {
let reader = BufReader::new(stdout); let reader = BufReader::new(stdout);
for line in reader.lines() { for line in reader.lines() {
match line { match line {
Ok(l) => { 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; break;
} }
} }
@@ -55,40 +86,59 @@ impl RealIrtServer {
let timeout = Duration::from_secs(5); let timeout = Duration::from_secs(5);
let start = Instant::now(); let start = Instant::now();
let mut stdout_lines = Vec::new(); let mut captured_lines = Vec::new();
loop {
let ready = loop {
let remaining = timeout let remaining = timeout
.checked_sub(start.elapsed()) .checked_sub(start.elapsed())
.unwrap_or(Duration::ZERO); .unwrap_or(Duration::ZERO);
match rx.recv_timeout(remaining) {
Ok(line) => { let line = select_line(&out_rx, &err_rx, remaining);
stdout_lines.push(line.clone()); match line {
if line.contains("[ListenerStart]") { Some((stream, text)) => {
break; 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) => { 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.kill().ok();
child.wait().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!( return Err(format!(
"timeout waiting for irtt server to start\nstdout:\n{}", "irtt server failed to start\noutput:\n{output}{extra}"
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")
));
}
}
}
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 { pub fn addr(&self) -> SocketAddr {
@@ -109,7 +159,47 @@ impl RealIrtServer {
impl Drop for RealIrtServer { impl Drop for RealIrtServer {
fn drop(&mut self) { 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.kill().ok();
self.child.wait().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;