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
23 changes: 23 additions & 0 deletions docs/adr-004-pluggable-protocol-codecs.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,29 @@ Design choices:
generic parameter.
- A `codec()` accessor was added to `WireframeApp` for convenience.

### Codec test fixtures (resolved 2026-02-28)

Roadmap item 9.7.2 added codec fixture functions to `wireframe_testing` for
generating valid, invalid, incomplete, and correlation-bearing Hotline-framed
wire bytes. These fixtures support test authors exercising error paths without
hand-crafting raw byte sequences.

Design choices:

- Fixtures produce raw `Vec<u8>` wire bytes rather than typed `HotlineFrame`
values. Invalid and malformed frames cannot be represented as valid typed
values, and wire bytes are what decoders and test drivers consume directly. A
`valid_hotline_frame` convenience function is also provided for tests needing
metadata inspection without a wire round-trip.
- Fixtures are Hotline-specific rather than generic over `FrameCodec`.
Generating invalid frames requires knowledge of the specific wire format
(header layout, field positions, size constraints). A generic approach would
need a `MalformedFrameGenerator` trait, which is over-engineering for a test
utility. If additional codecs need fixtures, they can follow the same pattern.
- Fixtures construct headers directly using big-endian `u32` writes, bypassing
the tokio-util encoder. This ensures fixtures are independent of encoder
implementation and can represent data the encoder would reject.

## Architectural Rationale

A dedicated `FrameCodec` abstraction aligns framing with the protocol boundary
Expand Down
582 changes: 582 additions & 0 deletions docs/execplans/9-7-2-codec-fixtures-in-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 @@ -432,7 +432,7 @@ integration boundaries.

