Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
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
Expand Down
35 changes: 35 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -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")
}
Comment on lines +26 to +35
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential race condition in unused_port fixture.

The unused_port fixture creates a listener, extracts its address, then drops the listener. This introduces a race condition where another process could bind to the same port before the test uses it.

Replace the fixture with direct usage of unused_listener() in tests that need a bound listener, or ensure the listener remains alive until the test binds to it.

-#[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")
-}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In tests/common/mod.rs around lines 26 to 35, the unused_port fixture creates a
listener, extracts its address, and then drops the listener, causing a race
condition where the port may be taken before use. To fix this, modify tests to
use unused_listener() directly so the listener stays alive during the test, or
change the fixture to return the listener itself instead of just the address,
ensuring the port remains bound until the test completes.

15 changes: 6 additions & 9 deletions tests/preamble.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<F, S, E>(
factory: F,
Expand Down Expand Up @@ -68,9 +65,9 @@ where
Fut: std::future::Future<Output = ()>,
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");
Comment on lines +68 to +70
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Remove unused variable assignment.

Line 69 assigns listener.local_addr() to _addr but this value is never used. The actual server address is obtained from server.local_addr() on line 71.

-    let listener = unused_listener();
-    let _addr = listener.local_addr().expect("addr");
-    let server = server.bind_listener(listener).expect("bind");
+    let listener = unused_listener();
+    let server = server.bind_listener(listener).expect("bind");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let listener = unused_listener();
let _addr = listener.local_addr().expect("addr");
let server = server.bind_listener(listener).expect("bind");
let listener = unused_listener();
let server = server.bind_listener(listener).expect("bind");
🤖 Prompt for AI Agents
In tests/preamble.rs around lines 68 to 70, the variable _addr is assigned the
value of listener.local_addr() but never used. Remove the assignment to _addr on
line 69 to eliminate the unused variable, since the server address is correctly
obtained later from server.local_addr().

let addr = server.local_addr().expect("addr");
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
let handle = tokio::spawn(async move {
Expand Down
27 changes: 12 additions & 15 deletions tests/server.rs
Original file line number Diff line number Diff line change
@@ -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);
}

Expand All @@ -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();
Comment on lines +42 to +47
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Remove unused variable assignment.

Line 43 assigns listener.local_addr() to _addr but this value is never used. The server address is obtained from server.local_addr() on line 49.

-    let listener = unused_listener();
-    let _addr = listener.local_addr().unwrap();
-    let server = WireframeServer::new(factory())
+    let listener = unused_listener();
+    let server = WireframeServer::new(factory())
         .workers(1)
         .bind_listener(listener)
-        .unwrap();
+        .unwrap();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 listener = unused_listener();
let server = WireframeServer::new(factory())
.workers(1)
.bind_listener(listener)
.unwrap();
🤖 Prompt for AI Agents
In tests/server.rs around lines 42 to 47, the variable _addr is assigned the
value of listener.local_addr() but is never used later. Remove the assignment to
_addr on line 43 to clean up the code, as the server address is correctly
obtained from server.local_addr() on line 49.


let addr = server.local_addr().expect("local addr missing");
// Create channel and immediately drop receiver to force send failure
Expand Down
8 changes: 6 additions & 2 deletions tests/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
Expand Down
Loading