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
39 changes: 19 additions & 20 deletions docs/wireframe-testing-crate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions tests/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<R, A>(
counter: &Arc<AtomicUsize>,
Expand Down Expand Up @@ -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());
Expand Down
116 changes: 35 additions & 81 deletions wireframe_testing/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ where
}

const DEFAULT_CAPACITY: usize = 4096;
const MAX_CAPACITY: usize = 1024 * 1024 * 10; // 10MB limit

macro_rules! forward_default {
(
Expand Down Expand Up @@ -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<S, C, E>, frame: Vec<u8>) -> io::Result<Vec<u8>>
=> 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<S, C, E>, frame: Vec<u8>, capacity: usize) -> io::Result<Vec<u8>>
=> 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<S, C, E>, frames: Vec<Vec<u8>>) -> io::Result<Vec<u8>>
=> 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<S, C, E>(
pub async fn run_app<S, C, E>(
app: WireframeApp<S, C, E>,
frames: Vec<Vec<u8>>,
capacity: usize,
capacity: Option<usize>,
) -> io::Result<Vec<u8>>
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 });
Comment thread
leynos marked this conversation as resolved.

for frame in &frames {
client.write_all(frame).await?;
Expand All @@ -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)
}

Expand Down
5 changes: 1 addition & 4 deletions wireframe_testing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Loading