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
27 changes: 27 additions & 0 deletions docs/adr-004-pluggable-protocol-codecs.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,33 @@ Roadmap item 9.6.1 is now covered by dedicated criterion benchmark targets:
The benchmark harness remains internal: no library runtime API changes were
required to ship these measurements.

### Codec-aware test harness drivers (resolved 2026-02-26)

Roadmap item 9.7.1 extended `wireframe_testing` with codec-aware driver
functions that accept any `FrameCodec` and handle frame encoding/decoding
transparently:

- **Payload-level drivers** (`drive_with_codec_payloads` and variants) accept
raw payload byte vectors, encode them through the codec, drive the app, and
return decoded payload bytes. This covers the common case where tests care
only about payload content.
- **Frame-level drivers** (`drive_with_codec_frames` and variants) follow the
same flow but return decoded `F::Frame` values, enabling tests to inspect
codec-specific metadata (e.g. transaction identifiers, sequence numbers).
- **Codec helpers** (`encode_payloads_with_codec`, `decode_frames_with_codec`,
`extract_payloads`) provide composable building blocks for custom test
patterns.

Design choices:

- The codec is passed explicitly as a `&F` parameter rather than extracted from
the app. This avoids coupling to `WireframeApp` field visibility and mirrors
the existing `payloads.rs` pattern.
- No new trait is introduced; the existing `TestSerializer` bound captures
serializer constraints while `F: FrameCodec` is added as an orthogonal
generic parameter.
- A `codec()` accessor was added to `WireframeApp` for convenience.

## Architectural Rationale

A dedicated `FrameCodec` abstraction aligns framing with the protocol boundary
Expand Down
588 changes: 588 additions & 0 deletions docs/execplans/9-7-1-codec-aware-wireframe-testing.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ integration boundaries.

### 9.7. Codec test harness and observability

- [ ] 9.7.1. Extend `wireframe_testing` with codec-aware drivers that can run
- [x] 9.7.1. Extend `wireframe_testing` with codec-aware drivers that can run
`WireframeApp` instances configured with custom `FrameCodec` values.
- [ ] 9.7.2. Add codec fixtures in `wireframe_testing` for generating valid and
invalid frames, including oversized payloads and correlation metadata.
Expand Down
53 changes: 53 additions & 0 deletions docs/users-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,59 @@ let app = WireframeApp::new()?
See `examples/hotline_codec.rs` and `examples/mysql_codec.rs` for complete
implementations.

#### Codec accessor

Retrieve the configured codec from a `WireframeApp` instance:

```rust,no_run
use wireframe::app::WireframeApp;
use wireframe::codec::examples::HotlineFrameCodec;

let codec = HotlineFrameCodec::new(4096);
let app = WireframeApp::new()?.with_codec(codec);
let codec_ref = app.codec(); // &HotlineFrameCodec
```

#### Testing custom codecs with `wireframe_testing`

The `wireframe_testing` crate provides codec-aware driver functions that handle
frame encoding and decoding transparently:

