From f868861e2426941f00881d0e9f7c0fd69cb0fcb9 Mon Sep 17 00:00:00 2001 From: Leynos Date: Mon, 23 Jun 2025 01:12:17 +0100 Subject: [PATCH 1/4] Add wireframe_testing crate and migrate tests --- Cargo.lock | 11 +++ Cargo.toml | 1 + tests/metadata.rs | 8 +- tests/routes.rs | 8 +- tests/util.rs | 132 ------------------------------- wireframe_testing/Cargo.toml | 13 +++ wireframe_testing/src/helpers.rs | 99 +++++++++++++++++++++++ wireframe_testing/src/lib.rs | 10 +++ 8 files changed, 140 insertions(+), 142 deletions(-) delete mode 100644 tests/util.rs create mode 100644 wireframe_testing/Cargo.toml create mode 100644 wireframe_testing/src/helpers.rs create mode 100644 wireframe_testing/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 5d495cbd..12b03ee3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -564,4 +564,15 @@ dependencies = [ "rstest", "serde", "tokio", + "wireframe_testing", +] + +[[package]] +name = "wireframe_testing" +version = "0.1.0" +dependencies = [ + "bincode", + "bytes", + "tokio", + "wireframe", ] diff --git a/Cargo.toml b/Cargo.toml index 71f202d6..fec8fb8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ log = "0.4" [dev-dependencies] rstest = "0.18.2" +wireframe_testing = { path = "./wireframe_testing" } [lints.clippy] pedantic = "warn" diff --git a/tests/metadata.rs b/tests/metadata.rs index 5441bded..90d621fd 100644 --- a/tests/metadata.rs +++ b/tests/metadata.rs @@ -9,9 +9,7 @@ use wireframe::{ frame::{FrameMetadata, FrameProcessor, LengthPrefixedProcessor}, serializer::{BincodeSerializer, Serializer}, }; - -mod util; -use util::{TestSerializer, run_app_with_frame}; +use wireframe_testing::{TestSerializer, drive_with_frame}; fn mock_wireframe_app_with_serializer(serializer: S) -> WireframeApp where @@ -66,7 +64,7 @@ async fn metadata_parser_invoked_before_deserialize() { .encode(&bytes, &mut framed) .unwrap(); - let out = run_app_with_frame(app, framed.to_vec()).await.unwrap(); + let out = drive_with_frame(app, framed.to_vec()).await.unwrap(); assert!(!out.is_empty()); assert_eq!(counter.load(Ordering::SeqCst), 1); } @@ -114,7 +112,7 @@ async fn falls_back_to_deserialize_after_parse_error() { .encode(&bytes, &mut framed) .unwrap(); - let out = run_app_with_frame(app, framed.to_vec()).await.unwrap(); + let out = drive_with_frame(app, framed.to_vec()).await.unwrap(); assert!(!out.is_empty()); assert_eq!(parse_calls.load(Ordering::SeqCst), 1); assert_eq!(deser_calls.load(Ordering::SeqCst), 1); diff --git a/tests/routes.rs b/tests/routes.rs index 843495ae..20232460 100644 --- a/tests/routes.rs +++ b/tests/routes.rs @@ -12,9 +12,7 @@ use wireframe::{ message::Message, serializer::BincodeSerializer, }; - -mod util; -use util::{run_app_with_frame, run_app_with_frames}; +use wireframe_testing::{drive_with_frame, drive_with_frames}; #[derive(bincode::Encode, bincode::BorrowDecode, PartialEq, Debug)] struct TestEnvelope { @@ -63,7 +61,7 @@ async fn handler_receives_message_and_echoes_response() { .encode(&env_bytes, &mut framed) .unwrap(); - let out = run_app_with_frame(app, framed.to_vec()).await.unwrap(); + let out = drive_with_frame(app, framed.to_vec()).await.unwrap(); let mut buf = BytesMut::from(&out[..]); let frame = LengthPrefixedProcessor::default() @@ -105,7 +103,7 @@ async fn multiple_frames_processed_in_sequence() { }) .collect(); - let out = run_app_with_frames(app, frames).await.unwrap(); + let out = drive_with_frames(app, frames).await.unwrap(); let mut buf = BytesMut::from(&out[..]); let first = LengthPrefixedProcessor::default() diff --git a/tests/util.rs b/tests/util.rs deleted file mode 100644 index 2ef136c8..00000000 --- a/tests/util.rs +++ /dev/null @@ -1,132 +0,0 @@ -use rstest::fixture; -use tokio::io::{self, AsyncReadExt, AsyncWriteExt, duplex}; -use wireframe::{ - app::{Envelope, Packet, WireframeApp}, - frame::{FrameMetadata, LengthPrefixedProcessor}, - serializer::Serializer, -}; - -/// Trait alias for serializers used in tests. -pub trait TestSerializer: - Serializer + FrameMetadata + Send + Sync + 'static -{ -} - -impl TestSerializer for T where - T: Serializer + FrameMetadata + Send + Sync + 'static -{ -} - -/// Feed a single frame into `app` and collect the response bytes. -/// -/// # Errors -/// -/// Propagates I/O errors from the in-memory connection. -/// -/// # Panics -/// -/// Panics if the spawned task running the application panics. -/// Optional duplex buffer capacity for `run_app_with_frame`. -const DEFAULT_CAPACITY: usize = 4096; - -/// Create a default length-prefixed frame processor for tests. -#[fixture] -#[rustfmt::skip] -pub fn processor() -> LengthPrefixedProcessor { - LengthPrefixedProcessor::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. -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. -/// -/// # Errors -/// -/// Propagates any I/O errors from the in-memory connection. -/// -/// # Panics -/// -/// Panics if the spawned task running the application panics. -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. -#[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. -pub async fn run_app_with_frames_with_capacity( - app: WireframeApp, - frames: Vec>, - capacity: usize, -) -> io::Result> -where - S: TestSerializer, - C: Send + 'static, - E: Packet, -{ - let (mut client, server) = duplex(capacity); - let server_task = tokio::spawn(async move { - app.handle_connection(server).await; - }); - - for frame in &frames { - client.write_all(frame).await?; - } - client.shutdown().await?; - - let mut buf = Vec::new(); - client.read_to_end(&mut buf).await?; - - server_task.await.unwrap(); - Ok(buf) -} diff --git a/wireframe_testing/Cargo.toml b/wireframe_testing/Cargo.toml new file mode 100644 index 00000000..e97cdc9f --- /dev/null +++ b/wireframe_testing/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "wireframe_testing" +version = "0.1.0" +edition = "2024" + +[dependencies] +tokio = { version = "1", features = ["macros", "rt", "io-util"] } +wireframe = { path = ".." } +bincode = "2" +bytes = "1" + +[dev-dependencies] +rstest = "0.18" diff --git a/wireframe_testing/src/helpers.rs b/wireframe_testing/src/helpers.rs new file mode 100644 index 00000000..c9461de7 --- /dev/null +++ b/wireframe_testing/src/helpers.rs @@ -0,0 +1,99 @@ +use bincode::config; +use bytes::BytesMut; +use tokio::io::{self, AsyncReadExt, AsyncWriteExt, duplex}; +use wireframe::{ + app::{Envelope, Packet, WireframeApp}, + frame::{FrameMetadata, FrameProcessor, LengthPrefixedProcessor}, + serializer::Serializer, +}; + +pub trait TestSerializer: + Serializer + FrameMetadata + Send + Sync + 'static +{ +} +impl TestSerializer for T where + T: Serializer + FrameMetadata + Send + Sync + 'static +{ +} + +const DEFAULT_CAPACITY: usize = 4096; + +pub async fn drive_with_frame( + app: WireframeApp, + frame: Vec, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, +{ + drive_with_frame_with_capacity(app, frame, DEFAULT_CAPACITY).await +} + +pub async fn drive_with_frame_with_capacity( + app: WireframeApp, + frame: Vec, + capacity: usize, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, +{ + drive_with_frames_with_capacity(app, vec![frame], capacity).await +} + +pub async fn drive_with_frames( + app: WireframeApp, + frames: Vec>, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, +{ + drive_with_frames_with_capacity(app, frames, DEFAULT_CAPACITY).await +} + +pub async fn drive_with_frames_with_capacity( + app: WireframeApp, + frames: Vec>, + capacity: usize, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, +{ + let (mut client, server) = duplex(capacity); + let server_task = tokio::spawn(async move { + app.handle_connection(server).await; + }); + + for frame in &frames { + client.write_all(frame).await?; + } + client.shutdown().await?; + + let mut buf = Vec::new(); + client.read_to_end(&mut buf).await?; + + server_task.await.unwrap(); + Ok(buf) +} + +pub async fn drive_with_bincode( + app: WireframeApp, + msg: M, +) -> io::Result> +where + M: bincode::Encode, + S: TestSerializer, + C: Send + 'static, + E: Packet, +{ + let bytes = bincode::encode_to_vec(msg, config::standard()).unwrap(); + let mut framed = BytesMut::new(); + LengthPrefixedProcessor::default().encode(&bytes, &mut framed)?; + drive_with_frame(app, framed.to_vec()).await +} diff --git a/wireframe_testing/src/lib.rs b/wireframe_testing/src/lib.rs new file mode 100644 index 00000000..4e876115 --- /dev/null +++ b/wireframe_testing/src/lib.rs @@ -0,0 +1,10 @@ +pub mod helpers; + +pub use helpers::{ + TestSerializer, + drive_with_bincode, + drive_with_frame, + drive_with_frame_with_capacity, + drive_with_frames, + drive_with_frames_with_capacity, +}; From 5b0f7dd973698a7ccbd388ad706fab5e411497ec Mon Sep 17 00:00:00 2001 From: Leynos Date: Mon, 23 Jun 2025 02:33:31 +0100 Subject: [PATCH 2/4] Use bincode helpers in tests --- tests/metadata.rs | 19 ++++--------------- tests/routes.rs | 9 ++------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/tests/metadata.rs b/tests/metadata.rs index 90d621fd..81e86b58 100644 --- a/tests/metadata.rs +++ b/tests/metadata.rs @@ -3,13 +3,12 @@ use std::sync::{ atomic::{AtomicUsize, Ordering}, }; -use bytes::BytesMut; use wireframe::{ app::{Envelope, WireframeApp}, - frame::{FrameMetadata, FrameProcessor, LengthPrefixedProcessor}, + frame::{FrameMetadata, LengthPrefixedProcessor}, serializer::{BincodeSerializer, Serializer}, }; -use wireframe_testing::{TestSerializer, drive_with_frame}; +use wireframe_testing::{TestSerializer, drive_with_bincode}; fn mock_wireframe_app_with_serializer(serializer: S) -> WireframeApp where @@ -58,13 +57,8 @@ async fn metadata_parser_invoked_before_deserialize() { let app = mock_wireframe_app_with_serializer(serializer); let env = Envelope::new(1, vec![42]); - let bytes = BincodeSerializer.serialize(&env).unwrap(); - let mut framed = BytesMut::new(); - LengthPrefixedProcessor::default() - .encode(&bytes, &mut framed) - .unwrap(); - let out = drive_with_frame(app, framed.to_vec()).await.unwrap(); + let out = drive_with_bincode(app, env).await.unwrap(); assert!(!out.is_empty()); assert_eq!(counter.load(Ordering::SeqCst), 1); } @@ -106,13 +100,8 @@ async fn falls_back_to_deserialize_after_parse_error() { let app = mock_wireframe_app_with_serializer(serializer); let env = Envelope::new(1, vec![7]); - let bytes = BincodeSerializer.serialize(&env).unwrap(); - let mut framed = BytesMut::new(); - LengthPrefixedProcessor::default() - .encode(&bytes, &mut framed) - .unwrap(); - let out = drive_with_frame(app, framed.to_vec()).await.unwrap(); + let out = drive_with_bincode(app, env).await.unwrap(); assert!(!out.is_empty()); assert_eq!(parse_calls.load(Ordering::SeqCst), 1); assert_eq!(deser_calls.load(Ordering::SeqCst), 1); diff --git a/tests/routes.rs b/tests/routes.rs index 20232460..31ff2164 100644 --- a/tests/routes.rs +++ b/tests/routes.rs @@ -12,7 +12,7 @@ use wireframe::{ message::Message, serializer::BincodeSerializer, }; -use wireframe_testing::{drive_with_frame, drive_with_frames}; +use wireframe_testing::{drive_with_bincode, drive_with_frames}; #[derive(bincode::Encode, bincode::BorrowDecode, PartialEq, Debug)] struct TestEnvelope { @@ -55,13 +55,8 @@ async fn handler_receives_message_and_echoes_response() { id: 1, msg: msg_bytes, }; - let env_bytes = BincodeSerializer.serialize(&env).unwrap(); - let mut framed = BytesMut::new(); - LengthPrefixedProcessor::default() - .encode(&env_bytes, &mut framed) - .unwrap(); - let out = drive_with_frame(app, framed.to_vec()).await.unwrap(); + let out = drive_with_bincode(app, env).await.unwrap(); let mut buf = BytesMut::from(&out[..]); let frame = LengthPrefixedProcessor::default() From 2ad4bd0789f650290b0b9489880fc4235991dd87 Mon Sep 17 00:00:00 2001 From: Leynos Date: Mon, 23 Jun 2025 02:33:37 +0100 Subject: [PATCH 3/4] Add mutable driver variants --- wireframe_testing/src/helpers.rs | 65 ++++++++++++++++++++++++++++++++ wireframe_testing/src/lib.rs | 2 + 2 files changed, 67 insertions(+) diff --git a/wireframe_testing/src/helpers.rs b/wireframe_testing/src/helpers.rs index c9461de7..d28c0c11 100644 --- a/wireframe_testing/src/helpers.rs +++ b/wireframe_testing/src/helpers.rs @@ -82,6 +82,71 @@ where Ok(buf) } +pub async fn drive_with_frame_mut( + app: &mut WireframeApp, + frame: Vec, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, +{ + drive_with_frame_with_capacity_mut(app, frame, DEFAULT_CAPACITY).await +} + +pub async fn drive_with_frame_with_capacity_mut( + app: &mut WireframeApp, + frame: Vec, + capacity: usize, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, +{ + drive_with_frames_with_capacity_mut(app, vec![frame], capacity).await +} + +pub async fn drive_with_frames_mut( + app: &mut WireframeApp, + frames: Vec>, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, +{ + drive_with_frames_with_capacity_mut(app, frames, DEFAULT_CAPACITY).await +} + +pub async fn drive_with_frames_with_capacity_mut( + app: &mut WireframeApp, + frames: Vec>, + capacity: usize, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, +{ + let (mut client, server) = duplex(capacity); + + let server_fut = app.handle_connection(server); + let client_fut = async { + for frame in &frames { + client.write_all(frame).await?; + } + client.shutdown().await?; + + let mut buf = Vec::new(); + client.read_to_end(&mut buf).await?; + io::Result::Ok(buf) + }; + + let ((), buf) = tokio::join!(server_fut, client_fut); + buf +} + pub async fn drive_with_bincode( app: WireframeApp, msg: M, diff --git a/wireframe_testing/src/lib.rs b/wireframe_testing/src/lib.rs index 4e876115..d581a67c 100644 --- a/wireframe_testing/src/lib.rs +++ b/wireframe_testing/src/lib.rs @@ -4,7 +4,9 @@ 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, }; From d7659b74deab1159a407c8e3fe337c408869aca5 Mon Sep 17 00:00:00 2001 From: Leynos Date: Mon, 23 Jun 2025 02:43:52 +0100 Subject: [PATCH 4/4] Address PR feedback --- tests/metadata.rs | 8 ++++++-- tests/routes.rs | 8 ++++++-- wireframe_testing/Cargo.toml | 6 +++--- wireframe_testing/src/helpers.rs | 16 +++++++++++++--- wireframe_testing/src/lib.rs | 15 +++++++++++++++ 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/tests/metadata.rs b/tests/metadata.rs index 81e86b58..de9c187d 100644 --- a/tests/metadata.rs +++ b/tests/metadata.rs @@ -58,7 +58,9 @@ async fn metadata_parser_invoked_before_deserialize() { let env = Envelope::new(1, vec![42]); - let out = drive_with_bincode(app, env).await.unwrap(); + let out = drive_with_bincode(app, env) + .await + .expect("drive_with_bincode failed"); assert!(!out.is_empty()); assert_eq!(counter.load(Ordering::SeqCst), 1); } @@ -101,7 +103,9 @@ async fn falls_back_to_deserialize_after_parse_error() { let env = Envelope::new(1, vec![7]); - let out = drive_with_bincode(app, env).await.unwrap(); + let out = drive_with_bincode(app, env) + .await + .expect("drive_with_bincode failed"); assert!(!out.is_empty()); assert_eq!(parse_calls.load(Ordering::SeqCst), 1); assert_eq!(deser_calls.load(Ordering::SeqCst), 1); diff --git a/tests/routes.rs b/tests/routes.rs index 31ff2164..1a1453b4 100644 --- a/tests/routes.rs +++ b/tests/routes.rs @@ -56,7 +56,9 @@ async fn handler_receives_message_and_echoes_response() { msg: msg_bytes, }; - let out = drive_with_bincode(app, env).await.unwrap(); + let out = drive_with_bincode(app, env) + .await + .expect("drive_with_bincode failed"); let mut buf = BytesMut::from(&out[..]); let frame = LengthPrefixedProcessor::default() @@ -98,7 +100,9 @@ async fn multiple_frames_processed_in_sequence() { }) .collect(); - let out = drive_with_frames(app, frames).await.unwrap(); + let out = drive_with_frames(app, frames) + .await + .expect("drive_with_frames failed"); let mut buf = BytesMut::from(&out[..]); let first = LengthPrefixedProcessor::default() diff --git a/wireframe_testing/Cargo.toml b/wireframe_testing/Cargo.toml index e97cdc9f..de9f5b66 100644 --- a/wireframe_testing/Cargo.toml +++ b/wireframe_testing/Cargo.toml @@ -6,8 +6,8 @@ edition = "2024" [dependencies] tokio = { version = "1", features = ["macros", "rt", "io-util"] } wireframe = { path = ".." } -bincode = "2" -bytes = "1" +bincode = "^2.0" +bytes = "^1.0" [dev-dependencies] -rstest = "0.18" +rstest = "0.18.2" diff --git a/wireframe_testing/src/helpers.rs b/wireframe_testing/src/helpers.rs index d28c0c11..8a754068 100644 --- a/wireframe_testing/src/helpers.rs +++ b/wireframe_testing/src/helpers.rs @@ -78,8 +78,13 @@ where let mut buf = Vec::new(); client.read_to_end(&mut buf).await?; - server_task.await.unwrap(); - Ok(buf) + match server_task.await { + Ok(_) => Ok(buf), + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("server task failed: {e}"), + )), + } } pub async fn drive_with_frame_mut( @@ -157,7 +162,12 @@ where C: Send + 'static, E: Packet, { - let bytes = bincode::encode_to_vec(msg, config::standard()).unwrap(); + let bytes = bincode::encode_to_vec(msg, config::standard()).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("bincode encode failed: {e}"), + ) + })?; let mut framed = BytesMut::new(); LengthPrefixedProcessor::default().encode(&bytes, &mut framed)?; drive_with_frame(app, framed.to_vec()).await diff --git a/wireframe_testing/src/lib.rs b/wireframe_testing/src/lib.rs index d581a67c..bec3748b 100644 --- a/wireframe_testing/src/lib.rs +++ b/wireframe_testing/src/lib.rs @@ -1,3 +1,18 @@ +//! Utilities for driving a [`WireframeApp`](wireframe::app::WireframeApp) +//! with in-memory streams during tests. +//! +//! These helpers spawn the application on a `tokio::io::duplex` stream and +//! return all bytes written by the app for easy assertions. +//! +//! ```rust +//! use wireframe::app::WireframeApp; +//! use wireframe_testing::drive_with_bincode; +//! +//! # async fn example(app: WireframeApp<_, _, ()>) { +//! let bytes = drive_with_bincode(app, 42u8).await.unwrap(); +//! # } +//! ``` + pub mod helpers; pub use helpers::{