diff --git a/docs/wireframe-testing-crate.md b/docs/wireframe-testing-crate.md index dc19cbe4..3972a71f 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,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_with_frame_with_capacity` and `run_app_with_frames_with_capacity` +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( @@ -167,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/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 cba2b093..95149f02 100644 --- a/wireframe_testing/src/helpers.rs +++ b/wireframe_testing/src/helpers.rs @@ -100,6 +100,7 @@ where } const DEFAULT_CAPACITY: usize = 4096; +const MAX_CAPACITY: usize = 1024 * 1024 * 10; // 10MB limit macro_rules! forward_default { ( @@ -348,106 +349,53 @@ where drive_with_frame(app, framed.to_vec()).await } -forward_default! { - /// Run `app` with a single input `frame` 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_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 fn run_app_with_frame(app: WireframeApp, frame: Vec) -> io::Result> - => run_app_with_frame_with_capacity(app, frame) -} - -forward_with_capacity! { - /// Drive `app` with a single frame 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_frame_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_frame_with_capacity(app, vec![1], 128).await?; - /// # Ok(()) - /// # } - /// ``` - pub fn run_app_with_frame_with_capacity(app: WireframeApp, frame: Vec, capacity: usize) -> io::Result> - => run_app_with_frames_with_capacity(app, vec![frame], capacity) -} - -forward_default! { - #[allow(dead_code)] - /// 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(()) - /// # } - /// ``` - pub fn run_app_with_frames(app: WireframeApp, frames: Vec>) -> io::Result> - => run_app_with_frames_with_capacity(app, frames) -} - -/// Drive `app` with multiple frames using a duplex buffer of `capacity` bytes. +/// Run `app` with input `frames` using an optional duplex buffer `capacity`. /// -/// # Errors +/// 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. /// -/// Propagates any I/O errors from the in-memory connection. -/// -/// # Panics +/// # Errors /// -/// 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::{run_app_with_frames_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_frames_with_capacity(app, vec![vec![1], vec![2]], 64).await?; +/// let out = run_app(app, vec![vec![1]], None).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); + 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; - }); + let server_task = tokio::spawn(async move { app.handle_connection(server).await }); for frame in &frames { client.write_all(frame).await?; @@ -457,7 +405,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) } diff --git a/wireframe_testing/src/lib.rs b/wireframe_testing/src/lib.rs index 83525e0a..b3913181 100644 --- a/wireframe_testing/src/lib.rs +++ b/wireframe_testing/src/lib.rs @@ -31,10 +31,7 @@ pub use helpers::{ 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_app, run_with_duplex_server, }; pub use logging::{LoggerHandle, logger};