Files
fips/src/testutil.rs
T
Johnathan Corgan af40a158fe Log the OS error and peer when a TCP connect fails
Trying every resolved address moved the connect into
connect_to_any_addr, which maps any I/O failure to ConnectionRefused
and kept the real error only at trace level with no transport or peer
fields. The background connect's debug line then printed the variant,
whose text is always "connection refused", so an operator at debug
level saw that for "No route to host" and "Network is unreachable" too,
including for a single numeric address.

connect_to_any_addr now takes the transport id and the peer address,
logs the last candidate's OS error at debug level with both, and names
the peer in its no-addresses error as the resolver did before. The
background path's own refusal line is dropped, since it would repeat
the same failure without the error. Connect-on-send, which logged
nothing on a refusal, now gets the same debug line.

A test captures the log and requires the debug line to carry the OS
error, transport id and peer. It registers the log callsite before
installing the capture, because a parallel test that registers it
first while the capture is the only subscriber caches its own thread's
"never" interest and the event is lost.
2026-09-30 17:24:41 +00:00

81 lines
2.8 KiB
Rust

//! Crate-wide generic test helpers.
use crate::NodeAddr;
/// Build a `NodeAddr` from a single discriminating byte in position 0.
pub(crate) fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 16];
bytes[0] = val;
NodeAddr::from_bytes(bytes)
}
/// Collects emitted tracing events so a test can assert on a log line.
///
/// Some behaviour is reported only in the log: a structured field an operator
/// greps on is part of the contract even when no counter or return value
/// carries it. Installed with `tracing::subscriber::with_default`, which is
/// thread-local, so tests running in parallel do not see each other's events.
#[derive(Clone, Default)]
pub(crate) struct LogCapture(std::sync::Arc<std::sync::Mutex<Vec<String>>>);
impl LogCapture {
/// Every captured line, each prefixed with its level.
pub(crate) fn lines(&self) -> Vec<String> {
self.0.lock().unwrap().clone()
}
/// Only the captured lines emitted at WARN.
pub(crate) fn warnings(&self) -> Vec<String> {
self.0
.lock()
.unwrap()
.iter()
.filter(|line| line.starts_with("WARN"))
.cloned()
.collect()
}
}
impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for LogCapture {
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
struct Fields(String);
impl tracing::field::Visit for Fields {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
self.0.push_str(&format!(" {}={:?}", field.name(), value));
}
}
let mut fields = Fields(event.metadata().level().to_string());
event.record(&mut fields);
self.0.lock().unwrap().push(fields.0);
}
}
/// Run `f` with a capturing subscriber installed, returning its value and the capture.
pub(crate) fn capture_logs<T>(f: impl FnOnce() -> T) -> (T, LogCapture) {
use tracing_subscriber::layer::SubscriberExt;
let capture = LogCapture::default();
let subscriber = tracing_subscriber::registry().with(capture.clone());
let out = tracing::subscriber::with_default(subscriber, f);
(out, capture)
}
/// Install a capturing subscriber for the rest of the current scope.
///
/// The async counterpart of [`capture_logs`]: an `async` test cannot wrap its
/// awaits in a closure, so it holds this guard instead and reads the capture
/// once the awaited work has run.
pub(crate) fn capture_logs_scoped() -> (LogCapture, tracing::subscriber::DefaultGuard) {
use tracing_subscriber::layer::SubscriberExt;
let capture = LogCapture::default();
let subscriber = tracing_subscriber::registry().with(capture.clone());
let guard = tracing::subscriber::set_default(subscriber);
(capture, guard)
}