```rust,no_run
use wireframe::app::WireframeApp;
use wireframe::codec::examples::HotlineFrameCodec;
use wireframe_testing::{drive_with_codec_payloads, drive_with_codec_frames};

let codec = HotlineFrameCodec::new(4096);
let payload: Vec<u8> = vec![0x01, 0x02, 0x03];

// Payload-level: returns decoded response payloads as byte vectors.
let app = WireframeApp::new()?.with_codec(codec.clone());
let payloads =
drive_with_codec_payloads(app, &codec, vec![payload.clone()]).await?;

// Frame-level: returns decoded codec frames for metadata inspection.
let app = WireframeApp::new()?.with_codec(codec.clone());
let frames =
drive_with_codec_frames(app, &codec, vec![payload]).await?;
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Available codec-aware driver functions:

- `drive_with_codec_payloads` / `drive_with_codec_payloads_with_capacity` —
owned app, returns payload bytes.
- `drive_with_codec_payloads_mut` /
`drive_with_codec_payloads_with_capacity_mut` — mutable app reference,
returns payload bytes.
- `drive_with_codec_frames` / `drive_with_codec_frames_with_capacity` — owned
app, returns decoded `F::Frame` values.

Supporting helpers for composing custom test patterns:

- `encode_payloads_with_codec` — encode payloads to wire bytes.
- `decode_frames_with_codec` — decode wire bytes to frames.
- `extract_payloads` — extract payload bytes from decoded frames.

#### Zero-copy payload extraction

For performance-critical codecs, use `Bytes` instead of `Vec<u8>` for payload
Expand Down
3 changes: 3 additions & 0 deletions src/app/builder/codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ where
E: Packet,
F: FrameCodec,
{
/// Return a reference to the configured frame codec.
pub fn codec(&self) -> &F { &self.codec }

/// Replace the frame codec used for framing I/O.
///
/// This resets any installed protocol hooks because the frame type may
Expand Down
161 changes: 161 additions & 0 deletions tests/codec_test_harness.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//! Integration tests for the codec-aware test harness drivers in
//! `wireframe_testing`.
#![cfg(not(loom))]

use std::{io, sync::Arc};

use bytes::Bytes;
use futures::future::BoxFuture;
use wireframe::{
app::{Envelope, WireframeApp},
codec::examples::{HotlineFrame, HotlineFrameCodec},
serializer::{BincodeSerializer, Serializer},
};
use wireframe_testing::{
decode_frames_with_codec,
drive_with_codec_frames,
drive_with_codec_payloads,
drive_with_codec_payloads_mut,
encode_payloads_with_codec,
extract_payloads,
};

fn hotline_codec() -> HotlineFrameCodec { HotlineFrameCodec::new(4096) }

fn build_echo_app(
codec: HotlineFrameCodec,
) -> io::Result<WireframeApp<BincodeSerializer, (), Envelope, HotlineFrameCodec>> {
WireframeApp::<BincodeSerializer, (), Envelope>::new()
.map_err(|e| io::Error::other(format!("app init: {e}")))?
.with_codec(codec)
.route(
1,
Arc::new(|_: &Envelope| -> BoxFuture<'static, ()> { Box::pin(async {}) }),
)
.map_err(|e| io::Error::other(format!("route: {e}")))
}

fn serialize_envelope(payload: &[u8]) -> io::Result<Vec<u8>> {
let env = Envelope::new(1, Some(7), payload.to_vec());
BincodeSerializer
.serialize(&env)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("serialize: {e}")))
}

#[test]
fn encode_payloads_with_codec_produces_decodable_frames() -> io::Result<()> {
let codec = hotline_codec();
let payloads = vec![vec![1, 2, 3], vec![4, 5]];
let encoded = encode_payloads_with_codec(&codec, payloads.clone())?;

let wire: Vec<u8> = encoded.into_iter().flatten().collect();
let frames = decode_frames_with_codec(&codec, wire)?;

let extracted = extract_payloads::<HotlineFrameCodec>(&frames);
if extracted != payloads {
return Err(io::Error::other("round-trip payloads must match"));
}
Ok(())
}

#[test]
fn decode_frames_with_codec_handles_empty_input() -> io::Result<()> {
let codec = hotline_codec();
let frames = decode_frames_with_codec(&codec, vec![])?;
if !frames.is_empty() {
return Err(io::Error::other("empty input should produce no frames"));
}
Ok(())
}

#[test]
fn extract_payloads_returns_payload_bytes() {
let frames = vec![
HotlineFrame {
transaction_id: 1,
payload: Bytes::from_static(b"hello"),
},
HotlineFrame {
transaction_id: 2,
payload: Bytes::from_static(b"world"),
},
];
let payloads = extract_payloads::<HotlineFrameCodec>(&frames);
assert_eq!(payloads, vec![b"hello".to_vec(), b"world".to_vec()]);
}

#[tokio::test]
async fn drive_with_codec_payloads_round_trips_through_echo_app() -> io::Result<()> {
let codec = hotline_codec();
let app = build_echo_app(codec.clone())?;

let payload = vec![10, 20, 30];
let serialized = serialize_envelope(&payload)?;

let response_payloads = drive_with_codec_payloads(app, &codec, vec![serialized]).await?;

if response_payloads.len() != 1 {
return Err(io::Error::other(format!(
"expected one response payload, got {}",
response_payloads.len()
)));
}
let first = response_payloads
.first()
.ok_or_else(|| io::Error::other("missing response payload"))?;
let (decoded, _) = BincodeSerializer
.deserialize::<Envelope>(first)
.map_err(|e| io::Error::other(format!("deserialize: {e}")))?;
if decoded.payload_bytes() != payload.as_slice() {
return Err(io::Error::other("payload mismatch"));
}
Ok(())
}

#[tokio::test]
async fn drive_with_codec_frames_preserves_codec_metadata() -> io::Result<()> {
let codec = hotline_codec();
let app = build_echo_app(codec.clone())?;

let serialized = serialize_envelope(&[42])?;

let frames = drive_with_codec_frames(app, &codec, vec![serialized]).await?;

if frames.len() != 1 {
return Err(io::Error::other(format!(
"expected one response frame, got {}",
frames.len()
)));
}
let frame = frames
.first()
.ok_or_else(|| io::Error::other("missing response frame"))?;
// wrap_payload assigns transaction_id 0, confirming codec metadata flows
// through the driver pipeline.
if frame.transaction_id != 0 {
return Err(io::Error::other(format!(
"wrap_payload should assign transaction_id 0, got {}",
frame.transaction_id
)));
}
Ok(())
}

#[tokio::test]
async fn drive_with_codec_payloads_mut_allows_app_reuse() -> io::Result<()> {
let codec = hotline_codec();
let mut app = build_echo_app(codec.clone())?;

let serialized = serialize_envelope(&[1])?;

let first = drive_with_codec_payloads_mut(&mut app, &codec, vec![serialized.clone()]).await?;
if first.is_empty() {
return Err(io::Error::other("first call should produce output"));
}

let second = drive_with_codec_payloads_mut(&mut app, &codec, vec![serialized]).await?;
if second.is_empty() {
return Err(io::Error::other("second call should produce output"));
}
Ok(())
}
14 changes: 14 additions & 0 deletions tests/features/codec_test_harness.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Feature: Codec-aware test harness drivers
The wireframe_testing crate provides codec-aware driver functions
that handle frame encoding and decoding transparently for any
FrameCodec implementation.

Scenario: Payload round-trip through a custom codec driver
Given a wireframe app configured with a Hotline codec allowing frames up to 4096 bytes
When a test payload is driven through the codec-aware payload driver
Then the response payloads are non-empty

Scenario: Frame-level driver preserves codec metadata
Given a wireframe app configured with a Hotline codec allowing frames up to 4096 bytes
When a test payload is driven through the codec-aware frame driver
Then the response frames contain valid transaction identifiers
Loading
Loading