- [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
- [x] 9.7.2. Add codec fixtures in `wireframe_testing` for generating valid and
invalid frames, including oversized payloads and correlation metadata.
- [ ] 9.7.3. Introduce a test observability harness in `wireframe_testing` that
captures logs and metrics per test run for asserting codec failures and
Expand Down
44 changes: 44 additions & 0 deletions docs/users-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,50 @@ Supporting helpers for composing custom test patterns:
- `decode_frames_with_codec` — decode wire bytes to frames.
- `extract_payloads` — extract payload bytes from decoded frames.

#### Codec test fixtures

The `wireframe_testing` crate provides fixture functions for generating
Hotline-framed wire bytes covering common test scenarios — valid frames,
invalid frames, incomplete (truncated) frames, and frames with correlation
metadata. These fixtures construct raw bytes directly, so they can represent
malformed data that the encoder would reject:

```rust,no_run
use wireframe::codec::examples::HotlineFrameCodec;
use wireframe_testing::{
valid_hotline_wire, oversized_hotline_wire,
truncated_hotline_header, correlated_hotline_wire,
decode_frames_with_codec,
};

let codec = HotlineFrameCodec::new(4096);

// Valid frame — decodes cleanly.
let wire = valid_hotline_wire(b"hello", 7);
let frames = decode_frames_with_codec(&codec, wire).unwrap();

// Oversized frame — rejected with "payload too large".
let wire = oversized_hotline_wire(4096);
assert!(decode_frames_with_codec(&codec, wire).is_err());

// Truncated header — rejected with "bytes remaining on stream".
let wire = truncated_hotline_header();
assert!(decode_frames_with_codec(&codec, wire).is_err());

// Correlated frames — all share the same transaction ID.
let wire = correlated_hotline_wire(42, &[b"a", b"b"]);
let frames = decode_frames_with_codec(&codec, wire).unwrap();
```

Available fixture functions:

- `valid_hotline_wire` / `valid_hotline_frame` — well-formed frames.
- `oversized_hotline_wire` — payload exceeds `max_frame_length`.
- `mismatched_total_size_wire` — header with incorrect `total_size`.
- `truncated_hotline_header` / `truncated_hotline_payload` — incomplete data.
- `correlated_hotline_wire` — frames sharing a transaction ID.
- `sequential_hotline_wire` — frames with incrementing transaction IDs.

#### Zero-copy payload extraction

For performance-critical codecs, use `Bytes` instead of `Vec<u8>` for payload
Expand Down
153 changes: 153 additions & 0 deletions tests/codec_fixtures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! Integration tests for the codec fixture functions in `wireframe_testing`.
//!
//! These tests verify that each fixture category produces wire bytes with the
//! expected decoding behaviour when used with `HotlineFrameCodec`.
#![cfg(not(loom))]

use std::io;

use wireframe::codec::examples::HotlineFrameCodec;
use wireframe_testing::{
correlated_hotline_wire,
decode_frames_with_codec,
mismatched_total_size_wire,
oversized_hotline_wire,
sequential_hotline_wire,
truncated_hotline_header,
truncated_hotline_payload,
valid_hotline_frame,
valid_hotline_wire,
};

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

/// Decode `wire` with a fresh `HotlineFrameCodec` and verify that decoding
/// fails with an error message containing `expected_error_substring`.
fn assert_decode_fails_with(wire: Vec<u8>, expected_error_substring: &str) -> io::Result<()> {
let codec = hotline_codec();
let result = decode_frames_with_codec(&codec, wire);

let err = result
.err()
.ok_or_else(|| io::Error::other("expected decode to fail but it succeeded"))?;
if !err.to_string().contains(expected_error_substring) {
return Err(io::Error::other(format!(
"expected error containing '{expected_error_substring}', got: {err}"
)));
}
Ok(())
}

/// Assert that `frames` contains exactly `expected` elements.
fn assert_frame_count(
frames: &[wireframe::codec::examples::HotlineFrame],
expected: usize,
) -> io::Result<()> {
if frames.len() != expected {
return Err(io::Error::other(format!(
"expected {expected} frame(s), got {}",
frames.len()
)));
}
Ok(())
}

// ── Valid frame fixtures ────────────────────────────────────────────────

#[test]
fn valid_hotline_wire_decodes_successfully() -> io::Result<()> {
let wire = valid_hotline_wire(b"hello", 7);
let codec = hotline_codec();
let frames = decode_frames_with_codec(&codec, wire)?;

assert_frame_count(&frames, 1)?;

let frame = frames
.first()
.ok_or_else(|| io::Error::other("expected one decoded frame"))?;

if frame.transaction_id != 7 {
return Err(io::Error::other(format!(
"expected transaction_id 7, got {}",
frame.transaction_id
)));
}
if frame.payload.as_ref() != b"hello" {
return Err(io::Error::other("payload mismatch"));
}
Ok(())
}

#[test]
fn valid_hotline_frame_has_correct_metadata() {
let frame = valid_hotline_frame(b"data", 42);
assert_eq!(frame.transaction_id, 42);
assert_eq!(frame.payload.as_ref(), b"data");
}

// ── Invalid frame fixtures ──────────────────────────────────────────────

#[test]
fn oversized_hotline_wire_rejected_by_decoder() -> io::Result<()> {
let wire = oversized_hotline_wire(4096);
assert_decode_fails_with(wire, "payload too large")
}

#[test]
fn mismatched_total_size_rejected_by_decoder() -> io::Result<()> {
let wire = mismatched_total_size_wire(b"test");
assert_decode_fails_with(wire, "invalid total size")
}

// ── Incomplete frame fixtures ───────────────────────────────────────────

#[test]
fn truncated_header_produces_decode_error() -> io::Result<()> {
let wire = truncated_hotline_header();
assert_decode_fails_with(wire, "bytes remaining")
}

#[test]
fn truncated_payload_produces_decode_error() -> io::Result<()> {
let wire = truncated_hotline_payload(100);
assert_decode_fails_with(wire, "bytes remaining")
}

/// Verify each frame carries the expected transaction ID.
fn assert_transaction_ids(
frames: &[wireframe::codec::examples::HotlineFrame],
expected_ids: &[u32],
) -> io::Result<()> {
assert_frame_count(frames, expected_ids.len())?;
for (i, (frame, expected_id)) in frames.iter().zip(expected_ids.iter()).enumerate() {
if frame.transaction_id != *expected_id {
return Err(io::Error::other(format!(
"frame {i}: expected transaction_id {expected_id}, got {}",
frame.transaction_id
)));
}
}
Ok(())
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// ── Correlation metadata fixtures ───────────────────────────────────────

#[test]
fn correlated_frames_share_transaction_id() -> io::Result<()> {
let wire = correlated_hotline_wire(42, &[b"a", b"b", b"c"]);
let codec = hotline_codec();
let frames = decode_frames_with_codec(&codec, wire)?;

assert_frame_count(&frames, 3)?;
assert_transaction_ids(&frames, &[42, 42, 42])
}

#[test]
fn sequential_frames_have_incrementing_ids() -> io::Result<()> {
let wire = sequential_hotline_wire(10, &[b"x", b"y", b"z"]);
let codec = hotline_codec();
let frames = decode_frames_with_codec(&codec, wire)?;

assert_frame_count(&frames, 3)?;
assert_transaction_ids(&frames, &[10, 11, 12])
}
23 changes: 23 additions & 0 deletions tests/features/codec_fixtures.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Feature: Codec test fixtures
The wireframe_testing crate provides codec fixture functions for
generating valid and invalid Hotline-framed wire bytes for testing.

Scenario: Valid fixture decodes to expected payload
Given a Hotline codec allowing fixtures up to 4096 bytes
When a valid fixture frame is decoded
Then the decoded payload matches the fixture input

Scenario: Oversized fixture is rejected by decoder
Given a Hotline codec allowing fixtures up to 4096 bytes
When an oversized fixture frame is decoded
Then the decoder reports an invalid data error

Scenario: Truncated fixture produces a decode error
Given a Hotline codec allowing fixtures up to 4096 bytes
When a truncated fixture frame is decoded
Then the decoder reports bytes remaining on stream

Scenario: Correlated fixtures share the same transaction identifier
Given a Hotline codec allowing fixtures up to 4096 bytes
When correlated fixture frames are decoded
Then all frames have the expected transaction identifier
Loading
Loading