From 9b93dc8abe0bdbffa5b8cd07cbe7d898d2e1d696 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 3 Aug 2025 23:30:36 +0100 Subject: [PATCH 1/3] Unify run_app helpers --- docs/wireframe-testing-crate.md | 34 +++++----- tests/lifecycle.rs | 4 +- wireframe_testing/src/helpers.rs | 106 ++++--------------------------- wireframe_testing/src/lib.rs | 17 +---- 4 files changed, 34 insertions(+), 127 deletions(-) diff --git a/docs/wireframe-testing-crate.md b/docs/wireframe-testing-crate.md index dc19cbe4..7433dfaa 100644 --- a/docs/wireframe-testing-crate.md +++ b/docs/wireframe-testing-crate.md @@ -6,12 +6,11 @@ frames, enabling fast tests without opening real network connections. ## Motivation -The existing tests in [`tests/`](../tests) use helper functions such as -`run_app_with_frame` and `run_app_with_frames` to feed length-prefixed frames -through an in-memory duplex stream. These helpers simplify testing handlers by -allowing assertions on encoded responses without spinning up a full server. -Encapsulating this logic in a dedicated crate keeps test code concise and -reusable across projects. +The existing tests in [`tests/`](../tests) use a helper function `run_app` to +feed length-prefixed frames through an in-memory duplex stream. This helper +simplifies testing handlers by allowing assertions on encoded responses without +spinning up a full server. Encapsulating this logic in a dedicated crate keeps +test code concise and reusable across projects. ## Crate Layout @@ -63,15 +62,15 @@ where M: Serialize; ``` -These functions mirror the behaviour of `run_app_with_frame` and -`run_app_with_frames` found in the repository’s test utilities. They create a -`tokio::io::duplex` stream, run the application on the server half, and write -the provided frame(s) to the client side. All helpers delegate to a single -internal function that handles this I/O plumbing, ensuring consistent -behaviour. Should the application panic, the panic message is returned as an -`io::Error` beginning with `server task failed`, helping surface failures in -tests. After the application finishes processing the input frames, the bytes -written back are collected for inspection. +These functions mirror the behaviour of the `run_app` helper found in the +repository’s test utilities. They create a `tokio::io::duplex` stream, run the +application on the server half, and write the provided frame(s) to the client +side. All helpers delegate to a single internal function that handles this I/O +plumbing, ensuring consistent behaviour. Should the application panic, the +panic message is returned as an `io::Error` beginning with +`server task failed`, helping surface failures in tests. After the application +finishes processing the input frames, the bytes written back are collected for +inspection. Any I/O errors surfaced by the duplex stream or failures while decoding a length prefix propagate through the returned `IoResult`. Malformed or truncated @@ -81,8 +80,7 @@ assert on these failure conditions directly. ### Custom Buffer Capacity A variant accepting a buffer `capacity` allows fine-tuning the size of the -in-memory duplex channel, matching the existing -`run_app_with_frame_with_capacity` and `run_app_with_frames_with_capacity` +in-memory duplex channel, matching the existing `run_app` helper. ```helpers. pub async fn drive_with_frame_with_capacity( @@ -167,7 +165,7 @@ with prebuilt frames and their responses decoded for assertions. ### Capturing Logs in Tests -The `wireframe_testing` crate exposes a [`LoggerHandle`] fixture for asserting +The `wireframe_testing` crate exposes a \[`LoggerHandle`\] fixture for asserting log output. Acquire it in a test and call `clear()` to discard any records from fixture setup. Records can then be inspected using `pop()`: diff --git a/tests/lifecycle.rs b/tests/lifecycle.rs index a81068b0..33b824dc 100644 --- a/tests/lifecycle.rs +++ b/tests/lifecycle.rs @@ -17,7 +17,7 @@ use wireframe::{ frame::{FrameProcessor, LengthPrefixedProcessor}, serializer::{BincodeSerializer, Serializer}, }; -use wireframe_testing::{processor, run_app_with_frame, run_with_duplex_server}; +use wireframe_testing::{processor, run_app, run_with_duplex_server}; fn call_counting_callback( counter: &Arc, @@ -135,7 +135,7 @@ async fn helpers_propagate_connection_state() { .encode(&bytes, &mut frame) .expect("encode should succeed"); - let out = run_app_with_frame(app, frame.to_vec()) + let out = run_app(app, vec![frame.to_vec()], None) .await .expect("app run failed"); assert!(!out.is_empty()); diff --git a/wireframe_testing/src/helpers.rs b/wireframe_testing/src/helpers.rs index 13749250..dcb6d0c8 100644 --- a/wireframe_testing/src/helpers.rs +++ b/wireframe_testing/src/helpers.rs @@ -19,7 +19,9 @@ use wireframe::{ unused_braces, reason = "Clippy is wrong here; this is not a redundant block" )] -pub fn processor() -> LengthPrefixedProcessor { LengthPrefixedProcessor::default() } +pub fn processor() -> LengthPrefixedProcessor { + LengthPrefixedProcessor::default() +} pub trait TestSerializer: Serializer + FrameMetadata + Send + Sync + 'static @@ -327,31 +329,11 @@ where /// /// # Errors /// -/// Returns any I/O errors encountered while interacting with the in-memory -/// duplex stream. +/// Run `app` with input `frames` using an optional duplex buffer `capacity`. /// -/// ```rust -/// # use wireframe_testing::{run_app_with_frame, processor}; -/// # use wireframe::app::WireframeApp; -/// # async fn demo() -> tokio::io::Result<()> { -/// let app = WireframeApp::new().frame_processor(processor()).unwrap(); -/// let out = run_app_with_frame(app, vec![1]).await?; -/// # Ok(()) -/// # } -/// ``` -pub async fn run_app_with_frame( - app: WireframeApp, - frame: Vec, -) -> io::Result> -where - S: TestSerializer, - C: Send + 'static, - E: Packet, -{ - run_app_with_frame_with_capacity(app, frame, DEFAULT_CAPACITY).await -} - -/// Drive `app` with a single frame using a duplex buffer of `capacity` bytes. +/// When `capacity` is `None`, a buffer of [`DEFAULT_CAPACITY`] bytes is used. +/// Frames are written to the client side in order and the bytes emitted by the +/// server are collected for inspection. /// /// # Errors /// @@ -362,89 +344,27 @@ where /// Panics if the spawned task running the application panics. /// /// ```rust -/// # use wireframe_testing::{run_app_with_frame_with_capacity, processor}; +/// # use wireframe_testing::{processor, run_app}; /// # use wireframe::app::WireframeApp; /// # async fn demo() -> tokio::io::Result<()> { /// let app = WireframeApp::new().frame_processor(processor()).unwrap(); -/// let out = run_app_with_frame_with_capacity(app, vec![1], 128).await?; +/// let out = run_app(app, vec![vec![1]], None).await?; /// # Ok(()) /// # } /// ``` -pub async fn run_app_with_frame_with_capacity( - app: WireframeApp, - frame: Vec, - capacity: usize, -) -> io::Result> -where - S: TestSerializer, - C: Send + 'static, - E: Packet, -{ - run_app_with_frames_with_capacity(app, vec![frame], capacity).await -} - -/// Run `app` with multiple input `frames` using the default buffer capacity. -/// -/// # Errors -/// -/// Returns any I/O errors encountered while interacting with the in-memory -/// duplex stream. -/// -/// ```rust -/// # use wireframe_testing::{run_app_with_frames, processor}; -/// # use wireframe::app::WireframeApp; -/// # async fn demo() -> tokio::io::Result<()> { -/// let app = WireframeApp::new().frame_processor(processor()).unwrap(); -/// let out = run_app_with_frames(app, vec![vec![1], vec![2]]).await?; -/// # Ok(()) -/// # } -/// ``` -#[allow(dead_code)] -pub async fn run_app_with_frames( - app: WireframeApp, - frames: Vec>, -) -> io::Result> -where - S: TestSerializer, - C: Send + 'static, - E: Packet, -{ - run_app_with_frames_with_capacity(app, frames, DEFAULT_CAPACITY).await -} - -/// Drive `app` with multiple frames using a duplex buffer of `capacity` bytes. -/// -/// # Errors -/// -/// Propagates any I/O errors from the in-memory connection. -/// -/// # Panics -/// -/// Panics if the spawned task running the application panics. -/// -/// ```rust -/// # use wireframe_testing::{run_app_with_frames_with_capacity, processor}; -/// # use wireframe::app::WireframeApp; -/// # async fn demo() -> tokio::io::Result<()> { -/// let app = WireframeApp::new().frame_processor(processor()).unwrap(); -/// let out = run_app_with_frames_with_capacity(app, vec![vec![1], vec![2]], 64).await?; -/// # Ok(()) -/// # } -/// ``` -pub async fn run_app_with_frames_with_capacity( +pub async fn run_app( app: WireframeApp, frames: Vec>, - capacity: usize, + capacity: Option, ) -> io::Result> where S: TestSerializer, C: Send + 'static, E: Packet, { + let capacity = capacity.unwrap_or(DEFAULT_CAPACITY); let (mut client, server) = duplex(capacity); - let server_task = tokio::spawn(async move { - app.handle_connection(server).await; - }); + let server_task = tokio::spawn(async move { app.handle_connection(server).await }); for frame in &frames { client.write_all(frame).await?; diff --git a/wireframe_testing/src/lib.rs b/wireframe_testing/src/lib.rs index 83525e0a..2a227cda 100644 --- a/wireframe_testing/src/lib.rs +++ b/wireframe_testing/src/lib.rs @@ -22,19 +22,8 @@ pub mod helpers; pub mod logging; pub use helpers::{ - TestSerializer, - drive_with_bincode, - drive_with_frame, - drive_with_frame_mut, - drive_with_frame_with_capacity, - drive_with_frames, - drive_with_frames_mut, - drive_with_frames_with_capacity, - processor, - run_app_with_frame, - run_app_with_frame_with_capacity, - run_app_with_frames, - run_app_with_frames_with_capacity, - run_with_duplex_server, + TestSerializer, drive_with_bincode, drive_with_frame, drive_with_frame_mut, + drive_with_frame_with_capacity, drive_with_frames, drive_with_frames_mut, + drive_with_frames_with_capacity, processor, run_app, run_with_duplex_server, }; pub use logging::{LoggerHandle, logger}; From 38b95115f8a34fd0bb59b18a845a118b160dc24c Mon Sep 17 00:00:00 2001 From: Leynos Date: Mon, 4 Aug 2025 01:15:09 +0100 Subject: [PATCH 2/3] Validate run_app capacity --- docs/wireframe-testing-crate.md | 9 +++++---- wireframe_testing/src/helpers.rs | 34 ++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/docs/wireframe-testing-crate.md b/docs/wireframe-testing-crate.md index 7433dfaa..3972a71f 100644 --- a/docs/wireframe-testing-crate.md +++ b/docs/wireframe-testing-crate.md @@ -80,7 +80,8 @@ assert on these failure conditions directly. ### Custom Buffer Capacity A variant accepting a buffer `capacity` allows fine-tuning the size of the -in-memory duplex channel, matching the existing `run_app` helper. +in-memory duplex channel, matching the existing `run_app` helper. The value +must be greater than zero and does not exceed 10 MB. ```helpers. pub async fn drive_with_frame_with_capacity( @@ -165,9 +166,9 @@ with prebuilt frames and their responses decoded for assertions. ### Capturing Logs in Tests -The `wireframe_testing` crate exposes a \[`LoggerHandle`\] fixture for asserting -log output. Acquire it in a test and call `clear()` to discard any records from -fixture setup. Records can then be inspected using `pop()`: +The `wireframe_testing` crate exposes a \[`LoggerHandle`\] fixture for +asserting log output. Acquire it in a test and call `clear()` to discard any +records from fixture setup. Records can then be inspected using `pop()`: ```rust use wireframe_testing::logger; diff --git a/wireframe_testing/src/helpers.rs b/wireframe_testing/src/helpers.rs index dcb6d0c8..eb6db0c2 100644 --- a/wireframe_testing/src/helpers.rs +++ b/wireframe_testing/src/helpers.rs @@ -34,6 +34,7 @@ impl TestSerializer for T where } const DEFAULT_CAPACITY: usize = 4096; +const MAX_CAPACITY: usize = 1024 * 1024 * 10; // 10MB limit async fn drive_internal( server_fn: F, @@ -325,10 +326,6 @@ where drive_with_frame(app, framed.to_vec()).await } -/// Run `app` with a single input `frame` using the default buffer capacity. -/// -/// # Errors -/// /// Run `app` with input `frames` using an optional duplex buffer `capacity`. /// /// When `capacity` is `None`, a buffer of [`DEFAULT_CAPACITY`] bytes is used. @@ -337,11 +334,9 @@ where /// /// # Errors /// -/// Propagates any I/O errors from the in-memory connection. -/// -/// # Panics -/// -/// Panics if the spawned task running the application panics. +/// Returns an error if `capacity` is zero or exceeds [`MAX_CAPACITY`]. Any +/// panic in the application task or I/O error on the duplex stream is also +/// surfaced as an error. /// /// ```rust /// # use wireframe_testing::{processor, run_app}; @@ -363,6 +358,19 @@ where E: Packet, { let capacity = capacity.unwrap_or(DEFAULT_CAPACITY); + if capacity == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "capacity must be greater than zero", + )); + } + if capacity > MAX_CAPACITY { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("capacity must not exceed {MAX_CAPACITY} bytes"), + )); + } + let (mut client, server) = duplex(capacity); let server_task = tokio::spawn(async move { app.handle_connection(server).await }); @@ -374,7 +382,13 @@ where let mut buf = Vec::new(); client.read_to_end(&mut buf).await?; - server_task.await.expect("server task panicked"); + if let Err(e) = server_task.await { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("server task failed: {e}"), + )); + } + Ok(buf) } From eb2be1acac68d5f7f309ec33e6a45ae0458504f6 Mon Sep 17 00:00:00 2001 From: Payton McIntosh Date: Mon, 4 Aug 2025 12:11:48 +0100 Subject: [PATCH 3/3] Apply formatting --- wireframe_testing/src/helpers.rs | 5 +---- wireframe_testing/src/lib.rs | 14 +++++++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/wireframe_testing/src/helpers.rs b/wireframe_testing/src/helpers.rs index c5f4739b..95149f02 100644 --- a/wireframe_testing/src/helpers.rs +++ b/wireframe_testing/src/helpers.rs @@ -19,9 +19,7 @@ use wireframe::{ unused_braces, reason = "Clippy is wrong here; this is not a redundant block" )] -pub fn processor() -> LengthPrefixedProcessor { - LengthPrefixedProcessor::default() -} +pub fn processor() -> LengthPrefixedProcessor { LengthPrefixedProcessor::default() } pub trait TestSerializer: Serializer + FrameMetadata + Send + Sync + 'static @@ -33,7 +31,6 @@ impl TestSerializer for T where { } - /// Run `server_fn` against a duplex stream, writing each `frame` to the client /// half and returning the bytes produced by the server. /// diff --git a/wireframe_testing/src/lib.rs b/wireframe_testing/src/lib.rs index 2a227cda..b3913181 100644 --- a/wireframe_testing/src/lib.rs +++ b/wireframe_testing/src/lib.rs @@ -22,8 +22,16 @@ pub mod helpers; pub mod logging; pub use helpers::{ - TestSerializer, drive_with_bincode, drive_with_frame, drive_with_frame_mut, - drive_with_frame_with_capacity, drive_with_frames, drive_with_frames_mut, - drive_with_frames_with_capacity, processor, run_app, run_with_duplex_server, + TestSerializer, + drive_with_bincode, + drive_with_frame, + drive_with_frame_mut, + drive_with_frame_with_capacity, + drive_with_frames, + drive_with_frames_mut, + drive_with_frames_with_capacity, + processor, + run_app, + run_with_duplex_server, }; pub use logging::{LoggerHandle, logger};