diff --git a/src/server.rs b/src/server.rs index e5b1d4b8..dbc650f5 100644 --- a/src/server.rs +++ b/src/server.rs @@ -274,6 +274,17 @@ where Ok(self) } + /// Bind the server to an existing standard TCP listener. + /// + /// # Errors + /// Returns an [`io::Error`] if configuring the listener fails. + pub fn bind_listener(mut self, listener: StdTcpListener) -> io::Result { + listener.set_nonblocking(true)?; + let listener = TcpListener::from_std(listener)?; + self.listener = Some(Arc::new(listener)); + Ok(self) + } + /// Run the server until a shutdown signal is received. /// /// Each worker accepts connections concurrently and would diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 00000000..51f04b8b --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,35 @@ +//! Shared utilities for integration tests. +//! +//! Provides fixtures for a basic [`WireframeApp`] factory and an unused +//! local port. These helpers reduce duplication across test modules. + +use std::net::{Ipv4Addr, SocketAddr, TcpListener as StdTcpListener}; + +/// Create a TCP listener bound to a free local port. +pub fn unused_listener() -> StdTcpListener { + let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0); + StdTcpListener::bind(addr).expect("failed to bind port") +} + +use rstest::fixture; +use wireframe::app::WireframeApp; + +#[fixture] +#[allow( + unused_braces, + reason = "rustc false positive for single line rstest fixtures" +)] +pub fn factory() -> impl Fn() -> WireframeApp + Send + Sync + Clone + 'static { + || WireframeApp::new().expect("WireframeApp::new failed") +} + +#[fixture] +#[allow( + unused_braces, + reason = "rustc false positive for single line rstest fixtures" +)] +pub fn unused_port() -> SocketAddr { + unused_listener() + .local_addr() + .expect("failed to obtain local addr") +} diff --git a/tests/preamble.rs b/tests/preamble.rs index 79d5177f..6eb315d3 100644 --- a/tests/preamble.rs +++ b/tests/preamble.rs @@ -4,7 +4,9 @@ use std::io; use bincode::error::DecodeError; use futures::future::BoxFuture; -use rstest::{fixture, rstest}; +mod common; +use common::{factory, unused_listener}; +use rstest::rstest; use tokio::{ io::{AsyncReadExt, AsyncWriteExt, duplex}, net::TcpStream, @@ -34,11 +36,6 @@ impl HotlinePreamble { } } -#[fixture] -fn factory() -> impl Fn() -> WireframeApp + Send + Sync + Clone + 'static { - || WireframeApp::new().expect("WireframeApp::new failed") -} - /// Create a server configured with `HotlinePreamble` handlers. fn server_with_handlers( factory: F, @@ -68,9 +65,9 @@ where Fut: std::future::Future, B: FnOnce(std::net::SocketAddr) -> Fut, { - let server = server - .bind("127.0.0.1:0".parse().expect("hard-coded socket addr")) - .expect("bind"); + let listener = unused_listener(); + let _addr = listener.local_addr().expect("addr"); + let server = server.bind_listener(listener).expect("bind"); let addr = server.local_addr().expect("addr"); let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); let handle = tokio::spawn(async move { diff --git a/tests/server.rs b/tests/server.rs index 14559199..daa13cdc 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -1,31 +1,31 @@ //! Tests for [`WireframeServer`] configuration. -use wireframe::{app::WireframeApp, server::WireframeServer}; +mod common; +use common::{factory, unused_listener}; +use wireframe::server::WireframeServer; #[test] fn default_worker_count_matches_cpu_count() { - let server = WireframeServer::new(|| WireframeApp::new().expect("WireframeApp::new failed")); + let server = WireframeServer::new(factory()); let expected = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get); assert_eq!(server.worker_count(), expected); } #[test] fn default_workers_at_least_one() { - let server = WireframeServer::new(|| WireframeApp::new().expect("WireframeApp::new failed")); + let server = WireframeServer::new(factory()); assert!(server.worker_count() >= 1); } #[test] fn workers_method_enforces_minimum() { - let server = - WireframeServer::new(|| WireframeApp::new().expect("WireframeApp::new failed")).workers(0); + let server = WireframeServer::new(factory()).workers(0); assert_eq!(server.worker_count(), 1); } #[test] fn workers_accepts_large_values() { - let server = WireframeServer::new(|| WireframeApp::new().expect("WireframeApp::new failed")) - .workers(128); + let server = WireframeServer::new(factory()).workers(128); assert_eq!(server.worker_count(), 128); } @@ -39,15 +39,12 @@ async fn readiness_receiver_dropped() { time::{Duration, sleep}, }; - let factory = || WireframeApp::new().expect("WireframeApp::new failed"); - let server = WireframeServer::new(factory) + let listener = unused_listener(); + let _addr = listener.local_addr().unwrap(); + let server = WireframeServer::new(factory()) .workers(1) - .bind( - "127.0.0.1:0" - .parse() - .expect("hard-coded socket address must be valid"), - ) - .expect("bind failed"); + .bind_listener(listener) + .unwrap(); let addr = server.local_addr().expect("local addr missing"); // Create channel and immediately drop receiver to force send failure diff --git a/tests/world.rs b/tests/world.rs index f1a22e98..e1814277 100644 --- a/tests/world.rs +++ b/tests/world.rs @@ -9,6 +9,10 @@ use cucumber::World; use tokio::{net::TcpStream, sync::oneshot}; use wireframe::{app::WireframeApp, server::WireframeServer}; +#[path = "common/mod.rs"] +mod common; +use common::unused_listener; + #[derive(Debug)] struct PanicServer { addr: SocketAddr, @@ -24,11 +28,11 @@ impl PanicServer { .on_connection_setup(|| async { panic!("boom") }) .expect("Failed to set connection setup callback") }; + let listener = unused_listener(); let server = WireframeServer::new(factory) .workers(1) - .bind("127.0.0.1:0".parse().expect("Failed to parse address")) + .bind_listener(listener) .expect("bind"); - let addr = server.local_addr().expect("Failed to get server address"); let (tx_shutdown, rx_shutdown) = oneshot::channel(); let (tx_ready, rx_ready) = oneshot::channel();