diff --git a/src/metrics/mod.rs b/src/metrics/mod.rs index b9644a6..dc39ce2 100644 --- a/src/metrics/mod.rs +++ b/src/metrics/mod.rs @@ -1216,10 +1216,12 @@ mod tests { // render() recounts from disk) let output = metrics.render(); assert!(output.contains("ngit_repositories_total 0")); - assert!(output.contains("process_cpu_seconds_total")); - assert!(output.contains("process_resident_memory_bytes")); #[cfg(target_os = "linux")] { + // ProcessCollector is registered only on Linux, just like the + // cgroup collectors below. Repository metrics are portable. + assert!(output.contains("process_cpu_seconds_total")); + assert!(output.contains("process_resident_memory_bytes")); if let Some(directory) = cgroup_v2_directory() { if cgroup_cpu_seconds(&directory).is_some() { assert!(output.contains("ngit_cgroup_cpu_seconds_total")); diff --git a/src/test_listener.rs b/src/test_listener.rs index 744999e..c72e86b 100644 --- a/src/test_listener.rs +++ b/src/test_listener.rs @@ -38,12 +38,21 @@ fn listener_from_fd(fd: RawFd) -> Result { return Err(std::io::Error::last_os_error()).context("protect test listener from exec"); } } + if !is_listening(owned)? || !listener.local_addr()?.ip().is_loopback() { + bail!("test listener must be a listening loopback TCP socket"); + } + listener.set_nonblocking(true)?; + TcpListener::from_std(listener).context("adopt test listener") +} + +#[cfg(not(target_vendor = "apple"))] +fn is_listening(fd: RawFd) -> Result { let mut accepting: libc::c_int = 0; let mut length = std::mem::size_of_val(&accepting) as libc::socklen_t; // SAFETY: both pointers reference live, correctly sized writable values. let result = unsafe { libc::getsockopt( - owned, + fd, libc::SOL_SOCKET, libc::SO_ACCEPTCONN, (&mut accepting as *mut libc::c_int).cast(), @@ -53,11 +62,32 @@ fn listener_from_fd(fd: RawFd) -> Result { if result != 0 { return Err(std::io::Error::last_os_error()).context("inspect test listener"); } - if accepting == 0 || !listener.local_addr()?.ip().is_loopback() { - bail!("test listener must be a listening loopback TCP socket"); + Ok(accepting != 0) +} + +#[cfg(target_vendor = "apple")] +fn is_listening(fd: RawFd) -> Result { + // XNU defines SO_ACCEPTCONN but does not support querying it with + // getsockopt. TCP_CONNECTION_INFO exposes the TCP state without accepting + // a queued connection or changing the inherited listener's backlog. + // SAFETY: tcp_connection_info consists entirely of integer fields. + let mut info: libc::tcp_connection_info = unsafe { std::mem::zeroed() }; + let mut length = std::mem::size_of_val(&info) as libc::socklen_t; + // SAFETY: info and length are live, correctly sized writable values. + let result = unsafe { + libc::getsockopt( + fd, + libc::IPPROTO_TCP, + libc::TCP_CONNECTION_INFO, + (&mut info as *mut libc::tcp_connection_info).cast(), + &mut length, + ) + }; + if result != 0 { + return Err(std::io::Error::last_os_error()).context("inspect test listener"); } - listener.set_nonblocking(true)?; - TcpListener::from_std(listener).context("adopt test listener") + // TCPS_LISTEN from XNU's netinet/tcp_fsm.h (not exported by libc). + Ok(info.tcpi_state == 1) } #[cfg(test)] @@ -89,5 +119,27 @@ mod tests { let file = std::fs::File::open("/dev/null").unwrap(); assert!(listener_from_fd(file.as_raw_fd()).is_err()); assert!(listener_from_fd(-1).is_err()); + let datagram = std::net::UdpSocket::bind("127.0.0.1:0").unwrap(); + assert!(listener_from_fd(datagram.as_raw_fd()).is_err()); + } + + #[tokio::test] + async fn adoption_preserves_queued_connections_and_rejects_connected_streams() { + let reserved = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let client = tokio::time::timeout( + std::time::Duration::from_secs(5), + tokio::net::TcpStream::connect(reserved.local_addr().unwrap()), + ) + .await + .unwrap() + .unwrap(); + assert!(listener_from_fd(client.as_raw_fd()).is_err()); + let listener = listener_from_fd(reserved.as_raw_fd()).unwrap(); + drop(reserved); + let (_, peer) = tokio::time::timeout(std::time::Duration::from_secs(5), listener.accept()) + .await + .unwrap() + .unwrap(); + assert_eq!(peer, client.local_addr().unwrap()); } } diff --git a/tests/common/relay.rs b/tests/common/relay.rs index 5b55ac0..0bc517a 100644 --- a/tests/common/relay.rs +++ b/tests/common/relay.rs @@ -1114,6 +1114,37 @@ impl Drop for TestRelay { // Ensure process is killed when TestRelay is dropped let _ = self.process.kill(); let _ = self.process.wait(); + if std::thread::panicking() { + // Sandboxed builders discard /tmp after failure. Preserve a bounded + // tail from every participating relay in libtest's failure output. + // Diagnostic I/O must never cause a second panic during unwinding. + use std::io::{Read, Seek, SeekFrom, Write}; + let path = self.log_path(); + let tail = (|| -> std::io::Result> { + let mut file = std::fs::File::open(&path)?; + let start = file.metadata()?.len().saturating_sub(64 * 1024); + file.seek(SeekFrom::Start(start))?; + let mut bytes = Vec::new(); + file.take(64 * 1024).read_to_end(&mut bytes)?; + Ok(bytes) + })(); + let mut stderr = std::io::stderr().lock(); + let _ = writeln!( + stderr, + "Relay {} failure log ({})", + self.port, + path.display() + ); + match tail { + Ok(bytes) => { + let _ = stderr.write_all(&bytes); + let _ = writeln!(stderr); + } + Err(error) => { + let _ = writeln!(stderr, "Could not read relay log: {error}"); + } + } + } } }