Skip to content

Commit e5f08b6

Browse files
Add Hotline codec fixtures and tests for wireframe_testing (#487)
* feat(wireframe_testing): add codec fixtures for Hotline protocol testing Introduce a new module providing fixture functions that generate valid, invalid, truncated, and correlated Hotline-framed wire bytes for use in tests. These fixtures allow test authors to easily produce raw wire data for codec exercise, including error conditions that are hard to manually craft. Includes: - Valid frames with specified payloads and transaction IDs - Oversized payload frames that trigger decoder errors - Mismatched total_size frames to test error handling - Truncated headers and payloads producing decode errors - Multi-frame sequences sharing or incrementing transaction IDs Supports comprehensive unit and BDD integration tests verifying correct codec behaviours when decoding these fixtures. Also adds documentation updates and marks roadmap item 9.7.2 as done. Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com> * refactor(codec-fixtures): introduce typed wrappers and helper functions - Added `TransactionId`, `MaxFrameLength`, and `PayloadLength` newtypes for hotline frame parameters for improved type safety and clarity. - Updated fixture functions to accept these newtypes instead of raw primitives. - Refactored decode test helpers to use assertion helper functions simplifying test code and error validation. - Cleaned up decoding tests by consolidating repeated error string checks into reusable assertions. - Updated exports to include new helper types. - Improved documentation and consistency in wireframe testing helpers. These changes consolidate frame codec fixture logic, improve readability, and ease future maintenance and extension of tests and helpers. Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com> * test(codec_fixtures): refactor frame transaction ID checks into helper function Introduced `assert_transaction_ids` helper to verify transaction IDs in frames. Replaced repetitive individual frame ID assertions with this helper in existing tests for improved code clarity and reuse. Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com> * refactor(tests,codec-fixtures): consolidate codec fixture decoding logic Refactored the codec fixture decoding methods in tests/fixtures/codec_fixtures.rs to delegate to a single helper method decode_fixture, reducing code duplication and improving clarity. Also fixed frame count assertion in tests/codec_fixtures.rs by using assert_frame_count helper. Minor fixes include using wrapping_add for transaction ID calculation in wireframe_testing helpers and a typo correction in docs. This change improves maintainability without altering functionality. Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com> * refactor(codec-fixtures, wireframe-testing): generalize fixture helpers and improve test state reset - Changed multiple fixture helper function parameters to accept `impl Into<T>` for flexibility. - Updated decoding fixture method to clear previous frames and errors before decoding. - Adjusted documentation and comments for clarity, including error messages wording in scenarios. - Ensured truncated payload fixture always claims at least 1 byte to guarantee truncation. These improvements enhance API ergonomics and test reliability. Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com> * docs(codec_fixtures): update codec_fixtures docs for API and error handling Revise documentation in codec_fixtures to reflect changes in error handling, parameter types (using Into traits), and fixture API descriptions. Clarify behavior on truncated frame decoding and error reporting via decode_eof. Improve explanations of fixture functions and update terminology for error paths to enhance clarity and accuracy in the docs. Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com> --------- Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
1 parent 859f2b9 commit e5f08b6

File tree

15 files changed

+1441
-1
lines changed

15 files changed

+1441
-1
lines changed

docs/adr-004-pluggable-protocol-codecs.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,29 @@ Design choices:
367367
generic parameter.
368368
- A `codec()` accessor was added to `WireframeApp` for convenience.
369369

370+
### Codec test fixtures (resolved 2026-02-28)
371+
372+
Roadmap item 9.7.2 added codec fixture functions to `wireframe_testing` for
373+
generating valid, invalid, incomplete, and correlation-bearing Hotline-framed
374+
wire bytes. These fixtures support test authors exercising error paths without
375+
hand-crafting raw byte sequences.
376+
377+
Design choices:
378+
379+
- Fixtures produce raw `Vec<u8>` wire bytes rather than typed `HotlineFrame`
380+
values. Invalid and malformed frames cannot be represented as valid typed
381+
values, and wire bytes are what decoders and test drivers consume directly. A
382+
`valid_hotline_frame` convenience function is also provided for tests needing
383+
metadata inspection without a wire round-trip.
384+
- Fixtures are Hotline-specific rather than generic over `FrameCodec`.
385+
Generating invalid frames requires knowledge of the specific wire format
386+
(header layout, field positions, size constraints). A generic approach would
387+
need a `MalformedFrameGenerator` trait, which is over-engineering for a test
388+
utility. If additional codecs need fixtures, they can follow the same pattern.
389+
- Fixtures construct headers directly using big-endian `u32` writes, bypassing
390+
the tokio-util encoder. This ensures fixtures are independent of encoder
391+
implementation and can represent data the encoder would reject.
392+
370393
## Architectural Rationale
371394

372395
A dedicated `FrameCodec` abstraction aligns framing with the protocol boundary

docs/execplans/9-7-2-codec-fixtures-in-wireframe-testing.md

Lines changed: 582 additions & 0 deletions
Large diffs are not rendered by default.

docs/roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ integration boundaries.
432432

433433
- [x] 9.7.1. Extend `wireframe_testing` with codec-aware drivers that can run
434434
`WireframeApp` instances configured with custom `FrameCodec` values.
435-
- [ ] 9.7.2. Add codec fixtures in `wireframe_testing` for generating valid and
435+
- [x] 9.7.2. Add codec fixtures in `wireframe_testing` for generating valid and
436436
invalid frames, including oversized payloads and correlation metadata.
437437
- [ ] 9.7.3. Introduce a test observability harness in `wireframe_testing` that
438438
captures logs and metrics per test run for asserting codec failures and

docs/users-guide.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,50 @@ Supporting helpers for composing custom test patterns:
261261
- `decode_frames_with_codec` — decode wire bytes to frames.
262262
- `extract_payloads` — extract payload bytes from decoded frames.
263263

264+
#### Codec test fixtures
265+
266+
The `wireframe_testing` crate provides fixture functions for generating
267+
Hotline-framed wire bytes covering common test scenarios — valid frames,
268+
invalid frames, incomplete (truncated) frames, and frames with correlation
269+
metadata. These fixtures construct raw bytes directly, so they can represent
270+
malformed data that the encoder would reject:
271+
272+
```rust,no_run
273+
use wireframe::codec::examples::HotlineFrameCodec;
274+
use wireframe_testing::{
275+
valid_hotline_wire, oversized_hotline_wire,
276+
truncated_hotline_header, correlated_hotline_wire,
277+
decode_frames_with_codec,
278+
};
279+
280+
let codec = HotlineFrameCodec::new(4096);
281+
282+
// Valid frame — decodes cleanly.
283+
let wire = valid_hotline_wire(b"hello", 7);
284+
let frames = decode_frames_with_codec(&codec, wire).unwrap();
285+
286+
// Oversized frame — rejected with "payload too large".
287+
let wire = oversized_hotline_wire(4096);
288+
assert!(decode_frames_with_codec(&codec, wire).is_err());
289+
290+
// Truncated header — rejected with "bytes remaining on stream".
291+
let wire = truncated_hotline_header();
292+
assert!(decode_frames_with_codec(&codec, wire).is_err());
293+
294+
// Correlated frames — all share the same transaction ID.
295+
let wire = correlated_hotline_wire(42, &[b"a", b"b"]);
296+
let frames = decode_frames_with_codec(&codec, wire).unwrap();
297+
```
298+
299+
Available fixture functions:
300+
301+
- `valid_hotline_wire` / `valid_hotline_frame` — well-formed frames.
302+
- `oversized_hotline_wire` — payload exceeds `max_frame_length`.
303+
- `mismatched_total_size_wire` — header with incorrect `total_size`.
304+
- `truncated_hotline_header` / `truncated_hotline_payload` — incomplete data.
305+
- `correlated_hotline_wire` — frames sharing a transaction ID.
306+
- `sequential_hotline_wire` — frames with incrementing transaction IDs.
307+
264308
#### Zero-copy payload extraction
265309

266310
For performance-critical codecs, use `Bytes` instead of `Vec<u8>` for payload

tests/codec_fixtures.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
//! Integration tests for the codec fixture functions in `wireframe_testing`.
2+
//!
3+
//! These tests verify that each fixture category produces wire bytes with the
4+
//! expected decoding behaviour when used with `HotlineFrameCodec`.
5+
#![cfg(not(loom))]
6+
7+
use std::io;
8+
9+
use wireframe::codec::examples::HotlineFrameCodec;
10+
use wireframe_testing::{
11+
correlated_hotline_wire,
12+
decode_frames_with_codec,
13+
mismatched_total_size_wire,
14+
oversized_hotline_wire,
15+
sequential_hotline_wire,
16+
truncated_hotline_header,
17+
truncated_hotline_payload,
18+
valid_hotline_frame,
19+
valid_hotline_wire,
20+
};
21+
22+
fn hotline_codec() -> HotlineFrameCodec { HotlineFrameCodec::new(4096) }
23+
24+
/// Decode `wire` with a fresh `HotlineFrameCodec` and verify that decoding
25+
/// fails with an error message containing `expected_error_substring`.
26+
fn assert_decode_fails_with(wire: Vec<u8>, expected_error_substring: &str) -> io::Result<()> {
27+
let codec = hotline_codec();
28+
let result = decode_frames_with_codec(&codec, wire);
29+
30+
let err = result
31+
.err()
32+
.ok_or_else(|| io::Error::other("expected decode to fail but it succeeded"))?;
33+
if !err.to_string().contains(expected_error_substring) {
34+
return Err(io::Error::other(format!(
35+
"expected error containing '{expected_error_substring}', got: {err}"
36+
)));
37+
}
38+
Ok(())
39+
}
40+
41+
/// Assert that `frames` contains exactly `expected` elements.
42+
fn assert_frame_count(
43+
frames: &[wireframe::codec::examples::HotlineFrame],
44+
expected: usize,
45+
) -> io::Result<()> {
46+
if frames.len() != expected {
47+
return Err(io::Error::other(format!(
48+
"expected {expected} frame(s), got {}",
49+
frames.len()
50+
)));
51+
}
52+
Ok(())
53+
}
54+
55+
// ── Valid frame fixtures ────────────────────────────────────────────────
56+
57+
#[test]
58+
fn valid_hotline_wire_decodes_successfully() -> io::Result<()> {
59+
let wire = valid_hotline_wire(b"hello", 7);
60+
let codec = hotline_codec();
61+
let frames = decode_frames_with_codec(&codec, wire)?;
62+
63+
assert_frame_count(&frames, 1)?;
64+
65+
let frame = frames
66+
.first()
67+
.ok_or_else(|| io::Error::other("expected one decoded frame"))?;
68+
69+
if frame.transaction_id != 7 {
70+
return Err(io::Error::other(format!(
71+
"expected transaction_id 7, got {}",
72+
frame.transaction_id
73+
)));
74+
}
75+
if frame.payload.as_ref() != b"hello" {
76+
return Err(io::Error::other("payload mismatch"));
77+
}
78+
Ok(())
79+
}
80+
81+
#[test]
82+
fn valid_hotline_frame_has_correct_metadata() {
83+
let frame = valid_hotline_frame(b"data", 42);
84+
assert_eq!(frame.transaction_id, 42);
85+
assert_eq!(frame.payload.as_ref(), b"data");
86+
}
87+
88+
// ── Invalid frame fixtures ──────────────────────────────────────────────
89+
90+
#[test]
91+
fn oversized_hotline_wire_rejected_by_decoder() -> io::Result<()> {
92+
let wire = oversized_hotline_wire(4096);
93+
assert_decode_fails_with(wire, "payload too large")
94+
}
95+
96+
#[test]
97+
fn mismatched_total_size_rejected_by_decoder() -> io::Result<()> {
98+
let wire = mismatched_total_size_wire(b"test");
99+
assert_decode_fails_with(wire, "invalid total size")
100+
}
101+
102+
// ── Incomplete frame fixtures ───────────────────────────────────────────
103+
104+
#[test]
105+
fn truncated_header_produces_decode_error() -> io::Result<()> {
106+
let wire = truncated_hotline_header();
107+
assert_decode_fails_with(wire, "bytes remaining")
108+
}
109+
110+
#[test]
111+
fn truncated_payload_produces_decode_error() -> io::Result<()> {
112+
let wire = truncated_hotline_payload(100);
113+
assert_decode_fails_with(wire, "bytes remaining")
114+
}
115+
116+
/// Verify each frame carries the expected transaction ID.
117+
fn assert_transaction_ids(
118+
frames: &[wireframe::codec::examples::HotlineFrame],
119+
expected_ids: &[u32],
120+
) -> io::Result<()> {
121+
assert_frame_count(frames, expected_ids.len())?;
122+
for (i, (frame, expected_id)) in frames.iter().zip(expected_ids.iter()).enumerate() {
123+
if frame.transaction_id != *expected_id {
124+
return Err(io::Error::other(format!(
125+
"frame {i}: expected transaction_id {expected_id}, got {}",
126+
frame.transaction_id
127+
)));
128+
}
129+
}
130+
Ok(())
131+
}
132+
133+
// ── Correlation metadata fixtures ───────────────────────────────────────
134+
135+
#[test]
136+
fn correlated_frames_share_transaction_id() -> io::Result<()> {
137+
let wire = correlated_hotline_wire(42, &[b"a", b"b", b"c"]);
138+
let codec = hotline_codec();
139+
let frames = decode_frames_with_codec(&codec, wire)?;
140+
141+
assert_frame_count(&frames, 3)?;
142+
assert_transaction_ids(&frames, &[42, 42, 42])
143+
}
144+
145+
#[test]
146+
fn sequential_frames_have_incrementing_ids() -> io::Result<()> {
147+
let wire = sequential_hotline_wire(10, &[b"x", b"y", b"z"]);
148+
let codec = hotline_codec();
149+
let frames = decode_frames_with_codec(&codec, wire)?;
150+
151+
assert_frame_count(&frames, 3)?;
152+
assert_transaction_ids(&frames, &[10, 11, 12])
153+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
Feature: Codec test fixtures
2+
The wireframe_testing crate provides codec fixture functions for
3+
generating valid and invalid Hotline-framed wire bytes for testing.
4+
5+
Scenario: Valid fixture decodes to expected payload
6+
Given a Hotline codec allowing fixtures up to 4096 bytes
7+
When a valid fixture frame is decoded
8+
Then the decoded payload matches the fixture input
9+
10+
Scenario: Oversized fixture is rejected by decoder
11+
Given a Hotline codec allowing fixtures up to 4096 bytes
12+
When an oversized fixture frame is decoded
13+
Then the decoder reports an invalid data error
14+
15+
Scenario: Truncated fixture produces a decode error
16+
Given a Hotline codec allowing fixtures up to 4096 bytes
17+
When a truncated fixture frame is decoded
18+
Then the decoder reports bytes remaining on stream
19+
20+
Scenario: Correlated fixtures share the same transaction identifier
21+
Given a Hotline codec allowing fixtures up to 4096 bytes
22+
When correlated fixture frames are decoded
23+
Then all frames have the expected transaction identifier

0 commit comments

Comments
 (0)