From 027060722dee18fa3388143477388106a2cc1475 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 26 Feb 2026 13:15:49 +0000 Subject: [PATCH 1/3] feat(testing): add codec-aware test drivers and helpers to wireframe_testing - Introduce payload-level and frame-level codec-aware driver functions that handle encoding and decoding transparently for any FrameCodec. - Add generic encode/decode helper functions for FrameCodec implementations. - Provide a new codec() accessor on WireframeApp for convenient codec access. - Implement comprehensive unit and BDD tests for the new codec-aware testing APIs. - Document the new testing capabilities in users-guide, update ADR and roadmap accordingly. Co-authored-by: devboxerhub[bot] --- docs/adr-004-pluggable-protocol-codecs.md | 28 + .../9-7-1-codec-aware-wireframe-testing.md | 588 ++++++++++++++++++ docs/roadmap.md | 2 +- docs/users-guide.md | 48 ++ src/app/builder/codec.rs | 3 + tests/codec_test_harness.rs | 161 +++++ tests/features/codec_test_harness.feature | 14 + tests/fixtures/codec_test_harness.rs | 132 ++++ tests/fixtures/mod.rs | 1 + .../scenarios/codec_test_harness_scenarios.rs | 25 + tests/scenarios/mod.rs | 1 + tests/steps/codec_test_harness_steps.rs | 40 ++ tests/steps/mod.rs | 1 + wireframe_testing/Cargo.lock | 1 + wireframe_testing/src/helpers.rs | 11 + wireframe_testing/src/helpers/codec_drive.rs | 255 ++++++++ wireframe_testing/src/helpers/codec_ext.rs | 118 ++++ wireframe_testing/src/lib.rs | 9 + 18 files changed, 1437 insertions(+), 1 deletion(-) create mode 100644 docs/execplans/9-7-1-codec-aware-wireframe-testing.md create mode 100644 tests/codec_test_harness.rs create mode 100644 tests/features/codec_test_harness.feature create mode 100644 tests/fixtures/codec_test_harness.rs create mode 100644 tests/scenarios/codec_test_harness_scenarios.rs create mode 100644 tests/steps/codec_test_harness_steps.rs create mode 100644 wireframe_testing/src/helpers/codec_drive.rs create mode 100644 wireframe_testing/src/helpers/codec_ext.rs diff --git a/docs/adr-004-pluggable-protocol-codecs.md b/docs/adr-004-pluggable-protocol-codecs.md index fc71bf2d..954952b8 100644 --- a/docs/adr-004-pluggable-protocol-codecs.md +++ b/docs/adr-004-pluggable-protocol-codecs.md @@ -235,6 +235,34 @@ pub trait FrameCodec: Send + Sync + Clone + 'static { failures and recovery policies using the test observability harness once available.[^adr-006] +## Resolved decisions + +### Codec-aware test harness drivers (9.7.1) + +The `wireframe_testing` crate now provides 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. + ## Known Risks and Limitations - Associated types avoid RPITIT for `FrameCodec`, but the overall design still diff --git a/docs/execplans/9-7-1-codec-aware-wireframe-testing.md b/docs/execplans/9-7-1-codec-aware-wireframe-testing.md new file mode 100644 index 00000000..684b3b5e --- /dev/null +++ b/docs/execplans/9-7-1-codec-aware-wireframe-testing.md @@ -0,0 +1,588 @@ +# 9.7.1 Extend wireframe\_testing with codec-aware test drivers + +This ExecPlan (execution plan) is a living document. The sections +`Constraints`, `Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, +`Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work +proceeds. + +Status: COMPLETE + +## Purpose / big picture + +The `wireframe_testing` crate provides in-memory test drivers that spin up a +`WireframeApp` on a `tokio::io::duplex` stream and return the bytes written by +the app. Today every driver is hard-wired to the default +`LengthDelimitedFrameCodec`. A test author wanting to exercise a custom +`FrameCodec` (e.g. `HotlineFrameCodec`, `MysqlFrameCodec`, or any user-defined +codec) must manually create a duplex, spawn the app, encode frames with the +codec's encoder, collect raw output, and decode with the codec's decoder. This +is roughly 30 lines of boilerplate per test. + +After this change a test author can write: + +```rust +use wireframe::codec::examples::HotlineFrameCodec; +use wireframe_testing::drive_with_codec_payloads; + +let codec = HotlineFrameCodec::new(4096); +let app = WireframeApp::new()?.with_codec(codec.clone()); +let response_payloads = drive_with_codec_payloads( + app, &codec, vec![payload_bytes], +).await?; +``` + +The codec-aware drivers handle frame encoding, transport, and decoding +internally, returning decoded payload bytes (or, for the frame-level variant, +decoded `F::Frame` values for metadata inspection). + +Observable success: running `make test` passes, including new unit tests in +`wireframe_testing` and new rstest-bdd behavioural scenarios that exercise the +codec-aware drivers with `HotlineFrameCodec`. + +## Constraints + +- All existing `wireframe_testing` public APIs must remain source-compatible. + The new functions are purely additive. +- No file may exceed 400 lines. +- Use `rstest` for new unit tests and `rstest-bdd` v0.5.0 for new behavioural + tests. +- Use existing in-tree codecs (`HotlineFrameCodec` from + `wireframe::codec::examples`) rather than defining new codec types. +- en-GB-oxendict spelling in all comments and documentation. +- Strict Clippy with `-D warnings` on all targets including tests. +- Every module must begin with a `//!` doc comment. +- Public functions must have `///` doc comments with examples. +- Quality gates (`make check-fmt`, `make lint`, `make test`) must pass before + completion. + +## Tolerances (exception triggers) + +- Scope: if implementation requires more than 15 new/modified files or 1,200 + net changed lines, stop and re-scope. +- Dependencies: if any new external crate is required beyond what is already in + `wireframe_testing/Cargo.toml`, stop and escalate. +- Interface: if a public API signature on the main `wireframe` crate must + change beyond an additive accessor, stop and escalate. +- Iterations: if the same failing root cause persists after 3 fix attempts, + stop and document options in the Decision Log. + +## Risks + +- Risk: `drive_internal` is `pub(super)`, so a new sibling module + `codec_drive.rs` under `helpers/` can call it directly. If the module + hierarchy changes, this access path breaks. Severity: low. Likelihood: low. + Mitigation: the new module lives alongside `drive.rs` under `helpers/`, + matching the existing pattern. + +- Risk: the `FrameCodec` trait's `Encoder`/`Decoder` associated types are + opaque; codec-aware decode may fail on partial frames if the duplex buffer is + too small. Severity: medium. Likelihood: medium. Mitigation: default capacity + is 4096 bytes (matching existing helpers); document that callers should + increase capacity for large-frame codecs. + +- Risk: adding a `pub fn codec(&self) -> &F` accessor to `WireframeApp` is a + minor public API addition to the main crate. Severity: low. Likelihood: + certain. Mitigation: this is a non-breaking, additive accessor. It enables + convenient codec reuse in tests without requiring the caller to hold a + separate clone. Document in `docs/users-guide.md`. + +## Progress + +- [x] (2026-02-26) Drafted ExecPlan. +- [x] (2026-02-26) Stage A: add `codec()` accessor to `WireframeApp`. +- [x] (2026-02-26) Stage B: implement codec-aware encode/decode helpers in + `wireframe_testing`. +- [x] (2026-02-26) Stage C: implement codec-aware driver functions. +- [x] (2026-02-26) Stage D: add rstest unit tests for the new drivers. +- [x] (2026-02-26) Stage E: add rstest-bdd behavioural tests. +- [x] (2026-02-26) Stage F: documentation and roadmap updates. +- [x] (2026-02-26) Stage G: full validation and evidence capture. + +## Surprises & discoveries + +- `WireframeApp` does not implement `Debug`. The BDD world struct + `CodecTestHarnessWorld` stores an `Option>`, so + `#[derive(Debug)]` cannot be used. Resolved with a manual `Debug` + implementation that redacts the app field. + +- `wireframe_testing` is a dev-dependency of the main crate, not a workspace + member. Internal `#[cfg(test)]` modules within `wireframe_testing` do not run + via `cargo test` from the workspace root. Resolved by placing the unit tests + in `tests/codec_test_harness.rs` as an integration test file instead. + +- `WireframeApp::new()` returns a `Result` whose `Ok` variant uses + `LengthDelimitedFrameCodec` as the default codec. When chaining + `.with_codec()` immediately, the compiler cannot infer the initial codec type + parameter. Resolved by writing + `WireframeApp::::new()`. + +- Clippy `panic_in_result_fn` lint fires on test functions that return + `io::Result<()>` and use `assert_eq!`/`assert!`. Resolved by replacing + assertions with explicit `if` checks returning `Err(io::Error::other(...))`. + +## Decision log + +- Decision: accept the codec as an explicit `&F` parameter rather than + extracting it from the app. Rationale: `WireframeApp.codec` is + `pub(in crate::app)` and not accessible from `wireframe_testing`. While we + add a `codec()` accessor for convenience, the driver API takes the codec + explicitly so it works regardless of whether the caller holds the app or a + reference. This also mirrors the existing `payloads.rs` pattern where the + codec is constructed independently. Date/Author: 2026-02-26 / Plan phase. + +- Decision: provide two levels of codec-aware drivers — payload-level + (returns `Vec>`) and frame-level (returns `Vec`). + Rationale: payload-level covers the common case (90% of tests care about + payload bytes, not codec metadata). Frame-level covers advanced tests needing + to inspect correlation IDs, sequence numbers, or other frame-level metadata. + Date/Author: 2026-02-26 / Plan phase. + +- Decision: do not introduce a new `TestCodecSerializer` trait. + Rationale: the codec generic `F` is orthogonal to the serializer generic `S`. + The existing `TestSerializer` trait already captures serializer bounds. The + new drivers simply add `F: FrameCodec` as an additional generic parameter. + Date/Author: 2026-02-26 / Plan phase. + +## Outcomes & retrospective + +Completed 2026-02-26. All quality gates pass: + +- `make fmt` — clean (no formatting changes) +- `make check-fmt` — clean +- `make lint` — clean (cargo doc + clippy with `-D warnings`) +- `make test` — all green (0 failures) + +New test coverage: + +- 6 integration tests in `tests/codec_test_harness.rs` (encode round-trip, + empty decode, extract payloads, payload driver round-trip, frame metadata + preservation, mutable app reuse). +- 2 BDD scenarios in `tests/scenarios/codec_test_harness_scenarios.rs` + (payload round-trip, frame-level metadata). + +Files created: 7 (codec\_ext.rs, codec\_drive.rs, codec\_test\_harness.rs, +codec\_test\_harness.feature, codec\_test\_harness fixture/steps/scenarios). +Files modified: 10 (codec.rs accessor, helpers.rs, lib.rs re-exports, BDD mod +files, ADR, users-guide, roadmap). + +The Stage D plan originally called for tests inside +`wireframe_testing/src/helpers/tests/`, but the crate is not a workspace member +so internal tests never execute. Moving them to `tests/` as an integration test +was the correct approach. Future ExecPlans targeting `wireframe_testing` +internals should account for this constraint. + +## Context and orientation + +### Repository layout (relevant subset) + +```plaintext +src/ + codec.rs # FrameCodec trait (line 59-103) + codec/examples.rs # HotlineFrameCodec, MysqlFrameCodec + app/builder/core.rs # WireframeApp struct (line 29-50) + app/builder/codec.rs # with_codec(), serializer(), buffer_capacity() + app/inbound_handler.rs # handle_connection[_result]() for any F: FrameCodec + +wireframe_testing/ + Cargo.toml # dev-dependency on wireframe (path = "..") + src/lib.rs # public re-exports + src/helpers.rs # TestSerializer trait, module root, constants + src/helpers/drive.rs # drive_internal (pub(super)), drive_with_frame[s][_mut] + src/helpers/payloads.rs # drive_with_payloads, drive_with_bincode + src/helpers/codec.rs # new_test_codec, decode_frames, encode_frame + src/helpers/runtime.rs # run_app, run_with_duplex_server + src/helpers/tests/ # internal unit tests + +tests/ + fixtures/mod.rs # BDD world fixtures + steps/mod.rs # BDD step definitions + scenarios/mod.rs # BDD scenario registrations + features/ # Gherkin .feature files + fixtures/codec_stateful.rs # Existing codec BDD world (reference pattern) +``` + +### Key types + +`WireframeApp` is the central application builder, generic over +serializer (`S`), connection context (`C`), envelope/packet type (`E`), and +frame codec (`F`). The codec defaults to `LengthDelimitedFrameCodec`. + +`FrameCodec` (defined in `src/codec.rs:59-103`) requires +`Send + Sync + Clone + 'static` and has associated types `Frame`, `Decoder`, +`Encoder` with methods `decoder()`, `encoder()`, `frame_payload()`, +`wrap_payload()`, `correlation_id()`, and `max_frame_length()`. + +`drive_internal` (in `wireframe_testing/src/helpers/drive.rs:31-72`) is the +low-level transport function: it takes a `server_fn: FnOnce(DuplexStream)`, raw +`Vec>` frames, and a capacity, writes the frames to the client half, +and returns the raw bytes produced by the server half. All existing drivers +delegate to it. It is `pub(super)`, accessible from sibling modules under +`helpers/`. + +### BDD test pattern + +The project uses rstest-bdd v0.5.0. A BDD test domain consists of four files: + +1. `tests/features/.feature` — Gherkin scenarios. +2. `tests/fixtures/.rs` — a world struct with `#[fixture]` function. +3. `tests/steps/_steps.rs` — `#[given]`, `#[when]`, `#[then]` + functions that mutate the world. +4. `tests/scenarios/_scenarios.rs` — `#[scenario]` functions binding + feature file to steps. + +Steps use `tokio::runtime::Runtime::new()?.block_on(...)` for async. Worlds are +plain structs implementing `Default`. Each must be wired into +`tests/fixtures/mod.rs`, `tests/steps/mod.rs`, and `tests/scenarios/mod.rs`. + +## Plan of work + +### Stage A: add `codec()` accessor to `WireframeApp` + +Add a public method to `src/app/builder/codec.rs` within the existing +`impl WireframeApp` block (line 10-38): + +```rust +/// Return a reference to the configured frame codec. +pub fn codec(&self) -> &F { &self.codec } +``` + +This is a one-line additive change. It enables test code (and library +consumers) to access the codec instance without cloning. The change goes in the +existing impl block that already has the correct bounds. + +Stage A acceptance: `make check-fmt && make lint && make test` pass. The +accessor is callable from an external crate. + +### Stage B: implement codec-aware encode/decode helpers + +Create `wireframe_testing/src/helpers/codec_ext.rs` with generic functions that +encode payloads into raw bytes and decode raw bytes into frames or payloads +using any `FrameCodec`. + +Functions to implement: + +1. `encode_payloads_with_codec` — for each payload, call + `codec.wrap_payload(Bytes::from(payload))` then encode the resulting frame + using `codec.encoder()` into a `BytesMut`, returning the raw bytes for each + encoded frame. + +2. `decode_frames_with_codec` — feed the raw bytes into + `codec.decoder()` and collect all decoded frames. + +3. `extract_payloads` — call `F::frame_payload(frame).to_vec()` + for each frame. + +These are pure, testable functions with no async or side effects. + +Register the module in `wireframe_testing/src/helpers.rs` as `mod codec_ext;` +and re-export the three functions via `pub use codec_ext::{...};`. + +Stage B acceptance: `make check-fmt && make lint` pass. Functions compile and +are accessible from the crate root. + +### Stage C: implement codec-aware driver functions + +Create `wireframe_testing/src/helpers/codec_drive.rs` with the following public +async functions. All delegate to `drive_internal` from the sibling `drive` +module. + +**Payload-level drivers** (return `Vec>`): + +1. `drive_with_codec_payloads` +2. `drive_with_codec_payloads_with_capacity` +3. `drive_with_codec_payloads_mut` +4. `drive_with_codec_payloads_with_capacity_mut` + +**Frame-level drivers** (return `Vec`): + +1. `drive_with_codec_frames` +2. `drive_with_codec_frames_with_capacity` + +Generic bounds on all functions: +`S: TestSerializer, C: Send + 'static, E: Packet, F: FrameCodec`. + +Internal flow for payload-level drivers: + +```plaintext +encode_payloads_with_codec(codec, payloads) + -> Vec> (raw encoded frames) + -> drive_internal(|s| app.handle_connection(s), frames, capacity) + -> Vec (raw output bytes) + -> decode_frames_with_codec(codec, raw_output) + -> Vec + -> extract_payloads(frames) + -> Vec> +``` + +Frame-level drivers follow the same flow but return after the decode step +without extracting payloads. + +Add module declaration `mod codec_drive;` and re-exports in +`wireframe_testing/src/helpers.rs`. Add public re-exports in +`wireframe_testing/src/lib.rs`. + +Stage C acceptance: `make check-fmt && make lint` pass. Functions compile and +are accessible from the crate root. + +### Stage D: add rstest unit tests + +Create `wireframe_testing/src/helpers/tests/codec_drive_tests.rs` with unit +tests using `rstest`. Wire the module from +`wireframe_testing/src/helpers/tests/mod.rs`. + +Tests to implement: + +1. `encode_payloads_with_codec_produces_decodable_frames` — round-trip test: + encode payloads with `HotlineFrameCodec`, decode back, verify payloads match. + +2. `decode_frames_with_codec_handles_empty_input` — empty byte vector + produces an empty frame vector. + +3. `extract_payloads_returns_payload_bytes` — construct `HotlineFrame` + values manually, verify `extract_payloads` returns the correct byte slices. + +4. `drive_with_codec_payloads_round_trips_through_echo_app` — build a + `WireframeApp` with `HotlineFrameCodec` and an echo route, drive it, verify + payload bytes round-trip. + +5. `drive_with_codec_frames_preserves_codec_metadata` — build a + `WireframeApp` with `HotlineFrameCodec`, drive it, verify decoded + `HotlineFrame` values carry expected `transaction_id` values. + +6. `drive_with_codec_payloads_mut_allows_app_reuse` — drive a mutable app + reference twice, verify both calls succeed. + +Stage D acceptance: `make test` passes with the new tests green. + +### Stage E: add rstest-bdd behavioural tests + +Create four files for the `codec_test_harness` BDD domain: + +**`tests/features/codec_test_harness.feature`**: + +```gherkin +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 + When a test payload is driven through the codec-aware driver + Then the response payloads are non-empty + + Scenario: Frame-level driver preserves codec metadata + Given a wireframe app configured with a Hotline codec + When a test payload is driven through the frame-level driver + Then the response frames contain transaction identifiers +``` + +**`tests/fixtures/codec_test_harness.rs`**: A `CodecTestHarnessWorld` struct +holding an optional app, codec, response payloads, and response frames. +Provides async helper methods for building the app, driving it with the +payload-level driver, and driving it with the frame-level driver. Includes a +`#[fixture]` function `codec_test_harness_world`. + +**`tests/steps/codec_test_harness_steps.rs`**: Step definitions using +`#[given]`, `#[when]`, `#[then]` from `rstest_bdd_macros`. Each step wraps +async calls with `tokio::runtime::Runtime::new()?.block_on(...)`. + +**`tests/scenarios/codec_test_harness_scenarios.rs`**: Two `#[scenario]` +functions binding to the feature file. + +Wire the new module into: + +- `tests/fixtures/mod.rs` — add `pub mod codec_test_harness;` +- `tests/steps/mod.rs` — add `mod codec_test_harness_steps;` +- `tests/scenarios/mod.rs` — add `mod codec_test_harness_scenarios;` + +Stage E acceptance: `make test` passes with the new BDD scenarios green. + +### Stage F: documentation and roadmap updates + +1. Update `docs/adr-004-pluggable-protocol-codecs.md` with a design decision + recording the codec-aware test harness API and its rationale (explicit codec + parameter, payload vs frame level drivers, reuse of `drive_internal`). + +2. Update `docs/users-guide.md` with a section documenting: + - The new `codec()` accessor on `WireframeApp`. + - The new `drive_with_codec_payloads` and `drive_with_codec_frames` + families of functions in `wireframe_testing`. + - A short usage example. + +3. Mark roadmap item `9.7.1` as done in `docs/roadmap.md`: + change `- [ ] 9.7.1.` to `- [x] 9.7.1.` + +Stage F acceptance: documentation is internally consistent and +`make markdownlint` passes. + +### Stage G: full validation and evidence capture + +Run all quality gates with logging: + + +```shell +set -o pipefail; make fmt 2>&1 | tee /tmp/9-7-1-fmt.log +set -o pipefail; make check-fmt 2>&1 | tee /tmp/9-7-1-check-fmt.log +set -o pipefail; make markdownlint 2>&1 | tee /tmp/9-7-1-markdownlint.log +set -o pipefail; make lint 2>&1 | tee /tmp/9-7-1-lint.log +set -o pipefail; make test 2>&1 | tee /tmp/9-7-1-test.log +``` + + +Update the `Progress` and `Outcomes & Retrospective` sections with final +evidence and timestamps. + +Stage G acceptance: all commands exit 0. + +## Validation and acceptance + +Quality criteria (what "done" means): + +- Tests: `make test` passes, including the new rstest unit tests in + `wireframe_testing/src/helpers/tests/codec_drive_tests.rs` and the new + rstest-bdd scenarios in `tests/scenarios/codec_test_harness_scenarios.rs`. +- Lint: `make lint` passes (Clippy with `-D warnings` on all targets). +- Format: `make check-fmt` passes. +- Markdown: `make markdownlint` passes. +- Documentation: `docs/users-guide.md` documents the new public API. + `docs/roadmap.md` item 9.7.1 is marked done. + +Quality method: + +- Run the shell commands in Stage G and verify all exit 0. +- Verify the new BDD scenarios pass: `cargo test codec_test_harness`. +- Verify the new unit tests pass: `cargo test --package wireframe_testing`. + +## Idempotence and recovery + +All stages produce additive changes. If a stage fails partway through, the +incomplete changes can be reverted with `git checkout -- .` and the stage +retried from the beginning. No destructive operations are involved. + +## Interfaces and dependencies + +### New public accessor (wireframe crate) + +In `src/app/builder/codec.rs`, within the existing impl block: + +```rust +/// Return a reference to the configured frame codec. +pub fn codec(&self) -> &F { &self.codec } +``` + +### New public functions (wireframe\_testing crate) + +In `wireframe_testing/src/helpers/codec_ext.rs`: + +```rust +pub fn encode_payloads_with_codec( + codec: &F, + payloads: Vec>, +) -> io::Result>>; + +pub fn decode_frames_with_codec( + codec: &F, + bytes: Vec, +) -> io::Result>; + +pub fn extract_payloads( + frames: &[F::Frame], +) -> Vec>; +``` + +In `wireframe_testing/src/helpers/codec_drive.rs`: + +```rust +pub async fn drive_with_codec_payloads( + app: WireframeApp, + codec: &F, + payloads: Vec>, +) -> io::Result>> +where + S: TestSerializer, C: Send + 'static, E: Packet, F: FrameCodec; + +pub async fn drive_with_codec_payloads_with_capacity( + app: WireframeApp, + codec: &F, + payloads: Vec>, + capacity: usize, +) -> io::Result>> +where + S: TestSerializer, C: Send + 'static, E: Packet, F: FrameCodec; + +pub async fn drive_with_codec_payloads_mut( + app: &mut WireframeApp, + codec: &F, + payloads: Vec>, +) -> io::Result>> +where + S: TestSerializer, C: Send + 'static, E: Packet, F: FrameCodec; + +pub async fn drive_with_codec_payloads_with_capacity_mut( + app: &mut WireframeApp, + codec: &F, + payloads: Vec>, + capacity: usize, +) -> io::Result>> +where + S: TestSerializer, C: Send + 'static, E: Packet, F: FrameCodec; + +pub async fn drive_with_codec_frames( + app: WireframeApp, + codec: &F, + payloads: Vec>, +) -> io::Result> +where + S: TestSerializer, C: Send + 'static, E: Packet, F: FrameCodec; + +pub async fn drive_with_codec_frames_with_capacity( + app: WireframeApp, + codec: &F, + payloads: Vec>, + capacity: usize, +) -> io::Result> +where + S: TestSerializer, C: Send + 'static, E: Packet, F: FrameCodec; +``` + +### Files to create + +| File | Purpose | Est. lines | +| ------------------------------------------------- | ----------------------------- | ---------- | +| `wireframe_testing/src/helpers/codec_ext.rs` | Generic encode/decode helpers | ~80 | +| `wireframe_testing/src/helpers/codec_drive.rs` | Codec-aware driver functions | ~200 | +| `tests/codec_test_harness.rs` | Integration tests | ~160 | +| `tests/features/codec_test_harness.feature` | BDD feature file | ~15 | +| `tests/fixtures/codec_test_harness.rs` | BDD world fixture | ~120 | +| `tests/steps/codec_test_harness_steps.rs` | BDD step definitions | ~50 | +| `tests/scenarios/codec_test_harness_scenarios.rs` | BDD scenario registrations | ~25 | + +### Files to modify + +| File | Change | +| ---------------------------------------------------------------- | ---------------------------------------------------- | +| `src/app/builder/codec.rs` | Add `pub fn codec(&self) -> &F` accessor | +| `wireframe_testing/src/helpers.rs` | Add `mod codec_ext; mod codec_drive;` and re-exports | +| `wireframe_testing/src/lib.rs` | Add re-exports for new public functions | +| (none — tests placed in `tests/codec_test_harness.rs` instead) | See Surprises | +| `tests/fixtures/mod.rs` | Add `pub mod codec_test_harness;` | +| `tests/steps/mod.rs` | Add `mod codec_test_harness_steps;` | +| `tests/scenarios/mod.rs` | Add `mod codec_test_harness_scenarios;` | +| `docs/adr-004-pluggable-protocol-codecs.md` | Add test harness design decision | +| `docs/users-guide.md` | Document new public API | +| `docs/roadmap.md` | Mark 9.7.1 done | + +## Artifacts and notes + +Reference pattern for BDD world: `tests/fixtures/codec_stateful.rs` — defines +`CodecStatefulWorld` with a `SeqFrameCodec`, server management, and frame +exchange helpers. The new `CodecTestHarnessWorld` follows the same pattern but +uses `wireframe_testing` codec-aware drivers instead of manual +`Framed`/`SinkExt` operations. + +Reference pattern for step definitions: `tests/steps/codec_stateful_steps.rs` — +wraps async calls with `tokio::runtime::Runtime::new()?.block_on(...)`. + +Reference pattern for scenarios: `tests/scenarios/codec_stateful_scenarios.rs` +— `#[scenario]` macro with `#[expect(unused_variables)]` attribute. diff --git a/docs/roadmap.md b/docs/roadmap.md index 00fd382c..920fc129 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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. diff --git a/docs/users-guide.md b/docs/users-guide.md index f7e54a28..4abb98da 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -208,6 +208,54 @@ 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 +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 +use wireframe::codec::examples::HotlineFrameCodec; +use wireframe_testing::{drive_with_codec_payloads, drive_with_codec_frames}; + +let codec = HotlineFrameCodec::new(4096); +let app = WireframeApp::new()?.with_codec(codec.clone()); + +// Payload-level: returns decoded response payloads as byte vectors. +let payloads = drive_with_codec_payloads(app, &codec, vec![serialized_envelope]).await?; + +// Frame-level: returns decoded codec frames for metadata inspection. +let frames = drive_with_codec_frames(app, &codec, vec![serialized_envelope]).await?; +``` + +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` for payload diff --git a/src/app/builder/codec.rs b/src/app/builder/codec.rs index 98d49dcb..a48cc827 100644 --- a/src/app/builder/codec.rs +++ b/src/app/builder/codec.rs @@ -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 diff --git a/tests/codec_test_harness.rs b/tests/codec_test_harness.rs new file mode 100644 index 00000000..ad492f1f --- /dev/null +++ b/tests/codec_test_harness.rs @@ -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::::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> { + 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 = encoded.into_iter().flatten().collect(); + let frames = decode_frames_with_codec(&codec, wire)?; + + let extracted = extract_payloads::(&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::(&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::(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(()) +} diff --git a/tests/features/codec_test_harness.feature b/tests/features/codec_test_harness.feature new file mode 100644 index 00000000..31b66122 --- /dev/null +++ b/tests/features/codec_test_harness.feature @@ -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 diff --git a/tests/fixtures/codec_test_harness.rs b/tests/fixtures/codec_test_harness.rs new file mode 100644 index 00000000..4e4f08f7 --- /dev/null +++ b/tests/fixtures/codec_test_harness.rs @@ -0,0 +1,132 @@ +//! Test world for codec-aware test harness behavioural scenarios. +//! +//! Exercises the `wireframe_testing` codec-aware driver API with +//! `HotlineFrameCodec` to verify transparent encoding and decoding. + +use std::sync::Arc; + +use futures::future::BoxFuture; +use rstest::fixture; +use wireframe::{ + app::{Envelope, WireframeApp}, + codec::examples::{HotlineFrame, HotlineFrameCodec}, + serializer::{BincodeSerializer, Serializer}, +}; +/// Re-export `TestResult` from `wireframe_testing` for use in steps. +pub use wireframe_testing::TestResult; + +/// BDD world holding the app, codec, and collected responses. +/// +/// `WireframeApp` does not implement `Debug`, so this type provides a manual +/// implementation that redacts the app field. +#[derive(Default)] +pub struct CodecTestHarnessWorld { + codec: Option, + app: Option>, + response_payloads: Vec>, + response_frames: Vec, +} + +impl std::fmt::Debug for CodecTestHarnessWorld { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CodecTestHarnessWorld") + .field("codec", &self.codec) + .field("app", &self.app.as_ref().map(|_| "..")) + .field("response_payloads", &self.response_payloads.len()) + .field("response_frames", &self.response_frames.len()) + .finish() + } +} + +/// Fixture for codec test harness scenarios used by rstest-bdd steps. +/// +/// Note: rustfmt collapses simple fixtures into one line, which triggers +/// `unused_braces`, so keep `rustfmt::skip`. +#[rustfmt::skip] +#[fixture] +pub fn codec_test_harness_world() -> CodecTestHarnessWorld { + CodecTestHarnessWorld::default() +} + +impl CodecTestHarnessWorld { + /// Configure the app with a `HotlineFrameCodec`. + /// + /// # Errors + /// Returns an error if the app or route registration fails. + pub fn configure_app(&mut self, max_frame_length: usize) -> TestResult { + let codec = HotlineFrameCodec::new(max_frame_length); + let app = WireframeApp::::new()? + .with_codec(codec.clone()) + .route( + 1, + Arc::new(|_: &Envelope| -> BoxFuture<'static, ()> { Box::pin(async {}) }), + )?; + self.codec = Some(codec); + self.app = Some(app); + Ok(()) + } + + /// Drive the app through the payload-level codec driver. + /// + /// # Errors + /// Returns an error if serialization or the driver fails. + pub async fn drive_payload(&mut self) -> TestResult { + let app = self.app.take().ok_or("app not configured")?; + let codec = self.codec.as_ref().ok_or("codec not configured")?; + + let env = Envelope::new(1, Some(7), b"bdd-test".to_vec()); + let serialized = BincodeSerializer.serialize(&env)?; + + self.response_payloads = + wireframe_testing::drive_with_codec_payloads(app, codec, vec![serialized]).await?; + Ok(()) + } + + /// Drive the app through the frame-level codec driver. + /// + /// # Errors + /// Returns an error if serialization or the driver fails. + pub async fn drive_frames(&mut self) -> TestResult { + let app = self.app.take().ok_or("app not configured")?; + let codec = self.codec.as_ref().ok_or("codec not configured")?; + + let env = Envelope::new(1, Some(7), b"bdd-frame-test".to_vec()); + let serialized = BincodeSerializer.serialize(&env)?; + + self.response_frames = + wireframe_testing::drive_with_codec_frames(app, codec, vec![serialized]).await?; + Ok(()) + } + + /// Verify that payload responses are non-empty. + /// + /// # Errors + /// Returns an error if the assertion fails. + pub fn verify_payloads_non_empty(&self) -> TestResult { + if self.response_payloads.is_empty() { + return Err("expected non-empty response payloads".into()); + } + Ok(()) + } + + /// Verify that response frames contain valid transaction identifiers. + /// + /// # Errors + /// Returns an error if no frames were received. + pub fn verify_frames_have_transaction_ids(&self) -> TestResult { + if self.response_frames.is_empty() { + return Err("expected at least one response frame".into()); + } + // wrap_payload assigns transaction_id = 0; confirming the field is + // present in the decoded frame proves codec metadata survives the + // driver pipeline. + for frame in &self.response_frames { + if frame.transaction_id != 0 { + return Err( + format!("expected transaction_id 0, got {}", frame.transaction_id).into(), + ); + } + } + Ok(()) + } +} diff --git a/tests/fixtures/mod.rs b/tests/fixtures/mod.rs index c55ae4ad..2d41f97e 100644 --- a/tests/fixtures/mod.rs +++ b/tests/fixtures/mod.rs @@ -13,6 +13,7 @@ pub mod codec_error; pub mod codec_performance_benchmarks; pub mod codec_property_roundtrip; pub mod codec_stateful; +pub mod codec_test_harness; pub mod correlation; pub mod fragment; pub mod interleaved_push_queues; diff --git a/tests/scenarios/codec_test_harness_scenarios.rs b/tests/scenarios/codec_test_harness_scenarios.rs new file mode 100644 index 00000000..7e36a7d4 --- /dev/null +++ b/tests/scenarios/codec_test_harness_scenarios.rs @@ -0,0 +1,25 @@ +//! Scenario tests for codec-aware test harness behaviours. + +use rstest_bdd_macros::scenario; + +use crate::fixtures::codec_test_harness::*; + +#[scenario( + path = "tests/features/codec_test_harness.feature", + name = "Payload round-trip through a custom codec driver" +)] +#[expect( + unused_variables, + reason = "rstest-bdd wires steps via parameters without using them directly" +)] +fn payload_round_trip(codec_test_harness_world: CodecTestHarnessWorld) {} + +#[scenario( + path = "tests/features/codec_test_harness.feature", + name = "Frame-level driver preserves codec metadata" +)] +#[expect( + unused_variables, + reason = "rstest-bdd wires steps via parameters without using them directly" +)] +fn frame_level_metadata(codec_test_harness_world: CodecTestHarnessWorld) {} diff --git a/tests/scenarios/mod.rs b/tests/scenarios/mod.rs index d7ea76c9..babd696d 100644 --- a/tests/scenarios/mod.rs +++ b/tests/scenarios/mod.rs @@ -17,6 +17,7 @@ mod codec_error_scenarios; mod codec_performance_benchmarks_scenarios; mod codec_property_roundtrip_scenarios; mod codec_stateful_scenarios; +mod codec_test_harness_scenarios; mod correlation_scenarios; mod fragment_scenarios; mod interleaved_push_queues_scenarios; diff --git a/tests/steps/codec_test_harness_steps.rs b/tests/steps/codec_test_harness_steps.rs new file mode 100644 index 00000000..96020f54 --- /dev/null +++ b/tests/steps/codec_test_harness_steps.rs @@ -0,0 +1,40 @@ +//! Step definitions for codec-aware test harness behavioural tests. + +use rstest_bdd_macros::{given, then, when}; + +use crate::fixtures::codec_test_harness::{CodecTestHarnessWorld, TestResult}; + +#[given( + "a wireframe app configured with a Hotline codec allowing frames up to \ + {max_frame_length:usize} bytes" +)] +fn given_app_with_hotline_codec( + codec_test_harness_world: &mut CodecTestHarnessWorld, + max_frame_length: usize, +) -> TestResult { + codec_test_harness_world.configure_app(max_frame_length) +} + +#[when("a test payload is driven through the codec-aware payload driver")] +fn when_payload_driven(codec_test_harness_world: &mut CodecTestHarnessWorld) -> TestResult { + let rt = tokio::runtime::Runtime::new()?; + rt.block_on(codec_test_harness_world.drive_payload()) +} + +#[when("a test payload is driven through the codec-aware frame driver")] +fn when_frame_driven(codec_test_harness_world: &mut CodecTestHarnessWorld) -> TestResult { + let rt = tokio::runtime::Runtime::new()?; + rt.block_on(codec_test_harness_world.drive_frames()) +} + +#[then("the response payloads are non-empty")] +fn then_payloads_non_empty(codec_test_harness_world: &mut CodecTestHarnessWorld) -> TestResult { + codec_test_harness_world.verify_payloads_non_empty() +} + +#[then("the response frames contain valid transaction identifiers")] +fn then_frames_have_transaction_ids( + codec_test_harness_world: &mut CodecTestHarnessWorld, +) -> TestResult { + codec_test_harness_world.verify_frames_have_transaction_ids() +} diff --git a/tests/steps/mod.rs b/tests/steps/mod.rs index 57efc70d..0b60065c 100644 --- a/tests/steps/mod.rs +++ b/tests/steps/mod.rs @@ -13,6 +13,7 @@ mod codec_error_steps; mod codec_performance_benchmarks_steps; mod codec_property_roundtrip_steps; mod codec_stateful_steps; +mod codec_test_harness_steps; mod correlation_steps; mod fragment_steps; mod interleaved_push_queues_steps; diff --git a/wireframe_testing/Cargo.lock b/wireframe_testing/Cargo.lock index 93496afd..d16e396e 100644 --- a/wireframe_testing/Cargo.lock +++ b/wireframe_testing/Cargo.lock @@ -1831,6 +1831,7 @@ dependencies = [ "logtest", "metrics-util", "rstest", + "thiserror", "tokio", "tokio-util", "wireframe", diff --git a/wireframe_testing/src/helpers.rs b/wireframe_testing/src/helpers.rs index a3dece4c..2178c48e 100644 --- a/wireframe_testing/src/helpers.rs +++ b/wireframe_testing/src/helpers.rs @@ -10,6 +10,8 @@ use wireframe::{ }; mod codec; +mod codec_drive; +mod codec_ext; mod drive; mod payloads; mod runtime; @@ -45,6 +47,15 @@ pub(crate) const EMPTY_SERVER_CAPACITY: usize = 64; pub const TEST_MAX_FRAME: usize = DEFAULT_CAPACITY; pub use codec::{decode_frames, decode_frames_with_max, encode_frame, new_test_codec}; +pub use codec_drive::{ + drive_with_codec_frames, + drive_with_codec_frames_with_capacity, + drive_with_codec_payloads, + drive_with_codec_payloads_mut, + drive_with_codec_payloads_with_capacity, + drive_with_codec_payloads_with_capacity_mut, +}; +pub use codec_ext::{decode_frames_with_codec, encode_payloads_with_codec, extract_payloads}; pub use drive::{ drive_with_frame, drive_with_frame_mut, diff --git a/wireframe_testing/src/helpers/codec_drive.rs b/wireframe_testing/src/helpers/codec_drive.rs new file mode 100644 index 00000000..48d59007 --- /dev/null +++ b/wireframe_testing/src/helpers/codec_drive.rs @@ -0,0 +1,255 @@ +//! Codec-aware in-memory driving helpers. +//! +//! These functions extend the frame-oriented drivers in [`super::drive`] with +//! automatic encoding and decoding through an arbitrary [`FrameCodec`]. Test +//! authors pass raw payloads and receive decoded payloads (or frames) without +//! manually constructing encoders or decoders. + +use std::io; + +use wireframe::{ + app::{Packet, WireframeApp}, + codec::FrameCodec, +}; + +use super::{ + DEFAULT_CAPACITY, + TestSerializer, + codec_ext::{decode_frames_with_codec, encode_payloads_with_codec, extract_payloads}, + drive::drive_internal, +}; + +// --------------------------------------------------------------------------- +// Payload-level drivers (return Vec>) +// --------------------------------------------------------------------------- + +/// Drive `app` with payloads encoded by `codec` and return decoded response +/// payloads. +/// +/// Each input payload is wrapped and encoded using the codec, sent through an +/// in-memory duplex stream, and the server's response bytes are decoded back +/// into payload byte vectors. +/// +/// # Errors +/// +/// Returns any I/O or codec error encountered during encoding, transport, or +/// decoding. +/// +/// ```rust +/// # use wireframe::app::WireframeApp; +/// # use wireframe::codec::examples::HotlineFrameCodec; +/// # use wireframe_testing::drive_with_codec_payloads; +/// # async fn demo() -> std::io::Result<()> { +/// let codec = HotlineFrameCodec::new(4096); +/// let app = WireframeApp::new().expect("app").with_codec(codec.clone()); +/// let payloads = drive_with_codec_payloads(app, &codec, vec![vec![1]]).await?; +/// # Ok(()) +/// # } +/// ``` +pub async fn drive_with_codec_payloads( + app: WireframeApp, + codec: &F, + payloads: Vec>, +) -> io::Result>> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, + F: FrameCodec, +{ + drive_with_codec_payloads_with_capacity(app, codec, payloads, DEFAULT_CAPACITY).await +} + +/// Drive `app` with payloads using a duplex buffer of `capacity` bytes. +/// +/// # Errors +/// +/// Returns any I/O or codec error encountered during encoding, transport, or +/// decoding. +/// +/// ```rust +/// # use wireframe::app::WireframeApp; +/// # use wireframe::codec::examples::HotlineFrameCodec; +/// # use wireframe_testing::drive_with_codec_payloads_with_capacity; +/// # async fn demo() -> std::io::Result<()> { +/// let codec = HotlineFrameCodec::new(4096); +/// let app = WireframeApp::new().expect("app").with_codec(codec.clone()); +/// let payloads = +/// drive_with_codec_payloads_with_capacity(app, &codec, vec![vec![1]], 8192).await?; +/// # Ok(()) +/// # } +/// ``` +pub async fn drive_with_codec_payloads_with_capacity( + app: WireframeApp, + codec: &F, + payloads: Vec>, + capacity: usize, +) -> io::Result>> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, + F: FrameCodec, +{ + let frames = drive_with_codec_frames_with_capacity(app, codec, payloads, capacity).await?; + Ok(extract_payloads::(&frames)) +} + +/// Drive a mutable `app` with payloads encoded by `codec`. +/// +/// The mutable reference allows the app instance to be reused across +/// successive calls. +/// +/// # Errors +/// +/// Returns any I/O or codec error encountered during encoding, transport, or +/// decoding. +/// +/// ```rust +/// # use wireframe::app::WireframeApp; +/// # use wireframe::codec::examples::HotlineFrameCodec; +/// # use wireframe_testing::drive_with_codec_payloads_mut; +/// # async fn demo() -> std::io::Result<()> { +/// let codec = HotlineFrameCodec::new(4096); +/// let mut app = WireframeApp::new().expect("app").with_codec(codec.clone()); +/// let payloads = drive_with_codec_payloads_mut(&mut app, &codec, vec![vec![1]]).await?; +/// # Ok(()) +/// # } +/// ``` +pub async fn drive_with_codec_payloads_mut( + app: &mut WireframeApp, + codec: &F, + payloads: Vec>, +) -> io::Result>> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, + F: FrameCodec, +{ + drive_with_codec_payloads_with_capacity_mut(app, codec, payloads, DEFAULT_CAPACITY).await +} + +/// Drive a mutable `app` with payloads using a duplex buffer of `capacity` +/// bytes. +/// +/// # Errors +/// +/// Returns any I/O or codec error encountered during encoding, transport, or +/// decoding. +/// +/// ```rust +/// # use wireframe::app::WireframeApp; +/// # use wireframe::codec::examples::HotlineFrameCodec; +/// # use wireframe_testing::drive_with_codec_payloads_with_capacity_mut; +/// # async fn demo() -> std::io::Result<()> { +/// let codec = HotlineFrameCodec::new(4096); +/// let mut app = WireframeApp::new().expect("app").with_codec(codec.clone()); +/// let payloads = +/// drive_with_codec_payloads_with_capacity_mut(&mut app, &codec, vec![vec![1]], 8192).await?; +/// # Ok(()) +/// # } +/// ``` +pub async fn drive_with_codec_payloads_with_capacity_mut( + app: &mut WireframeApp, + codec: &F, + payloads: Vec>, + capacity: usize, +) -> io::Result>> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, + F: FrameCodec, +{ + let encoded = encode_payloads_with_codec(codec, payloads)?; + let raw = drive_internal( + |server| async { app.handle_connection(server).await }, + encoded, + capacity, + ) + .await?; + let frames = decode_frames_with_codec(codec, raw)?; + Ok(extract_payloads::(&frames)) +} + +// --------------------------------------------------------------------------- +// Frame-level drivers (return Vec) +// --------------------------------------------------------------------------- + +/// Drive `app` with payloads and return decoded response frames. +/// +/// Unlike the payload-level drivers, this variant returns the full codec +/// frames so tests can inspect frame-level metadata such as transaction +/// identifiers or sequence numbers. +/// +/// # Errors +/// +/// Returns any I/O or codec error encountered during encoding, transport, or +/// decoding. +/// +/// ```rust +/// # use wireframe::app::WireframeApp; +/// # use wireframe::codec::examples::HotlineFrameCodec; +/// # use wireframe_testing::drive_with_codec_frames; +/// # async fn demo() -> std::io::Result<()> { +/// let codec = HotlineFrameCodec::new(4096); +/// let app = WireframeApp::new().expect("app").with_codec(codec.clone()); +/// let frames = drive_with_codec_frames(app, &codec, vec![vec![1]]).await?; +/// # Ok(()) +/// # } +/// ``` +pub async fn drive_with_codec_frames( + app: WireframeApp, + codec: &F, + payloads: Vec>, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, + F: FrameCodec, +{ + drive_with_codec_frames_with_capacity(app, codec, payloads, DEFAULT_CAPACITY).await +} + +/// Drive `app` with payloads using a duplex buffer of `capacity` bytes and +/// return decoded response frames. +/// +/// # Errors +/// +/// Returns any I/O or codec error encountered during encoding, transport, or +/// decoding. +/// +/// ```rust +/// # use wireframe::app::WireframeApp; +/// # use wireframe::codec::examples::HotlineFrameCodec; +/// # use wireframe_testing::drive_with_codec_frames_with_capacity; +/// # async fn demo() -> std::io::Result<()> { +/// let codec = HotlineFrameCodec::new(4096); +/// let app = WireframeApp::new().expect("app").with_codec(codec.clone()); +/// let frames = drive_with_codec_frames_with_capacity(app, &codec, vec![vec![1]], 8192).await?; +/// # Ok(()) +/// # } +/// ``` +pub async fn drive_with_codec_frames_with_capacity( + app: WireframeApp, + codec: &F, + payloads: Vec>, + capacity: usize, +) -> io::Result> +where + S: TestSerializer, + C: Send + 'static, + E: Packet, + F: FrameCodec, +{ + let encoded = encode_payloads_with_codec(codec, payloads)?; + let raw = drive_internal( + |server| async move { app.handle_connection(server).await }, + encoded, + capacity, + ) + .await?; + decode_frames_with_codec(codec, raw) +} diff --git a/wireframe_testing/src/helpers/codec_ext.rs b/wireframe_testing/src/helpers/codec_ext.rs new file mode 100644 index 00000000..ce91634f --- /dev/null +++ b/wireframe_testing/src/helpers/codec_ext.rs @@ -0,0 +1,118 @@ +//! Generic encode/decode helpers for any [`FrameCodec`] implementation. +//! +//! These functions translate between raw payload bytes and codec-framed wire +//! bytes, allowing test drivers to work transparently with arbitrary codecs. + +use std::io; + +use bytes::{Bytes, BytesMut}; +use tokio_util::codec::{Decoder, Encoder}; +use wireframe::codec::FrameCodec; + +/// Encode each payload into wire bytes using `codec`. +/// +/// For every payload the codec's [`FrameCodec::wrap_payload`] produces a +/// frame, which is then serialized through the codec's encoder. The resulting +/// raw byte vectors are suitable for writing directly to a duplex stream. +/// +/// # Errors +/// +/// Returns an error if the encoder rejects a frame (e.g. payload too large). +/// +/// ```rust +/// use wireframe::codec::examples::HotlineFrameCodec; +/// use wireframe_testing::encode_payloads_with_codec; +/// +/// let codec = HotlineFrameCodec::new(4096); +/// let frames = encode_payloads_with_codec(&codec, vec![vec![1, 2, 3]]).unwrap(); +/// assert!(!frames.is_empty()); +/// ``` +pub fn encode_payloads_with_codec( + codec: &F, + payloads: Vec>, +) -> io::Result>> { + let mut encoder = codec.encoder(); + payloads + .into_iter() + .map(|payload| { + let frame = codec.wrap_payload(Bytes::from(payload)); + let mut buf = BytesMut::new(); + encoder.encode(frame, &mut buf).map_err(|error| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("codec encode failed: {error}"), + ) + })?; + Ok(buf.to_vec()) + }) + .collect() +} + +/// Decode raw wire bytes into frames using `codec`. +/// +/// The byte vector is fed into the codec's decoder until no more complete +/// frames can be extracted. Trailing bytes that do not form a complete frame +/// are silently ignored (matching the behaviour of a connection that is shut +/// down after the last response). +/// +/// # Errors +/// +/// Returns an error if the decoder encounters malformed data. +/// +/// ```rust +/// use wireframe::codec::examples::HotlineFrameCodec; +/// use wireframe_testing::{decode_frames_with_codec, encode_payloads_with_codec}; +/// +/// let codec = HotlineFrameCodec::new(4096); +/// let wire: Vec = encode_payloads_with_codec(&codec, vec![vec![42]]) +/// .unwrap() +/// .into_iter() +/// .flatten() +/// .collect(); +/// let frames = decode_frames_with_codec(&codec, wire).unwrap(); +/// assert_eq!(frames.len(), 1); +/// ``` +pub fn decode_frames_with_codec( + codec: &F, + bytes: Vec, +) -> io::Result> { + let mut decoder = codec.decoder(); + let mut buf = BytesMut::from(bytes.as_slice()); + let mut frames = Vec::new(); + while let Some(frame) = decoder.decode(&mut buf).map_err(|error| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("codec decode failed: {error}"), + ) + })? { + frames.push(frame); + } + Ok(frames) +} + +/// Extract raw payload bytes from a slice of codec frames. +/// +/// Calls [`FrameCodec::frame_payload`] on each frame and collects the +/// results into owned byte vectors. +/// +/// ```rust +/// use bytes::Bytes; +/// use wireframe::codec::{ +/// FrameCodec, +/// examples::{HotlineFrame, HotlineFrameCodec}, +/// }; +/// use wireframe_testing::extract_payloads; +/// +/// let frame = HotlineFrame { +/// transaction_id: 1, +/// payload: Bytes::from_static(b"hi"), +/// }; +/// let payloads = extract_payloads::(&[frame]); +/// assert_eq!(payloads, vec![b"hi".to_vec()]); +/// ``` +pub fn extract_payloads(frames: &[F::Frame]) -> Vec> { + frames + .iter() + .map(|frame| F::frame_payload(frame).to_vec()) + .collect() +} diff --git a/wireframe_testing/src/lib.rs b/wireframe_testing/src/lib.rs index dc472b13..9575a06f 100644 --- a/wireframe_testing/src/lib.rs +++ b/wireframe_testing/src/lib.rs @@ -30,8 +30,15 @@ pub use helpers::{ TEST_MAX_FRAME, TestSerializer, decode_frames, + decode_frames_with_codec, decode_frames_with_max, drive_with_bincode, + drive_with_codec_frames, + drive_with_codec_frames_with_capacity, + drive_with_codec_payloads, + drive_with_codec_payloads_mut, + drive_with_codec_payloads_with_capacity, + drive_with_codec_payloads_with_capacity_mut, drive_with_frame, drive_with_frame_mut, drive_with_frame_with_capacity, @@ -41,6 +48,8 @@ pub use helpers::{ drive_with_payloads, drive_with_payloads_mut, encode_frame, + encode_payloads_with_codec, + extract_payloads, new_test_codec, run_app, run_with_duplex_server, From da93f08fb709755bf483308944e8c7870fe3a2e3 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 26 Feb 2026 14:59:21 +0000 Subject: [PATCH 2/3] feat(wireframe_testing): add codec-aware test harness drivers for frame encoding/decoding Introduced new driver functions in `wireframe_testing` that accept any `FrameCodec` implementation and handle transparent frame encoding and decoding. These include payload-level and frame-level drivers that facilitate testing with raw payloads or codec-specific frame types, respectively. Added codec helper functions for encoding and decoding payloads and frames. Design choices ensure no new traits are introduced, codec is explicitly passed to avoid coupling, and a `codec()` accessor was added to `WireframeApp` for convenience. Updated documentation and tests to cover these new features, including integration tests outside the dev-dependency crate. This enhancement supports extensible and expressive protocol codec testing under roadmap item 9.7.1. Co-authored-by: devboxerhub[bot] --- docs/adr-004-pluggable-protocol-codecs.md | 55 +++++++++---------- .../9-7-1-codec-aware-wireframe-testing.md | 24 ++++---- docs/users-guide.md | 4 +- wireframe_testing/src/helpers/codec_drive.rs | 44 +++++++++++---- wireframe_testing/src/helpers/codec_ext.rs | 40 +++++++++++--- 5 files changed, 108 insertions(+), 59 deletions(-) diff --git a/docs/adr-004-pluggable-protocol-codecs.md b/docs/adr-004-pluggable-protocol-codecs.md index 954952b8..7d955c8f 100644 --- a/docs/adr-004-pluggable-protocol-codecs.md +++ b/docs/adr-004-pluggable-protocol-codecs.md @@ -235,34 +235,6 @@ pub trait FrameCodec: Send + Sync + Clone + 'static { failures and recovery policies using the test observability harness once available.[^adr-006] -## Resolved decisions - -### Codec-aware test harness drivers (9.7.1) - -The `wireframe_testing` crate now provides 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. - ## Known Risks and Limitations - Associated types avoid RPITIT for `FrameCodec`, but the overall design still @@ -368,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 diff --git a/docs/execplans/9-7-1-codec-aware-wireframe-testing.md b/docs/execplans/9-7-1-codec-aware-wireframe-testing.md index 684b3b5e..45375022 100644 --- a/docs/execplans/9-7-1-codec-aware-wireframe-testing.md +++ b/docs/execplans/9-7-1-codec-aware-wireframe-testing.md @@ -100,10 +100,10 @@ codec-aware drivers with `HotlineFrameCodec`. ## Surprises & discoveries -- `WireframeApp` does not implement `Debug`. The BDD world struct - `CodecTestHarnessWorld` stores an `Option>`, so - `#[derive(Debug)]` cannot be used. Resolved with a manual `Debug` - implementation that redacts the app field. +- `WireframeApp` does not implement `Debug`. The behaviour-driven + development (BDD) world struct `CodecTestHarnessWorld` stores an + `Option>`, so `#[derive(Debug)]` cannot be used. Resolved + with a manual `Debug` implementation that redacts the app field. - `wireframe_testing` is a dev-dependency of the main crate, not a workspace member. Internal `#[cfg(test)]` modules within `wireframe_testing` do not run @@ -324,11 +324,11 @@ Add module declaration `mod codec_drive;` and re-exports in Stage C acceptance: `make check-fmt && make lint` pass. Functions compile and are accessible from the crate root. -### Stage D: add rstest unit tests +### Stage D: add integration tests -Create `wireframe_testing/src/helpers/tests/codec_drive_tests.rs` with unit -tests using `rstest`. Wire the module from -`wireframe_testing/src/helpers/tests/mod.rs`. +Create `tests/codec_test_harness.rs` as an integration test file exercising the +new codec-aware drivers. (The tests live outside `wireframe_testing` because +the crate is a dev-dependency, not a workspace member — see Surprises.) Tests to implement: @@ -439,9 +439,9 @@ Stage G acceptance: all commands exit 0. Quality criteria (what "done" means): -- Tests: `make test` passes, including the new rstest unit tests in - `wireframe_testing/src/helpers/tests/codec_drive_tests.rs` and the new - rstest-bdd scenarios in `tests/scenarios/codec_test_harness_scenarios.rs`. +- Tests: `make test` passes, including the new integration tests in + `tests/codec_test_harness.rs` and the new rstest-bdd scenarios in + `tests/scenarios/codec_test_harness_scenarios.rs`. - Lint: `make lint` passes (Clippy with `-D warnings` on all targets). - Format: `make check-fmt` passes. - Markdown: `make markdownlint` passes. @@ -452,7 +452,7 @@ Quality method: - Run the shell commands in Stage G and verify all exit 0. - Verify the new BDD scenarios pass: `cargo test codec_test_harness`. -- Verify the new unit tests pass: `cargo test --package wireframe_testing`. +- Verify the new integration tests pass: `cargo test codec_test_harness`. ## Idempotence and recovery diff --git a/docs/users-guide.md b/docs/users-guide.md index 4abb98da..ddd2dd9c 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -212,7 +212,7 @@ implementations. Retrieve the configured codec from a `WireframeApp` instance: -```rust +```rust,no_run use wireframe::app::WireframeApp; use wireframe::codec::examples::HotlineFrameCodec; @@ -226,7 +226,7 @@ let codec_ref = app.codec(); // &HotlineFrameCodec The `wireframe_testing` crate provides codec-aware driver functions that handle frame encoding and decoding transparently: -```rust +```rust,no_run use wireframe::codec::examples::HotlineFrameCodec; use wireframe_testing::{drive_with_codec_payloads, drive_with_codec_frames}; diff --git a/wireframe_testing/src/helpers/codec_drive.rs b/wireframe_testing/src/helpers/codec_drive.rs index 48d59007..ba1386eb 100644 --- a/wireframe_testing/src/helpers/codec_drive.rs +++ b/wireframe_testing/src/helpers/codec_drive.rs @@ -7,6 +7,7 @@ use std::io; +use tokio::io::DuplexStream; use wireframe::{ app::{Packet, WireframeApp}, codec::FrameCodec, @@ -19,6 +20,31 @@ use super::{ drive::drive_internal, }; +// --------------------------------------------------------------------------- +// Shared internal helper +// --------------------------------------------------------------------------- + +/// Encode payloads, drive the server handler, and decode response frames. +/// +/// Every public driver in this module delegates to this function. Keeping the +/// encode → transport → decode pipeline in one place prevents behavioural +/// drift between the owned and mutable variants. +async fn drive_codec_frames_internal( + handler: H, + codec: &F, + payloads: Vec>, + capacity: usize, +) -> io::Result> +where + F: FrameCodec, + H: FnOnce(DuplexStream) -> Fut, + Fut: std::future::Future + Send, +{ + let encoded = encode_payloads_with_codec(codec, payloads)?; + let raw = drive_internal(handler, encoded, capacity).await?; + decode_frames_with_codec(codec, raw) +} + // --------------------------------------------------------------------------- // Payload-level drivers (return Vec>) // --------------------------------------------------------------------------- @@ -162,14 +188,13 @@ where E: Packet, F: FrameCodec, { - let encoded = encode_payloads_with_codec(codec, payloads)?; - let raw = drive_internal( - |server| async { app.handle_connection(server).await }, - encoded, + let frames = drive_codec_frames_internal( + |server| async move { app.handle_connection(server).await }, + codec, + payloads, capacity, ) .await?; - let frames = decode_frames_with_codec(codec, raw)?; Ok(extract_payloads::(&frames)) } @@ -244,12 +269,11 @@ where E: Packet, F: FrameCodec, { - let encoded = encode_payloads_with_codec(codec, payloads)?; - let raw = drive_internal( + drive_codec_frames_internal( |server| async move { app.handle_connection(server).await }, - encoded, + codec, + payloads, capacity, ) - .await?; - decode_frames_with_codec(codec, raw) + .await } diff --git a/wireframe_testing/src/helpers/codec_ext.rs b/wireframe_testing/src/helpers/codec_ext.rs index ce91634f..c6ca27b8 100644 --- a/wireframe_testing/src/helpers/codec_ext.rs +++ b/wireframe_testing/src/helpers/codec_ext.rs @@ -24,7 +24,8 @@ use wireframe::codec::FrameCodec; /// use wireframe_testing::encode_payloads_with_codec; /// /// let codec = HotlineFrameCodec::new(4096); -/// let frames = encode_payloads_with_codec(&codec, vec![vec![1, 2, 3]]).unwrap(); +/// let frames = encode_payloads_with_codec(&codec, vec![vec![1, 2, 3]]) +/// .expect("encoding should succeed for a valid payload"); /// assert!(!frames.is_empty()); /// ``` pub fn encode_payloads_with_codec( @@ -51,13 +52,16 @@ pub fn encode_payloads_with_codec( /// Decode raw wire bytes into frames using `codec`. /// /// The byte vector is fed into the codec's decoder until no more complete -/// frames can be extracted. Trailing bytes that do not form a complete frame -/// are silently ignored (matching the behaviour of a connection that is shut -/// down after the last response). +/// frames can be extracted. After the main decode loop, `decode_eof` is called +/// to flush any partial frame remaining in the buffer. If the buffer still +/// contains unconsumed bytes after `decode_eof` returns `None`, an error is +/// returned — this catches truncation bugs where the server emits incomplete +/// trailing data. /// /// # Errors /// -/// Returns an error if the decoder encounters malformed data. +/// Returns an error if the decoder encounters malformed data or if trailing +/// bytes remain that do not form a complete frame. /// /// ```rust /// use wireframe::codec::examples::HotlineFrameCodec; @@ -65,11 +69,12 @@ pub fn encode_payloads_with_codec( /// /// let codec = HotlineFrameCodec::new(4096); /// let wire: Vec = encode_payloads_with_codec(&codec, vec![vec![42]]) -/// .unwrap() +/// .expect("encoding should succeed") /// .into_iter() /// .flatten() /// .collect(); -/// let frames = decode_frames_with_codec(&codec, wire).unwrap(); +/// let frames = decode_frames_with_codec(&codec, wire) +/// .expect("decoding should succeed for well-formed wire bytes"); /// assert_eq!(frames.len(), 1); /// ``` pub fn decode_frames_with_codec( @@ -87,6 +92,27 @@ pub fn decode_frames_with_codec( })? { frames.push(frame); } + + // Flush any partial frame remaining at stream end. + while let Some(frame) = decoder.decode_eof(&mut buf).map_err(|error| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("codec decode_eof failed: {error}"), + ) + })? { + frames.push(frame); + } + + if !buf.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "trailing {} byte(s) after decoding — possible truncated frame", + buf.len() + ), + )); + } + Ok(frames) } From 7fe6408cd88f9a6bd4f704b209b8c8404ad9a3e4 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 26 Feb 2026 16:41:04 +0000 Subject: [PATCH 3/3] docs(users-guide): fix code examples for drive_with_codec usage Updated the example in the users-guide to correctly use drive_with_codec_payloads and drive_with_codec_frames functions with proper payload initialization and app instantiation order. This improves clarity and accuracy of the documentation for codec-aware driver functions. Co-authored-by: devboxerhub[bot] --- docs/users-guide.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/users-guide.md b/docs/users-guide.md index ddd2dd9c..ab27c355 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -227,17 +227,22 @@ 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 app = WireframeApp::new()?.with_codec(codec.clone()); +let payload: Vec = vec![0x01, 0x02, 0x03]; // Payload-level: returns decoded response payloads as byte vectors. -let payloads = drive_with_codec_payloads(app, &codec, vec![serialized_envelope]).await?; +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 frames = drive_with_codec_frames(app, &codec, vec![serialized_envelope]).await?; +let app = WireframeApp::new()?.with_codec(codec.clone()); +let frames = + drive_with_codec_frames(app, &codec, vec![payload]).await?; ``` Available codec-aware driver functions: