test: refactor real backend support for reliable output capture
This commit is contained in:
1 parent
706e3bd1e1
commit
ed62a5c80c
2 files changed
+88
-91
No files matched your search
@@ -36,15 +36,27 @@ pub fn selected_backend() -> TestBackendKind {
|
|||||||
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() => TestBackendKind::Real,
|
Ok(output) if output.status.success() => {
|
||||||
|
debug_backend("[backend] selected backend=real");
|
||||||
|
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(_) => TestBackendKind::Fake,
|
Ok("fake") | Err(_) => {
|
||||||
|
debug_backend("[backend] selected backend=fake");
|
||||||
|
TestBackendKind::Fake
|
||||||
|
}
|
||||||
Ok(other) => panic!("unknown IRTT_TEST_BACKEND value: {other}"),
|
Ok(other) => panic!("unknown IRTT_TEST_BACKEND value: {other}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn debug_backend(message: &str) {
|
||||||
|
if std::env::var("IRTT_TEST_BACKEND_DEBUG").as_deref() == Ok("1") {
|
||||||
|
eprintln!("{message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub enum BackendPeer {
|
pub enum BackendPeer {
|
||||||
Fake(FakeServer),
|
Fake(FakeServer),
|
||||||
Real(RealIrtServer),
|
Real(RealIrtServer),
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader, Read};
|
||||||
use std::net::{SocketAddr, UdpSocket};
|
use std::net::{SocketAddr, UdpSocket};
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
use std::sync::mpsc;
|
use std::sync::{mpsc, Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread::{self, JoinHandle};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
pub struct RealIrtServer {
|
pub struct RealIrtServer {
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
child: Child,
|
child: Child,
|
||||||
keepalive_ms: Option<u64>,
|
keepalive_ms: Option<u64>,
|
||||||
|
stdout_thread: Option<JoinHandle<()>>,
|
||||||
|
stderr_thread: Option<JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RealIrtServer {
|
impl RealIrtServer {
|
||||||
pub fn start(hmac_key: Option<&[u8]>) -> Result<Self, String> {
|
pub fn start(hmac_key: Option<&[u8]>) -> Result<Self, String> {
|
||||||
let port = Self::find_free_port()?;
|
let port = Self::find_free_port()?;
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||||
let bind = format!("127.0.0.1:{}", port);
|
let bind = format!("127.0.0.1:{port}");
|
||||||
|
|
||||||
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")
|
let keepalive_ms = std::env::var("IRTT_TEST_KEEP_SERVER_MS")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|v| v.parse::<u64>().ok());
|
.and_then(|v| v.parse::<u64>().ok());
|
||||||
|
|
||||||
debug_eprintln!("[real_irtt] backend=real irtt_bin={irtt_bin} bind={bind}");
|
debug_eprintln!("[real_irtt] selected 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")
|
||||||
@@ -37,7 +38,7 @@ impl RealIrtServer {
|
|||||||
|
|
||||||
if let Some(key) = hmac_key {
|
if let Some(key) = hmac_key {
|
||||||
let hex_key: String = key.iter().map(|b| format!("{:02x}", b)).collect();
|
let hex_key: String = key.iter().map(|b| format!("{:02x}", b)).collect();
|
||||||
cmd.arg(format!("--hmac=0x{}", hex_key));
|
cmd.arg(format!("--hmac=0x{hex_key}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let full_cmd = format!("{cmd:?}");
|
let full_cmd = format!("{cmd:?}");
|
||||||
@@ -45,7 +46,7 @@ impl RealIrtServer {
|
|||||||
|
|
||||||
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();
|
let pid = child.id();
|
||||||
debug_eprintln!("[real_irtt] child pid={pid}");
|
debug_eprintln!("[real_irtt] child pid={pid}");
|
||||||
@@ -53,91 +54,49 @@ impl RealIrtServer {
|
|||||||
let stdout = child.stdout.take().ok_or("failed to capture stdout")?;
|
let stdout = child.stdout.take().ok_or("failed to capture stdout")?;
|
||||||
let stderr = child.stderr.take().ok_or("failed to capture stderr")?;
|
let stderr = child.stderr.take().ok_or("failed to capture stderr")?;
|
||||||
|
|
||||||
let (out_tx, out_rx) = mpsc::channel();
|
let captured = Arc::new(Mutex::new(Vec::new()));
|
||||||
let (err_tx, err_rx) = mpsc::channel();
|
let (line_tx, line_rx) = mpsc::channel();
|
||||||
|
|
||||||
thread::spawn(move || {
|
let stdout_thread = {
|
||||||
let reader = BufReader::new(stdout);
|
let line_tx = line_tx.clone();
|
||||||
for line in reader.lines() {
|
let captured = Arc::clone(&captured);
|
||||||
match line {
|
thread::spawn(move || drain_stream("stdout", stdout, line_tx, captured))
|
||||||
Ok(l) => {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let timeout = Duration::from_secs(5);
|
|
||||||
let start = Instant::now();
|
|
||||||
let mut captured_lines = Vec::new();
|
|
||||||
|
|
||||||
let ready = loop {
|
|
||||||
let remaining = timeout
|
|
||||||
.checked_sub(start.elapsed())
|
|
||||||
.unwrap_or(Duration::ZERO);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
break false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let stderr_thread = {
|
||||||
|
let captured = Arc::clone(&captured);
|
||||||
|
thread::spawn(move || drain_stream("stderr", stderr, line_tx, captured))
|
||||||
|
};
|
||||||
|
|
||||||
|
let ready = wait_for_ready(&line_rx, Duration::from_secs(5));
|
||||||
|
|
||||||
if !ready {
|
if !ready {
|
||||||
let exit_status = child.try_wait().ok().flatten();
|
let exit_status = child.try_wait().ok().flatten();
|
||||||
debug_eprintln!("[real_irtt] startup failed, exit_status={exit_status:?}");
|
debug_eprintln!("[real_irtt] readiness result=false exit_status={exit_status:?}");
|
||||||
child.kill().ok();
|
child.kill().ok();
|
||||||
child.wait().ok();
|
child.wait().ok();
|
||||||
|
stdout_thread.join().ok();
|
||||||
|
stderr_thread.join().ok();
|
||||||
|
|
||||||
let output = captured_lines.join("\n");
|
let output = captured.lock().unwrap().join("\n");
|
||||||
let extra = match exit_status {
|
let extra = match exit_status {
|
||||||
Some(status) => format!("\nexit status: {status}"),
|
Some(status) => format!("\nexit status: {status}"),
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
};
|
};
|
||||||
|
debug_eprintln!("[real_irtt] failure diagnostics:\n{output}{extra}");
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"irtt server failed to start\noutput:\n{output}{extra}"
|
"irtt server failed to start\noutput:\n{output}{extra}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
debug_eprintln!("[real_irtt] ready on {addr}");
|
debug_eprintln!("[real_irtt] readiness result=true addr={addr}");
|
||||||
|
|
||||||
drop(out_rx);
|
|
||||||
drop(err_rx);
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
addr,
|
addr,
|
||||||
child,
|
child,
|
||||||
keepalive_ms,
|
keepalive_ms,
|
||||||
|
stdout_thread: Some(stdout_thread),
|
||||||
|
stderr_thread: Some(stderr_thread),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,11 +105,11 @@ impl RealIrtServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn find_free_port() -> Result<u16, String> {
|
fn find_free_port() -> Result<u16, String> {
|
||||||
let socket = UdpSocket::bind("127.0.0.1:0")
|
let socket =
|
||||||
.map_err(|e| format!("failed to find free port: {}", e))?;
|
UdpSocket::bind("127.0.0.1:0").map_err(|e| format!("failed to find free port: {e}"))?;
|
||||||
let port = socket
|
let port = socket
|
||||||
.local_addr()
|
.local_addr()
|
||||||
.map_err(|e| format!("failed to get local addr: {}", e))?
|
.map_err(|e| format!("failed to get local addr: {e}"))?
|
||||||
.port();
|
.port();
|
||||||
drop(socket);
|
drop(socket);
|
||||||
Ok(port)
|
Ok(port)
|
||||||
@@ -169,29 +128,55 @@ impl Drop for RealIrtServer {
|
|||||||
debug_eprintln!("[real_irtt] killing child pid={}", self.child.id());
|
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();
|
||||||
|
if let Some(thread) = self.stdout_thread.take() {
|
||||||
|
thread.join().ok();
|
||||||
|
}
|
||||||
|
if let Some(thread) = self.stderr_thread.take() {
|
||||||
|
thread.join().ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_line(
|
fn wait_for_ready(line_rx: &mpsc::Receiver<(&'static str, String)>, timeout: Duration) -> bool {
|
||||||
out_rx: &mpsc::Receiver<(&str, String)>,
|
let start = Instant::now();
|
||||||
err_rx: &mpsc::Receiver<(&str, String)>,
|
loop {
|
||||||
timeout: Duration,
|
let remaining = timeout
|
||||||
) -> Option<(&'static str, String)> {
|
.checked_sub(start.elapsed())
|
||||||
if let Ok((_, line)) = out_rx.recv_timeout(timeout) {
|
.unwrap_or(Duration::ZERO);
|
||||||
return Some(("stdout", line));
|
|
||||||
|
match line_rx.recv_timeout(remaining) {
|
||||||
|
Ok((stream, text)) => {
|
||||||
|
debug_eprintln!("[real_irtt] {stream}: {text}");
|
||||||
|
if text.contains("[ListenerStart]") {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
if let Ok((_, line)) = err_rx.recv_timeout(Duration::ZERO) {
|
if (text.contains("[ListenerStop]") || text.contains("[ServerStop]"))
|
||||||
return Some(("stderr", line));
|
&& (text.contains("error") || text.contains("Error"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
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)) {
|
Err(_) => return false,
|
||||||
return Some(("stderr", line));
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drain_stream(
|
||||||
|
stream: &'static str,
|
||||||
|
pipe: impl Read,
|
||||||
|
tx: mpsc::Sender<(&'static str, String)>,
|
||||||
|
captured: Arc<Mutex<Vec<String>>>,
|
||||||
|
) {
|
||||||
|
let reader = BufReader::new(pipe);
|
||||||
|
for line in reader.lines() {
|
||||||
|
match line {
|
||||||
|
Ok(text) => {
|
||||||
|
captured.lock().unwrap().push(format!("{stream}: {text}"));
|
||||||
|
let _ = tx.send((stream, text));
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! debug_eprintln {
|
macro_rules! debug_eprintln {
|
||||||
|
|||||||
Reference in new issue
Block a user